[태그:] 패킹 PE

  • 📘 Windows PE 로더의 Import Table 동적 복원 분석

    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) 분석

    복원 루틴의 전체 흐름은 다음과 같음:

    1. 스택 프레임 생성
      • push ebp / mov ebp, esp
      • sub esp, 0x40로 로컬 문자열 버퍼 확보
    2. 복호화 루틴 호출
      • 함수 호출 규약: cdecl
      • 매개변수: 목적지 버퍼 → 암호화된 문자열 → 길이
      • 반환값 없음
      • 문자열은 XOR/ROL 등 단순암호로 확인
    3. PEB 접근 기반 동적 API 획득
      • PEB (fs:[0x30]) → LDR → InMemoryOrderModuleList
      • kernel32.dll 베이스 주소 획득
      • Export Directory 파싱
      • 문자열 비교로 LoadLibraryA, GetProcAddress 검색
      • 실제로는 K32 API를 직접 호출하지 않고 자체 구현한 Export 파서 사용
    4. 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-debugPEB BeingDebugged / INT3 체크

    ✨ 마무리 한 줄

    패커의 Import 복원 루틴은 패킹된 PE를 해석하는 첫 관문이며,
    PEB 기반 API 조회와 IAT 패치는 악성코드 분석에서 반복적으로 마주치는 핵심 구조입니다.


    📍 Written by Code & Compass