介绍
在很多时候为了能够对目标进程空间数据进行修改,或者使用目标进程的名称来执行自己的代码,实现危害用户的操作,通常是将一个 DLL
文件或者 ShellCode
注入到目标进程中去执行。这里分享四种常用的注入技术,其中使用 DLL
注入的方法最为普遍。
全局钩子注入
在Windows中大部份的应用程序都是基于消息机制的,他们都有一个消息过程函数,根据消息完成不同的功能。Windows操作系统提供的钩子机制就是用来截获和监视这些消息的。按照钩子的范围不同,它们又可以分为局部钩子和全局钩子,局部钩子是针对某个线程的;而全局钩子则是作用于整个系统的基于消息的应用。全局钩子需要使用 DLL
文件,在 DLL
中实现相应的钩子函数。
-
关键函数安装钩子程序
SetWindowsHookEx()
WINUSERAPIHHOOKWINAPI SetWindowsHookExA( _In_ int idHook, // 要安装的钩子的类型例如键盘 鼠标 对话框等 _In_ HOOKPROC lpfn, // 一个指向钩子程序的指针 _In_opt_ HINSTANCE hmod,// 包含lpfn参数指向的钩子过程的DLL句柄 _In_ DWORD dwThreadId); // 与钩子程序相关联的线程标识符
-
卸载钩子函数
UnhookWindowsHookEx
BOOL UnhookWindowsHookEx( HHOOK hhk);
-
钩子回调函数
// 表示将当前的钩子传递给钩子链中的下一个钩子LRESULTWINAPI CallNextHookEx( _In_opt_ HHOOK hhk, _In_ int nCode, _In_ WPARAM wParam, _In_ LPARAM lParam);
远程线程注入
远程线程注入是指一个进程在另一个进程中创建线程的技术,是一种经典的注入技术
-
函数
OpenProcess()
——打开目标进程
HANDLEWINAPI OpenProcess( _In_ DWORD dwDesiredAccess, // 访问进程对象 _In_ BOOL bInheritHandle, // 若此值为true,则此进程创建的进程将继承该句柄 _In_ DWORD dwProcessId // 要打开的本地进程的PID );// 返回值:若成功返回句柄,失败返回NULL
-
函数
VirtualAllocEx()
指定进程的虚拟地址空间内保留、提交或者更改内存的状态
LPVOIDWINAPIVirtualAllocEx( _In_ HANDLE hProcess, // 进程句柄 _In_opt_ LPVOID lpAddress, // 指定要分配页面所需的起始指针,为NULL自动分配 _In_ SIZE_T dwSize, // 要分配内存的大小 _In_ DWORD flAllocationType, // 内存分配的类型:保留、提交和更改 _In_ DWORD flProtect // 页面区域的内存保护 );// 返回值:函数成功返回分配的基址,失败返回NULL
-
函数
WriteProcessMemory()
——在指定的进程中将数据写入内存区域
BOOLWINAPI WriteProcessMemory( _In_ HANDLE hProcess, // 要修改的进程句柄 _In_ LPVOID lpBaseAddress, // 指向指定进程中写入数据的基地址指针 _In_reads_bytes_(nSize) LPCVOID lpBuffer, // 指向缓冲区的指针 _In_ SIZE_T nSize, // 要写入指定进程的字节数 _Out_opt_ SIZE_T* lpNumberOfBytesWritten // 指向变量的指针,该变量接收传输到指定进程的字节数 );// 返回值:函数成功 != 0;失败返回0 // 注意:写入区域的内存要可访问,否则操作失败
-
函数
CreateRemoteThread()
——实现注入的核心函数在另一个进程的虚拟地址中创建运行的线程
HANDLEWINAPI CreateRemoteThread( _In_ HANDLE hProcess, // 要创建线程的进程的句柄 _In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes, // 指向安全描述符的指针 _In_ SIZE_T dwStackSize, // 堆栈的初始大小,若为0则新线程使用可执行文件的默认大小 _In_ LPTHREAD_START_ROUTINE lpStartAddress, // 指向由线程执行类型为LPTHREAD_START_ROUTINE的应用程序定义的函数指针,并表示远程进程 中线程的起始地址,该函数必须存在于远程进程中 _In_opt_ LPVOID lpParameter, // 要传递给线程函数的变量的指针 _In_ DWORD dwCreationFlags, // 控制线程创建的标志 _Out_opt_ LPDWORD lpThreadId // 接收线程标志符变量的指针 );// 返回值:成功 新线程的句柄,失败:返回NULL
从以上这些函数的作用我们实现的原理就很清晰了,先在指定进程申请一段地址然后将准备好的 shellcode
或者一个 DLL
文件写入到这块内存空间中。
注意:对于一些系统服务这样通常会注入失败,由于系统存在SESSION 0隔离的安全机制,需调用一个更加底层的 ZwCreateThreadEx
()
来实现。
APC队列注入
APC ( AsynchronusProcedureCall
) 为异步过程调用,是指函数在特定线程中被异步执行。在Windows系统中, APC
是一种并发机制,用于异步IO或者定时器。
每一个线程都有自己的 APC 队列,使用 QueueUserAPC
函数把一个 APC 函数压入 APC 队列中。当处于用户模式的 APC 压入线程 APC 队列后,该线程并不直接调用 APC 函数,除非该线程处于可通知状态,调用的顺序为先入先出。
-
函数
WINBASEAPIDWORD WINAPI QueueUserAPC( _In_ PAPCFUNC pfnAPC, // 指向APC函数的指针 _In_ HANDLE hThread, // 线程句柄 _In_ ULONG_PTR dwData // 由pfnAPC参数指向的APC函数的单个值 );// 返回值 成功非0;失败返回0
APC
的注入原理是利用当线程被唤醒时 APC
中的注册函数会执行的机制,并以此去执行 DLL
加载代码,进而完成 DLL
注入。为了增加成功率,可以向目标进程中的所有线程都插入 A
PC
。
自定义HOOK
-
自定义HOOK大致可以分为两类
-
inlinehook
-
IATHOOK
-
inlinehook 是一种通过修改机器码的方式来实现HOOK的技术
原理:对于一个正常的程序如下图,通过 CALL
指令来调用函数。关于 CALL
指令相当于 push
当前函数地址和 jmp
要执行的指令位置,即 push0171B7B3
jmp0171B430
,这是我们正常执行 00.0171B430
这个函数的样子。
我们在hook的时候就是将CALL指令直接改成 jmp
指令,跳到我们自己编写的函数的位置,执行完成之后跳回函数原来指令的下一条指令 0171B7B3
,需要注意的是跳转偏移要多计算5个字节
计算公式: 跳转偏移 = 目标地址 – jmp
所在的地址 – 5
-
实现方法
-
获取函数的实际地址
-
修改内存分页属性
-
计算跳转偏移,修改目标地址,还原内存属性
-
获取实际地址返回
void OnHook() { //获取函数实际地址 HMODULE Module = GetModuleHandleA("kernel32.dll"); LPVOID func = GetProcAddress(Module, "OpenProcess"); //保存5个字节 memcpy(g_oldCode, func, 5); //修改内存分页属性,由于代码段是不可写的,所有必须先将它的属性变成可写 DWORD dwProtect; VirtualProtect(func, 5, PAGE_EXECUTE_READWRITE, &dwProtect); //计算跳转偏移 *(DWORD*)&g_newCode[1] = (DWORD)MyOpenProcess - (DWORD)func - 5; //修改目标地址 memcpy(func, g_newCode, 5); //还原内存分页属性 VirtualProtect(func, 5, dwProtect, &dwProtect); };
-
用户层的
IATHook
是通过替换IAT
表中函数的原始地址从而实现的Hook
与普通的 InlineHook
不一样, IATHook
需要充分理解PE文件的结构才能完成,关于相对虚拟地址( RVA
)、文件偏移地址( FOA
)和加载基址等概念可以自行查阅相关资料。
-
实现方法
//获取指定dll导出地址表的中函数地址 DWORD * GetIatAddress(const char * dllName, const char* funName) { // 1. 获取加载基址并转换成DOS头 auto DosHeader = (PIMAGE_DOS_HEADER)GetModuleHandle(NULL); // 2. 通过 DOS 头的后一个字段 e_lfanew 找到 NT 头的偏移 auto NtHeader = (PIMAGE_NT_HEADERS)(DosHeader->e_lfanew + (DWORD)DosHeader); // 3. 在数据目录表下标为[1]的地方找到导入表的RVA DWORD ImpRVA = NtHeader->OptionalHeader.DataDirectory[1].VirtualAddress; // 4. 获取到导入表结构体,因为程序已经运行了,所以不需要转FOA auto ImpTable = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)DosHeader + ImpRVA); // 遍历导入表,以一组全0的结构结尾 while (ImpTable->Name) { // 获取当前导入表结构描述的结构体的名称 CHAR* Name = (CHAR*)(ImpTable->Name + (DWORD)DosHeader); // 忽略大小写进行比较,查看是否是需要的导入表结构 if (!_stricmp(Name, dllName)) { // 找到对应的 INT 表以及 IAT 表 DWORD* IntTable = (DWORD*)((DWORD)DosHeader + ImpTable->OriginalFirstThunk); DWORD* IatTable = (DWORD*)((DWORD)DosHeader + ImpTable->FirstThunk); // 遍历所有的函数名称,包括有/没有名称 for (int i = 0; IntTable[i] != 0; ++i) { // 比对函数是否存在函数名称表中 if ((IntTable[i] & 0x80000000) == 0) { // 获取到导入名称结构 auto Name = (PIMAGE_IMPORT_BY_NAME)((DWORD)DosHeader + IntTable[i]); // 比对函数的名称 if (!strcmp(funName, Name->Name)) { // 返回函数在IAT中保存的地址 return &IatTable[i]; } } } } ImpTable++; } return 0; }
总结
钩子技术总结起来就是通过各种手段来修改代码或者地址从而让程序来执行我们自己编写的代码,在分析恶意程序时关注一下这些敏感的 API
函数组合,在查看程序基本信息的时候就可以大致做出猜测。下一篇继续分享常见的启动和隐藏技术,继续刨析病毒的实现原理。
来源:freebuf.com 2020-06-15 15:27:38 by: 雷石安全实验室
请登录后发表评论
注册