好奇心の足跡

飽きっぽくすぐ他のことをしてしまうので、忘れないため・形にして頭に残すための備忘録。

SECCON 2017 online CTF write-up

SECCIN 2015 online CTF の記事はこちら

kusuwada.hatenablog.com

SECCON2016は、すっかり忘れていて気づいたら過ぎてました・・・。
しかも後から問題が公開されていない模様。後追いもできず。
※と思ったら、アップされている!探し方が悪かったよう。こちらも後からやってみる!!! SECCON2016_online_CTF

後追い記事はぼちぼち書く(&やってみる)として、今回は初めて参加できたので早めにwrite-upを。

はじめに

この記事は SECCON 2017 online CTF に参加した際の記録になります。
SECCON online 初参加記念。

  • 簡単な問題しか解けていませんのでご了承ください
  • ネタバレなので閲覧の際はご注意ください
  • write-upの記事に後追いの記録を徐々に追加更新予定

基本装備

そうです!2015の時からほとんど変わっていないです!
イベントの時だけ頑張る君になりつつあります。
そろそろMacBookAir買い換えたい・・・

Vigener3d (Crypto) 100

すぐにタイトルでググるといい、くらいの記憶はあった。
Vigenere暗号、というのがあるらしい。
ヴィジュネル暗号 通常は2次元の表で、平文に対して鍵が1つなのだけど、今回の問題は平文1つに対して鍵が2つの3次元。
ビジュアルに頼りがちな私としては3次元までなら頭で対応表をイメージしやすい・・・
が、問題を解くにはプログラムに落とさねば。

問題

Vigenere3d
----- Vigenere3d.py
import sys
def _l(idx, s):
    return s[idx:] + s[:idx]
def main(p, k1, k2):
    s = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz_{}"
    t = [[_l((i+j) % len(s), s) for j in range(len(s))] for i in range(len(s))]
    i1 = 0
    i2 = 0
    c = ""
    for a in p:
        c += t[s.find(a)][s.find(k1[i1])][s.find(k2[i2])]
        i1 = (i1 + 1) % len(k1)
        i2 = (i2 + 1) % len(k2)
    return c
print main(sys.argv[1], sys.argv[2], sys.argv[2][::-1])
-----
$ python Vigenere3d.py SECCON{**************************} **************
POR4dnyTLHBfwbxAAZhe}}ocZR3Cxcftw9

解き方

問題のソースを見ると、平文がp, 鍵はk1k2で、k2k1の逆であることがわかる。
なので、結局上記のコードのk2k1で置き換えて表現することができ、平文1:鍵1の問題として解くことができる。
すでにわかっている平文は7文字、鍵は14文字なので、既にわかっているものから鍵を推測する。

サンプルコード

#!/usr/bin/env python
s = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz_{}"
C = 'POR4dnyTLHBfwbxAAZhe}}ocZR3Cxcftw9'
predef = 'SECCON{'
K = []

for i in range(len(predef)):
    K.append(s[(len(s) + s.find(C[i]) - s.find(predef[i])) % len(s)])

K = K + K[::-1]
print "key= " + ''.join(K)

i = 0
P = ''
for a in C:
    P += s[(len(s) + s.find(a) - s.find(K[i])) % len(s)]
    i = (i + 1) % len(K)

print P

flag: SECCON{Welc0me_to_SECCON_CTF_2017}

Run me! (Programing) 100

ほとんどのチームが解けていたっぽい問題

問題

Run me!
-----  RunMe.py
import sys
sys.setrecursionlimit(99999)
def f(n):
    return n if n < 2 else f(n-2) + f(n-1)
print "SECCON{" + str(f(11011))[:32] + "}"
-----

ヒント

このプログラムの構造は再帰を使用しているが、なんだかメモリ・CPUを沢山食いそうだ・・・ 試しにnを 3~7くらいで計算すると、見たことのある数列になるかも

解き方

大変シンプルなスクリプト
漸化式を再帰で表現方式で、記述がシンプルになるのは良いのだが処理効率が低下してしまうので
普通にn=1から計算をしていって前の計算結果を使う、というようなプログラムにした方が俄然効率が良い。
後から調べたらこのページがわかりやすかった。
http://www.kogures.com/hitoshi/webtext/al-recursive/index.html 今回のプログラムはフィボナッチ数列なので、最後のケースにそのまま当てはまる。

