Step 180. CTF 맛보기 5: Forensics/기타 3개 — 수상한 파일의 해부

Step 180. CTF 맛보기 5: Forensics/기타 3개 — 수상한 파일의 해부

Level 3 — CTF 실전과 공격 스킬 심화 | 난이도 ★★★☆☆ | 예상 소요 시간 5시간

전제: Step 176~179(CTF 형식, Crypto 입문). 파일을 바이트로 다루는 파이썬 기초를 압니다.

⚠️ 이 챕터의 실습은 내 랩·합법 플랫폼 전용입니다. 허가 없는 시스템에 적용하면 범죄입니다. 드림핵(dreamhack.io)은 풀라고 만들어진 합법 학습 플랫폼입니다.

  • 준비물: WSL 우분투(file, xxd, strings — 실측: file 5.45), 파이썬 3(실측: 3.12.14).
  • 주의: binwalk, exiftool 등 실전 도구는 이 환경에 없으므로 "출력 예시"로만 소개하고, 핵심 루틴은 직접 만든 세 개의 수상한 파일로 실측합니다.

CTF의 Forensics(포렌식) 종목은 "수상한 파일 하나"를 던져 줍니다. 이미지, 패킷 덤프, 메모리 덤프 — 겉은 멀쩡한데 안에 플래그가 숨어 있습니다. 출발점은 언제나 같은 의심입니다: "겉으로 보이는 것과 파일의 실체가 다르다." 오늘은 포렌식의 기본 루틴을 몸에 붙이고, 세 개의 미니 사건 파일을 직접 해결합니다.


1. 학습 목표

이 챕터를 끝내면 다음을 할 수 있습니다:

  • 매직 바이트(파일 시그니처)로 파일의 실제 종류를 판별한다
  • 포렌식 기본 루틴(filestringsxxd → 추출 도구)을 순서대로 수행한다
  • PNG의 청크 구조(IHDR, IDAT, IEND)를 읽고, IEND 뒤의 숨은 데이터를 추출한다
  • 확장자 위장 파일을 file로 간파한다
  • 증거 파일의 분석 과정을 "루틴 문서"로 정리한다

2. 배경 지식 — 오늘의 도구와 개념

오늘의 도구 한눈에 보기

구분 내용
언어·환경 WSL 우분투 bash + 파이썬 3 (실측: file 5.45, xxd, strings / Python 3.12.14)
오늘의 명령어 file 파일, strings 파일 | grep -i flag, xxd 파일 | head, 파이썬 bytes.find(b"IEND")
필요한 개념 매직 바이트, PNG 청크 구조, 파일 끝 뒤 데이터(append), 확장자 위장
오늘의 산출물 세 사건 파일의 해결 기록 + 나만의 포렌식 루틴 문서

2-1. 매직 바이트 — 파일의 주민등록번호

파일의 진짜 종류는 확장자가 아니라 앞쪽 몇 바이트가 결정합니다. 이 약속된 시작 바이트를 매직 바이트(magic bytes) 또는 시그니처라 합니다.

파일 종류 시작 바이트 (hex) 모양
PNG 89 50 4E 47 0D 0A 1A 0A .PNG....
JPEG FF D8 FF ...
ZIP 50 4B 03 04 PK..
ELF(리눅스 실행 파일) 7F 45 4C 46 .ELF

file 명령은 이 매직 바이트를 읽어 종류를 알려 줍니다. 확장자가 .png인데 file이 "ASCII text"라고 하면 — 그 파일은 위장한 것입니다.

2-2. PNG의 청크 구조 — 상자의 연속

PNG 파일은 시그니처 8바이트 뒤에 청크(chunk)라는 상자들이 이어집니다. 각 청크는 길이 4바이트 + 종류 4바이트 + 데이터 + 검증값 4바이트 구조입니다.

  • IHDR: 그림의 크기·색 정보 (항상 첫 청크)
  • IDAT: 실제 픽셀 데이터
  • IEND: "여기가 끝" 표시

핵심은 이것입니다 — IEND 뒤에 오는 바이트는 PNG 규격상 아무 의미가 없습니다. 그림 뷰어도 무시합니다. 그래서 출제자가 플래그를 숨기는 단골 자리입니다.

2-3. 포렌식 기본 루틴 — 싸고 빠른 것부터

