hhkb.dev

BLOG

2025-12-28 / 악성코드 · 03

패킹 메커니즘과 수동 언패킹 원리

1. 패킹(Packing)과 프로텍터(Protector)

1.1. 패킹의 원리

패킹은 흔히 압축처럼 보이지만, 실제로는 실행 파일의 PE 구조를 다시 감싸서 원본 코드를 숨기는 방식에 가깝다. 단순히 용량을 줄이는 기술이라기보다, 원본 코드를 별도 컨테이너 안에 넣어두고 실행 시점에 다시 꺼내 쓰는 구조로 이해하는 편이 맞다.

패킹된 악성코드가 실행될 때는 보통 아래 순서로 제어권이 넘어간다.

  1. 초기 진입(Entry Point): 운영체제 로더는 원본 코드가 아니라 언패킹 스터브(Unpacking Stub)를 먼저 실행한다. 이 시점의 원본 코드는 아직 암호화된 데이터 덩어리에 가깝다.
  2. 복원 과정(Restoration): 스터브가 압축 데이터 영역을 읽어 디코딩하고, 메모리에 원본 기계어 코드를 풀어놓는다.
  3. 임포트 테이블 복구(IAT Resolution): LoadLibraryGetProcAddress를 이용해 원본 프로그램에 필요한 API 주소를 채워 넣는다.
  4. OEP 이동(Jump to OEP): 준비가 끝나면 스터브는 OEP(Original Entry Point)로 점프하고, 그때부터 실제 프로그램이 실행된다.

1.2. 실행 압축과 프로텍터의 차이

초기 패커(Packer), 예를 들어 UPX는 실행 압축이 주목적이라 스터브 구조가 단순하고 복구도 쉬웠다. 하지만 분석 방해 목적이 강해지면서 더 복잡한 프로텍터(Protector)가 등장했다.

구분패커(Packer)프로텍터(Protector)
대표 도구UPX, ASPack, FSGThemida, VMProtect, Enigma
핵심 목적실행 파일 크기 감소지적재산권 보호 및 분석 방해
기술적 차이단순 압축 알고리즘(LZMA 등) 사용안티 디버깅, 가상 머신 탐지, 코드 가상화, 다형성 등을 함께 적용

프로텍터는 단순히 코드를 숨기는 데서 끝나지 않는다. 디버거가 붙으면 스스로 종료하거나, 가짜 코드를 실행해 분석가를 헷갈리게 만들기도 한다.

2. 정적 분석: 디버거를 켜기 전에 알 수 있는 것들

파일을 실행해보지 않고 PE 헤더만 훑어봐도 "이거 뭔가 이상한데?"라고 느낄 수 있는 몇 가지 강력한 시그니처들이 있다.

2.1. 기형적인 섹션(Section) 구조

정상적인 컴파일러(MSVC, GCC)는 .text, .data 같은 표준 이름과 무난한 크기를 가진다. 반면 패커가 만든 파일은 섹션 구조부터 이상한 경우가 많다.

보통 아래 특징이 먼저 눈에 띈다.

  • 이름의 흔적: UPX0, UPX1처럼 패커 이름을 노출하거나, .vmp 같은 프로텍터 시그니처가 남는다. 이름이 아예 비어 있는 섹션이 있기도 하다.
  • 크기의 불일치 (Raw vs Virtual): 파일 상태에서는 작게 압축되어 있지만, 메모리에서는 압축이 풀릴 큰 공간이 필요하다.
    • SizeOfRawData: 0 또는 매우 작음
    • VirtualSize: 매우 큼
  • 위험한 권한 (Characteristics): 데이터 섹션인데도 쓰기(Write)와 실행(Execute) 권한을 동시에 가지면, 런타임에 코드를 풀어서 실행하려는 의도를 의심할 수 있다.

2.2. 텅 빈 임포트 테이블(IAT)

정상 파일이라면 수십 개 API를 임포트하는 편이 자연스럽다. 하지만 패킹된 파일의 IAT는 스터브가 동작하는 데 필요한 최소한만 남겨두는 경우가 많다. 대표적으로 LoadLibraryGetProcAddress만 보이고, 나머지 API는 실행 중 동적으로 로드된다.

2.3. 엔트로피(Entropy) 경고

Detect It Easy(DIE) 같은 도구로 봤을 때 그래프가 전반적으로 붉게 뜨면 압축이나 암호화를 의심해야 한다.

엔트로피 값은 보통 이렇게 해석한다.

  • 3~5점: 일반적인 코드. 어느 정도 패턴이 보인다.
  • 7점 이상: 압축되거나 암호화된 데이터. 난수에 가깝다.

3. 실전: UPX 수동 언패킹 가이드

자동 툴이 통하지 않을 때를 대비해서, x64dbg로 직접 껍질을 벗기는 수동 언패킹 흐름도 익혀둘 필요가 있다. 여기서는 가장 기초적인 ESP Trick을 기준으로 본다.

3.1. PUSHAD 찾기

파일을 열자마자 첫 명령어가 PUSHAD라면 실마리를 잡은 셈이다.

PUSHAD는 현재 레지스터 상태를 스택에 백업하는 명령어다. 스터브가 레지스터를 마음껏 쓴 뒤, 나중에 POPAD로 원상 복구하고 OEP로 넘겨주겠다는 뜻이기도 하다. 그래서 이 복구 시점이 언패킹 종료 타이밍이 된다.

3.2. ESP Trick 설정 (Hardware Breakpoint)

설정 순서는 단순하다.

  1. F7이나 F8PUSHAD를 실행한다.
  2. 스택 창(Stack View)에서 ESP 레지스터가 가리키는 주소를 찾는다.
  3. 그 주소에 하드웨어 접근 중단점(Hardware Breakpoint on Access)을 건다.

3.3. OEP 포착

F9로 실행하면 복잡한 압축 해제 루프를 지나 POPAD 직후에서 멈추는 경우가 많다.

이때 바로 아래에 Tail Jump(JMP 또는 RET)가 보이면 확인할 가치가 있다. 아주 멀리 떨어진 주소로 점프한다면, 그 지점이 OEP일 가능성이 높다.

3.4. 덤프 및 IAT 복구 (Dump & Fix)

OEP에 도착하면 코드가 정상적인 함수 프롤로그(PUSH EBP...) 형태로 바뀌는 경우가 많다. 그다음에는 Scylla 같은 도구로 현재 메모리 상태를 덤프하고, 손상된 IAT를 복구하면 다시 분석 가능한 실행 파일을 만들 수 있다.