GoCon CTF と呼ばれた問題の紹介と解説

こんにちは、バンドルカードを作っている Kanmu という会社で CTO をしています。kneeです。

昨日行われた Go Conference 2021 Spring にて Kanmu としてスポンサーをさせていただいており、そのオフィスアワーにて「CTF なんて言葉聞いたこともない、strings コマンドなんて知らない、でも Go は触ったことあるよ」という方に向けて Go を少し絡めたゆるい CTF の rev っぽい問題を出題させていただきました。
それなりにたくさんの方に楽しんでいただけたようですので、解説と合わせて出題者の意図の紹介をします。
ちなみに自分は CTF 超絶にわかですのでその点はご容赦ください。

まだ解いてないけど、解いてみたいという方はぜひ読み進める前に解いてみてください。
所要時間は解いていた方の様子を見るに5分~3時間程度です。

問題の紹介

問題はこれです https://github.com/kanmu/gocon2021_office_hour
ヒントや解説は時間経過に応じて追加していったものですので、出題当時は以下のような感じでした。

以下のイメージの中にGoのプログラムをビルドした実行ファイルがおいてあります。 これを調べて秘密の答えを見つけてください!

docker run -it ghcr.io/kanmu/gocon2021-office-hour:latest /bin/bash

実際に中を見てみるとこんな感じ。
問題文にある実行ファイルと MEMO がおいてあるだけ。そして実行してみるといきなり答えを聞いてくる、そんな感じの問題です。

$ docker run -it ghcr.io/kanmu/gocon2021-office-hour:latest /bin/bash
root@7a909bfa6f26:/go/src/app# ls
MEMO  gocon2021_office_hour
root@7a909bfa6f26:/go/src/app# ./gocon2021_office_hour
Enter your answer 答えを入力してください: 

想定解答と出題意図

想定解答の解説をしつつ出題意図を説明していきます。

まずノーヒントなので MEMO を見ます。 ちなみにこのイメージにはデフォルトで file コマンドすらなく、この時点でなんだこれとなられた方も多かったようです。

$ cat MEMO
何回失敗しても諦めないで!
Don't give up no matter how many times you fail!

煽ってきていますね。
しかし実際に諦めないで何度も試してみるとヒントが出るようになっています。

$ ./gocon2021_office_hour
Enter your answer 答えを入力してください: a
Failed 残念
Enter your answer 答えを入力してください: b
Failed 残念
Enter your answer 答えを入力してください: c
Failed 残念
Enter your answer 答えを入力してください: d
Failed 残念
Enter your answer 答えを入力してください: e
Failed 残念
Enter your answer 答えを入力してください: f
Failed 残念

You can user one of go tool command. Let's run `go tool`.
go tool ~~~ コマンドで使えるものがあるかも…? `go tool` を実行してみてください

You can usertypo です。

gocon2021_office_hour 自体を直接調べるという選択肢を持っていない場合、できることとしてぱっと思いつくのは、実行していろいろ入力してみる、他のファイルを探しに行くことぐらいだろうと想定していました。ただ後者は脱線して時間がかかりますし、前者はただの当てずっぽうになってしまう。
そういった状況で自然に gocon2021_office_hour 自体の解析に誘導する必要があり、何回も実行するだけならどんな前提知識の方もいけるだろうということでこういう形をとりました。 ただノーヒントでわからんとなって1,2回入力して離脱したら悲しいので諦めないでという MEMO を置くことにしました。

$ go tool
addr2line
asm
buildid
cgo
compile
cover
dist
doc
fix
link
nm
objdump
pack
pprof
test2json
trace
vet

ちょっと Go 要素が出てきました。ここが一番の難所です。go tool を使えとだけヒントが出ているので、どれが使えるのか自分で調べる必要があります。ほとんどの方は普段使ったことないコマンドもたくさんあると思うので調べたり試行錯誤することになります。 この過程で普段触れることのない Go に関する情報にあたってもらえたらいいなーというような気持ちがこもっています。

