原载于bbs.xdsec.org

最近逆向区各种Crack,我今天就来简单介绍一下内存补丁技术。今天拿来开刀的软件是之前演示过的一个简单CrackMe。不过为了提高patch难度加了一个UPX壳。

###0x01

我们先用OD带壳分析一下这个程序。OD载入后,利用ESP定律,来到 JMP 00448F3D

memory-patcher-1.png

这一句是壳的结尾,0048F3D也就是程序的OEP了。这时利用查找字符串的方法,来到关键跳转这里。

memory-patcher-2.png

只要NOP覆盖关键跳就能实现爆破,我们NOP后直接F9运行试试看。

memory-patcher-3.png

显示成功界面!别高兴得太早,现在我们来试试保存文件。OD马上弹出了一个框。

memory-patcher-4.png

这是怎么回事呢?别忘了我们调试的这个程序是带壳的,程序代码被壳加密过,所以不能直接覆盖。那么如何在不脱掉壳的情况下进行破解呢?

###0x02

我们先来理解一下“壳”的概念。简单来看,壳是附加在原始程序上的一个程序,它对原始程序的代码进行加密,使得我们无法直接修改原始程序,也不能通过静态反编译得到程序代码。但是,加密过的原始程序代码不能直接执行,所以在原始程序执行之前,壳必须对原始程序代码进行解密、还原,然后才能执行原始程序。

也就是说,在壳解密完成之前,我们找不到原始程序代码,更不可能进行修改;然而在壳运行完之后,我们就能看到原始程序的代码,也就能修改程序进行爆破了!

现在回忆一下我们刚才用OD爆破的步骤:

  1. 运行到壳的结尾
  2. 修改关键代码(NOP覆盖关键跳)
  3. 运行程序(爆破成功)

接下来我们来写一个程序实现上面的步骤,也就是今天的主角——内存补丁(Loader)。

内存补丁的思想就是当程序运行到某个时刻时,对程序代码进行修改,然后实现我们想要的操作。今天我们利用Debug API来写内存补丁。利用Debug API,我们就能像OD一样调试程序进行爆破。 Debug API具体实现的步骤是这样的:

  1. 载入程序 (使用CreateProcess)
  2. 在壳的结尾下断点 (保存此处原始指令,修改成INT3)
  3. 修改关键代码 (使用WriteProcessMemory)
  4. 删除断点(恢复原始指令)
  5. 运行程序

显然,我们应该把 004A0824 处 JMP 00448F3D 修改成INT3(0xcc),然后用NOP(0x90)覆盖 0040119E 处的关键跳。注意关键跳的地方有6个字节,覆盖的时候要用6个 0x90。 下面是代码:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
/********************************************//**
 * 内存补丁技术学习笔记--Debug API 机制
 *
 * MinGW gcc 4.7.1 编译通过
 * 2013.11.17
 *  by skiyer
 ***********************************************/
   
   
#include <windows.h>
   
#define BREAK_POINT      0x004A0824            //需要下断点处的地址
#define PATCH_POINT      0x0040119E            //需要打补丁处地址
#define SZFILENAME       ".\\第一集.exe"       //目标程序文件名
   
   
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    PSTR szCmdLine, int iCmdShow)
{
   
    STARTUPINFO             si ;
    PROCESS_INFORMATION     pi ;
    ZeroMemory(&si, sizeof(STARTUPINFO)) ;
    ZeroMemory(&pi, sizeof(PROCESS_INFORMATION)) ;
    si.cb = sizeof(STARTUPINFO) ;
   
    BOOL    WhileDoFlag = TRUE;
   
    BYTE    dwINT3code[1] = {0xCC};         //INT3指令
    BYTE    dwOldbyte[1] = {0};             //用于保存BREAK_POINT处原指令
    BYTE    dwPatchcode[] = {0x90, 0x90, 0x90, 0x90, 0x90, 0x90}; //补丁指令
    unsigned int nPatchSize = 6;            //补丁字节数
   
    //调试载入目标程序
    if( !CreateProcess(SZFILENAME, NULL, NULL, NULL, FALSE,
                       DEBUG_PROCESS | DEBUG_ONLY_THIS_PROCESS,
                       NULL, NULL, &si, &pi ) )
    {
        MessageBox(NULL, "程序载入失败!", "ERROR", MB_OK);
        return FALSE;
    }
   
    DEBUG_EVENT     DBEvent ;
    CONTEXT         Regs ;
    DWORD           dwState, Oldpp;
   
   
    Regs.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS ;
   
    while (WhileDoFlag)
    {
        //等待调试事件发生
        WaitForDebugEvent (&DBEvent, INFINITE);
        dwState = DBG_EXCEPTION_NOT_HANDLED ;
        switch (DBEvent.dwDebugEventCode)
        {
        case CREATE_PROCESS_DEBUG_EVENT:
            //如果进程开始
            //备份BREAK_POINT处原指令
            ReadProcessMemory(pi.hProcess, (LPCVOID)(BREAK_POINT), &dwOldbyte, sizeof(dwOldbyte), NULL) ;
            //修改内存属性,使之可写
            VirtualProtectEx(pi.hProcess, (LPVOID)BREAK_POINT, 1, PAGE_EXECUTE_READWRITE, &Oldpp);
            //写入INT3指令
            WriteProcessMemory(pi.hProcess, (LPVOID)BREAK_POINT, &dwINT3code, 1, NULL);
            dwState = DBG_CONTINUE ;
            break;
   
        case    EXCEPTION_DEBUG_EVENT:
            switch (DBEvent.u.Exception.ExceptionRecord.ExceptionCode)
            {
            case    EXCEPTION_BREAKPOINT:
                //如果触发断点异常
            {
                GetThreadContext(pi.hThread, &Regs) ;
                if(Regs.Eip == BREAK_POINT + 1)
                    //如果在BREAK_POINT处中断
                {
                    //在PATCH_POINT处写入补丁
                    WriteProcessMemory(pi.hProcess, (LPVOID)PATCH_POINT, &dwPatchcode, nPatchSize, NULL);
                    //恢复BREAK_POINT处原指令
                    WriteProcessMemory(pi.hProcess, (LPVOID)BREAK_POINT, &dwOldbyte, 1, NULL);
                    //重新执行原指令
                    Regs.Eip--;
                    SetThreadContext(pi.hThread, &Regs) ;
                }
                dwState = DBG_CONTINUE ;
                break;
            }
            }
            break;
   
        case    EXIT_PROCESS_DEBUG_EVENT :
            //进程退出
            WhileDoFlag = FALSE;
            break ;
        }
   
        ContinueDebugEvent(pi.dwProcessId, pi.dwThreadId, dwState) ;
    }
   
    CloseHandle(pi.hProcess) ;
    CloseHandle(pi.hThread)  ;
    return TRUE;
}

把loader与程序放在同一文件夹下,运行loader

memory-patcher-5.png

事实上这只是Debug API 内存补丁技术的一种简单应用。除了爆破,我们可以利用 Debug API 实现更强大的功能。比如,在内存中出现注册码时下断点,再利用MessageBox弹框显示注册码,就实现了注册机功能…另外内存补丁还有其他的实现方法,有兴趣的可以看看《加密与解密》补丁技术一章。