計算量の比較はこのページの最後に書いてある通り

n=30のときでは、f2ならば90回なのに、f1(再帰)では(n=30のフィボナッチ数は)832,040回になってしまいます

サンプルコード

#!/usr/bin/env python
import sys

NUM = 11011
def f(N):
  F[0] = 1
  F[1] = 1
  for i in range(2, N):
    F[i] = F[i-2] + F[i-1]
  return F[N-1]

# main
print "SECCON{" + str(f(NUM))[:32] + "}"

flag: SECCON{65076140832331717667772761541872}

putchar music (Programming) 100

これもとにかくググる
http://royal-paw.com/2012/01/bytebeats-in-c-and-python-generative-symphonies-from-extremely-small-programs/
こんなサイトが出てきた。ググるの大事。

問題

putchar music
This one line of C program works on Linux Desktop. What is this movie's title?
Please answer the flag as SECCON{MOVIES_TITLE}, replace all alphabets with capital letters, and spaces with underscores.
main(t,i,j){unsigned char p[]="###<f_YM\204g_YM\204g_Y_H #<f_YM\204g_YM\204g_Y_H #+-?[WKAMYJ/7 #+-?[WKgH #+-?[WKAMYJ/7hk\206\203tk\\YJAfkkk";for(i=0;t=1;i=(i+1)%(sizeof(p)-1)){double x=pow(1.05946309435931,p[i]/6+13);for(j=1+p[i]%6;t++%(8192/j);)putchar(t>>5|(int)(t*x));}}

解法

Linuxと問題文にあるので、とにかくLinux上で作業をする。 とりあえず問題文のコンパイルを通す。 warnは無視。

#include <stdio.h>
#include <math.h>

int main(void){
  int i = 0;
  int j = 0;
  int t = 1;
  unsigned char p[]="###<f_YM¥204g_YM¥204g_Y_H #<f_YM¥204g_YM¥204g_Y_H #+-?[WKAMYJ/7 #+-?[WKgH #+-?[WKAMYJ/7hk¥206¥203tk¥¥YJAfkkk";

  for(i=0;t=1;i=(i+1)%(sizeof(p)-1)){
      double x=pow(1.05946309435931,p[i]/6+13);
      for(j=1+p[i]%6;t++%(8192/j);)
        putchar(t>>5|(int)(t*x));
  }

  return 0;
}

上記、putchar musicのサイトを読むとc言語の場合はあとSoXというのを使えば鳴りそうだ。
How To Install SOX 14.4.1 On Ubuntu 14.10, Ubuntu 14.04 And Derivative Systems
これを参考にinstall。ありがたや。

なんの捻りもなく、言われるがままにコンパイル・実行

$ gcc -o bytebeat PutCharMusic.c -lm
$ ./bytebeat | sox -r 8000 -b 8 -c 1 -t raw -s - -d

音楽が鳴った!すごい!ブツブツ切れてるけど・・・

flag: SECCON{STAR_WARS}

SHA-1 is dead (Crypto) 100

SHA-1がすでに安全ではなくなったよ、系の問題と思われる。

問題

SHA-1 is dead
http://sha1.pwn.seccon.jp/
Upload two files satisfy following conditions:

file1 != file2
SHA1(file1) == SHA1(file2)
SHA256(file1) <> SHA256(file2)
2017KiB < sizeof(file1) < 2018KiB
2017KiB < sizeof(file2) < 2018KiB
* 1KiB = 1024 bytes

下調べ

下記の条件を満たすfile1,2を作成する。 * file1とfile2は異なる * SHA-1が一致(衝突) * SHA-256は異なる * 2017KiB より大きく 2018KiB より小さい

完全にSHA-1はもうダメだという問題ですね。
危ないからもう使えない、という知識くらいはあったものの、実際にどうやったら衝突するものを自由に作れるのか?は考えたことがなかった。
のでググってみると、2017年2月24日の記事で

Googleとオランダの研究機関CWI Instituteは23日、 2つの異なるファイルから同じSHA-1ハッシュ値を生成する“衝突”に成功し、 その手法とハッシュ値が同一の2つのPDFファイルを公開した。 Googleではより安全なSHA-256やSHA-3への移行を推奨している。

というのを発見。
記事はこちら
イムリーな話題だったんですね。
元ネタはこちらだそう。

