标题:《也谈code a loader》
作者:oep
时间:2005.5.25

一、前言
    还没进入正题前,请先翻看Iczelion的《Win32调试API》,还有nbw翻译的(How to code a loader)。
    KeyMaker做的内存注册机可以得到本机号码,并且必须是明码的,KeyMaker无法运算远程用户电脑里
的注册号。如果能改进Kaymaker生成的代码,让内存注册机算任意程序中的注册号,不是更完美的loader吗?
这样我们就不必要花太多的时间去研究研究软件的算法了,而且现在很多软件中为了防止跟踪和破解他们的
算法,都在程序循环中加入了大量的废循环和多种类型的障碍。
    曾建议过树袋熊要求他在Keymaker中加入这种代码,可以把截取出来的数据为我们自己所调用。但树袋熊
说加入这种代码后错误太多,所以在3年后的今天,我们只有用自己手动构造这种代码了。:(
     另:以前讲loader的文章也很多的,如果觉得我是在说废话,千万不要向我扔西瓜皮吆:)


二、原理
    普通的软件算法都是这样的:
        根据机器号(x)-->经过算法运算f(x)-->得到真实的注册号明码或者暗码
    特别是暗码,在Keymaker中是无法截取的。 
   
      比如以下crackme的代码:
        0040B8DC    > \68>push eProject.0054BA58             ;  ASCII "BCDFGHJKMPQRTVWXY2346789"
        0040B8E1    .  53 push ebx                           ;这里压入机器号码
        0040B8E2    .  56 push esi                           ;
        0040B8E3    .  8D>lea ecx,dword ptr ss:[esp+20]
        0040B8E7    .  51 push ecx
        0040B8E8    .  8D>lea ecx,dword ptr ss:[esp+20]
        0040B8EC    .  E8>call eProject.00409AE0             ;第一次进行大量的算法循环变形
        0040B8F1    .  8B>mov eax,dword ptr ds:[edi+C]       ;运算结果返回EAX中,此处是暗码
        ........
        ........
        ........  
        0040B9FE    .  68>push eProject.0054BA04             ;  ASCII "P6VQJ392XHYRB7GK8MTD4WCF"
        0040BA03    .  50 push eax                           ;上面第一次计算的结果,是暗码
        0040BA04    .  56 push esi                          
        0040BA05    .  8D>lea eax,dword ptr ss:[esp+34]
        0040BA09    .  50 push eax
        0040BA0A    .  8D>lea ecx,dword ptr ss:[esp+20]
        0040BA0E    .  E8>call eProject.00409AE0             ;这里第二次进入大量的循环和变形
        0040BA13    .  50 push eax  ------------------------ ;这里产生正确的注册号码:)


       CS:40BA13时EAX指向堆栈的指针,指针又指向了准确的注册号。所以我们现在要获得EAX指向的
堆栈指针,再把指针数据所指向的数据截获,就可以得到正确的注册号了。
       如果只是简单的得到本机的注册号,不言而喻,那完全可以用KeyMaker截获CS:40BA13,就可以得到
准确的结果了。但我们这里要谈的是:!!!计算远程计算机上的号码!!! 况且,如果CS:40BA13处得到的是暗
码呢? hoho,let's go 


三、实战

      1、程序在运行的时候,都会显示给你机器号的。如果我们在CS:40B8E1处,压入其他电脑上的机器号,
那在CS:40BA13处得到的注册号又会是谁的呢?哈,想到了吗?
       你一定猜测到了:得到的注册号就是CS:40B8E1所压入堆栈的机器号所对应的注册号码。

      2、想法是不错吧?但如何让程序来自动完成呢?不是每个人都能熟练的运用OD和SOFTICE的,假如你
要让一个全不懂电脑的人来操作(呵呵,夸张了),他又怎么知道如何做到呢?那就需要我们自己来写程序,
这就是要打造我们自己的loader。
        在需要构造我们的代码前,有几个API函数必须让你了解的,如果你还不熟练,我建议你去查查
