Step 225. 악성코드 정적 분석 (격리 랩 필수) — 실행하지 않고 속을 읽는다

Step 225. 악성코드 정적 분석 (격리 랩 필수) — 실행하지 않고 속을 읽는다

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

전제: Step 168(악성코드 구조와 생애주기), Step 169(암호화와 탐지 회피), Step 224(난독화 분석 전략). 리눅스 쉘 기본 명령을 쓸 수 있습니다.

⚠️ 이 챕터의 실습은 내 랩·합법 플랫폼 전용입니다. 허가 없는 시스템에 적용하면 범죄입니다. 이 챕터는 추가로 아래 "격리 랩 규칙"을 반드시 지킵니다.

  • 준비물: 리눅스 랩(WSL 또는 전용 VM, gcc 포함), 메모장. 분석 대상은 직접 만드는 무해한 가짜 악성 샘플입니다.
  • 주의: 오늘의 절대 규칙 두 개 — ① 실제 악성코드 샘플은 절대 실행하지 않고 읽기만 한다(정적 분석). ② 실제 샘플을 다룰 때는 네트워크가 물리적으로 차단된 전용 VM에서만 다룬다. 오늘은 무해한 자체 제작 샘플로 절차를 연습하므로 WSL에서도 안전합니다.

악성코드 분석가의 첫 번째 무기는 실행이 아니라 독서입니다. 파일을 열지도, 실행하지도 않은 채 해시, 문자열, 헤더, 임포트 목록만으로 "이 파일이 무엇을 하려는가"를 추정하고 IoC(Indicator of Compromise, 침해 지표)를 뽑아냅니다. 이것이 정적 분석(static analysis)입니다. 오늘은 격리 랩의 규칙을 세우고, 수상한 문자열이 박힌 무해한 샘플을 직접 만들어, 실무와 같은 다섯 단계 절차로 분석 보고서까지 완성합니다.


1. 학습 목표

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

  • 격리 랩의 세 규칙(네트워크 차단, 스냅샷, 전용 환경)을 이유와 함께 설명한다
  • 정적 분석의 표준 절차(파일 종류 → 해시 → 문자열 → 헤더/라이브러리 → IoC)를 순서대로 수행한다
  • file, sha256sum, strings, readelf, objdump/nm 출력에서 의미 있는 단서를 골라낸다
  • 발견한 문자열에서 IoC(도메인, 경로, 뮤텍스 등)를 분류해 목록화한다
  • "추정 행동 + IoC + 탐지 아이디어"의 3단 구성으로 분석 보고서를 쓴다

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

오늘의 도구 한눈에 보기

구분 내용
언어·환경 리눅스 랩 (실측: WSL Ubuntu 24.04, gcc 13), C 소스 1개 자체 제작
오늘의 명령어 file, sha256sum, md5sum, strings, strings -e l, readelf -h/-S, objdump -p, nm
필요한 개념 정적 분석 vs 동적 분석, IoC, 해시 지문, 임포트 테이블, 패커 징후, 격리 랩
오늘의 산출물 가짜 악성 샘플의 정적 분석 보고서 1부 (추정 행동 + IoC 목록 + 탐지 아이디어)

2-1. 왜 실행하지 않는가 — 정적 분석의 이유

악성코드를 "돌려 보면" 빠르지 않을까요? 위험합니다. 실행하는 순간 감염·전파·파괴가 일어날 수 있고, 안티디버깅(Step 219)이 달린 샘플은 분석 환경을 감지해 얌전한 척합니다. 그래서 1차는 언제나 정적 분석 — 파일을 데이터로만 읽는 분석입니다.

정적 분석의 한계도 분명합니다. 패킹(Step 220)돼 있거나 문자열이 암호화(Step 224)돼 있으면 읽히는 것이 적습니다. 그때는 정적 분석의 결과물("패킹됨", "문자열 암호화됨") 자체가 다음 단계(언패킹, 동적 분석)로 가는 표지판이 됩니다.

2-2. 격리 랩의 세 규칙

