동적 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_hash후add 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)을 기준으로 분석하면 다음 단계가 명확하게 드러납니다.
- LoadLibraryA 호출
- PE Export Table 파싱
- 이름 문자열 반복 → hashing
- 해시 매칭 시 Ordinal → Function RVA → VA 변환
- 반환된 주소 기반의 후속 로직 존재 여부 검사
- 안티디버깅 스텁과 결합 여부 확인
- 이상 동작 탐지 시 샌드박스/메모리 덤프 후 실행 흐름 재구성
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