WGGの活動log

都内でゲーム開発だったりVRだったりをしてるかもしれないエンジニアです. WGGは「ワグ」と読みます

CTF SECCON TOWERに挑んでみました

解けなかったのでwriteupじゃないです.感想です.


12/10 15:00~12/11 15:00までの間,「SECCON 2016 Online CTF」というセキュリティの大会が行われていました.

僕はセキュリティについてほとんど勉強をしたことがなかったのですが,とあるチームに入って参加してみました.

CTFとは

キャプチャー・ザ・フラッグ - Wikipedia

与えられた問題文,データから,プログラムの脆弱性を見つけるなどして隠されたFlagを手に入れろってルールです
例えば,暗号化された文章が用意されて,それを復号化するとか,
Webアプリケーションが用意されて,そこに不正アクセスするとか...
そんな感じのセキュリティの知識を競う大会です.

SECCON TOWER

まぁ,予想はしてたのですが,素人の僕がいきなり挑んで勝てる問題は殆ど無かったです.
でも面白そうな問題が1問,それがこの「SECCON TOWER」でした.

PNG over Telegraph

PNG over Telegraph
Analyze signal in this video.
You will able to get PNG, if you success to decode it.
https://youtu.be/Y6voaURtKlM

Youtubeの動画が与えられて,この動画からPNG画像を手に入れろという問題です
動画を再生してみると...
f:id:wgg00sh:20161211180044p:plain

なんかよくわからない機械がぐるぐる回ってます.50分間それが流れるだけの動画でした.

解いてみる

チームメイトと考えていたら,「動画初めに書かれている英文と動きが対応しているのでは?」と推測したので,
動画の0:12~1:10頃に書かれる英文と,動きを眺めてみると,「1つの姿勢=1つの文字」と対応しているっぽいことがわかりました.
動画内の全てのアルファベットと,動きを対応付けてみたら,下のようになりました.

f:id:wgg00sh:20161211181922p:plain

足りない文字がありますね...
でも近辺の文字から推測できそうです.
A~Hが子の腕が同じ姿勢で中心が45度回りの1つのグループ,
I~QのうちJを除いた8つが同じく1つのグループ,
R~Yも1つのグループっぽいです.

f:id:wgg00sh:20161211181940p:plain

Zは推測できませんでした...

動画から姿勢を抜き出す

流石に50分の動画を見続けるのは苦痛なので,プログラムで全ての動きを抽出してみました.
c++とopencv3.1を使用しています

void capture()
{
  VideoCapture video("SECCON TOWER 2016.mp4");

  const int OFFSET = 15;
  const int START_POS = 2250 + OFFSET;
  const double FPS = 29.97;
  video.set(CV_CAP_PROP_POS_FRAMES, START_POS);
  int i = 0;
  while (1)
  {
    Mat frame;
    i++;
    video >> frame;
    video.set(CV_CAP_PROP_POS_FRAMES, START_POS + i * FPS);
    if (frame.empty() || waitKey(30) >= 0 || video.get(CV_CAP_PROP_POS_AVI_RATIO) == 1) {
      break;
    }
    string fName = "image/"+to_string(i)+".jpg";
    imwrite(fName, frame);
  }
}

できあがったのがこれ↓
f:id:wgg00sh:20161211182930p:plain
3000枚近くの画像が生成されました.

自動で識別する

この3000枚の画像を手作業でアルファベットに置き換えていては時間が足りません.
そこで,画像を与えたらどの文字なのか返してくれる処理を作ります.
考え方としては,
「『入力画像』と『25種類のサンプル画像』を比較して,最も似ているものが,対応する文字」
と決めつけます.
まずは画像に無駄な部分が多すぎるので機械だけが映るようにトリミングします.
f:id:wgg00sh:20161211183459p:plain

そして,先に作った姿勢ー文字の対応表を見ながら25枚のサンプル画像を探し出します.
同じ文字であれば,別の画像でも殆ど同じように映ると予測しています.

類似度計算

入力画像,サンプル画像に対してそれぞれ,グレースケール化をした後に画像の類似度を計算します.
25枚のサンプル画像との類似度で最も高いものを選別します.

void convert1(Mat &mat)
{
  cvtColor(mat, mat, CV_BGR2GRAY);
}

void similarly()
{
  const int FIRST = 3;
  const int LAST = 2982;
  const int CHARA = 25;

  int channels[] = { 0 };
  int dimNum = 1;
  int binNum = 64;
  int binNums[] = { binNum };
  float range[] = { 0,256 };
  const float *ranges[] = { range };

  int sampleNum[CHARA] =
  {16,11,271,252,634,4,
    570,652,12,1014,27,102,44,
    856,2059,514,41,2018,992,53,84,
    959,33,104,686,};
  MatND sampleHist[CHARA];
  for (int i = 0; i < CHARA; i++) {
    string fName = "resize/" + to_string(sampleNum[i]) + ".jpg";
    Mat mat = imread(fName);
    convert1(mat);
    calcHist(&mat, 1, channels, Mat(), sampleHist[i], dimNum, binNums, ranges);
  }

  for (int i = FIRST; i <= LAST; i++) {
    string fName = "resize/" + to_string(i) + ".jpg";
    Mat mat = imread(fName);
    MatND hist;
    double similarly;
    convert1(mat);
    calcHist(&mat, 1, channels, Mat(), hist, dimNum, binNums, ranges);

    int max = 0;
    int maxPos = -1;
    for (int j = 0; j < CHARA; j++) {
      similarly = compareHist(hist, sampleHist[j], CV_COMP_INTERSECT);
      if (max < similarly) {
        max = similarly;
        maxPos = j;
      }
    }

    char c = 'A' + maxPos;
    cout << c;
  }
  cout<<endl;
}

実行結果
f:id:wgg00sh:20161211184356p:plain

「目視したのと全然違う...」
どうやら計算誤差が発生して希望通りの符号を返してくれませんでした.

誤差の原因

動画の前半と後半の画像を見比べてみました

f:id:wgg00sh:20161211184831p:plainf:id:wgg00sh:20161211184840p:plain

↓横に並べます
f:id:wgg00sh:20161211184919p:plain

「なんかずれてる...」

もうダメぽ

というわけで,カメラの位置が時間経過で動くことに気づいて,それを補正するトリミングを実装する途中で時間切れになってしまいました...

敗因

一番大きいのは類似度計算に頼った点かもしれないですね
画像の状態が変化
すると敏感に反応するので
子の腕の黒色部分の位置と向きを入力画像から識別するマッチングが実装できれば結果は変わったかも...

腕木暗号

www.silex.jp
今回出題された問題は「腕木通信」と呼ばれる情報の伝達方法の一つらしいです.
あと正しく解読できればCTFおなじみのQRコードの画像が生成されたらしいですが,自分にはできませんでした...

おわりに

学校の授業でちょうど最近まで画像処理の実験をやっていたので,その成果を実践できる良い機会でした.
できれば正解してチームに貢献したかったのですが解けなくて悔しかったです.

初めてCTFに挑戦したのですがとても楽しかったのでまた機会があればやってみたいですね~