수상한 파일을 받으면, 가벼운 검사부터 순서대로:

1. file 파일              → 진짜 종류 확인 (1초)
2. strings 파일 | grep -i flag  → 보이는 문자열 즉시 검색 (1초)
3. xxd 파일 | head/tail   → 앞뒤 바이트 직접 보기 (시그니처·꼬리 확인)
4. 전문 도구               → binwalk(내장 파일), exiftool(메타데이터), steghide/zsteg(이미지 스테가노그래피)

실전 문제의 상당수는 1~3단계에서 끝납니다. 도구가 안 잡을 때 마지막 수단은 hex 에디터로 파일 앞뒤를 직접 보는 것 — 오늘 우리가 할 일입니다.

2-4. 실전 도구 지도 — 출력 예시

이 환경에는 없지만 실전 표준인 도구들을 출력 예시로 봅니다 (화면 예시 — 실제 실행하지 않았습니다):

# 화면 예시 — binwalk: 파일 안에 들어 있는 파일을 찾는 도구
$ binwalk image.png
DECIMAL   HEXADECIMAL   DESCRIPTION
0         0x0           PNG image
69436     0x10F3C       Zip archive data, at least v2.0 to extract
# 화면 예시 — exiftool: 메타데이터(촬영 정보 등)를 읽는 도구
$ exiftool photo.jpg
Camera Model Name     : iPhone 13
Comment               : DH{m3t4d4t4_1s_4_g1ft}

binwalk는 "PNG인 척하는 파일 안에 ZIP이 통째로 붙어 있는" 문제를, exiftool은 "사진의 메모 칸에 쓰인 플래그"를 잡습니다. pcap 파일은 Wireshark로 열어 Follow TCP Stream — 대화 속 플래그를 읽는 흐름이 표준입니다.


3. 따라 하기

3-1. 사건 파일 생성 — 세 개의 수상한 파일

아래 스크립트로 오늘의 사건 파일 세 개를 만듭니다 (이 챕터의 로컬 출력은 2026-09-09 실측입니다).

입력 (forensics_lab.py)

import struct, zlib

def chunk(typ, data):
    c = struct.pack(">I", len(data)) + typ + data
    return c + struct.pack(">I", zlib.crc32(typ + data) & 0xFFFFFFFF)

def make_png(path):
    sig = b"\x89PNG\r\n\x1a\n"
    ihdr = chunk(b"IHDR", struct.pack(">IIBBBBB", 1, 1, 8, 2, 0, 0, 0))
    idat = chunk(b"IDAT", zlib.compress(b"\x00\xff\x00\x00"))
    iend = chunk(b"IEND", b"")
    with open(path, "wb") as f:
        f.write(sig + ihdr + idat + iend)

make_png("sig.png")          # 사건 0: 정상 PNG (기준)
make_png("hidden.png")       # 사건 1: IEND 뒤에 뭔가 있다
with open("hidden.png", "ab") as f:
    f.write(b"DH{1s_th3r3_s0m3th1ng_4ft3r_iend}")
with open("fake.png", "w") as f:   # 사건 2: 확장자 위장
    f.write("I am not an image. The flag is DH{n0t_4_r34l_png}\n")

출력

(파일 세 개 생성 — sig.png 69바이트, hidden.png 102바이트, fake.png)

3-2. 사건 0: 정상 PNG — 기준선 세우기

먼저 멀쩡한 파일이 어떻게 생겼는지 봐야 수상한 것이 보입니다. WSL에서:

file sig.png
sig.png: PNG image data, 1 x 1, 8-bit/color RGB, non-interlaced
xxd sig.png | head -6
00000000: 8950 4e47 0d0a 1a0a 0000 000d 4948 4452  .PNG........IHDR
00000010: 0000 0001 0000 0001 0802 0000 0090 7753  ..............wS
00000020: de00 0000 0c49 4441 5478 9c63 f8cf c000  .....IDATx.c....
00000030: 0003 0101 00c9 fe92 ef00 0000 0049 454e  .............IEN
00000040: 44ae 4260 82                             D.B`.

(2026-09-09 실측.)

출력 읽는 법: 첫 8바이트 89 50 4e 47 ...가 PNG 시그니처(오른쪽에 .PNG로도 보입니다). 이어서 IHDR(길이 13 = 0000000d), IDAT(픽셀 데이터), 맨 끝 IEND(49 45 4e 44). 정상 PNG는 IEND에서 끝납니다. 이 배치가 오늘의 기준선입니다.

3-3. 사건 1: IEND 뒤의 불청객

hidden.png에 루틴을 적용합니다. file은 조용합니다:

file hidden.png
hidden.png: PNG image data, 1 x 1, 8-bit/color RGB, non-interlaced

주목: 크기가 102바이트인데 정상 PNG(sig.png)는 69바이트였습니다. file은 시그니처만 보고 "PNG"라고 답할 뿐, 뒤에 붙은 33바이트는 신고하지 않습니다. file을 믿되 맹신하지 않는 이유입니다.

꼬리를 직접 봅니다:

xxd hidden.png | tail -3
00000040: 44ae 4260 8244 487b 3173 5f74 6833 7233  D.B`.DH{1s_th3r3
00000050: 5f73 306d 3374 6831 6e67 5f34 6674 3372  _s0m3th1ng_4ft3r
00000060: 5f69 656e 647d                           _iend}

