标 题: 【原创】内存补丁初探(C语言实现)
作 者: 珂珂
时 间: 2010-02-22,16:02
链 接: http://bbs.pediy.com/showthread.php?t=107573
    在看雪注册已经一个多月了,由于是加密解密方面的新手,一直未敢上来献丑。本人毕业两年多,主要从事java程序编程,渐渐发现这样的工作并不是我所渴望的,淹埋进java的行业洪流中,让我离计算机的底层越来越远,不禁喟叹,原来那个喜欢汇编、C语言,喜欢操作系统、组成原理的小子,怎么成了代码民工,天天重复着相似的步骤,建表,建类,写功能,做优化,甚至连数据结构都忘了是长什么样了。

    程序员部落酋长Joel在他所著的《软件随想录》说,大学里用Java来教学生,会毁了计算机系的学生,Java只适合在社区学校里做技能培训。回想这两年的工作,发觉自己对计算机真的是越来越陌生了,我怀疑会不会在两年之后,自己除了面向对象什么都不会了。我苦恼,懊悔自己当初的选择。

    重新定位,重新摆正学习的方向,重新捡起自己感兴趣的东西,重新走自己喜欢的路,一切重新开始……,于是两个月内恶补win32汇编与软件加密解密基础,使用的教材都是大家耳熟能详的《Windows环境下32位汇编语言程序设计》及《加密与解密》,经常在看雪上潜水,由于是非正式会员,仅能浏览大师们的帖子,故在此原创一文,期盼能获得一邀请码,也能在各个版面和大师们煮酒论是非。

    首次发帖,略显激动,废话感慨颇多,现言归正传……

    我们采用《Windows环境下32位汇编语言程序设计》13.3.3节(调试API的使用)中的例子程序Text.exe,该程序使用Upx做了压缩处理,类似于简单的加壳。加壳之后,原来的程序代码已被做了处理,只进行静态分析已经不能起作用了。此时,必须采用动态分析技术,跟踪代码一直到可执行程序在内存中被恢复为止,然后分析原始的可执行程序,手工地打上补丁。或者,在分析原程序的基础上,写出内存补丁。书本上的破解例子是采用单步中断方式,一直到00401000,本文的实现是在对原程序分析的基础上,一步到位在00401000处下断点的方式,其实效果都一样,异曲同工。
   
    原程序运行时的状况:

    先进行静态分析,找出壳的结尾部分,用IDA打开Text.exe,计算出start子程序的结尾,然后跳转到这个地方,看一下汇编代码为:jmp     near ptr dword_401000,记下该指令的虚拟地址:0040526F。在该地址处壳代码已经结束,说明原程序已经在内存中恢复完毕,我们可以在内存补丁程序中,在此地址打下断点,然后再打补丁即可。
    
    补丁在哪里打?打什么?当然要动态分析一下了……,打开OD,加载Text.exe,Ctrl+G,跳到0040526F的地方,按F2打断点,按F9执行到此处,然后F8单步调试。

代码:
00401000    33C0            xor     eax, eax           
00401002    0BC0            or      eax, eax
00401004    74 15           je      short 0040101B     
00401006    6A 00           push    0
00401008    68 40204000     push    00402040
0040100D    68 2C204000     push    0040202C
00401012    6A 00           push    0
00401014    E8 19000000     call    00401032                         ; jmp to USER32.MessageBoxA
00401019    EB 10           jmp     short 0040102B
0040101B    6A 10           push    10
0040101D    6A 00           push    0
0040101F    68 10204000     push    00402010
00401024    6A 00           push    0
00401026    E8 07000000     call    00401032                         ; jmp to USER32.MessageBoxA
0040102B    6A 00           push    0
0040102D    E8 06000000     call    00401038                         ; jmp to kernel32.ExitProcess
00401032  - FF25 08204000   jmp     dword ptr [402008]               ; USER32.MessageBoxA
00401038  - FF25 00204000   jmp     dword ptr [402000]               ; kernel32.ExitProcess
    由于xor     eax, eax,故eax==0,or eax,eax结果仍为零,所以je      short 0040101B这里一定跳转(这是测试CM,所以有些小儿科……^+^),而它跳到的地方就是提示“盗版软件”的地方,这里就是需要打补丁的地方。所以,我们可以在此处做文章,可以将00401004与00401005处的字节改为90h与90h(即nop指令),可以将je改为jne,也可以将00401005处的15改为00,直接让程序走下一条指令。效果都是一样的,这里选择最后一种来实现。

    到此对原程序的分析已经完毕,我们的内存补丁思路也已经清晰了:    
  1. 以调试方式打开被破解的程序,利用CreateProcess()函数
  2. 等待调试事件发生,利用WaitForDebugEvent()函数
  3. 当被调试程序的进程创建完毕的时候,保存0x0040526F处的原指令,打断点(int 3指令)
  4. 程序执行到0x0040526F时产生中断,此时执行内存补丁,恢复0x0040526F处的原指令,并让程序从0x0040526F处重新执行
