恶意代码分析技巧13-HOOK与注入

  • A+

HOOK和注入技术经常被恶意代码使用。利用HOOK和注入技术,恶意代码提高了执行隐蔽性,增加了恶意代码分析难度,在某些情况下还能实现权限提升和内存常驻。

1.挂钩(HOOK)

挂钩(HOOK)就是在来往信息间安装“钩子”,钩取来往信息。
在用户层,常见的Hook技术有:

①消息钩子

②Inline Hook

③IAT Hook

1.1.消息钩子

1.1.1.消息钩取的原理

用户对应用程序的各种操作(eg:键盘输入、点击、移动等)都会产生事件;发生事件时,操作系统会把该事件对应的消息发送给相应的应用程序,应用程序分析收到的信息后执行相应的动作。以键盘输入为例,Windows的消息流大体上是如下三个步骤:

①发生键盘输入事件,WM_KEYDOWN消息被添加到系统消息队列;

②系统判断是哪个应用程序中发生了事件,然后从系统消息队列中取出消息,添加到相应的应用程序的消息队列中;

③程序监视自身的消息队列,发现新添加的WM_KEYDOWN消息后,调用相应的事件(消息)处理程序进行处理。

消息钩子就安装在“系统将消息发送给应用程序”的过程中。在挂钩的过程中,可以同时设置多个相同的消息钩子,按照设置的顺序依次调用这些钩子,它们组成的链条被称为钩链。

1.1.2.SetWindowsHookEx

SetWindowsHookEx是winuser.h中提供的专门用于消息钩取的API

language
HHOOK WINAPI SetWindowsHookExA(
int idHook, //钩取的消息的类型
HOOKPROC lpfn, //指向钩子过程的指针,即回调函数地址
HINSTANCE hmod, //包含lpfn过程的dll实例句柄
DWORD dwThreadId //线程ID
);

