CTFするぞ

CTF以外のことも書くよ

zer0pts CTF 2022開催記

はじめに

zer0pts CTF 2022を開催しました。 今年は論文・就職勢がいたり国際オンサイトCTF(?)と開催日が被っていたりでアクティブなメンバー数が少なかったです。 参加された方の中には「st98さんのwebがないぞ?」や「yoshikingの暗号がないやん!」とブチ切れた方もいるかと思いますが、来年はきっと彼らも問題を作ってくれます。 一方今年はkeymoon先生が長年温めた難問を出してくれたので、特にwebはいつもと違う問題セットになっていたかと思います。

問題のソースコードと解法はそのうちGitLabにアップロードします。 Writeupもなるべく1つにまとめてDiscordでアナウンスする予定です。

出題した問題

いくつか問題を提供したので、軽くwriteupを書きます。 簡単な問題については、今回全然解けなかったよ〜という方へのアドバイス(?)も含めていますので、参考にしてください。

ジャンル 問題 想定難易度 キーワード
rev service warmup Windows, IAT, 難読化
q-solved easy quantum, Grover'sのアルゴリズム, SAT
dreamland medium-hard ストリーム暗号, 難読化, Z3
crypto Anti-Fermat warmup RSA, 二次方程式
web GitFile Explorer warmup ディレクトリトラバーサル
miniblog++ ? 正規表現, SSTI
miniblog# hard SSTI, Zip Slip, zipファイル
pwn accountant easy Integer Overflow, Stack OOB, 剰余方程式
MemSafeD medium-easy D言語, OOB, NULL Pointer Dereference
sbxnote medium prlimitシステムコール, サンドボックス
kvstore medium-hard memcmp, fclose, double free
kRCE hard kernel, OOB
redis-lite lunatic real-world, Fuzzing, Heap Spray, Call Oriented Programming, Race Condition, Heap Overflow, Integer Overflow, Multi-Thread
misc MathHash easy IEEE754, 三角関数
0AV medium-easy fanotify, namespace, unshare, MS_BIND

rev

service (90 solves)

IDAやghidraで見るとWindowsサービス関連のAPIを叩きまくっていますが、実際には暗号関連のAPIです。 これはPEのIATとPE importsが必ずしも一致している必要がないことを利用しています。 現状のデコンパイラはPE importsだけを見るため間違った結果を表示してしまいます。 デバッガで動的に調べると正しいAPIが表示され、SHA256を計算していることが分かります。

IDAやghidraで読んでも分からない時はデバッガで追えば大抵の場合解決しますので、reversingを始めた方は動的解析も試してみてください。

q-solved (8 solves)

量子コンピューティングに関する問題を1つ出す予定で、何を出すか悩んだあげく錬成された謎rev問です。 本当はもつれ判定をmiscで出そうと思っていましたが、大抵観測結果からguessできるのでやめました。

Schorのアルゴリズムは昔別のCTFで出題されていたので、対抗して(?)Groverのアルゴリズムを出題しました。 量子回路を作るルーチンを見ると自明に探索アルゴリズムであることが分かります。 初期化と振幅増幅、観測を取り除いたオラクルのみを読めば良いのですが、すべてmulti-controlled X gateを使っています。 これは明らかに入力量子ビットから論理演算を計算しているだけなので、回路のjsonファイルはそのままCNFに落とせます。 ちなみに増幅回数を見ると解が1意に定まることも分かりますが、それは鶏と卵なので今回はあまり関係ないです。

ところで回路の構築方法が間違っていたそうなのでQiskitを道連れに岬から飛び降ります。

dreamland (11 solves)

MICKEYという暗号スキームを魔改造した暗号手法を使ってフラグを暗号化しています。 難読化ポイントとしてはFOR文をすべてマクロを使ってsetjmpとlongjmpに置き換えていますが、これは読む上でそこまで苦にはなりません。

この問題の難しいポイントその1は、未知変数を使った条件分岐をどのようにZ3に計算させるかです。 リバージングすると、下のようなコード(sが求めたい配列)が出てきます。

