Step 239. 헥스와 파일 시그니처: file, binwalk, 수동 치빙 — 바이트 여섯 개의 신원 조회

Step 239. 헥스와 파일 시그니처: file, binwalk, 수동 치빙 — 바이트 여섯 개의 신원 조회

Level 3 — Forensics 트랙 | 난이도 ★★☆☆☆ | 예상 소요 시간 3시간

전제: Step 180(CTF 맛보기 Forensics)을 마쳤다. file, xxd, strings의 기본 사용과 PNG 청크 구조를 안다.

⚠️ 이 챕터의 실습은 내 랩·합법 플랫폼 전용입니다. 허가 없는 시스템에 적용하면 범죄입니다.

  • 준비물: WSL 우분투(file 5.45, xxd, strings — 실측 확인), 파이썬 3(실측: 3.12.14).
  • 주의: binwalk는 이 환경에 없어 "출력 예시"로만 소개합니다. 나머지 — 판별기 작성, 깨진 헤더 복구, 수동 치빙 — 은 전부 2026-09-09 로컬 실측입니다.

Forensics 트랙의 첫 챕터입니다. 포렌식 분석가가 증거 파일을 받아 제일 먼저 하는 일은 "네가 정말 너니?"를 묻는 것입니다. 파일의 신원을 증명하는 것은 확장자가 아니라 앞쪽 몇 바이트 — 매직 바이트입니다. 오늘은 이 시그니처로 판별기를 직접 만들고, 깨진 헤더를 손으로 고치고, 쓰레기 더미 속에서 파일을 뜯어내는 치빙(carving)까지 해 봅니다. 이 세 가지가 파일 포렌식의 기본 체력입니다.


1. 학습 목표

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

  • 주요 포맷 10종의 매직 바이트를 보고 파일 종류를 말한다
  • 파이썬으로 파일 시그니처 판별기를 직접 작성한다
  • 시그니처가 파괴된 파일의 헤더를 수동으로 복구한다
  • 날것의 바이트 덩어리에서 시그니처~푸터 구간을 잘라 파일을 살린다(치빙)
  • binwalk가 하는 일을 수동 치빙과 대조해 설명한다

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

오늘의 도구 한눈에 보기

구분 내용
언어·환경 WSL 우분투 bash + 파이썬 3 (실측: file 5.45, xxd / Python 3.12.14)
오늘의 명령어 file 파일, xxd 파일, xxd -s 오프셋 -l 길이 파일, 파이썬 bytes.find()
필요한 개념 매직 바이트, 파일 헤더·푸터, ZIP 계열의 동족 문제, 데이터 치빙
오늘의 산출물 시그니처 표 + 파이썬 판별기 + 복구된 PNG 2개(헤더 수리, 치빙)

2-1. 매직 바이트 — 파일의 앞마당

대부분의 파일 포맷은 "나는 이런 파일이다"를 약속된 시작 바이트로 알립니다. 이것이 매직 바이트(magic bytes) 또는 파일 시그니처(file signature)입니다. file 명령은 이 바이트들과 그 뒤의 구조를 읽어 종류를 판별합니다.

핵심 원칙은 두 가지입니다. 첫째, 확장자는 장식입니다. 운영체제가 아이콘을 고를 때 쓰는 꼬리표일 뿐 파일 내용과 무관합니다. 둘째, 시그니처는 파괴 가능합니다. 앞 바이트 몇 개가 깨지면 도구는 그 파일을 "data"(정체불명)로 분류합니다 — 그리고 그 깨진 바이트를 손으로 고치면 파일이 살아납니다.

2-2. 헤더와 푸터 — 파일의 시작과 끝 표지

시그니처가 "시작 표지"라면, 많은 포맷은 "끝 표지"도 가집니다. PNG는 IEND 청크로, JPEG는 FF D9(EOI)로 끝납니다. ZIP은 파일 끝부분에 중앙 디렉터리(50 4B 05 06으로 시작)를 둡니다.

