Step 248. 종합 포렌식 챌린지 5개 — 기법 사슬을 혼자 힘으로 잇는다
Level 3 — CTF 실전과 공격 스킬 심화 | 난이도 ★★★★☆ | 예상 소요 시간 4~6시간
전제: Step 239(파일 시그니처), Step 245(로그 분석), Step 246(EXIF/메타데이터), Step 247(암호화 아티팩트 복구) 완료. 파이썬 3을 사용합니다.
⚠️ 이 챕터의 실습은 내 랩·합법 플랫폼 전용입니다. 허가 없는 시스템에 적용하면 범죄입니다.
- 준비물: 파이썬 3(실측: 3.12.14, pillow·python-docx 포함), 기록용 메모 파일. 인터넷 연결은 필요 없습니다.
- 주의: 워게임 챕터입니다. 3절에서 챌린지 파일을 직접 생성하고, 4절의 문제만 보고 먼저 풀어 보세요. 5절의 모범 답안은 막혔을 때나 다 푼 뒤 비교용입니다.
실전 포렌식 문제는 기법이 섞여 나옵니다. 파일 하나를 열었더니 다음 단서가 나오고, 그 단서가 다른 파일의 열쇠가 되는 사슬(chain) 구조입니다. "pcap에서 파일 추출 → 그 파일이 암호화 → 크래킹 → 안에 메타데이터 단서"처럼, 트랙에서 배운 것들을 순서대로 연결하는 사고가 검증 대상입니다. 오늘은 다섯 개의 미니 챌린지를 직접 만들고, 정답을 모르는 척하며 풀고, 기법 사슬도를 그리는 것까지가 완료 조건입니다.
1. 학습 목표
이 챕터를 끝내면 다음을 할 수 있습니다:
- 미지의 파일을 받으면 시그니처 확인부터 시작하는 조사 순서를 몸으로 수행한다
- 하나의 증거에서 나온 단서를 다른 증거의 열쇠로 연결한다(기법 사슬)
- 막혔을 때 "아직 안 써본 도구" 체크리스트로 회귀하는 습관을 들인다
- 각 풀이를 "기법 사슬도"로 정리해 재현 가능하게 문서화한다
- 다섯 문제 중 넷 이상을 독립 해결한다
2. 배경 지식 — 오늘의 도구와 개념
오늘의 도구 한눈에 보기
| 구분 | 내용 |
|---|---|
| 언어·환경 | 파이썬 3 — pillow, python-docx, zipfile, struct, re |
| 오늘의 기법 | 시그니처 판별(Step 239), 카빙(carving), 로그에서 단서 추출(Step 245), EXIF/문서 메타데이터(Step 246), XOR 키 복구(Step 247) |
| 필요한 개념 | 기법 사슬, 카빙(carving), 증거 목록 → 기법 매핑 계획, 막힘 대응 루틴 |
2-1. 기법 사슬 — 한 증거가 다른 증거의 열쇠
종합 챌린지의 표준 구조는 사슬입니다. 예를 들어:
로그 파일 ──(읽기)──→ 비밀번호 단서 ──(XOR 복호)──→ 잠긴 파일 ──(열기)──→ 플래그
각 고리는 지난 챕터 하나하나에 대응됩니다. 그래서 종합 문제의 실력은 새 기법이 아니라 "지금 이 증거에 어떤 기법을 꺼낼 것인가"의 선택 속도입니다. 문제를 시작할 때 "증거 목록 → 각각에 적용할 기법" 표를 먼저 그리는 이유입니다.
2-2. 카빙 — 파일 속의 파일 꺼내기
카빙(carving)은 파일 안에 숨은 다른 파일을 시그니처로 찾아 잘라내는 기법입니다. 이미지 끝에 ZIP이 이어 붙어 있는 식의 위장이 단골입니다. 원리는 단순합니다: ZIP은 PK\x03\x04로 시작하므로, 바이너리에서 그 시그니처의 위치를 찾아 거기서부터 끝까지 잘라내면 ZIP이 됩니다. Step 239에서 시그니처를 배운 이유가 바로 이것입니다.
2-3. 막힘 대응 루틴 — 체크리스트로 회귀
막혔을 때의 표준 루틴은 "아직 안 써본 도구 목록"을 훑는 것입니다. 이 트랙 기준으로는:
- 시그니처 확인했는가? (확장자를 믿지 말 것)
- 문자열 추출(
strings역할) 했는가? - 메타데이터(EXIF, core.xml) 봤는가?
- 파일 안에 또 파일이 있는가? (카빙)
- 암호화라면 — 키 공간이 작은가? 평문을 아는가? 비밀번호 단서가 다른 증거에 있는가?
다섯 번째가 오늘 챌린지의 핵심 동선입니다 — "이 파일을 여는 비밀번호가 저기 있지 않을까"라는 연결 의심.
3. 따라 하기 — 챌린지 묶음 생성
3-1. 다섯 챌린지 만들기
챌린지 파일들을 직접 생성합니다. build_challenges.py:
from PIL import Image
from pathlib import Path
from zipfile import ZipFile, ZIP_DEFLATED
import io, docx
lab = Path("lab248"); lab.mkdir(exist_ok=True)
# C1: JPEG인데 .png로 위장 — EXIF에 플래그
img = Image.new("RGB", (200, 120), (120, 30, 30))
ex = Image.Exif()
ex[0x010E] = "FLAG{wr0ng_ext3nsi0n}" # ImageDescription
ex[0x010F] = "LabCam"
buf = io.BytesIO(); img.save(buf, "JPEG", exif=ex)
(lab / "c1_evidence.png").write_bytes(buf.getvalue())
# C2: PNG 끝에 ZIP 이어붙이기 (카빙 대상)
png = Image.new("RGB", (100, 60), (20, 120, 60))
b2 = io.BytesIO(); png.save(b2, "PNG")
zb = io.BytesIO()
with ZipFile(zb, "w", ZIP_DEFLATED) as z:
z.writestr("flag.txt", "FLAG{th3r3_1s_m0r3_4ft3r_IEND}\n")
(lab / "c2_sunset.png").write_bytes(b2.getvalue() + zb.getvalue())
# C3: 웹 로그에 새어 나온 비밀번호 + XOR로 잠긴 파일
log = """192.168.0.5 - - [09/Sep/2026:10:01:02 +0900] "GET /index.html HTTP/1.1" 200 512
203.0.113.77 - - [09/Sep/2026:22:31:10 +0900] "GET /admin/ HTTP/1.1" 403 162
203.0.113.77 - - [09/Sep/2026:22:33:41 +0900] "GET /uploads/shell.php?cmd=zip%20-e%20-P%20bluewave42%20backup.zip%20db.sql HTTP/1.1" 200 88
203.0.113.77 - - [09/Sep/2026:22:34:02 +0900] "GET /files/backup.zip.enc HTTP/1.1" 200 240
"""
(lab / "c3_access.log").write_text(log, encoding="utf-8")
key = b"bluewave42"
plain = b"PK\x03\x04 fake-zip-body ... FLAG{st0l3n_but_br0k3n} ..."
(lab / "c3_backup.zip.enc").write_bytes(
bytes(b ^ key[i % len(key)] for i, b in enumerate(plain)))
# C4: docx 메타데이터에 숨은 메모
d = docx.Document()
d.add_paragraph("Nothing to see here.")
d.core_properties.author = "temp.staff"
d.core_properties.comments = "reminder: FLAG{m3t4d4t4_t3lls_tal3s}"
d.save(lab / "c4_memo.docx")
# C5: 사슬 — EXIF의 Model 값이 flag.bin의 XOR 키
img5 = Image.new("RGB", (160, 100), (10, 10, 80))
e5 = Image.Exif(); e5[0x0110] = "horizon"
b5 = io.BytesIO(); img5.save(b5, "JPEG", exif=e5)
(lab / "c5_photo.jpg").write_bytes(b5.getvalue())
flag5 = b"FLAG{ch41n_0f_ev1d3nce}"
(lab / "c5_flag.bin").write_bytes(bytes(f ^ ord("horizon"[i % 7]) for i, f in enumerate(flag5)))
print("생성:", sorted(p.name for p in lab.iterdir()))
실행 (2026-09-09 실측):
생성: ['c1_evidence.png', 'c2_sunset.png', 'c3_access.log', 'c3_backup.zip.enc', 'c4_memo.docx', 'c5_flag.bin', 'c5_photo.jpg']
규칙: 생성 코드를 보긴 했지만, 풀이할 때는 코드를 닫고 파일만 보세요. 정답을 알고 푸는 것과 모르고 푸는 것은 다른 훈련입니다. 한 시간 뒤에 풀면 기억이 흐려져서 더 좋습니다.
3-2. 조사 시작 의식 — 증거 목록과 기법 매핑
풀기 전에 메모 파일(plan.md)에 이 표를 먼저 채우세요:
| 증거 파일 | 첫 확인할 것 | 쓸 수 있는 기법 |
|---|---|---|
| c1_evidence.png | 시그니처가 진짜 PNG인가 | 시그니처 판별, EXIF |
| c2_sunset.png | 파일 크기와 구조 | 카빙 |
| c3_access.log + c3_backup.zip.enc | 로그에 유출된 단서 | 로그 분석, XOR 복호 |
| c4_memo.docx | 본문 외의 층 | 문서 메타데이터 |
| c5_photo.jpg + c5_flag.bin | 두 파일의 관계 | EXIF, 사슬 사고 |
이 표가 "계획 먼저" 습관의 실물입니다. 실제 조사에서도 첫 10분은 도구가 아니라 이 표에 씁니다.
4. 미션과 연습문제
미션 — 다섯 챌린지 독립 해결
아래 다섯 문제를 풀어 각각의 FLAG{...}를 찾으세요. 규칙: 5절을 보지 않고, 찾은 플래그와 풀이 과정을 solutions.md에 기록합니다.
- C1. 열리지 않는 사진 —
c1_evidence.png를 뷰어로 열었더니 안 열립니다. 왜일까요? 파일의 진짜 정체를 밝히고 플래그를 찾으세요. - C2. 노을 사진의 비밀 —
c2_sunset.png는 평범한 100×60 이미지입니다. 그런데 파일 크기가 356바이트로 뭔가 큽니다. 숨겨진 것을 찾으세요. - C3. 도둑맞은 백업 —
c3_access.log는 침입자가 백업 파일을 가져간 기록입니다.c3_backup.zip.enc의 내용을 복구하세요. (힌트: 공격자는 명령을 로그에 남겼습니다) - C4. 아무것도 없는 문서 —
c4_memo.docx의 본문은 "Nothing to see here." 한 줄뿐입니다. 정말 아무것도 없을까요? - C5. 두 개의 증거 —
c5_photo.jpg와c5_flag.bin이 함께 발견됐습니다. 후자는 잠겨 있습니다. 전자가 열쇠입니다.
각 문제를 풀 때마다 기법 사슬도를 한 줄로 그리세요 — 예: "로그 읽기 → 비밀번호 획득 → XOR 복호 → 플래그".
연습문제
문제 1. C1에서 "확장자가 png인데 안 열리는" 상황의 조사 첫 단계는 무엇이어야 하나요? 그 이유는?
문제 2. C3에서 공격자가 실수로 남긴 것은 무엇이며, 왜 이런 실수가 실제 사건에서도 흔한가요?
문제 3. C5 같은 사슬 문제에서 "열려 있는 증거"와 "잠긴 증거"의 올바른 조사 순서는 무엇이고, 왜 그런가요?
문제 4. 다섯 문제를 다 풀었는데 4절의 기법 사슬도를 못 그리겠다면, 무엇이 부족한 상태인가요?
5. 모범 답안과 완료 기준
미션 모범 답안 (전부 2026-09-09 실측)
C1 풀이 — 시그니처가 확장자를 이긴다. 확장자가 아니라 내용을 봅니다:
raw = open("lab248/c1_evidence.png", "rb").read()
print(raw[:4].hex(" ")) # ff d8 ff e0 <- JPEG 매직!
from PIL import Image
img = Image.open("lab248/c1_evidence.png")
print(img.format) # JPEG
print(img.getexif().get(0x010E)) # FLAG{wr0ng_ext3nsi0n}
실측 출력:
ff d8 ff e0
JPEG
FLAG{wr0ng_ext3nsi0n}
사슬도: 시그니처 확인 → JPEG로 식별 → EXIF 읽기 → 플래그. 파일 시그니처 FF D8 FF는 JPEG의 지문입니다 (Step 239). 확장자는 주장일 뿐, 시그니처가 사실입니다.
C2 풀이 — IEND 뒤의 세계. PNG는 IEND 청크로 끝나야 하는데 파일이 더 깁니다. ZIP 시그니처를 찾아 잘라냅니다:
d = open("lab248/c2_sunset.png", "rb").read()
print(d.find(b"IEND"), d.find(b"PK\x03\x04"), len(d)) # 203 211 356
from zipfile import ZipFile
import io
with ZipFile(io.BytesIO(d[d.find(b"PK\x03\x04"):])) as z:
print(z.namelist()) # ['flag.txt']
print(z.read("flag.txt").decode().strip())
실측 출력:
203 211 356
['flag.txt']
FLAG{th3r3_1s_m0r3_4ft3r_IEND}
사슬도: 크기 이상 감지 → PK 시그니처 탐색 → 카빙 → ZIP 해제 → 플래그. 뷰어는 PNG 부분만 보여 주니 이어붙인 ZIP이 눈에 안 띕니다 — "보이는 끝"과 "파일의 끝"이 다른 것이 위장의 요점입니다.
C3 풀이 — 공격자의 손가락이 남긴 것. 로그를 읽으면 웹쉘 명령이 보입니다:
import re
log = open("lab248/c3_access.log").read()
m = re.search(r"-P%20(\w+)", log)
print(m.group(1)) # bluewave42
key = m.group(1).encode()
enc = open("lab248/c3_backup.zip.enc", "rb").read()
print(bytes(b ^ key[i % len(key)] for i, b in enumerate(enc)).decode())
실측 출력:
bluewave42
PK fake-zip-body ... FLAG{st0l3n_but_br0k3n} ...
사슬도: 로그 분석 → 웹쉘 명령에서 비밀번호(-P 옵션) 획득 → XOR 복호 → 플래그. %20은 URL 인코딩된 공백입니다. 공격자는 암호를 걸면서 그 비밀번호를 명령행에 평문으로 남겼고, 웹 서버는 요청을 전부 기록합니다. Step 245의 웹쉘 흔적 읽기가 그대로 열쇠가 됐습니다.
C4 풀이 — 본문은 미끼. docx의 본문이 아니라 메타데이터를 봅니다:
from zipfile import ZipFile
import re
with ZipFile("lab248/c4_memo.docx") as z:
core = z.read("docProps/core.xml").decode("utf-8")
print(re.search(r"<dc:creator>(.*?)</dc:creator>", core).group(1))
print(re.search(r"<dc:description>(.*?)</dc:description>", core).group(1))
실측 출력:
temp.staff
reminder: FLAG{m3t4d4t4_t3lls_tal3s}
사슬도: 본문 확인(미끼) → ZIP으로 해부 → core.xml → 플래그. "본문에 없으면 끝"이 아니라, 문서에는 본문 외의 층(메타데이터, 주석, 숨은 텍스트)이 있습니다 (Step 246).
C5 풀이 — 열린 증거가 잠긴 증거의 열쇠. 사진의 EXIF부터:
from PIL import Image
key5 = Image.open("lab248/c5_photo.jpg").getexif().get(0x0110)
print(key5) # horizon
data = open("lab248/c5_flag.bin", "rb").read()
print(bytes(b ^ key5.encode()[i % len(key5)] for i, b in enumerate(data)).decode())
실측 출력:
horizon
FLAG{ch41n_0f_ev1d3nce}
사슬도: EXIF 읽기 → Model 값이 키라고 의심 → XOR 복호 → 플래그. 잠긴 파일부터 덤비는 게 아니라 열린 증거를 먼저 다 읽는 것이 사슬 문제의 철칙입니다.
연습문제 해답
문제 1 해답. 확장자를 믿지 말고 파일의 첫 바이트(시그니처)를 확인합니다. 확장자는 이름표를 바꿔 달면 그만이지만, 시그니처는 내용 자체의 지문이기 때문입니다. 실측에서도 ff d8 ff e0(JPEG)이 드러났습니다.
문제 2 해답. 비밀번호를 명령행 옵션(-P bluewave42)으로 입력했고, 그 명령이 웹 서버 접근 로그에 평문으로 기록됐습니다. 실제 사건에서도 공격자는 서버가 모든 요청을 로깅한다는 사실을 잊거나, 급해서 명령행에 비밀번호를 씁니다. 명령행·URL은 곳곳(웹 로그, bash_history, 프로세스 목록)에 남는다는 것이 공격자의 구조적 약점입니다.
문제 3 해답. 열린 증거를 먼저 전부 읽고, 그다음 잠긴 증거에 갑니다. 사슬 구조에서는 열린 쪽에 잠긴 쪽의 열쇠(비밀번호, 키, 힌트)가 있는 경우가 많기 때문입니다. C5의 EXIF가 그랬고, C3의 로그가 그랬습니다. 잠긴 것부터 붙잡으면 전수 조사밖에 없지만, 열린 것을 먼저 보면 열쇠가 공짜로 나올 수 있습니다.
문제 4 해답. "풀었다"가 아니라 "재현 가능하게 이해했다"가 부족한 상태입니다. 사슬도를 그리려면 각 고리에서 어떤 기법을 왜 꺼냈는지 설명해야 합니다. 우연히 맞힌 문제가 있다면 그 문제의 풀이를 처음부터 다시 문서화해 보세요 — 설명이 되는 순간이 진짜 완료입니다.
완료 기준 체크리스트
- [ ] 확장자가 아니라 시그니처로 파일 정체를 판별했다 (C1)
- [ ]
IEND뒤의 이어붙인 ZIP을 카빙했다 (C2) - [ ] 로그에서 비밀번호 단서를 찾아 잠긴 파일을 열었다 (C3)
- [ ] docx의 본문 외 층(core.xml)을 조사했다 (C4)
- [ ] EXIF 값을 다른 파일의 키로 연결했다 (C5)
- [ ] 다섯 문제 각각의 기법 사슬도를 한 줄로 그렸다
- [ ] 막혔을 때 "안 써본 기법 체크리스트"를 사용했다
- [ ] 5개 중 4개 이상을 5절 없이 독립 해결했다
6. 흔한 실수와 해결
벽 1. 확장자에 속아서 처음부터 포기
증상: "png가 안 열리니 손상된 파일"이라고 넘겼습니다.
원인: 확장자를 사실로 믿었습니다.
해결: 조사의 제1동작은 언제나 첫 4~16바이트 확인입니다. raw[:4].hex(" ") 한 줄이면 됩니다. JPEG는 ff d8 ff, PNG는 89 50 4e 47, ZIP은 50 4b 03 04입니다 (Step 239 복습).
벽 2. 카빙한 ZIP이 안 열린다
증상: BadZipFile 오류가 납니다.
원인: PK\x03\x04를 잘못된 위치에서 찾았거나(본문 데이터에 우연히 같은 바이트), ZIP 구조가 손상됐거나.
해결: 시그니처가 여러 번 나오면 find 대신 모든 위치를 뽑아(re.finditer) 뒤쪽 것부터 시도하세요. 실측 C2에서는 위치 211의 하나뿐이었지만, 실제 문제에서는 허위 시그니처가 함정으로 심어지기도 합니다.
벽 3. 로그의 비밀번호를 그대로 썼는데 복호가 안 된다
증상: -P%20bluewave42에서 bluewave42를 키로 썼는데 깨집니다.
원인: URL 인코딩(%20 등)을 디코딩 안 했거나, 키의 대소문자·공백을 바꿨거나.
해결: 로그의 문자열을 있는 그대로가 아니라 디코딩한 뒤의 실제 값으로 쓰세요. %20=공백, %22="입니다. 그래도 안 되면 키가 아니라 키의 "힌트"일 가능성을 열어 둡니다.
벽 4. 본문만 보고 "없다"고 결론
증상: 문서 본문이 빈 것 같아서 다음 문제로 넘어갔습니다.
원인: 파일을 "보이는 내용"으로만 판단했습니다.
해결: 오늘의 다섯 문제 전부가 같은 교훈입니다 — 보이는 층 뒤에 다른 층이 있습니다. 메타데이터(core.xml), 파일 뒤의 추가 데이터, EXIF. "없다"고 쓰기 전에 층 목록을 점검하세요.
벽 5. 한 증거에 매달려 시간을 다 씀
증상: 잠긴 파일 하나를 한 시간째 전수 조사 중입니다.
원인: 사슬 사고 없이 정면돌파만 시도했습니다.
해결: 타이머 규칙을 쓰세요 — 한 증거에 20분 막히면 다른 증거로 이동. 종합 문제는 어느 고리든 먼저 풀리는 곳이 사슬의 시작입니다. 잠긴 파일의 열쇠는 대개 다른 증거에 있습니다.
7. 정리
오늘의 개념
| 개념 | 한 줄 설명 |
|---|---|
| 기법 사슬 | 한 증거의 산출물이 다음 증거의 입력이 되는 종합 문제 구조 |
| 카빙(carving) | 시그니처로 파일 속 숨은 파일을 잘라내는 기법 |
| 증거 목록 → 기법 매핑 | 풀기 전에 "뭘, 어떤 기법으로" 표를 그리는 시작 의식 |
| 열린 증거 우선 | 잠긴 파일보다 열린 파일부터 — 열쇠는 열린 쪽에 있다 |
| 사슬도 | "기법 → 발견 → 기법 → 플래그" 한 줄 재현 문서 |
오늘의 명령어·코드
| 코드 | 하는 일 |
|---|---|
raw[:4].hex(" ") |
시그니처 확인 — 조사의 첫 동작 |
d.find(b"PK\x03\x04") |
이어붙은 ZIP 위치 찾기 |
ZipFile(io.BytesIO(잘라낸_바이트)) |
카빙한 ZIP 열기 |
re.search(r"-P%20(\w+)", log) |
로그에서 명령행 비밀번호 추출 |
img.getexif().get(태그) |
EXIF 단서 읽기 |
bytes(b ^ key[i % len(key)] ...) |
반복 키 XOR 복호 |
명령어보다 중요한 감각
오늘 다섯 문제의 기법은 전부 이전 챕터에서 배운 것들입니다. 새로운 것은 기법이 아니라 연결이었습니다 — 시그니처가 EXIF로, 로그가 비밀번호로, EXIF가 XOR 키로 이어지는 사슬. 실제 침해 조사도 정확히 이 모양입니다: 단서는 한 곳에 있지 않고, 각 단서가 다음 문을 엽니다.
그리고 이 챕터의 진짜 채점 기준은 플래그가 아니라 사슬도입니다. 플래그는 우연히도 맞힐 수 있지만, "왜 그 기법을 꺼냈는가"를 설명할 수 있어야 재현 가능한 실력입니다. Forensics 트랙의 절반이 지났습니다 — 헥스(239)에서 시작해 메모리(242), 디스크(243), 윈도우(244), 로그(245), 메타데이터(246), 암호 복구(247)까지, 오늘 그 전부가 한 문제 안에서 만났습니다.
전부 체크되면 Step 248 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.