void __nightmare_s(u8 s[], u8 is, u8 cs) {
  ...
  if (cs == 0) {
    FOR (i, 0, 100, {
        s_clocked[i] = s_i[i] ^ fb0[i] ^ fb;
      });
  } else {
    FOR (i, 0, 100, {
        s_clocked[i] = s_i[i] ^ fb1[i] ^ fb;
      });
  }
  ...
}
...
  u8 cs = s[51] ^ r[4];
...
  __nightmare_s(s, is, cs);
...

Z3のFunctionやSelectは非常に遅いので、条件分岐を1つにまとめる必要があります。 そのために、csの状態で分岐してfb0, fb1を使って計算するのではなく、fb0, fb1の状態からcsを使って計算する式に変形します。

cs fb0[i] fb1[i] 計算
0 0 0 s_i[i] ^ fb0[i] ^ fb;
0 0 1 s_i[i] ^ fb0[i] ^ fb;
0 1 0 s_i[i] ^ fb0[i] ^ fb;
0 1 1 s_i[i] ^ fb0[i] ^ fb;
1 0 0 s_i[i] ^ fb1[i] ^ fb;
1 0 1 s_i[i] ^ fb1[i] ^ fb;
1 1 0 s_i[i] ^ fb1[i] ^ fb;
1 1 1 s_i[i] ^ fb1[i] ^ fb;

これをfb0, fb1を入力とした形に書き直すと、

fb0[i] fb1[i] cs 計算
0 0 0 s_i[i] ^ 0 ^ fb;
0 0 1 s_i[i] ^ 0 ^ fb;
0 1 0 s_i[i] ^ cs ^ fb;
0 1 1 s_i[i] ^ cs ^ fb;
1 0 0 s_i[i] ^ cs ^ 1 ^ fb;
1 0 1 s_i[i] ^ cs ^ 1 ^ fb;
1 1 0 s_i[i] ^ 1 ^ fb;
1 1 1 s_i[i] ^ 1 ^ fb;

となり、既知の状態から未知の状態を使った式、つまりZ3に適した形に直せます。

難しいポイントその2は、状態が一意に定まらないことです。 ストリーム暗号なので1ラウンドごと状態を戻していくのですが、全単射ではなく特定の状態になる元状態を複数持つことがあります。 したがって、バイナリ中にあるASCIIチェックからフラグに制約を付け、誤った解が出た時点でロールバックする深さ優先探索を書く必要があります。

この問題はもともとBSidesに出す予定でしたが、深さ優先をバグらせていたまま放置していたものを、keymoon君がサッと直してくれたので出せました。

pwn

pwn以外のジャンルはつれづれなるままに書いているのですが、pwnには方針があります。(zer0pts CTFに何度も参加された方ならお気づきかもしれません)

  • warmupからlunaticまで幅広く出題する
  • Kernel問あるいはBrowser問を少なくとも1つ→kRCE
  • C/C++言語の問題を少なくとも1つ→MemSafeD
  • ヒープがちゃがちゃ問を多くとも1つ→kvstore
  • システムコールにまつわる問題を少なくとも1つ→sbxnote
  • 数学やパズル力が要求される問題を少なくとも1つ→accountant

このように浅くても広い知識を要求することで、参加者が飽きない問題セットを作れていると信じています。信じていいよね?

accountant (9 solves)

warmupの可能性すらあるとか言ってたくらい簡単なのですが、9チームにしか解かれませんでした・・・。

純粋integer overflowによりallocaで確保したサイズよりも書き込めるサイズの方が大きくなります。

  ssize_t n = get_value("Number of items: ");
  if (n <= 0) {
    puts("Invalid value");
    return 1;
  }

  if ((items = safe_alloca(n * sizeof(Item))) == NULL) {
    use_malloc = 1;
    if ((items = calloc(n, sizeof(Item))) == NULL) {
      puts("Memory Error\n");
      return 1;
    }
  }

入力を途中で打ち切りはできないのでnが大きくても意味がないように思えますが、

void input_all_data(Item *items, int n) {
  for (int i = 0; i < n; i++) {
    input_data(items, i);
  }
}

int64_t calc_total(Item *items, int n) {
  int64_t total = 0;
  int i = n - 1;
  do {
    total += items[i].price * items[i].quantity;
  } while(i-- > 0);
  return total;
}

