IceCTF 2016 Writeup

IceCTFに参加していたので、 writeup を書きます。

今回は人数制限の都合上、チームではなく、個人参加していました。なぜかコンテスト終了後は問題文もランキングも自分の順位も見ることができませんが、1396点でした。

問題数が多く、内容としても比較的主な要素を問題を通して学ぶことを教育的な問題が多めだと感じました。Crypt が比較的初心者に優しくてありがたかったです。

以下、提出順に書いていきます。

Hello, World!

サンプル問題。フラグが問題文に書いてあるので提出。

Spotlight

開くと、マウスがある領域の周辺だけ見ることができるウェブ画面が表示された。とりあえず HTML のソースを見るとフラグが埋め込まれていたので提出。Consoleにwriteしていたのでデバッグコンソールを開くなどが想定解法だったのかもしれない。

All your Base are belong to us

二進数みたいな形式で何かが与えられているのでパースするとフラグがでてきた。

#include <iostream>
using namespace std;

int main(){
	char tmp=0;
	int count=0;

	while(!cin.eof()){
		char t;
		cin>>t;

		if (t=='0' || t=='1'){
			count++;
			tmp=(tmp<<1)+(t-'0');

			if(count%8==0){
				cout<<tmp;
				tmp=0;
			}
		}
	}
}

Rotated!

5文字ずらして、そのあと8文字ずらしたみたいなことが書いてあって、要するにROT13なのでウェブでワンライナーを見つけて処理して提出。

Move Along

Web問題なのですが忘れました…… 事後に問題文すら見られないのはつらいです

Substituted

