如何找出elf32-i386反汇编中的方法参数大小和类型?

逆向工程 拆卸 争论
2021-06-13 03:39:04

考虑以下示例代码:

program code


build/program-x86:     file format elf32-i386

Disassembly of section my_text:

080a9dfc <subroutine_fnc>:
 80a9dfc:   55                      push   %ebp
 80a9dfd:   89 e5                   mov    %esp,%ebp
 80a9dff:   57                      push   %edi
 80a9e00:   56                      push   %esi
 80a9e01:   53                      push   %ebx
 80a9e02:   83 ec 14                sub    $0x14,%esp        // 20 bytes for local variables
 80a9e05:   c7 45 e0 00 00 00 00    movl   $0x0,-0x20(%ebp)  // zero local variable at address bp-0x20
 80a9e0c:   8d 7d f3                lea    -0xd(%ebp),%edi   // pointer in area of the local variables
 80a9e0f:   8b 75 0c                mov    0xc(%ebp),%esi    // 2. parameter
 80a9e12:   83 c6 30                add    $0x30,%esi        // add ascii ASCII '0' to the parameter

 80a9e15:   ba 01 00 00 00          mov    $0x1,%edx         // constatnt 1
 80a9e1a:   8b 5d 08                mov    0x8(%ebp),%ebx    // 1. function parameter
 80a9e1d:   89 f9                   mov    %edi,%ecx         // local buffer
 80a9e1f:   b8 03 00 00 00          mov    $0x3,%eax         // syscall read
 80a9e24:   cd 80                   int    $0x80             // read(par1, ptr to local var, 1)
 80a9e26:   83 f8 01                cmp    $0x1,%eax         // return value is 1?
 80a9e29:   74 0c                   je     80a9e37 <subroutine_fnc+0x3b>  // yes
 80a9e2b:   bb 01 00 00 00          mov    $0x1,%ebx
 80a9e30:   b8 01 00 00 00          mov    $0x1,%eax
 80a9e35:   cd 80                   int    $0x80             // no exit(1)

 80a9e37:   0f b6 45 f3             movzbl -0xd(%ebp),%eax   // expand value to 32 bits
 80a9e3b:   3c 2f                   cmp    $0x2f,%al         // is value < ASCII '0'
 80a9e3d:   7e 17                   jle    80a9e56 <subroutine_fnc+0x5a>  // yes end of the subroutine
 80a9e3f:   0f be d0                movsbl %al,%edx
 80a9e42:   39 f2                   cmp    %esi,%edx         // is value above >= par2 + '0'
 80a9e44:   7d 10                   jge    80a9e56 <subroutine_fnc+0x5a>  // yes => end
 80a9e46:   8b 45 0c                mov    0xc(%ebp),%eax    // read again param2
 80a9e49:   0f af 45 e0             imul   -0x20(%ebp),%eax  // multiply ebp-0x20 by param2
 80a9e4d:   8d 54 10 d0             lea    -0x30(%eax,%edx,1),%edx // add result with read character - ASCII '0'
 80a9e51:   89 55 e0                mov    %edx,-0x20(%ebp)  // store result to the local variable at ebp-0x20
 80a9e54:   eb bf                   jmp    80a9e15 <subroutine_fnc+0x19>  // repeat

 80a9e56:   8b 45 e0                mov    -0x20(%ebp),%eax  // function returns value from local variable at ebp-0x20
 80a9e59:   83 c4 14                add    $0x14,%esp
 80a9e5c:   5b                      pop    %ebx
 80a9e5d:   5e                      pop    %esi
 80a9e5e:   5f                      pop    %edi
 80a9e5f:   5d                      pop    %ebp
 80a9e60:   c3                      ret    