と引数の型がintになっていて、こっちでもinteger overflowがあるので0x2000000000000000のような値を渡せばループ回数を減らせます。

これでRIP controlは可能なのですが、PIEが有効なのでアドレスリークが必要です。 上のコードを良くみると、(int)nが0のときinput_all_dataは回りませんが、calc_totalのループが1回だけ走ります。 このときitems[0]は未初期化で、中に_startだったかのアドレスが入っているので、アドレスの上位32-bitと下位32-bitを掛けた結果が貰えることになります。

プログラムのアドレスの上位32-bitは0x5555から0x5700の範囲にあるはずなので、この範囲で剰余方程式を解いて、解がある場合はその下位12-bitが_startのものかを調べれば高確率でアドレスリークができます。

pwn初心者の方は簡単なバッファオーバーフロー問題を解けるのに難しいCTFのBOF問は解けないことが多いですが、難しいバッファオーバーフローの問題は十中八九整数オーバーフローとの組み合わせなので、算術演算やシフト演算などに注目して、値がオーバーフローしないかを確認するとワンステップ上の問題が解けるようになるかと思います。

hackmd.io

MemSafeD (19 solves)

去年go言語のpwnを出したので今年はD言語の問題を出しました。 当然D言語を書いたのも初めてだったので、汚いコードだったかもしれません。

さて、D言語はガベージコレクタを使っているので基本ヒープ周りの脆弱性は発生しませんが、公式ドキュメントのリリースモードで推奨されているオプションでは範囲外参照は検知してくれなかったので、それをメインに問題にしました。 今回の問題では3つのポイントがあります。

まず1つ目がアドレスリーク。D言語のsafeモードでは基本的にアドレスは漏洩しませんが、setbufを使うためにtrustedを適用しているmain関数で例外をキャッチして出力しています。これは通常safeモードなら禁止されていますが、trustedなので動作してしまい、例外情報からアドレス情報が漏洩しています。

2つ目が範囲外参照です。

  /* Set the position of a vertex */
  void set_vertex(ulong index, vertex v) {
    if (index > _vertices.length - 1)
      throw new Exception("Invalid index");

    _vertices[index] = v;
  }

今回のコンパイルオプションでは範囲外参照が検出できないので、上のコードでもしindexをvertices.lengthより大きい値にできれば範囲外参照がおきます。

3つ目がデストラクタを持つ構造体のmoveです。 D言語のドキュメントを読んだところ、構造体の所有権を移す際に、その構造体がデストラクタを持つときに限って所有権を捨てる側のメンバ変数が全て0に初期化されます。 これを利用するとPolygon構造体のvertices, vertices.lengthが0になり、範囲外参照が起きます。 たぶんPolygon構造体の空デストラクタを消すと解けないですが、まぁそんなことまで気にして解いたチームはいないでしょう。

これによりNULLポインタ参照の範囲外参照、つまり任意アドレスへの書き込みが成立します。 あとは適当にD言語君が持っているvtableっぽい場所など、適当な関数ポインタを書き換えてstack pivotなりCOPなりすれば終わりです。

pwn初心者の方は、D言語ということで問題を見ただけで捨てた方もいるかもしれませんが、どんな言語が出されてもまずは解こうとしてみましょう。 このようなマイナー言語やマイナーアーキテクチャの問題というのは、その言語で出題されている意味があります。 マイナー言語や謎アーキテクチャ上で通常のpwn(ヒープガチャやただのROP)を出している人がいたら、それはみんなの知らないアーキテクチャを使うことでしか難易度を担保できない残念な作問者なので解かなくても良いです。 そうでない問題の場合はほとんど、その言語やアーキテクチャ特有の脆弱性がテーマになっており、それを見つければ後は簡単に解けるようになっています。(当然その言語特有の挙動が難しいこともありますが。)

なので、変な問題が出たらサービス問だと思って挑戦してみてください。

hackmd.io

sbxnote (6 solves)

今年のわくわくシステムコール問です。 一昨年はwritev、去年はMMAP_FIXED、BSidesではprocess_vm_writeを出しました。今年は何かな?