ちなみに正解は go tool nm で、これはシンボルを列挙するコマンドです。が、その意味を理解しておらずとも問題はありません。
また go tool foo gocon2021_office_hour と件の実行ファイルを引数に渡して人間に可読っぽい出力がされるのはおそらく nm ぐらいかと思います。というところでなんとか答えににじり寄っていってくれたらうれしいなという感じですかね。

$ go tool nm gocon2021_office_hour
  4df6f8 r $f64.3eb0000000000000
  4df700 r $f64.3f847ae147ae147b
  ...
  54efa0 D unicode.foldLu
  54efa8 D unicode.foldM
  54efb0 D unicode.foldMn
  46dd40 T unicode.init
  46d500 T unicode/utf8.DecodeRune
  46d700 T unicode/utf8.DecodeRuneInString
  46d900 T unicode/utf8.EncodeRune
  46da80 T unicode/utf8.RuneCount
  46dbe0 T unicode/utf8.RuneCountInString
  53ff40 D unicode/utf8.acceptRanges
  5430e0 D unicode/utf8.first

とはいえその出力は2000行以上になるので、その中から必要な情報を見つける必要があります。ちなみに見つけてほしい行はこれです。

  49bc60 T type..eq.github.com/kanmu/gocon_2021_kanmu.LetsRunThisCommand_strings_gocon2021_office_hour_grep_question

ここで想定していたのは2種類の行動で、1つ目は長すぎて読めないのでとりあえず関係ありそうな単語で grep してみること。

$ go tool nm gocon2021_office_hour | grep gocon
  540560 D github.com/kanmu/gocon_2021_kanmu..inittask
  49bb60 T github.com/kanmu/gocon_2021_kanmu.New
  49bc60 T type..eq.github.com/kanmu/gocon_2021_kanmu.LetsRunThisCommand_strings_gocon2021_office_hour_grep_question

もう一つはどうすればいいかわからないけど、とりあえず実行結果を下からスクロールして眺めていったら不自然に長い行に気づく、です。出力順序は go tool nm の実装的にオプションを渡さない限りは変わらないのでスクロールしていくと割とすぐ出てくるはずです。

  466aa0 T type..eq.[6]string
  466b60 T type..eq.[9]string
  49b0e0 T type..eq.fmt.fmt
  49bc60 T type..eq.github.com/kanmu/gocon_2021_kanmu.LetsRunThisCommand_strings_gocon2021_office_hour_grep_question
  401c20 T type..eq.internal/cpu.CacheLinePad
  401c40 T type..eq.internal/cpu.option

この go tool nm を実行して、grep するなどしてこの行をみつけるところは難所だろうと思っていたので、出題してしばらく経過した後にヒントを公開しました。

続きます。 見つかった行は以下です。

  49bc60 T type..eq.github.com/kanmu/gocon_2021_kanmu.LetsRunThisCommand_strings_gocon2021_office_hour_grep_question

Let's Run This Command とあり、これが続きます strings_gocon2021_office_hour_grep_question

この _ で繋がれた文字列をみて strings コマンドを知っているかどうかで動きが変わります。知っていたらたぶんこのように分解するのは自明そうです。

strings gocon2021_office_hour | grep question

とはいえ知っていたらここにたどり着く前にゴールしてそうな気もしますし、そもそも想定解答者として strings コマンドも知らない人を置いている。かつ、そういった人にもこういうときに使う基本的なコマンドを一つでも覚えて帰ってもらいたいと思っていました。

ここで辿ってくれたらいいなーと思っていた思考の過程は以下です。 grep を知ってるのは前提で、途中で grep があるのでなにかを grep してるんじゃないかと想像し | grep question をひねり出す。つづいて strings_gocon2021_office_hour という部分の分解においては、 gocon2021_office_hour が実行ファイル名、また出題のリポジトリ名であることで一つの単語であることを想起し、余った strings という謎の存在を最後もしかしてこういうコマンドがあるのでは!?とひらめいてググってみるなどして答えにたどり着く。 という感じでした。

ここもやはり strings コマンド知らないとなかなか難しかったようですが、実際にそのような方が最終的に strings コマンドに辿り着いておられるのを拝見して、めちゃくちゃテンションがあがりました。

