secret 문장을 바이트 단위 Caesar로 암호화한 결과만 주어진다. 키는 랜덤이지만 1~255 사이 한 바이트라 경우의 수가 255개뿐이다. 각 키로 복호해 결과가 전부 인쇄 가능한 ASCII이고 DH가 들어간 키를 찾으면 된다. 함께 준 secret 파일은 가짜라, output.txt의 암호문을 전수조사해 진짜 플래그를 꺼냈다.
prob.py, secret, output.txt 세 파일이 주어진다. prob.py는 어떤 문장을 바이트 단위 Caesar 암호로 밀어 output.txt에 hex로 출력한다. 키는 실행할 때마다 랜덤으로 뽑히지만, 1~255 사이의 한 바이트라 경우의 수가 고작 255개다. 전수조사로 금방 뚫린다.
Caesar cipher는 글자를 일정 칸수만큼 미는 치환 암호다. 보통은 알파벳 26자 안에서 밀지만, 이 문제는 바이트 전체(0~255) 를 대상으로 민다.
암호화: enc[i] = (secret[i] + key) % 256
복호화: dec[i] = (enc[i] - key) % 256
여기서 핵심은 키가 한 바이트라는 점이다. 알파벳 Caesar가 키 25가지뿐이라 쉽게 뚫리듯, 바이트 Caesar도 키가 255가지(1~255)뿐이라 전부 대입해 보면 된다. 랜덤으로 뽑았다는 사실은 아무 방어가 되지 않는다 — 어차피 다 세어 볼 수 있으니까.
전수조사에서 "맞는 키"를 어떻게 알아볼까? 원문이 읽을 수 있는 문장(ASCII)이라는 걸 이용한다. 잘못된 키로 복호하면 제어문자·깨진 바이트가 섞여 나오고, 맞는 키로만 전부 인쇄 가능한 ASCII가 나온다. 여기에 "플래그 형식 DH{가 포함되는가"까지 조건으로 걸면 정답 키가 유일하게 걸린다.
🔬 정찰 — 소스와 가짜 secret
prob.py는 짧다. 랜덤 키로 secret 파일을 바이트 Caesar 암호화해 hex로 출력한다.
class Caesar: def __init__(self, key): assert isinstance(key, int) and 1 <= key <= 255 self._key = key def encrypt(self, msg): return bytes([(b + self._key) % 256 for b in msg])key = random.randint(1, 255) # 키는 1~255 랜덤 한 바이트secret_enc = Caesar(key).encrypt(open("secret","rb").read())print(f"my encrypted sentence > {secret_enc.hex()}")
prob.py — Caesar 클래스의 바이트 (b+key)%256 암호화, main에서 랜덤 키로 secret 암호화 후 hex 출력
주의할 함정이 하나 있다. 함께 주어진 secret 파일을 열어 보면 진짜 플래그가 아니라 가짜다.
제공된 secret 파일은 This is not real flag 가짜 문장, 진짜 암호문은 output.txt에
secret 파일 내용은 "This is not real flag, DH{FAKE_FLAG} :>" — 대놓고 가짜다. 진짜 암호문은 output.txt의 hex뿐이니, 그걸 복호해야 한다.
💣 핵심 — 255개 키를 전부 대입
output.txt의 암호문을 1부터 255까지 모든 키로 복호하면서, 결과가 전부 인쇄 가능하고 DH{를 포함하는 키를 찾는다.
키별 복호 시연 — key=177은 "Uif tvo..."로 한 칸 어긋나고, key=178이 "The sun slowly..."로 정답
키가 하나만 어긋나도 결과가 확 달라진다. key=177이면 "Uif tvo…"처럼 한 칸씩 밀린 글자가 나오고, key=178에서 비로소 "The sun slowly sets over the calm…"이라는 온전한 영어 문장이 나온다. 정답 키는 178이다.
🚀 Full Exploit
전수조사로 키를 찾아 복호하고, 문장 안에서 플래그를 정규식으로 뽑는다.
import reline = [l for l in open("output.txt") if "encrypted sentence >" in l][0]enc = bytes.fromhex(line.split("> ")[1].strip())for key in range(1, 256): dec = bytes((b - key) % 256 for b in enc) if b"DH{" in dec and all(32 <= c < 127 for c in dec): print("key =", key) print(re.search(rb"DH\{[^}]+\}", dec).group(0).decode()) break
solve.py 실행 — key=178로 복호한 긴 영문 문장과 그 끝에서 추출한 플래그
key=178로 복호하면 노을 지는 바다를 묘사한 긴 문단이 나오고, 그 마지막 문장 "…sleeping with the flag, DH{…}"에 플래그가 박혀 있다.
이 문제의 키는 매번 랜덤으로 뽑히지만 1~255 중 하나라, 255번만 시도하면 반드시 걸린다. 암호의 안전성은 알고리즘의 복잡함이 아니라 키 공간의 크기에서 나온다. 아무리 그럴듯한 암호라도 키가 몇백 가지뿐이면 전수조사 앞에 무너진다. 실전에서 키·PIN·토큰을 다룰 때 "경우의 수가 몇 개인가"를 먼저 따지는 습관이 중요하다.
복호의 정답은 평문의 형태로 판별한다.
전수조사에서 맞는 키를 자동으로 골라낸 기준은 "결과가 읽을 수 있는 ASCII이고 플래그 형식을 포함하는가"였다. 원문이 어떤 형태(영어 문장·특정 매직바이트·플래그 접두사)인지 알면, 수백~수만 개 후보 중에서 정답을 기계적으로 걸러낼 수 있다. 브루트포스는 "다 해보기"가 아니라 "다 해보되 정답을 알아보는 기준을 세우기"가 핵심이다.
Comments
댓글
댓글을 남기려면 로그인이 필요해요. (네이버 · 구글 계정)
댓글 불러오는 중…