内容は単純なヒープnoteですが、インターフェースは子プロセス、メモリ管理は親プロセスが担っており、子プロセス側は次のseccompで守られています。

 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x11 0xc000003e  if (A != ARCH_X86_64) goto 0019
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x0f 0x00 0x40000000  if (A >= 0x40000000) goto 0019
 0004: 0x15 0x0e 0x00 0x00000002  if (A == open) goto 0019
 0005: 0x15 0x0d 0x00 0x00000101  if (A == openat) goto 0019
 0006: 0x15 0x0c 0x00 0x0000003b  if (A == execve) goto 0019
 0007: 0x15 0x0b 0x00 0x00000142  if (A == execveat) goto 0019
 0008: 0x15 0x0a 0x00 0x00000055  if (A == creat) goto 0019
 0009: 0x15 0x09 0x00 0x00000039  if (A == fork) goto 0019
 0010: 0x15 0x08 0x00 0x0000003a  if (A == vfork) goto 0019
 0011: 0x15 0x07 0x00 0x00000038  if (A == clone) goto 0019
 0012: 0x15 0x06 0x00 0x00000065  if (A == ptrace) goto 0019
 0013: 0x15 0x05 0x00 0x0000003e  if (A == kill) goto 0019
 0014: 0x15 0x04 0x00 0x000000c8  if (A == tkill) goto 0019
 0015: 0x15 0x03 0x00 0x000000ea  if (A == tgkill) goto 0019
 0016: 0x15 0x02 0x00 0x00000136  if (A == process_vm_readv) goto 0019
 0017: 0x15 0x01 0x00 0x00000137  if (A == process_vm_writev) goto 0019
 0018: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0019: 0x06 0x00 0x00 0x00000000  return KILL

たぶんUbuntu 20.04 LTSだとこれは回避できません。

子プロセスには自明BOFがあり任意のシェルコードが実行可能ですが、当然seccompによりコマンド実行はできません。 親プロセス側の脆弱性を探すと、以下の箇所しか脆弱性がないはずです。

  uint64_t *old, *buffer = (uint64_t*)malloc(0);
  size_t size = 0;
...
        /* Allocate new buffer */
        old = buffer;
        if (!(buffer = (uint64_t*)malloc(req.size * sizeof(uint64_t)))) {
          /* Memory error */
          size = -1;
          RESPONSE(-1);
          break;
        }

つまりmallocが失敗するとsizeが異常に大きくなり、範囲外参照が可能になります。 NULLポインタの範囲外参照なのでMemSafeDのときと同じで任意アドレス読み書きができるようになります。

mallocを失敗させるには当然サイズをでかくしたいのですが、サイズ制限があります。

        if (req.size > 2800) {
          /* Invalid size*/
          RESPONSE(-1);
          break;
        }

また、メモリリークはないので、tcacheやfastbinなどをうまく使ってもこのサイズでは何回mallocを読んでもメモリ不足には陥りません。

そこで登場するのはprlimitシステムコールです。 このシステムコールは同じユーザーの他プロセスの資源を制限できる上、特段権限を必要としません。 つまり、このシステムコールを子プロセスから呼び出し、親プロセスのメモリ使用量を極小に設定してやった上で、mmapが発生するまでmallocを呼ぶとmallocが失敗し、親プロセスがexploitableな状態になります。

以上、システムコールと愉快な仲間たちでした。

kvstore (5 solves)

ヒープガチャガチャ問です。

脆弱性は2つあります。 1つはfcloseによるdouble free、もう1つはmemcmpの誤った使用です。

glibcにはreallocやstrtok, tmpnam等、設計が終わってる関数がたくさんありますが、昔からmemcmpの設計も酷いと思っていたので問題にしました。 memcmpというのは2つのメモリ領域を比較するにも関わらず片方のサイズしか受け取りません。*1 そのため、次のような使い方は誤りです。

Item *item_lookup(const char *key, size_t keylen) {
  for (Item *cur = top; cur != NULL; cur = cur->next) {
    if (memcmp(key, cur->key, keylen) == 0)
      return cur; /* Found item */
  }
  return NULL; /* Item not found */
}

keylenが大きいとkeyの範囲を超えて比較してしまうため、ヒープ上のデータを過剰に読んでしまいます。 これを使うと、1バイトずつヒープ上のデータがリークできるため、ヒープやlibcのアドレスリークが可能です。

