65.9K
CodeProject 正在变化。 阅读更多。
Home

代码移植:无限的代码重用

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (27投票s)

2007年11月5日

CPOL

17分钟阅读

viewsIcon

98555

downloadIcon

2355

使用现有的已编译机器码为项目添加功能。

引言

本文将演示如何使用ASM源文件将已编译的机器码集成到现有项目中。汇编源文件将从已编译的机器码创建。此外,本文还将消除Imagehlp.dll的单线程安全限制,并展示将已编译的STDCALL过程转换为C-CALL汇编语言例程的技术。

示例一将呈现一个标准的控制台应用程序。该程序将两个数字相加,然后返回结果。该示例将介绍将遇到的几个障碍。示例二将使用第一个示例中Add()的已编译机器码来集成移植。

最后,示例三将使用来自imagehlp.dll的外部代码来补充PEChecksum程序。这将消除项目对imagehlp库的依赖性要求,并充分演示这些技术。PEChecksum程序已在Windows PE校验和算法分析中介绍。熟悉x86汇编、WinDbg和IDA Pro的读者应从示例三开始。

这些示例将使用标准的C语言。对于有兴趣重用已编译C++的读者,请参阅Paul Vincent Sabanal和Mark Vincent Yason在BlackHat 2007上的演示文稿Reversing C++

优化

为了保持前两个示例的学术性,将禁用优化。通常,带有优化的发布代码缺乏本文示例所需的结构。发布示例将以禁用优化(/Od)的方式构建。请参阅图1。

Disable Optimizations

图1:禁用优化

启用优化后,某些函数将被内联。例如,图2显示没有与源代码中存在的函数对应的调用。wmain在0x00401000处进入。遇到的第一个函数调用位于0x40101D。0x40101D是调用cout。在这种情况下,Add()已被优化掉。

Listing with Missing Function Due to Optimization

图2:由于优化导致的函数调用丢失

另一个不希望的优化是帧指针省略。为了便于理解示例,没有使用帧指针省略。帧指针通常通过下面所示的指令序列创建。缺少帧指针给原本学术性的练习增加了一点小麻烦。帧指针将在“堆栈、ESP和EBP”部分讨论。

全局变量

全局于进程的变量被放置在.data段或(如果存在).bss段中。这些段分别对应于已初始化和未初始化的数据段。通常,程序通过地址而不是基于相对寻址(如局部变量中使用的EBP)来引用全局变量。例如,请注意图3中访问全局临时变量的代码。

Global Variable Storage Access

图3:全局变量存储访问

使用的地址是0x00417000。在PE Browse中检查程序时,该变量在地址0x00417000处的.data段下列出。请参阅图4。

Allocation of Global Variable

图4:全局变量的分配

图5显示了未初始化全局变量的反汇编和PE头。地址0x00403374处的四个字节值是垃圾:CR、LF、[空格]和[空格]。

Allocation of Unitialized Global Variable

图5:未初始化全局变量的分配

push ECX的含义将在“局部变量”部分进行探讨。

局部变量

局部变量存储在线程的堆栈上。图5显示了一个单独的ECX压栈指令,尽管ECXmain()本身中并未被使用。这是编译器为变量i创建局部存储的一种技术。与其发出一个三字节的sub ESP, 4操作码(0x83 0xEC 0x04),不如使用一个一字节的push ECX操作码(0x51)。

Local Allocation of a Single Variable

图6:单个变量的局部分配

当需要更多变量进行存储分配时,将使用sub ESP, n(其中n是所需的字节数)。例如,图7显示了五个DWORD变量的分配。编译器不发出五个push ECX指令,而是发出一个sub ESP, 0x14

Local Allocation of Multiple Variables

图7:多个变量的局部分配

堆栈、ESP和EBP

堆栈是线程在程序执行期间用作“临时区域”的内存区域。进程中的每个线程都有自己的堆栈。通常,人们将堆栈的内存视为一个从低地址开始并顺序移动到大地址的连续区域。这类似于在堆或数组中寻址——A[0]位于比A[63]低的地址。然而,与大多数内存访问操作不同,堆栈是向下增长的。

ESP是堆栈指针,由处理器维护。EBP(如果使用)由线程维护。当处理器遇到push n指令时,将按以下顺序执行两个操作:

  • 处理器将ESP减去机器字大小
  • 将值n放在ESP处的堆栈上

这意味着ESP始终指向放在堆栈上的最后一个值。

Argument Size and Call Stack

图8:参数大小和调用堆栈

由于压入的值始终是机器字大小,压入五个连续的**字节**会在堆栈上占用20个字节(0x14)——即使局部分配只需要两个DWORD(8个字节)。最后,对于截断的字节压栈,没有0扩展。寄存器高三字节中的任何内容都会作为函数参数出现,即使只对低字节感兴趣。这体现在指令序列mov al, byte ptr [a]; push eax中。请参阅图8。

