技术基础:会编写加壳程序、了解PE结构、X86指令系统。
开发工具:MASM、VC++

1、我们有时候在改造一个PE的时候,对其中的某些call要做大量的改动,这时
候,往往通过手动增加一个节,在这个节中完成这个call的改造,再返回原来的地方继续执行。此法虽然可行,但工作量实在太大,且调试十分不方便。

2、“通用简单的方法”之实现原理:
    使用jmp far address 和 call far address的机器码长度相同,且仅仅第一字节不同的原则,jmp第一字节是0xE9,而call第一字节是0xE8,后面4字节相同。
    这种方法就是:通过给PE加个小壳,同时在加壳的时候,将PE中要改造的call 修改成调用外壳中的call,在外壳中的call中调用原来PE中的call,而外壳中的call我们在编写的时候较容易实现功能扩充和改造。
    在外壳中的call中,我们有两种扩充功能的方式:
    (1)在调用原来PE中的call之前扩充功能;
    (2)在调用原来PE中的call之后扩充功能;
    扩充功能可以直接在外壳call中实现,但较难调试和复杂。
    简单通用的方法是这个外壳中的新call仅仅完成:
        1、加载一个外部补丁DLL;
        2、调用这个外部补丁DLL中的函数,完成原来PE中的call的功能扩充和改造。
        3、调用原来PE中的call的;
    是先调用原来PE中的call还是后调用,视具体需要决定。这样我们就可以很简单的写扩充的功能代码了,因为这个外部补丁DLL可以用高级语言来写,且调试十分简单。

3、外壳中函数的具体实现:
    (一) 首先,在外壳中设定几个变量:
    (1) 保存外部DLL的各个函数地址的变量(DWORD):
        pfnExtPatchFun_1--pfnExtPatchFun_n,n为多大具体看要改造PE中几个函数。
    (2) 保存原来PE中的call的“地址差”变量(DWORD):
        dwOrgCallDifference_1--dwOrgCallDifference_n,n为多大具体看要改造PE中几个函数。
        dwOrgCallDifference_x = 原来call的机器码的后4个字节(双字)。
    (3) 保存原来PE中的call的RVA地址的变量(DWORD):
        dwOrgCallRVA_1--dwOrgCallRVA_n,n为多大具体看要改造PE中几个函数。
    (4) 保存外部DLL的hModule的变量(DWORD):
        hExtPatchDll。
    ;-----------------------------------------------------------------
    举例说明:
      如:原来PE的ImageBase=0x00400000,且有下面代码(IDA显示):
          .
          .
          004010CB 50                push    eax
          004010CC 57                push    edi
          004010CD E8 CE 08 00 00    call    sub_4019A0 ;需要改造的call,注意机器码后4字节
          004010D2 85 C0             test    eax, eax
          .
          .
      则:dwOrgCallRVA_1 = 004010CD - 0x00400000 = 0x000010CD
          dwOrgCallDifference_1 = 0x000008CE
   ;------------------------------------------------------------------
   ;
   (二) 在外壳程序中编写这些call,需要改造几个函数,就有几个call,格式如下:
;*****************************************************************************
;---------------------------------------------------;
; 外壳中的call,用于替换原来PE中的call               ;
;---------------------------------------------------;
align 4
wjq_API_SMC_Label_1:
GeneralPEShellCall_1    proc
;{
    ;原PE中的call的入口参数,我们通过堆栈传递到Dll中的新call,此时需注意EBP在本call中不要改变。
    @nPara1 EQU [ebp+0ch] ;原来PE中的Call的参数,也可以通过esp取得
    @nPara2 EQU [ebp+8h]
    ;
    pusha
    call GetPatchDllFunctions ;获取外部DLL中的所有函数地址并保存
    call GetAddressDifference ;获取地址差
    .if [pfnExtPatchFun_1+eax] != 0  ;如果功能函数1实现了,就调用
       push  @nPara1 ;原来PE中的Call的参数
       push  @nPara2
       call  [pfnExtPatchFun_1+eax] ;调用外部DLL中的补丁call
    .endif
    ;------------调用原来的call-------------------------
    call GetAddressDifference ;获取地址差
    lea esi,[TempCall_1+eax]  ;现在call的VA
    sub esi,[Image_Base+eax]  ;转换成RVA
    sub esi,[dwOrgCallRVA_1+eax] ;减去原来PE中的call的RVA = 两个call的地址差
    ;
    mov ebx,[dwOrgCallDifference_1+eax]  ;原来call xxxxxxxx 指令的偏移差
    .if ebx < 0  ;负数,原来call向上调用
        add ebx,esi
    .else        ;正数,原来call向下调用
        sub ebx,esi
    .endif
    ;
    mov  [TempCallDifference_1+eax],ebx  ;修正后的差值
    popa
    ;
    ;下面等同于一个 jmp far xxxxxxxx 语句,调用原来的call
    ;这里之所以采用这种方式,而不是用call,是为了通用,因为PE中的call有些是__stdcall,有些是___cdecl,如果一概采用call实现,要考虑出栈问题,不是很通用了。
TempCall_1           db 0E9h
TempCallDifference_1 dd  0
    ret
;}
GeneralPEShellCall_1 endp