次の脆弱性がdouble fcloseです。

      default: { /* exit */
        char ans;
        fclose(fp);

        if (!is_saved) {
          /* Ask when list is not saved */
          puts("The latest item list has not been saved yet.");
          puts("Would you like to discard the changes? [y/N]");
          scanf("%c%*c", &ans);
          if (ans != 'y' && ans != 'Y')
            break;
        }

        puts("Bye (^o^)ノシ");
        return 0;
      }

fcloseはfreeを呼びますが、FILE構造体サイズのチャンクはtcacheに送られます。今回FILE構造体と同じサイズのデータは別の場所で確保できないので、smallbinなどに入れることはできず必ずdouble freeチェックを通ることになります。 それだと一見uneploitableですが、実はtcacheのdouble freeチェックの実装を考えると簡単に回避可能です。 fcloseするとFILE構造体はクリアされ、free後にtcache keyが入ります。 しかし、その後fwriteをすると、バッファリング用のポインタがNULLになっているためlibcはバッファを新たに確保し、tcache keyの箇所に新たなポインタを書き込みます。 その後fcloseすると、再度FILE構造体をfreeしますが、fwriteの効果によりtcache keyが上書きされているため一切検知されずにdouble freeが可能です。

あとはunsorted binに入るまでfreeし、チャンクを分割して頑張ってtcache poisoningすればOKです。

kRCE (8 solves)

potetisenseiが以前「リモートのkernel exploitはsprayとかが難しいから大変」と言っていたので作ってみた問題です。 今回の問題はカーネルドライバに自明脆弱性が複数あるのですが、ユーザーランドから叩いているため他の便利構造体を確保したり、modprobeを呼んだりできないという難しさがあります。 範囲外参照を使えばカーネル空間のアドレスリークやAAR,AAWは簡単に実現できるため、カーネル空間は完全に制圧できます。

しかし、ユーアーランドアプリから出られないためシェルが取れないというのがこの問題の難しい点です。 カーネルドライバ→カーネルヒープ→カーネルスタック→ユーザーランドプログラム→ユーザーランドulibc→ユーザーランドスタックの順にアドレスリークして、カーネル空間の脆弱性を使ってユーザーランドでROPするという豪華exploitが想定解です。 他にもまぁまぁ手法があったようですが、カーネルランドの脆弱性でユーザーランドを攻撃する流れになっていれば全部OKです。

SECCONで発生した「ramfs上のフラグにジャンプする解法」を防ぐためにフラグの名前や形式を不明にしたのですが、「ramfs上のbusyboxを書き換えてrebootする解法」が出てきて終焉を感じました。 Kernel問でramfs使うのはもうダメですね。kone_gadgetで歴史が変わってしまった・・・。

redis-lite (3 solves)

CTFというのは残念なことに実世界で使われるような攻撃手法というのがなかなか出題されません。 CTFで出される単純なサービスというのはHeap Sprayなどの手法を使わない方が簡単に解けることが多く、逆に0-dayやn-dayのような問題は出題したところで大して面白くならない上に時間的に解かれづらいという欠点もあります。

そこで、今回はめちゃくちゃ頑張って実際にありそうだけど十分に小さいアプリケーション(redisサーバー)を一から書いて、ありそうな脆弱性を埋め込み、実用的なexploit手法でしか解けない問題を仕上げました。 実際には複数脆弱性があるのですが、今回攻撃可能なヒープオーバーフローの発見は文法ベースfuzzerなどで比較的簡単に見つかります。 redisのような複雑なアプリケーションだと、いわゆるglibc heap exploitationはまず諦めた方が良いです。 こういう場合は関数ポインタや文字列ポインタを探し、それを書き換えてRIPを取る方法が圧倒的に実用的です。

この問題の面白いところは、ヒープオーバーフローがあるものの無限ループ*2でreadを読むためヒープオーバーフローを起こしたところでその後何も操作ができないという点です。 redisではキーを特定の時間後に失効させられるのですが、それがスレッドで実装されています。 そこで、オーバーフロー前にスレッドを立ち上げた後にオーバーフローで関数ポインタを書き換えると、メインスレッドは入力待ち状態でも別スレッドから関数ポインタが呼ばれてRIPを制御できます。