关于代码生成的最后一个评论是编译器对处理器中多个流水线的认识。与其重用eax通过发出

mov  al, byte ptr [a]
push eax
...
mov  al, byte ptr [d]
push eax
...

编译器将旋转寄存器使用(eaxecxedx),以便执行流水线保持满载。这种优化对于性能至关重要,因为没有可能因分支预测错误而导致执行停顿的分支。因此,我们可以预期看到以下情况:

mov  al, byte ptr [a]
push eax
...
mov  dl, byte ptr [d]
push edx
...

在创建基于地址引用的堆栈帧时,编译器将发出标准的指令对。该帧创建了一个定义明确的“函数上下文”。堆栈操作的典型序言是序列:

push EBP
mov  EBP, ESP

上述序列在每次调用的函数中发出,以防止线程(函数)意外破坏堆栈指针(ESP)。相反,当函数退出时,通常会遇到堆栈和EBP的恢复。这是必需的,因为调用函数有不同的参考点来工作。

pop EBP
ret

应得出结论,EBP可以相对于函数(如果使用),而ESP相对于线程。这意味着当看到EBP-0xn时,函数是指函数内的局部存储。EBP+0xn表示线程正在访问由调用函数或调用链中的函数创建的局部变量。

OR和XOR

在查看反汇编时,遇到xor eaxeaxor eax, 0xFFFFFFFF是很常见的。第一条指令等同于mov eax, 0,第二条指令等同于mov eax, -1。它们是生成代码的优化版本。通常,指令序列比其等效的近亲使用的空间更少。

代码移植1

代码移植1是其余示例将扩展的基础。由于编译器和链接器的行为,第一个示例比预期的要复杂一些。以下将详细介绍遇到的问题,并概述本文中使用的通用解决方法。

第一个示例的源代码如下。main()调用Add(),后者将两个数字相加。然后将结果显示在标准输出上。

int main( )
{
    DWORD Augend = 32437;   // 0x7EB5
    DWORD Addend = 15369;   // 0x3C09
    DWORD Sum = 0;          // 0xBABE
    
    Sum = Add ( Augend, Addend );
 
    cout << _T("Augend: ") << Augend << endl;
    cout << _T("Addend: ") << Addend << endl;
    cout << _T("   Sum: ") << Sum << endl;
 
    return 0;
}
 
DWORD Add( DWORD Augend, DWORD Addend )
{
    DWORD result = Augend + Addend;
 
    return result;
}

遇到的第一个问题是存储布局不遵循源代码声明。请参阅图9。

Storage Layout versus Source Code Declaration

图9:存储布局与源代码声明

变量按以下顺序声明和初始化:

  • 被加数
  • 加数
  • Sum

然而,内存中的布局是:

  • Addend (EBP-0x04) - 高内存
  • Augend (EBP-0x08)
  • Sum (EBP-0x0C) - 低内存

遇到的第二个问题是Add()函数对变量的使用。Add()创建一个临时变量(result)并将两个值相加。然后将结果返回给main()。由于Add()接受两个参数——AugendAddend——人们期望该函数操作EBP+0x04和EBP+0x08。EBP+0x04和EBP+0x08是预期的相对基地址,因为假设它们已被压入堆栈。对于临时结果,期望该值会通过以下两种方式之一返回:

  • 通过使用Sum变量在EBP+0x00处
  • 通过使用EAX

然而,在Add()中停止执行时,观察到了一种不同的情况。请参阅图10。

图10:存储布局与源代码声明

在执行Add()的指令之前(但在进入函数之后),堆栈如下所示。请参阅图11。

图11:调用Add()后的堆栈布局

一旦函数执行完毕(但在执行返回指令之前),堆栈布局如​​图12所示。

Stack Layout Relative to EBP

图12:堆栈布局

表1解释了这些值相对于其地址(RVA)的含义:

项目

地址

注释

1

0x12FF5C

BABE

Add::result(由Add创建)

2

0x12FF60

0012FF7C

main的EBP

3

0x12FF64

00401028

返回地址

4

0x12FF68

7EB5

Add::Augden(由main压入)

5

0x12FF6C

3C09

Add::Addend(由main压入)

6

0x12FF70

0

main::Result

7

0x12FF74

7EB5

main::Augend

8

0x12FF78

3C09

main::Addend

表1:堆栈布局

代码移植2

示例二为代码移植1,但删除了C++源文件中的Add()函数。WinDbg下代码移植1生成的Add()代码如下所示。

