CTFするぞ

あたまよくないけどがんばります

SECCON CTF 2019 FinalのWriteup

2019年もSECCONが秋葉原で開催されました。 今回はNaruseJunで国際決勝に参加し、最終的には4323点を獲得して1位でした。

f:id:ptr-yudai:20191224142428p:plain

SECCONについて

国内チームも参加しやすい敷居の低い競技を毎年開催していただき感謝です。 ただ年々セキュリティコンテストからプログラミングコンテストへ移行している気がします。 KoHに適する問題を作るのは難しいでしょうが、だからといって競プロみたいな問題を出されても参加者はがっかりするだけです。 レポート締め切り間近の高専生ではあるまいし、問題はもっと時間を掛けて丁寧に作って貰いたいです。

問題について

問題概要は書きます。問題名は相変わらずありませんでした。

1. UDPのやつ

プログラミング問題です。 各チームにポートが割り当てられており、チームトークンを与えた後にUDPのポートが指定されるので制限時間内に10回UDPで同じトークンを与えれば良いです。 初日は最初に与えたトークンを紛失したので完全に何もできない状態になりました。 まぁそういうことは想定してなかったんでしょうが、問題は再起動してくれませんでした。

2. 画像

プログラミング問題です。 去年の使い回しらしく、チームメンバーが去年のスクリプトでAttack Flagを瞬殺しました。 Defenseもチームメンバーが頑張っていくらか取ってくれました。 私は去年国際には参加していないのであまり言及しませんが、問題を使い回すのは本当にやってはいけないことの1つだと思います。

3. 謎pwn

pwn問です。 これに関しては作問を頑張ったのは伝わりました。 ただ、懇親会で作問者の方にも言いましたが、意味不明なアーキテクチャを大量に出すことで難易度を挙げるのはご法度なので今後やめていただきたいです。 意図としてはアーキテクチャごとのexploitのサイズなどを体感して欲しいという目的が大きかったようです。 しかし、問題はShortest Exploitなのでx64でも良かったのでは?と思います。 例えばBOF, FSB, Heap Exploitなどの普通の問題に対してそれぞれ最短のexploitを書いたチームに防御点が入る、といった仕組みなら面白かったです。

4. ブラックボックス

予選に出た実行トレースから入力を当てるやつが4つ出ました。 これはチームメイトがやってくれましたが、面白かったそうです。

5. 蛇ゲーム

プログラミング問題です。 チームメンバーが最強のAIを書いてくれたのでずっと防御点を稼いで勝ちました。 今回はサーバーにAIの挙動を書く問題でしたが、クライアントサイドを変えるとRCEがあるのは作問者の方は気づいていたのでしょうか。

6. Jeopardy

Jeopardyもありました。私はpwn2つとhardware1つを解きました。

QR Decoder

QRコードを投げるとデコードした結果を読んで出力するバイナリが渡されます。 webインターフェースのURLにflag1.htmlとかflag2.htmlとか入れるとフラグが全部手に入りました。 終盤に確認したら同じ手法では取られなくなっていました。 流石に笑えないです。

この問題について考察しました。 2日目に解こうとしたチームが総じて解き方を聞いてきたので私もバイナリを読んでみました。 QRコードの中身を画像ファイルのサイズだけ読んでしまうBOFがありますが、PIEとRELROが有効で、かつone shotなexploitを書く必要があるので自明な解法はなさそうです。 これは想像ですが、たぶん作問者の書いたMakefileは次のようになっています。

all:
  gcc qrdecoder.c -o qrdecoder -fno-stack-protector

私の予想としては、作問者がこれを古いgccコンパイルしたためPIEやRELROが無効になったが、本番前のどこかの段階で新しいgccで再度コンパイルされてしまったためRELROとPIEが掛かってしまったのだと思います。 zbarimgなどが呼ばれているのでその辺りにOS command injection的なものがあるのかとも思いましたが、もしそうならzbarのバージョンを記載するはずですし、バイナリにする意味もありません。 ということで、作問ミスだと思います。(間違ってたらごめんなさい。)

Bad Mouse

ハードウェア問2つは面白かったです。(mimuraはダンプの解析をサボって解けなかったです。) こっちはタイトルの通りBad USBのマウス版です。