《Microsoft?Win32?Programmer's Reference 》和API手册,他们是:
     CreateProcess ,WriteProcessMemory,ReadProcessMemory,GetThreadContext,SetThreadContext,
     WaitForDebugEvent

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; 核心代码如下(win32汇编语言):
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
.................................
................................. 
invoke GetStartupInfo,addr startinfo 
invoke CreateProcess, addr Crackme, NULL, NULL, NULL, FALSE,\
                      DEBUG_PROCESS or DEBUG_ONLY_THIS_PROCESS, NULL, NULL,\
                      addr startinfo, addr pi 
.while TRUE 
       invoke WaitForDebugEvent, addr DBEvent, INFINITE 
       .if DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
          invoke  WriteProcessMemory,pi.hProcess,040B8E1h,addr newtext,1,NULL
          invoke  WriteProcessMemory,pi.hProcess,040BA13h,addr newtext,1,NULL
          invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId,DBG_CONTINUE 
          .continue
       .elseif DBEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT 
          invoke wsprintf, addr buffer, addr ExitProc, TotalInstruction 
          ;invoke MessageBox, 0, addr buffer, addr AppName, MB_OK+MB_ICONINFORMATION 
          .break 
       .elseif DBEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT
           .if DBEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT 
             mov context.ContextFlags, CONTEXT_FULL 
             invoke GetThreadContext, pi.hThread, addr context 
             .if     context.regEip == 040BA14h
              dec     context.regEip
              invoke  WriteProcessMemory,pi.hProcess,040BA13h,addr oldbyte,1,NULL
              mov eax,context.regEax             
              invoke ReadProcessMemory,pi.hProcess,eax,addr value,4,NULL
              mov eax,value
              invoke ReadProcessMemory,pi.hProcess,eax,addr BUFFER,01Dh,NULL
              invoke SetThreadContext,pi.hThread, addr context  
              invoke  CloseHandle,pi.hProcess
              invoke  CloseHandle,pi.hThread
;             invoke TerminateProcess,pi.hProcess,-1        ;可采用强行退出的办法
              .break                                        ;这里中断循环
            .elseif  context.regEip == 040B8E2h
              dec     context.regEip
              invoke  WriteProcessMemory,pi.hProcess,040B8E1h,addr oldbyte1,1,NULL
              mov eax,context.regEsi
              invoke  WriteProcessMemory,pi.hProcess,eax,addr BUFFER,0Dh,NULL
              invoke SetThreadContext,pi.hThread, addr context  
              invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_CONTINUE 
              .continue 
            .endif           
          .endif 
       .endif 
       invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId,DBG_CONTINUE 
