回顾

中间有一周没有看这本书了, 前面的知识有些遗忘, 所以在这里先把前面的东西缕一缕(copy)

调试器钩取API

  • 使用DebugActiveProcess()建立起调试器被调试者的关系
  • 通过DLL共享虚拟内存地址相同的特性获取到目标API的起始地址
  • 修改该API的起始第一个字节为0xCC, 即INT3断点指令机器码
  • 我们在调用该API时, 就会遇上这个断点指令从而将控制权交给了调试器
  • 在调试器中通过ReadProcessMemory()和WriteProcessMemory()实现调试器和被调试者之间的数据交互, 从而达到修改数据的操作
  • 然后脱钩, 即将0xCC改回原来的内容(通过全局变量来实现), 并将EIP减一回到断点前, 如果不脱勾的话, 当控制流从调试器转回被调试者时又会遇到INT3指令再次回到调试器钩取, 陷入死循环.
  • 再根据需要决定是否要继续挂钩

通过DLL修改IAT钩取API

还是要重复: DLL注入之所以能过如此灵活的使用于多个场景, 重要的不是在DllMain中做了什么操作, 而是在于它可以让我们可以轻易进入一个目标的虚拟空间, 并对其进行读写.

  • 首先时Inject程序将我们的DLL注入到目标进程中
  • DllMain获取了我们目标API的地址(便于后面的IAT替换和恢复), 同时也轻易得到了我们的钩取函数MyFun()的地址
  • 随后进入的IAT修改函数通过进程句柄的转换得到了进程的基址, 然后通过对PE头的结构体信息利用找到了IDT(导入表, 描述的是导入的全部DLL), 然后通过DLL的名称遍历筛选找到了目标API所在的DLL
  • 同样通过结构体IID的成员信息找到了对应的IAT, 通过函数名称遍历筛选找到了我们的目标API在IAT表中的对应的ITD结构体, 并修改其内容: 将其中描述函数起始地址的成员修改为我们的钩取函数MyFun()的地址
  • 当我们决定脱钩的时候仍然调用的是IAT修改函数, 经过同样的过程将原来的API地址恢复到IAT中

33.1 技术图表

Untitled

我们所用到的技术仍然是DLL注入, 其作用在上面的回顾和上一章(好像)都提到过

33.2 API代码修改技术的原理

跟上一章的修改IAT技术实现钩取有相似之处, 所以理解起来会相对的简单, 但是也要注意区分二者的区别

  • IAT通过修改IAT值来实现API钩取
  • API代码修改技术则是通过将API前5个字符修改为JMP XXXXX来钩取API(这又有点像是调试钩取API技术了, 只不过调试是只修改一个字节为0xCC), 从而将控制转移给钩取函数.

33.2.1 钩取之前

正常调用API

Untitled

钩取后

Untitled

这里看起来可能有点绕, 我们分步分析

  • 00422CF7调用了48C69C地址上的内容作为调用地址, 48C69C的内容是7C93D92E, 也就是API
  • 控制流来到了7C93D92E, 而API的开头内容就是JMP 10001120, 所以我们不会执行API的内容, 而是跳至目标地址
  • 跳转至10001120 这个地址就是我们的Hooking函数, 在这里我们先使用脱钩函数(这里的目的跟前面调试钩取的脱钩思路是一样的)
  • 在Hooking函数中我们先调用unhook()脱钩, 因为后面还要调用到目标函数, 如果不脱勾, API开始的内容又是JMP变成了类似无穷递归的操作(这跟上面调试脱钩是一样的道理)
  • 然后在对数据进行了一系列我们想要的操作以后, 我们调用原来的API实现其功能
  • 随后调用hook()重新挂钩
  • 最后我们使用show参数取消挂钩, 然后在DllMain中使用另一个标签直接脱钩.

33.3 进程隐藏

进程隐藏最常用的ntdll.ZwQuerySystemInformation()API钩取技术

33.3.1 进程隐藏工作原理

想要隐藏进程首先想到的方法是让目标进程隐身, 但就像是我们现实生活中一样, 隐身是不可能实现的. 所以我们用相反的方法实现隐藏进程, 那就是让其他的进程都变成瞎子, 所有的进程都看不见我们的目标进程, 那它也就相当于隐身了.