実際にPCに刺すと「The flag is」のようにマウスが動いて描いてくれますが、そこから徐々にカーソルの速度が落ちたので、終わらないと判断してファームウェアを解析することにしました。

AVRなのでバイナリにした後はghidraで読みましたが、特にio命令がよく分かりませんでした。 最初の数時間は頑張って読んでいましたが、kriwさんがDigiSpark用のファームウェアだと気づいて教えてくれたので、マウス操作をするコードを実際にArduino IDE+DigiMouseでビルドしました。 ビルド結果をGhidraで読んだものと問題のバイナリを比較し、移動やクリックなどの命令列を明らかにしました。 それでも結構ややこしくて、ghidraのデコンパイル結果にいろいろ名前とか付けながらさらに5時間近く格闘しました。 以下は読みやすくしたloop関数のコードです。

void loop(undefined2 uParm1,byte bParm2,undefined2 uParm3)

{
  byte bVar1;
  byte bVar2;
  char cVar3;
  undefined uVar4;
  byte bVar5;
  undefined uVar6;
  undefined uVar7;
  undefined uVar8;
  char delta;
  char dir;
  char _delta;
  
  uVar8 = Yhi;
  uVar7 = Ylo;
  read_volatile(VAR_position);
  uParm1 = CONCAT11(R23R22._1_1_,6);
  W = div_mod_qi4();
  bParm2 = W._1_1_;
  bVar1 = read_volatile_1(VAR_offset_lo);
  cVar3 = read_volatile_1(VAR_offset_hi);
  R23R22._0_1_ = bVar1 + (byte)W;
  R23R22._1_1_ = cVar3 + R1 + CARRY1(bVar1,(byte)W);
  if ((W & 0x80) != 0) {
    R23R22._1_1_ = R23R22._1_1_ + -1;
  }
  bVar1 = (byte)R23R22 + 0x20;
  Z = CONCAT11(R23R22._1_1_ - ((bVar1 < 0xe0) + -1),bVar1);
  bVar2 = -(byte)R23R22 - 0x3f;
  uParm3 = CONCAT11(-1 - (R23R22._1_1_ + (bVar2 < (byte)R23R22)),bVar2);
  Z._0_1_ = (char)(*(uint *)(uint3)(Z >> 1) >> ((uint)bVar1 & 1)) + bVar2 & 0x3f;
  while( true ) {
    Z._1_1_ = 0;
    bParm2 = bParm2 - 1;
    if ((bParm2 & 0x80) == 0x80) break;
    Z._0_1_ = (byte)Z >> 1;
  }
  Ylo = (byte)Z & 1;
  Yhi = '\b';
  do {
    delta = read_volatile_1(__deltaY);
    if (delta == -0x80) {
      delta = -0x7f;
    }
    write_volatile_1(VAR_click,Ylo);
    write_volatile_1(VAR_deltaX,R1);
    write_volatile_1(VAR_deltaY,delta);
    write_volatile_1(ADCH,R1);
    do_mouse_event();
    Yhi = Yhi + -1;
  } while (Yhi != 0);
  dir = read_volatile_1(VAR_position);
  _delta = read_volatile_1(__deltaY);
  if (_delta == 1) {
    cVar3 = dir + 1;
    W = CONCAT11(cVar3,dir);
    write_volatile_1(VAR_position,cVar3);
    if ('\v' < cVar3) {
      write_volatile_1(__deltaY,0xff);
      write_volatile_1(VAR_position,dir);
      write_volatile_1(VAR_click,Ylo);
      write_volatile_1(VAR_deltaX,4);
      write_volatile_1(VAR_deltaY,R1);
      write_volatile_1(ADCH,R1);
      W = do_mouse_event();
      Ylo = uVar7;
      Yhi = uVar8;
      return;
    }
  }
  else {
    bVar1 = dir - 1;
    W = CONCAT11(bVar1,dir);
    write_volatile_1(VAR_position,bVar1);
    if ((bVar1 & 0x80) != 0) {
      write_volatile_1(__deltaY,1);
      write_volatile_1(VAR_position,dir);
      uVar4 = read_volatile_1(VAR_offset_lo);
      uVar6 = read_volatile_1(VAR_offset_hi);
      W = CONCAT11(uVar6,uVar4);
      W = W + 2;
      write_volatile_1(VAR_offset_hi,W._1_1_);
      write_volatile_1(VAR_offset_lo,(byte)W);
      write_volatile_1(VAR_click,Ylo);
      write_volatile_1(VAR_deltaX,4);
      write_volatile_1(VAR_deltaY,R1);
      write_volatile_1(ADCH,R1);
      do_mouse_event();
      bVar5 = read_volatile_1(VAR_offset_lo);
      cVar3 = read_volatile_1(VAR_offset_hi);
      bVar2 = (bVar5 < 0x43) + 2;
      bVar1 = cVar3 - bVar2;
      W = CONCAT11(bVar1,bVar5);
      if (bVar2 <= bVar1) {
        write_volatile_1(VAR_offset_hi,R1);
        write_volatile_1(VAR_offset_lo,R1);
      }
    }
  }
  Ylo = uVar7;
  Yhi = uVar8;
  return;
}