시작 표지와 끝 표지가 둘 다 있으면, 두 표지 사이를 잘라 내면 온전한 파일 하나를 얻을 수 있습니다. 이것이 치빙(carving) — 바이트 덩어리에서 조각상을 깎아내듯 파일을 뜯어내는 기법 — 의 원리입니다. 포렌식 도구(PhotoRec, foremost)와 binwalk의 추출 기능이 하는 일이 정확히 이것입니다.

2-3. ZIP 계열의 동족 문제

50 4B 03 04(PK..)를 보면 ZIP이라고 답하고 싶지만, docx, xlsx, jar, apk, odt는 전부 ZIP 컨테이너입니다. 시그니처만으로는 이 동족들을 구분할 수 없습니다. 안에 무엇이 들었는지([Content_Types].xml이 있으면 docx, AndroidManifest.xml이 있으면 apk)를 봐야 합니다. 오늘 실측에서 이 모습을 직접 확인합니다.

2-4. binwalk — 치빙의 자동화 (출력 예시)

binwalk는 파일 전체를 훑어 알려진 시그니처가 나타나는 모든 위치를 보고합니다. 이 환경에는 설치돼 있지 않으므로 출력 예시로만 봅니다 (실제 실행하지 않았습니다):

# 출력 예시 — binwalk로 내장 파일 탐지
$ binwalk firmware.bin
DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             PNG image
41472         0xA200          gzip compressed data
102400        0x19000         Zip archive data, at least v2.0 to extract
# 출력 예시 — 추출
$ binwalk -e firmware.bin        # 찾은 파일들을 _firmware.bin.extracted/ 에 풀어 줌
$ binwalk --dd='.*' firmware.bin # 시그니처 단위로 전부 잘라 파일로 저장

오늘 우리가 파이썬으로 할 수동 치빙이 이 binwalk -e의 원리 그 자체입니다. 도구가 없어도 원리를 아는 사람은 손으로 해낼 수 있습니다.


3. 따라 하기

3-1. 시그니처 실습장 만들기

아래 스크립트로 9종 포맷의 샘플과 위장·손상·매장 파일을 만듭니다 (이 챕터의 로컬 출력은 2026-09-09 실측입니다).

입력 (sig_lab.py)

import struct, zlib, zipfile, gzip, os, sqlite3

os.makedirs("siglab", exist_ok=True); os.chdir("siglab")

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", 2, 2, 8, 2, 0, 0, 0))
    raw  = b"\x00" + b"\xff\x00\x00" * 2 + b"\x00" + b"\x00\xff\x00" * 2
    idat = chunk(b"IDAT", zlib.compress(raw))
    open(path, "wb").write(sig + ihdr + idat + chunk(b"IEND", b""))

make_png("sample.png")
open("sample.jpg", "wb").write(bytes.fromhex("FFD8FFE000104A46494600010100000100010000") + b"\x00"*32 + b"\xff\xd9")
with zipfile.ZipFile("sample.zip", "w") as z: z.writestr("hello.txt", "PK\n")
open("sample.gif", "wb").write(b"GIF89a" + b"\x00" * 40)
open("sample.pdf", "wb").write(b"%PDF-1.4\n%%EOF\n")
with gzip.open("sample.gz", "wb") as f: f.write(b"gzip sample\n")
open("sample.bmp", "wb").write(b"BM" + b"\x00" * 60)
con = sqlite3.connect("sample.db"); con.execute("create table t(a)"); con.close()
with open("sample.docx", "wb") as f:
    with zipfile.ZipFile(f, "w") as z: z.writestr("[Content_Types].xml", "<Types/>")

make_png("disguise.txt")           # 위장: PNG 내용, txt 확장자
make_png("broken.png")
d = bytearray(open("broken.png", "rb").read())
d[1:4] = b"\x00\x00\x00"           # 손상: 'PNG' 세 글자 파괴
open("broken.png", "wb").write(bytes(d))

inner = open("sample.png", "rb").read()   # 매장: 쓰레기 사이에 PNG
open("fragment.bin", "wb").write(os.urandom(300) + inner + os.urandom(180))

출력

(파일 12개 생성 — sample 9종, disguise.txt, broken.png, fragment.bin)