返回值hhk:成功,则返回值就是该钩子的句柄;失败,则返回值为NULL(0),可使用GetLastError函数。
参数idHook表示可以钩取的消息的类型,它可以是一下选项之一(超链接:https://www.kanxue.com/chm.htm?id=10237&pid=node1000841):

图片 1.png
参数lpfn是钩子回调函数的地址,它具有如下形式:
language
LRESULT CALLBACK HookProc(
int nCode,//钩子代码,表示如何处理消息;
//wParam和lParam表示消息,根据消息的类型不同具有不同的含义;
WPARAM wParam,
LPARAM lParam
){
// process event
...
return CallNextHookEx(NULL, nCode, wParam, lParam);
}

参数dwThreadId表示要挂钩的线程ID,如果是0,则表示安装全局钩子,他会影响到运行中的所有的进程。
在钩子回调函数中必须使用CallNextHookEx将消息向后传递,否则消息将阻塞在钩子中,无法传递给应用程序:
language
LRESULT WINAPI CallNextHookEx(
_In_opt_ HHOOK hhk,//钩子的句柄
_In_ int nCode,
_In_ WPARAM wParam,
_In_ LPARAM lParam);

与SetWindowsHookEx对应的是卸载钩子的API——UnhiikWindowsHookEx:
language
BOOL WINAPI UnhookWindowsHookEx(
_In_ HHOOK hhk //SetWindowsHookEx的返回值
);

使用SetWindowsHookEx挂钩的核心代码:
language
HMODULE h = LoadLibraryA(hook_dll_path);
HOOKPROC f = (HOOKPROC)GetProcAddress(h, "GetMsgProc"); // 获取钩子函数地址,只要GetMessage或PeekMessage函数从应用程序消息队列中检索到消息,系统就会调用此函数。
SetWindowsHookExA(WH_GETMESSAGE, f, h, thread_id); //给线程安装钩子
PostThreadMessage(thread_id, WM_NULL, NULL, NULL); // 触发钩子

1.2.SetWinEventHook

SetWinEventHook和SetWindowsHookEx类似,也可以在其他进程中进行挂钩。但两者不同的地方在于,SetWindowsHookEx能截取WM_ 开头的消息,例如鼠标键盘消息等;而 SetWinEventHook 截取是以EVENT_ 开头的事件,这些事件是跟对象的状态相关的,虽然我们也可以把事件看作一种消息,但是当我们收到一个事件发生的消息后,并不能控制拦截该事件不再传递事件。

SetWinEventHook函数原型:
language
HWINEVENTHOOK WINAPI SetWinEventHook(
// SetWinEventHook的第1,2个参数可以标识一个范围,表示截获哪个范围类的事件,EVENT_MIN-EVENT_MAX:0x00000001 - 0x7FFFFFFF
__in UINT eventMin,//指定钩子函数处理的事件范围中最低事件值的事件常量(超链接:https://docs.microsoft.com/zh-cn/windows/win32/winauto/event-constants)。
__in UINT eventMax,//指定钩子函数处理的事件范围中的最高事件值的事件常量。
__in HMODULE hmodWinEventProc,//如果在dwFlags参数中指定了WINEVENT_INCONTEXT标志,则指向包含lpfnWinEventProc中的钩子函数的DLL;若指定了WINEVENT_OUTOFCONTEXT标志,则此参数为NULL。
__in WINEVENTPROC lpfnWinEventProc, //指向事件挂钩函数的指针。
__in DWORD idProcess,//指定钩子函数从中接收事件的进程的ID。指定零(0)以从当前桌面上的所有进程接收事件。
__in DWORD idThread,//指定钩子函数从中接收事件的线程的ID。如果此参数为零,则钩子函数与当前桌面上的所有现有线程相关联。
__in UINT dwflags//标记值,指定挂钩函数的位置和要跳过的事件的位置。
);

WINEVENTPROC回调函数原型:
language
typedef void (CALLBACK *WINEVENTPROC)(
HWINEVENTHOOK hWinEventHook,//SetWinEventHook返回值,钩子函数句柄
DWORD event,//指定发生的事件(事件常量)
HWND hwnd,//生成事件的窗口,如果没有窗口与事件关联,则NULL;
LONG idObject,//对象标识符,表示窗口某个部分
LONG idChild,//如果此值为CHILDID_SELF,则事件由对象触发; 如果此值是子ID,则该事件由子元素触发。
DWORD dwEventThread,//标识生成事件的线程或拥有当前窗口的线程
DWORD dwmsEventTime//指定生成事件的时间
);

使用UnhookWinEvent卸载钩子:
BOOL WINAPI UnhookWinEvent(
__in HWINEVENTHOOK hWinEventHook);
使用SetWinEventHook挂钩的代码可以参考微软提供的demo(超链接:https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwineventhook)。
```language
// Global variable.
HWINEVENTHOOK g_hook;
// Initializes COM and sets up the event hook.
void InitializeMSAA(){
CoInitialize(NULL);
g_hook = SetWinEventHook(
EVENT_SYSTEM_MENUSTART, EVENT_SYSTEM_MENUEND, // Range of events (4 to 5).
NULL, // Handle to DLL.
HandleWinEvent, // The callback.
0, 0, // Process and thread IDs of interest (0 = all)
WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS); // Flags.
}
// Unhooks the event and shuts down COM.
void ShutdownMSAA(){
UnhookWinEvent(g_hook);
CoUninitialize();
}
// Callback function that handles events.
void CALLBACK HandleWinEvent(HWINEVENTHOOK hook, DWORD event, HWND hwnd,
LONG idObject, LONG idChild,
DWORD dwEventThread, DWORD dwmsEventTime){
IAccessible* pAcc = NULL;
VARIANT varChild;
HRESULT hr = AccessibleObjectFromEvent(hwnd, idObject, idChild, &pAcc, &varChild);
if ((hr == S_OK) && (pAcc != NULL)){
BSTR bstrName;
pAcc->get_accName(varChild, &bstrName);
if (event == EVENT_SYSTEM_MENUSTART){
printf("Begin: ");
}
else if (event == EVENT_SYSTEM_MENUEND){
printf("End: ");
}
printf("%Sn", bstrName);
SysFreeString(bstrName);
pAcc->Release();
}
}

```

1.3.IAT Hook

IAT Hook的原理说起来很简单,就是通过修改IAT中保存的地址来钩取某个API。

图片 2.png
如图(摘自《恶意代码分析实战》)所示,在正常的情况下,程序执行应该走上面的流程,程序调用TerminateProcess,在IAT中找到TerminateProcess的函数地址,转到该地址处执行函数;但是,经过IAT Hook后,程序走了下面的流程,程序调用TerminateProcess,在IAT中找到的是被修改的函数地址,但是程序并不知道这一点,而是直接跑到被修改的地址处执行了。

使用IAT Hook的程序,一般会读取PE文件头信息,解析PE结构,查找IAT。关于定位IAT的具体实现,网上有很多分析,请自行上网搜索。

1.4.Inline Hook

Inline Hook俗称内联Hook。IAT Hook只是简单地修改了API指针,而Inline Hook是直接修改API代码。
Inline Hook根据修改的字节数不同可以分为5字节修改技术和7字节修改技术(7字节修改又被称为“热补丁”Hot Patch)。

1.4.1.5字节修改

Inline Hook将API的前5个字节修改为JMP XXX指令(该指令占五个字节)来钩取API,控制程序执行流程。

图片 3.png
如上图所示,我们修改了0x76001920-0x76001925处的5个字节,但是却影响0x76001920-0x76001927处的7个字节。5字节修改技术,只修改了5个字节,但是对API产生的破坏却可能不止5个字节。使用该技术,一定要在控制程序流程后修复API。

1.4.2.7字节修改

另一种方式是修改7个字节的技术。该技术一般用于Hook具有如下特征的API:
①API代码以“MOV EDI,EDI(0x8BFF)”指令开始;
②API代码上方有至少5个字节的空余;

图片 4.png
这些API具有这种特征是微软故意这么设计的,微软这么设计API是为了方便打热补丁。MOV EDI,EDI指令2个字节,该指令是没有意义的,我们可以将它修改成2字节的短跳转,跳转到API代码上方的空余处,在这空余处我们就有足够的空间控制程序流程了。

图片 5.png
我们虽然修改了7个字节,但是对API却没有产生破坏。

2.DLL劫持

在前一篇文章中,我们已经简单的介绍了DLL劫持,介绍的是DLL搜索路径劫持。接下来我们介绍一下其他的DLL劫持技术。

2.1.AppInit_DLLs——注入

注册表中有一个AppInit_DLLs键,位于HKLMSoftwareMicrosoftWindows NTCurrentVersionWindows或HKLMSoftwareWow6432NodeMicrosoft Windows NTCurrentVersionWindows下。

图片 6.png
在User32.dll的DLL_PROCESS_ATTACH过程中使用LoadLibrary()函数加载AppInit DLL。几乎每一个应用程序都会加载User32.dll,因此,几乎每一个应用程序都会加载AppInit DLL。

图片 7.png

图片 8.png
攻击者可以在AppInit_DLLs中添加恶意DLL的绝对路径,并把LoadAppInit_DLLs的值该为1,这样就实现了DLL注入,使得所有加载USER32.dll的进程全部加载恶意DLL。Ginwui病毒(md5:23c75249b1e30e332cdcb65c7aace588)就曾使用该技术进行dll注入:

图片 9.png
关于AppInit_DLLs的使用,还有两点需要注意:
①AppInit_DLLs值的类型为“REG_SZ”。此值必须指定以空格或逗号分隔的以NULL结尾的DLL字符串。由于空格用作分隔符,不可以使用长文件名。
②从Windows 8开始,启用安全启动时将禁用AppInit_DLLs。在未来的系统版本中,微软可能完全禁用AppInit_DLLs。

2.2.AppCert DLL——DLL注入

如果有进程使用了CreateProcess、CreateProcessAsUser、CreateProcessWithLoginW、CreateProcessWithTokenW或WinExec
函数,那么此进程会获取HKEY_LOCAL_MACHINESystemCurrentControlSetControlSessionManagerAppCertDlls注册表项,此项下的dll都会被加载到此进程。
此技术的实现逻辑是CreateProcess、CreateProcessAsUser、CreateProcessWithLoginW、CreateProcessWithTokenW或WinExec
函数在创建进程的时候其内部会调用BasepIsProcessAllowed函数,而BasepIsProcessAllowed则会打开AppCertDlls注册表项,将此项下的dll都加载到进程中。

图片 10.png
前文我们提到了CreateProcess过程的七个阶段,BasepIsProcessAllowed的过程发生在阶段2——创建文件映像和兼容性检查之间。
值得注意的是win xp-win 10 默认不存在这个注册表项,为了利用该技术需要自行创建AppCertDlls项。

3.跨进程写内存

3.1.VirtualAlloc和WriteProcessMemory

这两个API是最经典也是最常见的用户跨进程写内存。
VirtualAllocEx向指定进程申请一段内存空间,WriteProcessMemory向指定进程内存空间写数据:
language
LPVOID WINAPI VirtualAllocEx(
__in HANDLE hProcess, //需要在其中分配空间的进程的句柄.
__in_opt LPVOID lpAddress, //想要获取的地址区域..
__in SIZE_T dwSize, //要分配的内存大小.
__in DWORD flAllocationType, //内存分配的类型
__in DWORD flProtect //内存页保护.
);
BOOL WriteProcessMemory(
HANDLE hProcess, // 进程的句柄
LPVOID lpBaseAddress, // 要写入的起始地址
LPVOID lpBuffer, // 写入的缓存区
DWORD nSize, // 要写入缓存区的大小
LPDWORD lpNumberOfBytesWritten // 是返回实际写入的字节。
);

使用这两个API跨进程写内存只需要三步:打开进程——申请内存——写内存;核心代码:
language
//打开进程
HANDLE h = OpenProcess(PROCESS_VM_WRITE | PROCESS_VM_OPERATION, FALSE,process_id);
//申请内存
LPVOID target_payload = VirtualAllocEx(h, NULL, sizeof(payload), MEM_COMMIT |MEM_RESERVE, PAGE_EXECUTE_READWRITE);
//写内存
WriteProcessMemory(h, target_payload, payload, sizeof(payload), NULL);

3.2.Atom Bombing

请在阅读完本文 第5节—APC注入 后再阅读本小节。
Atom Bombing技术攻击的是原子表(atom table),需要结合APC注入技术才能实现。利用Atom Bombing,我们可以实现向目标进程的任意地址写入任意数据。

3.2.1.原子表和原子函数

原子表(atom table),是存储字符串和对应标识符的系统定义的表。应用程序将一个字符串放在一个原子表中,并接收一个16bit整数,该整数可以用于访问该字符串。在原子表中,整数被称为原子(atom),字符串被称为原子名(atom name)。

原子表分为本地原子表和全局原子表。本地原子表只可以被应用程序自身使用,不可以被其他应用程序使用;全局原子表可以被所有应用程序使用。windows系统提供了许多原子表,每个原子表都有不同的用途。例如,DDE程序使用全局原子表与其他应用程序共享项目名称和主题名称字符串;DDE程序不用传递实际的字符串,而是传递全局原子给其他进程,其他进程使用原子提取原子表中的字符串。

原子名字符串是以null终止的字符串。该字符串的最大大小为255个字节。
关于对原子的操作,有一组专门的API函数,本文主要使用的两个API是原子操作的函数有GlobalAddAtom(添加全局原子)和GlobalGetAtomNameA(取字符串):
language
ATOM WINAPI GlobalAddAtom(//向全局原子表添加一个字符串,并返回这个字符串的原子
_In_ LPCTSTR lpString//原子名字符串
);
UINT GlobalGetAtomNameA(//从表中获取全局原子名字符串,存储在缓冲区中
ATOM nAtom,//原子
LPSTR lpBuffer,//缓冲区
int nSize//缓冲区大小
);

3.2.2.Atom Bombing注入

根据前文内容,我们可以使用GlobalAddAtom创建一个原子,写入不含null的字符串,然后只要让目标进程调用GlobalGetAtomNameA就可以向目标进程任意地址写入任意代码了。

我们可以使用APC中的NtQueueApcThread函数另目标进程调用GlobalGetAtomNameA。使用NtQueueApcThread而不是QueueUserAPC是因为 GlobalGetAtomNameA有三个参数,NtQueueApcThread能传递三个参数而QueueUserAPC只能传递一个参数。

AtomBombing注入的核心代码:
language
HANDLE th = OpenThread(THREAD_SET_CONTEXT | THREAD_QUERY_INFORMATION, FALSE,thread_id);
for (char* pos = payload; pos < (payload + sizeof(payload)); pos += strlen(pos) + 1){
ATOM a = GlobalAddAtomA(pos);// 添加全局原子
DWORD64 offset = pos - payload;
ntdll!NtQueueApcThread(th, GlobalGetAtomNameA, (PVOID)a,(PVOID)(((DWORD64)target_payload) + offset), (PVOID)(strlen(pos) + 1));// 向目标逐字节写入payload
}

3.3.利用共享内存写入payload

3.3.1.共享内存

Windows中,当多个进程之间需要使用同样的数据的时候最好使用共享内存。顾名思义,所谓共享内存,就是说多个进程共同使用同一内存空间。但不同的进程都有不同的内存空间,共享内存是如何实现的呢?

图片 11.png

如图(https://blog.csdn.net/u013052326/article/details/76359588)所示,进程A和进程B有一个共享内存“hello”。在逻辑上,A和B有两个相互独立的内存存储“hello”;但是在实际内存中,他们使用的是同一个内存空间,当A修改了“hello”,B的“hello”也就被修改了。

在Windows下,创建操作共享内存的API主要有CreateFileMapping、MapViewOfFile、OpenFileMapping、FlushViewOfFile、UnmapViewOfFile等,这些API的函数原型:
language
//创建共享内存CreateFileMapping
HANDLE CreateFileMapping(
HANDLE hFile, //物理文件句柄
LPSECURITY_ATTRIBUTES lpAttributes, //安全设置
DWORD flProtect, //保护设置
DWORD dwMaximumSizeHigh, //高位文件大小
DWORD dwMaximumSizeLow, //低位文件大小
LPCTSTR lpName //共享内存名称
);
//将共享内存映射到进程的地址空间MapViewOfFile
LPVOID MapViewOfFile(
HANDLE hFileMappingObject,//为CreateFileMapping()返回的文件映像对象句柄
DWORD dwDesiredAccess, //映射对象的文件数据的访问方式
DWORD dwFileOffsetHigh,//表示文件映射起始偏移的高32位
DWORD dwFileOffsetLow,//表示文件映射起始偏移的低32位.(64KB对齐不是必须的)
DWORD dwNumberOfBytesToMap//指定映射文件的字节数.
);//MapViewOfFileEx多了一个lpBaseAddress参数,允许我们指定一个基本地址来进行映射。
//打开一个命名的共享内存
HANDLE OpenFileMappingW(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
LPCWSTR lpName //共享内存
);
//将对文件映射(共享内存)的修改写入磁盘文件中
BOOL FlushViewOfFile(
LPCVOID lpBaseAddress,//指向要刷新到映射文件的磁盘表示形式的字节范围的基地址的指针
SIZE_T dwNumberOfBytesToFlush //字节数
);
//停止指针到共享内存的映射
BOOL UnmapViewOfFile(
LPCVOID lpBaseAddress
);

在使用CreateFileMapping创建共享内存时,可以指定共享内存的名称。

3.3.2.利用目标进程共享内存进行注入

使用共享内存的在目标进程中注入的方式很简单,调用OpenFileMapping、MapViewOfFile打开共享内存就行了。难点在于:一、不是所有的目标进程都有共享内存,不是所有的共享内存都有足够的空间写入payload;二、我们在共享内存中写入payload后,如何定位payload在目标进程中的地址?
解决第一个问题的关键是选择合适的进程,在PowerLoader中展示了explorer.exe中的多个合适的共享内存:

"BaseNamedObjectsShimSharedMemory"
"BaseNamedObjectswindows_shell_global_counters"
"BaseNamedObjectsMSCTF.Shared.SFM.MIH"
"BaseNamedObjectsMSCTF.Shared.SFM.AMF"
"BaseNamedObjectsUrlZonesSM_Administrator"
"BaseNamedObjectsUrlZonesSM_SYSTEM"

解决第二个问题,可以在写入payload后,在目标进程内存中遍历搜索payload数据,从而定位payload地址。

综上,使用共享内存注入的基本步骤是:

①在目标进程找一个合适的共享内存,这个共享内存可以通过名称进行访问,并且具有足够的内存;

②打开该共享内存,写入payload(建议写在节的尾部);

③在目标进程定位payload地址,以待下一步利用;

核心代码:
language
//打开共享内存
HANDLE hm = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, section_name);
BYTE* buf = (BYTE*)MapViewOfFile(hm, FILE_MAP_ALL_ACCESS, 0, 0, section_size);
//写入payload
memcpy(buf + section_size - sizeof(payload), payload, sizeof(payload));
//打开目标进程
HANDLE h = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, process_id);char* read_buf = new char[sizeof(payload)];SIZE_T region_size;
//在目标进程中遍历搜索payload地址
for (DWORD64 address = 0; address < 0x00007fffffff0000ull; address += region_size)
{
MEMORY_BASIC_INFORMATION mem;
SIZE_T buffer_size = VirtualQueryEx(h, (LPCVOID)address, &mem,sizeof(mem));//查询地址空间中内存地址的信息
if ((mem.Type == MEM_MAPPED) && (mem.State == MEM_COMMIT) && (mem.Protect== PAGE_READWRITE) && (mem.RegionSize == section_size))
{
ReadProcessMemory(h, (LPCVOID)(address + section_sizesizeof(payload)), read_buf, sizeof(payload), NULL);
if (memcmp(read_buf, payload, sizeof(payload)) == 0)
{
// the payload is at address + section_size - sizeof(payload);

break;
}
}
region_size = mem.RegionSize;
}