5時過ぎになってもフラグの上半分しか求まらず死にかけていましたが、脳死でいろいろ試したらフラグが出てきました。

from ptrlib import u16
from PIL import Image

with open("firmware.bin", "rb") as f:
    f.seek(0x20)
    s = f.read(0x40 * 9)
w = b''.join([bytes([i]) for i in range(0x3f, 0x7f)])

img = Image.new('L', (600, 8 * 13), 255)

offset = 0
dd = 0
delta = 1
x, y = 0, 0
while True:
    try:
        """
        offset = offset + (dd // 6)
        bVar1 = (offset + 0x20) & 0xff
        Z = (offset & 0xff00) | bVar1
        bVar2 = (-(offset & 0xff) - 0x3f)
        Z = ((u16(s[Z-0x20:Z-0x20+2]) >> (bVar1 & 1)) & 0xff) + bVar2 & 0x3f
        """
        bVar2 = -(offset + (dd // 6)) - 0x3f
        Z = s[offset + (dd // 6)] + bVar2 & 0x3f
        #Z = (u16(s[Z >> 1:(Z >> 1) + 2]) >> (bVar1 & 1)) + bVar2 & 0x3f
        #print(Z)
        #Z = (offset & 0xff00) + bVar1
        #Z = ((s[(Z >> 1) >> (bVar1 & 1)]) + bVar2) & 0x3f
    except Exception as e:
        print(e)
        break
    c = dd % 6
    while True:
        c -= 1


        if c < 0: break
        Z >>= 1
    click = Z & 1
    
    for i in range(8):
        img.putpixel((x, y), (click ^ 1) * 255)
        y += delta
    if delta == 1:
        dd += 1
        if dd > 11:
            dd = 11
            delta = -1
            x += 1
            img.putpixel((x, y), (click ^ 1) * 255)
            continue
    else:
        dd -= 1
        if dd < 0:
            dd = 0
            delta = 1
            x += 1
            offset += 2
            img.putpixel((x, y), (click ^ 1) * 255)
            b = 0
            if (offset & 0xff) < 0x43:
                b = 1
            if b + 2 <= (offset >> 8) - (b + 2):
                break

img.resize((1200, 8*13)).save("flag.png")

突然出てきたもんでびっくり。

f:id:ptr-yudai:20191222155510p:plain

ハードウェア系はやっぱり本戦ならではの問題で解いてて楽しいです。(さすがに6時間経過したくらいから辛かったけど)

おわりに

国内のCTFerがここまで集まる機会は早々無いので、そういう意味ではSECCONを今後も続けて欲しいです。 しかし既に多くの方が言及しているように、作問したくない/できないのであればカンファレンスと展示、ワークショップだけでも十分だと思います。 SECCONは「日本最大のハッキング大会」と位置付けられる大きな大会ですので、CTFをやるなら本気で運営していただきたいです。 ということで、優勝できたのは嬉しかったですが、何とも言えない感情になりました。 面白い問題もありましたし、懇親会とかでいろんな人と話せたのは楽しかったです。 運営、参加者のみなさん、お疲れ様でした。