SECCON 2019 Online CTF Writeup

f:id:hikalium:20191020154255p:plain
follow-me

Harekazeの一員として参加しました。4724点を得て14位だったようです。

私は2問に取り組み、うち1問を解けたのでメモ。

follow-me (reversing)

ちょっとした計算機アプリのバイナリが渡される。そのプログラムをIntel Pin を利用したトレーサでトレースした結果と、そのトレーサのソースが与えられる。このトレース結果が同一になるような、アプリケーションへの入力を求めてねという問題。

与えられているトレース結果はこんな感じ。

{"event": "image_load", "image_name": "/home/tomori/follow-me/build/sample/calc", "image_id": 1, "base_addr": "0x55f6b4d44000", "image_size": "0x1377"},
{"event": "image_load", "image_name": "/lib64/ld-linux-x86-64.so.2", "image_id": 2, "base_addr": "0x7f13ae220000", "image_size": "0x26c23"},
{"event": "image_load", "image_name": "[vdso]", "image_id": 3, "base_addr": "0x7ffc2b775000", "image_size": "0x100a"},
{"event": "image_load", "image_name": "/lib/x86_64-linux-gnu/libc.so.6", "image_id": 4, "base_addr": "0x7f1399a39000", "image_size": "0x3f0adf"},
{"event": "branch", "inst_addr": "0x55f6b4d445de", "next_inst_addr": "0", "branch_taken": true},
{"event": "branch", "inst_addr": "0x55f6b4d44f44", "next_inst_addr": "0", "branch_taken": false},
{"event": "branch", "inst_addr": "0x55f6b4d44765", "next_inst_addr": "0", "branch_taken": true},

最初の行のbase_addrと、各分岐結果のinst_addrを差し引きすることで、実行バイナリ内のオフセットがわかる。

h_noson師匠が数秒で入力の文法と分岐の解析結果を与えてくれたので、あとは入力結果を推測するだけという簡単なお仕事。

0-9 (c): val = c - 0x30 + val * 10
,: push val to stack
+: pop twice and push(x + y)
-: pop twice and push(x - y)
*: pop twice and push(x * y)
m: pop twice and push(min(x, y))
M: pop twice and push(max(x, y))
C: pop twice and push(x C y)

たとえば./calc '5,3,+'の結果は8になる。そんなかんじ。 数値は複数桁になることもできて、先頭が0埋めでも問題ないことを確認した。

0xc1c (false): 0-9
0xbe9 (false): ,
0xc58 (false): +
0xcaf (false): -
0xd06 (false): *
0xd5d (false): m
0xdb4 (false): M
0xe08 (false): C

また、演算子による分岐だけでなく、計算時の値によっても分岐結果が変わることがわかった。

乗算は、左辺の値-1回、0xa1fを通る。

r '13,3,*' -> 0xa1fは12回呼ばれる

加算は、(右辺の値%10) + 1回、0xa1fを通る。

r '13,18,+' -> 0xa1fは9回

そして、0xe87の分岐がtrueのときに項を読み続けるので、これを境界としてトレース結果を分割し、上記の結果を総合して推測すればよい。

たとえば、以下のようなトレース列は

e87", "next_inst_addr": "0", "branch_taken": true},
be9", "next_inst_addr": "0", "branch_taken": true},
c1c", "next_inst_addr": "0", "branch_taken": true},
c58", "next_inst_addr": "0", "branch_taken": true},
caf", "next_inst_addr": "0", "branch_taken": true},
d06", "next_inst_addr": "0", "branch_taken": false},
8dc", "next_inst_addr": "0", "branch_taken": true},
8dc", "next_inst_addr": "0", "branch_taken": true},
a5b", "next_inst_addr": "0", "branch_taken": true},
a81", "next_inst_addr": "0", "branch_taken": true},
a0b", "next_inst_addr": "0", "branch_taken": true},
a1f", "next_inst_addr": "0", "branch_taken": true},
a1f", "next_inst_addr": "0", "branch_taken": true},
a1f", "next_inst_addr": "0", "branch_taken": true},
a1f", "next_inst_addr": "0", "branch_taken": true},
a1f", "next_inst_addr": "0", "branch_taken": true},
a1f", "next_inst_addr": "0", "branch_taken": true},
a1f", "next_inst_addr": "0", "branch_taken": false},
a81", "next_inst_addr": "0", "branch_taken": false},
93e", "next_inst_addr": "0", "branch_taken": true},
d54", "next_inst_addr": "0", "branch_taken": true},
  • 0xd06の分岐がfalseなので乗算
  • 0xa1fの分岐を7回通っているので、この演算が実行される際の左辺の値は8