換字暗号っぽいものが渡されて、とりあえず「IceCTF{」の部分が確定できるのであとは元の単語が推測できる部分から確定していった。

Time Traveler

ウェブページにつなぐと、「ここにかつてフラグがあった」みたいなことが書いてあったので、Internet Archiveで当該URLを検索してフラグを発見。

Flag Storage

なんだっけ……?解いた問題一覧を見るとWeb問なので SQLi でログインしたらフラグが見えた問題だった気がする。

Demo

主催者が用意した Linux 機で、フラッグを表示するためには特定の条件を満たさないといけないバイナリがおいてある(実行ファイルの sticky bit とかでフラグファイルそのものへのアクセスを与えず実行だけできるように工夫してある)。この問題ではそのバイナリのソースもおいてある。

この問題では argv[0] が特定の要件をみたさないといけないので、ホームディレクトリに要件を満たすようにシンボリックリンクを張って実行するとフラグが出た。

Complacent

忘れた

Search

ドメインだけ渡されるが、ウェブサーバに繋がらないと書いてある。conTEXT がどうとか書いてあるので、 DNS の TXT レコードを調べるとフラグが書いてあった。

Hidden in Plain Sight

忘れた

Exposed!

忘れた

RSA?

RSA を使ったが、何か間違っている。と書いてあるので見ると、 e=1 となっている。

RSA での暗号化は

  c=m^e \mod n

なので、e=1 では暗号化がされず平文のままになる。なので、暗号文をそのまま8ビットずつ区切って文字として出力すればフラグ。

Thor’s a hacker now

忘れた

Miners!

忘れた

Scavenger Hunt

忘れた

Smashing Profit!

忘れた

Kitty

admin のパスワードのハッシュだけが渡された。

パスワード入力フォームを見るとパスワードの形式がとても厳しく普通に入力すると通らない。アルファベット、数字、記号の順で5文字で、全探索可能だったので全探索した。

Dear diary

nc で接続し、そのサーバで実行されたバイナリが渡される pwn。

「日記を保存する機能」「最後に保存した日記を表示する機能」がついたバイナリが渡される。

見たところ最初に「flag」という関数を読んでおり、ファイルを open して特定のメモリのアドレス(少なくとも objdump で見る限りハードコード)に書き込んでいた。

バッファオーバーフローさせるのかと思いきや入力はすべて fgets だった。また、 strchr を呼び出して ‘n’ が含まれているとなぜか「rude!」と表示されて終了する仕様だった。

出力を見ると printf だったので、日記の内容を %d とかにするとその時のスタックの一部のデータが出力された。n がブロックされているので、%n が使えず、メモリに書き込むことはできない。

gdb で見たところ、しばらく先に日記の内容があったので、日記の先頭部分に flag のアドレスを書き、あとは %d でしばらく埋めつつ flag のアドレスが来る場所に %s を置くとフラグが表示された。

Toke

現代的なマリファナ工場なるサイトが渡され、ログインすると「FBIによって止められた」とだけ書いてある。

Cookie を見てみると jwt という名前がついた Cookie があり、検索すると検証機能付きの URL セーフなエスケープ方式で JWT というのがあった。

ということで見つけたオンラインのデバッガに中身を入れると、フラグが出てきた。

IRC I

IRC サーバ上でフラグを共有している人がいる、と問題文に書かれていた。主催者らしき人のプロファイルを表示すると「flag_share_(ランダムな英数)」という部屋が案内されていたので、その部屋に行くと部屋の説明にフラグがあった。

RSA

RSA に再挑戦したが、まだ何かおかしいようだ、と書いてある。見るとなんだかプロファイルが多い、というか秘密鍵が書いてある。ウェブで RSA の複合化手順を調べて、その通りに普通に秘密鍵を使ってエンコードするとフラグ。

#!/usr/bin/python

N=0x1564aade6f1b9f169dcc94c9787411984cd3878bcd6236c5ce00b4aad6ca7cb0ca8a0334d9fe0726f8b057c4412cfbff75967a91a370a1c1bd185212d46b581676cf750c05bbd349d3586e78b33477a9254f6155576573911d2356931b98fe4fec387da3e9680053e95a4709934289dc0bc5cdc2aa97ce62a6ca6ba25fca6ae38c0b9b55c16be0982b596ef929b7c71da3783c1f20557e4803de7d2a91b5a6e85df64249f48b4cf32aec01c12d3e88e014579982ecd046042af370045f09678c9029f8fc38ebaea564c29115e19c7030f245ebb2130cbf9dc1c340e2cf17a625376ca52ad8163cfb2e33b6ecaf55353bc1ff19f8f4dc7551dc5ba36235af9758b
e=0x10001
phi=0x1564aade6f1b9f169dcc94c9787411984cd3878bcd6236c5ce00b4aad6ca7cb0ca8a0334d9fe0726f8b057c4412cfbff75967a91a370a1c1bd185212d46b581676cf750c05bbd349d3586e78b33477a9254f6155576573911d2356931b98fe4fec387da3e9680053e95a4709934289dc0bc5cdc2aa97ce62a6ca6ba25fca6ae366e86eed95d330ffad22705d24e20f9806ce501dda9768d860c8da465370fc70757227e729b9171b9402ead8275bf55d42000d51e16133fec3ba7393b1ced5024ab3e86b79b95ad061828861ebb71d35309559a179c6be8697f8a4f314c9e94c37cbbb46cef5879131958333897532fea4c4ecd24234d4260f54c4e37cb2db1a0
d=0x12314d6d6327261ee18a7c6ce8562c304c05069bc8c8e0b34e0023a3b48cf5849278d3493aa86004b02fa6336b098a3330180b9b9655cdf927896b22402a18fae186828efac14368e0a5af2c4d992cb956d52e7c9899d9b16a0a07318aa28c8202ebf74c50ccf49a6733327dde111393611f915f1e1b82933a2ba164aff93ef4ab2ab64aacc2b0447d437032858f089bcc0ddeebc45c45f8dc357209a423cd49055752bfae278c93134777d6e181be22d4619ef226abb6bfcc4adec696cac131f5bd10c574fa3f543dd7f78aee1d0665992f28cdbcf55a48b32beb7a1c0fa8a9fc38f0c5c271e21b83031653d96d25348f8237b28642ceb69f0b0374413308481
c=0x126c24e146ae36d203bef21fcd88fdeefff50375434f64052c5473ed2d5d2e7ac376707d76601840c6aa9af27df6845733b9e53982a8f8119c455c9c3d5df1488721194a8392b8a97ce6e783e4ca3b715918041465bb2132a1d22f5ae29dd2526093aa505fcb689d8df5780fa1748ea4d632caed82ca923758eb60c3947d2261c17f3a19d276c2054b6bf87dcd0c46acf79bff2947e1294a6131a7d8c786bed4a1c0b92a4dd457e54df577fb625ee394ea92b992a2c22e3603bf4568b53cceb451e5daca52c4e7bea7f20dd9075ccfd0af97f931c0703ba8d1a7e00bb010437bb4397ae802750875ae19297a7d8e1a0a367a2d6d9dd03a47d404b36d7defe8469

c_powers = [c]
cur_power = 1

while cur_power <= d:
	tmp = c_powers[len(c_powers) - 1]
	c_powers.append((tmp * tmp) % N)
	cur_power *= 2

cur_power = 1
cur_pos = 0
ans = 1

while cur_power <= d: if d & cur_power > 0:
		ans *= 	c_powers[cur_pos]
		ans %= N
	cur_pos += 1
	cur_power *= 2

# print ans

ans_array = []
while ans:
	ans_array.append(chr(ans % 256))
	ans /= 256

ans_array.reverse()

print ''.join(ans_array)

Alien Message

エイリアンが書いたようだ、という次の画像が渡された。

alien_message_b84f283848b7f34fd4c7529186e66e120b0a374c9d0f2a225b0a7a215716afb5

IceCTF{ で始まる部分を見ると二つの C が同じなので、換字暗号のようだし、問題ジャンルも Cryptography になっていたが、確定できる文字が少なくて手がかりがつかめず、そもそもアルファベットらしきものも数えてみると 27 文字もあった。

そこで、alien alphabet で画像検索するとそれらしき画像があった。Futurama Alien Alphabet というフォントのようなので、見ながら複合化。

l33tcrypt

データを暗号化してくれるサーバが動いており、鍵とフラグは外部から読み込んでいる。サーバの Python コードが公開されている。

コードを見ると、”l33t please” で始まる文字列を渡すと、文字列の末尾にフラグを追加したうえで AES で暗号化して base64 で返してくれる。CBC などは特に使っていないようで、同じ文字列は毎回同じように返ってきた。

AES のブロックサイズは 128 ビットなので、16文字ごとに、望むものと一致すればその部分の暗号は同じになる。

フラグの i 文字目までがわかっているとき、 “l33t please”に続けて、適当な文字を適宜詰め、そのあとフラグの i 文字目までを埋めて最後のブロックが一文字だけ余る状態にし、残りの1文字を一通り試すことができる。
この時、”l33t please”とパディングのみを渡した結果(この場合はパディングの直後に正しいフラグを入れて暗号化してくれる)と比較すれば、正解の時だけ合致するブロックが多くなる。

これを順に試せば、フラグが出てくる。

#!/usr/bin/python

import commands
import sys

def encrypt(s):
	return commands.getoutput('echo -n \'' + s + '\' | (base64 -w 0; echo) | nc l33tcrypt.vuln.icec.tf 6001 | grep OH7').strip()

def sharestr(s, t):
	ret = 0
	for i in range(0, min(len(s), len(t))):
		if s[i] == t[i]:
			ret = i + 1
		else:
			break
	return ret

header = 'l33tserver please'
flag_cand = ''
cand_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_{}'

cur_cand = ''

while True:
	cur_cand_len = 0
	tmphead = header + '/' * (16 - (len(header) + len(flag_cand) + 1) % 16)
	cur_enc = encrypt(tmphead)

	for i_loop in range(0, len(cand_chars)):
		now_cand = flag_cand + cand_chars[i_loop]
		print 'Trying: ', tmphead + now_cand
		tmp_enc = encrypt(tmphead + now_cand)

		tmp_len = sharestr(cur_enc, tmp_enc)
		print tmp_len

		if tmp_len > cur_cand_len:
			cur_cand_len = tmp_len
			cur_cand = now_cand
	
	if len(cur_cand) == len(flag_cand):
		sys.exit(1)
	flag_cand = cur_cand

Over the hill

謎の行列みたいなのがおいてある。そのままではどうしようもないので、とりあえず matrix encryption とかで検索すると、 Hill Cipher が出てきて問題名とも合致。というわけで Hill Cipher の Wikipedia の記事を見ながら解く。

mod Nでの逆行列の計算に戸惑うが、ウェブ検索した結果見つけた動画(https://www.youtube.com/watch?v=LhBovzm4Ajs)では、普通の逆行列に近い処理をしていた。普通の逆行列は numpy なるツールで計算できそうなので、確認しながら最終的には以下のようなコードに。

#!/usr/bin/python

import numpy

alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789_{}"
N = len(alphabet)

matrix = [[54, 53, 28, 20, 54, 15, 12, 7],
          [32, 14, 24, 5, 63, 12, 50, 52],
          [63, 59, 40, 18, 55, 33, 17, 3],
          [63, 34, 5, 4, 56, 10, 53, 16],
          [35, 43, 45, 53, 12, 42, 35, 37],
          [20, 59, 42, 10, 46, 56, 12, 61],
          [26, 39, 27, 59, 44, 54, 23, 56],
          [32, 31, 56, 47, 31, 2, 29, 41]]

ciphertext = "7Nv7}dI9hD9qGmP}CR_5wJDdkj4CKxd45rko1cj51DpHPnNDb__EXDotSRCP8ZCQ"

# cf: http://arc360.info/algo/privatekey.html
def euclidean(x, y):
	x1 = 1
	y1 = 0
	z1 = x
	x2 = 0
	y2 = 1
	z2 = y

	while z2 != 1:
		q = (z1 - (z1 % z2)) / z2
		x1 = x1 - q * x2
		y1 = y1 - q * y2
		z1 = z1 - q * z2

		x1, y1, z1, x2, y2, z2 = x2, y2, z2, x1, y1, z1

	while x2 < 0:
		x2 += y
	
	return x2

alphabet_to_number = {}

for i in range(0, len(alphabet)):
	alphabet_to_number[alphabet[i]] = i

# https://www.youtube.com/watch?v=LhBovzm4Ajs
det = numpy.around(numpy.linalg.det(matrix)).astype(numpy.int64)
inv = numpy.around(det * numpy.linalg.inv(matrix)).astype(numpy.int64)

mul = euclidean(det, N)
inv = mul * inv

for i in range(0, len(inv)):
	for j in range(0, len(inv[i])):
		inv[i][j] = inv[i][j] % N

ans = ''

for j in range(0, 8):
	cipherarray = []
	for i in range(0, len(inv)):
		cipherarray.append(alphabet_to_number[ciphertext[j * len(inv) + i]])
	plain = numpy.dot(inv, cipherarray)

	for i in range(0, len(plain)):
		ans += alphabet[ plain[i] % N]

print ans

Flagstaff

鍵とフラグを隠したサーバが動いていて、 Python のコードがおかれている。

このサーバは、

  • decrypt に続けて暗号化データを渡すと、渡されたデータを復号化して返す
  • secret に続けて文字列を渡すと、その文字列を復号化して、それをコマンドとみなして次のことを実行する(なので暗号化して渡さないといけない)
    • コマンドに “encrypt” が含まれていれば、そのあと渡された平文の文字列を暗号化して渡す
    • コマンドに “flag” が含まれていれば、フラグを暗号化して渡す

また、暗号化方式は AES256-CBC で、サーバとの受け渡しの時はどのデータも base64 でやりとりする。

なお、暗号化の際は初期ベクタを毎回ランダムに生成している。

暗号化データの解除はしてもらえるので、「”flag”を暗号化したもの」が手に入ればよいことになる。ただ、暗号化機能を呼び出すにも「”encrypt”を暗号化したもの」が必要。

CBC なので、あるデータを暗号化した結果が分かった時、それを見て初期ベクタをいじることで、先頭のブロックの結果を好きなようにいじることができる。

なので、最初に \0 を32バイト並べたものを復号化し、初期ベクタを「先ほど得られた先頭ブロック ^ 望む状態先頭ブロック」に書き換えると、先頭16バイトは好きなようになる。今回は先頭4バイトが「flag」になればよいので、初期ベクタの4バイトを修正した。

試しに修正したあとのものを decrypt してもらうと望む通り flag で始まったので、 secret に送ると無事暗号化されたフラグが手に入った。これを復号化してもらって提出。

writeup 用に保存しておいたログから必要な部分を抜き出したものが以下。

$ dd if=/dev/zero bs=32 count=1 | base64
1+0 records in
1+0 records out
32 bytes copied, 0.000194334 s, 165 kB/s
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=

$ nc flagstaff.vuln.icec.tf 6003 
Welcome to the secure flag server.
Send me a command: decrypt
Send me some data to decrypt: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
6VlxPr4/oHDN5x6RmAek+w==

$ echo -n 6VlxPr4/oHDN5x6RmAek+w== | base64 -d | od -t x1
0000000 e9 59 71 3e be 3f a0 70 cd e7 1e 91 98 07 a4 fb
0000020

$ python
Python 2.7.12 (default, Jul  1 2016, 15:12:24) 
[GCC 5.4.0 20160609] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> hex(0xe959713e ^ 0x666c6167)
'0x8f351059'
>>> quit()

$ (perl -e 'print "\x8f\x35\x10\x59";'; dd if=/dev/zero bs=28 count=1) | base64
1+0 records in
1+0 records out
28 bytes copied, 0.000133464 s, 210 kB/s
jzUQWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=

$ nc flagstaff.vuln.icec.tf 6003 
Welcome to the secure flag server.
Send me a command: decrypt
Send me some data to decrypt: jzUQWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
ZmxhZ74/oHDN5x6RmAek+w==

$ echo ZmxhZ74/oHDN5x6RmAek+w== | base64 -d
flag�?�p������

$ nc flagstaff.vuln.icec.tf 6003 
Welcome to the secure flag server.
Send me a command: secret
Send me an encrypted command: jzUQWQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
6g0QvuIcEb1TfCq9vlApP6tKr+agmfm44nOWreOHXhM3F7QQrlwwmC4u9ZII4LzaY8VyCSy7Wci2txjkHiUREjks38JcKkm84AmETtJ4utI=

$ nc flagstaff.vuln.icec.tf 6003 
Welcome to the secure flag server.
Send me a command: decrypt
Send me some data to decrypt: 6g0QvuIcEb1TfCq9vlApP6tKr+agmfm44nOWreOHXhM3F7QQrlwwmC4u9ZII4LzaY8VyCSy7Wci2txjkHiUREjks38JcKkm84AmETtJ4utI=
SWNlQ1RGe3JldmVyc2VfYWxsX3RoZV9ibG9ja3NfYW5kX2dldF90b190aGVfbWVhbmluZ19iZWhpbmR9

$ echo SWNlQ1RGe3JldmVyc2VfYWxsX3RoZV9ibG9ja3NfYW5kX2dldF90b190aGVfbWVhbmluZ19iZWhpbmR9 | base64 -d
IceCTF{reverse_all_the_blocks_and_get_to_the_meaning_behind}

RSA 2

RSA で、これまでと違ってとりあえず形式的には正しそうなパラメータが付与された。鍵も2048ビットあった。

実は素数のどちらかが小さくて素因数分解できるのでは、と思って奇数で割り続けるプログラムを走らせたところ、予想通りすぐに素因数分解が完了した。

というわけで、あとはその結果をもとに Wikipedia とかを見ながら暗号化解除。

#!/usr/bin/python

N=0xee290c7a603fc23300eb3f0e5868d056b7deb1af33b5112a6da1edc9612c5eeb4ab07d838a3b4397d8e6b6844065d98543a977ed40ccd8f57ac5bc2daee2dec301aac508f9befc27fae4a2665e82f13b1ddd17d3a0c85740bed8d53eeda665a5fc1bed35fbbcedd4279d04aa747ac1f996f724b14f0228366aeae34305152e1f430221f9594497686c9f49021d833144962c2a53dbb47bdbfd19785ad8da6e7b59be24d34ed201384d3b0f34267df4ba8b53f0f4481f9bd2e26c4a3e95cd1a47f806a1f16b86a9fc5e8a0756898f63f5c9144f51b401ba0dd5ad58fb0e97ebac9a41dc3fb4a378707f7210e64c131bca19bd54e39bbfa0d7a0e7c89d955b1c9f
e=0x10001
c=0x3dbf00a02f924a70f44bdd69e73c46241e9f036bfa49a0c92659d8eb0fe47e42068eaf156a9b3ee81651bc0576a91ffed48610c158dc8d2fb1719c7242704f0d965f8798304925a322c121904b91e5fc5eb3dc960b03eb8635be53b995217d4c317126e0ec6e9a9acfd5d915265634a22a612de962cfaa2e0443b78bdf841ff901423ef765e3d98b38bcce114fede1f13e223b9bd8155e913c8670d8b85b1f3bcb99353053cdb4aef1bf16fa74fd81e42325209c0953a694636c0ce0a19949f343dc229b2b7d80c3c43ebe80e89cbe3a3f7c867fd7cee06943886b0718a4a3584c9d9f9a66c9de29fda7cfee30ad3db061981855555eeac01940b1924eb4c301

# cf: http://arc360.info/algo/privatekey.html
def euclidean(x, y):
	x1 = 1
	y1 = 0
	z1 = x
	x2 = 0
	y2 = 1
	z2 = y

	while z2 != 1:
		q = (z1 - (z1 % z2)) / z2
		x1 = x1 - q * x2
		y1 = y1 - q * y2
		z1 = z1 - q * z2

		x1, y1, z1, x2, y2, z2 = x2, y2, z2, x1, y1, z1

	while x2 < 0:
		x2 += y
	
	return x2

def decrypt_rsa(N, e, d, cipher):
	c_powers = [cipher]
	cur_power = 1

	while cur_power <= d:
		tmp = c_powers[len(c_powers) - 1]
		c_powers.append((tmp * tmp) % N)
		cur_power *= 2

	cur_power = 1
	cur_pos = 0
	ans = 1

	while cur_power <= d: if d & cur_power > 0:
			ans *= 	c_powers[cur_pos]
			ans %= N
		cur_pos += 1
		cur_power *= 2

	return ans


#i = 3
i = 57970027

while True:
	if N%i==0:
		p=i
		q=N/p
		break

	i+=2

d = euclidean(e, (p-1)*(q-1))

ans = decrypt_rsa(N, e, d, c)

ans_array = []
while ans:
	ans_array.append(chr(ans % 256))
	ans /= 256

ans_array.reverse()

print ''.join(ans_array)