33.3.2 相关API

进程间要检测其存在, 需要调用相关API, 这种API相当于其他进程的”眼睛”

CreateToolhelp32Snapshot() & EnumProcess()

//作用是得到系统进程快照的句柄
HANDLE WINAPI CreateToolhelp32Snapshot(
		DWORD    dwFlags,             //指定获取系统进程快照的类型
		DWORD    th32ProcessID        //指定要获取进程快照的ID,  获取系统内所有进程快照时是0
);

//作用是检索系统中每个进程对象的进程标识符
BOOL EnumProcesses(
		DWORD*   pProcessIds,        //指向接收进程标识符列表的数组的指针
		DWORD    cb,                 //pProcesslds数组的大小, 以字节为单位
		DWORD*   pBytesReturned      //pProcesslds数组中返回的字节数
)

上面的两个APi均在内部调用了ntdll.ZwQuerySystemInformation()API

ntdll.ZwQuerySystemInformation()

//检索指定的系统信息
NTSTATUS ZwQuerySystemInformation(
		SYSTEM_INFORMATION_CLASS    SystemInformationClass,    //要检索的系统信息的类型
		PVOID    SystemInformation,                            //指向接收请求信息的缓冲区的指针
		ULONG    SystemInformationLength,                      //描述上面参数所指向的缓冲区的大小, 以字节为单位
		PULONG   ReturnLength                                  //指向函数写入请求信息的实际大小的位置的可选指针
)

ZwQuerySystemInformation()所获取的是进程信息的链表, 所以我们要做的就是从除目标函数意外的所有函数中钩取ZwQuerySystemInformation(), 在获取的链表中删去关于目标函数的节点. 这样我们刺瞎了其他进程的眼睛(当然, 有点不恰当, 因为其他进程只是看不见目标进程, 其他的进程仍然可以看到)

33.3.3 隐藏技术的问题

问题一: 要钩取的进程个数

能看到我们目标进程不只有检索进程的那些查找工具, 任何函数都可以调用ZwQuerySystemInformation()来实现查看进程

问题二: 新创建的进程

如果用户再运行一个任务管理器, 如果没有被钩取的话, 我们隐藏进程的目的仍然没有达到.

解决方法: 全局钩取

为了解决上面的问题, 我们隐藏test.exe进程时需要钩取系统中运行的所有进程的ZwQuerySystemInformation()API, 并且对后面将要启动运行的所有进程进行相同的操作. 这就是全局钩取的概念.

33.4 练习 #1 (HideProc.exe, stealth.dll)

  • HideProc.exe负责将stealth.dll文件注入到所有运行中的进程.
  • stealth.dll负责钩取进程的ntdll.ZwQuerySystemInformation()

33.4.1 运行notepad.exe, procexp.exe, taskmgr.exe

首先将HideProc.exe跟stealth.dll放入同一个文件夹

Untitled

随后打开notepad.exe文件

Untitled

使用procxp查看进程

Untitled

此时procxp是可以检测到notepad的

然后我们开始钩取API

Untitled

再次使用procxp查看进程

Untitled

此时procxp不能检测到notepad

我们查看有哪些进程是被注入了stealth.dll的

Untitled

可以看到我们用来检索notepad的procxp被注入了stealth.dll, 所以才没有检索到notepad进程

接下来我们停止隐藏进程

Untitled

再次使用procxo查看进程

Untitled

此时notepad再一次出现了

33.5 源代码分析

33.5.1 HideProc.cpp

HideProc.exe负责向运行中的所有进程注入/卸载指定DLL文件, 它再原有InjectDll.exe的基础上添加了向所有进程注入DLL的功能, 可以认为是InjectDll.exe程序的加强版

#define _CRT_SECURE_NO_WARNINGS
#include <Windows.h>
#include <tlhelp32.h>