3.3.3.创建共享内存进行注入

上一节我们使用的是目标进程的共享内存,主动打开目标进程的共享内存,将payload加入的共享内存。这一次我们反其道而行之,我们主动创建一个共享内存,然后强制让目标进行加载这个共享内存。实现这一技术的核心是NtMapViewOfSection,NtMapViewOfSection可以让指定的进程在指定的地址(还未分配的地址)加载指定的共享内存:
language
NTSYSAPI NTSTATUS NtMapViewOfSection(
HANDLE SectionHandle,
HANDLE ProcessHandle,//目标进程
PVOID *BaseAddress,//加载地址
ULONG_PTR ZeroBits,
SIZE_T CommitSize,
PLARGE_INTEGER SectionOffset,
PSIZE_T ViewSize,
SECTION_INHERIT InheritDisposition,
ULONG AllocationType,
ULONG Win32Protect
);

该技术的实现步骤:
①使用CreateFileMapping/NtCreateSection将创建一个共享内存;
②调用MapViewOfFile 映射到进程内存;
③将payload数据复制到分区的映射内存共享内存中;
④NtMapViewOfSection强制让目标进行加载这个共享内存(payload);
核心代码:
language
HANDLE fm = CreateFileMappingA(INVALID_HANDLE_VALUE, NULL, PAGE_EXECUTE_READWRITE,0, sizeof(payload), NULL);
LPVOID map_addr = MapViewOfFile(fm, FILE_MAP_ALL_ACCESS, 0, 0, 0);
HANDLE p = OpenProcess(PROCESS_VM_WRITE | PROCESS_VM_OPERATION, FALSE,
process_id);
memcpy(map_addr, payload, sizeof(payload));
LPVOID requested_target_payload = 0;
SIZE_T view_size = 0;
ntdll!NtMapViewOfSection(fm, p, &requested_target_payload, 0, sizeof(payload),NULL, &view_size, ViewUnmap, 0, PAGE_EXECUTE_READWRITE);
target_payload = requested_target_payload;