ということがわかる。

トレース結果より、入力列は

ccc,ccc,ccc,ccc,ccc,cccc,ccc,mm-mM-ccc,ccc,ccc,mm-ccc,ccc,ccc,ccc,ccc,-+-M+ccc,ccc,ccc,mm*

と予想できる(cは数字が入る)。これは実際に値が1つに収束するのでよさそうである。

加算と乗算に気をつけつつ、後ろ側からいい感じに値を割り当てていく。たとえばこんなかんじに。

001,002,003,004,005,0006,007,mm-mM-001,002,003,mm-008,005,001,004,001,-+-M+001,002,003,mm*

あとは問題の指示通りにサーバーに答えを投げてあげれば、フラグを得ることができた。

{"error":false,"flag":"SECCON{Is it easy for you to recovery input from execution trace? Keep hacking:)}","message":"Thanks! I'll give you a flag as a thank you."}

repair (forensics)

解けなかった…。

問題としては、壊れたAVIファイルが与えられ、その先頭1セクタ(512Bytes)が欠損しているので、なんとかしてその部分(つまりはヘッダ)を復元してねーという話だった。

解析プログラムをささっと書いて、だいたいどんなデータ構造か、フレーム数はいくつかなどを調べて、それっぽいパラメータでffmpegで動画を生成し、先頭512Bytesをつぎはぎするなどしたが、codecが特定できず断念した。

AVI file reader for SECCON 2019 Online CTF

moviリスト内の00dcというチャンクがフレームのデータで、こんな感じで並んでいる。

@+0x00146E: LIST                                                                                     
  size = 2789052                                                                                  
  movi                                                                                               
  @+0x00147A: 00dc                                                                                
    size = 32440                                                                                     
   00 00 7E B8 03 C0 02 1C                                                                        
   00 01 10 00 7E AE 00 00 00 00 02 1C 03 C0 20 00 00 04 22 00 00 0A 00 00 00 00 00 00 32 00 7E 94...
  @+0x00933A: 00dc                                                                                
    size = 4086                                                                                      
   01 00 0F F6 03 C0 02 1C                                                                        
   00 01 11 00 0F EC 00 00 00 00 02 1C 03 C0 20 00 00 04 22 00 00 04 31 00 0F D8 00 00 00 00 00 00...
  @+0x00A338: 00dc                                                                                
    size = 4086                                                                                      
   01 00 0F F6 03 C0 02 1C                                                                        
   00 01 11 00 0F EC 00 00 00 00 02 1C 03 C0 20 00 00 04 22 00 00 04 31 00 0F D8 00 00 00 00 00 00...

各フレームデータの先頭8bytesは以下のようなフォーマットになっているっぽい

     00 00 7E B8 03 C0 02 1C 
     ^^ 0ならキーフレーム, 1なら中間フレーム
           ^^ ^^ ビッグエンディアンでこのフレーム全体のサイズ(00dcのサイズと一緒)
                 ^^ ^^ 960(Width)
                       ^^ ^^ 540(Height)

ということがわかったので、ビッグエンディアンでこういうフォーマットになっているcodecないかなと探したり、ffmpegのcodecを片端から試してこのような出力が得られないか試したが間に合わなかった。かなしい。

参考文献: OpenDML AVI File Format Extensions

まとめ

CTFは生活リズムを破壊する(たのしいので)

あとAVIファイルのフレームデータの区切りがHex上で見えるようになったのでよかった。