Windows Shellcode의 해시 기반 동적 API 호출 기법

이번 글은 실제 악성코드에서 가장 빈번하게 사용되는
해시 기반 동적 API 호출 기법(Hash-Based Dynamic API Invocation Technique)을 정리함.

이 기법은 다음 목적을 위해 활용된다:

  • IAT 없이 API 호출 — 정적 분석 회피
  • API 이름 문자열 제거 — 시그니처 탐지 우회
  • 쉘코드·패커·언패킹 루틴 등에서 환경 독립적 호출 구조 확보
  • 샌드박스·에뮬레이터 우회

리버싱 기록


1. 개요 — 왜 “해시 기반 동적 API 호출 기법”인가?

일반 PE는 Import Table(IAT)을 통해 필요한 API를 정적으로 가져오지만,
악성코드 및 Shellcode는 다음 이유로 IAT를 회피하여 해시 기반 호출을 사용한다.

  • 악성코드 분석 시 IAT만 보면 기능이 그대로 드러남
  • Shellcode는 PE 구조가 아니라서 IAT 자체가 존재하지 않음
  • 패킹된 샘플은 언패킹 전까지 API 주소를 알 수 없음
  • API 이름 문자열을 제거해 탐지 난이도를 높임

→ 따라서 API 이름을 해시값만으로 저장하고,
실행 시 PEB → 모듈 → Export Table 순회로 API 주소를 직접 계산해 호출한다.

이를 전체적으로 “해시 기반 동적 API 호출 기법”이라 부른다.


2. PEB 기반 모듈 검색: 호출 기법의 첫 단계

이 기법의 구조적 시작점은 항상 PEB(Process Environment Block) 이다.
Shellcode는 보통 다음 순서로 모듈 위치를 찾는다.

  1. FS:[0x30] → PEB
  2. PEB->Ldr
  3. LDR_DATA_TABLE_ENTRY 리스트 순회
  4. kernel32.dll, ntdll.dll 등 모듈 이름 해시 비교
  5. 일치 시 Export Table 탐색 시작

C 의사코드

PPEB get_peb() {
    return (PPEB)__readfsdword(0x30);
}

어셈블리

mov eax, fs:[0x30]   ; eax = PEB

3. 해시 기반 동적 API 호출 기법의 핵심 구조

해시 기반 호출은 다음 세 가지 루틴으로 구성된다.

  1. 문자열 해시 계산 루틴
  2. Export Table 순회 & 해시 비교 루틴
  3. 함수 주소 계산 & 호출

다음은 1~3 항목에 대한 내용이다.


4. 문자열 해시 계산 루틴

가장 흔한 구현은 djb2 변형, ROR/ROL 기반, XOR 기반 해시다.

C 의사코드

DWORD hash_string_ansi(const char* s) {
    DWORD h = 0;
    while (*s) {
        char c = *s++;
        if (c >= 'a' && c <= 'z')
            c -= 0x20;  // uppercase
        h = ((h << 5) + h) + c;  // djb2 변형
    }
    return h;
}

어셈블리

push ebp
mov  ebp, esp
push ebx

xor  eax, eax
mov  edx, [ebp+8]   ; s

hash_loop:
mov  bl, [edx]
cmp  bl, 0
je   hash_done

; lowercase → uppercase
cmp  bl, 'a'
jl   skip
cmp  bl, 'z'
jg   skip
sub  bl, 0x20
skip:

inc  edx
mov  ecx, eax
shl  eax, 5
add  eax, ecx
add  eax, ebx

jmp  hash_loop

hash_done:
pop  ebx
pop  ebp
ret  4

5. Export Table 순회 및 해시 기반 함수 검색

해시 기반 호출 기법의 핵심은
API 이름 문자열 없이 해당 API 주소를 Export Table에서 직접 찾는 것이다.

C 의사코드