また、関数ポインタを1バイトずつ書き換えて、サブスレッドがクラッシュすれば通信が切れるので、それをオラクルにlibcやヒープのアドレスを1バイトずつ特定できます。 そんなこんなでCOPなどをすれば任意コマンド呼び出しが可能になります。

ソケット経由なので当然RIP制御の後にstack pivotかCOPが必要だと思ったのですが、スレッドを複数建ててdupとone gadgetを組み合わせる意地でもone gadgetを使う解法があって面白かったです。gadgetが少ないときにはかなり実用的だと思いました。

hackmd.io

crypto

Anti-Fermat (125 solves)

CakeCTFのcrypto warmupに作りましたが、ふるつき先生にwarmupじゃないと言われたのでzer0pts warmupにやって来ましたこんにちは。 フェルマー法を防ぐために素数pをビット反転したものに近い素数を使っています。 ビット反転というのは2^{n}-1-pなので、

n = p(2^{n}-1-p + \delta)

p^{2} - (2^{n}-1 + \delta)p + n = 0

という2次方程式が立ちます。deltaを総当りして2次方程式を解けば素因数分解できて終わります。

web

st98さんのweb問は......どこだぁ? :pleading_face:

GitFile Explorer (181 solves)

直前になってもwebが2問しかなかったので作りました。 PHPではファイルパスを先に解決してからアクセスするので

https//github/../../../../../flag.txt

のように存在しないフォルダを跨いでファイルにアクセスできます。

Zer0TP

keymoon先生がmiscでdeflate compression oracleなるものを発明していたのですが、「webサービスに乗っけた方が面白くね?」というと「作って」と返されたので開催前日から当日にかけて作りました。 その割には割と妥当な問題設計で非想定解も生まれなかったので良かったと思います。

去年のweb warmupが14 solveくらいしかなくて泣いたので、今年は妥当難易度になってよかったです。

miniblog++ (75 solves)

正規表現チェックが甘いのでSSTI可能です。 決して非想定解などではありません。 決して非想定解などではありません。 決して非想定解などではありません。

miniblog♯ (6 solves)

決して非想定解を修正した問題を出題した訳ではありません。 決して非想定解を修正した問題を出題した訳ではありません。 決して非想定解を修正した問題を出題した訳ではありません。

pwnを勉強し始めた頃にInterKosenCTFでziplistという問題を出題したのですが、このときからZIPファイルの仕様ってかなりゴミでは?という思いがありました。 というのも、ZIPファイルはファイルの追加や更新を簡単にするために、メタ情報(EDH; End of Directory Header)をZIPファイルの末尾に付けています。 EDHが固定サイズなら後ろから固定サイズ読めば問題ないのですが、実はEDHにはコメントが追加でき、そのコメントは当然可変長な訳です。 そして一番ゴミだと思うのが、コメントのサイズ情報がコメントよりも前に保持されるという点です。

ではZIPユーティリティはどのようにZIPをパースするかと言うと、ファイル末尾からEDHのマジックナンバーを探すO(n)の処理が入っています。 これだけでも嫌な気持ちになりますが、さらにコメントの文字に制約はないため、コメントにEDHっぽい構造を入れるとそっちをEDHと認識してしまい、コメント部分に自由なZIPファイルを入れることができます。

miniblog#では、サーバー側でブログ記事をzipでまとめてユーザー名を含む署名をコメントに入れて暗号化します。 暗号化してエクスポートしたバックアップファイルを再度インポートできるのですが、もしユーザー名に上手く構築したZIPファイルを入れておけば、インポート時にそちらがzipの内容と認識されるため、任意のファイルをサーバー側に展開できます。 これに気づくのが1つ目のポイントです。

しかし、Flaskサーバーの場合、私の知る限りHTTPリクエストにはUnicodeバイト列しか送れない(そしてサーバー側でencodeを呼んでいる)ので、ユーザー名に非Unicode文字は入れられません。 そこで2つ目のポイントが、ZIPファイルをASCII文字で作れるかという問題です。次のようなデータをASCII(0x00〜0x7f)で表現する必要があります。

  • マジックナンバー
  • 圧縮メソッド・フラグ
  • オフセット情報
  • サイズ情報
  • CRC32
  • 更新日時
  • ファイル名
  • ファイル内容

