恶意代码分析之注入技术
在很多时候为了能够对目标进程空间数据进行修改,或者使用目标进程的名称来执行自己的代码,实现危害用户的操作,通常是将一个DLL
文件或者ShellCode
注入到目标进程中去执行。这里分享四种常用的注入技术,其中使用DLL
注入的方法最为普遍。
全局钩子注入
在Windows中大部份的应用程序都是基于消息机制的,他们都有一个消息过程函数,根据消息完成不同的功能。Windows操作系统提供的钩子机制就是用来截获和监视这些消息的。按照钩子的范围不同,它们又可以分为局部钩子和全局钩子,局部钩子是针对某个线程的;而全局钩子则是作用于整个系统的基于消息的应用。全局钩子需要使用DLL
文件,在DLL
中实现相应的钩子函数。
- 关键函数安装钩子程序
SetWindowsHookEx()
WINUSERAPI
HHOOK
WINAPI SetWindowsHookExA(
_In_ int idHook, // 要安装的钩子的类型例如键盘 鼠标 对话框等
_In_ HOOKPROC lpfn, // 一个指向钩子程序的指针
_In_opt_ HINSTANCE hmod,// 包含lpfn参数指向的钩子过程的DLL句柄
_In_ DWORD dwThreadId); // 与钩子程序相关联的线程标识符
成功返回DLL
句柄,失败返回NULL
- 卸载钩子函数
UnhookWindowsHookEx
BOOL UnhookWindowsHookEx(
HHOOK hhk
);
- 钩子回调函数
// 表示将当前的钩子传递给钩子链中的下一个钩子
LRESULT
WINAPI CallNextHookEx(
_In_opt_ HHOOK hhk,
_In_ int nCode,
_In_ WPARAM wParam,
_In_ LPARAM lParam);
远程线程注入
远程线程注入是指一个进程在另一个进程中创建线程的技术,是一种经典的注入技术
- 函数
OpenProcess()
——打开目标进程
HANDLE
WINAPI OpenProcess(
_In_ DWORD dwDesiredAccess, // 访问进程对象
_In_ BOOL bInheritHandle, // 若此值为true,则此进程创建的进程将继承该句柄
_In_ DWORD dwProcessId // 要打开的本地进程的PID
);
// 返回值: 若成功返回句柄,失败返回NULL
- 函数
VirtualAllocEx()
指定进程的虚拟地址空间内保留、提交或者更改内存的状态
LPVOID
WINAPI
VirtualAllocEx(
_In_ HANDLE hProcess, // 进程句柄
_In_opt_ LPVOID lpAddress, // 指定要分配页面所需的起始指针,为NULL自动分配
_In_ SIZE_T dwSize, // 要分配内存的大小
_In_ DWORD flAllocationType, // 内存分配的类型:保留、提交和更改
_In_ DWORD flProtect // 页面区域的内存保护
);
// 返回值:函数成功返回分配的基址,失败返回NULL
- 函数
WriteProcessMemory()
——在指定的进程中将数据写入内存区域
BOOL
WINAPI 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()
——实现注入的核心函数在另一个进程的虚拟地址中创建运行的线程
HANDLE
WINAPI 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
(Asynchronus Procedure Call
)为异步过程调用,是指函数在特定线程中被异步执行。在Windows系统中,APC
是一种并发机制,用于异步IO或者定时器。
每一个线程都有自己的APC
队列,使用QueueUserAPC
函数把一个APC
函数压入APC
队列中。当处于用户模式的APC
压入线程APC
队列后,该线程并不直接调用APC
函数,除非该线程处于可通知状态,调用的顺序为先入先出。
- 函数
WINBASEAPI
DWORD WINAPI QueueUserAPC(
_In_ PAPCFUNC pfnAPC, // 指向APC函数的指针
_In_ HANDLE hThread, // 线程句柄
_In_ ULONG_PTR dwData // 由pfnAPC参数指向的APC函数的单个值
);
// 返回值 成功非0; 失败返回0
APC
的注入原理是利用当线程被唤醒时APC
中的注册函数会执行的机制,并以此去执行DLL
加载代码,进而完成DLL
注入。为了增加成功率,可以向目标进程中的所有线程都插入APC
。
自定义HOOK
- 自定义HOOK大致可以分为两类
inlineHOOK
IATHOOK
inlineHook
是一种通过修改机器码的方式来实现HOOK的技术
原理:对于一个正常的程序如下图,通过CALL
指令来调用函数。关于CALL
指令相当于push
当前函数地址和jmp
要执行的指令位置,即 push 0171B7B3
jmp 0171B430
,这是我们正常执行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
函数组合,在查看程序基本信息的时候就可以大致做出猜测。下一篇继续分享常见的启动和隐藏技术,继续剖析病毒的实现原理。