00401cc0 55           push    ebp
00401cc1 8bec         mov     ebp, esp
00401cc3 51           push    ecx
00401cc4 8b4508       mov     eax, dword ptr [ebp+8]
00401cc7 03450c       add     eax, dword ptr [ebp+0Ch]
00401cca 8945fc       mov     dword ptr [ebp-4], eax
00401ccd 8b45fc       mov     eax, dword ptr [ebp-4]
00401cd0 8be5         mov     esp, ebp
00401cd2 5d           pop     ebp
00401cd3 c3           ret

移植

此时,供体(代码移植1)为受体(代码移植1)提供了20字节的代码。集成该功能的最简单方法是通过一个汇编文件(带有自定义构建步骤),该文件已添加到项目中。这种方法的好处是允许集成x32和x64例程,因为没有使用内联汇编。

还有其他方法可以集成移植。第一种方法是在WinDbg中直接编辑内存。这会创建一个临时示例。第二种方法是可以使用内联汇编来发出指令序列。其缺点是x64平台不支持内联汇编。第三种选择是修补。修补可执行文件通常属于病毒和破解者的范畴。修补留给读者作为练习。成功链接可执行文件需要两个更改:

  • Add()更改为Addition()
  • Addition()的函数原型更改为extern "C"

Add()更改为Addition()是因为add是汇编器(MASM)中的保留字。添加extern "C"是由于名称修饰和链接错误LNK2001: unresolved external symbol "unsigned long __cdecl Addition(unsigned long,unsigned long)" (?Addition@@YAKKK@Z)。几乎未更改的C++文件如下所示:

extern "C" DWORD Addition( DWORD, DWORD );
 
int main( int argc, char* argv[] )
{
    DWORD Augend = 32437;   // 0x7EB5
    DWORD Addend = 15369;   // 0x3C09
    DWORD Sum = 0;
    
    Sum = Addition ( Augend, Addend );
 
    cout << _T("Augend: ") << Augend << endl;
    cout << _T("Addend: ") << Addend << endl;
    cout << _T("   Sum: ") << Sum << endl;
 
    return 0;
}

首先,在项目目录中创建一个名为Addition.asm的文件。然后,将该文件添加到项目中。请参阅图13。

Adding ASM File to Project

图13:将ASM文件添加到项目

在Visual Studio的后续版本中,环境会询问是否使用masm.rules自定义构建规则。选择“确定”。请参阅图14。

MASM Custom Build Rule

图14:MASM自定义构建规则

如果自定义构建规则不可用,请添加以下内容作为自定义构建步骤:

  • 调试命令行
    • ml -c -Zi "-Fl$(IntDir)\$(InputName).lst" "-Fo$(IntDir)\$(InputName).obj" "$(InputPath)"
  • 发布命令行
    • ml -c "-Fl$(IntDir)\$(InputName).lst" "-Fo$(IntDir)\$(InputName).obj" "$(InputPath)"
  • 输出
    • $(IntDir)\$(InputName).obj

将ASM文件添加到项目后,项目将显示如图15所示。

图15:添加ASM文件

接下来,将以下内容添加到Addition.asm中。下面的代码演示了汇编过程的最低要求:

PUBLIC Addition
 
.486
.MODEL FLAT, C
 
.CODE
 
Addition PROC
 
    push ebp                        ; Save Caller's EBP
    mov ebp, esp                    ; Grab our Frame Reference 
    push ecx                        ; Storage for local 'result'
    mov eax, dword ptr [ebp+8]      ; Augend 
    add eax, dword ptr [ebp+0Ch]    ; Addend
    mov dword ptr [ebp-4], eax      ; Temporary 'result'
    mov eax, dword ptr [ebp-4]      ; ??? Already in EAX
    mov esp, ebp                    ; Clean 'push ECX' from stack
    pop ebp                         ; Resotre Caller's EBP
 
    ret
 
Addition ENDP
 
END     ; End of .CODE

PUBLIC Addition通知链接器过程Addition可供任何模块使用。.486处理器指令.MODEL简化段指令,它指示MASM为特定的内存模型生成代码。语言(“C”)告知汇编器调用约定。

.CODE是另一个简化段指令.CODE开始代码段,而END标记代码段的结束。还存在其他段,例如.DATAPROCENDP过程指令,它们构成了Addition函数的前后。其他过程将以类似的方式用不同的标签框起来。位于Addition过程中的是从代码移植1复制粘贴的代码。

编译和链接后,首先注意到的是第二个可执行文件比第一个程序小0x200字节(一个段)。请参阅图14。

Comparison of Executable File Sizes

图14:可执行文件大小比较

然而,就main()Addition()的执行而言,生成的代码保持不变。请参阅图15。

Source Code Analysis in WinDbg

图15:WinDbg中的源代码分析

Altered PE Checksum using Grafted Code

代码移植3

代码移植3将演示从外部可执行文件进行代码移植。具体来说,它将重用Imagehlp.dll中的CheckSumMappedFile()Imagehlp.dll是一个单线程库,因此这是一个改进该函数的机会。PE校验和算法的近乎完整的处理已在Windows PE校验和算法分析中介绍。