マジックナンバーや更新日時、ファイル名などはASCIIの範囲で表現できます。 圧縮メソッド・フラグは基本0にしておけばOKで、そのとき無圧縮にできるのでファイル内容もそのままSSTIを起こすテンプレートを入れれば良いです。

次にオフセットやサイズ情報ですが、これも無駄なデータを挟めば調整できるのでASCIIで表現できます。 最後にCRC32ですが、これも高々4バイトのハッシュ値なので、ファイル内容などの一部を適当に総当りで変えればすぐにASCIIで表現可能なCRC値が求まります。

ということで、ZIPファイルはASCIIコードで作れます! という問題でした。

misc

miscはなるべく簡単にする主義です。

MathHash (57 solves)

CakeCTFのcryptoに出題予定でしたが、CakeCTFのeasy枠にしては難しすぎるという結論からzer0pts CTFのeasy枠にやって来ましたこんにちは。

def MathHash(m):
    hashval = 0
    for i in range(len(m)-7):
        c = struct.unpack('<Q', m[i:i+8])[0]
        t = math.tan(c * math.pi / (1<<64))
        hashval ^= struct.unpack('<Q', struct.pack('<d', t))[0]
    return hashval

こういうハッシュ関数が使われています。 やっていることは、データを8バイトに分割し、それぞれを浮動小数点として認識してtan関数に突っ込んで、結果をバイト列として認識してxorする、という処理を繰り返します。

フラグに自由な箇所に値を加算できます。

            key = bytes.fromhex(input("Key: "))
            assert len(FLAG) >= len(key)

            flag = FLAG
            for i, c in enumerate(key):
                flag = flag[:i] + bytes([(flag[i] + key[i]) % 0x100]) + flag[i+1:]

            h = MathHash(flag)

この問題では浮動小数点がバイト列でどのように表現されているかという知識が問われます。 問われますといっても当然IEE754なので、それくらい知っとるわ!とパンチを食らいそうなのですが、今回の問題では符号ビットのみを使います。

t = math.tan(c * math.pi / (1<<64))

とtan関数にデータを入れているので、この値がpi/2を超えた時点で符号がマイナスになります。 つまり、フラグに1ずつ値を加算したハッシュ値を見て、ある時符号ビットの箇所が反転するので、その時の加算値を0x80から引けば元の文字が求まります。

0AV (12 solves)

去年のzer0pts CTFに出題予定でしたが、qemuの上でfanotifyが動かず間に合わなかったので今年に回しました。

内容としてはfanotifyを利用してファイルのアクセス時に内容をチェックするアンチウイルスが動作しています。 fanotifyは適用された名前空間ファイルシステムのみ検知できるため、unshareで新しい名前空間を作り、mountでフラグを新しい場所にバインドしてやれば検知を回避できます。 fanotifyを使ったセキュリティソフトやファイル監視機構は多いですが、割と回避できるよという問題でした。

開催前

全般

開催前はいつも忙しくなりますが、今年も忙しくなりました。 忙しさランクとしては過去2位か3位くらいだと思います。 今年の個人的忙しいポイントは、やはりweb問が不足していた分を直前に埋めたのが大変でした。 一方、いつも作っているスコアサーバー死んだ時用バックアップやサーバー確認君スクリプトは今回theoremoonがスコアサーバーの機能として実装し、自動化されていたのでいつも消費する1時間半がなくて楽でした。

脆弱性対応

BSidesの時もそうでしたが、我々がCTFを開催する直前になるとPoC付き脆弱性が公開されがちです。

今年は開催2週間前にDirty Pipeが公開されました。 これによりkRCEと0AVのカーネルを更新する必要が出てきた上、万が一開催までに修正されなかった時用に動的に脆弱性にパッチを当てるカーネルドライバの構想もしていました。 開催1週間前の段階で修正されたので大きな問題は発生しませんでした。

次にCVE-2022-25636のPoCが公開されました。 こちらも開催2週間前くらいの段階でしたが、一般ユーザーからunshareを禁止すれば防げるので/proc/sys/user/max_user_namespacesを変更することで対応しました。