BOOL InjectAllProcess(int nMode, LPCTSTR szDllPath)
{
	DWORD              dwPID = 0;//进程PID
	HANDLE             hSnapShot = INVALID_HANDLE_VALUE;//初始化系统快照句柄
	PROCESSENTRY32     pe;
	/*
	* 作用是描述快照中的进程列表中的条目结构体
	typedef struct tagPROCESSENTRY32 {
  DWORD     dwSize;                 //该结构的大小
  DWORD     cntUsage;				//弃用, 始终为0
  DWORD     th32ProcessID;			//该进程的PID
  ULONG_PTR th32DefaultHeapID;		//弃用, 始终为0
  DWORD     th32ModuleID;			//弃用, 始终为0
  DWORD     cntThreads;				//进程启动的执行线程数
  DWORD     th32ParentProcessID;	//父进程PID, 即PPID
  LONG      pcPriClassBase;			//此进程创建的任何线程的基本优先级
  DWORD     dwFlags;				//弃用, 始终为0
  CHAR      szExeFile[MAX_PATH];	//进程的可执行文件的名称
} PROCESSENTRY32;
	*/
	//获取系统快照
	pe.dwSize = sizeof(PROCESSENTRY32);//获取描述进程的结构体的内存大小, 并给dwSize成员, 防止报错
	hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPALL, NULL);//获取系统快照

	//查找进程
	Process32First(hSnapShot, &pe);//首先获取第一个进程信息结构体
	do
	{
		dwPID = pe.th32ProcessID;//从进程信息结构体中获取对应进程的PID

		//鉴于系统安全性的考虑, 对于PID小于100的系统进程不执行DLL注入操作
		if (dwPID < 100)
			continue;

		if (nMode == INJECTION_MODE)//根据这个传入的参数来决定是注入还是卸载
			InjectDll(dwPID, szDllPath);//注入DLL
		else
			EjectDll(dwPID, szDllPath);//卸载DLL
	} while (Process32Next(hSnapShot, &pe));//获取下一个进程信息结构体

	CloseHandle(hSnapShot);//关闭系统快照句柄

	return TRUE;//返回值为真
}

这个函数是对全部(除了特殊的进程)进程注入或者是卸载stealth.dll

我们来到下一个源码分析

33.5.2 stealth.cpp

实际的API钩取操作由Stealth.dll文件负责

SetProcName()导出函数

// global variable (in sharing memory)
#pragma comment (linker, "/SECTION:.SHARE,RWS")//当第一个参数为linker表示链接器选项, 
///SECTION表示更改节的属性, RWS表示Read可读, W表示Write可写, S表示Shared共享 
#pragma data_seg(".SHARE")
    TCHAR g_szProcName[MAX_PATH] = { 0 };//建立g_szProcName缓冲区
#pragma data_seg()

//export function 导出函数
#ifdef __cplusplus//__cplusplus表示这是第一段C++代码, 这样的意义是: 如果这是一段cpp代码, 就加入extern"C"{和}处理其中的代码
extern "C" {//这个的作用是告诉链接器, 这是一个用C语言写的代码, 要用C语言的方式去链接它, 而前提条件是上面的"如果这是一段C++代码"
#endif
__declspec(dllexport) void SetProcName(LPCTSTR szProcName)//导出函数, 作用是将隐藏的进程名称保存再g_szProcName数组中
{                     //这个函数在HideProc()中被调用执行
    _tcscpy_s(g_szProcName, szProcName);
}
#ifdef __cplusplus
}
#endif

下面我们看DllMain函数

DllMain

BOOL APIENTRY DllMain( HMODULE hModule, DWORD  ul_reason_for_call, LPVOID lpReserved)
{

    char     szCurProc[MAX_PATH] = { 0, };
    char     * p = NULL;

    //异常处理: 若当前进程为HideProc.exe,  则终止, 不进行钩取操作
    GetModuleFileNameA(NULL, szCurProc, MAX_PATH);//查找当前进程的文件名
    p = strrchr(szCurProc, '\\');//获取路径中文件名的前一个地址
    if ((p != NULL) && !_stricmp(p + 1, "HideProc.exe"))//用前一个地址来找到文件名并跟HideProc作比较
        return TRUE;//如果是HideProc就返回为真直接退出, 不对HideProc进程钩取API
    switch (ul_reason_for_call)
    {
        //API钩取
    case DLL_PROCESS_ATTACH:
        hook_by_code(DEF_NTDLL, DEF_ZWQUERYSYSTEMINFORMATION, 
                     (PROC)NewZwQuerySystemInformation, g_pOrgBytes);
        break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
        //API脱钩
    case DLL_PROCESS_DETACH:
        unhook_by_code(DEF_NTDLL, DEF_ZWQUERYSYSTEMINFORMATION, g_pOrgBytes);
        
        break;
    }
    return TRUE;
}

