赞
踩
前几天逛看雪的时候,发现fickle大佬的这个思路很是不错,正好我也挺感兴趣的,就去学了一下inlinehook,嗯,不太难,所以就有了这篇文章,我从0开始讲这个代码框架,我很尽量的把这篇博客写的详细了,应该会一点点C基础就能看懂,想看原文这里https://bbs.pediy.com/thread-258054.htm
就是把我们的代码加在目标进程然后执行,可以理解为一个变种的exe文件,只不过main函数变成了DLLMain,
MSDN定义DLLMain原型
BOOL APIENTRY DllMain( HMODULE hModule, //指向自身的句柄
DWORD ul_reason_for_call, //调用原因
LPVOID lpReserved//隐式加载和显式加载
//LPVOID是一个没有类型的指针,也就是说你可以将任意类型的指针赋值给LPVOID类型的变量(一般作为参数传递),然后在使用的时候在转换回来
)
三个参数都是_In_类型
第二个参数
系统是在什么时候调用DllMain函数的呢?静态链接时,或动态链接时调用LoadLibrary和FreeLibrary都会调用DllMain函数。DllMain的第二个参数fdwReason指明了系统调用Dll的原因,它可能是:: DLL_PROCESS_ATTACH、 DLL_PROCESS_DETACH、 DLL_THREAD_ATTACH、 DLL_THREAD_DETACH。 以下从这四种情况来分析系统何时调用了DllMain。 进程映射 DLL_PROCESS_ATTACH 大家都知道,一个程序要调用Dll里的函数,首先要先把DLL文件映射到进程的地址空间。要把一个DLL文件映射到进程的地址空间,有两种方法:静态链接和动态链接的LoadLibrary或者LoadLibraryEx。 当一个DLL文件被映射到进程的地址空间时,系统调用该DLL的DllMain函数,传递的fdwReason参数为DLL_PROCESS_ATTACH,这种调用只会发生在第一次映射时。如果同一个进程后来为已经映射进来的DLL再次调用LoadLibrary或者LoadLibraryEx,操作系统只会增加DLL的使用次数,它不会再用DLL_PROCESS_ATTACH调用DLL的DllMain函数。不同进程用LoadLibrary同一个DLL时,每个进程的第一次映射都会用DLL_PROCESS_ATTACH调用DLL的DllMain函数。 可参考DllMainTest的DLL_PROCESS_ATTACH_Test函数。 进程卸载 DLL_PROCESS_DETACH 当DLL被从进程的地址空间解除映射时,系统调用了它的DllMain,传递的fdwReason值是DLL_PROCESS_DETACH。当DLL处理该值时,它应该执行进程相关的清理工作。 那么什么时候DLL被从进程的地址空间解除映射呢?两种情况: FreeLibrary解除DLL映射(有几个LoadLibrary,就要有几个FreeLibrary) 进程结束而解除DLL映射,在进程结束前还没有解除DLL的映射,进程结束后会解除DLL映射。(如果进程的终结是因为调用了TerminateProcess,系统就不会用DLL_PROCESS_DETACH来调用DLL的DllMain函数。这就意味着DLL在进程结束前没有机会执行任何清理工作。) 注意:当用DLL_PROCESS_ATTACH调用DLL的DllMain函数时,如果返回FALSE,说明没有初始化成功,系统仍会用DLL_PROCESS_DETACH调用DLL的DllMain函数。因此,必须确保清理那些没有成功初始化的东西。 可参考DllMainTest的DLL_PROCESS_DETACH_Test函数。 线程映射 DLL_THREAD_ATTACH 当进程创建一线程时,系统查看当前映射到进程地址空间中的所有DLL文件映像,并用值DLL_THREAD_ATTACH调用DLL的DllMain函数。 新创建的线程负责执行这次的DLL的DllMain函数,只有当所有的DLL都处理完这一通知后,系统才允许进程开始执行它的线程函数。 注意跟DLL_PROCESS_ATTACH的区别,我们在前面说过,第n(n>=2)次以后地把DLL映像文件映射到进程的地址空间时,是不再用DLL_PROCESS_ATTACH调用DllMain的。而DLL_THREAD_ATTACH不同,进程中的每次建立线程,都会用值DLL_THREAD_ATTACH调用DllMain函数,哪怕是线程中建立线程也一样。 线程卸载 DLL_THREAD_DETACH 如果线程调用了ExitThread来结束线程(线程函数返回时,系统也会自动调用ExitThread),系统查看当前映射到进程空间中的所有DLL文件映像,并用DLL_THREAD_DETACH来调用DllMain函数,通知所有的DLL去执行线程级的清理工作。 注意:如果线程的结束是因为系统中的一个线程调用了TerminateThread,系统就不会用值DLL_THREAD_DETACH来调用所有DLL的DllMain函数。
简单说
ul_reason_for_call的值 代表的状态
DLL_PROCESS_ATTACH Dll刚刚映射到进程空间中
DLL_THREAD_ATTACH 进程中有新线程创建
DLL_THREAD_DETACH 进程中有新线程销毁
DLL_PROCESS_DETACH Dll从进程空间中接触映射
一般只用DLL_PROCESS_ATTACH(Dll刚刚映射到进程空间的时候就执行相关的代码)就够了,
DLLMain函数怎么写
BOOL APIENTRY DllMain( HMODULE hModule, DWORD dwReason,LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
***添加想执行的语句(功能函数)***
case DLL_THREAD_ATTACH:
break;
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
当dll被加载(DLL_PROCESS_ATTACH)时,为了实现功能,因为这次插件要加载其他进程,所以要调用创建进程(CreateThread)调用函数(ThreadProc)
CreateThread函数
MSDN
该函数在主线程的基础上创建一个新线程。线程终止运行后,线程对象仍然在系统中,必须通过CloseHandle函数来关闭该线程对象。
原型:
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,//SD
SIZE_T dwStackSize,//initialstacksize
LPTHREAD_START_ROUTINE lpStartAddress,//threadfunction
LPVOID lpParameter,//threadargument
DWORD dwCreationFlags,//creationoption
LPDWORD lpThreadId//threadidentifier
)
参数
IpThreadAttributes:指向SECURITY_ATTRIBUTES型态的结构的指针。在Windows 98中忽略该参数。在Windows NT中,NULL使用默认安全性,不可以被子线程继承,否则需要定义一个结构体将它的bInheritHandle成员初始化为TRUE
第一个参数NULL就ok了
wStackSize,设置初始栈的大小,以字节为单位,如果为0,那么默认将使用与调用该函数的线程相同的栈空间大小。任何情况下,Windows根据需要动态延长堆栈的大小。
第二个0就行
lpStartAddress,指向线程函数的指针,形式:@函数名,函数名称没有限制,
就是功能函数
lpParameter:向线程函数传递的参数,是一个指向结构的指针,不需传递参数时,为NULL。
dwCreationFlags :线程标志,可取值如下
0就行
lpThreadId:保存新线程的id。```
NULL
本次形式:CreateThread(NULL, 0, LPTHREAD_START_ROUTINE(DlgThread), hModule, 0, NULL)
LPTHREAD_START_ROUTINE函数指针
如果监控到有被撤回的消息,找到这条消息的发出者(wxid)和内容,然后弹出一个对话框(DialogBox)
先了解一下对话框
对话框
msdn
int DialogBox(
HINSTANCE hInstance,//对话框属于当前进程,HINSTANCE是窗口进程句柄
LPCTSTR lpTemplate,//对话框使用哪个对话框资源
HWND hWndParent,//对话框的父窗口是哪个,NULL表示没有父窗口
DLGPROC lpDialogFunc//窗口有对应的消息处理函数
);
DialogBox(hInstance, MAKEINTRESOURCE(IDD_DIALOG1), NULL, Dlgproc);
最后一个函数是回调函数,继续查MSDN
DLGPROC Dlgproc;
INT_PTR Dlgproc(
HWND unnamedParam1,
UINT unnamedParam2,
WPARAM unnamedParam3,
LPARAM unnamedParam4
)
{...}
细看一下
unnamedParam1 Type: HWND A handle to the dialog box. unnamedParam2 Type: UINT The message. unnamedParam3 Type: WPARAM Additional message-specific information. unnamedParam4 Type: LPARAM Additional message-specific information. Type: INT_PTR
看这里,第二个参数
unnamedParam2
Type: UINT
The message.
有好几种类型,点进去看一下WM_INITDIALOG的documentation
我找到了中文文档,浏览器翻译不太行,一般都是自己翻译。这次竟然有官方中文文档https://docs.microsoft.com/zh-cn/windows/win32/dlgbox/wm-ctlcolordlg
WM _ INITDIALOG 消息:
在对话框显示之前立即发送到对话框过程。 对话框过程通常使用此消息来初始化控件并执行任何其他影响对话框外观的初始化任务。就是表明对话框及其所有子控件都创建完毕了
所以说对话框DialogBox最后一个参数的回调函数应该这样写
INT_PTR CALLBACK Dlgproc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { case WM_INITDIALOG: { m_dialog_hwnd = hWnd; wechatWinAddr = GetWxModuleAddress(); revockCallVA = wechatWinAddr + REVOCK_CALL_RVA; revockCallTargetVA = wechatWinAddr + REVOCK_CALL_TARGET_RVA; revockCallJmpBackVA = revockCallVA + 5; StartHook5(wechatWinAddr+REVOCK_CALL_RVA,backCode,_OnRevock); }break; case WM_COMMAND: break; case WM_CLOSE: close(hWnd); break; } return 0; }
RVA是什么呢,看我的这篇笔记
call的VA = LoadLibraryW(L"WeChatWin.dll") + RVA(0x28c33f)
目标地址(撤回的消息的地址)= LoadLibraryW(L"WeChatWin.dll") + RVA(0x28ccd0)
后面就是inline hook函数了
盗个图来解释一下流程
但是不能直接把汇编代码插入的,因为内存装载到内存里面以后,如果我们手动强制添加一串代码(指令)进去,那需要给程序重新做地址重定位,这是不可能的事情,所以我们只能在要hook的地方写入一个jmp,跳转到我们写的函数(call)执行功能,然后再跳到原来的函数去,这样内存结构不变,就是个指令替换,如上图,如果还没有看懂,我找到了另外一个图
自己也画了个图,按OD的感觉画的
构造一个长度为5字节的jmp指令(jmp的机器码占用1字节,跳转到的地址偏移占用4字节)来替换原来的5字节的call指令。E8,即我们需要寻找长度为5字节的call,来进行替换
这次用到的短跳转(近距离地址),E9,如果需要hook的假设需要hook的call的指令的内存地址为:0x1000,我们想要它执行后跳转到我们的函数(假设函数在内存中的地址:0x5000),那么,构造jmp指令时,指令应为:
jmp (0x5000-(0x1000 + 5))
jmp 0x3FFB
因为jmp指令占5字节,所以要加5
Write(Read)ProcessMemory太简单了,但是正好有好久好久之前的笔记,那就放出来吧,省得你们去查MSDN了
okk,对应Write(Read)ProcessMemory的文档就很明确了,hookAddr是要被替换的call指令的内存地址(0x1000);backCode是长度为5字节的数组缓冲区,写的是call指令(E9 0x3FF8);FuncBeCall是0x5000,用OpenProcess打开当前进程,然后读取当前进程hookAddr处的指令,写入backCode数组,再将构造好的jmp指令写入当前进程hookAddr处。
StartHook函数第3个参数接收一个函数地址,这个函数地址指向的函数应该是这样的:
DWORD tEsp = 0;
_declspec(naked) void _OnRevock() {
__asm {
mov tEsp, esp
pushad
}
OnRevock(tEsp);
__asm {
popad
call revockCallTargetVA
jmp revockCallJmpBackVA
}
}
OnRevock(tEsp)就是功能函数
void OnRevock(DWORD esp) { wchar_t *tips = *(wchar_t **)(esp + 0xC); wchar_t *msg = *(wchar_t **)(esp + 0x10); if (NULL != tips) { WCHAR buffer[8192]; wchar_t* pos = wcsstr(tips, L"撤回了一条消息"); if (pos!= NULL && NULL != msg) { swprintf_s(buffer, L"* %s,内容:%s \r\n\r\n",tips, msg); HWND hTextBox = GetDlgItem(m_dialog_hwnd, IDC_EDIT1); //EM_SETSEL消息,wParam代表开始,lParam代表结束 SendMessage(hTextBox, EM_SETSEL, -1, -1); //EM_REPLACESEL消息,wParam代表是否可以撤销替换,lParam代表替换成的文本 SendMessage(hTextBox, EM_REPLACESEL, 0, (LPARAM)buffer); } } }
_OnRevock函数用_declspec(naked)修饰,被它修饰的函数我们常称它为裸函数,编译器不对裸函数做任何操作,也就是说裸函数就是我们自己写的汇编代码,裸函数的特点是在编译生成的时候不会产生过多用于平衡堆栈的指令,这意味着在裸函数中我们要编写内联汇编控制堆栈平衡。一个比较简单写法是备份所有的寄存器,做完其他操作后再把寄存器的值还原回去,
笨代码示例
DWORD tEax = 0,tEcx = 0,tEdx = 0,tEbx = 0,tEsp = 0,tEbp = 0,tEsi = 0,tEdi = 0; _declspec(naked) void OnCall() { __asm { mov tEax, eax mov tEcx, ecx mov tEdx, edx mov tEbx, ebx mov tEsp, esp mov tEbp, ebp mov tEsi, esi mov tEdi, edi } //do something __asm { mov eax, tEax mov ecx, tEcx mov edx, tEdx mov ebx, tEbx mov esp, tEsp mov ebp, tEbp mov esi, tEsi mov edi, tEdi call ... jmp ... } }
裸函数规则
The following rules and limitations apply to naked functions(摘自MSDN)
官方示例代码
// nkdfastcl.cpp // compile with: /c // processor: x86 __declspec(naked) int __fastcall power(int i, int j) { // calculates i^j, assumes that j >= 0 // prolog __asm { push ebp mov ebp, esp sub esp, __LOCAL_SIZE // store ECX and EDX into stack locations allocated for i and j mov i, ecx mov j, edx } { int k = 1; // return value while (j-- > 0) k *= i; __asm { mov eax, k }; } // epilog __asm { mov esp, ebp pop ebp ret } }
改良裸函数
因为tEax = 0,tEcx = 0,tEdx = 0,tEbx = 0,tEsp = 0,tEbp = 0,tEsi = 0,tEdi = 0;
寄存器入栈可以改变一下
改前
push EAX
push ECX
push EDX
push EBX
push ESP
push EBP
push ESI
push EDI
改后
pushad
同理,mov 8个通用寄存器, tEax改为popad
所以,改后就成为了下面这种模板
_declspec(naked) void OnCall() {
__asm {
pushad
}
//do something 功能函数
__asm {
popad
call ...
jmp ...
}
}
卸载hook的流程比较简单,也是打开当前进程,把hook时备份的call指令写回原来的位置
int Unhook(DWORD hookAddr, BYTE backCode[5]) {
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, NULL, GetCurrentProcessId());
if (WriteProcessMemory(hProcess, (LPVOID)hookAddr, backCode, 5, NULL) == 0) {
return -1;
}
return 0;
}
参数hookAddr是原来的call的内存地址,backCode是之前备份下来的call指令(EB XXXX0000)
怎么说,正好手头有例子,看下恶意代码例子12-01吧,xp虚拟机启动,IDA启动
每分钟都会有个讨厌的弹窗,而且我虚拟机里的这个IDA没用反汇编插件,反正汇编也不难,直接分析吧
LoadLibraryA和GetProcAddress将psapi.dll中的三个函数的地址分别保存在dword_408714,dword_40870c,dword_408710
我继续向下看,到这我发现有点不太对
很明显了,用lab12-01.dll来调用LoadLibraryA然后注入Lab12-01.dll,和我们这次要说的没啥关系
浪费我五分钟时间,不说这个了,感兴趣的看这个博客https://blog.csdn.net/wangtiankuo/article/details/77097536,短小精干,我下次遇到inlinehook恶意代码我再补充
注入器来着吾爱大神,帖子https://www.52pojie.cn/thread-429548-1-1.html
注入器放网盘了
链接:https://pan.baidu.com/s/1UkIb-ODMQgzjNTBdxNjixQ
提取码:hlll
说实话,这个老哥把注入器的源码发出来了,真想今天晚上就搞明白,给这篇文章再加一个从0带你写注入器,MD,两点了熬不动了,明天周五和周末搞
源码放网盘了,带了点配置文件,懒
链接:https://pan.baidu.com/s/19Yn1L7guj-p5NjPBGpe2ag
提取码:hlll
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。