080a9e61 <toplevel_fnc>:
 80a9e61:   55                      push   %ebp
 80a9e62:   89 e5                   mov    %esp,%ebp
 80a9e64:   57                      push   %edi
 80a9e65:   56                      push   %esi
 80a9e66:   53                      push   %ebx
 80a9e67:   83 ec 20                sub    $0x20,%esp         // reserve stack space for local variables
 80a9e6a:   c6 45 f3 41             movb   $0x41,-0xd(%ebp)   // store ASCII 'A' at ebp-0xd
 80a9e6e:   c7 44 24 04 0a 00 00    movl   $0xa,0x4(%esp)     // store 10 to the first 32-bit slot bellow stack top
 80a9e75:   00 
 80a9e76:   c7 04 24 00 00 00 00    movl   $0x0,(%esp)        // store zero to the stack top
 80a9e7d:   e8 7a ff ff ff          call   80a9dfc <subroutine_fnc> // call subroutine_fnc(0,10)
 80a9e82:   89 c7                   mov    %eax,%edi          // store result
 80a9e84:   ba 80 01 00 00          mov    $0x180,%edx
 80a9e89:   b9 42 02 00 00          mov    $0x242,%ecx
 80a9e8e:   be 00 7f 0c 08          mov    $0x80c7f00,%esi    // setup pointer to "data"
 80a9e93:   89 f3                   mov    %esi,%ebx
 80a9e95:   b8 05 00 00 00          mov    $0x5,%eax          // syscall open
 80a9e9a:   cd 80                   int    $0x80              // open("data", 0x242, 0x180)
 80a9e9c:   89 45 dc                mov    %eax,-0x24(%ebp)   // store result to ebp-0x24
 80a9e9f:   85 c0                   test   %eax,%eax          // set flags according to the eax test
 80a9ea1:   79 0e                   jns    80a9eb1 <toplevel_fnc+0x50>  // sign is not set (>=0)
 80a9ea3:   b8 01 00 00 00          mov    $0x1,%eax
 80a9ea8:   89 c3                   mov    %eax,%ebx
 80a9eaa:   b8 01 00 00 00          mov    $0x1,%eax          // syscall exit
 80a9eaf:   cd 80                   int    $0x80              // exit(1)

 80a9eb1:   89 7d e0                mov    %edi,-0x20(%ebp)   // store subroutine_fnc result into ebp-0x20
 80a9eb4:   8d 75 f3                lea    -0xd(%ebp),%esi    // ebp-0xd is pointer to the 'A' character
 80a9eb7:   eb 22                   jmp    80a9edb <toplevel_fnc+0x7a>

 80a9eb9:   8b 5d dc                mov    -0x24(%ebp),%ebx   // fill ebx by open result (fd)
 80a9ebc:   89 f1                   mov    %esi,%ecx          // pointer to 'A'
 80a9ebe:   ba 01 00 00 00          mov    $0x1,%edx
 80a9ec3:   b8 04 00 00 00          mov    $0x4,%eax          // syscall write
 80a9ec8:   cd 80                   int    $0x80              // write(fd from open, "A", 1)
 80a9eca:   85 c0                   test   %eax,%eax          // check result
 80a9ecc:   79 09                   jns    80a9ed7 <toplevel_fnc+0x76>
 80a9ece:   89 d3                   mov    %edx,%ebx          // setup sign
 80a9ed0:   b8 01 00 00 00          mov    $0x1,%eax
 80a9ed5:   cd 80                   int    $0x80              // exit(1)
 80a9ed7:   83 6d e0 01             subl   $0x1,-0x20(%ebp)   // subtract 1 from ebp-0x20
 80a9edb:   83 7d e0 00             cmpl   $0x0,-0x20(%ebp)   // value 0 reached
 80a9edf:   75 d8                   jne    80a9eb9 <toplevel_fnc+0x58> // no => repeat

 80a9ee1:   8b 5d dc                mov    -0x24(%ebp),%ebx   // fd from open syscall
 80a9ee4:   b8 06 00 00 00          mov    $0x6,%eax          // syscall close
 80a9ee9:   cd 80                   int    $0x80              // close(fd from open)
 80a9eeb:   85 c0                   test   %eax,%eax          // test result
 80a9eed:   79 0e                   jns    80a9efd <toplevel_fnc+0x9c>
 80a9eef:   b8 01 00 00 00          mov    $0x1,%eax
 80a9ef4:   89 c3                   mov    %eax,%ebx
 80a9ef6:   b8 01 00 00 00          mov    $0x1,%eax
 80a9efb:   cd 80                   int    $0x80              // for error exit exit(1)

 80a9efd:   89 f8                   mov    %edi,%eax          // restore saved result of
                                                                  // subroutine_fnc call
 80a9eff:   83 c4 20                add    $0x20,%esp
 80a9f02:   5b                      pop    %ebx
 80a9f03:   5e                      pop    %esi
 80a9f04:   5f                      pop    %edi
 80a9f05:   5d                      pop    %ebp
 80a9f06:   c3                      ret    