可以看到在进入了DllMain后还会通过进程文件名来对进程进行一次筛选, 如果是HideProc.exe注入程序本身就会跳过注入.

ATTCH附加则调用hook_by_code实现API钩取, DETACH脱离则调用unhook_by_code()实现API脱钩

下面是对两个函数进行解析

hook_by_code()

//           目标API所在DLL的名称指针 目标API的名称指针   钩取函数的地址  存储原有代码的内存的缓冲区指针
BOOL hook_by_code(LPCSTR szDllName, LPCSTR szFuncName, PROC pfnNew, PBYTE pOrgBytes)
{
    FARPROC    pfnOrg;//用于存储目标API的地址
    DWORD      dwOldProtect, dwAddress;
    byte       pBuf[5] = { 0xE9, 0, };
    PBYTE      pByte;

    //获取要钩取的API地址
    pfnOrg = (FARPROC)GetProcAddress(GetModuleHandleA(szDllName),//通过名称调用两个Getxxx函数获取API的地址
                                      szFuncName);
    pByte = (PBYTE)pfnOrg;//转换类型: 从一个WINAPI类型的指针转换成了字节指针, 方便后面对内存进行操作

    //若已经被钩取, 则返回FALSE
    if (pByte[0] = 0xE9)//E9就是JMP的字节码, 正常的API哪会上来就跳转, 所以通过首地址的内容判断是否已被钩取
        return FALSE;

    //为了修改5个字节, 先向内存添加"写"属性
    VirtualProtect((LPVOID)pfnOrg, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect);//改变内存保护属性(权限), 这里是将其设为可读可写, 同时将旧保护属性存储在dwOldProtect结构体中

    //备份原有代码(5字节)
    memcpy(pOrgBytes, pfnOrg, 5);

    //计算JMP地址(E9 XXXXXXXX)
    dwAddress = (DWORD)pfnNew - (DWORD)pfnOrg - 5;//这里的跳转字节码用的是相对地址, 其值为 = 函数地址跟下条指令之间的距离, 所以还需要减5表示去掉指令本身的长度
    memcpy(&pBuf[1], &dwAddress, 4);//添加上这四个字节的内容pBuf就是一条完整的跳转指令了

    //钩子: 修改5个字节
    memcpy(pfnOrg, pBuf, 5);//将我们的jmp字节码修改到API的开头

    //恢复内存属性
    VirtualProtect((LPVOID)pfnOrg, 5, dwOldProtect, &dwOldProtect);//参三一个选项, 只是接下来的造作方式

    return TRUE;
}

大致思路

  • 获取目标API首地址
  • 查看是否已经注入
  • 修改内存属性
  • 计算JMP地址
  • 挂钩(将JMP修改到API首地址)
  • 恢复内存属性

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);//通过参一参二两个字符串指针得到了API首地址
    pByte = (PBYTE)pFunc;//转换类型: 从一个WINAPI类型的指针转换成了字节指针, 方便后面对内存进行操作

    if (pByte[0] != 0xE9)//如果开头不是JMP指令, 说明还没有挂钩, 所以不用脱钩直接退出
        return FALSE;

    //向内存添加"写"属性, 为恢复代码做准备
    VirtualProtect((LPVOID)pFunc, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect);

    //脱钩
    memcpy(pFunc, pOrgBytes, 5);

    //恢复内存属性
    VirtualProtect((LPVOID)pFunc, 5, dwOldProtect, &dwOldProtect);

    return TRUE;
}

unhook_by_code()跟hook_by_code几乎是一模一样

  • 获取目标API首地址
  • 查看是否有注入
  • 修改内存属性
  • 脱钩
  • 恢复内存属性

NewZwQuerySystemInformation()

最后, 分析钩取函数NewZwQuerySystemInformation(), 在这之前再次看看ntdll.ZwQuerySystemInformation()API

NTSTATUS WINAPI ZwQuerySystemInformation(
	__in        SYSTEM_INFORMATION_CLASS SystemInformationClass,//要检索的系统信息的类型
	__inout     PVOID SystemInformation,//指向接收请求信息的缓冲区的指针
	__in        ULONG SystemInformationLength,//描述上面参数所指向的缓冲区的大小, 以字节为单位
	__out_opt   PULONG ReturnLength//指向函数写入请求信息的实际大小的位置的可选指针
);

