[태그:] 미니멀 로더

  • 🧩 미니멀 로더

    동적 API 해석(dynamic API resolution) 패턴과, 그 과정에서 나타나는 PE 구조 기반 탐색, 호출 규약, 레지스터 사용 방식, 그리고 기본적인 안티디버깅 흐름을 파악하는 내용.

    리버싱 기록


    1. 개요

    일부 악성 로더는 Import Table 을 이용하지 않고 커스텀 PE 파서를 통해 필요한 API 주소를 직접 획득합니다.
    이 패턴은 정적 분석을 방해하고, 단순한 문자열 기반 탐지를 우회하기 위해 흔히 사용됩니다.

    • 고정된 DLL 이름 문자열
    • API 이름 Hashing
    • 커스텀 주소 탐색 루틴
    • 무해한 “placeholder” 호출

    2. C 기반 로더 구조(비활성/무해 예시)

    아래 코드는 실제 악성 기능이 제거된 안전한 구조 예시입니다.
    분석 패턴 연구 목적이며, 실행해도 아무 작업도 하지 않습니다.

    #include <stdint.h>
    #include <windows.h>
    
    typedef FARPROC(WINAPI* ResolveFn)(HMODULE, const char*);
    
    // 단순 문자열 hash (무해)
    uint32_t simple_hash(const char* s) {
        uint32_t h = 0;
        while (*s) {
            h = (h << 5) - h + (unsigned char)*s++;
        }
        return h;
    }
    
    // 예시: Export Table을 순회해 API 주소를 가져오는 패턴
    FARPROC resolve_api(HMODULE mod, uint32_t target_hash) {
        uint8_t* base = (uint8_t*)mod;
        IMAGE_DOS_HEADER* dos = (IMAGE_DOS_HEADER*)base;
        IMAGE_NT_HEADERS* nt = (IMAGE_NT_HEADERS*)(base + dos->e_lfanew);
    
        IMAGE_EXPORT_DIRECTORY* exp = (IMAGE_EXPORT_DIRECTORY*)(
            base + nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress
        );
    
        uint32_t* names = (uint32_t*)(base + exp->AddressOfNames);
        uint16_t* ords = (uint16_t*)(base + exp->AddressOfNameOrdinals);
        uint32_t* funcs = (uint32_t*)(base + exp->AddressOfFunctions);
    
        for (uint32_t i = 0; i < exp->NumberOfNames; i++) {
            const char* api_name = (const char*)(base + names[i]);
            uint32_t h = simple_hash(api_name);
    
            if (h == target_hash) {
                uint16_t ord = ords[i];
                return (FARPROC)(base + funcs[ord]);
            }
        }
    
        return NULL;
    }
    
    int main() {
        HMODULE k32 = LoadLibraryA("kernel32.dll");
        if (!k32) return 0;
    
        // 예: “GetTickCount” 해시값
        uint32_t hash = simple_hash("GetTickCount");
    
        FARPROC api = resolve_api(k32, hash);
        if (api) {
            // 실제 호출하지 않고 주소만 확인
            // ((DWORD(WINAPI*)())api)();  // 비활성화
        }
        return 0;
    }
    

    3. 어셈블리 레벨 제어 흐름 분석

    아래는 핵심 루틴인 resolve_api 내부 반복 구조를 디스어셈블한 형태이며,
    레지스터의 역할과 스택 프레임의 변화를 상세하게 설명합니다.

    loc_export_loop:
        mov     eax, [esi]            ; EAX = current RVA of API name
        add     eax, ebx              ; EBX = base, so EAX = VA(name)
        push    eax                   ; push(api_name)
        call    simple_hash
        add     esp, 4                ; clean args
        cmp     eax, edi              ; target_hash in EDI?
        jne     short next_api
    
        ; If matched:
        mov     cx, [eds + ecx*2]     ; fetch ordinal
        mov     edx, [funcs + ecx*4]  ; fetch RVA
        add     edx, ebx              ; convert RVA → VA
        mov     eax, edx              ; return address
        jmp     end_resolve
    
    next_api:
        inc     ecx
        cmp     ecx, [number_of_names]
        jb      loc_export_loop
    
    end_resolve:
        ret
    

    🧷 레지스터 동작 해설

    • EBX: 모듈 베이스 주소
    • ECX: 반복 인덱스(i)
    • EDI: 목표 해시 값
    • EDI == EAX 비교: API 이름 hash 비교
    • EAX: 현재 계산된 hash 또는 반환 주소

    📦 스택 프레임 관찰

    • 호출 규약은 단순 C 선언이므로 caller-cleaned, call simple_hashadd esp, 4 수행
    • recursion이나 frame pointer 사용 없음
    • 지역 변수는 레지스터 기반으로 처리됨

    4. 간단한 안티디버깅 관찰(비활성·무해 예시)

    일부 로더는 API 동적 탐색과 함께 디버거 존재 여부를 검사합니다.

    BOOL anti_debug_stub() {
        __asm {
            mov     eax, fs:[0x30]          ; PEB
            mov     al, [eax+2]             ; BeingDebugged
            ; al == 0이면 미디버깅 상태
            ; 실제 동작 제거
        }
        return FALSE;
    }
    

    이 패턴은 PEB 접근 → 디버깅 여부 확인 → 조건 분기 흐름


    5. 포렌식 관찰 포인트

    데이터 흐름(Data Flow)을 기준으로 분석하면 다음 단계가 명확하게 드러납니다.

    1. LoadLibraryA 호출
    2. PE Export Table 파싱
    3. 이름 문자열 반복 → hashing
    4. 해시 매칭 시 Ordinal → Function RVA → VA 변환
    5. 반환된 주소 기반의 후속 로직 존재 여부 검사
    6. 안티디버깅 스텁과 결합 여부 확인
    7. 이상 동작 탐지 시 샌드박스/메모리 덤프 후 실행 흐름 재구성

    6. 정리

    • PE 구조: Export Directory Table, Name/Ordinal/Function 배열 관계
    • 스택 프레임: Caller-cleaned 호출 규약의 ESP 조정
    • 디스어셈블 패턴: 이름 반복 루프, RVA→VA 변환
    • 제어 흐름 분석: 해시 비교 → 분기 → 조기 종료
    • API 후킹 대응: IAT 미사용으로 후킹 우회
    • 암호화 복원 패턴: Hash 기반 문자열 매핑
    • 동적 로딩 서명: LoadLibraryA + 커스텀 resolver

    7. 마무리

    동적 API 패턴은 단순해 보이지만, PE 구조와 제어 흐름을 깊이 이해할수록 그 안에서 드러나는 ‘로더의 의도’를 더욱 명확히 포착할 수 있습니다.


    📍 Written by Code & Compass