FARPROC resolve_function_by_hash(HMODULE mod, DWORD target_hash) {
    IMAGE_DOS_HEADER* dos = (IMAGE_DOS_HEADER*)mod;
    IMAGE_NT_HEADERS* nt = (IMAGE_NT_HEADERS*)((BYTE*)mod + dos->e_lfanew);

    DWORD expRVA = nt->OptionalHeader
        .DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
    IMAGE_EXPORT_DIRECTORY* exp =
        (IMAGE_EXPORT_DIRECTORY*)((BYTE*)mod + expRVA);

    DWORD* names  = (DWORD*)((BYTE*)mod + exp->AddressOfNames);
    WORD*  ords   = (WORD*)((BYTE*)mod + exp->AddressOfNameOrdinals);
    DWORD* funcs  = (DWORD*)((BYTE*)mod + exp->AddressOfFunctions);

    for (DWORD i = 0; i < exp->NumberOfNames; i++) {
        const char* nm = (const char*)mod + names[i];
        if (hash_string_ansi(nm) == target_hash) {
            WORD o = ords[i];
            DWORD r = funcs[o];
            return (FARPROC)((BYTE*)mod + r);
        }
    }
    return NULL;
}

어셈블리 패턴

mov  eax, [esi+3Ch]   ; e_lfanew
add  eax, esi         ; NT Headers
mov  ecx, [eax+78h]   ; Export Directory RVA
add  ecx, esi         ; pExport

mov  edx, [ecx+20h]   ; AddressOfNames RVA
add  edx, esi         ; pNames

이 구조는 거의 모든 악성 Shellcode/패커/드로퍼에서 동일한 패턴을 보인다.


6. 해시 기반 동적 API 호출의 실제 호출 방식

함수 주소를 획득한 뒤, Shellcode는 다음 방식으로 바로 호출한다.

C 의사코드

typedef LPVOID (WINAPI *pVirtualAlloc)(LPVOID, SIZE_T, DWORD, DWORD);

pVirtualAlloc va = (pVirtualAlloc)resolve_function_by_hash(k32, HASH_VIRTUALALLOC);
LPVOID mem = va(NULL, 0x2000, MEM_COMMIT, PAGE_EXECUTE_READWRITE);

어셈블리 패턴

push 40h          ; PAGE_EXECUTE_READWRITE
push 1000h        ; MEM_COMMIT
push 2000h        ; size
push 0            ; lpAddress
call eax          ; eax = VirtualAlloc resolved address

7. 악성코드에서 해시 기반 호출을 사용하는 이유

목적설명
정적 분석 회피Import Table을 비워두어 분석 난도 증가
문자열 제거함수 이름을 문자열 대신 해시값으로 보관
패커/언패커 최적화새 환경에서도 API 주소 자동 획득
샌드박스 우회일부 자동화 엔진은 해시 기반 호출을 트래킹하지 못함
쉘코드 호환성PE가 아니어도 API 호출 가능

결과적으로 탐지 난이도가 높고, 분석자가 직접 흐름을 추적해야 한다.


8. 해시 기반 호출 기법에서 흔히 보이는 부가 기능

8.1 안티디버깅

mov eax, fs:[0x30]
mov al, [eax+2]
test al, al
jnz  debugger_present

8.2 언패킹 전용 메모리 조작

xor_loop:
xor [edi], al
inc edi
dec ecx
jnz xor_loop

8.3 API 해시 테이블 존재

데이터 섹션에 다음처럼 나열된 DWORD가 보이면 의심:

EC0E4E8E
7C0DFCAA
5D1A902B
1A2B3C4D

9. 리버싱 관점의 분석 전략

  1. 해시 루틴 먼저 찾기
    • 문자열을 한 글자씩 읽는 루프가 핵심 시그니처
  2. Export Table 접근 코드 식별
    • e_lfanew, 78h, AddressOfNames 등 고정 오프셋 활용
  3. 해시값 목록 수집 → 실제 API 매핑
  4. 해시 기반 호출이 있는 경우 → Shellcode/Packer 1단계 의심
  5. PEB 접근 패턴이 보이면 → 동적 로딩 루틴 확률 ↑

10. 결론

해시 기반 동적 API 호출 기법은 악성코드에서 가장 널리 사용되는 스텔스 호출 방식이다.
IAT를 제거하고 Export Table에서 직접 API를 계산하여 호출하기 때문에
정적 분석·샌드박스·시그니처 기반 탐지를 어렵게 만든다.

이 글에서 다룬 분석 절차(PEB 구조, Export Table, 디스어셈블리 패턴 추적)를
기본 틀로 익혀두면, 대부분의 Shellcode/패커 초기 스텁을 빠르게 해석할 수 있다.


📍 Written by Code & Compass

MalClown에서 더 알아보기

지금 구독하여 계속 읽고 전체 아카이브에 액세스하세요.

계속 읽기