4.远程线程注入

远程线程注入是最经典最常见的注入技术,它是一个进程使用CreateRemoteThread在另一个进程中创建线程的技术。
CreateRemoteThread的函数原型
language
HANDLE WINAPI CreateRemoteThread(
__in HANDLE hProcess, //要创建线程的进程句柄
__in LPSECURITY_ATTRIBUTES lpThreadAttributes,//指向新线程的SECURITY_ATTRIBUTES结构体
__in SIZE_T dwStackSize,//堆栈的初始大小(字节),0表示默认大小
__in LPTHREAD_START_ROUTINE lpStartAddress,//远程线程的起始地址
__in LPVOID lpParameter,//远程线程的参数
__in DWORD dwCreationFlags,//远程线程创建的标志,0表示创建后立即运行
__out LPDWORD lpThreadId //指向接收线程标识符的变量,NULL表示不返回线程标识符
);

除了CreateRemoteThread,我们还可以使用RtlCreateUserThread、NtCreateThreadEx、ZwCreateThreadEx创建远程线程,这三个API均未公开,但是用法和CreateRemoteThread类似:
```language
typedef NTSTATUS(NTAPI * pfnRtlCreateUserThread)(
IN HANDLE ProcessHandle,
IN PSECURITY_DESCRIPTOR SecurityDescriptor OPTIONAL,
IN BOOLEAN CreateSuspended,
IN ULONG StackZeroBits OPTIONAL,
IN SIZE_T StackReserve OPTIONAL,
IN SIZE_T StackCommit OPTIONAL,
IN PTHREAD_START_ROUTINE StartAddress,
IN PVOID Parameter OPTIONAL,
OUT PHANDLE ThreadHandle OPTIONAL,
OUT PCLIENT_ID ClientId OPTIONAL);
typedef NTSTATUS(NTAPI pfnNtCreateThreadEx)
(
OUT PHANDLE hThread,
IN ACCESS_MASK DesiredAccess,
IN PVOID ObjectAttributes,
IN HANDLE ProcessHandle,
IN PVOID lpStartAddress,
IN PVOID lpParameter,
IN ULONG Flags,
IN SIZE_T StackZeroBits,
IN SIZE_T SizeOfStackCommit,
IN SIZE_T SizeOfStackReserve,
OUT PVOID lpBytesBuffer);
typedef NTSTATUS(NTAPI
_ZwCreateThreadEx)(
OUT PHANDLE ThreadHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN HANDLE ProcessHandle,
IN PTHREAD_START_ROUTINE StartRoutine,
IN PVOID StartContext,
IN ULONG CreateThreadFlags,
IN SIZE_T ZeroBits OPTIONAL,
IN SIZE_T StackSize OPTIONAL,
IN SIZE_T MaximumStackSize OPTIONAL,
IN PPROC_THREAD_ATTRIBUTE_LIST AttributeList
);

这几个创建线程的API都有一个指向ThreadProc回调函数的指针:language

DWORD WINAPI ThreadProc(
In LPVOID lpParameter);
```

