foo函数汇编详解
我们将把 foo 函数的汇编代码分成几个逻辑部分:函数准备(Prolog)、执行 C 代码 和 函数清理(Epilog)。
这是 foo 函数的完整汇编:
1 000000000040116e <foo>:
2 40116e:55 push %rbp
3 40116f:48 89 e5 mov %rsp,%rbp
4 401172:48 83 ec 30 sub $0x30,%rsp
5 401176:c7 45fc 0c 00 00 00 movl $0xc,-0x4(%rbp)
6 40117d:8b55 fc mov 0x4(%rbp),%edx
7 401180:8b45 fc mov 0x4(%rbp),%eax
8 401183:89 c6 mov %eax,%esi
9 401185:bf20 20 40 00 mov $0x402020,%edi
10 40118a:b800 00 00 00 mov $0x0,%eax
11 40118f:e8 ac fe ff ff call 401040 <printf@plt>
12 401194:48 8d 45 d0 lea 0x30(%rbp),%rax
13 401198:48 89 c7 mov %rax,%rdi
14 40119b:e8 b0 fe ff ff call 401050 <gets@plt>
15 4011a0:8b55 fc mov 0x4(%rbp),%edx
16 4011a3:8b45 fc mov 0x4(%rbp),%eax
17 4011a6:89 c6 mov %eax,%esi
18 4011a8:bf20 20 40 00 mov $0x402020,%edi
19 4011ad:b800 00 00 00 mov $0x0,%eax
20 4011b2:e889 fe ff ff call 401040 <printf@plt>
21 4011b7:90 nop
22 4011b8:c9 leave
23 4011b9:c3 ret
- 函数准备 (Function Prologue) - 建立栈帧
这部分代码的目的是为 foo 函数建立一个独立、干净的工作空间,也就是它的“栈帧”。
1 40116e:push %rbp
2 40116f:mov %rsp,%rbp
3 401172:sub $0x30,%rsp
- push %rbp:
- %rbp 是“基址指针”(Base Pointer),它指向上一个函数(这里是 main)的栈帧底部。这条指令先把 main 函数的基址指针存到栈上,以便 foo 函数结束后能恢复它。
- mov %rsp, %rbp:
- %rsp 是“栈指针”(Stack Pointer),它总是指向栈的顶部。这条指令让 %rbp 指向当前的栈顶。从此,%rbp 就成了 foo 函数自己的“基准点”,用来定位自己的局部变量。
- sub $0x30, %rsp:
- 这是为局部变量分配空间的关键指令。它将栈顶指针 %rsp 向下移动 0x30 (48) 个字节。这就在栈上开辟了一块 48 字节的内存,用来存放 foo 函数的所有局部变量(包括 fav_number 和 buf)。
栈的当前状态 (地址从高到低):
+-------------------------+
| main函数的返回地址 | <-- %rbp + 8
+-------------------------+
| main函数的rbp | <-- %rbp
+-------------------------+
| | \
| 48字节的局部变量空间 | } <-- 由 sub $0x30,%rsp 开辟
| | /
+-------------------------+ <-- %rsp
- 执行C代码
对应 const int fav_number = 12; 和第一个 printf
1 401176: movl $0xc,-0x4(%rbp)
2 ... (准备参数) ...
3 40118f: call 401040 <printf@plt>
- movl $0xc, -0x4(%rbp):
- $0xc 是十六进制的 c,即十进制的 12。
- -0x4(%rbp) 表示“从基准点 %rbp 向下 4 个字节的地址”。
- 这条指令就是把 12 这个值存入 %rbp - 4 的位置。编译器决定把 fav_number 这个变量放在这里。
- 接下来的几条 mov 指令是在为 printf 准备参数。在64位系统中,函数的前几个参数通过寄存器传递:
- mov $0x402020, %edi: 把格式化字符串的地址传给第一个参数寄存器 %edi。
- mov -0x4(%rbp), %eax 和 mov %eax, %esi: 把 fav_number 的值 (12) 传给第二个参数寄存器 %esi。
- mov -0x4(%rbp), %edx: 再次把 fav_number 的值传给第三个参数寄存器 %edx。
- call printf@plt: 调用 printf 函数,打印出 “My favorite number is 12…“。
对应 gets(buf); - 漏洞所在
1 401194:48 8d 45 d0 lea 0x30(%rbp),%rax
2 401198:48 89 c7 mov %rax,%rdi
3 40119b:e8 b0 fe ff ff call 401050 <gets@plt>
- lea -0x30(%rbp), %rax:
- 这是定位缓冲区的关键指令。lea (Load Effective Address) 的作用是计算地址。
- 它计算出 %rbp - 0x30 这个地址,并把它存入 %rax 寄存器。这个地址就是我们为局部变量开辟的48字节空间的最底部,也就是 buf 的起始地址。
- mov %rax, %rdi:
- 将 buf 的起始地址(刚刚存放在 %rax 中)移动到 %rdi 寄存器。
- 这是在为 gets 函数准备它的第一个(也是唯一一个)参数:缓冲区的地址。
- call gets@plt:
- 调用 gets 函数。程序会在这里暂停,等待你输入。gets 会把你输入的所有内容,无论多长,都复制到 %rdi 指向的地址,也就是 buf 的位置。溢出在这里发生!
对应第二个 printf
这部分代码和第一个 printf 几乎一样,它再次加载 fav_number 的值并打印。
但请注意:如果你输入的字符串足够长,它会溢出 buf,向上覆盖掉存放在 -0x4(%rbp) 的 fav_number。这时,第二个 printf 打印出的将不再是 12,而是你输入数据的一部分!这是缓冲区溢出的一个典型现象。
- 函数清理 (Function Epilogue) - 恢复栈帧并返回
这部分代码负责“打扫战场”,恢复 main 函数的栈帧,然后返回。
1 4011b7:90 nop
2 4011b8:c9 leave
3 4011b9:c3 ret
- nop: No Operation,空指令,什么也不做。通常是编译器为了对齐地址而插入的。
- leave: 这是一条很方便的指令,相当于执行下面两条指令:
- mov %rbp, %rsp: 将栈顶指针 %rsp 拉回到基址指针 %rbp 的位置,瞬间丢弃/释放 foo 函数的所有局部变量空间。
- pop %rbp: 从栈顶弹出一个值,恢复到 %rbp 寄存器中。这个值就是我们一开始 push %rbp 保存的 main 函数的基址指针。
- ret:
- 这是攻击的引爆点。ret (Return) 指令会从当前栈顶弹出一个地址,然后无条件地跳转到那个地址去执行。
- 在正常情况下,这个地址就是 main 函数调用 foo 后应该继续执行的下一条指令的地址。
- 在我们的攻击中,这个地址已经被我们的超长输入给覆盖了。ret 指令会读取我们精心构造的假地址(指向我们 Shellcode 的地址),然后跳转过去,从而让我们夺取程序的控制权。