【动态Dll注入】160个CrackMe之041
本帖最后由 whklhh 于 2017-11-2 02:57 编辑静态注入法:https://www.52pojie.cn/forum.php ... &extra=#pid17678797
在最后添加了Dll注出功能的实现~
=========================================11.2更新=======================================
dll注入的原理是利用一个进程控制被注入进程执行LoadLibrary函数,将外部dll注入其内存中。
注入以后由于共享内存空间,因此dll也就拥有了操作该进程内存的权限了。
当Dll被注入时会运行DllMain函数,这就是Dll的主函数。
由于之前没有写过Dll注入,因此这次准备先执行一个MessageBox作为Hook成功标志,明天再将前一次得到的成功经验照样通过dll注入来实现。
首先,Dll注入分为3个阶段:
1.外部函数控制被注入程序执行LoadLibrary函数加载外部Dll
2.Dll注入,执行DllMain函数
3.Hook住被注入程序的函数,当条件满足时触发Hook函数
依次完成,首先是外部函数:
#include <iostream>
#include <windows.h>
#include <tchar.h>
using namespace std;
BOOL InjectDll(int dwPID, LPCTSTR szDllPath)
{
HANDLE hProcess = NULL, hThread = NULL;
HMODULE hMod = NULL;
LPVOID pRemoteBuf = NULL;
DWORD dwBufSize = (DWORD)(_tcslen(szDllPath) + 1) * sizeof(TCHAR);
LPTHREAD_START_ROUTINE pThreadProc;
// 获取目标进程句柄
if(!(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)))
{
cout<<dwPID<<endl;
cout<<"failed!!!\n";
return FALSE;
}
// 在目标进程内存中分配szDllName大小的内存
pRemoteBuf = VirtualAllocEx(hProcess, NULL, dwBufSize, MEM_COMMIT, PAGE_READWRITE);
// 将myhack.dll路径写入分配的内存
WriteProcessMemory(hProcess, pRemoteBuf, (LPVOID)szDllPath, dwBufSize, NULL);
// 获取LoadLibraryA地址
hMod = GetModuleHandle("kernel32.dll");
pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(hMod, "LoadLibraryA");
// 运行线程
hThread = CreateRemoteThread(hProcess, NULL, 0, pThreadProc, pRemoteBuf, 0, NULL);
//等待该线程执行完毕
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
CloseHandle(hProcess);
return TRUE;
}
int main(int argc, TCHAR *argv[])
{
if(InjectDll(atoi(argv), "F:\\C\\cm41_hoook\\bin\\Debug\\cm41_hoook.dll"))
cout<<"Success "<<argv<<endl;
else
cout<<"Failed"<<endl;
return 0;
}
参照书上代码,main函数接收被注入程序的PID,从而执行InjectDll函数
InjectDll函数通过PID获取该进程的句柄,然后将参数(外部Dll的路径)和函数(Kernel32.dll中的LoadLibrary函数)准备好,通过CreateRemoteThread函数来控制被注入进程开启一个新线程加载dll
注意这里LoadLibrary函数取的地址事实上是注入函数的,但由于dll共享内存,因此地址可以通用
注意LoadLibrary函数的W和A,刚开始在这里犯错了OTZ
W表示宽字符,A表示ASCII,默认字符串是ASCII的,因此使用A
PID通过任务管理器可以查看到
第二步编写DllMain函数:
BOOL NewMenu()
{
return (MessageBoxA(NULL, "Hook success", "Second", MB_OKCANCEL));
}
extern "C" DLL_EXPORT BOOL APIENTRY DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
MessageBoxA(NULL, "Dll inject success", "First", MB_OK);
switch (fdwReason)
{
case DLL_PROCESS_ATTACH:
// attach to process
// return FALSE to fail DLL load
hook_by_code((PROC)NewMenu);
break;
}
return TRUE; // succesful
}
hook_by_code就是修改代码来hook的函数,NewMenu是hook目标函数
hook_by_code取得目标函数的地址作为参数,来修改代码:BOOL hook_by_code(PROC pfnNew)
{
HANDLE hProcess;
DWORD* pfnOrg;
//push的机器码
byte pBuf = {0x68};
DWORD oldProtect;
//Hook地址
pfnOrg = (DWORD*)0x430535;
//将hook目标函数的地址送入缓冲区中
memcpy(&pBuf, &pfnNew, 4);
//ret的机器码
pBuf = 0xc3;
//修改内存属性为可写
VirtualProtect((LPVOID)pfnOrg, 10, PAGE_EXECUTE_READWRITE, &oldProtect);
//将缓冲区写入被注入程序的内存
memcpy(pfnOrg, pBuf, 6);
return TRUE;
}
目前需要程序运行的内容为(汇编):
push 0xaaaaaaaa ;目标函数地址
ret ;call 目标函数地址
刚开始执行的时候花式报错,原来是data节区没有可写属性,因此需要用VirtualProtect函数开启该权限
hook的地址还是老地方:
http://img.blog.csdn.net/20171030234052197?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvd2hrbGhoaGg=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast
这样注入以后,就能实现弹窗了
http://img.blog.csdn.net/20171031002001418?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvd2hrbGhoaGg=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast
查到PID以后通过命令行执行程序:
http://img.blog.csdn.net/20171031002219051?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvd2hrbGhoaGg=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast
看到弹出注入成功的弹窗,说明DllMain已经开始执行,Dll成功加载入该程序中
http://img.blog.csdn.net/20171031002325735?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvd2hrbGhoaGg=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast
点击二级菜单后弹出Hook成功的弹窗,说明代码成功被Hook
虽然点完确定以后就会出现内存错误的警报,但这是预料之中的—因为我们只设置了Hook,没有令它返回嘛
以上是一天完成的内容,因此还有一些一步一步和实验性的东西
我觉得程序应该从小写起,从最基本的功能往上一点一点加,这样方便修BUG~
所以下面第二天内容,正式Hook:
首先因为编写代码语言是C++(其实用内联汇编代码写起来费事点,但是处理乱七八糟的堆栈和参数传递要省心的多),因此要注意参数问题
其次是Hook点
Hook函数的执行次序为
Hook函数应该尽可能地降低影响,来避免内存访问错误
是否要执行原函数由具体情况决定,为了降低对原程序的影响程度,最好执行
因为原函数内可能会执行一些对其他地方有影响的操作,不止是堆栈和寄存器的操作,还有一些全局变量操作等等
代码编写起来比较简单,主要是代码的改变,和hook函数的代码
为了在hook函数中执行原函数,有两种解决办法:DLL调用exe里的函数,如果知道函数地址的话 2种方法
1 dll里用函数指针指向exe里的函数
比如exe里有个函数 int SetHook(HWND, HWND);
DLL里就这样
typedef int (CALLBACK* sthook)(HWND, HWND);
sthook SetHook;
SetHook = (sthook)0x00XXXXXX;//0x00XXXXXX是 SetHook的地址
然后DLL里就能用SetHook函数了
2 内联汇编
比如exe里有个函数 int SetHook(HWND, HWND); , 函数地址是0x00XXXXXX
DLL中
__asm {
push eax
mov eax, hwnd_arg2
push eax
mov eax, hwnd_arg1
push eax
mov eax, 0x00XXXXXX
call eax
add esp, 8
pop eax
}
写好代码以后原函数调用总是失败
发现原函数的参数传递是使用寄存器eax,ebx
http://img.blog.csdn.net/20171031131142094?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvd2hrbGhoaGg=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast
而C++编译的函数是通过调用约定来提取参数的,存在于eax或堆栈中
也就是说C语言写的函数提不到ebx的值,即使是写在函数最顶端的内联汇编也由于编译而拿不到原始的ebx
如果Hook函数不需要判断的话,参数倒是无所谓。
但是本次Hook由于需要判断被点击的按钮是否为”Exit”来决定弹窗,因此必须拿到参数
最后没办法,想到了令被Hook函数在跳转前将参数保存到全局变量中的方法
即在push func_addr; retn之前 mov par_addr, eax
然后判断par即可
调试发现dll中使用C->函数指针的方法调用函数,实际上会占用寄存器:
http://img.blog.csdn.net/20171031143304355?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvd2hrbGhoaGg=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast
这样的话eax参数就不可能送进去了╮(╯_╰)╭只能使用内联汇编的方法了
我使用的Code::Blocks编译器支持的是gcc格式的内联汇编,是AT&T记法,用起来比较麻烦
有空赶紧换VS编译器_(:з」∠)_VC格式的内联汇编(INTEL记法)才比较熟悉
asm("movl %%eax, %0\n"
"movl %%ebx, %1\n"
"movl %%ecx, %2\n"
"call %%ecx"
:
:"a"(eax),"b"(ebx),"c"(hook_addr)
);
这样就将之前存储在变量中的值分别送入eax,ebx,ecx中了,然后call ecx就调用了原函数
我们的目标是为Exit按钮添加事件,事件内容很简单,关键在于如何判断Exit按钮
之前静态patch时使用的方法是Hook二级菜单的事件,判断edx(控件下标),由于这次Hook的是窗体事件的函数,控件下标edx已经丢失了,所以只能思考别的办法
eax中存储的是该控件结构体的指针,因此只要在其中找到Caption即可判断按钮。
内存中下断分析以后发现是0xe8的位置开始("&Exit"),因此只需要判断eax+0xe9的值是否为’E’即可
Dll代码如下:
#include "main.h"
#define hook_addr 0x42f3c0
//mov edx, dl
//push addr
//ret
byte pBuf = {0x89, 0x5,0,0,0,0,0x89, 0x1d, 0,0,0,0, 0x68};
byte OrgBytes;
int ebx;
int eax;
int* ebx_p;
int* eax_p;
BOOL hook_by_code(PROC pfnNew)
{
DWORD* pfnOrg;
DWORD oldProtect;
pfnOrg = (DWORD*)hook_addr;
ebx_p = &ebx;
eax_p = &eax;
memcpy(OrgBytes, pfnOrg, 18);
memcpy(&pBuf, &eax_p, 4);
memcpy(&pBuf, &ebx_p, 4);
memcpy(&pBuf, &pfnNew, 4);
//ret
pBuf = 0xc3;
//修改内存属性为可写
VirtualProtect((LPVOID)pfnOrg, 10, PAGE_EXECUTE_READWRITE, &oldProtect);
memcpy(pfnOrg, pBuf, 18);
return TRUE;
}
BOOL New_42f3c0()
{
/*函数指针调用方法,由于eax传参而废弃
typedef int (CALLBACK* func)();
org_func = (func)another_func_addr;
func org_func;
org_func();
*/
DWORD* pfnOrg;
org_func = (func)hook_addr;
byte oldBuf = {0x53, 0x8b, 0xd8, 0x80, 0x7b, 0x2d, };
pfnOrg = (DWORD*)hook_addr;
//脱钩
memcpy(pfnOrg, OrgBytes, 18);
//送入参数并执行原函数
asm("movl %%eax, %0\n"
"movl %%ebx, %1\n"
"movl %%ecx, %2\n"
"call %%ecx"
:
:"a"(eax),"b"(ebx),"c"(hook_addr)
);
//check
if(*(byte*)(eax + 0xe9)=='E')
if(MessageBoxA(NULL, "Do you fickbirne really want to quit?", "Exit", MB_YESNO)==6)
exit(0);
else
;
//再次挂钩
memcpy(pfnOrg, pBuf, 18);
return 0;
}
extern "C" DLL_EXPORT BOOL APIENTRY DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
//MessageBoxA(NULL, "Dll inject success", "First", MB_OK);
switch (fdwReason)
{
case DLL_PROCESS_ATTACH:
// attach to process
// return FALSE to fail DLL load
hook_by_code((PROC)New_42f3c0);
break;
}
return TRUE;
}
这样就可以通过Dll注入的方法来外部添加事件了,
相比起静态Patch而言这种方法要更加灵活
如果Hook的是API,参数也乖乖按照调用约定通过堆栈传递的话,C的代码写起来要比汇编方便许多
PS:
中间我也试过许多其他的方法,例如像静态Patch的方法一样Hook二级菜单的函数,也吃了不少教训。
例如说Hook点最好在函数开头,参数与该函数一致,这样可以尽可能地保持堆栈平衡
像静态Patch那样直接掐下来二级菜单的函数,通过id判断来执行两个事件而不执行原函数的方法对于该程序也是可以的,并且相对而言要简单不少
只不过方法来讲不是太完善,因为完全跳过了原函数,如果其中还有什么其他处理的话就会造成程序不稳定。
因此标准起见,应该在Hook函数内执行原函数。
明天再来继续完善这个程序,例如说可以注出Dll,还原patch;通过进程名直接注入defiler.1,而不用像现在这样每次都要检测pid;检测是否被注入过,进行错误处理,等等
附件中包含了原题、注入程序和Dll,为了方便实用需要自行写入pid和【完整】Dll路径
使用方法:
======================================================注出更新================================
今天主要需要完成的是根据进程名自动搜索pid、错误检查和注出Dll的功能首先是Pid的搜索功能
查了一下貌似没有什么比较方便的方法,只能暴力地遍历句柄取得进程名来对比给定的进程名了
比起暴力地遍历所有句柄来说,相对优雅一点的方式是获取进程快照,来一一对比:
(代码来自《逆向工程核心原理》“卸载Dll”一节和http://blog.csdn.net/jfkidear/article/details/27056955指导)
int GetProcessIdByName(const char*ProcessName)
{
PROCESSENTRY32 stProcess;
HANDLE hProcessShot;
stProcess.dwSize=sizeof(PROCESSENTRY32);
//创建进程快照
hProcessShot=CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,0);
//首个项
Process32First(hProcessShot,&stProcess);
//遍历快照,对比进程名和给定字符串
do
{
if(!strcmp(ProcessName,stProcess.szExeFile))
return stProcess.th32ProcessID;
}while(Process32Next(hProcessShot,&stProcess));
//若未找到进程则返回0,否则返回Pid
CloseHandle(hProcessShot);
return 0;
}
有了这个进程以后,我们的注入程序就不再需要自己去查看任务管理器来取得Pid了
内置进程名defiler.1.exe就可以直接找到Pid,虽然限制了可用性,不过本来Dll就是专用的~
所以为了提升便捷性而牺牲自由性是当然的嘛
错误检查主要就是各个函数的错误返回值加上log,没什么好说的,依次注意就好
本来按书上的走法,DllHookApi开头之前先要进行检查是否已经Hook过
不过自己写的代码检查总是失败,太晚了很困调试不动了囧
再加上,本来以为检查的原因是如果注入两次Dll造成重复Hook的话就会引起错误;但是在调试过程中突然想起,同一个Dll被一个进程装载以后,不会再装载第二次了。
因此这个担忧是不必要的~
于是就放弃Dll的自检查了,而是直接通过注入程序来增加注出功能:
(代码同样参考书上)
BOOL Deject(DWORD dwPID, LPCTSTR szDllName)
{
BOOL bMore = FALSE, bFound = FALSE;
HANDLE hSnapshot, hProcess, hThread;
HMODULE hModule = NULL;
MODULEENTRY32 me = {sizeof(me)};
LPTHREAD_START_ROUTINE pThreadProc;
hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwPID);
bMore = Module32First(hSnapshot, &me);
for(;bMore ; bMore = Module32Next(hSnapshot, &me))
{
if(!_tcsicmp((LPCTSTR)me.szModule, szDllName)||
!_tcsicmp((LPCTSTR)me.szExePath, szDllName))
{
bFound = TRUE;
break;
}
}
if(!bFound)
{
cout<<"dll not found!\n";
CloseHandle(hSnapshot);
return FALSE;
}
if(!(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)))
{
cout<<"Open process failed!"<<endl;
return FALSE;
}
hModule = GetModuleHandle("kernel32.dll");
pThreadProc = (LPTHREAD_START_ROUTINE)GetProcAddress(hModule, "FreeLibrary");
hThread = CreateRemoteThread(hProcess, NULL, 0, pThreadProc, me.modBaseAddr, 0, NULL);
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
CloseHandle(hProcess);
CloseHandle(hSnapshot);
return TRUE;
}
大体上来说与Inject函数类似,只是多了一步查找进程快照中的Dll来取得句柄,其他都是同样的:
申请空间,创建Proc,令其开启线程执行FreeLibrary函数释放Dl
l虽然这个程序其实不太需要注出功能,不过学习嘛,经验要紧~
另外一边,Dll的Detech选项也需要添加脱钩的功能
也就一行,很简单,在DllMain函数中的Switch内增加:
case DLL_PROCESS_DETACH:
// detach from process
//脱钩
memcpy((DWORD*)hook_addr, OrgBytes, 18);
break;
将之前复制的原字节复制回去,即脱钩
(附件已更新,仍然需要提供完整Dll路径,注出时需要加上-d参数)
使用方法:
(从上到下分别为注入时未开启程序、注入成功、注出成功、注出时程序中未加载dll、注出时未开启程序)
到此Dll注入的全套都练习了一遍啦~
基本过程和经验都得到了不少长进,果然还是要实际操作才能有用嘛
之后有好几场CTF,加油~ searchjack 发表于 2017-11-4 05:59
win 10x64+ 毒霸, 没有反应不知道什么鬼
哪里没有反应0.0?我也是win x64 倒是没有毒霸 不过火绒没有报毒来着 理论上来说dll注入应该算病毒行为,但是我进行过很多次了都没有报毒。而且没反应也不是杀软的反应吧~ csmimiyy 发表于 2017-11-21 23:08
vc++dll注入有什么好的书籍推荐
Windows核心编程里有Dll注入的相关章节,我还没看,但想来应该是非常偏内核的内容;本帖是在学习《逆向工程核心原理》中的相关部分时做的。我觉得讲的已经很清楚了。 慢慢学,这个需要时间来消化
有点高深 这个这个这个有点困难啊我慢慢来吧 学习了 努力中 不错,谢谢分享。 楼主高人~高产,非常感谢 努力学习,谢谢大神
谢谢分享!学习了!{:1_919:} 谢谢大佬分享! {:1_914:}
多谢楼主分享哟