首先,下载PE校验和源代码。打开StdAfx.h并注释掉对imagehlp.dll的引用;并为CheckSumMemMapFile()添加原型。由于与Imagehlp.lib(即使未指定)链接,因此纳入了名称更改。

extern "C" {
  PIMAGE_NT_HEADERS /*WINAPI*/ CheckSumMemMapFile(
    PVOID BaseAddress,
    DWORD FileLength,
    PDWORD ExistingCheckSum,
    PDWORD CalculatedCheckSum
  );
}

由于名称修饰和链接错误LNK1190: invalid fixup found, type 0x0002,需要extern "C"。另外请注意,缺少WINAPI__stdcall的宏)。这是因为链接对象文件时出现链接错误。我怀疑这可能是打包问题,但尚未进一步调查。

LNK1190:找到无效的修复,类型0x0002

由于模型不再是STDCALL,因此需要转换来自Imagehlp.dll的例程。三个最常见的问题是:

  • STDCALL到C调用转换(堆栈清理)
  • 添加局部帧引用(EBP)
  • 伪影清理

将一个名为“CheckSum.asm”的汇编文件添加到项目中。在CheckSum.asm中创建三个过程:CheckSumMemMapFile_ChkSum_ImageNtHeader。从imagehlp.dll获取CheckSumMappedFile()ChkSum()的汇编代码,并将它们放在CheckSum.asm的相应过程中。暂时将_ImageNtHeader留空。_ImageNtHeader将是一个手动编码的替换,用于替代Imagehlp.dllNTDLL.DLLRtlpImageNtHeader()的调用。

唯一需要PUBLIC属性的过程是CheckSumMemMapFile。这使得_ChkSum_ImageNtHeader成为CheckSumMemMapFile可以使用的“私有”过程。

或者,使用本文提供的函数的列表文件。文件是CheckSummMappedFile.listingChkSum.listing。列表文件是通过在WinDbg中检查原始CodeGraft.exe时进行复制粘贴操作创建的。请参阅图16。

图16:WinDbg复制粘贴

Labels

此时,列表包括内存地址、操作码和助记符。为遇到的任何跳转创建标签(将CheckSumMappedFile更改为CheckSumMemMapFile)。例如,在0x76c96f3b处是以下指令:

76c96f3b eb1d     jmp     imagehlp!CheckSumMappedFile+0x4f (76c96f5a)

跳转目标是0x76C9F5A。在该位置创建一个标签。请注意,标签名称基于反汇编提供的地址(“+”已更改为“_”)

76c96f3b eb1d          jmp     imagehlp!CheckSumMappedFile+0x4f (76c96f5a)
...
76c96f57 8b7de4        mov     edi,dword ptr [ebp-1Ch]
CheckSumMemMapFile_0x4f:
76c96f5a 85c0          test    eax,eax

最后,清理原始指令以与跳转到标签的指令保持一致。

76c96f3b eb1d          jmp     CheckSumMemMapFile_0x4f

构件

代码中存在一些看起来是伪影的区域。例如,检查0x76c96fc8。由于没有生成操作码的汇编助记符,因此使用DB指令创建代码。请注意,在MASM中使用十六进制表示法时,数字前应加上“0”。DUP是一个运算符,用于按请求的次数创建数据字节。

;; 76c96fc8 ff              ???
;; 76c96fc9 ff              ???
;; 76c96fca ff              ???
DB 3 DUP(0FFh)
 
;; 76c96fcb ff426f          inc     dword ptr [edx+6Fh]
DB 0FFh, 042h, 06Fh
 
;; 76c96fce c9              leave
DB 0C9h
 
;; 76c96fcf 764b            jbe     imagehlp!MapFileAndCheckSumA+0x43 (76c9701c)
DB 076h, 04Bh
 
;; 76c96fd1 6f              outs    dx,dword ptr [esi]
DB 06Fh
 
;; 76c96fd2 c9              leave
DB 0C9h
 
;; 76c96fd3 7690            jbe     imagehlp!CheckSumMappedFile+0x5a (76c96f65)
DB 076h, 090h
 
;; 76c96fd5 90              nop
;; 76c96fd6 90              nop
;; 76c96fd7 90              nop
;; 76c96fd8 90              nop
DB 4 DUP(090h)

最后,我们可以删除列表中不需要的内容。要删除列表中的项目,只需注释掉它即可。