远程线程注入的基本步骤:
①使用OpenProcess打开注入进程
②使用VirtualAlloc向注入进程申请内存(根据实际情况既要为将要注入的代码申请空间,也要为远程线程的参数申请空间)
③使用WriteProcessMemory向申请的内存中写入数据
④调用CreateRemoteThread或RtlCreateUserThread或NtCreateThreadEx或ZwCreateThreadEx创建远程线程。
核心代码:
language
// #1. 使用PID获取目标进程句柄
hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE,dwPID)
// #2. 在目标进程中分配线程参数需要的内存
pRemoteBuf[0] = VirtualAllocEx(hProcess,NULL, dwSize,MEM_COMMIT,PAGE_READWRITE)
// #3. 将参数写入分配的内存.
WriteProcessMemory(hProcess,pRemoteBuf[0],(LPVOID)&param, dwSize, NULL)
// #4. 在目标进程中分配远程线程代码需要的内存
pRemoteBuf[1] = VirtualAllocEx(hProcess, NULL,dwSize,MEM_COMMIT,PAGE_EXECUTE_READWRITE)
// #5. 将线程代码写入分配的内存
WriteProcessMemory(hProcess,pRemoteBuf[1],(LPVOID)payload,dwSize, NULL)
// #6. 启动远程线程
hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pRemoteBuf[1],pRemoteBuf[0],0,NULL)