.endw 
          invoke  CloseHandle,pi.hProcess
          invoke  CloseHandle,pi.hThread         ;再次调用CloseHandle,你能明白原因的:)
          ............................
          ............................                                        

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; 上面代码分析如下:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;         

            <1>用DEBUG_PROCESS+DEBUG_ONLY_THIS_PROCESS标志创建我们的Crackme进程

            <2>用WaitForDebugEvent来等待调试事件的发生
                  invoke WaitForDebugEvent, addr DBEvent, INFINITE 

            <3>crackme在入口处会向我们的窗口发送CREATE_PROCESS_DEBUG_EVENT消息,我们在这里
               修改程序的代码 
              .if DBEvent.dwDebugEventCode==CREATE_PROCESS_DEBUG_EVENT
                   invoke  WriteProcessMemory,pi.hProcess,040B8E1h,addr int_3,1,NULL
                   invoke  WriteProcessMemory,pi.hProcess,040BA13h,addr int_3,1,NULL
                   invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId,DBG_CONTINUE 
                   .continue
               就是向CS:40B7BD 和CS:40BA13处写入INT 3断点CC,然后让循环继续进行,等待我们的
               INT3断点发生

            <4> 第一次INT 3断点发生后。我们窗口收到EXCEPTION_DEBUG_EVENT消息,所以我们在这里判断
                是不是到达我们设定的IP地址了?所以调用
             mov context.ContextFlags, CONTEXT_FULL       ;表示全模式读写
             invoke GetThreadContext, pi.hThread, addr context 
                程序首先运行到CS:40B8E1处,中断,此时EIP已经指向了CS:40B8E2,所以dec context.regEip
             就是让Crackme在恢复循环的时候,继续从CS:40B8E1执行。因为此时ESI中指向的地址就是机器号
             所以我们要得到机器号在Crackme中的地址,所以 mov eax,context.regEsi,得到地址后能操作吗?
             答案“是的”,调用invoke  WriteProcessMemory,pi.hProcess,eax,addr BUFFER,0Dh,NULL
              BUFFER中,就是窗口输入的机器号,我们虽然不能通过寄存器直接读写Crackme中的数据,
              但CreateProcess给了我们尚方宝剑,我们可以直接写Crackme的内存,这个内存正是ESI所要压栈
              的内容,此时,0D个字节写入后,我们自己电脑里的机器号,已经替换成其他电脑里的号码了。
              然后用SetThreadContext把EIP的数据写回去,并让Crackme恢复执行。.continue
               .elseif  context.regEip == 040B8E2h
                 dec     context.regEip
                 invoke  WriteProcessMemory,pi.hProcess,040B8E1h,addr oldbyte1,1,NULL ;恢复
                 mov eax,context.regEsi
                 invoke  WriteProcessMemory,pi.hProcess,eax,addr BUFFER,0Dh,NULL
                 invoke SetThreadContext,pi.hThread, addr context                  ;设置生效果  
                invoke ContinueDebugEvent, DBEvent.dwProcessId, DBEvent.dwThreadId, DBG_CONTINUE 
              .continue                                                   ;继续循环

            <5>程序第2次发生INT 3断点的时候,被输入的机器号,正确的执行了算法,得到我们需要的
               结果了,呵呵,估计你已经猜测到了,重复上面<4>的步骤,得到RegEax中的数值,并从内存
               中把这个DWORD读出来,放到EAX中,再次读内存中EAX指向的数据,就是我们所需要的数据了
 
             .if     context.regEip == 040BA14h
              dec     context.regEip
              invoke  WriteProcessMemory,pi.hProcess,040BA13h,addr oldbyte,1,NULL
              mov eax,context.regEax             
              invoke ReadProcessMemory,pi.hProcess,eax,addr value,4,NULL
              mov eax,value
              invoke ReadProcessMemory,pi.hProcess,eax,addr BUFFER,01Dh,NULL
              invoke SetThreadContext,pi.hThread, addr context  
              invoke  CloseHandle,pi.hProcess
              invoke  CloseHandle,pi.hThread                ;第一次关闭程序句炳 
;             invoke TerminateProcess,pi.hProcess,-1        ;可采用强行退出的办法
              .break                                        ;结果已经得到了,中断循环       
                    
        3、剩下的事情你已经知道该做什么了吧?正确的注册号已经读到你自己的BUFFER中了
           想做什么就是你自己的事情了:),还是输入吧,你的窗口不是有个输入框吗?           
四、小结

      我写的很多,不知道是不是已经说明白,我已经力求详尽。
      写完程序后我自己也有很多深思的问题:
      1、被加壳的程序中,KEYMAKER是如何来定位断点的?指令在没释放到内存前,是无法插入CC点的。
         是利用单步跟踪指令,判断EIP为所需要的数值,然后断下来的吗?如果那样,Crackme运行起
         来岂不是特慢?
      2、如果Crackme中刚好有SEH断点来处理INT_3指令呢?如何才能让断点准确的返回到我们的控制端?
         Keymaker中是利用的哪种类型的断点?如果壳中有INT3和INT1的断点,就更无法顺利完成我们的
         程序了。可惜找不到树袋熊了,要不可以问个究竟。



五、后记    
       写到这里就要结束了,把我的思路就当是抛砖引玉吧,就当是对《How to code a loader》的一点
补充吧。献给象我一样摸索中前进的人,写给所有的Cracker。
       如果你有更好的想法,我们可以交流,我的EMAIL:miaomiao226@hotmail.com,转载请保持信息的
完整性,谢谢!


                                             oep
                                             2005.5.25晚