//系统进程信息结构体
typedef struct _SYSTEM_PROCESS_INFORMATION {
		ULONG NextEntryOffset;//当前节点的地址加上NextEntryOffset就是下一个节点的起始地址
		ULONG NumberOfThreads;//描述该进程中的线程数
		byte Reserved1[48];//保留
		PVOID Reserved2[3];//保留, 类型为指针, 根据下面的源代码1号元素存有进程名字符串指针
		HANDLE UniqueProcessId;//PID
		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;

下面是我们的NewZwQuerySystemInformation()

NTSTATUS WINAPI NewZwQuerySystemInformation(
    SYSTEM_INFORMATION_CLASS SystemInformationClass,
    PVOID SystemInformation//进程信息单项链表
    ULONG SystemInformationLength,//表示的就是unsigned long
    PULONG ReturnLength//表示是unsigned long的指针
)
{
    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)//通过函数起始地址调用原来的API
        (SystemInformationClass, SystemInformation,
            SystemInformationLength, ReturnLength);//返回值: NTSTATUS成功或者错误代码

    if (status != STATUS_SUCCESS)//如果调用该API失败, 则直接跳转到结尾
        goto __NTQUERYSYSTEMINFORMATION_END;

    //仅针对SystemProcessInformation类型操作
    if (SystemInformationClass == SystemProcessInformation)//确定获取的系统信息是进程信息
    {
        //通过类型转换获取到pCur单项链表的头
        pCur = (PSYSTEM_PROCESS_INFORMATION)SystemInformation;

        while (TRUE)
        {
            if (pCur->Reserved2[1] != NULL)//Reserved2[1]存有进程名字符串指针
            {   //g_szProcName是目标进程名的字符串指针, 这里是根据名称比较来确定是否是需要隐藏的进程节点
                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);//获取链表的下一项
        }
    }
__NTQUERYSYSTEMINFORMATION_END:
    //函数终止前, 再次执行API钩取操作,为下一次调用做准备
    hook_by_code(DEF_NTDLL, DEF_ZWQUERYSYSTEMINFORMATION,
        (PROC)NewZwQuerySystemInformation, g_pOrgBytes);

    return status;
}

大致流程:

  • 首先脱钩, 为了后面能正常使用函数, 而不是无限递归
  • 然后通过Loadxxx获取到了隐藏API的地址
  • 通过地址正常调用了API
  • 获取进程信息链表
  • 通过名称字符串对比找到了隐藏进程所在节点, 并删去, 这时我们的隐藏进程对于当前进程来说就是不存在的了
  • 重新挂钩

我之前对于ZwQuerySystemInformation()的理解有点误区, 那就是我认为进程信息的链表是全局的, 也就是说只用需改一次就可以了, 但是实际上并不是, ZwQuerySystemInformation()的工作原理是获取当前的进程信息组成局部链表, 然后给其他的API使用, 如果下一次没有过去的话, ZwQuerySystemInformation()又会重新获取新的进程信息链表, 这时该链表中又有我们想要隐藏的进程信息了, 所以我们每次调用ZwQuerySystemInformation()时都需要重复钩取.

33.6 全局API钩取

全局钩取的对象有两个:

  • 当前运行的所有进程
  • 将来要运行的所有进程

我们前面只完成了第一个目标, 接下来我们将在此基础上完成第二个目标.

33.6.1 Kernel32.CreateProcess()API

Kernel32.CreateProcess()API用来创建新进程.其他创建进程的API内部都调用了这个API