在上面的demo中,我们注入的是任意payload,如果我想将payload设置为LoadLibrary(“any_dll”),我们也就实现了DLL注入。

5.APC注入

APC(Asynchronous Procedure Call),Windows异步过程调用,APC是一种并发机制,它能让线程在它正常的执行路径运行之前运行一些代码。
每一个线程都有自己的APC队列(队列先入先出),可以使用QueueUserAPC函数把一个函数压入APC队列。当线程空闲,处于可警告的等待状态被处理时(例如线程调用SignalObjectAndWait、WaitForSingleObjectE、WaitForMultipleObjectsEx、SleepEx等),该线程将逐个调用APC队列,当APC队列完成时,线程才进行沿着它正常的路径继续运行。
使用QueueUserAPC函数插入APC函数,QueueUserAPC内部调用的是NtQueueApcThread,再内部是KiUserApcDispatcher:
language
DWORD WINAPI QueueUserApc(
_In_ PARCFUNC pfnApc, //指向APC函数
_In_ HANDLE hThread, //线程句柄,具有THREAD_SET_CONTEXT访问权限
_In_ ULONG_PTR dwData //APC函数参数
);
NTSTATUS NTAPI NtQueueApcThread(
IN HANDLE ThreadHandle,
IN PKNORMAL_ROUTINUE ApcRoutine,
IN PVOID SystemArgument1, //dwData
IN PVOID SystemArgument2, //dwData
IN PVOID SystemArgument3);
VOID KiUserApcDispatcher(
IN PCONTEXT Context,
IN PVOID ApcContext,
IN PVOID Argument1,
IN PVOID Argument2,
IN PKNORMAL_ROUTINE ApcRoutine)