3-2. 시그니처 10종 표 직접 완성하기

각 파일의 앞 8바이트를 뜯어 봅니다:

cd siglab
for f in sample.*; do printf '%-12s ' "$f"; xxd -l 8 -p "$f" | tr -d '\n'; echo; done
sample.bmp   424d000000000000
sample.db    53514c6974652066
sample.docx  504b030414000000
sample.gif   4749463839610100
sample.gz    1f8b08080c34a16a
sample.jpg   ffd8ffe000104a46
sample.pdf   255044462d312e34
sample.png   89504e470d0a1a0a
sample.zip   504b030414000000

(2026-09-09 실측. ELF는 /bin/ls, PE는 윈도우 DLL에서 각각 7f 45 4c 46(.ELF)와 4d 5a(MZ)로 확인했습니다.)

이 결과를 표로 정리하면 오늘의 첫 산출물입니다:

포맷 앞 바이트 (hex) 눈에 보이는 모양 푸터
PNG 89 50 4E 47 0D 0A 1A 0A .PNG.... IEND 청크 + CRC
JPEG FF D8 FF FF D9
ZIP (zip/docx/jar/apk) 50 4B 03 04 PK.. 중앙 디렉터리 50 4B 05 06
GIF 47 49 46 38 GIF8 3B
PDF 25 50 44 46 2D %PDF- %%EOF
gzip 1F 8B 08 원본 CRC32+크기
BMP 42 4D BM 없음
SQLite 53 51 4C 69 74 65 20 66 SQLite f
ELF 7F 45 4C 46 .ELF
PE (exe/dll) 4D 5A MZ

읽는 법: 89 50 4E 4750 4E 47이 ASCII로 PNG입니다. 시그니처는 알아보기 쉬운 글자를 섞어 설계된 경우가 많습니다 — MZ는 MS-DOS 시대 개발자 이니셜(Mark Zbikowski)이라는 일화가 있을 정도입니다.

3-3. file과의 대조 — 그리고 한 가지 반전

file로 같은 파일들을 판별해 봅니다:

file sample.png sample.jpg sample.zip sample.gif sample.pdf sample.gz sample.bmp sample.db sample.docx
sample.png:  PNG image data, 2 x 2, 8-bit/color RGB, non-interlaced
sample.jpg:  JPEG image data, JFIF standard 1.01, aspect ratio, density 1x1, segment length 16
sample.zip:  Zip archive data, at least v2.0 to extract, compression method=store
sample.gif:  GIF image data, version 89a, 1 x 1
sample.pdf:  PDF document, version 1.4
sample.gz:   gzip compressed data, was "sample", ..., original size modulo 2^32 17
sample.bmp:  ASCII text, with no line terminators
sample.db:   SQLite 3.x database, ...
sample.docx: Zip archive data, at least v2.0 to extract, compression method=store

(2026-09-09 실측.)

여기서 반전 두 개.sample.bmp가 "BMP"가 아니라 "ASCII text"로 판정됐습니다. BMP 시그니처는 BM 단 두 바이트라 너무 짧아서, file은 헤더 뒤의 구조(크기 필드 등)까지 확인합니다 — 우리의 최소 샘플은 그 구조가 비어 있어 통과하지 못했습니다. 시그니처가 짧을수록 오탐·미탐의 여지가 커집니다.sample.docx가 "Zip archive"로 판정됐습니다. 2-3의 동족 문제가 실물로 확인된 것입니다 — docx의 진짜 정체는 ZIP이고, 구분은 내부 파일 목록으로 합니다.

3-4. 파이썬 판별기 작성 — file의 축소판

시그니처 표를 코드로 옮깁니다:

SIGS = {
    b"\x89PNG\r\n\x1a\n": "PNG 이미지",
    b"\xff\xd8\xff":      "JPEG 이미지",
    b"PK\x03\x04":        "ZIP 계열 (zip/docx/jar/apk...)",
    b"GIF8":              "GIF 이미지",
    b"%PDF":              "PDF 문서",
    b"\x1f\x8b":          "gzip 압축",
    b"BM":                "BMP 이미지",
    b"SQLite format 3":   "SQLite 데이터베이스",
    b"\x7fELF":           "ELF 실행 파일 (리눅스)",
    b"MZ":                "PE 실행 파일 (윈도우)",
}