;; push    10h
;; push    offset `string'+0x3c (76c96fc8)
;; call    _SEH_prolog (76c934b9)
    
mov     esi,dword ptr [ebp+10h]
and     dword ptr [esi],0
mov     eax,dword ptr [ebp+0Ch]
shr     eax,1
push    eax
push    dword ptr [ebp+8]
push    0
    
; 76c96f1e e856d6ffff      call    ChkSum (76c94579)    
call _ChkSum

其他修复

原始代码在入口时安装了一个结构化异常处理程序。CodeGraft.exe代码将代码包装在处理程序中,因此可以跳过安装。通过注释掉上面的调用来实现处理程序的移除。这会创建一个堆栈不平衡,将在STDCALL到C调用转换中解决。

STDCALL到C调用转换

这一步需要最多的分析。这是因为缺少帧指针。因此,每个过程都将接收常规的:

push ebp
mov ebp, esp

一旦遇到额外的压栈,就必须注意代码/堆栈依赖关系。CheckSumMemMapFile如下所示。大写字母的指令是为了堆栈管理而添加的。注释掉的行已被删除。最后,STDCALL执行ret n,其中n是对ESP的调整。C-CALL使用标准的ret,由被调用者执行堆栈调整。清理的结果可在CodeGraft4.zip中找到。

CheckSumMemMapFile

CheckSumMemMapFile PROC

    ;;push    10h
    ;;push    offset `string'+0x3c (76c96fc8)
    ;   Inspecting 0x76c96fc8 shows this is '-1'...
    ;   push 0FFFFFFFFh
    ;; 76c96f08 e8acc5ffff      call    _SEH_prolog (76c934b9)

    PUSH EBP        ; Reference
    MOV EBP, ESP
    
    SUB ESP, 10h    ; Space for 4 Temporary Variables
                    ; T1: EBP-10h use in place of ebp-18h
                    ; T2: EBP-0Ch use in place of ebp-1Ch
                    ; T3: EBP-08h use in place of ebp-20h
                    ; T4: EBP-04h use in place of ebp-04h
                    
    mov     esi,dword ptr [ebp+10h]     ; Header CheckSum Variable (Read From PE Header)
    and     dword ptr [esi],0           ;   Header CheckSum = 0
    mov     eax,dword ptr [ebp+0Ch]     ; File Size
    shr     eax,1                       ;   File Size = File Size / 2
    push    eax                         ; Parameter 3: File Size
    push    dword ptr [ebp+8]           ; Parameter 2: Source (pBaseAddress)
    push    0                           ; Parameter 1: Partial Sum
    
    ; 76c96f1e e856d6ffff      call    _ChkSum@4(76c94579)    
    call _ChkSum
    
    ;; No Longer STDCALL
    ;;   Clean the parameters from the Stack
    ADD ESP, 0Ch
 
    mov     edi,eax                     ; EDI = Return from _ChkSum
    mov     dword ptr [EBP-0Ch],edi     ; Sum
    and     dword ptr [EBP-04h],0       ; File Size = 0???
 
    ;; push    dword ptr [ebp+8]
    ;; 76c96f2f e81ed2ffff      call    RtlpImageNtHeader (76c94152)
    push [ebp+8]                        ; Source (pBaseAddress)
    call _ImageNTHeader
    ADD ESP, 4                          ; Stack Maintenance - No longer STDCALL
 
    mov     dword ptr [EBP-08h],eax
    or      dword ptr [EBP-04h],0FFFFFFFFh    ; EBP-04h = -1
    jmp     _CheckSum_0x4f

    ;; Retain the Noise Bytes
    DB 5 DUP (090h)

    xor     eax,eax
    inc     eax
    ret

    ;; Retain the Noise Bytes
    DB 5 DUP (090h)

    mov     esp,dword ptr [EBP-10h]           ; Local Temporary Storage
    xor     eax,eax
    or      dword ptr [EBP-04h],0FFFFFFFFh    ; Local Temporary Storage
    mov     esi,dword ptr [ebp+10h]           ; Local Temporary Storage
    mov     edi,dword ptr [EBP-0Ch]           ; Local Temporary Storage
 
_CheckSumMemMapFile_0x4f:
    test    eax,eax
    je      _CheckSum_0x90
    cmp     eax,dword ptr [ebp+8]
    je      _CheckSum_0x90
    mov     cx,word ptr [eax+18h]
    cmp     cx,10Bh
    je      _CheckSum_0x6a
    cmp     cx,20Bh
    jne     _CheckSum_0xb5
 
_CheckSumMemMapFile_0x6a:
    lea     ecx,[eax+58h]           ; Existing (Header) Checksum
    mov     edx,dword ptr [ecx]     ; This routine removes the existing
    mov     dword ptr [esi],edx     ;   Checksum from the calculated value
    xor     edx,edx                 ;
    mov     dx,word ptr [ecx]       ; Notice the use of Subtract with Borrow (sbb)
    cmp     di,dx                   ;
    sbb     esi,esi                 ; This is consistent with the Documnetation stating
    neg     esi                     ;  'Calculate the checksum of the file
    add     esi,edx                 ;  with the the existing taken as 0.'
    sub     edi,esi
    movzx   ecx,word ptr [ecx+2]
    cmp     di,cx
    sbb     edx,edx
    neg     edx
    add     edx,ecx
    sub     edi,edx
 
