whklhh 发表于 2017-10-31 20:12

【动态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,加油~

whklhh 发表于 2017-11-4 07:58

searchjack 发表于 2017-11-4 05:59
win 10x64+ 毒霸, 没有反应不知道什么鬼

哪里没有反应0.0?我也是win x64 倒是没有毒霸 不过火绒没有报毒来着 理论上来说dll注入应该算病毒行为,但是我进行过很多次了都没有报毒。而且没反应也不是杀软的反应吧~

whklhh 发表于 2017-11-22 00:03

csmimiyy 发表于 2017-11-21 23:08
vc++dll注入有什么好的书籍推荐

Windows核心编程里有Dll注入的相关章节,我还没看,但想来应该是非常偏内核的内容;本帖是在学习《逆向工程核心原理》中的相关部分时做的。我觉得讲的已经很清楚了。

xyz1125 发表于 2017-10-31 20:35

慢慢学,这个需要时间来消化
有点高深

大黑屋 发表于 2017-10-31 20:58

这个这个这个有点困难啊我慢慢来吧

3683057 发表于 2017-11-1 00:34

学习了 努力中

courageous 发表于 2017-11-1 08:32

不错,谢谢分享。

xiawan 发表于 2017-11-1 08:58

楼主高人~高产,非常感谢

yaojin666666 发表于 2017-11-1 09:07

努力学习,谢谢大神

netle8 发表于 2017-11-1 09:29

谢谢分享!学习了!{:1_919:}

林先生的一天 发表于 2017-11-1 09:47

谢谢大佬分享!

xie83544109 发表于 2017-11-1 10:05

{:1_914:}
多谢楼主分享哟
页: [1] 2 3 4 5 6 7
查看完整版本: 【动态Dll注入】160个CrackMe之041