;---------------------------------------------------------------------
; 获取外部DLL中的所有函数地址并保存
; 此函数中使用的W_LoadLibraryA等W_开始的函数,是外壳自建引入表是实现的
;---------------------------------------------------------------------
GetPatchDllFunctions  proc
    pusha
    call GetAddressDifference
    mov  edx,eax
    .if [hExtPatchDll+edx] == 0 ;如果已经装载了,就直接返回。
        ;
        lea ebx,[pExtPatchDll+edx]
        push edx ;保存地址差
        ;--------------------------
        push ebx
        call [W_LoadLibraryA+edx]
        ;--------------------------
        pop  edx ;恢复地址差
        mov  [hExtPatchDll+edx],eax
        .if eax != 0
           ;-------------;
           ;  function_1 ;
           ;-------------;
           push   edx  ;保存地址差
           lea   ebx,[pExtPatchFun_1+edx]
           push   ebx  ; 功能名的字符偏移
           push   dword ptr [hExtPatchDll+edx] ; 模块的句柄
           call   [W_GetProcAddress+edx] ; 调用Kernel32!GetpProcAddress以获得功能调用地址
           pop   edx ;恢复地址差
           mov   [pfnExtPatchFun_1+edx],eax
           ;-------------;
           ;  function_2 ;
           ;-------------;
           push   edx  ;保存地址差
           lea   ebx,[pExtPatchFun_2+edx]
           push   ebx  ; 功能名的字符偏移
           push   dword ptr [hExtPatchDll+edx] ; 模块的句柄
           call   [W_GetProcAddress+edx] ; 调用Kernel32!GetpProcAddress以获得功能调用地址
           pop   edx ;恢复地址差
           mov   [pfnExtPatchFun_2+edx],eax
           .
           .
           ;-直到取得n个函数地址。
       .endif
   .endif
   popa
   ret
GetPatchDllFunctions  endp

;-------------------------------------------------------------------
;该函数返回:地址差
;-------------------------------------------------------------------
align 4
GetAddressDifference proc
   call  @F
@@:
   pop  eax             ;获得实际偏移
   sub  eax, offset @B  ;减去偏移=地址差
   ret
GetAddressDifference endp
;*****************************************************************************

4、加壳部分的实现:
   相对来说,加壳部分的实现加简单,只要按照上面计算call的偏移差的方法,计算出新的偏移差,改写这个call的机器码的后4字节为新的偏移差,使其调用我们外壳中的call就可以了。
  ;
  具体计算过程如下:
  mov  eax,dwOrgCallRVA_1    ;原来PE中call处的RVA,对应上面例子就是:dwOrgCallRVA_1 = 0x000010CD
  mov  edx,Sheller_RVA       ;外壳程序入口的RVA
  add  edx,API_CALL_ADJUST_1 ;API_CALL_ADJUST_1 = wjq_API_SMC_Label_1 - Sheller_Start
  sub  edx,eax   ;差值
  sub  edx,5     ;减去Call指令的长度5
  mov  eax,edx   ;此时后的eax就是要修改的原PE中的call的后4字节(偏移差),对应上面例子就是:004010CD E8 CE 08 00 00 的 CE 08 00 00,变为双字就是0x000008CE,这个值改为eax中的值。

  需要注意的是:API_CALL_ADJUST_1的计算要使用wjq_API_SMC_Label_1标号,而不是函数名字GeneralPEShellCall_1。


5、外部DLL的编写格式(使用高级语言,快速编写,十分方便的调试):
//具体参数格式,请根据原来PE中的call的入口参数确定
//调用约定请参照外壳中的call,主要看出栈规则,我用的是__stdcall,也可以用___cdecl,主要根据外壳call中的使用方式,这个不能错,否则堆栈就乱套了。
extern "C" DWORD __stdcall ExtPatchFun_1(DWORD nPara1,DWORD nPara2)
{
  //实现功能扩充。。。
  return;
}
//
extern "C" DWORD __stdcall ExtPatchFun_2(DWORD nPara1,DWORD nPara2)
{
  //实现功能扩充。。。
  return;
}

6、关于补丁DLL的卸载
   我们在外壳call中只加载外部DLL,但不去卸载。这会不会有问题呢?回答是否定的,因为PE在结束进程时候,会自动卸载所有加载到本进程的DLL,所以,不用关心这个DLL不会卸载掉。而且,我们通过只加载方式使用这个DLL,会给我们带来很大方便,因为,只要在一处加载了这个外部DLL,我们就可以在原PE的任何地方调用其中的函数。

7、上面介绍的是先调用功能扩展部分,再调用原来的call。反之实现的方法类似,可自行发挥。

  排版不是很好,凑合着看吧。。呵呵。
                                                       Spring.W
                                                       2005.3.14

  • 标 题: 答复
  • 作 者:cyclotron
  • 时 间:2005-03-14 14:44

不错,详细介绍了与外壳代码通信的一种可行方案
补充一下,如何在原PE文件中选择合适的call
call XXXXXXXX
搜索0E8h,并确定相应指令的RVA,进而计算出XXXXXXXX确定是否位于代码段。不知道误码的概率是多少,是否还有其他的方法搜索这种有效替换指令呢?
如果在外壳中这样处理还有一个缺陷,那就是脱壳以后外壳代码仍然有效,被替换的call无需修复,但是若外壳申请动态内存保存附加的功能代码,则势必要同时修补这些被替换的call指令的相对偏移量,从而把修复的线索保留在了外壳的执行过程中。

记得Spring.W兄在以前的精华里提过一个与外壳通信的简单思路,好像是替换add eax,XXXXXXXX。
相比之下,如今的想法更成熟可行度也更高了