program data


build/program-x86:     file format elf32-i386

Contents of section my_data:
 80c7f00 64617461 00                          data.           

一般来说,我如何才能了解subroutine_fnc使用了哪些参数我对这个的一般方法感兴趣。我知道这可能并不总是 100% 可能,但至少我对学习基础知识很感兴趣。

1个回答

基础

1. 必备信息:调用约定

为了确定如何将参数传递给函数,必须知道调用约定。

函数调用约定取决于目标体系结构和编译器1(另请参阅:Agner Fog对不同 C++ 编译器和操作系统的调用约定)。用于创建上面反汇编代码的编译器没有明确说明并不重要,因为输出中有足够的信息来确定目标体系结构和调用约定。

从上面的反汇编我们观察到指令集是x86,调用约定是cdecl

2. 识别调用约定

在这种情况下,我们可以从上面的反汇编中推断出调用约定。我们观察到符合被调用函数在根据cdecl约定保存和恢复寄存器方面的行为

080a9e61 <toplevel_fnc>:
 80a9e61:   55                      push   %ebp
 80a9e62:   89 e5                   mov    %esp,%ebp
 80a9e64:   57                      push   %edi         
 80a9e65:   56                      push   %esi         
 80a9e66:   53                      push   %ebx         
 80a9e67:   83 ec 20                sub    $0x20,%esp
    .
    .
    .
 80a9eff:   83 c4 20                add    $0x20,%esp
 80a9f02:   5b                      pop    %ebx
 80a9f03:   5e                      pop    %esi
 80a9f04:   5f                      pop    %edi
 80a9f05:   5d                      pop    %ebp
 80a9f06:   c3                      ret  

080a9dfc <subroutine_fnc>:
 80a9dfc:   55                      push   %ebp
 80a9dfd:   89 e5                   mov    %esp,%ebp
 80a9dff:   57                      push   %edi
 80a9e00:   56                      push   %esi
 80a9e01:   53                      push   %ebx
 80a9e02:   83 ec 14                sub    $0x14,%esp
    .
    .
    .
 80a9e59:   83 c4 14                add    $0x14,%esp
 80a9e5c:   5b                      pop    %ebx
 80a9e5d:   5e                      pop    %esi
 80a9e5e:   5f                      pop    %edi
 80a9e5f:   5d                      pop    %ebp
 80a9e60:   c3                      ret

在这两种功能的现有的x86函数序言之后是在寄存器中保存的值%edi%esi并且%ebx在堆栈中。这些寄存器被称为被调用者保存寄存器(与调用者保存寄存器%eax%ecx%edx)。然后在ret执行之前恢复这些寄存器的先前值

注意:函数的堆栈帧<toplevel_fnc>明显对齐到 16 字节边界,表明 GCC 可能是用于生成代码的编译器。

3. 向函数传递参数 cdecl

要进行子路由调用,调用者应该:

  1. 在调用子程序之前,调用者应该保存指定调用者保存的某些寄存器的内容。调用者保存的寄存器是 EAX、ECX、EDX。由于允许被调用的子程序修改这些寄存器,如果子程序返回后调用者依赖它们的值,则调用者必须将这些寄存器中的值压入堆栈(以便子程序返回后可以恢复它们)。

  2. 要将参数传递给子例程,请在调用之前将它们压入堆栈。参数应该倒序推送(即最后一个参数在前)由于堆栈向下增长,第一个参数将存储在最低地址(这种参数反转在历史上用于允许函数传递可变数量的参数)。

  3. 要调用子程序,请使用 call 指令。该指令将返回地址放在堆栈上的参数顶部,并跳转到子程序代码。这将调用子程序,它应该遵循下面的被调用者规则。2

要传递给的参数<subroutine_fnc>将在调用函数之前保存在堆栈中:

080a9e61 <toplevel_fnc>:
 80a9e61:   55                      push   %ebp
 80a9e62:   89 e5                   mov    %esp,%ebp
 80a9e64:   57                      push   %edi
 80a9e65:   56                      push   %esi
 80a9e66:   53                      push   %ebx
 80a9e67:   83 ec 20                sub    $0x20,%esp         
 80a9e6a:   c6 45 f3 41             movb   $0x41,-0xd(%ebp)
 80a9e6e:   c7 44 24 04 0a 00 00    movl   $0xa,0x4(%esp)            <- arg 2
 80a9e75:   00 
 80a9e76:   c7 04 24 00 00 00 00    movl   $0x0,(%esp)               <- arg 1
 80a9e7d:   e8 7a ff ff ff          call   80a9dfc <subroutine_fnc>

一般来说,我如何才能了解subroutine_fnc使用了哪些参数

在简单的情况下(无优化),如果调用约定已知,则很有可能发现函数参数。

不那么基础:优化

让我们检查在gcc使用-O3参数(最大优化)执行时从简单示例源(见下文)生成的一些目标代码的反汇编

080482f0 <main>:
 80482f0:       b8 01 00 00 00          mov    $0x1,%eax
 80482f5:       c3                      ret

080483f0 <function>:
 80483f0:       8b 44 24 08             mov    0x8(%esp),%eax
 80483f4:       03 44 24 04             add    0x4(%esp),%eax
 80483f8:       03 44 24 0c             add    0xc(%esp),%eax
 80483fc:       c3                      ret    

的论据是<main>什么?的论据是<function>什么?如果有的话,这两个函数之间是什么关系?

我们可以看到很多信息根本不存在于优化代码中。

优化的目标代码和未优化的目标代码之间的差异是巨大的。未优化的源代码汇编可以在这里找到:https ://godbolt.org/g/HS57Wp

这是源代码(尝试猜测发生了什么,然后将鼠标光标移动到下面的块上):

int function(int a, int b, int c); //prototype int main(void) { int a = 1; int b = 2; int c = 3; int k = function(a, b, c); return k / 6; }
int function(int a, int b, int c) { return a + b + c; }

正如我们从上面非常简单的例子中看到的那样,优化将调用约定抛到了一边,让人们很难弄清楚代码中真正发生了什么。在优化后的代码中, 中没有call说明<main>,这使得参数识别变得相当困难。

可以在此处找到有关此的更多讨论:在函数调用中传递了多少个参数?

更少的基础知识:变量类型恢复

如何找出elf32-i386反汇编中的方法参数大小和类型?

从目标代码的反汇编中推导出函数参数类型称为类型恢复,并且与变量恢复密切相关两者都是难题,也是研究的课题。

对象代码中不存在变量和类型的概念。变量名是一个标签,它被赋予一个内存地址,该地址对应于位于该地址的数据。虽然类型信息对于编译器评估源代码的语法和语义正确性是必要的,但由 CPU 直接执行的目标代码不保存这些信息(至少不是直接保存)。

为每个变量提供高级类型的类型恢复任务更具挑战性。类型恢复具有挑战性,因为高级类型通常在编译过程的早期就被编译器丢弃。在编译后的代码中,我们有字节可寻址的内存和寄存器。例如,如果将一个变量放入 eax 中,很容易断定它是与 32 位寄存器兼容的类型,但很难推断出高级类型,例如有符号整数、指针、联合和结构。

当前的类型恢复解决方案要么采用动态方法,这会导致程序覆盖率较差,要么使用无原则的启发式方法,这通常会给出不正确的结果。当前基于静态的工具通常使用一些关于众所周知的函数原型的知识来推断参数,然后使用专有的启发式方法来猜测剩余变量的类型,例如局部变量。3

不同的反编译器对这类问题采取不同的方法。以下是 TIE(可执行文件类型推断)采用的方法:

类型推断的 TIE 方法

Hex-rays 在他们的白皮书Decompilers 中讨论了他们的方法


1.调用约定(维基百科)

2. x86 Assembly Guide(需要注意的是术语“参数”用错了——应该是“参数”)

3. TIE:二进制程序中类型的原理逆向工程