(3/4) 포렌식과 데이터 복원 — 흔적을 읽고 깨진 데이터를 되살리기
🛡️ 생성형 AI × 보안 4부작 (전체 4편)
- 공격 — 인가된 모의침투에 LLM 붙이기
- 방어 — 탐지·대응 자동화
- 포렌식과 데이터 복원 ← 지금 글
- LLM 그 자체의 보안 — 프롬프트 인젝션과 가드레일
Summary
앞의 두 편이 “사고 전·중” 이었다면, 포렌식은 “사고 후” 예요. 무슨 일이 있었는지 흔적으로 재구성하고, 지워지거나 깨진 데이터를 되살리는 일이죠. 데이터가 워낙 많고 형식이 제각각이라, LLM의 “읽고·해석하고·변환하는” 능력이 특히 빛나는 영역이에요.
다만 포렌식엔 다른 편에 없는 제약이 하나 더 있어요. 증거능력(admissibility) 입니다. LLM의 출력은 그 자체로 증거가 될 수 없고, 원본을 건드려서도 안 돼요. 그래서 이 글은 “LLM을 어디까지 믿고, 어디서 선을 긋는가” 를 계속 강조합니다.
💡 이 글에서 다루는 것
- 포렌식 워크플로우에서 LLM이 들어갈 자리(와 들어가면 안 되는 자리)
- 아티팩트 해석 — 레지스트리·브라우저·prefetch 등
- 타임라인 재구성 — 이벤트 더미를 내러티브로
- 파일 카빙 — 시그니처 기반 삭제 파일 복구
- 손상 데이터 복원 — 깨진 JSON·잘린 로그 되살리기
- 무결성과 증거능력 — 해시, 원본 보존, LLM의 한계
1. 포렌식에서 LLM의 자리 (그리고 선)
디지털 포렌식의 큰 흐름은 이래요. 수집 → 보존 → 분석 → 보고. 이 중에서 LLM이 들어갈 수 있는 곳과, 절대 들어가면 안 되는 곳이 분명히 갈립니다.
| 단계 | LLM 역할 | 주의 |
|---|---|---|
| 수집(acquisition) | ❌ 안 씀 | 비트 단위 이미징. 원본 무결성이 전부 |
| 보존(preservation) | ❌ 안 씀 | 해시·쓰기방지. 손대면 증거능력 상실 |
| 분석(analysis) | ✅ 강점 | 아티팩트 해석, 타임라인, 데이터 변환 |
| 보고(reporting) | ✅ 보조 | 초안·요약. 결론은 분석가가 검증·서명 |
핵심은 이거예요. LLM은 “분석을 빠르게” 하지, “증거를 만들지” 않아요. LLM이 “이 사용자가 파일을 삭제했다” 고 말해도 그건 가설이고, 분석가가 원본 아티팩트로 검증해야 비로소 사실이 됩니다. LLM 출력은 보고서의 결론이 아니라 분석가의 작업 가설 입니다.
이 선만 지키면, 분석 단계에서 LLM은 정말 강력한 조수예요.
2. 아티팩트 해석 — “이게 무슨 흔적인가”
윈도우 포렌식만 해도 아티팩트 종류가 수십 가지예요. prefetch, ShimCache, 레지스트리 Run 키, 점프리스트, 브라우저 히스토리… 베테랑도 다 외우진 못해요. LLM은 이 “이 흔적이 무엇을 의미하는지” 의 사전 역할을 합니다.
artifact = r"""
[HKLM\SYSTEM\CurrentControlSet\Services\bam\State\UserSettings\S-1-5-21-...]
"\Device\HarddiskVolume3\Users\Public\svc.exe" = 0x01dba1... (FILETIME)
"""
system = (
"너는 디지털 포렌식 분석가다. 아래 윈도우 아티팩트가 무엇이며 "
"수사적으로 어떤 의미인지 설명하라. 단정 대신 '무엇을 추가로 확인해야 "
"이 가설을 검증할 수 있는지' 도 함께 제시하라."
)
print(ask_llm(system=system, user=artifact))
이것은 BAM(Background Activity Moderator) 레지스트리 항목입니다. 특정 사용자
(SID)가 svc.exe 를 '실행'했고, 마지막 실행 시각이 FILETIME 으로 기록돼 있다는
뜻입니다. C:\Users\Public 경로의 실행파일은 정상 소프트웨어 위치가 아니라
의심 신호입니다.
[검증할 것]
- svc.exe 의 해시를 위협 인텔과 대조
- 동일 시각대의 Prefetch / Amcache / Security 로그(4688) 교차 확인
- 해당 SID 사용자의 로그온 세션 시간과 일치 여부
여기서 1편·2편과 같은 패턴이 또 나와요 — “검증할 것” 을 같이 출력하게 하는 거예요. 포렌식에서 가장 위험한 건 단일 아티팩트로 결론 내는 거거든요. BAM 하나만 보고 “실행했다” 단정하면 안 되고, Prefetch·Amcache·이벤트 로그로 교차검증해야 법정에서 버팁니다. LLM에게 그 교차검증 목록을 뽑게 하면, 베테랑의 체크리스트를 신입도 따라갈 수 있어요.
💡
1편의ask_llm헬퍼를 그대로 씁니다. 포렌식에서도 system 프롬프트에 “단정 금지, 검증 항목 제시” 를 박아두는 게 핵심 패턴이에요.
3. 타임라인 재구성 — 더미를 이야기로
포렌식 분석의 꽃은 타임라인이에요. plaso(log2timeline) 같은 도구로 디스크 전체의 이벤트를 시간순으로 뽑으면 수십만 줄이 나와요. 사람이 다 읽을 수 없죠. LLM은 이 더미에서 “사건의 줄거리” 를 추려내는 데 강합니다.
핵심은 LLM에 통째로 넣지 않고, 먼저 기계적으로 필터링한 뒤 의심 구간만 해석시키는 거예요.
# 1) 기계적 필터: 의심 시간창의 이벤트만 추림 (LLM 비용·환각 모두 절감)
events = [
{"ts": "11:02:13", "src": "web", "msg": "GET /admin/../etc/passwd sqlmap"},
{"ts": "11:09:41", "src": "fs", "msg": "CREATE C:\\Users\\Public\\svc.exe"},
{"ts": "11:10:02", "src": "reg", "msg": "Scheduled Task 'SystemUpdate' 등록"},
{"ts": "11:10:55", "src": "net", "msg": "OUTBOUND 198.51.100.203:443"},
]
def narrate(events: list) -> str:
"""필터링된 이벤트 리스트 → 시간순 내러티브 + 공백 구간 지적."""
table = "\n".join(f"{e['ts']} [{e['src']}] {e['msg']}" for e in events)
system = (
"너는 포렌식 분석가다. 아래 이벤트를 시간순 내러티브로 재구성하고, "
"인과관계 가설과 '증거가 비어있는 구간'을 함께 지적하라."
)
return ask_llm(system=system, user=table)
print(narrate(events))
[재구성된 내러티브]
11:02 공격자가 자동화 도구(sqlmap)로 웹 취약점을 탐색.
11:09 약 7분 뒤 Public 폴더에 svc.exe 가 생성됨 — 이 사이 '어떻게 실행권한을
얻었는지'에 대한 증거가 비어 있음(공백 구간).
11:10 작업 스케줄러로 지속성 확보 직후 외부 443 아웃바운드(C2 추정).
[증거 공백]
11:02~11:09 사이의 인증/권한획득 단계 로그가 없음 → 웹쉘·자격증명 탈취 여부를
별도 확인 필요. 이 공백을 메우기 전엔 '초기 실행 벡터'를 단정할 수 없음.
“증거가 비어있는 구간” 을 지적하게 한 게 이 패턴의 핵심이에요. 좋은 포렌식 보고서는 “무엇을 알아냈나” 만큼 “무엇을 아직 모르나” 를 정직하게 적어요. LLM에게 공백을 찾게 하면, 분석가가 다음에 어디를 더 파야 할지가 분명해집니다.
⚠️ LLM은 이벤트 사이를 그럴듯하게 메우려는(서사를 완성하려는) 경향이 있어요. 그래서 “공백을 지적하라” 를 명시적으로 시켜야 환각으로 빈칸을 채우는 걸 막습니다. 내러티브는 가설일 뿐, 타임스탬프 원본이 진실입니다.
4. 파일 카빙 — 삭제된 파일 되살리기
이제 복원이에요. 파일을 “삭제” 해도 디스크의 실제 바이트는 보통 한동안 남아있어요. 파일시스템의 목차만 지워질 뿐이죠. 파일 카빙(file carving) 은 이 raw 바이트에서 파일의 시그니처(magic number) 를 찾아 경계를 추정하고 잘라내는 기법이에요.
여기서 LLM의 역할은 카빙 자체가 아니라 — 그건 바이트 연산이라 코드가 합니다 — “이 알 수 없는 헤더가 무슨 포맷인지” 를 식별하는 보조예요. 먼저 고전적인 시그니처 카빙부터.
# 디스크 이미지(또는 unallocated 덤프)에서 JPEG 를 카빙하는 최소 예시
SIGNATURES = {
"jpg": (b"\xff\xd8\xff", b"\xff\xd9"), # (시작 시그니처, 끝 시그니처)
"png": (b"\x89PNG\r\n\x1a\n", b"IEND\xaeB`\x82"),
"pdf": (b"%PDF", b"%%EOF"),
}
def carve(blob: bytes, kind: str) -> list:
"""raw 바이트에서 해당 포맷의 시작~끝 시그니처 쌍을 찾아 잘라냄."""
start_sig, end_sig = SIGNATURES[kind]
out, i = [], 0
while True:
s = blob.find(start_sig, i)
if s == -1:
break
e = blob.find(end_sig, s)
if e == -1:
break
out.append(blob[s:e + len(end_sig)])
i = e + len(end_sig)
return out
disk = b"....garbage...." + b"\xff\xd8\xff" + b"FAKEJPEGDATA" + b"\xff\xd9" + b"...."
recovered = carve(disk, "jpg")
print(f"복구된 JPEG 조각 수: {len(recovered)}, 첫 조각 크기: {len(recovered[0])} bytes")
복구된 JPEG 조각 수: 1, 첫 조각 크기: 18 bytes
실무에선 scalpel·photorec 같은 전용 도구를 쓰지만, 원리는 위와 똑같아요. 그런데 시그니처를 모르는 헤더를 만나면? 여기서 LLM이 들어옵니다.
unknown_header = "첫 16바이트(hex): 52 61 72 21 1A 07 01 00 ..."
system = (
"너는 파일 포맷 분석가다. 주어진 매직 바이트가 어떤 파일 포맷인지 식별하고, "
"그 포맷의 끝(footer) 시그니처와 카빙 시 주의점을 알려달라."
)
print(ask_llm(system=system, user=unknown_header))
52 61 72 21 1A 07 01 00 은 RAR 아카이브(v5) 의 시그니처('Rar!\x1A\x07\x01\x00')
입니다. RAR 은 명확한 단일 footer 가 없어 시그니처 카빙이 까다롭고, 블록 구조를
따라가야 정확히 잘립니다. unrar 로 무결성 검사 후 추출을 권장합니다.
🚨 카빙 복구물은 단편화(fragmentation) 로 깨질 수 있어요. 파일이 디스크에 연속 저장됐다는 보장이 없거든요. 그래서 복구한 파일은 항상 무결성 검사(헤더-푸터 일치, 포맷 검증)를 거치고, 원본 이미지는 절대 손대지 않고 사본에서만 작업합니다.
5. 손상 데이터 복원 — 깨진 JSON·잘린 로그
물리적 삭제뿐 아니라 논리적 손상도 흔해요. 전송 중 잘린 JSON, 인코딩이 깨진 CSV, 일부만 남은 설정 파일… 사람이 손으로 고치면 오래 걸리는데, LLM은 구조를 추론해서 복원하는 데 특히 강합니다.
깨진 JSON을 복원하는 예시예요. 닫는 괄호가 날아가고 값이 잘린 상태입니다.
broken = '{"user": "admin", "role": "oper'
system = (
"너는 데이터 복구 엔지니어다. 손상된 JSON 을 '가장 그럴듯한' 형태로 복원하라. "
"복원한 JSON 과, 어디를 추정했는지 '추정 표시' 를 함께 답하라. "
"확신 못 하는 값은 추정임을 명시하라."
)
print(ask_llm(system=system, user=broken))
[복원 JSON]
{"user": "admin", "role": "operator"}
[추정 표시]
- "role" 값이 'oper' 에서 잘림 → 'operator' 로 추정(확신도 중간). 'operation',
'operations' 가능성도 있음. 원본 스키마/다른 레코드와 대조 권장.
- 닫는 중괄호 '}' 추가(구조적 확실).
여기서 결정적인 건 “추정임을 명시하게” 한 것이에요. 복원 데이터의 가장 큰 위험은 “LLM이 채워넣은 값을 원본으로 착각하는 것” 이거든요. operator 가 진짜였는지 operations 였는지는 LLM이 알 수 없어요 — 그저 그럴듯할 뿐이에요. 그래서 복원물은 “원본” 이 아니라 “복원 추정본” 으로 라벨링하고, 가능하면 다른 레코드·백업과 대조해 확정합니다.
잘린 로그 파일도 같은 방식으로 구조를 복원해요.
def recover(broken_text: str, schema_hint: str) -> str:
"""손상 텍스트 + 스키마 힌트 → 복원 추정본. 추정 부분은 [추정] 으로 표시."""
system = (
f"너는 데이터 복구 엔지니어다. 예상 스키마는 다음과 같다: {schema_hint}. "
"손상된 데이터를 복원하되, 추정한 필드는 [추정] 으로 표시하라."
)
return ask_llm(system=system, user=broken_text)
print(recover(
broken_text="2026-06-09 11:02 198.51.100.77 GET /admin sta",
schema_hint="timestamp ip method path status_code",
))
2026-06-09 11:02 198.51.100.77 GET /admin [추정]status_code=200
(status 가 'sta' 에서 잘림. 200/403/404 중 하나로 추정 — 후속 이벤트로 확정 필요)
💡 OCR로 종이·스캔 증거를 디지털화할 때도 같은 패턴이 유용해요. OCR이 만든 오타·깨짐을 LLM이 문맥으로 교정하되, 원본 스캔 이미지를 같이 보존해서 “교정본 vs 원본” 을 항상 비교 가능하게 둡니다.
6. 무결성과 증거능력 — 넘지 말아야 할 선
포렌식이 다른 보안 작업과 결정적으로 다른 점이 법정이에요. 아무리 잘 분석해도 증거능력이 깨지면 무용지물이죠. LLM을 쓸 때 반드시 지켜야 할 원칙들이에요.
import hashlib
def evidence_hash(path: str) -> str:
"""증거 파일의 SHA-256 — 작업 전후로 찍어 무결성 입증."""
h = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(8192), b""):
h.update(chunk)
return h.hexdigest()
# 분석 시작 전 원본 이미지 해시를 기록하고, 작업은 '사본'에서만
before = evidence_hash("./evidence/disk.img")
print(f"원본 SHA-256: {before[:16]}... (전 과정 동안 불변이어야 함)")
원본 SHA-256: 9f86d081884c7d65... (전 과정 동안 불변이어야 함)
지켜야 할 선을 정리하면 이래요.
- 원본 불가침 — 분석은 항상 사본에서. 원본 해시는 수집 시점에 찍어 보관하고, 작업 종료 후 다시 찍어 변하지 않았음을 입증합니다.
- 관리 연속성(chain of custody) — 누가·언제·무엇을 했는지 기록. LLM에 무엇을 입력했는지도 이 기록의 일부예요.
- LLM 출력 ≠ 증거 — LLM의 해석·복원물은 분석가의 작업 가설 이지 증거가 아닙니다. 결론은 원본 아티팩트로 재현·검증한 뒤 분석가가 책임지고 서명해요.
- 민감정보 유출 주의 — 증거에는 개인정보가 가득해요. 외부 API에 넣기 전 2편의 마스킹을 거치거나, 사건 데이터는 로컬 모델로만 다룹니다. 특히 수사 자료는 외부 전송 자체가 금지인 경우가 많아요.
- 재현 가능성 — 같은 입력에 같은 결론이 나와야 해요. LLM은 본질적으로 비결정적이라, “LLM이 그랬다” 가 아니라 “LLM이 가리킨 원본을 사람이 확인했다” 가 보고서에 들어갑니다.
🚨 한 줄로 요약하면: LLM은 포렌식의 “속도” 를 올리는 도구지, “진실” 의 출처가 아니에요. 속도를 얻되 무결성과 증거능력은 한 치도 양보하지 마세요.
마무리
포렌식·복원에서 LLM은 “방대한 데이터를 빠르게 읽고, 낯선 포맷을 식별하고, 깨진 구조를 추론” 하는 데 정말 강력해요. 아티팩트 해설, 타임라인 내러티브, 시그니처 식별, 손상 데이터 복원 — 이전엔 베테랑의 경험에 의존하던 일을 1차로 깔아줍니다.
대신 포렌식엔 다른 편에 없는 빨간 선이 있어요. 원본 무결성과 증거능력. LLM이 가리킨 것을 사람이 원본으로 검증하고, 복원물은 “추정본” 으로 정직하게 라벨링하고, 결론엔 분석가가 서명합니다.
마지막 편에서는 시선을 뒤집어요. 지금까지는 LLM을 도구로 썼다면, 4편은 LLM 그 자체가 공격 대상이 됐을 때 — 프롬프트 인젝션, 데이터 유출, 에이전트 악용 — 를 어떻게 막는지 정리합니다.
일단 오늘은 여기까지…..
다음 글에서는 LLM 자체를 어떻게 안전하게 지키는지 정리해볼게요.
← 이전 글: (2/4) 방어 — 탐지·대응 자동화 | 다음 글 →: (4/4) LLM 그 자체의 보안 — 프롬프트 인젝션과 가드레일