攻击者可以将恶意代码作为一个APC函数插入APC队列(调用QueueUserAPC或NtQueueApcThread),而这段恶意代码一般实现加载DLL的操作,实现DLL注入。
APC函数的原型:
language
VOID CALLBACK APCProc(
_In_ ULONG_PTR dwParam
);

利用APC实现DLL注入的流程:
①获取目标进程的目标线程ID;
②调用VirtualAllocEx向目标进程申请内存,调用WriteProcessMemory向内存写入DLL的注入路径;
③获取线程句柄(调用OpenThread函数以THREAD_ALL_ACCESS访问权限打开线程),调用QueueUserAPC函数向线程插入APC函数,设置APC函数的地址为LoadLibrary函数地址,设置APC函数参数为上述DLL路径地址。
核心代码:
language
// 打开注入进程
hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
// 在注入进程空间申请内存
pBaseAddress = ::VirtualAllocEx(hProcess, NULL, dwDllPathLen, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
// 向申请的空间中写入DLL路径数据
::WriteProcessMemory(hProcess, pBaseAddress, pszDllName, dwDllPathLen, &dwRet);
// 获取 LoadLibrary 地址
pLoadLibraryAFunc = ::GetProcAddress(::GetModuleHandle("kernel32.dll"),"LoadLibraryA");
// 打开线程
hThread = ::OpenThread(THREAD_ALL_ACCESS, FALSE, pThreadId[i]);
// 插入APC
::QueueUserAPC((PAPCFUNC)pLoadLibraryAFunc, hThread, (ULONG_PTR)pBaseAddress);

6.HOLLOWING

Hollowing技术又被称为傀儡进程,它是将目标进程(合法程序)的内存中的合法代码挖空,并用另一个可执行文件(恶意程序)覆盖目标进程的内存空间,这样看似执行的是目标进程(合法程序),实际上执行的是另一个程序(恶意程序)。
这项技术的核心原理是用户在使用CreateProcess创建进程的时候,可以指定CREATE_SUSPENDED标志以挂起状态创建目标进程。在目标进程挂起的时候,用户可以对目标进程进行任意的修改。
创建傀儡进程的流程:
①调用CreateProcess,以CREATE_SUSPENDED状态创建进程;
②调用GetThreadContext(标志CONTEXT_FULL)获取进程中所有线程的上下文;
③调用VirtualAlloc向目标进程申请一共可读写执行的内存;(如果要写入的payload数据过大,需要先调用ZwUnmapViewOfSection/NtUnmapViewOfSection卸载目标进程原有区块);
④调用WriteProcessMemory写入payload数据,如果写入的是另一个程序的数据,则要模拟PE加载的过程(可参考本系列之前的文章),实现内存对齐等;
⑤调用SetThreadContext重新设置进程上下文,设置EIP指向payload;
⑥在修改完毕后,调用ResumeThread唤醒目标进程,让目标进程按照修改后的EIP继续执行。
核心代码:
```language
// 创建进程并挂起主线程
bRet = ::CreateProcess(pszFilePath, NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi);
// 在替换的进程中申请一块内存
LPVOID lpDestBaseAddr = ::VirtualAllocEx(pi.hProcess, NULL, dwReplaceDataSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

// 写入替换的数据
bRet = ::WriteProcessMemory(pi.hProcess, lpDestBaseAddr, pReplaceData, dwReplaceDataSize, NULL);

// 获取线程上下文
threadContext.ContextFlags = CONTEXT_FULL;
bRet = ::GetThreadContext(pi.hThread, &threadContext);
// 修改进程的PE文件的入口地址以及映像大小,先获取原来进程PE结构的加载基址
threadContext.Eip = (DWORD)lpDestBaseAddr + dwRunOffset;
// 设置挂起进程的线程上下文
bRet = ::SetThreadContext(pi.hThread, &threadContext);
//唤醒线程
::ResumeThread(pi.hThread);

```

7.线程执行劫持

线程执行劫持技术和傀儡进程技术相似,傀儡进程替换的是整个进程而线程执行劫持替换的只是某一个线程。
线程执行劫持也需要先在RWX内存中写入payload,写入完毕后直接将线程执行地址替换为payload地址就行了:
language
HANDLE t = OpenThread(THREAD_SET_CONTEXT, FALSE, thread_id);//打开线程
SuspendThread(t);//挂起线程
CONTEXT ctx;
ctx.ContextFlags = CONTEXT_CONTROL;
ctx.Rip = (DWORD64)payload;//设置线程新的执行地址
SetThreadContext(t, &ctx);
ResumeThread(t);//唤醒线程

参考文献:

https://www.kanxue.com/chm.htm?id=13104&pid=node1000838
https://www.endgame.com/blog/technical-blog/ten-process-injection-techniques-technical-survey-common-and-trending-process
https://github.com/reactos/reactos
http://blog.sina.com.cn/s/blog_7230c5bf0101899n.html
https://msdn.microsoft.com/zh-cn/windows/ms649053(v=vs.95)

相关推荐: Primary Access Token Manipulation Attack(令牌操作攻击上)

令牌操纵攻击是APT组织所使用的一种常见技术,恶意软件可在受害者的系统上获得更高的特权或代表任何其他用户(假冒)执行某些操作。 这是一些MITRE上面使用的令牌操作攻击技术的APT和工具的示例: 我们可以看到里面包含了常见的cobalt strike的make…