[태그:] 리버싱

  • 📘 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

  • 🧩 미니멀 로더

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

  • 🔍 PE 파일의 Import Table 재구성 흐름

    악성코드나 패커가 적용된 실행 파일을 분석하다 보면,
    가장 먼저 무너져 있는 부분이 Import Table입니다.
    오늘은 Import Table이 훼손된 PE 파일을 어떻게 재구성하는지,
    그리고 악성코드가 어떤 방식으로 Import를 숨기는지 기록 중심으로 정리해봤습니다.

    리버싱 기록


    ## 1) 📌 동적 로딩 기반 Import 은닉 기법

    대표적으로 사용하는 API는 다음과 같습니다:

    • LoadLibraryA / LoadLibraryW
    • GetProcAddress
    • LdrLoadDll
    • LdrGetProcedureAddress

    악성코드에서 많이 보이는 전형적인 C 구조는 아래와 같습니다.


    ## 2) 🔧 C 코드 예시 — API 해시 기반 동적 로딩

    DWORD hash(char* s) {
        DWORD h = 0;
        while (*s) {
            h = ((h << 5) + h) + *s;
            s++;
        }
        return h;
    }
    
    FARPROC resolve(const char* dll, DWORD func_hash) {
        HMODULE mod = LoadLibraryA(dll);
        if (!mod) return NULL;
    
        PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)mod;
        PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)((BYTE*)mod + dos->e_lfanew);
    
        DWORD rva = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
        PIMAGE_EXPORT_DIRECTORY exp = (PIMAGE_EXPORT_DIRECTORY)((BYTE*)mod + rva);
    
        DWORD* names = (DWORD*)((BYTE*)mod + exp->AddressOfNames);
        WORD* ord   = (WORD*)((BYTE*)mod + exp->AddressOfNameOrdinals);
        DWORD* funcs = (DWORD*)((BYTE*)mod + exp->AddressOfFunctions);
    
        for (DWORD i = 0; i < exp->NumberOfNames; i++) {
            char* name = (char*)mod + names[i];
            if (hash(name) == func_hash) {
                return (FARPROC)((BYTE*)mod + funcs[ord[i]]);
            }
        }
        return NULL;
    }
    

    🔍 상세 분석

    • hash()는 이름 기반 해시 생성
    • Export Directory를 직접 순회
    • 문자 비교 대신 해시 비교로 탐지 회피
    • 실제 악성코드에서 가장 흔하게 쓰임
    • 정적 분석 도구가 Import를 재구성하기 어렵게 만드는 구조

    ## 3) 🔧 Assembly 예시 — Export 구조 직접 참조

    mov     ebx, [LoadLibraryA]
    push    offset dll_name
    call    ebx
    
    mov     esi, eax                ; module base
    mov     edi, [esi+3Ch]          ; e_lfanew
    add     edi, esi                ; PE header
    
    mov     eax, [edi+78h]          ; RVA of export directory
    add     eax, esi                ; export directory VA
    
    mov     ecx, [eax+18h]          ; NumberOfNames
    mov     edx, [eax+20h]          ; AddressOfNames
    add     edx, esi
    
    next_name:
        mov     ebx, [edx]
        add     ebx, esi            ; name address
        push    ebx
        call    hash_function
        cmp     eax, target_hash
        je      found
        add     edx, 4
        loop    next_name
    

    🔍 레지스터 기반 상세 설명

    • esi = 모듈 베이스
    • edi = NT Header 시작
    • eax = Export Directory 주소
    • ecx = 이름 개수
    • API 이름 하나씩 순회하며 해시 비교
    • 실제 패커들이 가장 많이 사용하는 방식

    ## 4) 📌 Import Table 재구성 흐름 (Forensics 관점)

    악성코드 분석 시 Import Table이 파괴된 경우,
    보통 아래 단계로 복원합니다.

    1. 실행 파일 전체에 BP on GetProcAddress
    2. 실행 중 호출되는 실제 API 목록 수집
    3. 호출 순서·DLL 매핑 기록
    4. Scylla, ImportREC 등으로 Import Table 재생성
    5. 패커 제거 후 정적 분석 진행

    📍 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