Step 242. 메모리 포렌식: Volatility 3 — 꺼지면 사라지는 증거를 붙잡는 법

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.infopslist/psscanpstree → 수집)를 설명한다
  • pslistpsscan의 차이가 루트킷 탐지에 쓰이는 원리를 안다

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. 미션과 연습문제

미션 — 내 시스템 관찰 보고서

  1. 3-2의 코드를 확장해 각 프로세스의 PPID(부모 번호)까지 읽는 목록을 만든다 (/proc/<pid>/statusPPid: 줄)
  2. 그 목록으로 부모-자식 트리를 출력한다 (들여쓰기 형태) — pstree의 원리
  3. 3-4의 스캐너로 victim 말고 다른 프로세스 하나(예: 실행 중인 셸)를 스캔해 어떤 문자열이 나오는지 관찰한다
  4. "왜 사고 대응에서는 전원을 끄지 않고 메모리부터 덤프하는가"를 오늘 실험 세 개를 근거로 5문장 이내로 쓴다

연습문제

문제 1. pslistpsscan의 차이는 무엇이며, 두 결과의 차이가 의심스러운 이유는 무엇인가요?

문제 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 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.