_CheckSumMemMapFile_0x90:
    mov     ecx,dword ptr [ebp+0Ch]
    test    cl,1
    je      _CheckSumMemMapFile_0xa3
    mov     edx,dword ptr [ebp+8]
    movzx   dx,byte ptr [edx+ecx-1]
    add     edi,edx
 
_CheckSumMemMapFile_0xa3:
    movzx   edx,di
    add     edx,ecx
    mov     ecx,dword ptr [ebp+14h]
    mov     dword ptr [ecx],edx
    
    ;; 76c96fb8 e83cc5ffff      call    _SEH_epilog (76c934f9)
    
    ADD ESP, 10h
    POP EBP
 
    ;; No Longer STDCALL
    ;; ret     10h
    ret

CheckSumMemMapFile ENDP

遇到的第一个更改是移除SEH。程序将操作包装在处理程序中,因此在此级别添加SEH机制已被放弃。下一个添加是引用push ebpmov ebp, esp

SUB ESP, 10h    ; Space for 4 Temporary Variables
                ; T1: EBP-10h use in place of ebp-18h
                ; T2: EBP-0Ch use in place of ebp-1Ch
                ; T3: EBP-08h use in place of ebp-20h
                ; T4: EBP-04h use in place of ebp-04h

原始代码会访问EBP-0x1C,而不保留堆栈空间。分析表明,堆栈需要容纳四个DWORD。上述操作完成了任务。下面,手动堆栈调整完成了过程和EBP的恢复。

ADD ESP, 10h
POP EBP
 
;; No Longer STDCALL
;; ret     10h

ret

正如Joe Partridge所指出的,原始移植错过了上面对ESI的使用。由于寄存器被使用,必须保存和恢复它。在函数调用期间,必须保留EBXESIEDIEBPEAXECXEDX是临时寄存器。

ChkSum

此过程基本未更改。由于该过程将压入堆栈的值(参数)移动到寄存器中,因此未创建局部帧引用。转换的明显效果是将ret 0Ch更改为ret,因为调用者现在正在清理堆栈。_ChkSum可以在Windows PE校验和算法分析中详细检查。

_ChkSum PROC
 
    push    esi
    mov     ecx,[esp+10h]   ; File Size / 2
    mov     esi,[esp+0Ch]   ; Source (pBaseAddress)
    mov     eax,[esp+8]     ; Partial Sum
    shl     ecx,1           ; File Size = File Size * 2
    je      _ChkSum_0x16e
    test    esi,2
    je      _ChkSum_0x2d
    sub     edx,edx
    mov     dx,[esi]
    add     eax,edx
    adc     eax,0
    add     esi,2
    sub     ecx,2

    ...

_ChkSum_0x16e:
    mov     edx,eax         ;; Fold 32 bits in 16
    shr     edx,10h
    and     eax,0FFFFh
    add     eax,edx
    mov     edx,eax
    shr     edx,10h
    add     eax,edx
    and     eax,0FFFFh
    pop     esi
    
    ;; No longer STDCALL
    ;; ret     0Ch
    
    ret
 
_ChkSum ENDP

_ChkSum没有使用局部堆栈帧——它正在访问ESP处的参数。

push esi
mov ecx,[esp+10h]   ; File Size / 2
mov esi,[esp+0Ch]   ; Source (pBaseAddress)
mov eax,[esp+8]     ; Partial Sum

可以使用以下方法将其转换为使用局部帧引用(带有适当的序言):

PUSH EBP
MOV EBP, ESP

push esi

;; Stack Based
;; mov ecx,[esp+10h] ; File Size / 2
;; mov esi,[esp+0Ch] ; Source (pBaseAddress)
;; mov eax,[esp+8] ; Partial Sum

;; Frame Based
mov ecx,[EBP+10h] ; File Size / 2
mov esi,[EBP+0Ch] ; Source (pBaseAddress)
mov eax,[EBP+08h] ; Partial Sum

在上面的转换中,通过EBPESP引用值的偏移量是相同的。在这个例子中,这纯粹是巧合。情况可能并非总是如此。

_ImageNtHeader

_ImageNtHeader是原始调用RtlpImageNtHeader()的手动编码替换。该过程获取指向内存映射文件的指针,并为其加上IMAGE_DOS_HEADERe_lfanew值。函数在成功时返回总和(指向IMAGE_NT_HEADER的指针),在失败时返回NULL

