EBX寄存器用于存储器访问的这种模式是什么?

TTK*_*TTK 4 x86 reverse-engineering cpu-registers

我正在学习逆向工程的基础.在扭转裂缝的同时,在几乎所有功能的开头都看到了这种模式:

pushl %ebp                            
movl  %esp, %ebp              
pushl %ebx              # because ebx is a callee-saved register
subl  $0x14,%esp        # of course $0x14 changes depending on the function
calll 0x08048766
addl  $0x1a5f, %ebx     # also this value sometime changes depending on the function
Run Code Online (Sandbox Code Playgroud)

哪里0x08048766有一个函数可以做到这一点:

movl 0(%esp), %ebx         
retl 
Run Code Online (Sandbox Code Playgroud)

所以基本上,正常情况下,每个函数首先初始化寄存器ebpesp.然后寄存器ebx被推入堆栈,这也是完全可以理解的,因为ebx是被调用者保存的寄存器,稍后在函数中使用它来引用一些静态数据(来自.rodata),例如:

leal  -0x17b7(%ebx), %eax
movl  %eax, 0(%esp) 
calll printf   
Run Code Online (Sandbox Code Playgroud)

现在最有趣的(对我而言)部分:如果我已经正确理解,ebx首先用指向的值初始化esp(这使用函数at 0x08048766),为什么?里面有什么?这不是一个未初始化的点到堆栈中吗?

然后添加另一个值ebx.这个值代表什么?

我想更好地理解ebx在这种情况下如何使用寄存器,以及如何计算它指向的地址.

您可以在这里查看完整的程序,但遗憾的是没有任何C源代码可用.

Jon*_*art 8

此代码似乎已编译-fPIC.PIC代表"与位置无关的代码",这意味着它可以加载到任何地址,并且仍然能够访问它的全局变量.

在这种情况下ebx称为PIC寄存器,它用于指向GOT的末尾(全局偏移表).GOT具有偏移(从程序的基地址*)到正在使用的每个全局变量.

很多时候,了解这些事情的最好方法是自己编译一些代码,然后查看输出.当你有符号要看时,它会更容易.

我们来做一个实验:

pic.c

int global;

int main(void)
{
    global = 4;
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

$ gcc -v
...
gcc version 5.3.1 20160406 (Red Hat 5.3.1-6) (GCC)

$ gcc -m32 -Wall -Werror -fPIC -o pic pic.c
Run Code Online (Sandbox Code Playgroud)

部分(缩写)

$ readelf -S pic
Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [13] .text             PROGBITS        080482f0 0002f0 000182 00  AX  0   0 16
  [15] .rodata           PROGBITS        08048488 000488 00000c 00   A  0   0  4
  [22] .got              PROGBITS        08049ffc 000ffc 000004 04  WA  0   0  4
  [23] .got.plt          PROGBITS        0804a000 001000 000014 04  WA  0   0  4
  [24] .data             PROGBITS        0804a014 001014 000004 00  WA  0   0  1
  [25] .bss              NOBITS          0804a018 001018 000008 00  WA  0   0  4
Run Code Online (Sandbox Code Playgroud)

反汇编(英特尔语法,因为AT&T让我疯了)

$ objdump -Mintel -d --no-show-raw-insn pic

080483eb <main>:
 80483eb:   push   ebp
 80483ec:   mov    ebp,esp
 80483ee:   call   804840b <__x86.get_pc_thunk.ax> ; EAX = EIP + 5
 80483f3:   add    eax,0x1c0d            ; EAX = 0x804a000 (.got.plt, end of .got)
 80483f8:   lea    eax,[eax+0x1c]        ; EAX = 0x804a01C (.bss + 4)

 80483fe:   mov    DWORD PTR [eax],0x4   ; set `global` to 4
 8048404:   mov    eax,0x0
 8048409:   pop    ebp
 804840a:   ret    

0804840b <__x86.get_pc_thunk.ax>:
 804840b:   mov    eax,DWORD PTR [esp]
 804840e:   ret    
 804840f:   nop
Run Code Online (Sandbox Code Playgroud)

说明

在这种情况下,我的GCC决定使用eaxPIC寄存器而不是ebx.

另外,请注意编译器(GCC 5.3.1)在这里做了一些有趣的事情.它不是通过GOT访问变量,而是使用GOT作为"锚",而是直接偏移到该.bss部分中的变量.


回到你的代码:

pushl %ebp                            
movl %esp, %ebp              
pushl %ebx             ; because ebx is a callee-saved register
subl $0x14,%esp        ; end of typical prologue 

calll 0x08048766       ; __i686_get_pc_thunk_bx
                       ; Gets the current value of EIP after this call into EBX.
                       ; There is no other way to do this in x86 without a call

addl $0x1a5f, %ebx     ; Add the displacement to the end of the GOT.
                       ; This displacement of course changes depending on 
                       ; where the function is.
                       ; EBX now points to the end of the GOT.

leal -0x17b7(%ebx), %eax    ; EAX = EBX - 0x17b7
movl %eax, 0(%esp)          ; Put EAX on stack (arg 0 to printf)
                            ; EAX should point to some string
calll printf   
Run Code Online (Sandbox Code Playgroud)

在你的代码中,它实际上并没有"使用"GOT(否则我们会看到第二个内存去引用); 它使用它作为字符串的锚点,可能是在.rodataGOT之前的只读数据部分()中.

如果你看看函数0x08048766,你会看到它看起来像这样:

mov    (%esp),%eax  ; Put return address (pushed onto stack by call insn)
                    ; in eax
ret                 ; Return
Run Code Online (Sandbox Code Playgroud)

  • @PeterCordes好点.我在讨论是否将x86-64带入画面,但你是绝对正确的.Re:*"通过PLT间接调用共享库中的非静态函数."*IIRC有一些工作要做,这不再发生了.如果符号的可见性是"隐藏的"(因此它们在库外不可见),那么它们就无法被其他动态对象覆盖,所以我不明白为什么这些调用需要间接. (3认同)
  • 这是一个非常详细和清晰的答案,毫无疑问是我可以寻找的最佳答案.谢谢!(只是一点修复:EAX + 0x804a01C应该是EAX = 0x804a01C,我想) (2认同)
  • 值得注意的是,由于RIP相对寻址模式,PIC在x86-64代码中要简单得多.`rbx`没有绑定指向GOT的指针.因此所有这些额外开销仅适用于32位x86 PIC.(但是PIC在x86-64上仍然不是免费的,特别是如果你不小心声明尽可能多的函数和变量`static`.在共享库中调用非静态函数仍然通过PLT间接调用.) (2认同)