そしてここまで来たらもうゴールは目の前です。見つけたコマンドを実行します。

$ strings gocon2021_office_hour | grep question
P*gocon_2021_kanmu.LetsRunThisCommand_strings_gocon2021_office_hour_grep_question
  *************  question: what is the international standard of credit card messaging? The answer is ISOxxxx  ***************  000102030405060708091011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798990w
type..eq.github.com/kanmu/gocon_2021_kanmu.LetsRunThisCommand_strings_gocon2021_office_hour_grep_question
github.com/kanmu/gocon_2021_kanmu.(*LetsRunThisCommand_strings_gocon2021_office_hour_grep_question).Run
type..eq.github.com/kanmu/gocon_2021_kanmu.LetsRunThisCommand_strings_gocon2021_office_hour_grep_question

明らかに怪しい部分があるので取り出してみると、問題がかいてあります。

*************  question: what is the international standard of credit card messaging? The answer is ISOxxxx  ***************

クレジットカードのメッセージングの国際標準が答えです。意味がわからなくても問題文をそのまま検索したらヒットします。

ということで答えは ISO8583 です。

ソースコード

使った Go のソースコードが気になるところだと思うので公開します。
直前に慌てて作問していてなぜかソースコードrm するという失態をおかしたのでそのままのソースコードは残っておらず思い出しながら書きました。おおまかな作りはあってるとおもいます。 真に知りたい方はデバッガで見るなどして補完ください。

package main

import (
    "bufio"
    "fmt"
    "io/ioutil"
    "os"
  
  "github.com/kanmu/gocon_2021_kanmu"
)

func m5(text string) string {
    hasher := md5.New()
    hasher.Write([]byte(text))
    return hex.EncodeToString(hasher.Sum(nil))
}

func main() {
    x := "*************  question: what is the international standard of credit card messaging? The answer is ISOxxxx  ***************"
    fmt.Fprintf(ioutil.Discard, x)
    y := gocon_2021_kanmu.New().Run()
    fmt.Fprintf(ioutil.Discard, y)

    answer := "bc9ad0a243f1c91268437fefb3beba6b"
    i := 0
    scanner := bufio.NewScanner(os.Stdin)
    for scanner.Scan() {
        if m5(scanner.Text()) == answer {
            fmt.Println("Success! 正解です!すごい、おめでとうございます!")
        } else {
            fmt.Println("Failed 残念")
            if i == 5 {
                fmt.Println("You can user one of go tool command. Let's run `go tool`.")
                fmt.Println("go tool ~~~ コマンドで使えるものがあるかも…? `go tool` を実行してみてください")
            }
            i += 1
        }
    }
    if err := scanner.Err(); err != nil {
        os.Exit(1)
    }
}
package gocon_2021_kanmu

import (
    "math/rand"
    "time"
)

type LetsRunThisCommand_strings_gocon2021_office_hour_grep_question struct {
    x string
    y int
}

func (h *LetsRunThisCommand_strings_gocon2021_office_hour_grep_question) Run() string {
    return h.x
}

func New() *LetsRunThisCommand_strings_gocon2021_office_hour_grep_question {
    rand.Seed(time.Now().UnixNano())
    y := rand.Int() + 1
    return &LetsRunThisCommand_strings_gocon2021_office_hour_grep_question{
        x: "x",
        y: y,
    }
}

おわりに

Twitter での反響を見るに想定していたよりとても多くの方に楽しんでいただけたようで大変うれしかったです。
次回機会があれば strings だけでは倒せない歯ごたえがある問題も用意しようと思うので、今回興味を持ってもらえた方はぜひ CTF に入門して倒せるように鍛えてきてください!

この記事は Kanmu のテックブログの一発目なのですが、まさかの CTF の解説になってしまいました。
今後 Kanmu での普段のプロダクト開発の様子もいろいろ紹介できればと思っているのでのでぜひお楽しみに!
採用 | 株式会社カンム

GoCon の運営の皆様も参加してくださった方々も本当に楽しい会をありがとうございました!
ではまた。