解法

もうダメだの理由が、計算機の発展に伴って衝突するペアを見つけるのにかかる時間が減ったから、
だけだとすると、この貧弱なPCで処理をぶん回して衝突するペアを探すのは厳しい。
もう少し探して見ると、こんな記事とツールが(一瞬で出て来たけど)。
SHA-1ハッシュの衝突を現実的な時間で生成する攻撃「Shatterd」

Shattered攻撃は多数の暗号解析技術を組み合わせたもので、同じSHA-1ハッシュ値を持ち、内容の異なる2つのPDFファイルの生成などが可能だ。高速といっても263回の試行が必要となり、攻撃の第1フェーズは6,500 CPUで1年間、第2フェーズは110 GPUで1年間を要する。それでもブルートフォース攻撃と比較すると10万倍以上高速だという。

ふむふむ。ってえええ!2年も待てませんよ。
ということで、

shatterd.ioではPoCとして、同じSHA-1ハッシュ値で内容の異なる2つのPDFファイル (PDF 1/PDF 2)を公開している

このPDFたちを改造してやるのが良さそう。 それぞれDLしてみると、422KB(422435 byte)ずつ。

そしてこんなサイトを見つけてしまった。
巷で話題のGoogleのSHA-1衝突やってみた
とてもわかりやすい。ここが大事

先頭320バイトの部分で衝突が起きていてそれ以降が同じ値ならずっと衝突し続けるとのことです

ということは、足りないバイト数を同じ値で埋めてやれば衝突した指定のサイズのファイルが作成できそう。
ファイル形式についての指定はないので、PDFファイルであることは捨てて良い。

ファイルの末尾に適当なものを付け足すスクリプトを書こうかとも思ったのだけど、以前linuxwindowsコマンドでランダムな内容の任意のサイズのファイルを生成するのがあったのを思い出し、今回も何かあるのではないかと調べて見る。
容量指定のダミーファイルを作成したい
あった。
これで適当なサイズのファイルを作成し、元のshattered-1(2).pdfにくっつけてやれば良さそう。 適当なサイズは、くっつけた後に2017kb<ファイルサイズ<2018kbになる必要があるので、下記計算でサイズを出しておく。

(2017 * 1024 + 1) - 422435 = 1642974

以下、実行コマンド羅列

$ dd if=/dev/zero of=tempfile bs=1 count=1642974
1642974+0 records in
1642974+0 records out
1642974 bytes transferred in 7.541782 secs (217850 bytes/sec)

$ cat shattered-1.pdf tempfile > file1
$ cat shattered-2.pdf tempfile > file2
$ ll
-rw-r--r--  1 ***  staff  2065409 ** ** 09:33 file1
-rw-r--r--  1 ***  staff  2065409 ** ** 09:33 file2

$ openssl sha1 file1
SHA1(file1)= d5b192f19ef498b739a3ecd0498e20f44e4fa0c9
$ openssl sha1 file2
SHA1(file2)= d5b192f19ef498b739a3ecd0498e20f44e4fa0c9

ということで、作成したファイルが本当にSHA1が一致していることが確認できました。
これを問題のサイトに突っ込めばflagゲットです。 sha1-send.png

flag: SECCON{SHA-1_1995-2017?}

SqlSRF (Web) 400

4・・・400点問題だから無理かなぁ・・・とは思いつつ、せっかくリアルタイムで参加できたのでWeb系の問題を中心に見てみることに

問題

SqlSRF
The root reply the flag to your mail address if you send a mail that subject is "give me flag" to root.
http://sqlsrf.pwn.seccon.jp/sqlsrf/

足跡

まずはググるSQL関連のワードでSRFというと
Set Returning Functionsというのがヒットする
http://sqlsrf.pwn.seccon.jp/sqlsrf/
にアクセスすると、以下のページが表示される image
index.cgiにアクセスすると、ログイン画面が表示。ログイン情報を持っていないと、menu.cgiにアクセスしても同じようだ。
適当にlogin名を入れてloginボタンを押して見ると、こんなメッセージが。 image あとは、index.cgi_backup20171129にアクセスして見ると、index.cgiと思われるperlソースコードが。

#!/usr/bin/perl

use CGI;
my $q = new CGI;