BOOL WINAPI CreateProcess(
	__in_opt        LPCTSTR lpApplicationName,//要执行的模块的完整的路径名和文件名
	__inout_opt     LPTSTR lpCommandLine,//要执行的命令行
	__in_opt        LPSECURITY_ATTRIBUTES lpProcessAttributes,//指向**进程**的安全描述符结构体的指针
	__in_opt        LPSECURITY_ATTRIBUTES lpThreadAttributes,//指向**线程**的安全描述符结构体的指针
	__in            BOOL bInheritHandles,//为TRUE则进程中的每个可继承句柄都有子进程继承, 为FALSE则相反
	__in            DWORD dwCreationFlags,//控制优先级和进程创建的标志
	__in_opt        LPVOID lpEnvironment,//指向新进程的环境快指针
	__in_opt        LPCTSTR lpCurrentDirectory,//进程当前目录的完整路径
	__in            LPSTARTUPINFO lpStartupInfo,//指向STARTUPINFOA或STARTUPINFOEXA结构体, 这两个结构体都是描述新进程的窗口, 桌面, 标准句柄和属性的
	__out           LPPROCESS_INFORMATION lpProcessInformation//指向PROCESS_INFORMATION结构体指针, 该结构体接收有关新进程的标识信息
);

因此, 向当前运行的所有进程注入stealth.dll后, 如果在stealth.dll中将CreateProcess()API也一起钩取, 那么以后运行的进程也会自动注入stealth.dll文件.

但是要考虑以下方面:

  • 钩取CreateProcess()API时, 还要分别钩取CreateProcessA(), CreateProcessW()这两个API
  • CreateProcessA(), CreateProcessW()中又分别调用了CreateProcessInternalA(), CreateProcessInternalW()这两个函数.
  • 钩取函数(NewCreateProcess)要钩取子进程的API. 因此, 极短时间内, 子进程可能在未钩取的状态下运行.

对于上面的问题, 有一个一次性解决的方法, 那就是钩取比CreateProcess()更低级的API

33.6.2 Ntdll.ZwResumeThread()API

ZwResumeThread(
		IN     HANDLE     ThreadHandle,
		OUT    PULONG     SuspendCount OPTIONAL
)

ZwResumeThread()是在进程创建后, 主线程运行前被调用执行. 所以只用钩取这个函数, 即可在不运行子进程代码的状态下钩取API.

在读书的时候读到这里会有点晕, 首先对于子进程, 我们的目标是钩取子进程的API, 而钩取API所要做的事就是注入DLL, 而我们想要达成注入DLL到子进程中所要做的事是在父进程中钩取创建子进程的API, 然后在钩取函数中调用Loadxxx. 这样捋下来就是: 我们要在父进程中额外钩取API, 这一个钩取函数的行为是在子进程中再一次注入DLL钩取API, 同时子进程中也会发生同样的操作.

33.7 练习#2 (HideProc2.exe, Stealth2.dll)

33.7.1 复制stealth2.dll文件到%SYSTEM%文件夹中

为了把stealth2.dll文件注入所有运行进程, 首先要把stealth2.dll文件复制到%SYSTEM%文件夹

33.7.2 运行HideProc2.exe -hide

Untitled

33.7.3 运行ProcExp.exe

这时再运行ProcExp看是否是全局钩取

Untitled

可以看到即使是钩取后的进程也依然被钩取了

33.7.4 运行HideProc2.exe -show

Untitled

在取消钩取后又可以看到notepad了

33.8 源代码分析

33.8.1 HideProc2.cpp

和HideProc.cpp相比, HideProc2.cpp只是减少了参数个数, 内容跟HideProc差不多, 所以可以参照上面的内容

33.8.2 stealth2.cpp

DllMain.cpp

BOOL APIENTRY DllMain( HMODULE hModule, DWORD  ul_reason_for_call, LPVOID lpReserved)
{
    char szCurProc[MAX_PATH] = { 0, };
    char* p = NULL;
    
    //异常处理使注入不会发生在HideProc2.exe进程
    GetModuleFileNameA(NULL, szCurProc, MAX_PATH);//获取当前进程的文件路径(这里已经开始钩取了, 对每个进程注入DLL是HideProc的功能)
    p = strrchr(szCurProc, '\\');
    if ((p != NULL) && !_stricmp(p + 1, "HideProc2.exe"))
        return TRUE;

    //改变privilege
    SetPrivilege(SE_DEBUG_NAME, TURE);

    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        //钩取
        hook_by_code("kernel32.dll", "CreateProcessA", (PROC)NewCreateProcessA, g_pOrgCPA);//对创建进程的API进行钩取

        hook_by_code("kernel32.dll", "CreateProcessW", (PROC)NewCreateProcessW, g_pOrgCPW);//对创建进程的API进行钩取

        hook_by_code("ntdll.dll", "ZwQuerySystemInformation", (PROC)NewZwoQuerySystemInformation, g_pOrgZwQSI);//对检索进程的API进行钩取
        
        break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        //脱钩
        unhook_by_code("kernel32.dll", "CreateProcessA", g_pOrgCPA);

        unhook_by_code("kernel32.dll", "CreateProcessW", g_pOrgCPW);

        unhook_by_code("ntdll.dll", "ZwQuerySystemInformation", g_pOrgZwQSI);

        break;
    }
    return TRUE;
}