실제 악성코드 샘플(theZoo 같은 연구용 저장소의 것)을 다룰 때의 규칙입니다. 오늘은 무해한 샘플로 연습하지만, 규칙은 몸에 배어 있어야 합니다.

  1. 네트워크 차단: 분석 VM은 호스트 전용 네트워크 또는 네트워크 어댑터 자체를 끕니다. 샘플이 C2에 신호를 보낼 통로를 물리적으로 없앱니다.
  2. 스냅샷: 분석 전에 VM 스냅샷을 찍고, 끝나면 롤백합니다. 샘플이 시스템을 오염시켜도 되돌릴 수 있습니다.
  3. 전용 환경: 분석 VM에 개인 파일, 계정, SSH 키를 두지 않습니다. 호스트와 공유 폴더·클립보드도 끕니다.

실제 샘플은 대개 infected 같은 비밀번호로 압축돼 배포됩니다 — 실수로 백신이 삭제하거나 사용자가 실행하는 것을 막는 관행입니다. 압축 해제는 격리 VM 안에서만입니다.

2-3. 정적 분석 다섯 단계 절차

실무의 첫 30분은 대략 이렇게 흐릅니다.

1단계: file        — 이 파일이 진짜 뭔가 (ELF? PE? 스크립트?)
2단계: sha256sum   — 지문 찍기. 이 해시로 공개 DB(VirusTotal) 조회 → 이미 알려진 샘플인가
3단계: strings     — 보이는 문자열 전부. URL, 경로, 에러 메시지, 뮤텍스가 IoC 후보
4단계: 헤더·라이브러리 — readelf/objdump로 구조와 임포트 확인. 수상한 함수 조합 찾기
5단계: 종합        — 추정 행동 + IoC 목록 + 탐지 아이디어로 보고서 작성

2-4. 무엇이 "수상한" 문자열인가

모든 문자열이 단서는 아닙니다. 분석가가 먼저 집는 것들:

유형 의미
URL/도메인 http://.../beacon.php C2 서버 후보 — 최우선 IoC
파일 경로 %APPDATA%\...\xxx.exe 자기 복사·드롭 위치
레지스트리 키 ...\CurrentVersion\Run 시작 프로그램 등록(지속성)
뮤텍스 Global\... 중복 실행 방지 표식 — 탐지 지표로 유용
User-Agent Mozilla/5.0 ... HTTP 통신 위장용

반대로 libc.so.6, 컴파일러 문자열 같은 것은 잡음입니다. 오늘 실습에서 둘을 구분해 봅니다.


3. 따라 하기

3-1. 분석 대상 만들기 — 무해한 "가짜 악성"

수상한 문자열은 박혀 있지만 실제 동작은 출력 한 줄뿐인 샘플을 만듭니다. 실행 가능한 악성 코드는 작성하지 않습니다 — 구조 분석이 목표이므로 문자열 재료만 필요합니다.

// notmalware.c — 정적 분석 연습용 무해한 "가짜 악성"
#include <stdio.h>

const char *c2_url   = "http://c2.evil-example.invalid/beacon.php";
const char *c2_url2  = "https://backup.evil-example.invalid:8443/gate";
const char *drop_path = "%APPDATA%\\Microsoft\\update\\svchost_upd.exe";
const char *reg_key  = "HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
const char *mutex    = "Global\\NotRealMutex_7f3a";
const char *ua       = "Mozilla/5.0 (Windows NT 10.0; Win64; x64)";

int main(void) {
    /* 실제로는 아무것도 하지 않는다 — 네트워크 접속도, 파일 생성도 없음 */
    puts("i am harmless.");
    return 0;
}

.invalid는 인터넷 표준(RFC 2606)상 절대 존재할 수 없는 예약 도메인이라, 이 문자열이 어디로 유출돼도 아무 일도 일어나지 않습니다. 컴파일하고 "출제자"의 역할은 잊습니다.