def identify(path):
    head = open(path, "rb").read(16)
    for sig, name in sorted(SIGS.items(), key=lambda kv: -len(kv[0])):
        if head.startswith(sig):
            return name
    return "판별 불가 (data)"

for f in ["sample.png", "sample.docx", "disguise.txt", "broken.png"]:
    print(f"{f:15s} -> {identify(f)}")
sample.png      -> PNG 이미지
sample.docx     -> ZIP 계열 (zip/docx/jar/apk...)
disguise.txt    -> PNG 이미지
broken.png      -> 판별 불가 (data)

(2026-09-09 실측.)

출력 읽는 법: 세 가지가 입증됐습니다. ① 확장자가 .txtdisguise.txt가 PNG로 판별됨 — 신원은 바이트가 말합니다. ② broken.png가 "판별 불가" — 시그니처 세 글자 파괴가 신원을 지웠습니다. ③ sorted(..., key=-len)으로 긴 시그니처부터 검사하는 이유 — SQLite format 3(16바이트)보다 짧은 패턴이 먼저 걸리는 사고를 막기 위해서입니다. 실제 file도 수천 개의 매직 패턴을 우선순위로 정렬해 검사합니다. 우리는 그 장부의 10줄짜리 축소판을 만든 것입니다.

file 명령의 판정도 확인해 두세요 — file disguise.txtPNG image data, 2 x 2, 8-bit/color RGB로, file broken.pngdata로 나옵니다 (2026-09-09 실측).

3-5. 깨진 헤더 수동 복구

broken.png의 앞을 봅니다:

xxd -l 16 broken.png
00000000: 8900 0000 0d0a 1a0a 0000 000d 4948 4452  ............IHDR

(2026-09-09 실측.)

읽는 법: 89 뒤에 와야 할 50 4E 47(PNG)이 00 00 00으로 지워졌습니다. 하지만 8번째 바이트부터 IHDR이 멀쩡히 보입니다 — 몸통은 살아 있고 신분증 앞면이 찢긴 상태입니다. 수리법은 단순합니다. 정상 시그니처를 덮어씁니다:

data = bytearray(open("broken.png", "rb").read())
print("복구 전:", data[:8].hex(" "))   # 89 00 00 00 0d 0a 1a 0a
data[1:4] = b"PNG"
open("fixed.png", "wb").write(bytes(data))
print("복구 후:", bytes(data[:8]).hex(" "))  # 89 50 4e 47 0d 0a 1a 0a
file fixed.png
fixed.png:  PNG image data, 2 x 2, 8-bit/color RGB, non-interlaced

(2026-09-09 실측. 세 바이트 수리로 파일이 부활했습니다.)

왜 이게 되는가: 파일 포맷은 구조가 문서화된 약속입니다. "몇 번째 바이트에 무엇이 와야 하는지"가 정해져 있으므로, 깨진 자리에 정답을 써 넣으면 됩니다. CTF의 "그림이 안 열려요" 문제 대부분이 이 세 바이트짜리 수리입니다.

3-6. 수동 치빙 — 쓰레기 더미에서 PNG 뜯어내기

fragment.bin(554바이트)은 난수 300바이트 + PNG + 난수 180바이트입니다. 디스크의 미할당 영역을 흉내 낸 것입니다. 치빙 절차는 시그니처를 찾고, 푸터를 찾고, 그 사이를 잘라 내는 것:

blob = open("fragment.bin", "rb").read()
start = blob.find(b"\x89PNG\r\n\x1a\n")
end   = blob.find(b"IEND", start) + 8      # 'IEND' 4바이트 + CRC 4바이트
print(f"조각 크기: {len(blob)}, PNG 시작: {start}, 끝: {end}")
open("carved.png", "wb").write(blob[start:end])
조각 크기: 554, PNG 시작: 300, 끝: 374

