baseball 바이너리는 table 파일의 64글자를 알파벳으로 쓰는 커스텀 base64 인코더다. 정작 table은 주어지지 않지만, 평문 text_in과 그 인코딩 text_out이라는 알려진 쌍이 있다. 표준 base64의 위치별 6비트 값에 text_out 문자를 대응시켜 알파벳을 복원하고, 같은 알파벳으로 flag_out을 복호했다.
바이너리를 분석하여 플래그를 얻어주세요. 얻은 플래그는 DH{<flag>} 형식으로 인증해주세요.
baseball 바이너리와 text_in.txt·text_out.txt·flag_out.txt 세 텍스트 파일이 주어진다. 바이너리를 분석해 보면 이건 커스텀 알파벳 base64 인코더다. 알파벳(table)만 알면 flag_out을 복호할 수 있는데, table은 안 준다. 대신 평문과 그 인코딩 결과 한 쌍이 주어져서, 그걸로 알파벳을 역으로 캐낼 수 있다.
base64는 3바이트를 6비트씩 끊어 4글자로 바꾸는 인코딩이다. 표준 알파벳은 A-Z a-z 0-9 + / 64글자로 정해져 있지만, 그 64글자를 다른 순서(커스텀 알파벳) 로 바꿔치기하면 겉모습만 다른 base64가 된다. 비트 분해 방식은 그대로고, 6비트 값을 어떤 글자로 표기하느냐만 달라진다.
핵심은 여기 있다. base64는 위치별로 "값 → 글자"가 정해진 위치 독립적 인코딩이라, 같은 평문을 표준 base64와 커스텀 base64로 각각 인코딩하면 같은 위치에 같은 6비트 값이 온다. 표기 글자만 다를 뿐이다. 그래서 평문과 커스텀 인코딩 결과를 나란히 놓으면, 각 위치에서 "이 6비트 값은 이 커스텀 글자로 표기된다"를 하나씩 알아낼 수 있다. 이것이 known-plaintext 공격이다 — 알려진 평문/암호문 쌍으로 키(여기선 알파벳)를 복원한다.
🔬 정찰 — 세 파일과 바이너리
먼저 세 텍스트 파일을 본다.
head -c 70 text_in.txt # 평문 (Pepero 설명)head -c 70 text_out.txt # 커스텀 base64cat flag_out.txt # flag 인코딩
text_in은 Pepero 평문, text_out은 커스텀 base64, flag_out은 짧은 인코딩 문자열
text_in.txt는 읽을 수 있는 영어 문장이고, text_out.txt는 base64처럼 생긴 문자열이다(길이도 149바이트 → 200자로 base64 비율과 맞다). flag_out.txt는 짧은 인코딩 문자열이다. 바이너리가 무엇을 하는지 디스어셈블로 확인한다.
usage는 ./baseball table input, encode 함수 0x1289는 shr al,2 등 표준 base64 비트분해 후 알파벳 table@0x4040 인덱싱
usage는 ./baseball <table filename> <input filename>이고, 인코딩 함수(0x1289)는 shr al, 0x2 같은 표준 base64 비트 분해를 한 뒤 결과 6비트 값으로 전역 0x4040의 table을 인덱싱한다. 즉 table 파일의 64글자가 곧 출력 알파벳이다. 그런데 그 table 파일은 주어지지 않았다 — 이게 사실상 비밀 키다.
💣 핵심 — 알려진 쌍으로 알파벳 복원
text_in과 text_out이 바로 그 알려진 쌍이다. text_in을 표준 base64로 인코딩하면 위치별 6비트 값을 알 수 있고, 같은 위치의 text_out 글자가 그 값의 커스텀 표기다.
import base64STD = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"std_b64 = base64.b64encode(open("text_in.txt","rb").read()).decode()ct = open("text_out.txt").read().strip()custom = {}for a, b in zip(std_b64, ct): # 같은 위치끼리 if a == "=": continue custom[STD.index(a)] = b # 값 STD.index(a) 는 커스텀에서 b 로 표기
known-plaintext 알파벳 복원 — 표준 base64와 커스텀 out을 위치별 대응, 값0→미관측·값1→h·값2→s 식으로 53/64 복원
이렇게 하면 200자 텍스트에서 64칸 알파벳 중 53칸이 복원된다(나머지 11칸은 이 텍스트에 안 나온 값이다). 복원이 정확한지 확인하려고, 복원한 알파벳으로 text_in을 다시 인코딩해 text_out과 같은지 대조했다.
복원한 알파벳으로 text_in 재인코딩 결과가 text_out과 완전 일치, flag_out은 Did you know how base64 works로 복호
재인코딩 결과가 text_out과 정확히 일치한다 — 알파벳이 맞다는 뜻이다. 그리고 flag_out은 이 53칸 안의 글자만 쓰고 있어 그대로 복호된다.
▶ 삽질: 복호 결과가 DH 형식이 아니라 문장, 공백째 제출했다가 오답
flag_out을 복호하니 DH{...}가 아니라 "Did you know how base64 works"라는 평범한 영어 문장이 나왔다. 처음엔 "flag_out이 진짜 플래그가 아니라 힌트인가?" 싶어 바이너리에 숨은 flag가 있나, table 자체가 flag인가 한참 뒤졌다.
결국 이 문장이 곧 플래그 내용이었다. 문제 설명이 DH{<flag>} 형식이라 했으니 감싸면 되는데, 공백이 문제였다. DH{Did you know how base64 works}로 냈더니 오답. DreamHack 플래그는 공백 대신 언더스코어를 쓰는 관례라, DH{Did_you_know_how_base64_works}로 바꾸니 통과했다. 복호 결과를 그대로 내기 전에 플래그 형식(공백·대소문자)을 한 번 정규화하는 습관이 필요했다.
🚀 Full Exploit
복원한 알파벳으로 flag_out을 커스텀→표준 문자로 되돌린 뒤 표준 base64 디코드하고, 공백을 언더스코어로 바꿔 플래그 형식에 맞춘다.
import base64STD = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"std_b64 = base64.b64encode(open("text_in.txt","rb").read()).decode()ct = open("text_out.txt").read().strip()custom = {}for a, b in zip(std_b64, ct): if a != "=": custom[STD.index(a)] = binv = {b: STD[v] for v, b in custom.items()} # 커스텀 글자 → 표준 글자flag_ct = open("flag_out.txt").read().strip()std_flag = "".join("=" if c == "=" else inv[c] for c in flag_ct)plain = base64.b64decode(std_flag).decode()print("DH{" + plain.replace(" ", "_") + "}")
solve.py 실행 — 알파벳 53/64 복원, flag_out 복호 Did you know how base64 works, 최종 플래그 출력
복원한 커스텀 알파벳: 53/64 엔트리flag_out 복호: 'Did you know how base64 works'FLAG: DH{Did_you_know_how_base64_works}
📝 결론
커스텀 알파벳은 알려진 평문 한 쌍이면 무너진다.
table(알파벳)을 비밀로 감췄지만, 평문과 그 인코딩 결과가 함께 노출되는 순간 알파벳은 위치별 대응으로 그대로 복원된다. base64처럼 위치 독립적인 인코딩은 특히 취약하다 — 한 위치의 대응이 다른 위치와 무관하게 성립하므로, 충분히 긴 known-plaintext만 있으면 알파벳 대부분이 드러난다. 알파벳을 바꾼 "커스텀" base64는 난독화일 뿐 암호가 아니다.
복호 결과는 플래그 형식으로 정규화해서 제출한다.
복호로 얻은 원문이 곧 답이라도, 대회 플래그 형식(공백→언더스코어, 대소문자, 감싸는 DH{})에 맞추는 마지막 한 단계가 필요하다. 원문을 그대로 내서 오답이 나오면 "풀이가 틀렸나" 의심하기 쉬운데, 형식 문제인 경우가 많다. 복호 → 형식 정규화 → 제출 순서를 기억하면 이런 헛발질을 줄일 수 있다.
Comments
댓글
댓글을 남기려면 로그인이 필요해요. (네이버 · 구글 계정)
댓글 불러오는 중…