NewCreateProcessA()

针对创建进程API的钩取函数的源代码

BOOL CreateProcessA(
    LPCSTR                lpApplicationName,
    LPSTR                 lpCommandLine,
    LPSECURITY_ATTRIBUTES lpProcessAttributes,
    LPSECURITY_ATTRIBUTES lpThreadAttributes,
    BOOL                  bInheritHandles,
    DWORD                 dwCreationFlags,
    LPVOID                lpEnvironment,
    LPCSTR                lpCurrentDirectory,
    LPSTARTUPINFOA        lpStartupInfo,
    LPPROCESS_INFORMATION lpProcessInformation
) 
{
    BOOL bRet;
    FARPROC pFunc;

    //脱钩(针对当前进程)
    unhook_by_code("kernel32.dll", "CreateProcessA", g_pOrgCPA);

    //获取原函数的指针
    pFunc = GetProcAddress(GetModuleHandleA("kernel32.dll"), "CreateProcessA");
    //通过指针调用了该函数(已经脱钩),  生成子进程, 若成功则返回一个非零值, 失败则相反
    bRet = ((PFCREATEPROCESSA)pFunc)(
        lpApplicationName,
        lpCommandLine,
        lpProcessAttributes,
        lpThreadAttributes,
        bInheritHandles,
        dwCreationFlags,
        lpEnvironment,
        lpCurrentDirectory,
        lpStartupInfo,
        lpProcessInformation);//这个结构体负责存储子进程的信息, 在下面的作用是获取子进程PID

    //向生成的子进程注入stealth2.dll(针对子进程)
    if (bRet)//如果成功创建子进程
        InjectDll2(lpProcessInformation->hProcess, Str_MODULE_NAME);//参一的结构体赋值是在上面的函数调用中实现的, 这个结构体是最后一个参数

    //钩取(针对当前进程)
    hook_by_code("kernel32.dll", "CreateProcessA", (PROC)NewCreateProcessA, g_pOrgCPA);

    return bRet;
}

大致思路:

  • 在当前进程中对创建进程API脱钩, 防止无限递归
  • 然后调用脱钩后的原API创建子进程
  • 通过PID对该子进程进程DLL注入, 钩取了子进程的检索进程函数创建进程函数
  • 在当前进程中对创建进程挂钩

33.9 利用”热补丁”技术钩取API

33.9.1 API代码修改技术

由于线程问题, 在对API进行写的操作的同时, 运行代码会引发错误, 所以需要跟安全的API钩取技术.

33.9.2 “热补丁”(修改7个字节代码)

我们观察API的结构

Untitled

可以发现又7个字节的空余, 这些专门用于热补丁的

思路(二次跳转)

  • 前面5个字节用于JMP
  • 后面2个字节用于JMP SHORT, 跳转的目的就是前面的5字节JMP

跟代码修改JMP之间的区别在于, 这个API是可以正常运行原有功能的, 因为下面的指令都没有被破坏, 然后如果想要正常调用不需要再脱钩挂钩, 直接起始地址 + 2 就可调用了.

33.11 源代码分析

stealth3.cpp

hook_by_hotpatch()

