315技术社区's Archiver

咨询客服QQ:604164

敗類 发表于 2008-5-5 21:36

脱壳入门初级教学

标 题: 脱壳基础知识入门及FAQ
作 者: kanxue
现在加解密发展己形成2个分支了,一个就是传统的算法,另一个就是加密壳。越来越多的软件采用了密码学相关算法,现在要做出一个软件注册机己不象前几年那么容易,这就要求解密者必须要有一定的数学功底和密码学知识,而这些在短时间内是不容易掌握的。除了密码学的应用,越来越多的软件加壳了,因此要求解密者必须掌握一些脱壳技术,这就使得壳成了解密必须迈过的一个门槛。壳发展到今天,强度越来越高了,将许多人挡在门外,使得大家望壳兴叹。另外,论坛现在两极分化比较严重,高手讨论的脱壳技术新手看不懂,很多人想学脱壳,但看到壳这么难,只好放弃了,造成新手与高手间一个断档,为了鼓励更多新人加入脱壳的行列,很有必要将壳有关知识总结一下。

敗類 发表于 2008-5-5 21:37

[b][size=3][color=#ff0000]第一课 PE格式[/color][/size][/b]
要想学脱壳,第一步就得掌握PE格式,PE是Portable Executable File Format(可移植的执行体)简写,它是目前Windows平台上的主流可执行文件格式。

Microsoft Visual C++提供的[url=http://bbs.pediy.com/upload/bbs/unpackfaq/WINNTh.rar][b][color=darkblue]WINNT.H[/color][/b][/url]里有PE数据结构的完整定义。
学习PE格式的方法是自己先准备一个十六进制工具,如[url=http://www.pediy.com/tools/Editors.htm][color=darkblue]HexWorkshop[/color][/url],[url=http://www.pediy.com/tools/Editors.htm][color=darkblue]WinHex[/color][/url],用这些工具打开一个EXE文件对照着学。强烈推荐你用[url=http://www.pediy.com/tools/PEtools.htm][color=darkblue]
Stud_PE v.2.2.0.5[/color][/url]这款工具辅助学习PE格式。PE格式学习的重点是在输入表(Import Table)这块。
Stud_PE工具界面:
[attach]1512[/attach]

PE结构图:
[attach]1513[/attach]

[[i] 本帖最后由 敗類 于 2008-5-5 22:02 编辑 [/i]]

敗類 发表于 2008-5-5 21:37

[b][size=3][color=#ff0000]第二课 SEH技术[/color][/size][/b]
SEH  in ASM 研究(一)
                                            By Hume/冷雨飘心

为什么老调重弹:
    SEH出现已绝非一日,但很多人可能还不彻底了解Seh的运行机制;有关seh的知识资料不是很多,asm级的详细资料就
更少!seh不仅可以简化程序错误处理,使你的程序更加健壮,还被广泛应用于反跟踪以及加解密中,因此,了解seh非常必要,
但遗憾的是关于seh详细介绍的中文资料非常少,在实践的基础上,把自己学习的一点笔记奉献给大家,希望对喜欢ASM的朋
友有所帮助.如有错误,请高手不吝指正.

                                    PART  I  简单接触

一、SEH背景知识
SEH("Structured Exception Handling"),即结构化异常处理.是操作系统提供给程序设计者的强有力的处理程序错
误或异常的武器.在VISUAL C++中你或许已经熟悉了_try{} _finally{} 和_try{} _except {} 结构,这些并不是
编译程序本身所固有的,本质上只不过是对windows内在提供的结构化异常处理的包装,不用这些高级语言编译器所提供
的包装 ,照样可以利用系统提供的强大seh处理功能,在后面你将可以看到,用系统本身提供seh结构和规则以及ASM语言,
我们将对SEH的机制以及实现有一个彻底的了解.

发生异常时系统的处理顺序(by Jeremy Gordon):

    1.系统首先判断异常是否应发送给目标程序的异常处理例程,如果决定应该发送,并且目标程序正在被调试,则系统
    挂起程序并向调试器发送EXCEPTION_DEBUG_EVENT消息.呵呵,这不是正好可以用来探测调试器的存在吗?

    2.如果你的程序没有被调试或者调试器未能处理异常,系统就会继续查找你是否安装了线程相关的异常处理例程,如果
    你安装了线程相关的异常处理例程,系统就把异常发送给你的程序seh处理例程,交由其处理.

    3.每个线程相关的异常处理例程可以处理或者不处理这个异常,如果他不处理并且安装了多个线程相关的异常处理例程,
        可交由链起来的其他例程处理.

    4.如果这些例程均选择不处理异常,如果程序处于被调试状态,操作系统仍会再次挂起程序通知debugger.

    5.如果程序未处于被调试状态或者debugger没有能够处理,并且你调用SetUnhandledExceptionFilter安装了最后异
    常处理例程的话,系统转向对它的调用.

    6.如果你没有安装最后异常处理例程或者他没有处理这个异常,系统会调用默认的系统处理程序,通常显示一个对话框,
    你可以选择关闭或者最后将其附加到调试器上的调试按钮.如果没有调试器能被附加于其上或者调试器也处理不了,系统
    就调用ExitProcess终结程序.

    7.不过在终结之前,系统仍然对发生异常的线程异常处理句柄来一次展开,这是线程异常处理例程最后清理的机会.

如果你看了上面的步骤一头雾水的话,别着急,化点时间慢慢理解或者进入下一部分实例操作.

二.初步实战演习:

  安装异常处理句柄.
  
  有两种类型的异常处理句柄,一种是final型的,这是在你的异常未能得到线程相关处理例程处理操作系统在即将关闭程序之前会
  回调的例程,这个例程是进程相关的而不是线程相关的,因此无论是哪个线程发生异常未能被处理,都会调用这个例程.
    I. 见下面的例子1:
;//================================例子1---final型的异常处理=================
;// ex. 1,by Hume,2001,just copy make your own hd.h and compile&link
;//========================================================================
.386
.model flat, stdcall
option casemap :none  ; case sensitive
include hd.h          ;//相关的头文件,你自己维护一个吧
;//============================
.data
szCap    db "By Hume[AfO],2001...",0
szMsgOK db "OK,the exceptoin was handled by final handler!",0
szMsgERR1 db "It would never Get here!",0
buff    db 200 dup(0)

.code
_start:
;//========prog begin====================
    lea    eax,Final_Handler
    invoke    SetUnhandledExceptionFilter,eax  ;//调用SetUnhandledExceptionFilter来安装final SEH
                                                  ;//原型很简单SetUnhandledExceptionFilter proto
                                                  ;//pTopLevelExceptionFilter:DWORD
        xor    ecx,ecx
        mov    eax,200     
        cdq
    div    ecx
                                                  ;//以下永远不会被执行
        invoke    MessageBox,NULL,addr szMsgERR1,addr szCap,MB_OK+MB_ICONEXCLAMATION
        invoke    ExitProcess,NULL
        

;//============================
Final_Handler:
    invoke    MessageBox,NULL,addr szMsgOK,addr szCap,MB_OK+MB_ICONEXCLAMATION
    mov    eax,EXCEPTION_EXECUTE_HANDLER        ;//==1 这时不出现非法操作的讨厌对话框
    ;mov    eax,EXCEPTION_CONTINUE_SEARCH    ;//==0 出现,这时是调用系统默认的异常处理过程,程序被终结了
    ;mov    eax,EXCEPTION_CONTINUE_EXECUTION  ;//==-1 不断出现对话框,你将陷入死循环,可别怪我
    ret                                      ;因为我们并没有修复ecx,所以不断产生异常,然后不断调用这个例程

;//=============================Prog Ends==============
end _start
COMMENT $
  简单来解释几句,windows根据你的异常处理程序的返回值来决定如何进一步处理
        EXCEPTION_EXECUTE_HANDLER            equ 1    表示我已经处理了异常,可以优雅地结束了
        EXCEPTION_CONTINUE_SEARCH            equ 0    表示我不处理,其他人来吧,于是windows调用默认的处理程序
                                                      显示一个错误框,并结束
        EXCEPTION_CONTINUE_EXECUTION        equ -1  表示错误已经被修复,请从异常发生处继续执行
        你可以试着让程序返回0和-1然后编译程序,就会理解我所有苍白无力的语言...
$

;//========================================================================

    II.另一种是per_Thread Exception Handler->线程相关的异常处理,通常每个线程初始化准备好运行时fs指向一个TIB结构
    (THREAD INFORMATION BLOCK),这个结构的第一个元素fs:[0]指向一个_EXCEPTION_REGISTRATION结构
    后面_EXCEPTION_REGISTRATION为了简化,用ERR来代替这个结构...不要说没见过啊...
    fs:[0]->
    _EXCEPTION_REGISTRATION struc
    prev dd ?                      ;前一个_EXCEPTION_REGISTRATION结构
    handler dd ?                  ;异常处理例程入口....呵呵,现在明白该怎么作了吧
    _EXCEPTION_REGISTRATION ends
    我们可以建立一个ERR结构然后将fs:[0]换成指向他的指针,当然最常用的是堆栈,如果你用静态内存区也可以,没有人阻止你
    在asm世界,放心地干吧,除了多S几次之外,通常不会有更大的损失
    把handler域换成你的程序入口,就可以在发生异常时调用你的代码了,好马上实践一下,见例子2
;//========================================================================
;// ex. 2,by Hume,2001  线程相关的异常处理
;//========================================================================
.386
.model flat, stdcall
option casemap :none  ; case sensitive
include hd.h          ;//相关的头文件,你自己维护一个吧
;//============================
.data
szCap    db "By Hume[AfO],2001...",0
szMsgOK db "It's now in the Per_Thread handler!",0
szMsgERR1 db "It would never Get here!",0
buff    db 200 dup(0)

.code
_start:
;//========prog begin====================
  ASSUME FS:NOTHING
        push    offset perThread_Handler
    push    fs:[0]      
        mov    fs:[0],esp                      ;//建立SEH的基本ERR结构,如果不明白,就仔细研究一下吧
        xor    ecx,ecx                          
        mov    eax,200     
        cdq
    div    ecx
                                                  ;//以下永远不会被执行
        invoke    MessageBox,NULL,addr szMsgERR1,addr szCap,MB_OK+MB_ICONINFORMATION
        pop    fs:[0]
        add    esp,4
        invoke    ExitProcess,NULL        

;//============================
perThread_Handler:
        invoke    MessageBox,NULL,addr szMsgOK,addr szCap,MB_OK+MB_ICONINFORMATION
        mov    eax,1          ;//ExceptionContinueSearch,不处理,由其他例程或系统处理
        ;mov    eax,0          ;//ExceptionContinueExecution,表示已经修复CONTEXT,可从异常发生处继续执行
    ret                        ;//这里如果返回0,你会陷入死循环,不断跳出对话框....

;//=============================Prog Ends==============
end _start
COMMENT $
  嘿嘿,这个简单吧,我们由于没有足够的资料,暂时还不能修复ecx的值使之从异常发生处继续执行,只是简单显示一个MSG,然后
  让系统处理,自然跳出讨厌的对话框了....
  注意和final返回值的含义不同...
$
;//==================================================================================================

好像到此为止,我们并没有从异常处理中得到任何好处,除了在异常发生后可以执行一点我们微不足道的代码,事实上SEH可以修复这些
异常或者干我们想干的事情然后从希望的地方继续执行,嘿嘿,很爽吧,可惜我们没有足够的信息,那里找到我们所需要的信息?
三、传递给异常处理句柄的参数

      要想明白seh处理例程如何得到感兴趣的信息,首先要明白SEH的作用机制.事实上,当异常
发生时,系统给了我们一个处理异常的机会,他首先会调用我们自定义的seh处理例程,当然也包括
了相关信息,在调用之前,系统把包含这些信息结构的指针压入stack,供我们的异常处理例程调用,
传递给例程的参数通常是四个,其中只有三个有明确意义,另一个到现在为止还没有发现有什么作用,
这些参数是:pExcept:DWORD,pErr:DWORD,pContext:DWORD,pDispatch意义如下:
pExcept:  ---  EXCEPTION_RECORD结构的指针
pErr:    ---  前面ERR结构的指针
pContext: --- CONTEXT结构的指针
pDispatch:---没有发现有啥意义

    ERR结构是前面介绍的_EXCEPTION_REGISTRATION结构,往前翻翻,Dispatch省略,下面介绍
EXCEPTION_RECORD和CONTEXT结构的定义:
  
;//================================以下是两个成员的详细结构========================================

        EXCEPTION_RECORD STRUCT
          ExceptionCode        DWORD      ?      ;//异常码
          ExceptionFlags        DWORD      ?      ;//异常标志
          pExceptionRecord      DWORD      ?      ;//指向另外一个EXCEPTION_RECORD的指针
          ExceptionAddress      DWORD      ?      ;//异常发生的地址
          NumberParameters      DWORD      ?      ;//下面ExceptionInformation所含有的dword数目
          ExceptionInformation  DWORD EXCEPTION_MAXIMUM_PARAMETERS dup(?)
        EXCEPTION_RECORD ENDS                      ;//EXCEPTION_MAXIMUM_PARAMETERS ==15

;//================================具体解释========================================

ExceptionCode 异常类型,SDK里面有很多类型,你可以在windows.inc里查找STATUS_来找到更多
的异常类型,下面只给出hex值,具体标识定义请查阅windows.inc,你最可能遇到的几种类型如下:

              C0000005h----读写内存冲突
              C0000094h----非法除0
              C00000FDh----堆栈溢出或者说越界
              80000001h----由Virtual Alloc建立起来的属性页冲突
              C0000025h----不可持续异常,程序无法恢复执行,异常处理例程不应处理这个异常
              C0000026h----在异常处理过程中系统使用的代码,如果系统从某个例程莫名奇妙的返回,则出现此代码,
                          如果RtlUnwind时没有Exception Record参数也同样会填入这个代码
              80000003h----调试时因代码中int3中断
              80000004h----处于被单步调试状态

              注:也可以自己定义异常代码,遵循如下规则:
              _____________________________________________________________________+

              位:      31~30            29~28          27~16          15~0
              _____________________________________________________________________+
              含义:    严重程度          29位            功能代码        异常代码
                        0==成功        0==Mcrosoft    MICROSOFT定义  用户定义
                        1==通知        1==客户
                        2==警告          28位
                        3==错误        被保留必须为0
ExceptionFlags 异常标志
              0----可修复异常
              1----不可修复异常
              2----正在展开,不要试图修复什么,需要的话,释放必要的资源
pExceptionRecord 如果程序本身导致异常,指向那个异常结构
ExceptionAddress 发生异常的eip地址
ExceptionInformation 附加消息,在调用RaiseException可指定或者在异常号为C0000005h即内存异常时含义如下
              第一个dword 0==读冲突 1==写冲突
              第二个dword 读写冲突地址
;//================================解释结束========================================
                                                          off.
        CONTEXT STRUCT                    ; _
          ContextFlags  DWORD      ?      ;  |            +0
          iDr0          DWORD      ?      ;  |            +4
          iDr1          DWORD      ?      ;  |            +8
          iDr2          DWORD      ?      ;  >调试寄存器  +C
          iDr3          DWORD      ?      ;  |            +10
          iDr6          DWORD      ?      ;  |            +14
          iDr7          DWORD      ?      ; _|            +18
          FloatSave    FLOATING_SAVE_AREA <>  ;浮点寄存器区 +1C~~~88h
          regGs        DWORD      ?      ;--|            +8C
          regFs        DWORD      ?      ;  |\段寄存器    +90  
          regEs        DWORD      ?      ;  |/            +94            
          regDs        DWORD      ?      ;--|            +98
          regEdi        DWORD      ?      ;____________    +9C
          regEsi        DWORD      ?      ;      |  通用  +A0
          regEbx        DWORD      ?      ;      |  寄    +A4
          regEdx        DWORD      ?      ;      |  存    +A8
          regEcx        DWORD      ?      ;      |  器    +AC
          regEax        DWORD      ?      ;_______|___组_  +B0      
          regEbp        DWORD      ?      ;++++++++++++++++ +B4
          regEip        DWORD      ?      ;    |控制        +B8
          regCs        DWORD      ?      ;    |寄存        +BC
          regFlag      DWORD      ?      ;    |器组        +C0
          regEsp        DWORD      ?      ;    |            +C4
          regSs        DWORD      ?      ;++++++++++++++++ +C8
          ExtendedRegisters db MAXIMUM_SUPPORTED_EXTENSION dup(?)
        CONTEXT ENDS
;//================================以上是两个成员的详细结构========================================

   
        I、传递给final句柄的参数,只有两个可描述为EXCEPTION_POINTERS结构,定义如下:
            EXCEPTION_POINTERS STRUCT
              pExceptionRecord  DWORD      ?              
              ContextRecord    DWORD      ?
            EXCEPTION_POINTERS ENDS

            在call xHandler之前,堆栈结构如下:
            esp    -> *EXCEPTION_RECORD
            esp+4  -> *CONTEXT record  ;//具体结构见下面

                然后执行call _Final_Handler,这样在程序里要调用什么不轻而易举了吗?


   
    II、 传递给per_thread句柄的参数,如下:
    在call xHandler之前,在堆栈中形成如下结构
            esp    -> *EXCEPTION_RECORD
            esp+4  -> *ERR                    ;//注意这也就是fs:[0]的指向
            esp    -> *CONTEXT record          ;//point to registers
            esp    -> *Param                    ;//呵呵,没有啥意义

          然后执行 call _Per_Thread_Handler

调用handler的原型是这样
        invoke HANDLER,*EXCEPTION_RECORD,*_EXCEPTION_REGISTRATION,*CONTEXT record,*Param
        即编译代码如下:
        PUSH *Param                    ;//通常不重要,没有什么意义
        push *CONTEXT record            ;//上面的结构
        push *ERR                      ;//the struc above
        push *EXCEPTION_RECORD          ;//see above
        CALL HANDLER
        ADD ESP,10h


好现在你明白了应该如何访问具体有关系统信息的细节了吧,下一部分,让我们来看看如何应用三、传递给异常处理句柄的参数

      要想明白seh处理例程如何得到感兴趣的信息,首先要明白SEH的作用机制.事实上,当异常
发生时,系统给了我们一个处理异常的机会,他首先会调用我们自定义的seh处理例程,当然也包括
了相关信息,在调用之前,系统把包含这些信息结构的指针压入stack,供我们的异常处理例程调用,
传递给例程的参数通常是四个,其中只有三个有明确意义,另一个到现在为止还没有发现有什么作用,
这些参数是:pExcept:DWORD,pErr:DWORD,pContext:DWORD,pDispatch意义如下:
pExcept:  ---  EXCEPTION_RECORD结构的指针
pErr:    ---  前面ERR结构的指针
pContext: --- CONTEXT结构的指针
pDispatch:---没有发现有啥意义

    ERR结构是前面介绍的_EXCEPTION_REGISTRATION结构,往前翻翻,Dispatch省略,下面介绍
EXCEPTION_RECORD和CONTEXT结构的定义:
  
;//================================以下是两个成员的详细结构========================================

        EXCEPTION_RECORD STRUCT
          ExceptionCode        DWORD      ?      ;//异常码
          ExceptionFlags        DWORD      ?      ;//异常标志
          pExceptionRecord      DWORD      ?      ;//指向另外一个EXCEPTION_RECORD的指针
          ExceptionAddress      DWORD      ?      ;//异常发生的地址
          NumberParameters      DWORD      ?      ;//下面ExceptionInformation所含有的dword数目
          ExceptionInformation  DWORD EXCEPTION_MAXIMUM_PARAMETERS dup(?)
        EXCEPTION_RECORD ENDS                      ;//EXCEPTION_MAXIMUM_PARAMETERS ==15

;//================================具体解释========================================

ExceptionCode 异常类型,SDK里面有很多类型,你可以在windows.inc里查找STATUS_来找到更多
的异常类型,下面只给出hex值,具体标识定义请查阅windows.inc,你最可能遇到的几种类型如下:

              C0000005h----读写内存冲突
              C0000094h----非法除0
              C00000FDh----堆栈溢出或者说越界
              80000001h----由Virtual Alloc建立起来的属性页冲突
              C0000025h----不可持续异常,程序无法恢复执行,异常处理例程不应处理这个异常
              C0000026h----在异常处理过程中系统使用的代码,如果系统从某个例程莫名奇妙的返回,则出现此代码,
                          如果RtlUnwind时没有Exception Record参数也同样会填入这个代码
              80000003h----调试时因代码中int3中断
              80000004h----处于被单步调试状态

              注:也可以自己定义异常代码,遵循如下规则:
              _____________________________________________________________________+

              位:      31~30            29~28          27~16          15~0
              _____________________________________________________________________+
              含义:    严重程度          29位            功能代码        异常代码
                        0==成功        0==Mcrosoft    MICROSOFT定义  用户定义
                        1==通知        1==客户
                        2==警告          28位
                        3==错误        被保留必须为0
ExceptionFlags 异常标志
              0----可修复异常
              1----不可修复异常
              2----正在展开,不要试图修复什么,需要的话,释放必要的资源
pExceptionRecord 如果程序本身导致异常,指向那个异常结构
ExceptionAddress 发生异常的eip地址
ExceptionInformation 附加消息,在调用RaiseException可指定或者在异常号为C0000005h即内存异常时含义如下
              第一个dword 0==读冲突 1==写冲突
              第二个dword 读写冲突地址
;//================================解释结束========================================
                                                          off.
        CONTEXT STRUCT                    ; _
          ContextFlags  DWORD      ?      ;  |            +0
          iDr0          DWORD      ?      ;  |            +4
          iDr1          DWORD      ?      ;  |            +8
          iDr2          DWORD      ?      ;  >调试寄存器  +C
          iDr3          DWORD      ?      ;  |            +10
          iDr6          DWORD      ?      ;  |            +14
          iDr7          DWORD      ?      ; _|            +18
          FloatSave    FLOATING_SAVE_AREA <>  ;浮点寄存器区 +1C~~~88h
          regGs        DWORD      ?      ;--|            +8C
          regFs        DWORD      ?      ;  |\段寄存器    +90  
          regEs        DWORD      ?      ;  |/            +94            
          regDs        DWORD      ?      ;--|            +98
          regEdi        DWORD      ?      ;____________    +9C
          regEsi        DWORD      ?      ;      |  通用  +A0
          regEbx        DWORD      ?      ;      |  寄    +A4
          regEdx        DWORD      ?      ;      |  存    +A8
          regEcx        DWORD      ?      ;      |  器    +AC
          regEax        DWORD      ?      ;_______|___组_  +B0      
          regEbp        DWORD      ?      ;++++++++++++++++ +B4
          regEip        DWORD      ?      ;    |控制        +B8
          regCs        DWORD      ?      ;    |寄存        +BC
          regFlag      DWORD      ?      ;    |器组        +C0
          regEsp        DWORD      ?      ;    |            +C4
          regSs        DWORD      ?      ;++++++++++++++++ +C8
          ExtendedRegisters db MAXIMUM_SUPPORTED_EXTENSION dup(?)
        CONTEXT ENDS
;//================================以上是两个成员的详细结构========================================

   
        I、传递给final句柄的参数,只有两个可描述为EXCEPTION_POINTERS结构,定义如下:
            EXCEPTION_POINTERS STRUCT
              pExceptionRecord  DWORD      ?              
              ContextRecord    DWORD      ?
            EXCEPTION_POINTERS ENDS

            在call xHandler之前,堆栈结构如下:
            esp    -> *EXCEPTION_RECORD
            esp+4  -> *CONTEXT record  ;//具体结构见下面

                然后执行call _Final_Handler,这样在程序里要调用什么不轻而易举了吗?


   
    II、 传递给per_thread句柄的参数,如下:
    在call xHandler之前,在堆栈中形成如下结构
            esp    -> *EXCEPTION_RECORD
            esp+4  -> *ERR                    ;//注意这也就是fs:[0]的指向
            esp    -> *CONTEXT record          ;//point to registers
            esp    -> *Param                    ;//呵呵,没有啥意义

          然后执行 call _Per_Thread_Handler

调用handler的原型是这样
        invoke HANDLER,*EXCEPTION_RECORD,*_EXCEPTION_REGISTRATION,*CONTEXT record,*Param
        即编译代码如下:
        PUSH *Param                    ;//通常不重要,没有什么意义
        push *CONTEXT record            ;//上面的结构
        push *ERR                      ;//the struc above
        push *EXCEPTION_RECORD          ;//see above
        CALL HANDLER
        ADD ESP,10h


好现在你明白了应该如何访问具体有关系统信息的细节了吧,下一部分,让我们来看看如何应用
part 4  关于异常处理的嵌套和堆栈展开

    在实际程序设计过程中,不可能只有一个异常处理例程,这就产生了异常处理程序嵌套的问题,可能很多
处理例程分别监视若干子程序并处理其中某种异常,另外一个监视所有子程序可能产生的共性异常,这作
起来实际很容易,也方便调试.你只要依次建立异常处理框架就可以了.
    关于VC++异常处理可以嵌套很多人可能比较熟悉,用起来更容易不过实现比这里也就复杂得多,在VC++
中一个程序所有异常只指向一个相同的处理句例程,然后在这个处理例程里再实现对各个子异常处理例程的
调用,他的大致方法是建立一个子异常处理例程入口的数组表,然后根据指针来调用子处理例程,过程比较烦
琐,原来打算大致写一点,现在发现自己对C/C++了解实在太少,各位有兴趣还是自己
参考MSDN Matt Pietrek 1996年写的一篇文章<
ctured Exception Handling>>,里面有非常详细的说明,对于系统的实现细节也有所讨论,不过相信很多
人都没有兴趣.hmmm...:)实际上Kernel的异常处理过程和VC++的很相似.

    有异常嵌套就涉及到异常展开的问题,也许你注意到了如果按照我前面的例子包括final都不处理异常的话,
最后系统在终结程序之前会来一次展开,在试验之后发现,展开不会调用final只是对per_thread例程展开
(right?).什么是堆栈展开?为什么要进行堆栈展开?如何进行堆栈展开?
   
    我曾经为堆栈展开迷惑过,原因是各种资料的描述很不一致,Matt Pietrek说展开后前面的ERR结构被释放,
并且好像链后面如果决定处理必须展开,很多C/C++讲述异常处理的书也如斯说这使人很迷惑,我们再来看看Jeremy
Gordon的描述,堆栈展开是处理异常的例程自愿进行的.呵呵,究竟事实如何?
   
    在迷惑好久之后我终于找到了答案:Matt Pietrek讲的没有错,那是VC++以及系统kernel的处理方法,Jeremy
Gordon说的也是正确的,那是我门asm Fans的自由!

    好了现在来说堆栈展开,堆栈展开是异常处理例程在决定处理某个异常的时候给前面不处理这个异常的处理
例程的一个清洗的机会,前面拒绝处理这个异常的例程可以释放必要的句柄对象或者释放堆栈或者干点别的工作...
那完全是你的自由,叫stack unwind似乎有点牵强.堆栈展开有一个重要的标志就是
EXCEPTION_RECORD.ExceptionFlag为2,表示正在展开,你可以进行相应的处理工作,但实际上经常用的是6这是
因为还有一个UNWIND_EXIT什么的,具体含义我也没有搞明白,不过对我们的工作好像没有什么影响.

  注意在自己的异常处理例程中,unwind不是自动的,必须你自己自觉地引发,如果所有例程都不处理系统最后的
展开是注定的,当然如果没有必要你也可以不展开.
  win32提供了一个api RtlUnwind来引发展开,如果你想展开一下,就调用这个api吧,少候讲述自己代码如何展开

RtlUnwind调用描述如下:
        PUSH Return value        ;返回值,一般不用
        PUSH pExceptionRecord    ;指向EXCEPTION_RECORD的指针
        PUSH OFFSET CodeLabel    ;展开后从哪里执行
        PUSH LastStackFrame      ;展开到哪个处理例程终止返回,通常是处理异常的Err结构
        CALL RtlUnwind
调用这个api之前要注意保护ebx,esi和edi,否则...嘿嘿

MASM格式如下:        
        invoke RtlUnwind,pFrame,OFFSET return_code_Address,pExceptionRecord,Return_value

这样在展开的时候,就以pExceptionRecord.flag=2 依次调用前面的异常处理例程,到决定异常的处理例程停止,
Jeremy Gordon手动展开代码和我下面的例子有所不同.他描述最后决定处理异常的ERR结构的prev成员为-1,好像
我的结果和他的有所差异,因此采用了另外的方法,具体看下面的例子.
   
  最后一点要注意在嵌套异常处理程序的时候要注意保存寄存器,否则你经常会得到系统异常代码为C00000027h
的异常调用,然后就是被终结.

  一下给出一点垃圾代码演示可能有助于理解,注意link的时候要加入 /section:.text,RWE 否则例子里面的
代码段不能写,SMC功能会产生异常以致整个程序不能进行.

注意:2K/XP下非法指令异常的代码不一致,另外用下面的方法SMC代码段也不可以!不知如何解决?
只用于9X,为了在2k/Xp下也能运行我加了点代码,有兴趣看看,另外帮我解决一下2K/Xp下SMC的问题?thx!

    下面例子很烂,不过MASM格式写起来容易一点,也便于理解.
;-----------------------------------------
;Ex4,演示堆栈展开和异常嵌套处理 by Hume,2002
;[email=humewen@21cn.com]humewen@21cn.com[/email]
;hume.longcity.net
;-----------------------------------------
.586
.model flat, stdcall
option casemap :none  ; case sensitive
include hd.h
include mac.h

;;--------------
per_xHandler1        proto C :DWORD,:DWORD,:DWORD,:DWORD
per_xHandler2        proto C :DWORD,:DWORD,:DWORD,:DWORD
per_xHandler3        proto C :DWORD,:DWORD,:DWORD,:DWORD
;-----------------------------------------

.data
sztit  db "except Mess,by hume[AfO]",0
count  dd 0,0
Expt1_frm  dd 0                    ;ERR结构指针,用于堆栈展开手动代码
Expt2_frm  dd 0
Expt3_frm  dd 0
        
;;-----------------------------------------
    .CODE
_Start:
        assume  fs:nothing
        push    offset per_xHandler3
        push    fs:[0]
        mov    fs:[0],esp
        mov    Expt3_frm,esp

        push    offset per_xHandler2
        push    fs:[0]
        mov    fs:[0],esp
        mov    Expt2_frm,esp

        push    offset per_xHandler1
        push    fs:[0]
        mov    fs:[0],esp
        mov    Expt1_frm,esp
        ;--------------------------
        ;install xhnadler
        ;-----------------------------------------
        
        xor    ebx,ebx
        mov    eax,200
        cdq
        div    ebx                  ;除法错误

        invoke    MessageBox,0,ddd("Good,divide overflow was solved!"),addr sztit,40h

        sub    eax,eax
        mov    [eax],ebx            ;内存写错误

succ:
        invoke    MessageBox,0,ddd("Good,memory write violation solved!"),addr sztit,40h
        
        db      0F0h,0Fh,0C7h,0C8h  ;什么cmpchg8b指令的非法形式?我从来没有成功过!!
                                    ;演示程序中使用seh实现SMC技术,加密??...
        invoke    MessageBox,0,ddd("illeagal instruction was solved!"),addr sztit,20h
        ;--------------------------
        ;uninstall xhnadler
        ;-----------------------------------------

        pop    fs:[0]            
        add    esp,4
        pop    fs:[0]            
        add    esp,4
      ;或者add esp,10h
      
        pop    fs:[0]            
        add    esp,4

    invoke    ExitProcess,0
;-----------------------------------------        
;异常处理句柄1,处理除法异常错误
per_xHandler1 PROC C pExcept:DWORD,pFrame:DWORD,pContext:DWORD,pDispatch:DWORD
        pushad
        MOV    ESI,pExcept
ASSUME  ESI:pTR EXCEPTION_RECORD
        TEST    [ESI].ExceptionFlags,1
        JNZ    @cantdo1
        TEST    [ESI].ExceptionFlags,6
        JNZ    @unwind1
        CMP    [ESI].ExceptionCode,0C0000094h
        JNZ    @cantdo1
        MOV    EDI,pContext

ASSUME  EDI:pTR CONTEXT
        m2m    [edi].regEbx,20            ;将ebx置20,修复除法错误,继续执行
        popad
    MOV  EAX, ExceptionContinueExecution
        RET

@unwind1:
        invoke    MessageBox,0,CTEXT("state: unwinding in xhandler1..."),addr sztit,0
@cantdo1:
        popad
        MOV    EAX,ExceptionContinueSearch
    RET
per_xHandler1 ENDP
;-----------------------------------------
;异常处理句柄2,处理内存写错误,扩展可以有其他的例子如自动扩充堆栈
per_xHandler2 PROC C pExcept:DWORD,pFrame:DWORD,pContext:DWORD,pDispatch:DWORD
        
        pushad
    MOV    ESI,pExcept
ASSUME  ESI:pTR EXCEPTION_RECORD
        MOV    EDI,pContext
ASSUME  EDI:pTR CONTEXT

        call    Dispcont                            ;显示一点lame的消息,自己调试用

        TEST    [ESI].ExceptionFlags,1
        JNZ    @cantdo2
        TEST    [ESI].ExceptionFlags,6
        JNZ    @unwind2
        CMP    [ESI].ExceptionCode,0C0000005h
        JNZ    @cantdo2
        .data                                        ;ASM的数据定义灵活性,如果需要这是可以的
        validAddress dd 0
        .code
        
        m2m    [EDI].regEax,<offset  validAddress>    ;置eax为有效地址      
        popad
    MOV  EAX, ExceptionContinueExecution
        RET

@unwind2:
        invoke    MessageBox,0,CTEXT("hmmm... unwinding in xhandler2..."),addr sztit,40h
@cantdo2:
        popad
        MOV    EAX,ExceptionContinueSearch
    RET
per_xHandler2 ENDP
;-----------------------------------------

per_xHandler3 PROC C pExcept:DWORD,pFrame:DWORD,pContext:DWORD,pDispatch:DWORD
    pushad
        MOV    ESI,pExcept
ASSUME  ESI:pTR EXCEPTION_RECORD
        MOV    EDI,pContext
ASSUME  EDI:pTR CONTEXT      

        TEST    [ESI].ExceptionFlags,1
        JNZ    @cantdo3
        TEST    [ESI].ExceptionFlags,6
        JNZ    @unwind3
        ;-----------------------------------------
                                                   
        push    ecx
        mov    ecx,cs
        xor    cl,cl
        jecxz  win2k_Xp        
win9X:
        pop    ecx
        CMP    [ESI].ExceptionCode,0C000001DH      ;非法指令异常,与2K/XP下的不一致
        JNZ    @cantdo3
        jmp    ok_here
win2k_Xp:
        pop    ecx                                  ;注意,只有在9X下才可以
        CMP    [ESI].ExceptionCode,0C000001EH      ;非法指令异常->2K/XP
        JNZ    @cantdo3                            ;sMc不成

        mov    [edi].regEip,offset safereturn
        popad
        mov    eax,0
        ret
        
        
        
        push    ebx
        push    esi
        push    edi      
comment $ 调用RtlUnwind展开堆栈        
        lea    ebx,unwindback
        invoke    RtlUnwind,Expt3_frm,ebx,esi,0
        $
        mov    dword ptr [esi+4],2                    ;置展开标志,准备展开,这里是
                                                      ;手动代码
        mov    ebx,fs:[0]
        

selfun:
        ;mov    eax,Expt2_frm                    ;这里显示了ASM手动展开的灵活性
        mov    eax,Expt3_frm                     
        cmp    ebx,eax                            ;按照Jeremy Gordon的好像不大对头
        ;cmp    dword ptr [ebx],-1                ;这样好像有问题,只好如上,请教答案         
        jz      unwindback
        push    ebx
        push    esi                                ; 压入Err和Exeption_registration结构
        call    dword ptr[ebx+4]
        add    esp,8
        mov    ebx,[ebx]
        jmp    selfun

unwindback:
        invoke    MessageBox,0,CTEXT("I am Back!"),addr sztit,40h
        pop    edi
        pop    esi
        pop    ebx                                  ;一定要保存这三个寄存器!

        MOV    EAX,[EDI].regEip
        MOV    DWORD PTR[EAX],90909090H            ;改为nop指令...SMC呵呵这次不神秘了吧
                                                    ;SMC注意连接选项
        popad
    MOV  EAX, ExceptionContinueExecution
        RET

@unwind3:
        invoke    MessageBox,0,CTEXT("Note... unwinding in xhandler3..."),addr sztit,40h
@cantdo3:
        popad
        MOV    EAX,ExceptionContinueSearch
    RET
per_xHandler3 ENDP
;-----------------------------------------
;lame routine for debug
Dispcont    proc
                inc    count
                call    dispMsg
                ret
Dispcont    endp

dispMsg    proc
        local szbuf[200]:byte
        pushad
        mov    eax,dword ptr[esi]
        mov    ebx,dword ptr[esi+4]
        mov    ecx,dword ptr[edi+0b8h]
        mov    edx,dword ptr[edi+0a4h]
        .data
        fmt    db "Context eip--> %8X  ebx--> %8X ",0dh,0ah
                db "Flags  Ex.c-> %8x  flg--> %8X",0dh,0ah
                db "it's the %d times xhandler was called!",0
        .code
        invoke    wsprintf,addr szbuf,addr fmt,ecx,edx,eax,ebx,count
        invoke    MessageBox,0,addr szbuf,CTEXT("related Mess of context"),0
        popad
    ret
dispMsg    endp

;;------------------------------------------------
END    _Start
;---------------------------------下面是上面用到的宏,我的mac.h比较长,就不贴了-----
    ddd    MACRO Text                        ;define data in .data section
        local name                  ;This and other can be used as: ddd("My god!")
        .data                  ;isn't cool?
            name    db Text,0
        .code
        EXITM <addr name>  
    ENDM

  CTEXT MACRO y:VARARG                    ;This is a good macro
        LOCAL sym
    CONST segment
        IFIDNI <y>,<>
            sym db 0
        ELSE
            sym db y,0
        ENDIF
    CONST ends
        EXITM <OFFSET sym>
    ENDM

    m2m MACRO M1, M2                          ;mov is too boring sometimes!
      push M2
      pop  M1
    ENDM   
;-----------------------------------------
  最后更正一点前面介绍的传送给final型的参数是指向EXCEPTION_POINTERS 的指针,压栈前的堆栈
是如下的,不好意思,原来写的时候我也没深入研究,可能模糊了一点,如有错误,请大家指正
push    ptEXCEPTION_POINTERS
call    xHandler
下面补充一个final参数获得的一个例子
;--------------------------------------------
; Ex5,演示final处理句柄的参数获取,更正前面
; 模糊的介绍
;--------------------------------------------
.586
.model flat, stdcall
option casemap :none  ; case sensitive
include hd.h
include mac.h

;;--------------
.data
sztit  db "exceptION MeSs,by hume[AfO]",0
fmt    db "Context eip--> %8X  ebx--> %8X ",0dh,0ah
        db "Flags  Ex.c-> %8x  flg--> %8X",0
szbuf  db 200 dup(0)
;;-----------------------------------------
    .CODE
_Start:
        assume  fs:nothing
        push    offset _final_xHandler0
        call    SetUnhandledExceptionFilter
        xor    ebx,ebx
        mov    eax,200
        cdq
        div    ebx
        invoke    MessageBox,0,ddd("Good,divide overflow was solved!"),addr sztit,40h
        xor    eax,eax
        mov    [eax],ebx
        
    invoke    ExitProcess,0   

;-----------------------------------------
_final_xHandler0:
        push    ebp
        mov    ebp,esp
        
        mov    eax,[ebp+8]      ;the pointer to EXCEPTION_POINTERS
        mov    esi,[eax]        ;pointer to _EXCEPTION_RECORD
        mov    edi,[eax+4]      ;pointer to _CONTEXT
        test    dword ptr[esi+4],1  
        jnz    @_final_cnotdo
        test    dword ptr[esi+4],6  
        jnz    @_final_unwind

        ;call    dispMsg
        

        cmp    dword ptr[esi],0c0000094h
        jnz    @_final_cnotdo

        mov    dword ptr [edi+0a4h],10
        call    dispMsg
      
        mov    eax,EXCEPTION_CONTINUE_EXECUTION      ;GO ON
        jmp    @f

@_final_unwind:
        invoke    MessageBox,0,CTEXT("state:In final unwind..."),addr sztit,0
                                          ;好像不论处理不处理异常,系统展开的时候
                                          ;都不会被调用,right?
@_final_cnotdo:                            ;请教是真的吗?还是我写的有问题
        mov    eax,EXCEPTION_CONTINUE_SEARCH
        jmp    @f        
@@:      
        mov    esp,ebp
        pop    ebp
        ret
;-----------------------------------------
dispMsg    proc
        pushad
        mov    eax,[esi]
        mov    ebx,[esi+4]
        mov    ecx,[edi+0b8h]
        mov    edx,[edi+0a4h]
        invoke    wsprintf,addr szbuf,addr fmt,ecx,edx,eax,ebx
        invoke    MessageBox,0,addr szbuf,CTEXT("related Mess of context"),0
        popad
    ret
dispMsg    endp
;;------------------------------------------------
        
END    _Start
;====================================================================================

BTW:够长了吧,基本内容介绍完毕,更多内容下一部分介绍一点利用Seh的tricks,哪位大侠有什么好的想法
或者有什么错误,请不吝指正,毕竟我是菜鸟吗...

敗類 发表于 2008-5-5 21:39

[b][size=3][color=#ff0000]第三课 认识壳[/color][/size][/b]
[b]1. 什么是壳?[/b]

   在一些计算机软件里也有一段专门负责保护软件不被非法修改或反编译的程序。它们一般都是先于程序运行,拿到控制权,然后完成它们保护软件的任务。由于这段程序和自然界的壳在功能上有很多相同的地方,基于命名的规则,就把这样的程序称为“壳”了。
描述壳的示意图:
[attach]1514[/attach]
[b]2. 壳的加载过程[/b]

    这里谈的加壳工具不是WinZIP、WinRAR等数据压缩工具,而是谈压缩可执行文件EXE或DLL的工具。加壳过的EXE文件是可执行文件,它可以同正常的EXE文件一样执行。用户执行的实际上是外壳程序,这个外壳程序负责把用户原来的程序在内存中解压缩,并把控制权交还给解开后的真正程序,这一切工作都是在内存中运行的,整个过程对用户是透明的。
   壳和病毒在某些方面比较类似,都需要比原程序代码更早的获得控制权。壳修改了原程序的执行文件的组织结构,从而能够比原程序的代码提前获得控制权,并且不会影响原程序的正常运行。
   这里简单说说一般壳的装载过程。(参考了Ljtt以前写过的一篇文章)
   1)获取壳自己所需要使用的API地址
   如果用PE编辑工具查看加壳后的文件,会发现未加壳的文件和加壳后的文件的输入表不一样,加壳后的输入表一般所引入的DLL和API函数很少,甚至只有Kernel32.dll以及GetProcAddress这个API函数。
   壳实际上还需要其他的API函数来完成它的工作,为了隐藏这些API,它一般只在壳的代码中用显式链接方式动态加载这些API函数   2)解密原程序的各个区块(Section)的数据
   壳出于保护原程序代码和数据的目的,一般都会加密原程序文件的各个区块。在程序执行时外壳将会对这些区块数据解密,以让程序能正常运行。 壳一般按区块加密的,那么在解密时也按区块解密,并且把解密的区块数据按照区块的定义放在合适的内存位置。
   如果加壳时用到了压缩技术,那么在解密之前还有一道工序,当然是解压缩。这也是一些壳的特色之一,比如说原来的程序文件未加壳时1~2M大小,加壳后反而只有几百K。
   3)重定位
   文件执行时将被映像到指定内存地址中,这个初始内存地址称为基地址(ImageBase)。当然这只是程序文件中声明的,程序运行时能够保证系统一定满足其要求吗?
   对于EXE的程序文件来说,Windows系统会尽量满足。例如某EXE文件的基地址为0x400000,而运行时Windows系统提供给程序的基地址也同样是0x400000。在这种情况下就不需要进行地址“重定位”了。由于不需要对EXE文件进行“重定位”,所以加壳软件把原程序文件中用于保存重定位信息的区块干脆也删除了,这样使得加壳后的文件更加小巧。有些工具提供“Wipe Reloc”的功能,其实就是这个作用。
   不过对于DLL的动态链接库文件来说,Windows系统没有办法保证每一次DLL运行时提供相同的基地址。这样“重定位”就很重要了,此时壳中也需要提供进行“重定位”的代码,否则原程序中的代码是无法正常运行起来的。从这点来说,加壳的DLL比加壳的EXE更难修正。
   4)HOOK-API
   程序文件中的输入表的作用是让Windows系统在程序运行时提供API的实际地址给程序使用。在程序的第一行代码执行之前,Windows系统就完成了这个工作。
   壳一般都修改了原程序文件的输入表,然后自己模仿Windows系统的工作来填充输入表中相关的数据。在填充过程中,外壳就可填充HOOK-API的代码的地址,这样就可间接地获得程序的控制权。
   5)跳转到程序原入口点(OEP)
    从这个时候起壳就把控制权交还给原程序了,一般的壳在这里会有明显的一个“分界线”。但现在的猛壳己没这界限了,壳里有肉,肉里有壳。

