本文翻译自:Hack the virtual memory: the stack, registers and assembly code

Hack the virtual memory: the stack, registers and assembly code

正如我们在第2章中看到的,栈位于内存的高端并向下增长。但它如何正常工作?它如何转换为汇编代码?使用的寄存器是什么?在本章中,我们将详细介绍栈的工作原理,以及程序如何自动分配和释放局部变量。

一旦我们理解了这一点,我们就可以尝试劫持程序的执行流程。

*注意:我们将仅讨论用户栈,而不是内核栈

前提

为了完全理解本文,你需要知道:

  • C语言的基础知识(特别是指针部分)

环境

所有脚本和程序都已经在以下系统上进行过测试:

  • Ubuntu
    • Linux ubuntu 4.4.0-31-generic #50~14.04.1-Ubuntu SMP Wed Jul 13 01:07:32 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux
  • 使用的工具:
    • gcc
      • gcc (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4
    • objdump
      • GNU objdump (GNU Binutils for Ubuntu) 2.2

下文讨论的所有内容都适用于此系统/环境,但在其他系统上可能会有所不同

自动分配

首先让我们看一个非常简单的程序,只有一个函数,并在函数中使用了单个变量(0-main.c):

#include <stdio.h>

int main(void)
{
    int a;

    a = 972;
    printf("a = %d\n", a);
    return (0);
}

让我们编译这个程序,并使用objdump反汇编它:

holberton$ gcc 0-main.c
holberton$ objdump -d -j .text -M intel

main函数反汇编生成的代码如下:

000000000040052d <main>:
  40052d:       55                      push   rbp
  40052e:       48 89 e5                mov    rbp,rsp
  400531:       48 83 ec 10             sub    rsp,0x10
  400535:       c7 45 fc cc 03 00 00    mov    DWORD PTR [rbp-0x4],0x3cc
  40053c:       8b 45 fc                mov    eax,DWORD PTR [rbp-0x4]
  40053f:       89 c6                   mov    esi,eax
  400541:       bf e4 05 40 00          mov    edi,0x4005e4
  400546:       b8 00 00 00 00          mov    eax,0x0
  40054b:       e8 c0 fe ff ff          call   400410 <printf@plt>
  400550:       b8 00 00 00 00          mov    eax,0x0
  400555:       c9                      leave  
  400556:       c3                      ret    
  400557:       66 0f 1f 84 00 00 00    nop    WORD PTR [rax+rax*1+0x0]
  40055e:       00 00

现在先关注前三行:

000000000040052d <main>:
  40052d:       55                      push   rbp
  40052e:       48 89 e5                mov    rbp,rsp
  400531:       48 83 ec 10             sub    rsp,0x10

main函数的第一行引用了rbp和rsp;这些是有特殊用途的寄存器。rbp是基址指针寄存器,指向当前栈帧的底部,rsp是栈指针寄存器,指向当前栈帧的顶部。

让我们逐步分析这句汇编语句。这是main函数第一条指令运行之前栈的状态:

  • push rbp指令将寄存器rbp的值压入栈中。因为它“压入”到栈上,所以现在rsp的值是栈新的顶部的内存地址。栈和寄存器看起来像这样:

  • mov rbp,rsp将rsp的值复制到rbp中 -> rpb和rsp现在都指向栈的顶部

  • sub rsp,0x10预留一些空间(rbp和rsp之间的空间)来存储局部变量。请注意,此空间足以存储我们的整型变量

我们刚刚在内存中为局部变量创建了一个空间 —— 在栈中。这个空间被称为栈帧。每个具有局部变量的函数都将使用栈帧来存储自己的局部变量。

使用局部变量

main函数的第四行汇编代码如下:

400535:       c7 45 fc cc 03 00 00    mov    DWORD PTR [rbp-0x4],0x3cc

0x3cc的十进制值为972。这一行对应于我们的C代码:

a = 972;

mov DWORD PTR [rbp-0x4],0x3cc 将rbp-4地址处的内存赋值为972。[rbp - 4]是我们的局部变量a。计算机实际上并不知道我们在代码中使用的变量名称,它只是引用栈上的内存地址。

执行此指令后的栈和寄存器状态:

函数返回,自动释放

如果查看一下函数的末尾代码,我们会发现:

400555:       c9                      leave

此指令将rsp设置为rbp,然后将栈顶弹出赋值给rbp。


因为我们在函数的开头处将rbp的先前值压入栈,所以rbp现在设置为rbp的先前值。这就是:

  • 局部变量被“释放”
  • 在离开当前函数之前,先恢复上一个函数的栈帧。

栈和寄存器rbp和rsp恢复到刚进入main函数时的状态。

Playing with the stack

当变量从栈中自动释放时,它们不会被完全“销毁”。它们的值仍然存在内存中,并且这个空间可能会被其他函数使用。
所以在编写代码时要初始化变量,否则,它们将在程序运行时获取栈中的任何值。
让我们考虑以下C代码(1-main.c):

#include <stdio.h>

void func1(void)
{
     int a;
     int b;
     int c;

     a = 98;
     b = 972;
     c = a + b;
     printf("a = %d, b = %d, c = %d\n", a, b, c);
}

void func2(void)
{
     int a;
     int b;
     int c;

     printf("a = %d, b = %d, c = %d\n", a, b, c);
}

int main(void)
{
    func1();
    func2();
    return (0);
}

正如所见,func2函数并没有初始化局部变量a,b和c,如果我们编译并运行此程序,它将打印...

holberton$ gcc 1-main.c && ./a.out 
a = 98, b = 972, c = 1070
a = 98, b = 972, c = 1070
holberton$

...与func1函数输出的变量值相同!这是因为栈的工作原理。这两个函数以相同的顺序声明了相同数量的变量(具有相同的类型)。他们的栈帧完全相同。当func1结束时,其局部变量所在的内存不会被清除 —— 只有rsp递增。
因此,当我们调用func2时,它的栈帧位于与前一个func1栈帧完全相同的位置,当我们离开func1时,func2的局部变量具有与func1的局部变量相同的值。

让我们检查反汇编代码来证明上述:
```

点击收藏 | 0 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