_ImageNtHeader PROC
 
    push ebp
    mov ebp, esp
    
    push esi
  
    ;; ESI = pBaseAdddress
    mov eax, dword ptr[ ebp+08h ]
    mov esi, eax
    
    ;; pBaseAdddress == NULL?
    cmp esi, 0
    je  NULLRETURN
    
    ;; pBaseAdddress == 0xFFFFFFFF?
    cmp esi, 0FFFFFFFFh
    je  NULLRETURN    
    
    ;; MZ Signature
    cmp byte ptr [ESI], 'M'
    jne  NULLRETURN
    cmp byte ptr [ESI+01h], 'Z'
    jne  NULLRETURN
    
    ;; ESI is a pointer to IMAGE_DOS_HEADER
    ;; Grab the e_lfanew DWORD
    ;
    ;    IMAGE_DOS_HEADER
    ;      is 64 bytes (0x40) long
    ;
    ;    e_lfanew occupies bytes
    ;      IMAGE_DOS_HEADER[60-63]
    ;
    ;    ESI+060 is _not_ Hex!!!
    ;
    mov eax, esi
    add eax, dword ptr[ ESI+060 ]   ; value at e_lfanew
    mov esi, eax
    
    ;; PE Signature
    cmp byte ptr [ESI], 'P'
    jne  NULLRETURN
    cmp byte ptr [ESI+01h], 'E'
    jne  NULLRETURN
    cmp byte ptr [ESI+02h], 0
    jne  NULLRETURN
    cmp byte ptr [ESI+03h], 0
    jne  NULLRETURN
    
    ;;
    ;; EAX = IMAGE_NT_HEADER pointer
    ;;
 
    jmp CLEANSTACK
    
NULLRETURN:    
    mov eax, 0
    
CLEANSTACK:    
    pop esi
    pop ebp
    
    ret
 
_ImageNtHeader ENDP

代码移植5

第五个示例结合了前面的示例,并为CheckSumMemMapFile_ChkSum添加了优化。

优化后的CheckSumMemMapFile

通过观察代码中无用的局部变量,可以进一步清理CheckSumMemMapFile。此外,如果执行路径被发送到cmp cx,20Bh (IMAGE_NT_OPTIONAL_HDR64_MAGIC)之后的“Abort”跳转,则可以消除伪影。清理后的例程可在示例四中找到。

CheckSumMemMapFile PROC

    PUSH EBP                      ; Create Local Stack Frame
    MOV EBP, ESP

    PUSH ESI

    mov esi,dword ptr [ebp+10h]   ; Header CheckSum Variable (Read from PE Header)
    and dword ptr [esi],0         ; Header CheckSum = 0
    mov eax,dword ptr [ebp+0Ch]   ; File Size
    shr eax,1                     ; File Size = File Size / 2

    push eax                      ; Parameter 3: File Size
    push dword ptr [ebp+8]        ; Parameter 2: Source (pBaseAddress)
    push 0                        ; Parameter 1: Partial Sum
    call _ChkSum
    ADD ESP, 0Ch                  ; C-CALL, adjust stack

    mov edi,eax                   ; EDI = Return from _ChkSum

    push [ebp+8]                  ; Source (pBaseAddress)
    call _ImageNTHeader
    ADD ESP, 4                    ; C-CALL, adjust stack

    test eax,eax                  ; Return from _ImageNTHeader. Is it NULL?
    je _CheckSum_0x90             ;   Abort
    cmp eax,dword ptr [ebp+8]     ; pBaseAddress == _ImageNTHeader
    je _CheckSum_0x90             ;   Abort
    mov cx,word ptr [eax+18h]     ; IMAGE_OPTIONAL_HEADER.Magic
    cmp cx,10Bh                   ; IMAGE_NT_OPTIONAL_HDR32_MAGIC
    je _CheckSum_0x6a
    cmp cx,20Bh                   ; IMAGE_NT_OPTIONAL_HDR64_MAGIC
    jne _CheckSum_0x90            ;   Abort

_CheckSum_0x6a:
    lea ecx,[eax+58h]             ; ADDRESSOF(IMAGE_OPTIONAL_HEADER.Checksum)
    mov edx,dword ptr [ecx]       ; IMAGE_OPTIONAL_HEADER.Checksum (dereference)
    mov dword ptr [esi],edx       ; Save To Callee parameter dwHeaderCheckSum

    xor edx,edx                   ; EDX = 0
    mov dx,word ptr [ecx]         ; 2 bytes at IMAGE_OPTIONAL_HEADER.Checksum 
    cmp di,dx                     ; DI = result of _ChkSum
    sbb esi,esi
    neg esi
    add esi,edx
    sub edi,esi
    movzx ecx,word ptr [ecx+2]
    cmp di,cx
    sbb edx,edx
    neg edx
    add edx,ecx
    sub edi,edx

_CheckSum_0x90:
    mov ecx,dword ptr [ebp+0Ch]   ; File Size
    test cl,1
    je _CheckSum_0xa3
    mov edx,dword ptr [ebp+8]
    movzx dx,byte ptr [edx+ecx-1]
    add edi,edx

