在代码所构成的世界当中,始终是存在着一些希冀投机取巧之人的。一些带有恶意性质的代码借助 Hook 以及进程注入方面的技术手段,悄然不动声色地隐匿于你的个人电脑内部,它不但能够成功躲避开杀毒软件的仔细扫描,而且还能够长时间驻留在计算机内存当中,趁机窃取数据信息,甚至还能够获取到整台系统给予的最高权限。这样一种关于防范与攻击的对抗情况,每一天都在持续不断地上演。
HHOOK WINAPI SetWindowsHookExA(
int idHook, //钩取的消息的类型
HOOKPROC lpfn, //指向钩子过程的指针,即回调函数地址
HINSTANCE hmod, //包含lpfn过程的dll实例句柄
DWORD dwThreadId //线程ID
);
LRESULT CALLBACK HookProc(
int nCode,//钩子代码,表示如何处理消息;
//wParam和lParam表示消息,根据消息的类型不同具有不同的含义;
WPARAM wParam,
LPARAM lParam
){
// process event
...
return CallNextHookEx(NULL, nCode, wParam, lParam);
}
Window的消息钩子机制的原本意图是使得开发者能够对系统消息进行监控或者处理,像键盘输入、鼠标动作这类消息。攻击者能够借助SetWindowsHookEx函数来安装钩子,一旦消息被触发,他们所编写的代码便能够在受害进程里执行。当同时设置多个钩子时,它们会构建成一个链条,消息会按照顺次经过每一个钩子函数,这为攻击者提供了层层拦截的契机。
LRESULT WINAPI CallNextHookEx(
_In_opt_ HHOOK hhk,//钩子的句柄
_In_ int nCode,
_In_ WPARAM wParam,
_In_ LPARAM lParam);
BOOL WINAPI UnhookWindowsHookEx(
_In_ HHOOK hhk //SetWindowsHookEx的返回值
);
关键在于全局钩子的参数设置,线程ID参数设为0时,此钩子会对系统里所有正在运行的进程产生影响,攻击范围瞬间得到扩大,然而钩子回调函数当中必须调用CallNextHookEx,不然消息会在此处卡住,致使系统界面出现卡顿甚至崩溃,这种异常反倒会引起用户的警觉。
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); // 触发钩子
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//标记值,指定挂钩函数的位置和要跳过的事件的位置。
);
typedef void (CALLBACK *WINEVENTPROC)(
HWINEVENTHOOK hWinEventHook,//SetWinEventHook返回值,钩子函数句柄
DWORD event,//指定发生的事件(事件常量)
HWND hwnd,//生成事件的窗口,如果没有窗口与事件关联,则NULL;
LONG idObject,//对象标识符,表示窗口某个部分
LONG idChild,//如果此值为CHILDID_SELF,则事件由对象触发; 如果此值是子ID,则该事件由子元素触发。
DWORD dwEventThread,//标识生成事件的线程或拥有当前窗口的线程
DWORD dwmsEventTime//指定生成事件的时间
);
// 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("%S\n", bstrName);
SysFreeString(bstrName);
pAcc->Release();
}
}
每一个Windows程序均具备导入地址表,于其中所存放的是其打算调用的系统API函数地址。IAT Hook的原理是十分简单的,也就是寻找到这个表,将某个API的地址变更为恶意代码的地址。举例而言,程序原本计划调用MessageBox,然而却一头扎入了攻击者所编写的弹窗代码之中,用户根本无法察觉到异常。
达成这个技术要对PE文件结构予以解析,攻击者需读取目标程序的头部信息,遍历导入表,寻觅目标API的条目,这种变动极为隐蔽,鉴于程序的执行流程未发生改变,仅从中途转而弯折,平常的文件完整性核验很难察觉此处的异常。
HOOK方式里,最直接的是API函数开头修改,攻击者将函数前5个字节改成JMP指令,以此直接跳转到自己的代码,不过这么一改,那原来的5个字节就被覆盖了,要是恢复不及时,会导致系统功能异常,在Windows里,很多API开头都有MOV EDI, EDI这两个看似无用的字节。
机灵的攻击者会借助这两个字节来施行短跳转,跳转至函数上方的闲置区域。在那个地方存在着充足的空间用以写入跳转代码,既达成了 HOOK,又使原始指令得以留存,以这样的如同温水煮青蛙般的方式更不容易被察觉。恰似于你家门前挖掘了一条地道,然而门牌号依旧悬挂着。
有一个名为AppInit_DLLs的注册表项是Windows所提供的,任何加载user32.dll的进程,都会自动加载在此处指定的DLL,只要攻击者把写入恶意DLL的绝对路径,并且将LoadAppInit_DLLs值设为1,便能实现大规模注入,从WinXP到Win10都对该功能予以支持。
不过在默认情形之下,这个注册表项是不存在的,攻击者得自己去创建。借助这个机制,在系统启动之际,或者新进程创建之时,恶意代码会静悄悄地寄生进去。这种注入方式借助了系统自身的设计,杀毒软件很难分辨这到底是合法程序呢,还是恶意行为。
Windows的并发机制包含异步过程调用,该机制能够让线程在正常执行路径之前先去运行一些代码。若有攻击者,会将恶意函数安插到目标线程的APC队列,待线程进入要警告才能等待的状态之际,那些代码便可以被执行。当创建进程CreateProcess时,只要设置挂起这一标志,就能够在这个新进程的线程方面动手脚。
在实际进行实施对应攻击时,那位实施攻击方的人员,会首先去寻觅找寻到目标进程所拥有有的线程,接着调用QueueUserAPC这个操作来将恶意代码予以插入进去。当线程归属因为SleepEx、WaitForMultipleObjects等函数从而进入处于一种等待的状态之时,便会对APC队列进行对应的处理操作行为情况呈现此方式并不需要去从事创建远程线程的相关事宜,其显得更为隐蔽,特别适合于将其注入到那些会频繁出现等待情况的系统服务之中。
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 // 是返回实际写入的字节。
);
//打开进程
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);
进程挖空方面将合法程序代码予以完整替换成为恶意程序,攻击者凭借挂起状态去创建目标进程,像记事本这类,接着通过ZwUnmapViewOfSection卸载其内存,再借助VirtualAllocEx重新进行分配,最终依赖WriteProcessMemory写入恶意代码,等到时候恢复进程运行,表面看上去是记事本,可实际上运行的却是黑客程序。
处理PE文件内存对齐是此项技术最难之处,写入的数据得如同系统加载器那般,将各个节区按照内存对齐方式展开,若只是单纯复制文件内容,程序一旦运行便会崩溃,攻击者必须模拟Windows的加载过程,保证导入表、重定位表均正确设置,这可是个技术活。
你曾借助哪些工具去检测或者防御此类内存层面的攻击呢,欢迎于评论区域分享你的经验,点赞并且转发以使更多人知晓这些隐蔽的威胁。
ATOM WINAPI GlobalAddAtom(//向全局原子表添加一个字符串,并返回这个字符串的原子
_In_ LPCTSTR lpString//原子名字符串
);
UINT GlobalGetAtomNameA(//从表中获取全局原子名字符串,存储在缓冲区中
ATOM nAtom,//原子
LPSTR lpBuffer,//缓冲区
int nSize//缓冲区大小
);