[태그:] 정보보안

  • 🧩 미니멀 로더

    동적 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

  • 🔧 Process Hollowing

    리버싱 기록


    1. 개요

    Process Hollowing은 정상 프로세스를 생성한 뒤 내부 메모리를 제거하고 악성 페이로드를 삽입하여 실행 흐름을 탈취하는 방식이다.
    특징은 다음과 같다:

    • CreateProcess에서 CREATE_SUSPENDED 사용
    • PEB에서 ImageBaseAddress 파싱
    • NtUnmapViewOfSection 호출
    • 재할당 후 악성 PE 매핑
    • EntryPoint 이동 후 ResumeThread

    악성코드는 흔히 API 해시 기반 동적 API 로딩 기법을 함께 사용한다.


    2. 동적 API 해석 루틴 (C 코드)

    DWORD HashString(char* s) {
        DWORD h = 0;
        while (*s) {
            h = ((h >> 13) | (h << 19));
            h += (*s >= 'A' && *s <= 'Z') ? (*s + 0x20) : *s;
            s++;
        }
        return h;
    }
    
    FARPROC Resolve(DWORD targetHash) {
        PEB* peb = (PEB*)__readgsqword(0x60);
        LIST_ENTRY* mod = peb->Ldr->InMemoryOrderModuleList.Flink;
    
        while (mod) {
            LDR_DATA_TABLE_ENTRY* e = (LDR_DATA_TABLE_ENTRY*)((BYTE*)mod - 0x10);
            BYTE* base = (BYTE*)e->DllBase;
    
            IMAGE_DOS_HEADER* dos = (IMAGE_DOS_HEADER*)base;
            IMAGE_NT_HEADERS* nt = (IMAGE_NT_HEADERS*)(base + dos->e_lfanew);
    
            DWORD expRVA = nt->OptionalHeader.DataDirectory[0].VirtualAddress;
            IMAGE_EXPORT_DIRECTORY* exp = (IMAGE_EXPORT_DIRECTORY*)(base + expRVA);
    
            DWORD* names = (DWORD*)(base + exp->AddressOfNames);
            WORD* ord = (WORD*)(base + exp->AddressOfNameOrdinals);
            DWORD* funcs = (DWORD*)(base + exp->AddressOfFunctions);
    
            for (DWORD i = 0; i < exp->NumberOfNames; i++) {
                char* name = (char*)(base + names[i]);
                if (HashString(name) == targetHash) {
                    return (FARPROC)(base + funcs[ord[i]]);
                }
            }
    
            mod = mod->Flink;
        }
        return NULL;
    }
    

    해석 포인트

    • PEB → Ldr 리스트 순회
    • Export Directory 직접 파싱
    • 해시 비교를 통한 API 은닉
    • 《리버싱 핵심원리》의 PE 구조 분석 절차 그대로 적용됨

    3. Assembly 흐름 (동적 Export 파싱)

    mov     rax, gs:[0x60]           ; RAX = PEB
    mov     rbx, [rax+0x18]          ; Ldr
    mov     rcx, [rbx+0x20]          ; InMemoryOrderModuleList
    mov     rcx, [rcx]               ; Flink = next module
    
    ; Loop:
    mov     rdx, [rcx-0x10]          ; DllBase
    mov     r8,  [rdx+3Ch]           ; e_lfanew
    add     r8,  rdx                 ; NT Header
    
    mov     r9d, [r8+88h]            ; RVA Export Directory
    test    r9d, r9d
    jz      next_module
    
    ; Export 테이블 접근 …
    

    레지스터 변화 설명

    • RAX: PEB base
    • RBX/RCX: Ldr 리스트 이동
    • RDX: ImageBase
    • R8/R9: NT Header / Export Directory

    4. Process Hollowing 전체 흐름

    1. CreateProcess (CREATE_SUSPENDED)
    2. PEB → ImageBaseAddress 찾기
    3. NtUnmapViewOfSection
    4. VirtualAllocEx
    5. WriteProcessMemory
    6. EntryPoint 계산
    7. SetThreadContext
    8. ResumeThread

    5. EntryPoint 재지정 Assembly

    mov     ecx, hThread
    call    GetThreadContext
    mov     rax, [rsp+context.Rcx]
    mov     rdx, new_entry
    add     rdx, remote_base
    mov     [context.Rcx], rdx
    call    SetThreadContext
    call    ResumeThread
    

    포인트

    • RCX/RIP 조작 → 제어 흐름 탈취
    • AddressOfEntryPoint 기반 Entry 이동
    • 스택 프레임과 제어 흐름 규칙은 《리버싱 핵심원리》의 원리 그대로 적용됨

    6. 포렌식 관점의 탐지 지표

    단계흔적지표
    Unmap메모리 공백VAD 흔들림
    AllocImageBase 비정상RWX 상태
    WritePE 구조 잔존DOS/NT 시그니처
    ContextEntryPoint 불일치스레드 흐름 이탈
    APIImport 없음해시 기반 호출 패턴

    7. 정리

    Process Hollowing은
    PE 구조 분석 → 메모리 매핑 → 동적 API 해석 → 제어 흐름 조작
    순서로 리버싱하는 것이 핵심이다.

    📍 Written by Code & Compass

  • 🔍 Anti-Debugging, VM 난독화 구조, CrackMe 예시

    리버싱 기록


    📌 1. Anti-Debugging 심화 분석

    기본적인 IsDebuggerPresent 호출을 넘어서, 실제 크랙미나 악성코드에서 자주 사용하는 안티 디버깅 기법 정리.

    🧩 (1) PEB 직접 접근 기반 Anti-Debugging

    PEB 구조체 내부의 BeingDebugged 플래그를 직접 읽어서 디버거 여부 확인하는 방식.

    #include <stdio.h>
    
    int main() {
        unsigned char *peb = (unsigned char*)__readfsdword(0x30);
        unsigned char BeingDebugged = peb[2]; // PEB + 0x2
    
        if (BeingDebugged)
            printf("Debugger detected via PEB check!\n");
        else
            printf("No debugger detected.\n");
    }
    

    요약

    • __readfsdword(0x30) → x86 환경에서 PEB 주소 얻는 방식
    • peb[2] = BeingDebugged 플래그
    • 값이 1이면 디버거 존재

    ASM 흐름

    mov     eax, fs:[30h]     
    mov     al, [eax+2]       
    test    al, al
    jnz     debugger_found
    

    분석 관점

    • [eax+2] 값을 0으로 패치하는 방식
    • 또는 jnz → jz 반전

    🧩 (2) NtQueryInformationProcess 기반 탐지

    Native API를 이용해 디버그 플래그 확인하는 방식.

    #include <Windows.h>
    #include <winternl.h>
    
    typedef NTSTATUS (WINAPI *PFN)(HANDLE, PROCESSINFOCLASS, PVOID, ULONG, PULONG);
    
    int main() {
        PFN NtQueryInformationProcess =
            (PFN)GetProcAddress(GetModuleHandleA("ntdll"), "NtQueryInformationProcess");
    
        DWORD flags = 0;
        NtQueryInformationProcess(GetCurrentProcess(),
                                  0x1f,
                                  &flags, sizeof(flags), NULL);
    
        if (flags == 0)
            printf("Debugger detected (DebugFlags == 0)\n");
        else
            printf("No debugger detected.\n");
    }
    

    요약

    • ProcessDebugFlags = 0 → 디버거 존재
    • 값을 강제로 1로 패치하는 방식

    🧩 (3) Timing 기반 Anti-Debugging

    #include <Windows.h>
    
    int main() {
        DWORD t1 = GetTickCount();
        for (volatile int i = 0; i < 10000000; i++);
        DWORD t2 = GetTickCount();
    
        if ((t2 - t1) > 50)
            printf("Debugger likely detected.\n");
        else
            printf("Normal execution.\n");
    }
    

    요약

    • 디버깅 시 루프 실행 시간 증가 → threshold 이상이면 탐지
    • 타이밍 비교문 패치 방식

    📌 2. VM 기반 난독화 구조 분석

    코드를 직접 실행하지 않고 VM 명령 집합(bytecode)으로 변환하여 핸들러 위에서 실행하는 방식. 난이도 높은 난독화 기법.

    🧩 (1) VM Dispatcher 구조

    VM_Dispatch:
        movzx   eax, byte [esi]
        inc     esi
        jmp     [HandlerTable + eax*4]
    

    요약

    • esi = VM 바이트코드 포인터
    • opcode = [esi]
    • HandlerTable[opcode] → 해당 연산 핸들러로 점프

    🧩 (2) VM Handler 예시

    Handler_ADD:
        mov     edx, [stack_top]
        mov     ecx, [stack_top - 4]
        add     ecx, edx
        mov     [stack_top - 4], ecx
        sub     stack_top, 4
        jmp     VM_Dispatch
    

    요약

    • 스택 상단 두 값 pop → add 연산
    • 결과를 push하는 구조
    • Dispatcher로 복귀

    분석 관점

    • 핸들러 기능을 테이블화하여 의미 매핑
    • 바이트코드를 순차 분석하여 원래 로직 복원

    📌 3. CrackMe 예시

    🔍 (1) Anti-Debugging 우회

    CALL    IsDebuggerPresent
    TEST    EAX, EAX
    JNZ     fail
    

    요약

    • EAX != 0 → 디버거 탐지
    • JNZ → JZ 패치 방식

    🔍 (2) Serial 검증 루틴 분석

    xor     eax, eax
    mov     ecx, [input]
    
    xor_loop:
        mov     dl, [ecx]
        test    dl, dl
        jz      check_end
        xor     al, dl
        inc     ecx
        jmp     xor_loop
    
    check_end:
        cmp     al, 3Ch
        je      correct
        jmp     fail
    

    요약

    • 문자 반복 XOR 누적 방식
    • 결과값 == 0x3C → correct

    ✔ 예시 정답

    ABCDEF<
    

    정리:

    • Anti-Debugging은 API 검사 → PEB → 타이밍 검사 순으로 고도화
    • VM 난독화는 Dispatcher/Handler 구조 파악이 핵심
    • CrackMe는 작성자의 의도 파악 중심 분석

    📍Written by Code & Compass