Packed PE의 Import Table 동적 복원 과정 심층 분석
패킹된 Windows PE는 보통 Import Table을 제거하거나 난독화하여 정적 분석을 어렵게 만들고, 실행 시점에 복원 루틴이 동적으로 API 주소를 다시 조회해 Import Address Table(IAT)에 주입하는 방식은 《리버싱 핵심원리》에서 제시하는 대표적 분석 패턴임.
이번에는 패킹된 PE의 Import Table 재구성 루틴을 디스어셈블리 기반으로 추적하며,
스택 프레임, 레지스터 흐름, 메모리 접근, 제어 흐름, Windows API 동적 로딩 방식을 단계별로 기록.
리버싱 기록
1. 관찰 대상 샘플 개요
샘플은 다음 특성을 가짐:
- 정적 Import Table 없음
- 패킹 루틴 내부에서
LoadLibraryA/GetProcAddress호출 전부 암호화 - 런타임에서 Import Table 구조를 재구성하여
.text내 IAT 슬롯에 직접 기입 - Anti-debugging: 커스텀 PEB 접근
- 실행 흐름: 패커 스텁 → 언패킹 → Import 복원 → OEP 점프
2. Import 복원 루틴의 C 유사 형태(재구성)
샘플에서 디스어셈블리 기반으로 복원한 C 유사 형태는 아래와 같음.
typedef FARPROC (WINAPI *GETPROC)(HMODULE, LPCSTR);
typedef HMODULE (WINAPI *LOADLIB)(LPCSTR);
void RestoreImports() {
// 암호화된 문자열 복원
char dllName[16];
decrypt_string(dllName, enc_dll_name, sizeof(enc_dll_name));
char apiName[32];
decrypt_string(apiName, enc_api_name, sizeof(enc_api_name));
LOADLIB pLoadLibraryA = resolve_api_via_peb("kernel32.dll", "LoadLibraryA");
GETPROC pGetProcAddress = resolve_api_via_peb("kernel32.dll", "GetProcAddress");
HMODULE h = pLoadLibraryA(dllName);
FARPROC fn = pGetProcAddress(h, apiName);
// IAT에 바로 기입
DWORD *iatSlot = (DWORD*)IAT_ENTRY_RVA_TO_VA;
*iatSlot = (DWORD)fn;
}
이 루틴은 패커에서 주로 등장하는 패턴과 동일하며,
핵심은 PEB 접근 → API 주소 획득 → 복호화된 문자열로 DLL/API 로드 → IAT 슬롯 패치.
3. Assembly 분석(Win32, MASM 스타일)
스택과 레지스터 흐름을 명확히 보기 위해 상세 주석 포함.
; eax = 암호화된 DLL 문자열 주소
; esi = 암호화된 API 문자열 주소
; edi = IAT 슬롯 주소
RestoreImports:
push ebp
mov ebp, esp
sub esp, 0x40 ; 로컬 버퍼 64바이트 확보 (dll, api 이름)
; --- 문자열 복호화 단계 ---
lea ecx, [ebp-0x20] ; dllName 버퍼
push sizeof_dll
push enc_dll_name_ptr
push ecx
call decrypt_string
add esp, 0x0C
lea ecx, [ebp-0x40] ; apiName 버퍼
push sizeof_api
push enc_api_name_ptr
push ecx
call decrypt_string
add esp, 0x0C
; --- PEB를 통한 kernel32.dll의 LoadLibraryA 주소 획득 ---
push offset szLoadLibraryA
push offset szKernel32
call resolve_api_via_peb
mov ebx, eax ; EBX = LoadLibraryA
; --- PEB를 통한 GetProcAddress 조회 ---
push offset szGetProcAddress
push offset szKernel32
call resolve_api_via_peb
mov esi, eax ; ESI = GetProcAddress
; --- LoadLibraryA(dllName) ---
lea eax, [ebp-0x20]
push eax
call ebx ; LoadLibraryA
mov edi, eax ; EDI = HMODULE
; --- GetProcAddress(h, apiName) ---
lea eax, [ebp-0x40]
push eax
push edi
call esi ; GetProcAddress
mov ecx, eax ; ECX = 함수 주소
; --- IAT 패치 ---
mov eax, IAT_ENTRY_RVA_TO_VA
mov [eax], ecx ; 실제 IAT 슬롯에 함수 주소 기록
leave
ret
4. 제어 흐름(Control Flow) 분석
복원 루틴의 전체 흐름은 다음과 같음:
- 스택 프레임 생성
push ebp/mov ebp, espsub esp, 0x40로 로컬 문자열 버퍼 확보
- 복호화 루틴 호출
- 함수 호출 규약:
cdecl - 매개변수: 목적지 버퍼 → 암호화된 문자열 → 길이
- 반환값 없음
- 문자열은 XOR/ROL 등 단순암호로 확인
- 함수 호출 규약:
- PEB 접근 기반 동적 API 획득
- PEB (
fs:[0x30]) → LDR → InMemoryOrderModuleList - kernel32.dll 베이스 주소 획득
- Export Directory 파싱
- 문자열 비교로
LoadLibraryA,GetProcAddress검색 - 실제로는 K32 API를 직접 호출하지 않고 자체 구현한 Export 파서 사용
- PEB (
- IAT 패치
- IAT 엔트리 RVA는 패커 스텁에 하드코딩
- Unpacked OEP 진입 전 가장 마지막 단계
5. 메모리 접근 분석
5.1 복호화 루틴 인자
ecx: 목적지 버퍼esp+4: 암호화 문자열 주소esp+8: 길이
모든 문자열은 .data가 아닌 패커가 생성한 데이터 영역에 존재하며,
실행 전까지 접근 불가(페이지 권한 RX → RWX 전환 후 복호화).
5.2 IAT 패치
IAT는 .rdata, .idata가 아닌 패커가 임의로 만든 섹션에 존재.
해당 섹션은:
- 초기 상태: RWX
- 패치 후: 다시 RX로 바꿔 탐지 회피
이는 이른바 self-protection section 패턴으로 잘 알려진 기법.
6. Anti-Debugging 관찰 요소
본 루틴 전후에 존재한 체크:
mov eax, fs:[0x30] ; PEB
mov eax, [eax+0x02] ; BeingDebugged flag
test eax, eax
jnz DebugFound
또한:
call IsDebuggerPresent ; API 기반 체크
xor eax, eax
mov al, [esp+4] ; RET 이후 수정된 스택 검사
cmp eax, 0xCC ; INT3 패치 여부
이런 다단계 anti-debug는 패커가 많이 사용하는 패턴.
7. 분석 포인트 요약
| 분석 항목 | 적용 원리 |
|---|---|
| Import 복원 | PE 구조 / Export Directory 파싱 |
| 복호화 루틴 | 제어 흐름 분석 / 암호화 루틴 복원 |
| API 로딩 | Windows Internals / PEB 구조 |
| 스택 프레임 | 호출 규약(cdecl) / 로컬 메모리 |
| 패커 전형 패턴 | OEP 점프 / IAT 수동 재구성 |
| Anti-debug | PEB BeingDebugged / INT3 체크 |
✨ 마무리 한 줄
패커의 Import 복원 루틴은 패킹된 PE를 해석하는 첫 관문이며,
PEB 기반 API 조회와 IAT 패치는 악성코드 분석에서 반복적으로 마주치는 핵심 구조입니다.
📍 Written by Code & Compass