さらにSpectre variant 2の新しい回避方法が開催2週間ちょっと前くらいに公開されました。 epbfのJITに依存していたので/proc/sys/kernel/unprivileged_bpf_disabledのビットを立てて対応しました。

運営中

miniblogの非想定解を修正するような作業はなかったと思いますが、kRCEのソルバがリモートで動かなくなっていて、それの修正に数時間かけました。

あとはボードゲームをしていました。 なんか羊を直進させるゲームとサイコロでポーカーするみたいなゲームを新たに教えてもらいました*3が、どのゲームでもボコボコにされて一回も勝てなかったと記憶しています。 時間があればボードゲームも勉強したいのですが、なかなか厳しい・・・・・・

六角形のはちみつマップの実装に興味が出たので、そこから始めてみようと思います。

yoshikingが来てからHax Ballもしました。何はともあれ安定のHax Ball。 あとお絵描きゲームもしました。 まさかDisco Festivalのbotが動いてるDiscordサーバーで運営がお絵描きゲームして遊んでるとは誰も思ってなかったでしょう。

f:id:ptr-yudai:20220321212506p:plain
ハイライト

アンケート結果

現状のアンケート結果をまとめ、反省点などを洗い出します。 無限回言っていますが、アンケートで詳しく文章で書いてくれている人の意見はかなり重視して反映しています。 日本語でも良いので率直な意見をお聞かせ下さい。

得意分野

webが多くてrevが少ないのは世界共通。

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

CTF全体の感想

#4 > #5になるかsum(#1, #2, #3) > sum(#4, #5)になったらそのCTFは危険だと思っています。

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

スコアボードの使いやすさ

スコアボードが使いにくいといった意見を書いてくれる方がいますが、何がどう使いにくいのかや、どう改善して欲しいのかを書いてもらわないと対応できないので、よろしくお願いします。

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

難易度

Too hardよりもHardが多いのでかなり良さそう。 各ジャンルwarmupを2つくらい用意しても良いのかなと話しているのですが、どうでしょうか。 解ける人からするとどうでも良い問題が増えて煩わしい気もします。

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

zer0pts CTFではすべての問題が1 solve以上あり、どのチームも全完しない、を個人的に目標にしているのですが、今年はぎりぎり達成できました。

期間

いつも36hは長くないか24hで良くないか、みたいなことを言っていますが、今年のsolve数を見れば36hは必要だったと思います。 正直な話pwnをいつもより若干難しくしたので、24hでは解かれない問題もあったかと思います。

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

好きな問題

完全に最初の得意分野の質問の分布に従っている気がします。

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

嫌いな問題

飛び抜けて嫌われてる問題はなさそう。

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

ご意見

ちゃんと書いてくれた意見から抜粋

  • 難しすぎる/何も解けなかった→どうするか考えますが、自明な問題は出さないので少しは頭を使ってみてください
  • ソースコードがあって良かった→業界標準にしましょう
  • Forensicsが欲しい→頭の片隅に入れておきます
  • OSINTがあっても良さそう→多分これからも出ないと思いますごめんなさい
  • RSAやncするcryptoが欲しい→theoremoonが対応してくれそう
  • real-worldじゃないのでQEMU問が嫌い→QEMUだからどうという問題は出してないはず
  • miniblogでフラグの場所が知りたかった→root directoryにあると書いておくべきでした。次からは注意します。
  • miniblogでdocker-composeが欲しかった→環境非依存で動くので置きませんでしたが、次からは配布します。
  • 頭が悪くておしまいです→私も同じです
  • Vespiaryが強い→わかる

終わりに

もうちょっと簡単なCakeCTFを開催するかもなので、今回難しかったよ〜という方はCakeCTFに出てみてください。 CakeCTFには国内チーム向けに豪華商品があるかもしれません :eyes:

それでは また らいねん!

*1:memcpyなども明示的に2つのサイズを受け取るべきだと思っています。

*2:正確には264-1回のループ

*3:ボードゲーム上手い人、ルールはある程度教えてくれるけどどういう考え方で動くべきかは教えてくれないがち