(2026-09-09 실측. 시작 오프셋 300은 우리가 심어 둔 위치와 정확히 일치합니다.)

추출물이 진짜 온전한지는 체크까지 해야 복구 완료입니다. PNG 청크의 CRC를 직접 다시 계산해 봅니다:

import struct, zlib
carved = open("carved.png", "rb").read()
pos = 8
while pos < len(carved):
    length, = struct.unpack(">I", carved[pos:pos+4])
    typ = carved[pos+4:pos+8]
    crc_stored, = struct.unpack(">I", carved[pos+8+length:pos+12+length])
    crc_calc = zlib.crc32(typ + carved[pos+8:pos+8+length]) & 0xFFFFFFFF
    print(f"청크 {typ.decode():4s} 길이 {length:2d}  CRC {'OK' if crc_stored==crc_calc else 'CORRUPT'}")
    pos += 12 + length
청크 IHDR 길이 13  CRC OK
청크 IDAT 길이 17  CRC OK
청크 IEND 길이  0  CRC OK

(2026-09-09 실측. file carved.png도 정상 PNG로 판정했습니다.)

출력 읽는 법: 세 청크 전부 CRC가 일치 — 쓰레기 속에서 뜯어낸 74바이트가 바이트 단위로 완전한 PNG라는 수학적 증명입니다. binwalk -e나 PhotoRec이 보여 주는 "복구됨" 목록의 뒤에서 벌어지는 일이 이것입니다. 푸터가 없는 포맷(예: BMP, 일부 스트림)은 다음 시그니처 위치나 헤더의 길이 필드로 끝을 추정해야 해서, 치빙이 한 단계 어려워집니다.


4. 미션과 연습문제

미션 — 시그니처 장부와 치빙 도구 완성

  1. 3-2의 표에 3종을 추가한다 — RAR(Rar!), 7z, ELF의 앞 바이트를 직접 실험 또는 공식 문서로 확인해 표에 적는다
  2. identify()를 확장해, 파일 안에 두 번째 시그니처가 더 있는지까지 찾는 deep_scan(path)를 작성한다 (힌트: find를 모든 시그니처·모든 위치에 대해 반복)
  3. JPEG(FF D8 FF ~ FF D9)가 매장된 조각 파일을 직접 만들고, 치빙 스크립트로 추출해 file 판정까지 확인한다

연습문제

문제 1. 어떤 파일의 앞 4바이트가 50 4B 03 04이고 확장자는 .apk입니다. file의 판정은 무엇일까요? 그리고 이 파일이 진짜 안드로이드 앱인지 확신하려면 무엇을 더 봐야 하나요?

문제 2. 3-3에서 file이 최소 BMP 샘플을 "ASCII text"로 판정했습니다. 이 사례가 말해 주는 "시그니처 길이와 판별 신뢰도"의 관계를 설명하세요.

문제 3. 치빙에서 end = blob.find(b"IEND", start) + 8+ 8은 왜 필요한가요? + 4만 하면 어떤 일이 생기나요?

문제 4. 깨진 PNG를 복구할 때 시그니처만 고치면 되는 경우와 그렇지 않은 경우를 구분해 보세요. 어떤 손상은 시그니처 수리로도 파일이 안 열릴까요?


5. 모범 답안과 완료 기준

미션 모범 답안

① 시그니처 표 추가분 (공식 문서 기준; 7z는 37 7A BC AF 27 1C):

포맷 앞 바이트 (hex) 모양
RAR 4.x 52 61 72 21 1A 07 00 Rar!...
7z 37 7A BC AF 27 1C 7z..'

② deep_scan 예:

def deep_scan(path):
    blob = open(path, "rb").read()
    for sig, name in SIGS.items():
        pos = blob.find(sig)
        while pos != -1:
            print(f"  오프셋 {pos:6d}: {name}")
            pos = blob.find(sig, pos + 1)

fragment.bin에 적용하면 오프셋 300: PNG 이미지가 뜹니다 — 0번이 아닌 위치의 시그니처가 "파일 안의 파일"의 증거입니다.