BOOL hook_by_hotpatch(LPCSTR szDllName, LPCSTR szFuncName, PROC pfnNew)
{
    FARPROC pFunc;
    DWORD dwOldProtect, dwAddress;
    BYTE pBuf[5] = { 0xE9 , 0 };//第二次跳转, 调转到hook函数
    byte pBuf2[2] = { 0xEB, 0xF9 };//第一次跳转的字节码, 因为相对地址是固定的, 所以可以直接确定机器码
    PBYTE pByte;

    //获取目标API的首地址
    pFunc = (FARPROC)GetProcAddress(GetModuleHandleA(szDllName), szFuncName);
    //类型转换
    pByte = (PBYTE)pFunc;
    //检查是否已经挂钩, 如果挂钩内存内容就是0xEB短跳转
    if (pByte[0] == 0xEB)
        return FALSE;
    //改变那7个字节的保护属性
    VirtualProtect((LPVOID)((DWORD)pFunc - 5), 7, PAGE_EXECUTE_READWRITE, &dwOldProtect);

    //改变上面的五个字节
    dwAddress = (DWORD)pfnNew - (DWORD)pFunc;//计算出要跳转的偏移, 这里不用减5是因为跳转后的下一条指令就是函数的开头
    memcpy(&pBuf[1], &dwAddress, 4);//将地址赋给数组
    memcpy((LPVOID)((DWORD)pFunc - 5), pBuf, 5);//将数组赋值给内存

    memcpy(pFunc, pBuf2, 2);//将API开头两个字节修改

    VirtualProtect((LPVOID)((DWORD)pFunc - 5), 7, dwOldProtect, &dwOldProtect);

    return TRUE;
}

跟之前的钩取函数差不多, 只是修改了7个字节

unhook_by_hotpatch()

BOOL unhook_by_hotpatch(LPCSTR szDllName, LPCSTR szFuncName)
{
    FARPROC pFunc;
    DWORD dwOldProtect;
    PBYTE pByte;
    byte pBuf[5] = { 0x90, 0x90, 0x90, 0x90, 0x90 };//恢复的API开头之前的5个NOP
    byte pBuf2[2] = { 0x8b, 0xFF };//恢复的API开头两个字节
    //获取需要恢复的API地址
    pFunc = (FARPROC)GetProcAddress(GetModuleHandleA(szDllName), szFuncName);
    //类型转换
    pByte = (PBYTE)pFunc;
    //判断是否挂钩
    if (pByte[0] != 0xEB)
        return FALSE;
    //修改内存保护属性
    VirtualProtect((LPVOID)pFunc, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect);

    //恢复前五个字节
    memcpy((LPVOID)((DWORD)pFunc - 5), pBuf, 5);

    //恢复MOV EDI, EDI
    memcpy(pFunc, pBuf2, 2);

    VirtualProtect((LPVOID)pFunc, 5, dwOldProtect, &dwOldProtect);

    return TRUE;
}

修改后的NewCreateProcessA()

BOOL CreateProcessA(
    LPCSTR                lpApplicationName,
    LPSTR                 lpCommandLine,
    LPSECURITY_ATTRIBUTES lpProcessAttributes,
    LPSECURITY_ATTRIBUTES lpThreadAttributes,
    BOOL                  bInheritHandles,
    DWORD                 dwCreationFlags,
    LPVOID                lpEnvironment,
    LPCSTR                lpCurrentDirectory,
    LPSTARTUPINFOA        lpStartupInfo,
    LPPROCESS_INFORMATION lpProcessInformation
)
{
    BOOL bRet;
    FARPROC pFunc;

    //获取原函数的指针
    pFunc = GetProcAddress(GetModuleHandleA("kernel32.dll"), "CreateProcessA");
    //原API的地址现在设置为: 起始地址 + 2
    pFunc = (FARPROC)((DWORD)pFunc + 2);
    //通过指针调用了该函数(已经脱钩),  生成子进程, 若成功则返回一个非零值, 失败则相反
    bRet = ((PFCREATEPROCESSA)pFunc)(
        lpApplicationName,
        lpCommandLine,
        lpProcessAttributes,
        lpThreadAttributes,
        bInheritHandles,
        dwCreationFlags,
        lpEnvironment,
        lpCurrentDirectory,
        lpStartupInfo,
        lpProcessInformation);//这个结构体负责存储子进程的信息, 在下面的作用是获取子进程PID

    //向生成的子进程注入stealth2.dll(针对子进程)
    if (bRet)//如果成功创建子进程
        InjectDll2(lpProcessInformation->hProcess, Str_MODULE_NAME);//参一的结构体赋值是在上面的函数调用中实现的, 这个结构体是最后一个参数

    return bRet;
}

跟之前的HOOK函数相比

  • 省去了hook和unhook
  • 多出了一个 起始地址 + 2的操作

注意使用热补丁之前要看看API是否支持热补丁