use CGI::Session;
my $s = CGI::Session->new(undef, $q->cookie('CGISESSID')||undef, {Directory=>'/tmp'});
$s->expire('+1M'); require './.htcrypt.pl';

my $user = $q->param('user');
print $q->header(-charset=>'UTF-8', -cookie=>
  [
    $q->cookie(-name=>'CGISESSID', -value=>$s->id),
    ($q->param('save') eq '1' ? $q->cookie(-name=>'remember', -value=>&encrypt($user), -expires=>'+1M') : undef)
  ]),
  $q->start_html(-lang=>'ja', -encoding=>'UTF-8', -title=>'SECCON 2017', -bgcolor=>'black');
  $user = &decrypt($q->cookie('remember')) if($user eq '' && $q->cookie('remember') ne '');

my $errmsg = '';
if($q->param('login') ne '') {
  use DBI;
  my $dbh = DBI->connect('dbi:SQLite:dbname=./.htDB');
  my $sth = $dbh->prepare("SELECT password FROM users WHERE username='".$q->param('user')."';");
  $errmsg = '<h2 style="color:red">
Login Error!</h2>
';
  eval {
    $sth->execute();
    if(my @row = $sth->fetchrow_array) {
      if($row[0] ne '' && $q->param('pass') ne '' && $row[0] eq &encrypt($q->param('pass'))) {
        $s->param('autheduser', $q->param('user'));
        print "<scr"."ipt>document.location='./menu.cgi';</script>";
        $errmsg = '';
      }
    }
  };
  if($@) {
    $errmsg = '<h2 style="color:red">
Database Error!</h2>
';
  }
  $dbh->disconnect();
}
$user = $q->escapeHTML($user);

print <<"EOM";
<!-- The Kusomon by KeigoYAMAZAKI, 2017 -->
<div style="background:#000 url(./bg-header.jpg) 50% 50% no-repeat;position:fixed;width:100%;height:300px;top:0;">
</div>
<div style="position:relative;top:300px;color:white;text-align:center;">
<h1>
Login</h1>
<form action="?" method="post">
$errmsg
<table border="0" align="center" style="background:white;color:black;padding:50px;border:1px solid darkgray;">
<tr><td>Username:</td><td><input type="text" name="user" value="$user"></td></tr>
<tr><td>Password:</td><td><input type="password" name="pass" value=""></td></tr>
<tr><td colspan="2"><input type="checkbox" name="save" value="1">Remember Me</td></tr>
<tr><td colspan="2" align="right"><input type="submit" name="login" value="Login"></td></tr>
</table>
</form>
</div>
</body>
</html>
EOM

1;

※競技中はperlは触ったことがほとんどないので、これは何の言語だ?とぱっとわからなかったが、落ち着いて見て見ると1行目に書いてある・・・。競技中はだいぶテンパっていた様子。
どうやらログイン時のUsernameの値を、直接DBの検索クエリに使用している。ここが一つ攻撃ポイントっぽい。

my $sth = $dbh->prepare("SELECT password FROM users WHERE username='".$q->param('user')."';");

試しに、sql injectionの簡単なやつをUsername欄に入れてloginしてみる Username: kusuwada"' OR 1 = 1'"
Password: こんな画面が現れた。エラーメッセージが変わった。 image SQL injectionできてるようだ。
この先のコードを読んでいくと、認証が成功するとmenu.cgiに飛べることがわかる