mkdir -p ~/lab/s225 && cd ~/lab/s225
gcc -o notmalware notmalware.c
file notmalware
./notmalware        # 무해함 확인용 단 한 번 — 실제 샘플이라면 절대 금지
notmalware: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=39b63c265b0d7d02e280c89f353a195fdc5b4362, for GNU/Linux 3.2.0, not stripped
i am harmless.

(2026-09-09 WSL Ubuntu 24.04, gcc 13.3.0 실측. BuildID와 해시는 여러분 환경에서 다릅니다 — 정상입니다.)

3-2. 1~2단계 — 파일 종류와 지문

file 결과를 먼저 읽습니다: ELF 64-bit, x86-64, 동적 링크, not stripped. 마지막이 중요합니다 — 심볼(함수·변수 이름)이 살아 있다는 뜻이라 분석이 쉽습니다. 실전 샘플은 대개 stripped라서 이 호사가 없습니다.

다음, 지문을 찍습니다.

sha256sum notmalware
md5sum notmalware
9e4d3431d938711bce3ddef1979250f509da48c8abfc18f6667af404c27b6961  notmalware
5159148affcb1772fe80c0e414711490  notmalware

출력 읽는 법: 이 해시가 보고서 첫 줄에 들어가는 파일의 신분증입니다. 실무라면 이 SHA-256을 VirusTotal에 검색해 "이미 알려진 샘플인가, 몇 개 엔진이 잡는가"를 봅니다 (오늘은 외부 조회 없이 화면 예시로만):

# 화면 예시 — VirusTotal 해시 검색 결과의 전형적 형태
SHA-256: 9e4d3431...   탐지: 0/70   첫 발견: —   (미등록 샘플)

미등록이면 "신종 또는 변종일 수 있다"로 넘어가고, 등록돼 있으면 기존 보고서를 먼저 읽습니다 — 이미 해부된 샘플을 다시 해부할 이유는 없습니다.

3-3. 3단계 — strings, 분석의 꽃

strings notmalware | grep -vE "^(__|_|[.]|GCC|GNU|GLIBC)" | head -40
strings -e l notmalware | head    # UTF-16LE 문자열 (윈도우 샘플에서 특히 중요)
/lib64/ld-linux-x86-64.so.2
puts
libc.so.6
http://c2.evil-example.invalid/beacon.php
https://backup.evil-example.invalid:8443/gate
%APPDATA%\Microsoft\update\svchost_upd.exe
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run
Global\NotRealMutex_7f3a
Mozilla/5.0 (Windows NT 10.0; Win64; x64)
i am harmless.
notmalware.c
puts@GLIBC_2.2.5
...

(UTF-16LE 문자열은 이 샘플에 없어 두 번째 명령은 빈 출력이었습니다 — 실측.)

출력 읽는 법: 잡음(ld-linux, libc, 컴파일 흔적) 사이에서 2-4 표의 유형들이 전부 보입니다. 이 시점에서 이미 분석의 80%가 끝났습니다. C2 후보 2개, 드롭 경로, 지속성 레지스트리 키, 뮤텍스, 위장용 User-Agent — 이 파일은 "HTTP 비콘을 보내고 시작 프로그램에 등록되는 계열"로 추정할 수 있습니다.

만약 여기서 문자열이 거의 안 나왔다면? 그것이 곧 결과입니다 — 패킹 또는 문자열 암호화의 징후이고(Step 224의 배열 꼴), 다음 행동은 언패킹(Step 220) 또는 동적 분석입니다.

3-4. 4단계 — 헤더, 라이브러리, 임포트