③ JPEG 치빙: find(b"\xff\xd8\xff")find(b"\xff\xd9", start) + 2로 잘라 저장합니다. JPEG는 푸터가 2바이트라 + 2입니다. 추출물이 file에서 "JPEG image data"로 판정되면 성공. 다만 JPEG 내부 데이터에 우연히 FF D9가 등장할 수 있어 실전에서는 여러 후보를 잘라 보는 경우가 있습니다.

검증하는 법: ① 표의 바이트가 실제 파일과 일치하는가. ② deep_scan이 0이 아닌 오프셋도 보고하는가. ③ JPEG 추출물의 file 판정이 JPEG인가.

연습문제 해답

문제 1 해답. file은 "Zip archive data"로 판정합니다 (3-3에서 docx가 그렇게 판정된 것과 같습니다). 진짜 apk인지는 내부를 봐야 합니다 — unzip -lAndroidManifest.xmlclasses.dex가 있는지 확인합니다. 시그니처는 컨테이너 종류까지만 말해 줍니다.

문제 2 해답. 시그니처가 짧을수록 우연히 일치할 확률이 높아지고, 도구는 보조 검증(후속 구조 필드 확인)에 의존하게 됩니다. BM 두 바이트만으로는 BMP라 확신할 수 없어 file이 크기 필드까지 봤고, 우리 샘플은 그것이 비어 있어 미탐됐습니다. 반면 PNG의 8바이트 시그니처는 우연 일치가 사실상 불가능해 시그니처만으로 강한 증거가 됩니다.

문제 3 해답. find는 패턴이 시작되는 위치를 돌려줍니다. IEND 청크는 길이 4 + 종류 4('IEND') + 데이터 0 + CRC 4 구조라, IEND 글자가 시작된 뒤로도 CRC 4바이트가 더 있고, find 위치 자체도 종류 필드의 시작이므로 글자 4바이트를 포함해야 합니다. + 4만 하면 IEND 글자까지만 잘려 마지막 4바이트(CRC)가 날아가고, 엄격한 뷰어와 우리의 CRC 검증이 실패합니다.

문제 4 해답. 시그니처 수리로 되는 경우: 앞 바이트 몇 개만 깨지고 몸통(IHDR, IDAT)이 온전한 경우. 안 되는 경우: IDAT 데이터가 깨졌거나 CRC가 틀어졌거나(3-6의 검증에서 CORRUPT로 드러남), IHDR의 너비·높이 필드가 파괴된 경우. 포맷 문서를 보며 필드별로 정상값을 추정해 써 넣는 2차 수리가 필요하고, 이것이 hex 에디터를 쓰는 진짜 이유입니다.

완료 기준 체크리스트

  • [ ] 시그니처 표 10종을 보고 파일 종류를 즉답할 수 있다
  • [ ] 파이썬 identify() 판별기를 작성하고 위장 파일을 간파했다
  • [ ] ZIP 계열 동족(docx/jar/apk) 구분이 내부 목록 확인임을 안다
  • [ ] 깨진 PNG의 시그니처를 수리해 file 판정을 되돌렸다
  • [ ] 시그니처~푸터 치빙으로 PNG를 추출하고 CRC 검증까지 했다
  • [ ] binwalk의 -e/--dd가 수동 치빙의 자동화임을 설명할 수 있다
  • [ ] 미션: 시그니처 3종 추가 + deep_scan + JPEG 치빙을 완료했다

6. 흔한 실수와 해결

벽 1. binwalk: command not found

증상: bash: binwalk: command not found (2026-09-09 실측 — 이 환경에는 없습니다).
원인: 도구 미설치.
해결: 오늘 실습은 파이썬 find로 전부 됩니다 — binwalk의 핵심 동작 자체가 시그니처 스캔이라서입니다. 내장 파일 추출이 정말 필요해지면 그때 sudo apt install binwalk를 고려하세요.

벽 2. find가 -1을 돌려준다

