[🥉 Bronze 4] 단일 바이트 XOR — 256개 키 전수 대입으로 뚫기 (DreamHack SingleByteXor 풀이) — ZINO
2026-07-08·1분 읽기·
[🥉 Bronze 4] 단일 바이트 XOR — 256개 키 전수 대입으로 뚫기 (DreamHack SingleByteXor 풀이)
DyingMessage 파일에 담긴 23바이트 hex 암호문. 단일 바이트 XOR 이라 키 후보는 256개뿐이다. 플래그가 DH{ 로 시작한다는 사실(known-plaintext)로 키를 한 번에 복원하거나, 256개를 전수 대입해 프린터블하게 풀리는 유일한 키(0x10)를 찾으면 플래그가 나온다.
단일 바이트 XOR 원리도: 암호화 C=P XOR K, 복호화 P=C XOR K(자기역원), 키공간 256개. known-plaintext 로 K = C[0] XOR 'D' = 0x54 XOR 0x44 = 0x10, 검산 후 전체 복호 → DHtHe_k1LleR_1s_dReAm
2. 정찰 — DyingMessage 는 무엇인가
제공된 파일은 DyingMessage 하나뿐이다. file로 보면 그냥 ASCII 텍스트고, 열어 보면 hex 문자열이 들어 있다.
정찰 스크립트 전문과 실행 명령이다.
# analysis/cap_recon.sh#!/usr/bin/env bash# 정찰 — 주어진 파일 하나(DyingMessage)의 정체 파악cd extractedecho "== file 타입 =="file DyingMessageechoecho "== 내용 (hexdump) — ASCII 로 보이는 hex 문자열 =="xxd DyingMessageechoecho "== 이 텍스트는 hex 문자열. 46 hex chars = 23바이트 암호문 =="echo -n "raw bytes: "; python3 -c "print(bytes.fromhex(open('DyingMessage').read().strip()).hex())"echoecho "== 첫 바이트로 키 추정: 평문 첫 글자는 'D'(DH{...) 라고 가정 =="python3 -c "ct=bytes.fromhex(open('DyingMessage').read().strip()); print(f'ct[0]=0x{ct[0]:02x}, D=0x44 -> key = 0x{ct[0]^0x44:02x}')"
bash cap_recon.sh
DyingMessage 정찰: file 은 ASCII text, xxd 로 보면 "54586b6458754f7b..." hex 문자열(46 hex chars = 23바이트 암호문), 첫 바이트로 키 추정 ct[0]=0x54 XOR D=0x44 = 0x10
54586b6458754f7b215c7c75424f21634f744275517d6d
46개의 hex 문자 = 23바이트 암호문이다. 파일명 DyingMessage("죽어가며 남긴 메시지")가 힌트인데, 결국 이 23바이트를 복호하면 범인을 지목하는 문장이 나온다.
3. 풀이 — 두 가지 길
방법 A. Known-plaintext 로 키 즉시 복원
플래그는 항상 DH{ 로 시작한다. 즉 평문의 첫 글자는 'D'(0x44) 임을 이미 안다.
XOR 의 자기역원 성질로 키를 바로 계산한다.
K = C[0] ^ ord('D') # 0x54 ^ 0x44 = 0x10
검산도 간단하다. 0x58 ^ 0x10 = 0x48 = 'H', 0x6b ^ 0x10 = 0x7b = '{'. 두 번째·세 번째 글자가 H, {로 딱 맞으니 키는 0x10이 확실하다.
방법 B. 256개 전수 대입
키를 몰라도 상관없다. 0x00부터 0xff까지 전부 XOR 해 보고, 전부 출력 가능한 문자로 풀리면서 DH{...} 형식인 키만 고르면 된다.
256개 단일바이트 키 전수 대입 결과: 프린터블하게 복호되는 후보 22개 중 key=0x10 에서만 DHtHe_k1LleR_1s_dReAm 형식이 나와 유일하게 확정
프린터블하게 풀리는 후보가 22개나 되지만(짧은 문자열이라 우연히 프린터블한 키가 여럿 나온다), DH{로 시작하고 }로 끝나는 건 0x10 하나뿐이라 유일하게 확정된다. 두 방법 모두 같은 결론에 도달한다.
4. 자체 완결 solve.py
정찰부터 플래그까지를 한 스크립트로 정리했다. 앵커 방식으로 키를 추정하고, 전수 대입으로 형식까지 검증한다.
ct = bytes.fromhex(open("DyingMessage").read().strip()) # 23바이트 암호문# 앵커: 평문 첫 글자 'D' → key = ct[0] ^ ord('D') = 0x10for k in range(256): # 전수 대입 + 형식 검증 pt = bytes(b ^ k for b in ct) if pt.startswith(b"DH{") and pt.endswith(b"}") and all(32 <= c < 127 for c in pt): print(f"key=0x{k:02x} FLAG: {pt.decode()}")
solve.py 실행: 23바이트 암호문 출력, 첫 바이트 앵커로 key=0x10 추정, 전수 대입으로 key=0x10 확정, FLAG: DHtHe_k1LleR_1s_dReAm 출력
복호된 문장은 tHe_k1LleR_1s_dReAm — "the killer is dream". DyingMessage라는 파일명에 걸맞은 다잉 메시지다.
플래그: DH{tHe_k1LleR_1s_dReAm}
5. 삽질 기록
▶🐛 삽질 — '프린터블하면 정답'이 아니었던 함정
처음엔 "전부 출력 가능한 문자로 풀리는 키 = 정답"이라고 생각했다. 그런데 암호문이 23바이트로 짧다 보니
프린터블하게 풀리는 키가 0x10 말고도 22개나 나왔다(0x0e는 ZVejV{Au/Rr{LA/mAzL{_sc 처럼 그럴싸하게 보이기까지 한다).
짧은 문자열에서는 "우연히 프린터블"이 흔하다는 걸 간과한 것. 판별 기준을 프린터블 여부가 아니라 **플래그 형식(DH{로 시작, }로 끝)**으로
좁히자 곧바로 0x10 하나로 확정됐다. known-plaintext(플래그 접두사)를 필터로 쓰는 게 이래서 강력하다.
6. 방어 관점 — XOR 을 암호로 쓰지 말 것
이건 애초에 "암호"라고 부를 수 없는 수준이지만, 실무에서도 XOR 난독화를 종종 본다(멀웨어 문자열 숨김, 설정파일 "암호화" 등). 교훈은 분명하다.
키가 짧으면 전수 대입으로 즉시 뚫린다. 1바이트는 256번, 4바이트라도 40억 번이면 끝난다. 키 공간이 작으면 브루트포스가 승리한다.
형식이 알려진 평문(known-plaintext)은 치명적이다. 플래그 접두사·파일 매직바이트·고정 헤더처럼 "평문 일부를 안다"면 XOR 키는 그 자리에서 복원된다.
XOR 반복 키(Vigenère류)도 안전하지 않다. 키 길이만 추정하면(카시스키/자기상관) 각 위치가 단일 바이트 XOR 로 쪼개져 동일하게 뚫린다.
기밀성이 필요하면 검증된 **AEAD(AES-GCM, ChaCha20-Poly1305)**를 쓰고, 키는 충분한 엔트로피로 안전하게 관리해야 한다. XOR 난독화는 "숨김"이지 "암호"가 아니다.
Comments
댓글
댓글을 남기려면 로그인이 필요해요. (네이버 · 구글 계정)
댓글 불러오는 중…