[attach]1515[/attach]
[b]3. 压缩引擎[/b]   
   
    各类加壳软件,其压缩算法一般不是自己实现的,大多是调用其他的压缩引擎。目前压缩引擎种类比较多,不同的压缩引擎有不同特点,如一些对图像压缩效果好,一些对数据压缩效果好。而加壳软件选择压缩引擎有一个特点,在保证压缩比的条件下,压缩速度慢些关系不是太大,但解压速度一定要快,这样加了壳的EXE文件运行起来速度才不会受太大的影响。例如下面几个压缩引擎就能满足这要求:

1. aPLib压缩引擎 [url=http://www.ibsensoftware.com/]http://www.ibsensoftware.com/[/url],这个库对于低于64K的文件压缩效果较好,速度较快。
2. JCALG1压缩引擎,相对于aPlib,JCALG1对于大文件效果好些。
3. LZMA压缩引擎 [url=http://www.7-zip.org/zh-cn/sdk.html]http://www.7-zip.org/zh-cn/sdk.html[/url],LZMA 是 7-Zip 程序中 7z 格式 的默认压缩算法,压缩率很高。

[[i] 本帖最后由 敗類 于 2008-5-5 22:03 编辑 [/i]]

敗類 发表于 2008-5-5 21:40

[b][size=3][color=#ff0000]第四课 常见压缩壳与加密壳[/color][/size][/b]
加壳软件按照其加壳目的和作用,可分为两类:一是压缩(Packers),二是保护(Protectors)。压缩这类壳主要目的是减小程序体积,如ASPacK、UPX和PECompact等。另一类是保护程序,用上了各种反跟踪技术保护程序不被调试、脱壳等,其加壳后的体积大小不是其考虑的主要因素,如ASProtect、Armadillo、EXECryptor等。随着加壳技术的发展,这两类软件之间的界线越来越模糊,很多加壳软件除具有较强的压缩性能,同时也有了较强的保护性能。
[b][color=#00008b]1.常用压缩壳介绍[/color][/b]   

[b]1). ASPacK[/b]   
主页:[url=http://www.aspack.com/]http://www.aspack.com/[/url]
   ASPack是款Win32可执行文件压缩软件,可压缩Windows 32位可执行文件(.exe)以及库文件(.dll、.ocx),文件压缩比率高达40%~70%。

[attach]1516[/attach]

[b]2). UPX[/b]
主页:[url=http://upx.sourceforge.net/]http://upx.sourceforge.net/[/url]
   UPX是一个以命令行方式操作的可执行文件经典免费压缩程序,压缩算法自己实现,速度极快。(开源)

[b]3). PECompact[/b]
主页:[url=http://www.bitsum.com/]http://www.bitsum.com/[/url]
   PECompact同样也是一款能压缩可执行文件的工具(支持EXE、DLL、SCR、OCX等文件)。相比同类软件,PECompact提供了多种压缩项目的选择,用户可以根据需要确定哪些内部资源需要压缩处理。同时,该软件还提供了加解密的插件接口功能。

[attach]1517[/attach]

[b][color=darkblue]2.加密保护壳介绍[/color][/b]   

  为了保护自己的软件不轻易被他人“借鉴”,有必要对软件进行一些加密保护,而这方面目前己有成熟的专业加密软件可选择。但不要太依赖壳的保护,大多数壳是可以被攻破的,还是在自身保护上下些功夫。加密软件比较多,但在强度与兼容性上做的好的并不多,这里向大家介绍几款常见的。

 现在壳的发展一个趋势就是虚拟机保护,利用虚拟机保护后,能大大提高强度,因此建议尽可能使用此类技术保护软件。如Themida ,WinLicense,EXECryptor等壳带有虚拟机保护功能,因此得用好其SDK。

[b]1). ASProtect[/b]
主页:[url=http://www.aspack.com/]http://www.aspack.com/[/url]
   ASProtect是一款非常强大的Windows 32位保护工具,这4、5年来,其一直在更新进步。其开发者是俄国人Alexey Solodovnikov。它拥有压缩、加密、反跟踪代码、反-反汇编代码、CRC校验和花指令等保护措施。它使用Blowfish、Twofish、TEA等强劲的加密算法,还用RSA1024作为注册密钥生成器。它还通过API钩子(API hooks,包括Import hooks(GPA hook)和Export hooks)与加壳的程序进行通信。甚至用到了多态变形引擎(Polymorphic Engine)。反Apihook代码(Anti-Apihook Code)和BPE32的多态变形引擎(BPE32的Polymorphic Engine)。并且ASProtect为软件开发人员提供SDK,实现加密程序内外结合。

[attach]1518[/attach]

   ASProtect SKE系列己采用了部分虚拟机技术,主要是在Protect Original EntryPoint与SDK上。保护过程中建议大量里使用SDK, SDK使用请参考其帮助文档,在使用时注意SDK不要嵌套,并且同一组标签用在同一个子程序段里。ASProtect使用相当的简单,打开被保护的EXE/DLL文件后,选上保护的选项。再单击菜单Modes,单击Add Mode按钮,将Is this Mode Avtive选上,最后,单击Protection标签,对软件进行保护即可。ASProtect加壳过程中也可外挂用户自己写的DLL文件,方法是在上图中的External Options选项加上目标DLL即可。这样,用户可以在DLL加入自己的反跟踪代码,以提高软件的反跟踪能力。

[b]强度评介:[/b][color=orangered]由于ASProtect名气太大,研究它的人很多,因此很容易被脱壳,不推荐使用。[/color]


[b]2). Armadillo加密壳[/b]

   Armadillo也称穿山甲,是一款应用面较广的壳。可以运用各种手段来保护你的软件,同时也可以为软件加上种种限制,包括时间、次数,启动画面等等!很多商用软件采用其加壳。Armadillo对外发行时有Public,Custom两个版本。Public是公开演示的版本,Custom是注册用户拿到的版本。只有Custom才有完整的功能,Public版有功能限制,没什么强度,不建议采用。

[attach]1519[/attach]

[b]强度评介:[/b][color=orangered]Armadillo中比较强大的保护选项是Nanomites保护(即CC保护),用的好能提高强度,其他选项没什么强度。[/color]


[b]3). EXECryptor加密壳[/b]

    EXECryptor也是一款猛壳,可能由于兼容性等原因,采用其保护的商业软件不是太多。这款壳的特点是Anti-Debug做的比较隐蔽,另外就是采用了虚拟机保护一些关键代码。

[attach]1520[/attach]

[b]强度评介:[/b][color=orangered]用好EXECryptor 虚拟机保护功能,将关键敏感代码用虚拟机保护起来,能提高强度。EXECryptor 壳能脱的人很多,但对付其虚拟机代码的人不多。[/color]


[b]4). Themida加密壳[/b]

    Themida是Oreans的一款商业壳,官方链接:[url=http://www.oreans.com]www.oreans.com[/url]。Themida 1.1以前版本带驱动,稳定性有些影响。Themida最大特点就是其虚拟机保护技术,因此在程序中擅用SDK,将关键的代码让Themida用虚拟机保护起来。Themida最大的缺点就是生成的软件有些大。WinLicense这款壳和Themida是同一公司的一个系列产品,WinLicense主要多了一个协议,可以设定使用时间,运行次数等功能,两者核心保护是一样的。

[attach]1521[/attach]

[b]强度评介:[/b][color=orangered]用好其虚拟机保护功能,将关键敏感代码用虚拟机保护起来,能提高强度。[/color]


[b]5). VMProtect[/b]


  VMProtect是一款纯虚拟机保护软件,官方链接:[url=http://www.VMProtect.ru]www.VMProtect.ru[/url]。它是当前最强的虚拟机保护软件,经VMProtect处理过的代码,至今还没有人公开宣称能还原。

  但也有缺点,就是会影响程序速度,因此在一些对速度要求很高的场合就不适合用了。VMProtect 1.22.3之前是免费版,可以支持EXE,DLL等文件。更高版本需要购买,其支持驱动的保护。现在流行的做法,先用VMProtect将你的核心代码处理一下,再选用一款兼容性好的壳保护。

[b](1).关键代码自己定位[/b]
 VMProtect并没有提供使用说明,必须告诉VMProtect你要加密的代码具体地址,这对使用者有一定的要求,至少要懂一些跟踪技术,可以用调试器,如OllyDbg跟踪到程序需要保护的地址,然后添加地址到VMProtect。
在这以一个记事本程序为例来演示一下使用方法。

 运行VMProtect后,打开NOTEPAD.EXE文件。单击Dump标签,输入要加密的起始地址,光标来到要加密代码起始地址后,点击菜单“project/new procedure”,会出现一个新的项目,如下图。

[attach]1522[/attach]

需要处理其他地址时,请依次操作。
[color=orangered]
注意事项:
1.用VMProtect处理,请多测试,如果不稳定,请调整被保护代码的范围。
2.VMProtect对双线程支持不是太好,请同一次仅处理一个线程内的代码。[/color]

[b](2).用SDK标记代码[/b]

 VMProtect v1.2以上支持SDK了,可以编程时插入一个标记,然后在加密时,VMProtect会认出这些标记,并在有标记的地方进行保护。在程序源码中,用这对标签将一些核心代码包含,编译成EXE文件。然后用VMProtect打开EXE,单击“Project”菜单下的“New procedure”或者单击工具栏中的“New procedure”按钮,在弹出的添加地址窗口中会自动将SDK定义代码的地址填上。然后在VMProtect的“Options”窗口中设置相应的选项,最后单击工具栏中的“Compilation (F9)”按钮,便可对目标软件进行保护。经VMProtect处理过的软件,可以继续用Asprotect, Themida等加壳软件进一步保护。


 VMProtect是当前最强的虚拟机保护软件,经过VMProtect处理的软件基本是没法分析原程序思路的,关键是用好,一定要将程序关键代码进行处理。另外,经虚拟机处理代码效率会降低,因此一些对效率要求比较高的代码就不要用VMProtect进行处理。


Delphi 中的标记模式

代码:
asm                               db $EB,$10,'VMProtect begin',0       //标记开始处.end;//想保护的程序代码asm  db $EB,$0E,'VMProtect end',0         //标记结束处.end;


VC的VMProtect的宏 :

代码:
#define  VMBEGIN  __asm    //标记开始处.    {        _emit 0xEB      _emit 0x10        _emit 0x56        _emit 0x4D        _emit 0x50       _emit 0x72       _emit 0x6F       _emit 0x74       _emit 0x65       _emit 0x63       _emit 0x74       _emit 0x20       _emit 0x62       _emit 0x65       _emit 0x67       _emit 0x69       _emit 0x6E       _emit 0x00    }    //想保护的程序代码 #define  VMEND    __asm  //标记结束处.    {        _emit 0xEB      _emit 0x0E        _emit 0x56         _emit 0x4D         _emit 0x50       _emit 0x72       _emit 0x6F       _emit 0x74       _emit 0x65       _emit 0x63       _emit 0x74       _emit 0x20       _emit 0x65       _emit 0x6E       _emit 0x64       _emit 0x00    }

[[i] 本帖最后由 敗類 于 2008-5-5 22:06 编辑 [/i]]

敗類 发表于 2008-5-5 21:40

[b][size=3][color=#ff0000]第五课 文件类型分析[/color][/size][/b]
拿到一个壳,第一步就是用相关工具分析一下是什么壳,然后就可心中有数地跟踪分析。文件分析工具有[url=http://www.pediy.com/tools/unpacker.htm][color=darkblue]PEID[/color][/url],[url=http://www.pediy.com/tools/unpacker.htm][color=darkblue]FileInfo[/color][/url]等。

[b]1.PEiD[/b]

   PEiD的GUI界面操作非常方便直观。它的原理是利用查特征串搜索来完成识别工作的。各种开发语言都有固定的启动代码部分,利用这点就可识别出是何种语言编编译的。同样,不同的壳也有其特征码,利用这点就可识别是被何种壳所加密。PEiD提供了一个扩展接口文件userdb.txt ,用户可以自定义一些特征码,这样就可识别出新的文件类型。
   有些外壳程序为了欺骗PEiD等文件识别软件,会伪造启动代码部分,例如将入口代码改成与Visual C++ 6.0所编程程序入口处类似代码,即可达到欺骗目的。[b]所以,文件识别工具所给出的结果只是个参考,文件是否被加壳处理过,还得跟踪分析程序代码才可得知。[/b] 目前Hying的壳PE-Armor伪装能力是最强的:
[attach]1523[/attach]

   PEiD分析不出类型的文件就报告是“Nothing found *”,如出现这情况一般都是未知壳或新版的壳。

下面PEiD识别出这个软件是用Asprotect 1.2x加的壳。
[attach]1524[/attach]

[b]2.FileInfo[/b]

   FileInfo(简称Fi)另一款不错的文件检测工具。Fi运行时是DOS界面,在DOS窗口中运行程序相当不便,建议采用下面的技巧:
1.用鼠标将文件拖到Fi主文件上。
2.将Fi快捷方放进Windows的SendTo文件夹里.以后要分析某文件,只需右击“发送到”功能就可打开Fi。   
   FileInfo升级慢,其识别库不能自定义。而PEiD升级比较频繁,用户也可自定义特征码,因此PEiD用的比较普遍。

   有时,FileInfo和PEID会报“PE Win GUI”,Win GUI就是Windows图形用户界面程序统称,表明程序[b]可能[/b]没加壳。但不排除也有加壳的可能性,下图是一个ExeCryptor 2.2x的壳,FileInfo报“*PE Win GUI *section* ??”,其不能识别出来。识别信息中带了个问号,表明FI对给出的结果不是太肯定。
[attach]1525[/attach]

[[i] 本帖最后由 敗類 于 2008-5-5 22:08 编辑 [/i]]

敗類 发表于 2008-5-5 21:42

[b][size=3][color=#ff0000]第六课 寻找OEP [/color][/size][/b]
一般的压缩壳,如Aspack等都有[url=http://www.pediy.com/tools/unpacker.htm][color=darkblue][b]专用的脱壳机 [/b][/color][/url]。而加密壳(如ASProtect,Armadillo) 一般很少有脱壳机,必须手工脱壳。手工脱壳一般情况是分三步:一是查找程序的真正入口点(OEP);二是抓取内存映像文件;三是输入表重建。(当然现在的加密壳复杂些,要考虑更多的东西)
OEP是Original Entry Point缩写,即程序加壳前的真正的入口点。
外壳初始化的现场环境(各寄存器值)与原程序的现场环境是相同的。加壳程序初始化时保存各寄存器的值,外壳执行完毕,会恢复各寄存器内容。其代码形式一般如下:

PUSHFD ; 将标志寄存器入栈保存
PUSHAD ; push eax, ecx, edx, ebx, esp, ebp, esi, edi
…… ; 外壳代码部分
POPAD ; pop edi, esi, ebp, esp, ebx, edx, ecx, eax
POPFD ; 恢复标志寄存器
JMP OEP ;
OEP: …… ; 解压后的程序原代码

为了讲述方便,本节用UPX加壳的Win98记事本来演示。首先用PEid查看加壳前的记事本:
[attach]1527[/attach]

PEid显示Notepad.exe程序是用Microsoft Visual C++ 6.0编译的,接下来用UPX来加壳,方法是开个DOS窗口,用命令upx notepad.exe。如下图所示:
[attach]1528[/attach]

这时再用PEid查看加壳的文件,PEid会给出如下信息:UPX 0.89.6 - 1.02 / 1.05 - 1.24 -> Markus & Laszlo
[attach]1529[/attach]

UPX的壳可以用UPX.exe自身来脱,命令是:upx -d 文件名 。一些变种的UPX壳用UPX.EXE自身脱不了,这时可以试试[url=http://www.pediy.com/tools/PACK/Packers/UPX/UPXShellEx.rar][color=darkblue]UPX ShellEx[/color][/url]这款工具。



脱壳前建议用PE工具[url=http://www.pediy.com/tools/PE_tools/Lordpe/LPE-DLX.rar][color=darkblue]LordPE[/color][/url]打开目标文件查看一下区块,以尽可能地多了解一些信息,对脱壳有帮助,如下图:

[attach]1530[/attach]

[b]1.根据跨段指令寻找OEP[/b]

推荐用Ollydbg来调试脱壳,比SoftICE和TRW2000方便多了。运行Ollydbg,点击菜单“选项/调试设置”,将第一次暂停设在WinMain函数上。再用Ollydbg打开实例notepad.upx.exe就可中断在外壳的入口点处了:

[attach]1531[/attach]

上图相关代码如下:
0040E8C0 > 60 [b]pushad[/b] //一开始Ollydbg就会中断这行,这个就是外壳的入口点,注意这个pushad指令

绝大多数加壳程序在被加密的程序中加上一个或多个段,所以依据跨段的转移指令(JMP)就可找到真正的入口点,此时就会有POPAD/POPFD指令出现。UPX 用了一次跨段的转移指令(JMP),在跳到OEP处会看到虚拟地址的值有一个突变,此时就能确定OEP了。
UPX壳比较简单,大家不必要跟踪去找这个跨段的转移指令,中断WinMain后,只需要在Ollydbg里往下翻屏,就会发现这个跨段转移指令:

[attach]1532[/attach]

上图相关代码如下:
0040EA0E 61 [b]popad[/b] //注意这里的popad指令,和开始的pushad对应
[color=deeppink]0040EA0F - E9 B826FFFF jmp 004010CC [/color]//这里跳到OEP,将光标移到这,按F4执行到这行

这一句[color=deeppink]0040EA0F jmp 004010CC [/color]就是跳到OEP的指令,执行到这,UPX外壳己将程序解压完毕,并模拟Windows加载器的将原始程序加载到内存,004010CC 就是映射到内存目标程序的入口点,此时就可抓取内存映像文件了。


[b]2.根据堆栈平衡原理找OEP[/b]


操作方法:多数壳在运行到OEP的时候ESP=0012FFC4,这就是说程序的第一句是对0012FFC0进行写入操作,只要在0012FFC0下硬件写入断点(命令行里键入HW 12FFC0),我们就能停在OEP的第二句处。
[b]注意:[/b]并不是所有程序加载时,ESP的值是0012FFC4,这个值是什么由操作系统决定,将SizeOfStackCOmmit改大ESP就会变,这是因为操作系统从这个页向上找一个足够大地方当作stack了(感谢forgot解释)。你只要记住你系统加载时的ESP值,对其设断即可。

用OllyDBG重新加载实例程序notepad.upx.exe,在命令行下硬件写断点:

[attach]1535[/attach]
按F9执行程序,就会中断在OEP第二行:

[attach]1533[/attach]

此时如果将光标向上移,会发现第一句代码变乱了:
004010C7 000D 0A000055 add [5500000A], cl
004010CD 8BEC mov ebp, esp

这是因为Ollydbg将数据当汇编代码来分析了,你可以按 Ctrl+ALT+向上光标键 将当前显示的代码向上滚动一个字节就可看到正确的汇编代码了:

004010CC 55 push ebp
[color=deeppink]004010CD 8BEC mov ebp, esp[/color] //中断在这行
004010CF 83EC 44 sub esp, 44
004010D2 56 push esi
004010D3 FF15 E4634000 call [4063E4] ; kernel32.GetCommandLineA

中断后,别忘点击菜单“调试/硬件断点/”打开硬件断点面板,将刚才的硬件断点删除。
[attach]1534[/attach]


[b]3.根据编译语言特点找OEP[/b]

各类语言编译的文件入口点都有一些规律,可以这利用这点来寻找入口点。
1)Delphi程序
执行程序,用LordPE(或Prodump)选dump(full)脱壳,存为dump.exe。接着用Hex Workshop打开dump.exe,搜索文本“runtime”,搜到后,向前查找离“runtime”最近的十六进制数字“55 8B EC”,数字所在的地址就是程序的OEP。
2)Visual C程序
可以利用Visual C启动部分的几个函数GetCommandLineA(W)、GetVersion、GetModuleHandleA(W)、GetStartupInfoA(W) 等来定位程序的OEP。

常见的各类编译语言的入口汇编代码都要熟悉,因为一些加密强壳会偷OEP处的代码到壳里,一般情况各编译语言入口代码都相同,到时只需要直接引用相关程序的入口代码,这给我们恢复代码带来方便。

[[i] 本帖最后由 敗類 于 2008-5-5 22:14 编辑 [/i]]

敗類 发表于 2008-5-5 21:43

[b]4.用内存断点找OEP
[/b]
1.前言
   发现论坛中很多兄弟在询问:什么是二次内存断点,三次内存断点。还有很多人对内存断点的原理不是很明白。其实只要懂得壳是如何解压代码的,那么就完全可以按自己的喜欢来下断。
   
   本文要解决的问题是:
   1.什么是内存断点?
   2.如何在寻找OEP时使用内存断点。
   3.内存断点的局限性。
   
2.内存断点寻找OEP的原理
  i.首先,在OD中内存断点,硬件断点和普通断点(F2下断)是有本质区别的。硬件断点等效与SoftICE命令bpm,他的中断要用到DR0-DR7的调试寄存器,也就是说OD通过这些DR0-DR7的调试寄存器来判断是否断下。
    普通断点(F2下断)等效于bpx,他是在所执行的的代码的当前地址的一个字节修改为CC(int3)。当程序运行到int3的时候就会产生一个异常,而这个异常将交给OD处理,把这个异常的regEIP-1以后就正好停在了需要的中断的地方(这个根据系统不同会不一样),同时OD在把上面的int3修改回原来的代码。
  而内存断点基本上使用的是对代码使用的保护属性来实现中断。

  内存断点分为:内存访问断点,内存写入断点。
  我们知道,在程序运行的时候会有3种基本的状态产生:读取,写入,执行。

004AE242     A1 00104000       mov eax,dword ptr ds:[004AE24C]            //004AE24C处的内存读取
004AE247     A3 00104000       mov dword ptr ds:[004AE24C],eax            //004AE24C处的内存写入
004AE24C     83C0 01           add eax,1                                  //004AE24C处的内存执行
  
  那么我们应该如何中断在上面的几行呢?
  1.当我们对004AE24C下内存访问断点的时候,可以中断在004AE242也可以中断在004AE247。
  2.当我们对004AE24C下内存写入断点的时候,只能中断在004AE247。
  3.当我们对004AE24C下内存访问断点的时候,能中断在004AE24C。

  到这里你可能不明白了,为什么内存访问断点能中断在004AE247这一句对004AE24C的写入,而且还能中断在004AE24C的执行呢?

  其实很简单,我们只要仔细体会一下“内存访问”这四个字的含义遍可以知道,当我们对004AE24C进行读取的时候需要“访问”他吧,当我对004AE24C进行写入的时候也需要“访问”他吧!!当然我们要执行内存地址004AE24C的代码的时候也是还是要“访问”他的!

  所以我们不难得出下面的结论:
  1.内存写入中断的地方,一定是也可以用内存访问中断。
  2.内存执行的地方,也可以用内存访问中断。
  如果这时你认为,那么内存写入岂不是没用了。呵呵~那我要告诉你当然不是,如果你想快速的准确的定位到004AE247这一行的时候,那么他就大有作用了!

  总结一下:内存断点不修改改原代码,不会像普通断点那样因为修改代码被程序校验而导致中断失败;对于区段的访问只是区域大了一点,其原理和上面分析的三行代码是一样的。

  ii.如何使用内存断点来寻找OEP呢?
  要回答这个问题首先要回答这一个问题:壳是如何解压代码的?

  正如我们知道的,壳如果要把原来加密或压缩的代码运行起来就必须要解压和解密原来的代码。而这一个过程我们难道不能将他看做是对代码段(code段)的写入吗?好了,解压完毕了。我们要从壳代码的区段JMP到原来的代码段的时候,难道不正是对代码段(code段)的执行吗?
  理清了上面的关系就好办了,那么如果载入OD后,我们直接对code段下内存访问断点的时候,一定会中断在壳对code段的写入的代码的上面,就像上面的004AE247的这一行。而如果当他把code段的代码全部解压解密完毕了以后,JMP到OEP的时候,我们是不是还可以停在OEP的代码上面呢?而且每按下F9都会中断,因为这时code段在执行中哦!

  相信很多人到这里已经明白了,为什么在教程中到达了某一个时候,某一行的时候。牛人们就叫我们对code段下内存访问断点了吧。
  而如果你还要继续问我为什么一定要到那个地方才可以下断呢?我难道不可以一开始就下断吗?
  正入我上面所说的,如果你在前面下断很可能壳对code段还没解压完毕呢,这时如果你不停的按F9,你将会看到OD的下方不断的在提示你“对401000写入中断” “对401002写入中断”“对401004写入中断”.......如果你不介意按F9到他把正个code段写完的话,我除了同情你的“F9”以外,没什么其他的意见!
  
  那么我们就没有别更快一点的办法了吗?
  有的!那就是我们呼之欲出的两次内存断点办法。
  怎么理解两次内存断点呢?

  让我来做一个假设吧,假设我是一个壳的作者。一个EXE文件的有code段,data段,rsrc段.....依次排列在你的内存空间中,那么我会怎么解码呢?呵呵~我比较笨一点,我会先将code段解码,然后再将data段解压,接着是rsrc段......那么聪明的你不难发现,只要你在data断或者rsrc段下内存访问断点,那么中断的时候code段就已经解压完毕了。这时我们再对code段下内存反问断点,不就可以到达OEP了吗?

  这里注意上面虽然下了两次内存访问断点,但是本质是不一样的,目的也是不一样的。

  1.对data段下内存访问断点而中断是因为内存写入中断,目的是断在对对data段的解压时,这时壳要对data段写数据,但是code段已经解压完毕。
  2.对code段下内存访问断点而中断是因为内存执行中断,目的当然就是寻找OEP了。

  总结一下:如果我们知道壳在什么地方对code段解压完毕我们就可以使用内存断点,找到OEP。如果不知道,那么我们就依靠2次内存断点去找,如果还不行就用多次内存断点。总之明白了原理在多次的内存断点其实都一样。从这个过程中我们了解的是壳在对区段解码的顺序!

  iii.实战

  说了这么多,我想大家都越越欲试了吧。
  好吧,来弄一个猛壳怎么样:
[b]注:本节实例有些难度,不适合新手练习,新手可以跳过这个实例的学习,等找到合适的实例会补充上来的[/b]



  这个壳是一个hying的旧版,我们用他来实验一下我们内存断点法。
  
  OD载入以后来到这里

0040D000 u>  56                    push esi              //这里
0040D001     52                    push edx
0040D002     51                    push ecx
0040D003     53                    push ebx
0040D004     55                    push ebp
0040D005     E8 15010000           call unpackme.0040D11F

  根据跟过一次的经验我们将先设置,除int3异常以外忽略其他异常,SHIFT+F9

[attach]1536[/attach]

003725B1    64:8925 0000000>       mov     fs:[0], esp
003725B8    CC                     int3
003725B9     90                    nop                    //到这里
003725BA     8BCD                  mov ecx,ebp

  然后再设置除“除零”异常外,忽略其他异常。SHIFT+F9

[attach]1538[/attach]]

00372660     F7F3                  div ebx                //到这里
00372662     90                    nop
  
  下面是很多的单步异常,太麻烦我们不管他,现在开始用内存断点的方法(记得将所有异常忽略)。

对code段下内存访问断点,希望他已经解压完毕。方法是按ALT+M键打开内存窗口,在.code段按F2设断:

[attach]1537[/attach]

SHIFT+F9执行:


0040D19D     A4                    movs byte ptr es:[edi],byte ptr ds:[esi]         //还没解完呢
0040D19E     B3 02                 mov bl,2

对data段下内存“写入”断点,试试看他是不是要写data段。

00372712     F3:A4                 rep movs byte ptr es:[edi],byte ptr ds:[esi]      //断到这里
00372714     5E                    pop esi

下面再对code段下内存访问断点。F9

00372855     8907                  mov dword ptr ds:[edi],eax                         ; SHELL32.DragFinish  //这里是对IAT加密

的地方了!!!
00372857     5A                    pop edx
00372858     0FB642 FF             movzx eax,byte ptr ds:[edx-1]
0037285C     03D0                  add edx,eax
0037285E     42                    inc edx
0037285F     83C7 04               add edi,4
00372862     59                    pop ecx
00372863   ^ E2 A9                 loopd short 0037280E
00372865   ^ E9 63FFFFFF           jmp 003727CD
0037286A     8BB5 93060000         mov esi,dword ptr ss:[ebp+693]                      //到这里下断F2

现在如果再对data下访问断点已经是没用了。这时应该格外的小心。

我们现在就想既然这一段是对code解码的,那么我们就绕过他吧!

到0037286A下断F2,然后清除内存断点!!!!

F9以后停在这里,继续对code下内存访问断点。

看看左下角还在解码,哎~真是麻烦!

003728E1    /EB 1D                 jmp short 00372900
003728E3    |25 FFFFFF7F           and eax,7FFFFFFF
003728E8    |0385 83060000         add eax,dword ptr ss:[ebp+683]
003728EE    |2B85 8F060000         sub eax,dword ptr ss:[ebp+68F]
003728F4    |8BDE                  mov ebx,esi
003728F6    |2BD8                  sub ebx,eax
003728F8    |8958 FC               mov dword ptr ds:[eax-4],ebx                     //停在这里
003728FB    |83C7 08               add edi,8
003728FE   ^|EB DB                 jmp short 003728DB
00372900    \64:FF35 30000000      push dword ptr fs:[30]                          //清除内存断点以后到这里下断,F9

又是一段解码的代码,再次使用上面的办法手动跳出去。

现在继续对code段下内存访问断点!!F9以后到达这里。

004010CC     FFD7                  call edi                                           ; unpackme.004010CE   //OEP哦
004010CE     58                    pop eax
004010CF     83EC 44               sub esp,44
004010D2     56                    push esi
004010D3     90                    nop
004010D4     E8 B518F7FF           call 0037298E
004010D9     8BF0                  mov esi,eax

呵呵~虽然不是我们熟悉的OEP,但是地址是没错了,况且根据我们的步骤,我可以很肯定的说这是code段的第一次“执行”中断!

所以这就是OEP了。

总结一下:当我们在寻找OEP的时候,要多次对code下断“赌”一“赌”他解压完毕,如果不是就对别的段试试~如果程序跑飞了,那就没办法了,重来呗~其实说起来要赌的是:当data段,idata段,rsrc段摆在你的面前,你会好好“珍惜”那个段,不过还好上天还会给我们从来一次的机会(ctrl+F2 ^_^),那么我们会对那个不会跑飞的段说3个字----“先断你”如果非要在上面加一个次数,我希望是“一次内存断点就好了”

  vi.下面来讨论一下内存断点的局限性问题。
  是不是什么壳都可以用内存中断啊?
  不是每个都可以的,一些像UPX和ASPACK就不行。
  为什么?
  呵呵~follew me!
  情况1.
  我们来看看UPX的壳
  
首先,他的壳代码在UPX1段。

这里是他要跳到OEP的地方

0040ED4F    /77 11             ja short NOTEPAD_.0040ED62            
0040ED51    |01C3              add ebx,eax
0040ED53    |8B03              mov eax,dword ptr ds:[ebx]
0040ED55    |86C4              xchg ah,al
0040ED57    |C1C0 10           rol eax,10                           //在解码
0040ED5A    |86C4              xchg ah,al
0040ED5C    |01F0              add eax,esi
0040ED5E    |8903              mov dword ptr ds:[ebx],eax
0040ED60   ^|EB E2             jmp short NOTEPAD_.0040ED44
0040ED62    \24 0F             and al,0F
0040ED64     C1E0 10           shl eax,10
0040ED67     66:8B07           mov ax,word ptr ds:[edi]
0040ED6A     83C7 02           add edi,2
0040ED6D   ^ EB E2             jmp short NOTEPAD_.0040ED51        //回跳解码
0040ED6F     61                popad
0040ED70   - E9 5723FFFF       jmp NOTEPAD_.004010CC             //跳到OEP

我们看到他在对code段解压完毕的时候马上就JMP到OEP去了,那么我们根本就来不及使用内存断点的办法。

你可能说,我可以在

0040ED6F     61                popad //这一句下段然后使用啊

呵呵~~当然可以,不过你把花在下内存断点的时间,多按下几次F8不更好?!

也就是说当一个壳如果他在JMP 到OEP前的一行代码仍在都在对code段解压,那么我们就不能再使用这种办法了!

或者说我们没必要使用内存断点更贴切一点!

  情况2.
  对于一些在OEP处有stolen code的代码
  我们来看看一个OEP

0049E2F4 u>  55                push ebp                               //OEP
0049E2F5     8BEC              mov ebp,esp
0049E2F7     83C4 F4           add esp,-0C
0049E2FA     B8 BCE04900       mov eax,unpack.0049E0BC
0049E2FF     E8 048CF6FF       call unpack.00406F08                   //这里调用子程序
0049E304     A1 B8FE4900       mov eax,dword ptr ds:[49FEB8]
0049E309     50                push eax
0049E30A     6A 00             push 0
0049E30C     68 1F000F00       push 0F001F
0049E311     E8 E68EF6FF       call <jmp.&kernel32.OpenFileMappingA>  //API
0049E316     A3 60194A00       mov dword ptr ds:[4A1960],eax
0049E31B     833D 60194A00 00  cmp dword ptr ds:[4A1960],0

这个软件在被PESPIN加壳了以后这些全被偷掉了!

也就是说,壳在模拟OEP代码的时候必然会执行

0049E2FF     E8 048CF6FF       call unpack.00406F08  //这一步

而这个地方是call向code段的。如果我们使用内存访问断点,那么就停在这个子程序的地方

00406F08     50                push eax                                        //会停在这里
00406F09     6A 00             push 0
00406F0B     E8 F8FEFFFF       call <jmp.&kernel32.GetModuleHandleA>
00406F10     BA 04F14900       mov edx,unpack.0049F104
00406F15     52                push edx

这里既不是处理stolen code的地方,也不是FOEP的地方。这就会对我们的判断产生误导。

当然你可以alt+F9返回到壳处理stolen的地方,然后用内存断点,或者按几下F8到达FOEP处,但试问如果你拿到一个未知的壳的时候又怎么知道应该这么处理呢?

还有其他一些情况留给大家总结吧!

在下的砖已抛出,各位的玉不久矣。

--------------------------------------------------
3.总结
      好了说了很多,大家应该对内存断点的办法有了全面的了解,如果了解了内存断点的原理就不难明白他的使用方法,不难明白为什么有写壳不能使用内存断点的办法,其实任何的一种办法都需要经验的积累。相信如果大家在回答开篇的3个问题,已经不难了。

[[i] 本帖最后由 敗類 于 2008-5-5 22:15 编辑 [/i]]

敗類 发表于 2008-5-5 21:44

[table=98%][tr][td][color=red][size=3][b]第七课 Dump内存映像[/b][/size][/color]
[/td][/tr][/table]外壳程序解压还原后就会跳到OEP处执行,此时内存映像文件是己解压的程序。这时就可抓取内存映像文件了(该过程称为Dump)。当然不一定非要在程序原入口点抓取,只要能保证内存映像文件是己还原的就行了。
继续上一个实例notepad.upx.exe,到OEP后就可以Dump取内存映像文件:
004010CC    55             push    ebp
004010CD    8BEC           mov     ebp, esp
004010CF    83EC 44        sub     esp, 44

   运行LordPE.EXE,点击Options,默认选项如下:

[attach]1539[/attach]

   默认选上“Full dump:paste header from disk”,PE头的信息直接从磁盘文件获得。设置好后,在LordPE的进程窗口选择notepad.upx.exe,点击右键,执行“dump full”菜单命令。如图:

[attach]1540[/attach]

   将内存抓取的文件另存为dumped.exe,此时程序还不能运行,接下来就是重建输入表。

[[i] 本帖最后由 敗類 于 2008-5-5 22:16 编辑 [/i]]

敗類 发表于 2008-5-5 21:44

[table=98%][tr][td][color=red][size=3][b]第八课 重建输入表 [/b][/size][/color]
[/td][/tr][/table]在脱壳中输入表处理是很关键的一个环节,因此要求脱壳者对PE格式中的输入表概念非常清楚。在磁盘文件中,PE文件的输入表结构如下图所示:

[attach]1541[/attach]
                         图8.1 磁盘文件中的输入表

   PE文件运行时,Windows系统加载器首先搜索OriginalFirstThunk,如果存在,装载程序迭代搜索数组中的每个指针,找到每个IMAGE_IMPORT_BY_NAME结构所指向的输入函数的地址,然后用函数入口地址来替代由FirstThunk指向的 IMAGE_THUNK_DATA 数组里的元素值(即用真实的函数地址填充到IAT里)。因当PE文件装载内存后准备执行时,上图己转换成这种情况了:

[attach]1542[/attach]
                         图8.2 PE文件装载内存后的输入表

   此时输入表中其它部分就不重要了,程序依靠IAT提供的函数地址就可正常运行(图8.2 红圈部分)。如果程序加壳了,那壳自己模仿Windows装载器的工作来填充IAT中相关的数据,此时内存中就一张IAT表,输入表的其他部分是不存的(当然不是绝对的,也有不少壳,如Aspack等,内存中会出现完整的输入表结构),如图8.3所示。

                      [attach]1543[/attach]
             图8.3 外壳加载程序后的内部IAT

   输入表重建就是根据图8.3这张IAT恢复整个输入表的结构(即图8.1这个结构),ImpREC这款工具就是这个功能。
   一些压缩壳,填充IAT过程中没做什么手脚,用ImpREC工具可以直接重建输入表。而一些加密壳为了防止输入表被还原,就在IAT加密上大作文章,此时壳填充IAT里的不是实际的API地址,而是填充壳中用来HOOK-API的外壳代码的地址。这样壳中的代码一旦完成了加载工作,在进入原程序的代码之后,仍然能够间接地获得程序的控制权。 因为程序总是需要与系统打交道,与系统交道的途径是API,而API的地址已经替换成了壳的HOOK-API的地址,那程序每一次与系统打交道,都会让壳的代码获得一次控制权,这样壳可以进行反跟踪继续保护软件,同时也可完成某些特殊的任务。所以[b]重建输入表的关键是获得没加密的IAT[/b] ,一般的做法是跟踪加壳程序对IAT处理过程,修改相关指令,不让外壳加密IAT。

   UPX、ASPack等加壳保护的壳没加密IAT,而ASProtect、tElock等加密保护的壳都对IAT进行了加密处理。这篇先来简单的,即UPX壳。用OD打开上面的notepad.upx.exe实例,运行到OEP。(实际跟踪过程中,不一定要到OEP,只要外壳处理完IAT就可)然后如下操作:

1) 运行ImportREC,在下拉列表框中选择notepad.upx.exe进程,如图:

[attach]1544[/attach]

2) 上面己得知notepad.upx.exe的OEP地址是4010CC,则在左下角OEP处填入OEP的RVA值,这里填上10CC。点击“IAT AutoSearch”按钮,让其自动检测IAT偏移和大小,如出现下图表示ImportREC自己找到IAT的地址与大小了,即IAT地址:000062E0,大小248。

[attach]1545[/attach]
如果ImportREC没找到IAT偏移,则必须手工填入IAT偏移和大小(IAT偏移手动获得以后再讲述)。

3) 点击“Get Import”按钮,让其分析IAT结构得到基本信息,如下图所示:

[attach]1546[/attach]

4)如发现某个DLL显示"valid :NO" ,按"Show Invalids"按钮将分析所有的无效信息,在Imported Function Found栏中点击鼠标右键,选择"Trace Level1 (Disasm)",再按"Show Invalids"按钮。如果成功,可以看到所有的DLL都为"valid:YES"字样;
5)再次刷新"Show Invalids"按钮查看结果,如仍有无效的地址,继续手动用右键的Level 2或3修复;
6)如还是出错,可以利用"Invalidate function(s)"、"Delete thunk(s)"、编辑Import表(双击函数)等功能手动修复。
7)开始修复已脱壳的程序。选择Add new section (缺省是选上的) 来为Dump出来的文件加一个Section(虽然文件比较大,但避免了许多不必要的麻烦) 。
8)单击"Fix Dump"按钮,并选择刚在前面己Dump出来的文件。如修复的文件名是"Dump.exe",它将创建一个"Dump_.exe",此外OEP也被修正。

    经过这些步骤,这个UPX壳己成功脱掉。此时再用PEID查一下脱壳后的程序dumped_.exe,会显示是“Microsoft Visual C++ 6.0 SPx Method 1”,如下图所示:

[attach]1548[/attach]

再用LordPE查看脱壳后的输入表:

[attach]1547[/attach]]

从上图可以看出,输入表己正确修复,此时脱壳后的文件己能成功运行了。

[[i] 本帖最后由 敗類 于 2008-5-5 22:19 编辑 [/i]]

敗類 发表于 2008-5-5 21:45

[b][size=3][color=#ff0000]第九课 手动确定IAT的地址与大小 [/color][/size][/b]
在第八课中讲到,点击ImportREC的“IAT AutoSearch”按钮,一般情况下ImportREC可以自动识别出IAT地址与大小。但如果不能自动识别,就必须手动确定IAT地址与大小,然后将IAT的RVA与Size填进ImportREC,点击“Get Import”按钮就可得到输入表。
    还是用上一节实例演示,用OD打开notepad.upx.exe,来到OEP处:

[attach]1550[/attach]

    随便找一个API函数调用语句,如:
004010D3    FF15 E4634000   call    [4063E4]          ; kernel32.GetCommandLineA

   其中地址4063E4就是IAT中的一部分,在数据窗口下命令:D 4063E4,显示如下:

[attach]1549[/attach]

  上图每一组数据都是指向一个API函数,如 8D 2C 81 7C 就是地址:7C812C8D,在OD里按Ctrl+G,输入7C812C8D跳到这个地址就会发现是kernel32.GetCommandLineA函数:

[attach]1553[/attach]

[attach]1554[/attach]

   IAT是一块连续排列的数据,因此在数据窗口向上翻屏,直到出现00数据,寻找IAT起始地址:

[attach]1551[/attach]
然后向下翻屏,寻找IAT结束地址:

[attach]1552[/attach]

为了直观些,你也可以这样让数据窗口直接显示这些API函数,以确定IAT是否正确,在数据窗口点击鼠标右键:

[attach]1555[/attach]

调整显示格式后的数据窗口:

[attach]1556[/attach]

这样就直观了,IAT中每组数据指向一个API函数,各DLL之间是以000000分开的。

[b]因此IAT范围:0x4062E4~0x406524 ,大小为0x406524-0x4062E4=0x240[/b]

如果IAT加密了,此时IAT中的地址不是指向系统DLL中的API函数了,可能指向外壳。这就十分有必要找到外壳处理IAT的代码了,前面己说过,外壳加载时,会模拟Windows加载器,向IAT里填充当前操作系统API函数的实际地址。所以,在IAT里设个内存写断点,就可中断到这段代码处。
重新加载notepad.upx.exe,在IAT某个地址下内存写断点,这里选择0x4062E4这个地址设内存写断点,先在数据窗口下命令:D 4062E4

[attach]1557[/attach]

然后选择一个地址,点击鼠标右键,下“内存写断点”。

[attach]1558[/attach]

此时只要有数据写入4062E4地址处,OD就会中断,按F9运行OD,会中断这里:

0040E96D    > /8A02          mov     al, [edx]
0040E96F    . |42            inc     edx
0040E970    . |8807          mov     [edi], al
0040E972    . |47            inc     edi
0040E973    . |49            dec     ecx
0040E974    .^\75 F7         jnz     short 0040E96D

这段还不是处理IAT,按F9继续执行程序,会中断这里:


0040E9E9    > /8A07          mov     al, [edi]
0040E9EB    . |47            inc     edi
0040E9EC    . |08C0          or      al, al
0040E9EE    .^|74 DC         je      short 0040E9CC
0040E9F0    . |89F9          mov     ecx, edi
0040E9F2    . |57            push    edi                        // 函数名字符串  
0040E9F3    . |48            dec     eax
0040E9F4    .  F2:AE         repne   scas byte ptr es:[edi]
0040E9F6    . |55            push    ebp                        // DLL模块句柄
0040E9F7    .  FF96 A4EC0000 call    [esi+ECA4]                 ;  kernel32.GetProcAddress
0040E9FD    . |09C0          or      eax, eax
0040E9FF    . |74 07         je      short 0040EA08
0040EA01    . |8903          mov     [ebx], eax                // EBX指向IAT,将取得的API地址填充进IAT
0040EA03    . |83C3 04       add     ebx, 4                    // 指向下一个地址
0040EA06    .^\EB E1         jmp     short 0040E9E9
0040EA08    >  FF96 A8EC0000 call    [esi+ECA8]

上面这段就是UPX外壳填充IAT的全过程,感兴趣的,动态跟踪一下就明白了。这里用GetProcAddress函数获得函数地址:

FARPROC GetProcAddress(
  HMODULE hModule,    // DLL模块句柄
  LPCSTR lpProcName   // 函数名

[[i] 本帖最后由 敗類 于 2008-5-5 22:22 编辑 [/i]]

敗類 发表于 2008-5-5 21:47

[b][size=3][color=#ff0000]第十课 DLL文件脱壳 [/color][/size][/b]
[b]13.5 DLL文件脱壳
[/b]
[align=center]说明[/align]DLL文件的脱壳与EXE文件步骤差不多,所不同的是,DLL文件多了个基址重定位表等要考虑。

在2003年出版的《加密与解密》(第二版)中以UPX,PECompact为例讲述了DLL重定位重建的方法,由于本人的思路限制,当时只是从UPX,PECompact自身特点找思路解决这问题,即先分析UPX,PECompact