Step 242. 메모리 포렌식: Volatility 3 — 꺼지면 사라지는 증거를 붙잡는 법
Level 3 — Forensics 트랙 | 난이도 ★★★★☆ | 예상 소요 시간 5시간
전제: Step 239(파일 시그니처)를 마쳤다. 프로세스가 무엇인지, 문자열 검색(strings)을 할 줄 안다.
⚠️ 이 챕터의 실습은 내 랩·합법 플랫폼 전용입니다. 허가 없는 시스템에 적용하면 범죄입니다.
- 준비물: WSL 우분투 + 파이썬 3(실측: 3.12).
/proc접근 권한(루트). - 주의: 이 환경에는 Volatility와 연습용 메모리 이미지가 없습니다. Volatility 출력은 전부 "화면 예시"이고, 메모리 포렌식의 원리 — 프로세스 목록, 네트워크 연결, 메모리 속 문자열 — 는 리눅스의
/proc에서 직접 실측합니다.
사건 현장의 컴퓨터 전원을 끄는 순간, RAM의 내용은 증발합니다. 그런데 정작 가장 중요한 증거 — 지금 실행 중인 악성 프로세스, 방금 친 명령어, 복호화된 키, 평문으로 풀린 암호 — 는 디스크가 아니라 RAM에만 있습니다. 메모리 포렌식(memory forensics)은 이 휘발성 증거를 덤프로 고정하고 해부하는 기술입니다. 오늘은 표준 도구 Volatility 3의 지도를 익히고, 그 플러그인들이 하는 일의 원리를 살아있는 리눅스에서 직접 확인합니다.
1. 학습 목표
이 챕터를 끝내면 다음을 할 수 있습니다:
- 메모리에만 존재하는 증거의 종류(프로세스, 연결, 명령줄, 평문 데이터)를 열거한다
/proc을 읽어 프로세스 목록과 네트워크 연결을 재구성한다- 다른 프로세스의 메모리에서 문자열을 직접 추출한다
- Volatility의 분석 순서(
windows.info→pslist/psscan→pstree→ 수집)를 설명한다 pslist와psscan의 차이가 루트킷 탐지에 쓰이는 원리를 안다
2. 배경 지식 — 오늘의 도구와 개념
오늘의 도구 한눈에 보기
| 구분 | 내용 |
|---|---|
| 언어·환경 | WSL 우분투 bash + 파이썬 3 (실측: Python 3.12) |
| 오늘의 도구 | /proc/<pid>/ (comm, cmdline, maps, mem), /proc/net/tcp — 전부 실측 |
| 필요한 개념 | 휘발성, 프로세스 메모리 맵, 덤프, Volatility 플러그인 체계(화면 예시) |
| 오늘의 산출물 | /proc 관찰 스크립트 + "다른 프로세스 메모리에서 꺼낸 비밀 문자열" |
2-1. 왜 메모리인가 — 디스크에 없는 것들
디스크는 "저장된 것"의 세계이고, 메모리는 "지금 벌어지는 것"의 세계입니다. RAM에만 있는 증거의 대표 목록:
- 실행 중인 프로세스와 그 명령줄 — 파일로 존재하지 않는 fileless 악성코드 포함
- 살아있는 네트워크 연결 — 지금 이 순간 누구와 통신 중인지
- 복호화된 데이터 — 디스크에는 암호화돼 있지만 실행 중엔 평문인 키·문자열
- 명령 이력과 클립보드 — 사용자가 방금 한 행동
사고 대응(IR)에서 "일단 전원부터 끄는" 초보 실수가 치명적인 이유가 이것입니다. 전원이 꺼지면 이 목록 전체가 증발합니다.
2-2. 덤프와 해부 — 두 단계의 분업
메모리 포렌식은 두 단계로 나뉩니다. ① 덤프: 사건 시점의 RAM을 통째로 파일로 고정합니다 (윈도우의 WinPmem, 리눅스의 LiME 같은 도구가 커널 수준에서 읽습니다). ② 해부: 그 날것의 바이트 덩어리에서 의미를 복원합니다. 덤프는 그냥 수 GB의 바이트라서, "프로세스 목록이 어디 있고, 각 프로세스의 구조체가 어떻게 생겼는지"를 아는 도구가 필요합니다 — 그것이 Volatility입니다.
2-3. /proc — 살아있는 시스템의 메모리 창
리눅스에는 실습용으로 완벽한 창이 이미 열려 있습니다. /proc은 커널이 만들어 주는 가상 파일시스템으로, 지금 살아있는 시스템의 상태를 파일처럼 보여 줍니다:
| 경로 | 내용 | Volatility 대응 |
|---|---|---|
/proc/<pid>/comm, cmdline |
프로세스 이름·명령줄 | pslist, cmdline |
/proc/net/tcp |
현재 TCP 연결 표 | netstat |
/proc/<pid>/maps |
프로세스의 메모리 지도 | memmap |
/proc/<pid>/mem |
그 메모리의 실제 내용 | memmap --dump |
오늘 우리는 덤프 파일 대신 살아있는 시스템에서 같은 정보를 읽습니다. Volatility가 죽은 덤프에서 하는 일의 원리를, /proc이 산 시스템에서 그대로 보여 주기 때문입니다.
2-4. Volatility 3 — 플러그인 지도 (화면 예시)
이 환경에는 설치돼 있지 않지만, 실전 표준 워크플로는 이렇게 생겼습니다 (화면 예시 — 실제 실행하지 않았습니다):
# 화면 예시 — 0단계: 이미지의 OS 확인 (모든 분석의 첫 단계)
$ vol -f memory.raw windows.info
Variable Value
Kernel Base 0xf8054b600000
NTBuildLab 19041.329.amd64fre.vb_release.190602-1739
# 화면 예시 — 1단계: 프로세스 목록 두 가지
$ vol -f memory.raw windows.pslist # 커널의 "현재 목록"을 따라감
PID PPID ImageFileName
4 0 System
512 4 smss.exe
3844 512 powershell.exe
$ vol -f memory.raw windows.psscan # 메모리 전체를 훑어 구조체 흔적 탐색
PID PPID ImageFileName
...
5920 3844 evil.exe ← pslist에는 없던 프로세스!
두 목록의 차이가 핵심 단서입니다. pslist는 운영체제의 공식 명부를 읽고, psscan은 메모리 전체에서 프로세스 구조체의 흔적을 스캔합니다. 공식 명부에서 자기 이름을 지운 프로세스(루트킷)는 psscan에만 나타납니다.
# 화면 예시 — 2단계: 관계와 흔적 수집
$ vol -f memory.raw windows.pstree # 부모-자식: winword.exe 아래 powershell.exe → 매크로 감염의 얼굴
$ vol -f memory.raw windows.cmdline # 각 프로세스가 어떤 명령으로 떴는가
$ vol -f memory.raw windows.netstat # 덤프 시점의 네트워크 연결
$ vol -f memory.raw -o out/ windows.memmap --dump --pid 5920 # 수상한 프로세스의 메모리를 파일로
$ strings out/pid.5920.dmp | grep -i "DH{" # 거기서 문자열 찾기
마지막 두 줄 — 수상한 프로세스의 메모리를 덤프해 strings를 거는 것 — 이 오늘 우리가 /proc으로 직접 해 볼 작업입니다.
3. 따라 하기
3-1. 사건 조성 — 비밀을 품은 프로세스
메모리에만 비밀을 들고 있는 프로세스를 만듭니다 (이 챕터의 로컬 출력은 2026-09-09 WSL 실측입니다).
입력 (victim.py)
import time, sys, os
SECRET = "DH{m3m0ry_n3v3r_f0rg3ts}" # 이 값은 실행 중인 프로세스의 RAM에 실재한다
print(f"[victim] PID={os.getpid()} 비밀을 메모리에 들고 대기 중...")
sys.stdout.flush()
time.sleep(120)
python3 victim.py &
[victim] PID=616 비밀을 메모리에 들고 대기 중...
읽는 법: 이 프로세스는 아무 파일도 쓰지 않고, 네트워크도 쓰지 않습니다. 비밀은 오직 이 프로세스의 RAM에만 있습니다. 디스크를 아무리 뒤져도 실행 중인 이 프로세스의 변수 값은 나오지 않습니다 — 메모리를 봐야만 보이는 증거입니다.
3-2. 프로세스 목록 재구성 — pslist의 원리
/proc의 숫자 디렉터리 하나가 프로세스 하나입니다. 이름과 명령줄을 읽어 봅니다:
import os
for pid in sorted((p for p in os.listdir("/proc") if p.isdigit()), key=int):
try:
name = open(f"/proc/{pid}/comm").read().strip()
cmd = open(f"/proc/{pid}/cmdline", "rb").read().replace(b"\x00", b" ").decode().strip()
print(f"PID {pid:>6} {name:16s} {cmd[:60]}")
except (FileNotFoundError, PermissionError):
pass
PID 1 systemd /sbin/init
PID 2 init-systemd(Co /init
PID 49 systemd-journal /usr/lib/systemd/systemd-journald
PID 98 systemd-udevd /usr/lib/systemd/systemd-udevd
PID 616 python3 python3 victim.py
...
(2026-09-09 실측 — 총 52개 프로세스, 일부만 발췌.)
읽는 법: PID 616이 눈에 들어옵니다 — python3 victim.py라는 명령줄까지 보입니다. Volatility의 pslist(목록)와 cmdline(명령줄) 플러그인이 덤프에서 복원하는 것이 정확히 이 정보입니다. 우리는 산 시스템에서 같은 것을 읽은 것입니다.
3-3. 네트워크 연결 재구성 — netstat의 원리
/proc/net/tcp는 현재 TCP 연결 표입니다. 주소가 리틀엔디언 hex로 돼 있어 해석이 필요합니다:
import socket, struct
STATES = {"0A": "LISTEN", "01": "ESTABLISHED"}
def dec(hexaddr):
h, p = hexaddr.split(":")
ip = socket.inet_ntoa(struct.pack("<I", int(h, 16)))
return f"{ip}:{int(p, 16)}"
with open("/proc/net/tcp") as f:
next(f)
for line in f:
cols = line.split()
st = STATES.get(cols[3], cols[3])
if st:
print(f"{dec(cols[1]):22s} -> {dec(cols[2]):22s} {st}")
127.0.0.54:53 -> 0.0.0.0:0 LISTEN
127.0.0.1:46047 -> 0.0.0.0:0 LISTEN
10.255.255.254:53 -> 0.0.0.0:0 LISTEN
127.0.0.53:53 -> 0.0.0.0:0 LISTEN
(2026-09-09 실측. 53번 포트의 LISTEN은 WSL의 내부 DNS 창구입니다.)
읽는 법: 지금은 조용하지만, 만약 여기에 모르는 외부 IP로의 ESTABLISHED가 있었다면 — 그것이 "지금 이 순간 누구와 연결돼 있는가"라는, 전원을 끄면 사라지는 증거입니다. windows.netstat이 덤프에서 복원하는 것이 이 표의 사건 시점 버전입니다.
3-4. 프로세스 메모리에서 비밀 꺼내기 — memmap+strings의 원리
오늘의 정점입니다. /proc/<pid>/maps(메모리 지도)를 읽고, 쓰기 가능한 구역의 실제 내용(/proc/<pid>/mem)에서 DH{로 시작하는 문자열을 찾습니다:
import os, re, sys
target = sys.argv[1] # victim의 PID
found = []
with open(f"/proc/{target}/maps") as f:
maps = [l.split() for l in f if "rw" in l.split()[1]] # 쓰기 가능 구역만
mem = os.open(f"/proc/{target}/mem", os.O_RDONLY)
scanned = 0
for m in maps:
start, end = (int(x, 16) for x in m[0].split("-"))
size = end - start
if size > 8 * 1024 * 1024:
continue
try:
os.lseek(mem, start, os.SEEK_SET)
data = os.read(mem, size)
scanned += size
for mt in re.finditer(rb"DH\{[ -~]{1,60}\}", data):
s = mt.group().decode()
if s not in found:
found.append(s)
except OSError:
continue
os.close(mem)
print(f"PID {target}의 쓰기 가능 메모리 {scanned/1024:.0f}KB 스캔")
for s in found:
print("발견:", s)
python3 memscan.py 616
PID 616의 쓰기 가능 메모리 5140KB 스캔
발견: DH{m3m0ry_n3v3r_f0rg3ts}
(2026-09-09 실측.)
읽는 법: 디스크 어디에도 저장된 적 없는 문자열을 다른 프로세스의 RAM에서 꺼냈습니다. 이 30줄이 Volatility의 memmap --dump + strings 조합이 하는 일의 원리 그대로입니다 — 메모리 지도(maps)를 따라가며 내용(mem)을 읽고, 문자열 패턴을 찾는 것. 실전에서는 여기서 복호화된 키, C2 주소, 평문 자격증명이 나옵니다. "암호화된 파일도 실행되는 순간 메모리에서는 평문이다" — 메모리 포렌식의 위력이 이 한 줄에 있습니다.
3-5. 전체 워크플로 정리 — 덤프를 받았다면
오늘의 실습을 Volatility의 언어로 번역하면 이렇습니다 (화면 예시):
# 화면 예시 — 메모리 덤프 분석의 표준 순서
1. windows.info → 이 덤프가 어떤 OS인지 확인 (심볼 선택의 기준)
2. windows.pslist → 공식 프로세스 목록
3. windows.psscan → 스캔 목록 — pslist와 차이가 나면 은폐 의심 ← 3-2의 원리
4. windows.pstree → 이상한 부모-자식 (문서가 셸을 띄움?)
5. windows.cmdline → 수상한 프로세스의 명령줄 ← 3-2의 원리
6. windows.netstat → 덤프 시점의 연결 ← 3-3의 원리
7. memmap --dump + strings → 수상한 프로세스 메모리에서 문자열 ← 3-4의 원리
오늘 우리가 /proc에서 한 3-2, 3-3, 3-4가 이 순서의 2, 5, 6, 7번과 같은 원리입니다. 도구는 "죽은 덤프에서" 하고, 우리는 "산 시스템에서" 했을 뿐입니다.
4. 미션과 연습문제
미션 — 내 시스템 관찰 보고서
- 3-2의 코드를 확장해 각 프로세스의 PPID(부모 번호)까지 읽는 목록을 만든다 (
/proc/<pid>/status의PPid:줄) - 그 목록으로 부모-자식 트리를 출력한다 (들여쓰기 형태) — pstree의 원리
- 3-4의 스캐너로 victim 말고 다른 프로세스 하나(예: 실행 중인 셸)를 스캔해 어떤 문자열이 나오는지 관찰한다
- "왜 사고 대응에서는 전원을 끄지 않고 메모리부터 덤프하는가"를 오늘 실험 세 개를 근거로 5문장 이내로 쓴다
연습문제
문제 1. pslist와 psscan의 차이는 무엇이며, 두 결과의 차이가 의심스러운 이유는 무엇인가요?
문제 2. 암호화된 악성코드 파일을 디스크에서 찾지 못했는데, 메모리 분석에서는 찾을 수 있었습니다. 왜 그럴까요?
문제 3. 3-4에서 maps 중 "쓰기 가능(rw)" 구역만 골라 읽었습니다. 이 선택의 이유를 추측해 보세요.
문제 4. /proc/<pid>/mem을 읽으려 할 때 권한 오류가 날 수 있습니다. 어떤 권한이 필요하며, 실제 사고 현장에서 이에 해당하는 장벽은 무엇일까요?
5. 모범 답안과 완료 기준
미션 모범 답안
①~② PPID 트리의 핵심 코드:
procs = {}
for pid in (p for p in os.listdir("/proc") if p.isdigit()):
try:
status = open(f"/proc/{pid}/status").read()
name = re.search(r"Name:\t(.+)", status).group(1)
ppid = int(re.search(r"PPid:\t(\d+)", status).group(1))
procs[int(pid)] = (name, ppid)
except Exception:
pass
def tree(pid, depth=0):
if pid in procs:
print(" " * depth + f"{procs[pid][0]} ({pid})")
for p, (n, pp) in procs.items():
if pp == pid:
tree(p, depth + 1)
tree(1)
③ 관찰 예: 셸 프로세스를 스캔하면 환경 변수, 최근 명령어 조각, 히스토리의 잔재가 보입니다 — "프로세스의 메모리는 그 프로세스가 한 일의 찌꺼기를 담고 있다"는 직접 확인이 됩니다.
④ 답변의 골격: 프로세스 목록(3-2), 연결 표(3-3), 평문 비밀(3-4)은 전부 RAM에만 존재하며 전원이 꺼지면 사라집니다. 오늘 우리가 PID 616에서 꺼낸 문자열이 그 증명입니다. 그래서 현장에서는 디스크보다 먼저 메모리를 덤프합니다.
검증하는 법: ① 트리가 systemd 아래로 실제 계층을 그리는가. ② 스캔 관찰 결과가 기록됐는가. ③ ④번 답변에 실험 근거가 세 개 다 인용됐는가.
연습문제 해답
문제 1 해답. pslist는 커널이 관리하는 연결 리스트(공식 명부)를 따라가고, psscan은 메모리 전체를 스캔해 프로세스 구조체의 흔적을 찾습니다. 루트킷은 공식 명부에서 자기 노드를 떼어 내 숨지만, 메모리 어딘가의 구조체 흔적까지 지우기는 어렵습니다. 그래서 psscan에만 있는 프로세스는 은폐된 프로세스로 의심받습니다 — 종료된 프로세스의 잔재일 수도 있어 추가 확인은 필요합니다.
문제 2 해답. 실행하려면 CPU가 코드를 읽어야 하고, 그 순간 코드와 데이터는 메모리에서 평문으로 풀립니다. 디스크의 암호화는 "저장 상태"의 보호일 뿐 "실행 상태"를 보호하지 못합니다. 오늘 3-4에서 본 것처럼, 메모리는 실행의 모든 비밀이 평문으로 드러나는 장소입니다.
문제 3 해답. 쓰기 가능 구역에는 힙·스택 — 프로세스가 실행 중 만들어 낸 데이터(변수, 입력값, 복호화 결과) — 가 있습니다. 읽기 전용 구역은 대부분 프로그램 코드와 상수라 디스크의 원본 파일과 같습니다. "실행 중에 생긴 것"을 찾는 것이 목적이므로 rw 구역이 1순위 수색 구역입니다.
문제 4 해답. 리눅스에서는 같은 사용자이거나 루트 권한(ptrace 제약 포함)이 필요합니다. 실제 현장에서 이에 해당하는 장벽은 커널 수준의 보호입니다 — 메모리 덤프 도구(WinPmem, LiME)가 관리자·커널 드라이버 권한을 요구하는 이유이며, 최신 OS의 보호 기법(커널 메모리 접근 제한, 암호화된 RAM)이 이 수집을 점점 어렵게 만들고 있습니다.
완료 기준 체크리스트
- [ ] 메모리에만 있는 증거 네 종류를 말할 수 있다
- [ ]
/proc에서 프로세스 목록·명령줄을 직접 읽었다 - [ ]
/proc/net/tcp를 해석해 연결 표를 재구성했다 - [ ] 다른 프로세스의 메모리에서 비밀 문자열을 추출했다
- [ ] Volatility의 분석 순서(info → pslist/psscan → pstree → 수집)를 외웠다
- [ ] pslist/psscan 차이와 루트킷 탐지의 관계를 설명할 수 있다
- [ ] 미션: PPID 트리와 관찰 보고서를 완성했다
6. 흔한 실수와 해결
벽 1. Permission denied로 /proc/<pid>/mem을 못 읽는다
증상: os.open에서 PermissionError가 납니다.
원인: 다른 사용자의 프로세스 메모리는 보호돼 있습니다. ptrace 보안 정책(ptrace_scope)도 읽기를 막을 수 있습니다.
해결: 내가 띄운 프로세스를 대상으로 하거나 루트로 실행하세요. 이 제약 자체가 "메모리는 보호된 영역"이라는 운영체제의 답입니다 — 실제 덤프 도구가 커널 권한을 필요로 하는 이유이기도 합니다.
벽 2. Volatility가 어느 플러그인이든 실패한다
증상 (화면 예시): vol -f dump.raw windows.pslist가 심볼 오류를 냅니다.
원인: 덤프의 OS 버전에 맞는 심볼(구조체 지도)이 없으면 Volatility는 바이트의 의미를 해석할 수 없습니다.
해결: 항상 windows.info부터 — 그 출력이 심볼 선택의 기준입니다. 순서를 거꾸로 하면 모든 플러그인이 거짓말을 합니다.
벽 3. psscan 결과가 너무 많다
증상 (화면 예시): psscan에 수십 개의 알 수 없는 프로세스가 나옵니다.
원인: 스캔은 종료된 프로세스의 잔해까지 잡아냅니다. 전부 살아있는 것이 아닙니다.
해결: pslist와의 차집합으로 "숨어 있는 것" 후보를 좁히고, 종료 시각 필드가 있다면 함께 봅니다. 잔해와 은폐를 구분하는 것이 분석가의 일입니다.
벽 4. maps의 주소로 mem을 읽었는데 OSError
증상 (2026-09-09 실습 중 확인 가능): 어떤 구역은 lseek/read가 실패합니다.
원인: 매핑됐지만 현재 물리 메모리에 없는(스왑 아웃된) 페이지, 또는 커널 전용 구역입니다.
해결: 예외 처리하고 건너뛰면 됩니다 — 실습 코드의 try/except OSError가 그 역할입니다. 실제 덤프에서도 스왑된 페이지는 덤프에 없습니다. "메모리 덤프에도 없는 것이 있다"는 것도 기억해 둘 사실입니다.
벽 5. strings를 걸었는데 의미 있는 게 안 나온다
증상 (화면 예시): 덤프한 프로세스 메모리에서 수만 줄의 문자열이 나와 요점을 못 찾겠습니다.
원인: 메모리에는 코드·라이브러리의 문자열이 섞여 있습니다. 전부가 단서는 아닙니다.
해결: 패턴으로 좁히세요 — 플래그 형식(DH{), IP 형태, http, .exe, 파일 경로. 3-4의 정규식처럼 "찾을 것의 모양"을 먼저 정하는 것이 문자열 분석의 절반입니다.
7. 정리
오늘의 개념
| 개념 | 한 줄 설명 |
|---|---|
| 휘발성 | RAM의 내용은 전원과 함께 증발 — 그래서 먼저 덤프한다 |
| 메모리에만 있는 증거 | 실행 중 프로세스, 살아있는 연결, 평문 데이터, 명령 이력 |
/proc |
살아있는 시스템의 메모리 창 — Volatility 원리의 실습장 |
| 덤프와 해부 | 수집(커널 권한)과 해석(심볼)의 두 단계 분업 |
| pslist vs psscan | 공식 명부 vs 전체 스캔 — 차이가 은폐의 신호 |
| 심볼 | 덤프의 바이트를 구조체로 번역하는 사전 — info가 먼저 |
오늘의 명령어·도구
| 명령·도구 | 하는 일 |
|---|---|
/proc/<pid>/comm, cmdline |
프로세스 이름·명령줄 (pslist·cmdline의 원리) |
/proc/net/tcp |
현재 TCP 연결 표 (netstat의 원리) |
/proc/<pid>/maps + mem |
프로세스 메모리 지도와 내용 (memmap의 원리) |
vol -f dump.raw windows.info (화면 예시) |
덤프의 OS·심볼 확인 — 항상 첫 단계 |
vol ... windows.pslist / psscan / pstree (화면 예시) |
프로세스 목록·은폐 탐지·관계 |
vol ... windows.memmap --dump --pid N (화면 예시) |
수상한 프로세스의 메모리 수집 |
명령어보다 중요한 감각
메모리 포렌식의 세계관은 한 문장입니다 — 실행되는 것은 전부 어딘가에 평문으로 존재한다. 암호화, 패킹, fileless 기법은 디스크에서의 위장이고, 메모리는 그 위장이 벗겨지는 무대 뒤입니다. 오늘 다른 프로세스의 RAM에서 비밀 문자열을 꺼낸 30줄이 그 세계관의 증명입니다.
동시에 반대 방향도 보이셨을 겁니다. 이렇게 풍부한 증거가 전원 버튼 하나에 사라집니다. 사고 현장에서의 첫 행동 — "끄지 말고 덤프하라" — 이 단순한 규칙이 왜 골든룰인지, 이제 몸으로 아시게 됐습니다.
전부 체크되면 Step 242 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.