(2026-09-09 실측. `D.B“까지가 IEND와 그 검증값, 그 뒤로 플래그가 그대로 보입니다.)

strings로도 잡힙니다:

strings hidden.png | grep DH
DH{1s_th3r3_s0m3th1ng_4ft3r_iend}

파이썬으로 정식 추출 — IEND의 위치를 계산해 그 뒤를 잘라 냅니다:

data = open("hidden.png", "rb").read()
pos = data.find(b"IEND")
end = pos + 8          # IEND 4바이트 + 검증값 4바이트
print(f"파일 크기: {len(data)}, IEND 위치: {pos}, 정상 끝: {end}")
print("숨은 데이터:", data[end:].decode())
파일 크기: 102, IEND 위치: 61, 정상 끝: 69
숨은 데이터: DH{1s_th3r3_s0m3th1ng_4ft3r_iend}

출력 읽는 법: "정상 끝(69) < 파일 크기(102)" — 이 부등식 하나가 위장을 폭로합니다. 이 계산이 binwalk가 하는 일의 축소판입니다.

3-4. 사건 2: 확장자 위장

fake.png는 이름만 PNG입니다. 루틴 1단계로 즉시 끝납니다:

file fake.png
fake.png: ASCII text, with CRLF line terminators
strings fake.png | grep -i flag
I am not an image. The flag is DH{n0t_4_r34l_png}

(2026-09-09 실측.)

읽는 법: 매직 바이트가 PNG의 것이 아니므로 file이 즉시 "ASCII text"를 선고했습니다. 확장자는 사용자 인터페이스의 장식일 뿐, 파일의 신원은 앞쪽 바이트가 증명합니다.

3-5. 실제 플랫폼에서의 모습 — 화면 예시

드림핵 Forensics 카테고리에서의 흐름입니다 (화면 예시 — 이 환경에서는 접속하지 않았습니다):

# 화면 예시 — 플랫폼 문제 페이지
[Forensics] hidden-in-image   난이도: 1
첨부: evidence.zip  ← 수상한 파일을 받는다

$ file evidence.png
$ strings evidence.png | grep -i "DH{"
$ binwalk -e evidence.png     # 내장 파일 추출
$ steghide extract -sf photo.jpg   # JPG 스테가노그래피 (비밀번호 없이 Enter)

오늘 익힌 루틴 — file부터 시작해 꼬리를 직접 보기까지 — 이 그대로 첫 두 수가 됩니다. 도구가 바뀔 뿐, 의심의 순서는 같습니다.


4. 미션과 연습문제

미션 — 사건 파일 하나를 직접 만들고 루틴 문서 쓰기

  1. 여러분만의 플래그(DH{...})를 숨긴 사건 파일을 하나 만든다 — 방법은 셋 중 하나: ① IEND 뒤 추가, ② 확장자 위장, ③ strings로 잡히지 않는 곳(청크 데이터 속)에 삽입
  2. 동료 역할이 되어, 자신의 파일에 2-3의 루틴을 처음부터 적용하며 각 단계의 명령과 결과를 기록한다
  3. 그 기록을 "나의 포렌식 루틴 문서"로 정리한다 — 명령, 기대 결과, 안 되면 다음 수

연습문제

문제 1. xxd로 어떤 파일을 열었더니 시작이 50 4B 03 04였습니다. 이 파일의 진짜 종류는? 확장자가 .jpg라면 무엇을 의심해야 하나요?

문제 2. file이 "PNG image data"라고 답한 파일에서도 플래그가 숨어 있을 수 있습니다. 그 이유를 file이 검사하는 범위로 설명하세요.

문제 3. PNG 파일의 크기가 500바이트인데, IEND 청크가 끝나는 위치가 450바이트입니다. 무엇을 의심하고 어떤 파이썬 코드로 확인하나요?

문제 4. strings가 하는 일을 설명하고, 왜 플래그가 strings에 안 잡히는 문제가 더 어려운 등급인지 추측해 보세요.


5. 모범 답안과 완료 기준

미션 모범 답안

루틴 문서의 예 (형식은 자유):

[나의 포렌식 루틴 v1]
1. file {파일}           → 종류 확인. 확장자와 다르면 위장 의심
2. ls -l {파일}          → 크기 기록 (같은 종류의 정상 파일과 비교)
3. strings | grep -iE "flag|DH\{"  → 즉답 가능성 검사
4. xxd | head            → 시그니처 확인
5. xxd | tail            → 꼬리 확인 (정상 종료 위치 뒤의 데이터)
6. 안 나오면: 파이썬으로 청크/구조 단위 분석 → 전문 도구(binwalk 등) 고려

검증하는 법: ① 만든 사건 파일을 루틴 문서만 보고 다시 풀 수 있는가(문서의 완결성). ② ③번 방법(청크 데이터 속)을 택했다면 strings에 안 잡히는 이유 — 데이터가 압축됐거나 바이트가 깨져서 — 를 설명할 수 있는가. IDAT 안에 글자를 넣으면 zlib 압축이 문자열을 갈아 버려 strings가 못 잡습니다.

연습문제 해답

문제 1 해답. 50 4B 03 04(PK..)는 ZIP의 시그니처입니다. 확장자가 .jpg라면 ZIP을 JPG로 위장한 파일 — 이름을 바꿔 압축을 풀거나(unzip), binwalk로 내용물을 추출해야 합니다. CTF의 가장 흔한 첫 단추입니다.

문제 2 해답. file은 파일 앞쪽의 시그니처를 중심으로 판별하고, 전체 내용을 검증하지 않습니다. 그래서 시그니처가 올바르면 뒤에 무엇이 붙어 있든 "PNG"라고 답합니다 — 사건 1에서 크기 102바이트(정상 69바이트)인데도 조용했던 이유입니다.

문제 3 해답. IEND 뒤 50바이트의 불청객을 의심합니다. 확인 코드:

data = open("mystery.png", "rb").read()
pos = data.find(b"IEND") + 8
print(len(data), pos)      # 500 vs 450 — 차이가 증거
print(data[pos:])          # 숨은 데이터 출력

문제 4 해답. strings는 파일에서 "출력 가능한 문자가 4개 이상 연속된" 구간을 뽑아 줍니다. 플래그가 압축·암호화·XOR된 채 숨어 있으면 출력 가능 문자열이 아니라서 안 잡힙니다. 그래서 그런 문제는 한 단계의 복호화가 더 필요한, 더 어려운 등급이 됩니다.

완료 기준 체크리스트

  • [ ] PNG·JPEG·ZIP·ELF의 매직 바이트를 표로 외웠다
  • [ ] filestringsxxd 루틴을 순서대로 수행할 수 있다
  • [ ] 정상 PNG의 청크 배치(IHDR → IDAT → IEND)를 설명할 수 있다
  • [ ] IEND 뒤 숨은 데이터를 파이썬으로 추출했다
  • [ ] 확장자 위장 파일을 file로 간파했다
  • [ ] binwalk/exiftool/steghide가 각각 무엇을 찾는 도구인지 안다
  • [ ] 미션: 사건 파일을 만들고 루틴 문서를 완성했다

6. 흔한 실수와 해결

벽 1. binwalk: command not found

증상: bash: line 1: binwalk: command not found (2026-09-09 실측 — 이 환경에는 binwalk가 없습니다).
원인: 도구가 설치되지 않았습니다. 포렌식 도구는 종류가 많아 처음부터 다 깔 수 없습니다.
해결: 오늘의 루틴(file, strings, xxd, 파이썬)은 전부 기본 설치로 됩니다. binwalk가 정말 필요한 순간(내장 파일 추출)이 오면 그때 sudo apt install binwalk를 고려하세요 — 도구부터 까는 것이 아니라, 문제가 요구할 때 하나씩 추가하는 것이 포렌식 장비 관리의 정석입니다.

벽 2. file만 믿고 넘어갔다가 플래그를 놓친다

증상: file이 "PNG"라고 해서 정상이라 판단했는데 정답은 뒤에 있었습니다.
원인: file은 시그니처 중심 판별이라 꼬리의 추가 데이터를 신고하지 않습니다 (사건 1 실측).
해결: file 다음에는 반드시 크기 비교와 xxd | tail을 붙이세요. 루틴은 한 단계가 아니라 사슬입니다.

벽 3. xxd 출력에서 글자를 못 찾겠다

증상: 16진수만 보이고 무슨 글자인지 모르겠습니다.
원인: xxd의 맨 오른쪽 열이 ASCII 번역입니다. 거기를 안 봤을 뿐입니다.
해결: 오른쪽 열만 훑으세요 — .은 출력 불가 바이트, 글자는 그대로 보입니다. 사건 1의 DH{1s_th3r3...이 그 열에서 발견됐습니다.

벽 4. 파이썬 data.find(b"IEND")가 -1을 돌려준다

증상: IEND를 찾았는데 -1(없음)이 나옵니다.
원인: 그 파일은 PNG가 아니거나, 텍스트 모드("r")로 열어 바이트가 변형됐습니다.
해결: 반드시 바이너리 모드로 여세요 — open(path, "rb"). 텍스트 모드는 줄바꿈을 바꿔 버려 바이트 위치 계산이 틀어집니다. 그리고 find 결과가 -1이면 시그니처부터 다시 의심하세요.

벽 5. 추출했는데 깨진 문자가 나온다

증상: IEND 뒤를 잘랐는데 읽을 수 없는 바이트입니다.
원인: 숨은 데이터가 평문이 아니라 인코딩·XOR된 것입니다 — Crypto와 혼합된 문제입니다.
해결: Step 179의 루틴을 이어서 적용하세요. 포렌식으로 "꺼내고", Crypto로 "읽는" 2단 문제는 실전에서 흔합니다.


7. 정리

오늘의 개념

개념 한 줄 설명
매직 바이트 파일 앞쪽의 종류 표시 바이트 — 신원은 확장자가 아니라 이것이 증명
PNG 청크 길이+종류+데이터+검증값의 상자 연속 — IHDR, IDAT, IEND
IEND 뒤 데이터 규격상 무의미한 영역 — 플래그 은닉의 단골 자리
포렌식 루틴 싸고 빠른 검사부터: file → strings → xxd → 전문 도구
위장 파일 시그니처와 실제 내용이 다른 파일 — file이 1차 심문관
스테가노그래피 데이터를 이미지 등에 숨기는 기술 — steghide, zsteg가 탐지

오늘의 명령어

명령 하는 일
file 파일 매직 바이트로 진짜 종류 판별
strings 파일 | grep -i flag 보이는 문자열 즉시 검색
xxd 파일 | head / | tail 앞(시그니처)·뒤(꼬리) 직접 보기
data.find(b"IEND") + 8 PNG 정상 끝 위치 계산
binwalk -e 파일 (예시) 내장 파일 추출
exiftool 파일 (예시) 메타데이터 읽기

명령어보다 중요한 감각

포렌식의 무기는 도구 목록이 아니라 의심의 순서입니다. "겉과 실체가 다르다"로 시작해, 싼 검사부터 비싼 검사로. 그리고 도구가 침묵할 때(file이 "정상"이라 할 때) 침묵을 증거로 착각하지 않는 것 — 침묵은 "시그니처는 정상"이라는 뜻일 뿐입니다.

이 눈은 CTF를 넘어 실제 침해 사고 분석의 첫 동작과 같습니다. 로그인 기록, 다운로드된 첨부 파일 — 전부 "수상한 파일 하나"로 시작합니다. Step 181에서 분야를 고를 때, 오늘처럼 "증거를 뒤지는 과정"이 즐거웠다면 그것도 중요한 신호입니다.


전부 체크되면 Step 180 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.