Pwn Ptmalloc2 Unsortbin Leak Libc
原理
如未特殊说明, 均假定 libc 2.23, 64 位.
Unsortbin
Unsortbin 就是 malloc state 中的 bins[1], 相关定义如下:
struct malloc_state
{
/* Normal bins packed as described above */
mchunkptr bins[NBINS * 2 - 2];
};
/* The otherwise unindexable 1-bin is used to hold unsorted chunks. */
#define unsorted_chunks(M) (bin_at (M, 1))
/* addressing -- note that bin_at(0) does not exist */
#define bin_at(m, i) \
(mbinptr) (((char *) &((m)->bins[((i) - 1) * 2])) \
- offsetof (struct malloc_chunk, fd))
Unsortbin 是一个双向链表结构. 其中的 freed chunk 由 fd, bk 指针双向链接.
只看和数据结构相关的部分, free 的时候, 如果不是放在 fastbin (和 tcache bin) 中, 则是在 unsorted bin 的双向链表的头部插入:
static void
_int_free (mstate av, mchunkptr p, int have_lock)
{
...
/*
Place the chunk in unsorted chunk list. Chunks are
not placed into regular bins until after they have
been given one chance to be used in malloc.
*/
bck = unsorted_chunks(av);
fwd = bck->fd;
...
p->fd = fwd;
p->bk = bck;
...
bck->fd = p;
fwd->bk = p;
...
}
malloc 一个比 fast chunk 大的块时, 会先去 smallbin 里找. 如果没有合适的 chunk, 就会去整理 fastbin, 将能够合并的 chunk 尽量合并, 然后把所有 chunk 也放入 unsortbin 中. 然后开始从后往前遍历 unsortbin.
如果是请求的块大小是 small chunk, 并且上一个切割剩余的 (last_remainder) 正好是 unsortbin 中唯一的一块, 那么尝试分割这个 chunk, 剩下的部分放回 unsortbin 中, 同时更新 last remainder. 返回这个块.
否则, 看当前遍历到的这个块大小是不是大小正好, 是的话就返回, 不是的话就把这块放到对应的 bin 中去.
遍历完了以后还不满足, 会通过 block map 来 重新 找 smallbin 和 largebin 的 best fit, 从这里切割, 并设置 last_remainder. 最后还不满足, 则去从 top chunk 中取.
leak libc
初始时, unsortbin 的 fd 和 bk 都指向自己. 所以, 如果我们能够泄漏双向链 表头部节点的 bk 指针, 或者 尾部节点的 fd 指针, 那么就能够泄漏出 bins[1] 的地址了. 而在 main arena 中, bins 是存在 libc 的数据段的. 所以, 相当于泄漏出了 libc 地址.
特别地, 如果 unsortbin 中只有一个节点, 那么它的 fd 和 bk 指针都可以泄漏 libc.
虽然有很多种情况, 都能往 unsortbin 中存放 chunk, 但是对利用来说, 最好就是直接把一个 smalll chunk free 掉, 这样就到了 unsortbin 中, 然后 leak fd 或者 bk.
学习资料
例题
2017 0ctf babyheap
(前半段是 unsortbin leak libc, 后半段是 malloc malloc_hook nearby)
64 位 ELF, 保护全开. 菜单题, Allocate, Fill, Free, Dump.
main:
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
Node *heap; // [rsp+8h] [rbp-8h]
heap = (Node *)Init();
while ( 1 )
{
menu();
switch ( readll() )
{
case 1LL:
Allocate(heap);
break;
case 2LL:
Fill(heap);
break;
case 3LL:
Free(heap);
break;
case 4LL:
Dump(heap);
break;
case 5LL:
return 0LL;
default:
continue;
}
}
}
Init 是一些 IO 初始化操作, 并 mmap 了一个足够大的地址并返回.
Allocate:
void __fastcall Allocate(Node *a1)
{
int i; // [rsp+10h] [rbp-10h]
int size; // [rsp+14h] [rbp-Ch]
char *str; // [rsp+18h] [rbp-8h]
for ( i = 0; i <= 15; ++i )
{
if ( !a1[i].inuse )
{
printf("Size: ");
size = readll();
if ( size > 0 )
{
if ( size > 4096 )
size = 4096;
str = (char *)calloc(size, 1uLL);
if ( !str )
exit(-1);
a1[i].inuse = 1;
a1[i].size = size;
a1[i].str = str;
printf("Allocate Index %d\n", (unsigned int)i);
}
return;
}
}
}
可以看到, 最多可以分配 16 个堆, 堆大小最大是 4096. 且用的是 calloc, 会将用户数据部分清空.
void __fastcall Fill(Node *a1)
{
int i; // [rsp+18h] [rbp-8h]
int size; // [rsp+1Ch] [rbp-4h]
printf("Index: ");
i = readll();
if ( i >= 0 && i <= 15 && a1[i].inuse == 1 )
{
printf("Size: ");
size = readll();
if ( size > 0 )
{
printf("Content: ");
my_read(a1[i].str, size);
}
}
}
检测 inuse, 为 1 表示该位置被分配, 即 str 有效. 但是, 这里输入的 size 没有和之前 calloc 的进行比较, 所以可以产生 堆溢出漏洞. my_read 没有漏洞, 不放代码了.
void __fastcall Free(Node *a1)
{
int i; // [rsp+1Ch] [rbp-4h]
printf("Index: ");
i = readll();
if ( i >= 0 && i <= 15 && a1[i].inuse == 1 )
{
a1[i].inuse = 0;
a1[i].size = 0LL;
free(a1[i].str);
a1[i].str = 0LL;
}
}
全清空.
void __fastcall Dump(Node *a1)
{
int i; // [rsp+1Ch] [rbp-4h]
printf("Index: ");
i = readll();
if ( i >= 0 && i <= 15 && a1[i].inuse == 1 )
{
puts("Content: ");
my_puts(a1[i].str, a1[i].size);
puts("");
}
}
my_puts 也没漏洞, 不放了.
程序主要就是堆溢出漏洞.
由于保护全开, 所以想办法修改 hook. 那就必须要先泄漏 libc.
由于在 unsortbin 中的 chunk 的 fd / bk 保存了 bins[1] 的值, 同时有打印堆块功能. 于是可以尝试利用溢出, 覆盖 freed chunk 的 fd, 再 malloc, 造成堆重叠. Fastbin attack 比较好实现堆重叠. 但是, 我们无法知道堆块的地址. 不过, 由于初始化时, 堆其实地址要进行页对齐 (0x1000), 所以, 我们其实是知道每个堆块 低 12 位 的值, 或者说, 相对位置.
那么可以这样想, 假如我 free 了两个大小相同的 fast chunk, 那么头节点的 fd 就已经指向了堆处了. 然后可以通过溢出, 覆盖 fd 的低 8 位, 从而使其指向其他堆的其他位置. 如果这个位置正好是另一个堆的位置, 那么我们就成功造成了堆重叠.
于是, 构造如下左图这样的堆. 其中, chunk 0, 1, 2, 3 都是 fast chunk, chunk 4 是 small chunk, 也就是我们尝试 free 掉然后去泄漏 libc 的地方. 由于在 unsortbin 中的 chunk free 时会尝试合并到 top chunk, 所以这里还需要一个堆 chunk 5 块顶在上面, 才能确保 chunk 4 被放入到 unsortbin 中. 这里我们先 free 掉 chunk 4.
首先, 需要构造一个指向堆某处的 fd. 可以先后 free 掉 chunk 2, 1. 这样根据 fastbin 的机制, chunk 1 的 fd 指针就会指向 chunk 2 处, 上中图所示. 然后对 chunk 0 进行 Fill 操作, 覆盖 chunk 1 的最低位字节为 0x80, 这样就使其指向了 chunk 4. 如上右图.
接下来, 如果想要 malloc 到 chunk 4, 还需要改一下 chunk 4 的 size 以绕过检测. 对 chunk 3 进行 Fill 操作, 覆盖 size 为 0x21 即可. 然后申请两个 0x20 的堆块, 第二个就是 chunk 4 所在位置了. 打印 Chunk 2, 就能够得到 libc 上的 &bins[1] 地址了.
后半段在这里