증상: 시그니처를 찾았는데 위치가 -1입니다.
원인 셋 중 하나: ① 파일을 텍스트 모드("r")로 열어 바이트가 변형됨, ② 시그니처 값을 오기(예: 89 50\x89\x50이 아니라 문자열 "8950"으로 씀), ③ 진짜로 그 포맷이 아님.
해결: open(path, "rb")인지부터 확인하고, 시그니처를 b"\x89PNG\r\n\x1a\n" 형태의 바이트 리터럴로 썼는지 확인하세요. 그래도 -1이면 xxd 파일 | head로 실제 앞 바이트를 봅니다.

벽 3. 치빙했는데 파일이 안 열린다

증상: 추출은 됐는데 뷰어가 못 엽니다.
원인: 대부분 끝을 잘못 잘랐습니다 — + 8을 빼먹거나, 푸터 패턴이 데이터 중간에 먼저 나타난 경우입니다.
해결: 3-6의 CRC 검증 루틴으로 어디가 깨졌는지 특정하세요. PNG라면 청크 단위 검증이 손상 지점을 정확히 가리켜 줍니다.

벽 4. file이 "data"라고만 한다

증상: broken.png: data (2026-09-09 실측).
원인: 시그니처가 파괴되어 매직 데이터베이스 어디에도 안 걸린 것입니다. "정체 불명"이지 "쓸모없음"이 아닙니다.
해결: xxd | head로 앞을 보고, 어느 포맷의 잔해인지 추정한 뒤 3-5처럼 헤더를 수리합니다. 데이터 중간의 IHDR 같은 구조 표지가 단서입니다.

벽 5. zip인 줄 알고 풀었는데 깨진다

증상: unzip이 에러를 냅니다.
원인: ZIP 시그니처(PK..)는 있지만 파일 끝의 중앙 디렉터리가 깨지거나 잘린 경우입니다. ZIP은 시작보다 에 핵심 목록이 있습니다.
해결: xxd 파일 | tail로 끝부분에 50 4B 05 06(중앙 디렉터리 끝)이 있는지 확인하세요. 잘렸다면 그 시점까지의 파일이 원본이 아닐 가능성이 큽니다.


7. 정리

오늘의 개념

개념 한 줄 설명
매직 바이트 파일 앞쪽의 신원 표시 — 확장자가 아니라 이것이 증거
헤더·푸터 시작·끝 표지 — 둘 사이를 자르면 파일 하나
치빙 바이트 덩어리에서 시그니처~푸터 구간을 뜯어내는 복구 기법
ZIP 계열 동족 docx·jar·apk는 전부 ZIP — 구분은 내부 파일 목록으로
시그니처 길이와 신뢰도 짧을수록 오탐·미탐 — 긴 시그니처부터 검사가 원칙
CRC 검증 추출물이 온전한지 수학으로 확인하는 절차

오늘의 명령어·코드

명령·코드 하는 일
file 파일 매직 바이트 DB로 종류 판별
xxd -l 8 -p 파일 앞 8바이트를 hex로 출력
xxd -s 오프셋 -l 길이 파일 파일 중간의 원하는 구간 보기
blob.find(시그니처) 치빙의 시작점 찾기
zlib.crc32(종류+데이터) PNG 청크 무결성 검증
binwalk -e 파일 (출력 예시) 내장 파일 자동 추출

명령어보다 중요한 감각

오늘 만든 40줄짜리 판별기와 치빙 스크립트는 file과 binwalk, PhotoRec의 심장부와 같은 논리입니다. 도구를 "설치해서 쓰는 것"과 "원리를 알아서 손으로 할 수 있는 것"은 실력의 단계가 다릅니다 — 도구가 침묵하거나 오판할 때(오늘의 BMP처럼) 차이가 드러납니다.

그리고 하나 더. 오늘 여러분은 파일을 깨뜨리고 고쳤습니다. 공격자가 증거를 숨기는 방법(헤더 파괴, 확장자 위장, 파일 속 매장)을 직접 해 봤기에, 방어자로서 그 흔적을 찾을 수 있게 된 것입니다. 포렌식의 모든 기법은 이 대칭 위에 서 있습니다.


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