_CheckSum_0xa3:
    movzx edx,di
    add edx,ecx
    mov ecx,dword ptr [ebp+14h]
    mov dword ptr [ecx],edx

    POP ESI

    POP EBP

    ret

CheckSumMemMapFile ENDP

优化后的_ChkSum

可以在_ChkSum的主求和循环中实现最后的“窥孔”优化。这个补充将利用处理器调度并行指令的能力。较小的求和(0x40个DWORD、0x20个DWORD、0x10个DWORD等)将被跳过,因为它们在该例程的执行过程中最多只遇到一次。

由于该例程大部分时间都花在执行下面的循环(消耗0x80个DWORD)上,进一步的优化将包括一次执行push ebxpush edx。一旦求和完成,在jne _ChkSum_0xe8处退出之前执行相应的pop

_ChkSum_0xe8:
 
    PUSH EBX
    PUSH EDX
 
    XOR EBX, EBX
    XOR EDX, EDX
 
    add     eax,dword ptr [esi]
    adc     EBX,dword ptr [esi+4]
    adc     EDX,dword ptr [esi+8]
    adc     eax,dword ptr [esi+0Ch]
    adc     EBX,dword ptr [esi+10h]
    adc     EDX,dword ptr [esi+14h]
    adc     eax,dword ptr [esi+18h]
    adc     EBX,dword ptr [esi+1Ch]
    adc     EDX,dword ptr [esi+20h]
    adc     eax,dword ptr [esi+24h]
    adc     EBX,dword ptr [esi+28h]
    adc     EDX,dword ptr [esi+2Ch]
    adc     eax,dword ptr [esi+30h]
    adc     EBX,dword ptr [esi+34h]
    adc     EDX,dword ptr [esi+38h]
    adc     eax,dword ptr [esi+3Ch]
    adc     EBX,dword ptr [esi+40h]
    adc     EDX,dword ptr [esi+44h]
    adc     eax,dword ptr [esi+48h]
    adc     EBX,dword ptr [esi+4Ch]
    adc     EDX,dword ptr [esi+50h]
    adc     eax,dword ptr [esi+54h]
    adc     EBX,dword ptr [esi+58h]
    adc     EDX,dword ptr [esi+5Ch]
    adc     eax,dword ptr [esi+60h]
    adc     EBX,dword ptr [esi+64h]
    adc     EDX,dword ptr [esi+68h]
    adc     eax,dword ptr [esi+6Ch]
    adc     EBX,dword ptr [esi+70h]
    adc     EDX,dword ptr [esi+74h]
    adc     eax,dword ptr [esi+78h]
    adc     EBX,dword ptr [esi+7Ch]
    
    ADC     EAX, EBX
    ADC     EAX, EDX
 
    adc     eax,0
    
    POP EDX
    POP EBX
    
    add     esi,80h
    sub     ecx,80h
    
    jne     _ChkSum_0xe8

    ...

校验和

  • CodeGraft1.zip
  • MD5: F8958E18071F9FFDE17286AC4243C514

    SHA-1: ECE1C15BA469CCABFF922C453A26C0BD6593CEEF

  • CodeGraft2.zip
  • MD5: B457ED277E848A106F20F94B1CE275F4

    SHA-1: 241E70C0660A652D4015C7787850DBA0684F62F8

  • CodeGraft3.zip
  • MD5: 5DD1A1B16D47385577C8D7FF1DD49041

    SHA-1: 98C5EFE3F2EA6CF5214C8A739FF99E1D60FD56EA

  • CodeGraft4.zip
  • MD5: AC3800CF5714922D9930D7A2EAFCBD5C

    SHA-1: 273C14760D4A438518513677424CE9A54E29294E

  • CodeGraft5.zip
  • MD5: 8F8B25301DB6C77683FF8918CD679B21

    SHA-1: 95D52336EE4EEC1A46D00ACD2BBC10C79489D41B

  • CheckSumAsm.zip
  • MD5: 35EA1BBC97F1A23E8F0B7D943BA0F9F3

    SHA-1: B0BE1D8BF772958191114A55FFB343B5B829E240

  • CodeGraft.zip
  • MD5: c0d4468002f6ff82228323dd226093b5

    SHA: 42bf918481881819fa8a1cc8f519303185964e15

  • PEChecksum.zip
  • MD5: C0D4468002F6FF82228323DD226093B5

    SHA-1: 42BF918481881819FA8A1CC8F519303185964E15

修订

  • 2008年6月3日:总体修订和文章格式设置。
  • 2007年11月20日:Bug修复 - 为CheckSumMemMapFile添加了ESI保存。
  • 2007年11月5日:初始发布。
© . All rights reserved.