下面贴出源代码供大家交流:
代码:
#include "stdafx.h"
#include "windows.h"

int _tmain(int argc, _TCHAR* argv[])
{
  STARTUPINFO stSi;
  PROCESS_INFORMATION stPi;
  DEBUG_EVENT stDe;
  int nAddrOfBreakPoint = 0x0040526F; //需要下断点的地址
  int nAddrOfPatch = 0x00401005;    //需要打补丁处的地址
  unsigned char cOldByte;        //用于保存nAddrOfBreakPoint断点处的原指令
  unsigned char cBreakPoint = 0xCC;  //int 3指令
  unsigned char cPatchByte = 0x00;  //补丁字节
    CONTEXT stThreadContext;
  BOOL bCreated = FALSE;
  BOOL bFinished = FALSE;
  LPTSTR szCmdLine = _tcsdup(TEXT("test.exe"));
  GetStartupInfo(&stSi);
  bCreated = CreateProcess(NULL,szCmdLine,NULL,NULL,FALSE,DEBUG_ONLY_THIS_PROCESS,NULL,NULL,&stSi,&stPi);
  if(!bCreated){
    MessageBox(GetActiveWindow(),TEXT("不能打开test.exe文件!"),TEXT("Result"),MB_OK);
    //不能打开test.exe时清除资源
    if(szCmdLine!=NULL)  free(szCmdLine);
    return 0;
  }
  //开始调试运行,等待调试事件发生
  while(WaitForDebugEvent(&stDe,INFINITE)){//有调试事件发生
    switch(stDe.dwDebugEventCode){
      case CREATE_PROCESS_DEBUG_EVENT:
        //写断点(int 3[0xCC])到nAddrOfBreakPoint,之前要保存原始指令
        ReadProcessMemory(stPi.hProcess,(LPVOID)nAddrOfBreakPoint,&cOldByte,1,NULL);
        WriteProcessMemory(stPi.hProcess,(LPVOID)nAddrOfBreakPoint,&cBreakPoint,1,NULL);
        break;
      case EXCEPTION_DEBUG_EVENT:
        if(stDe.u.Exception.ExceptionRecord.ExceptionCode == EXCEPTION_BREAKPOINT){//断点异常
        
        //若不是在nAddrOfBreakPoint处中断,则继续等待
          stThreadContext.ContextFlags = CONTEXT_CONTROL;
          GetThreadContext(stPi.hThread,&stThreadContext);
          if(stThreadContext.Eip!=nAddrOfBreakPoint+1) break;        
        //执行内存补丁
          WriteProcessMemory(stPi.hProcess,(LPVOID)0x00401005,&cPatchByte,1,NULL);
        //恢复nAddrOfBreakPoint原始指令,并重新执行该指令
          WriteProcessMemory(stPi.hProcess,(LPVOID)nAddrOfBreakPoint,&cOldByte,1,NULL);
          stThreadContext.ContextFlags = CONTEXT_FULL;
          GetThreadContext(stPi.hThread,&stThreadContext);
          stThreadContext.Eip = nAddrOfBreakPoint;
          SetThreadContext(stPi.hThread,&stThreadContext);
        }
        break;
      case EXIT_PROCESS_DEBUG_EVENT:  
        bFinished = TRUE;
        
    }
    ContinueDebugEvent(stPi.dwProcessId,stPi.dwThreadId,DBG_CONTINUE);//继续让被调试的程序执行
    if(bFinished)break;
  }
  //破解成功后清除内存资源
  if(szCmdLine!=NULL)  free(szCmdLine);
  CloseHandle(stPi.hThread);
  CloseHandle(stPi.hProcess);
  return 0;
}
其中代码中,判断中断的位置是否是我们自己下断点的地方是很关键的,因为程序会在其它地方也发生断点中断。而如果在那时候处理,原程序还没有在内存中恢复。
    该程序是在VS2008下编译通过的,运行程序,看一下结果:

    本人首次在看雪上发帖,新人难免出些差错,望大家见谅,批评指正,在此先谢过了。
    Upx处理过的Text.exe下载:Test.rar
编译的破解程序在回复贴中,有感兴趣的请下载。