readelf -h notmalware | grep -E "Class|Type|Machine|Entry"
objdump -p notmalware | grep NEEDED
nm -D notmalware
  Class:                             ELF64
  Type:                              DYN (Position-Independent Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Entry point address:               0x1060
  NEEDED               libc.so.6
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
                 w __cxa_finalize@GLIBC_2.2.5
                 w __gmon_start__
                 U __libc_start_main@GLIBC_2.34
                 U puts@GLIBC_2.2.5

출력 읽는 법: 임포트(ELF에서는 동적 심볼, 윈도우 PE에서는 Import Table)가 이 파일의 "할 수 있는 일의 목록"입니다. 이 샘플은 외부 함수가 puts 하나뿐 — 문자열은 수상하지만 실제 능력은 없다는 결론이 헤더에서 확정됩니다. 실제 악성코드였다면 socket, connect, CreateRemoteThread, URLDownloadToFile 같은 이름이 여기서 걸립니다. "문자열은 의심을 만들고, 임포트는 확신을 준다"로 기억하세요.

윈도우 PE 샘플이라면 같은 단계를 objdump -p sample.exe | grep "DLL Name"과 DIE(Detect It Easy)로 하며, DIE는 패커 탐지까지 해 줍니다.

3-5. 5단계 — 보고서 작성

수집한 것을 3단 구성으로 정리하면 분석이 끝납니다.

[샘플 정보]  notmalware | ELF 64-bit x86-64 | SHA-256 9e4d34... | not stripped
[추정 행동]  HTTP 비콘 전송(C2 2개, 8443 포트 백업 채널),
            시작 프로그램 등록으로 지속성 확보, 뮤텍스로 중복 실행 방지.
            단, 임포트가 puts뿐이라 현 빌드에는 실제 통신 기능 없음 (훈련용 샘플)
[IoC]       도메인: c2.evil-example.invalid, backup.evil-example.invalid:8443
            경로: %APPDATA%\Microsoft\update\svchost_upd.exe
            레지스트리: HKCU\...\CurrentVersion\Run
            뮤텍스: Global\NotRealMutex_7f3a
[탐지 아이디어] 뮤텍스명과 C2 도메인을 시그니처로, Run 키 변경을 행위 감시로

읽는 법: "추정"임을 명시하는 것이 보고서의 성의입니다. 문자열만 보고 기능을 단정하면 틀립니다 — 오늘 샘플처럼 문자열과 실제 능력(임포트)이 다를 수 있으니까요. 두 근거를 교차 확인하는 습관이 오늘의 핵심 산출물입니다.


4. 미션과 연습문제

미션 — 변형 샘플로 분석 보고서 1부 완성

  1. 3-1의 샘플을 변형합니다: C2 도메인, 드롭 경로, 뮤텍스명을 여러분만의 값으로 바꾸고(도메인은 반드시 .invalid 유지), 문자열 하나를 추가합니다 — 예: 작업 스케줄러 명령어 모양의 문자열
  2. 컴파일된 바이너리만 보고(소스를 덮고) 3-2~3-4의 절차를 수행합니다
  3. 3-5 형식의 보고서를 완성합니다. 단 "추정 행동"에는 근거(어떤 문자열/임포트에서 나온 추정인지)를 괄호로 답니다
  4. 마지막 줄에 "이 샘플이 패킹돼 있었다면 어떤 징후로 알았을까"를 두 문장으로 적습니다

연습문제

문제 1. 정적 분석과 동적 분석의 차이를 "실행 여부"와 "얻는 정보"로 각각 설명해 보세요.

문제 2. 격리 랩의 세 규칙을 나열하고, 각각 어떤 사고를 막는지 한 줄씩 써 보세요.

문제 3. 3-3에서 strings -e l 출력이 비어 있었습니다. 이 샘플에서 무엇을 알 수 있고, 어떤 샘플에서 이 옵션이 중요해지나요?

문제 4. 어떤 샘플의 strings가 온통 짧은 무의미 문자열뿐이고, 임포트가 LoadLibraryGetProcAddress 두 개뿐이라면, 무엇을 추정하고 다음에 무엇을 하나요?


5. 모범 답안과 완료 기준

미션 모범 답안

보고서의 "추정 행동" 부분은 이런 결입니다 (문구는 여러분의 것으로):

[추정 행동]
- 부팅 시 자동 실행을 노림 (문자열 "...\CurrentVersion\Run" 근거)
- C2로 주기적 비콘을 보낼 가능성 (도메인 2개 + User-Agent 문자열 근거)
- 단, nm -D 결과 외부 함수가 puts뿐 → 현재 빌드에 통신 코드는 없음 (임포트 근거)

"패킹됐다면" 문항의 모범 답: "strings 출력이 비정상적으로 적고, 섹션 이름이 UPX0/UPX1 같은 팩커 흔적이며, 엔트로피가 높게 나오면 패킹을 의심한다. 확인은 DIE로 하고, 대응은 Step 220의 수동 언패킹으로 넘어간다."

검증하는 법: ① 모든 추정에 근거가 괄호로 달렸는가. ② IoC가 유형별(도메인/경로/레지스트리/뮤텍스)로 분류됐는가. ③ 해시가 보고서 첫머리에 있는가. ④ "문자열 ≠ 실제 능력"을 임포트로 교차 확인했는가.

연습문제 해답

문제 1 해답. 정적 분석은 파일을 실행하지 않고 데이터로 읽습니다 — 해시, 문자열, 헤더, 임포트처럼 "하려는 일"의 단서를 얻습니다. 동적 분석은 샌드박스에서 실행해 행동을 관찰합니다 — 실제로 "한 일"(생성한 파일, 보낸 패킷)을 얻습니다. 실무는 정적 → 동적 순서로, 정적의 한계(패킹, 암호화 문자열)를 동적으로 넘습니다.

문제 2 해답. ① 네트워크 차단 — 샘플이 C2와 통신하거나 추가 페이로드를 받는 것을 막습니다. ② 스냅샷 — 분석 중 시스템이 오염돼도 롤백으로 복구합니다. ③ 전용 환경 — 샘플이 탈취할 개인정보·자격 증명이 애초에 없게 하고, 공유 폴더/클립보드 경유 호스트 전파를 막습니다.

문제 3 해답. UTF-16LE 문자열이 없다는 것 — 이 샘플의 문자열은 전부 ASCII라는 뜻입니다. 윈도우 PE 샘플에서는 문자열이 UTF-16LE로 저장되는 경우가 많아(레지스트리 경로, 파일명 등) 기본 strings만으로는 절반만 보입니다. 그래서 PE 분석에서는 -e l을 습관적으로 함께 돌립니다.

문제 4 해답. 강력한 패킹/런타임 로딩 징후입니다 — LoadLibrary+GetProcAddress 조합은 "실행 중에 필요한 함수를 동적으로 불러온다"는 뜻이라 정적 임포트 목록이 의도적으로 비워진 것입니다. 정적 분석의 한계로 인정하고, 다음 행동은 패커 확인(DIE) → 언패킹(Step 220) 또는 샌드박스 행위 분석입니다. 이 조합 자체가 보고서에 들어갈 탐지 아이디어이기도 합니다.

완료 기준 체크리스트

  • [ ] 격리 랩 세 규칙(네트워크 차단, 스냅샷, 전용 환경)을 이유와 함께 말할 수 있다
  • [ ] 정적 분석 다섯 단계를 순서대로 실행했다
  • [ ] file 출력에서 not stripped 등 분석 난이도 단서를 읽었다
  • [ ] strings 출력에서 잡음과 IoC 후보를 구분했다
  • [ ] 임포트/동적 심볼로 "실제 능력"을 교차 확인했다
  • [ ] IoC를 유형별로 분류한 목록을 만들었다
  • [ ] 미션: 추정 행동 + IoC + 탐지 아이디어의 보고서 1부를 완성했다

6. 흔한 실수와 해결

벽 1. strings가 너무 많이 나와서 못 읽겠다

증상: 수백 줄이 쏟아집니다.
원인: 컴파일러와 라이브러리가 넣은 잡음 문자열이 대부분입니다.
해결: 3-3처럼 grep -v로 잡음을 걸러도 되지만, 실무에서는 반대로 수상한 패턴만 찾습니다 — strings 파일 | grep -Ei "http|\.exe|hkey|mutex|cmd|powershell". 윈도우 샘플이면 strings -e l을 꼭 병행하세요.

벽 2. gcc 컴파일 시 \ 관련 에러가 난다

증상: warning: unknown escape sequence: '\M' 또는 error: incomplete universal character name \u (2026-09-09 실측).
원인: C 문자열에서 \는 이스케이프 문자라, 경로의 \를 그대로 쓰면 \M, \u를 특수 문자로 해석합니다.
해결: 경로의 백슬래시를 전부 \\로 두 번 씁니다. 3-1 소스가 그렇게 돼 있는지 확인하세요.

벽 3. nm: notmalware: no symbols가 나온다

증상: nm이 심볼을 하나도 못 보여줍니다.
원인: 바이너리가 stripped입니다 (strip notmalware를 하거나 -s 옵션으로 빌드하면 재현됩니다 — 실측 확인 가능).
해결: 실전 샘플의 정상 상태입니다. nm -D(동적 심볼)는 stripped여도 남으니 거기서 임포트를 보고, 나머지는 disassembly로 넘어갑니다. file 출력이 "stripped"로 바뀌는 것도 함께 확인하세요.

벽 4. 소스를 보며 분석해 "연습"이 안 된다

증상: 답을 알고 있으니 strings 출력이 안 궁금합니다.
원인: 출제와 분석을 한 사람이 한 것입니다.
해결: 하루 뒤에 하거나, 변형 샘플을 만들 때 조건을 무작위로 섞으세요(문자열 순서, 이름, 개수). 그래도 안 되면 동료와 샘플을 교환하는 것이 실무와 가장 비슷합니다.

벽 5. 실제 샘플을 "한 번만" 실행해 보고 싶은 유혹

증상: "어차피 VM인데" 하고 더블클릭합니다.
원인: 호스트 전용 네트워크가 아닌 VM, 스냅샷 없는 VM, 공유 폴더가 켜진 VM에서의 실행 사고가 실제로 반복됩니다. 악성코드 중에는 VM 탈출 취약점을 쓰는 것도 있습니다.
해결: 실행이 필요하면 그것은 정적 분석이 아니라 동적 분석이고, 전용 샌드박스 절차(네트워크 시뮬레이션, 스냅샷 확인)를 갖춘 뒤의 일입니다. 오늘 챕터의 범위 밖입니다.


7. 정리

오늘의 개념

개념 한 줄 설명
정적 분석 실행 없이 파일을 데이터로 읽는 분석 — 1차는 언제나 이것
동적 분석 샌드박스에서 실행해 행동을 보는 분석 — 정적의 한계를 넘을 때
IoC 침해 지표 — 도메인, 해시, 경로, 뮤텍스 등 탐지·추적에 쓰는 단서
격리 랩 네트워크 차단 + 스냅샷 + 전용 환경 — 샘플을 다루는 최소 안전 장치
임포트 테이블 파일이 외부에 요구하는 함수 목록 — "실제 능력"의 목록
stripped 심볼이 제거된 상태 — 실전 샘플의 기본값, 분석 난이도 상승

오늘의 명령어

명령 하는 일
file 대상 파일 종류, 아키텍처, stripped 여부 확인
sha256sum 대상 지문 채취 — 보고서 신분증, 공개 DB 조회용
strings 대상 ASCII 문자열 추출 — IoC의 광산
strings -e l 대상 UTF-16LE 문자열 추출 — 윈도우 샘플 필수 병행
readelf -h 대상 ELF 헤더 — 타입, 아키텍처, 엔트리
objdump -p 대상 | grep NEEDED 링크 라이브러리 목록
nm -D 대상 동적 심볼 — ELF의 임포트에 해당

명령어보다 중요한 감각

정적 분석은 "독서"지만 목표는 독후감이 아니라 판단 근거입니다. 문자열이 의심을 만들고 임포트가 확신을 주며, 둘이 어긋나면(오늘 샘플처럼) 그 어긋남 자체가 보고서의 내용입니다. 그리고 이 모든 것의 대전제가 격리 랩입니다 — 절차를 지키는 분석가만이 내일도 분석할 수 있습니다. 읽을 수 없는 샘플(패킹, 암호화)을 만나면 억지로 읽지 말고 "왜 안 읽히는가"를 기록하고 다음 단계로 넘기세요. 그 넘기는 판단도 분석입니다.


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