if(my @row = $sth->fetchrow_array) {
      if($row[0] ne '' && $q->param('pass') ne '' && $row[0] eq &encrypt($q->param('pass'))) {

この条件式をクリアすれば認証成功。
上のSELECT文で引っ張ってきた値が、入力画面のPasswordに入れた値をencrypt関数で暗号化したものと一致すれば良い。
普通のログイン処理だと失敗してしまうことから、DBに保存されているpassは平文か、もしくはencrypt関数以外で暗号化されているようだ。
そこのロジックはもはやわからないので、"SELECT文で引っ張ってきた値"が"入力画面のPasswordに入れた値をencrypt関数で暗号化したもの"になるようにSQL Injectionする。
ということで、どうも次ののstepにいくためには、平文のpass と暗号化したpassを入手すことが必要なようだ。
どうやったら手に入るのか、ヒントを探す。
ソースで呼ばれているencrypt関数は、別管理らしく公開されていない。

$q->cookie(-name=>'CGISESSID', -value=>$s->id),
    ($q->param('save') eq '1' ? $q->cookie(-name=>'remember', -value=>&encrypt($user), -expires=>'+1M') : undef)

ここから、save1の時、cookieremember:&encrypt($user) がsetされるらしいということがわかる。
$userはフォームのUsernameに入れた値なので、RememberMeにチェックをつけて(save==1にするため)login、remembercookieをみれば暗号文が取得できる。
同じencrypt関数が使われているので、userだろうがpassだろうが、平文と暗号文の対応が取れればOK。
さらに、

$user = &decrypt($q->cookie('remember')) if($user eq '' && $q->cookie('remember') ne '');

ここから、Usernameが空で、remembercookieが何かしら書かれている場合、$userremembercookieを復号したものが入ることがわかる。
おお〜。400点問題でも案外楽しめている。

ここで、手順を再整理

目的とまとめ

  • 認証を通してmenu.cgiに飛ぶ
  • 認証を通すためには、"SELECT文で引っ張ってきた値が、入力画面のPasswordに入れた値をencrypt関数で暗号化したものと一致"させる必要がある
  • 入力画面のUsernameSQL Injection可能なので、SELECT文で引っ張って来る値が所望のものになるようにUsernameを決める
  • SQL Injectionなんて実践することないので、クエリの組み立て方はググるGoogle検索: sql injection 一覧 検索ワードに日本語混じっているにもかかわらず、9万件以上のヒット。凄い。 トップにあったページを参照 http://www.byakuya-shobo.co.jp/hj/moh/sqlinjectioncheatsheet.html 自分の入れたテキストを返してもらえそうなやつを探す

手順

  1. 適当なパスワードを決めて、それの暗号文を取得
    今回は kusuwada にすることに。
    Username: kusuwada RememberMe: ON
    でloginしてremembercookieを確認 -> encrypt("kusuwada")取得 .2. 認証を通してlogin
    Username: kusuwada ' UNION SELECT "{encrypt("kusuwada")}" -- Password: kusuwada
    でlogin

cookieの確認方法。こういうことはしばらくやっていないとすぐ忘れてしまう・・・。
この辺の記事を参考に
1 の手順で、remember:743f7c045d8853660c482b84f487e01fcookieから得られる
2 の手順で
image
おお、menu.cgiに移行した!ああ、もう満足。

この画面上のnetstat -tnl ボタンを押すと、こんな感じでnetstat結果が取得できる image localの25番ポートが開いているようなので、メールが送れそう。
2.のフォームはadminのみが使えるよ、ということで、adminとしてログインし直す必要があるようだ。
いまは' UNION SELECT "743f7c045d8853660c482b84f487e01f" -- とかいうUsernameになっているが、ここがadminになるようにPasswordを探し出すのが次のミッション。

encrypt関数に対して、上で書いたように * RememberMeにチェックをつけて(save==1にするため)login、remembercookieをみれば暗号文が取得できる * Usernameが空で、remembercookieが何かしら書かれている場合、$userremembercookieを復号したものが入る 平文->暗号文->平文が自由に取得できることがわかる。今求めたいのはadminという暗号文に対する平文なので、一筋縄ではいかない。

ここで時間切れ。

解法

★後追いでやりたいなぁ

Log search (Web) 100

これも後追いできない可能性が高いので、すぐに着手。

問題

Log search
Search the flag on Elasticsearch.
http://logsearch.pwn.seccon.jp/

足跡

http://logsearch.pwn.seccon.jp/ の下の方にある、Log search を踏むと、 /logsearch.php に飛ぶ。
image

searchに検索パスを入れてそれにマッチするログの履歴が出るようになっているようだ。
検索結果の項目は timestamp, method, request, response

解き方

問題文に Search the flag on Elasticsearch. とあったので、素直に flag でまずはログを検索してみる
沢山結果が出てくるが、responseが200のもののみpickupするようにして何度か文言を変えて検索していくと
/flag-****.txtというpathでresponseが200の物を発見。 http://logsearch.pwn.seccon.jp/flag-****.txt に実際にアクセスしてみると、テキストでflagが出てきました。

flag: SECCON{N0SQL_1njection_for_Elasticsearch!}

後から

flagをかなり早い段階でgetできたものの、flag NOSQL injection for Elasticsearch と書いてある。(ほぼflagそのまま・・・)
私のやったことといえば、どちらかというとディレクトリ・トラバーサルのような。
もしかしたら運が良かっただけで、本来の解法ではないのかもしれない。
※ logが個人に閉じていない全員分のアクセスログで、たまたま前にflagをgetした人のが見えた可能性・・・
後追いで上記の想定でやって見たい気もするが、いつサーバー閉じられるんだろう。

JPEG file (Binary) 100

ステがノグラフィがなくなったので、せめてこれだけは・・・!
結構後の方でチェックしたら、多くの人が解けている。いけるかも!

問題

JPEG file
Read this JPEG is broken.
It will be fixed if you change somewhere by 1 bit.
tktk-892009a0993d079214efa167cda2e7afc85e6b9cb38588cba9dab23eb6eb3d46

と、JPEGファイルが添付されていました。

足跡 & 解法

問題文より、1bit反転するだけで読めるようになるのか。 とりあえずfileコマンド

$ file tktk-892009a0993d079214efa167cda2e7afc85e6b9cb38588cba9dab23eb6eb3d46.jpg
tktk-892009a0993d079214efa167cda2e7afc85e6b9cb38588cba9dab23eb6eb3d46.jpg:
JPEG image data, JFIF standard 1.01, resolution (DPI), density 192x192, segment length 16, baseline, precision 8, 339x53, frames 3

一般的なJpeg imageのようだ。 素直に開いてみると、黒い帯状。JpegAnalyzerで開いてみると、サムネイル画像は一切なし。特にAnalyzer側で壊れているところは検出されず。
1bit反転で読めるようになるということは、データ部分じゃなくてマーカーか何かが誤検出されているっぽい。(データ部分だとすると1bit反転したくらいでは大勢に影響がないため)
完全に勘だけど、imageの形状的に修復すれば絵でflagが表示されるっぽい。

とりあえず壊れているところのヒントを得るために、imagemagickに食わせてみる。

$ convert -geometry 70% tktk-892009a0993d079214efa167cda2e7afc85e6b9cb38588cba9dab23eb6eb3d46.jpg resize70.jpg
convert: Corrupt JPEG data: premature end of data segment `tktk-892009a0993d079214efa167cda2e7afc85e6b9cb38588cba9dab23eb6eb3d46.jpg' @ warning/jpeg.c/JPEGWarningHandler/352.
convert: Unsupported marker type 0xfc `tktk-892009a0993d079214efa167cda2e7afc85e6b9cb38588cba9dab23eb6eb3d46.jpg' @ warning/jpeg.c/JPEGErrorHandler/319.

お、壊れていると検知された。 Unsupported marker type 0xfcということなので、markerの識別子であるFFをくっつけてFFFCというのをマーカーとして誤検出してしまったっぽい。

バイナリエディタで開いてみる。 2年前からupdateされていた0xEDを使用。FFFCを検索。一箇所だけhitしたので、ここをFEFCに書き換え。 開いてみると、何やら壊れているが表示が変わったっぽい。修復できたか確認するために、再度imagemagickに食わせてみる。

$ convert -geometry 70% tktk-892009a0993d079214efa167cda2e7afc85e6b9cb38588cba9dab23eb6eb3d46.jpg resize70.jpg

resize70.jpgができ、無事中身を見ることができました。

resize70

Thank you for playing! (Thank you!) 100

問題

Thank you for playing!
SECCON{We have done all the challenges. Enjoy last 12 hours. Thank you!}

解法

なんと、そのままフラグを入れるだけ
リアル脱出ゲームの感覚だと、「絶対何か裏があるはず・・・!」とか思ってしまうが、今回は参加ボーナスみたいなものだった模様

終わりに

個人的には2015年と比べてステガノグラフィがなくなったのがちょっとショックでしたが
1問も解けないんじゃなかろうかと思っていたので、何かしら解けて良かったです。
あとはここ半年、仕事での自分のメインの開発言語がRuby->Pythonになっていたのですが、問題もPythonが多かったり、暗号系のライブラリもPythonが充実していたので好都合でした。
大会開催日時を知ったのが今週で先約もあり、なかなか時間が取れませんでしたが、あとからまたのんびりできればなあと思います。
※後追いの辛いところは、環境がなくなってしまうことと、ググったらwire-upが出てきてしまうこと・・・