首页
社区
课程
招聘
通用且简单的扩充和改造PE中的call的功能的方法
发表于: 2005-3-14 13:30 11073

通用且简单的扩充和改造PE中的call的功能的方法

2005-3-14 13:30
11073

技术基础:会编写加壳程序、了解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


[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 7
支持
分享
最新回复 (20)
雪    币: 201
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
目前还看不懂

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

记得Spring.W兄在以前的精华里提过一个与外壳通信的简单思路,好像是替换add eax,XXXXXXXX。
相比之下,如今的想法更成熟可行度也更高了
2005-3-14 14:44
0
雪    币: 332
活跃值: (479)
能力值: ( LV9,RANK:330 )
在线值:
发帖
回帖
粉丝
4
牛人!看来你已经摆脱麻烦了!收藏
2005-3-14 15:03
0
雪    币: 200
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
太美了!!!!!!!!
2005-3-14 15:26
0
雪    币: 47147
活跃值: (20460)
能力值: (RANK:350 )
在线值:
发帖
回帖
粉丝
6
有技术含量
好像Krypton等壳采用了这方法,对调用输入表的CALL和JMP等指令加密。
2005-3-14 15:31
0
雪    币: 255
活跃值: (207)
能力值: ( LV9,RANK:250 )
在线值:
发帖
回帖
粉丝
7
有道理。
Authorship!I support!
2005-3-14 19:22
0
雪    币: 538
活跃值: (32)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
纯支持的.为虾米偶看不懂呢~
2005-3-14 21:30
0
雪    币: 339
活跃值: (1510)
能力值: ( LV13,RANK:970 )
在线值:
发帖
回帖
粉丝
9
来晚了。有人给我说了。我还以为是上午那篇导出函数的。

学习!
2005-3-15 00:30
0
雪    币: 603
活跃值: (617)
能力值: ( LV12,RANK:660 )
在线值:
发帖
回帖
粉丝
10
认真学习~
2005-3-15 08:47
0
雪    币: 339
活跃值: (1510)
能力值: ( LV13,RANK:970 )
在线值:
发帖
回帖
粉丝
11
我觉得这种方法用于加壳的安全性比较好,如果DLL处理一下,可以增加脱壳难度。但是如果仅仅用来“有时候在改造一个PE的时候,对其中的某些call要做大量的改动,这时候,往往通过手动增加一个节”就必要性不大了,因为如果函数稍微大一点,就考虑在外面写个DLL,在PE里面仅仅需要加一个loadlibrary和GetProcessaddress调用DLL里面的函数。
2005-3-15 10:25
0
雪    币: 405
活跃值: (49)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
12
终于有人把技术关键说出来了,虽然现在还用不上,但是对楼主这种精神,我对他之景仰有如涛涛之江水,连绵不决,希望还有新的大作!
2005-3-15 10:33
0
雪    币: 392
活跃值: (909)
能力值: ( LV9,RANK:690 )
在线值:
发帖
回帖
粉丝
13
最初由 nbw 发布
我觉得这种方法用于加壳的安全性比较好,如果DLL处理一下,可以增加脱壳难度。但是如果仅仅用来“有时候在改造一个PE的时候,对其中的某些call要做大量的改动,这时候,往往通过手动增加一个节”就必要性不大了,因为如果函数稍微大一点,就考虑在外面写个DLL,在PE里面仅仅需要加一个loadlibrary和GetProcessaddress调用DLL里面的函数。

人家说的就是增加外部dll调用代码啊
2005-3-15 11:42
0
雪    币: 117
活跃值: (20)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
14
最初由 xtxt110 发布
目前还看不懂

纯支持一下
2005-3-15 11:52
0
雪    币: 61
活跃值: (160)
能力值: ( LV9,RANK:170 )
在线值:
发帖
回帖
粉丝
15
强人
2005-3-16 10:39
0
雪    币: 258
活跃值: (230)
能力值: ( LV12,RANK:770 )
在线值:
发帖
回帖
粉丝
16

如果在调用函数的例程或返回加密解密的例程中要是改的没有jmp(0xE9)或call(0xE8)指令(用别的相关转移指令如:ret或用别的改变指令方向的方法)
会是怎么个样子,,,没有CALL,JMP 可能好完完,,,理论仿佛可能,但是处理参数或栈等可能麻烦等,,,纯属本人的幼稚想法

支持楼主
2005-3-16 21:41
0
雪    币: 161
活跃值: (231)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
17
不懂也要顶
2005-3-17 11:06
0
雪    币: 342
活跃值: (323)
能力值: ( LV9,RANK:450 )
在线值:
发帖
回帖
粉丝
18
好文,顶
2005-3-17 11:22
0
雪    币: 200
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
19
厉害,高手密集
2005-3-19 19:04
0
雪    币: 107
活跃值: (54)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
20
最初由 采臣・宁 发布
不懂也要顶
2005-3-21 23:52
0
雪    币: 97697
活跃值: (200834)
能力值: (RANK:10 )
在线值:
发帖
回帖
粉丝
21
最初由 peaceclub 发布
有道理。
Authorship!I support!


YOU ARE RIGHT!
2005-3-22 02:17
0
游客
登录 | 注册 方可回帖
返回
//