tmxklab

Dropper 3-2(Process Hollowing) 본문

Security/07 Malware Technique

Dropper 3-2(Process Hollowing)

tmxk4221 2020. 12. 29. 12:05

0. 목차


1. Process Hollowing이란

  • Process Replacement, RunPE, Process Injection등 다양한 이름으로 불림
  • 악성코드가 대상 프로세스를 멈춤 상태로 실행 시킨 다음 악성코드 자신을 Injection하는 방식으로 진행
  • Injection 이후에는 대상 프로세스 실행 상태로 변경하여 악성코드를 실행

+) NtUnmapViewOfSection은 선택 사항 +) LoadLibrary(), CreateRemoteThread()를 사용하지 않음

+) 많이 사용하는 기법이라고 한다.

주요 흐름)

CreateProcessNtUnmapViewOfSectionVirtualAllocWriteProcessMemorySetContextThreadResumeThread


2. Process Hollowing 유형 분석

2.1 분석 환경

  • OS : Windows 7(x86)
  • Debugging Tool : IDA Pro, x32dbg, Windbg
  • etc Tool : VMMap, ProcessExplorer, PE View

2.2 예제 소스 코드 및 컴파일

[ hollow.c ]

#include <windows.h>
#include <winternl.h>

EXTERN_C NTSTATUS NTAPI NtTerminateProcess(HANDLE, NTSTATUS);
EXTERN_C NTSTATUS NTAPI NtReadVirtualMemory(HANDLE, PVOID, PVOID, ULONG, PULONG);
EXTERN_C NTSTATUS NTAPI NtWriteVirtualMemory(HANDLE, PVOID, PVOID, ULONG, PULONG);
EXTERN_C NTSTATUS NTAPI NtGetContextThread(HANDLE, PCONTEXT);
EXTERN_C NTSTATUS NTAPI NtSetContextThread(HANDLE, PCONTEXT);
EXTERN_C NTSTATUS NTAPI NtUnmapViewOfSection(HANDLE, PVOID);
EXTERN_C NTSTATUS NTAPI NtResumeThread(HANDLE, PULONG);

unsigned int _strlen(const char *f)
{
    INT i = 0;
    while (*f++)
        i++;
    return i;
}

void printf(char *fmtstr, ...)
{
    DWORD dwRet;
    CHAR buffer[256];
    va_list v1;
    va_start(v1, fmtstr);
    wvsprintfA(buffer, fmtstr, v1);
    WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE), buffer, _strlen(buffer), &dwRet, 0);
    va_end(v1);
}

void *_memset(void *b, int c, int len)
{
    int i;
    unsigned char *p = (unsigned char *)b;
    i = 0;
    while (len > 0)
    {
        *p = c;
        p++;
        len--;
    }
    return (b);
}

int main(int argc, char *argv[])
{

    PIMAGE_DOS_HEADER pDosH;
    PIMAGE_NT_HEADERS pNtH;
    PIMAGE_SECTION_HEADER pSecH;
    DWORD i, nSizeOfFile, read;
    PVOID image, mem, base;
    PROCESS_INFORMATION pi;
    STARTUPINFO si;
    CONTEXT ctx;
    ctx.ContextFlags = CONTEXT_FULL;
    HANDLE hFile;
    char *dst = "msg.exe";

    _memset(&si, 0, sizeof(si));
    _memset(&pi, 0, sizeof(pi));

    CreateProcessA(
        NULL, "explorer.exe", NULL,
        NULL, NULL, CREATE_SUSPENDED, NULL,
        NULL, &si, &pi);

    printf("%s\n", dst);

    hFile = CreateFileA(dst, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
    nSizeOfFile = GetFileSize(hFile, NULL);
    image = VirtualAlloc(NULL, nSizeOfFile, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    ReadFile(hFile, image, nSizeOfFile, &read, NULL);
    CloseHandle(hFile);
    printf("read\n");

    pDosH = (PIMAGE_DOS_HEADER)image;
    pNtH = (PIMAGE_NT_HEADERS)((LPBYTE)image + pDosH->e_lfanew);

    NtGetContextThread(pi.hThread, &ctx);
    NtReadVirtualMemory(pi.hProcess, (PVOID)(ctx.Ebx + 8), &base, sizeof(PVOID), NULL);
    if ((SIZE_T)base == pNtH->OptionalHeader.ImageBase)
    {
        NtUnmapViewOfSection(pi.hProcess, base);
    }
    mem = VirtualAllocEx(pi.hProcess, (PVOID)pNtH->OptionalHeader.ImageBase,
                         pNtH->OptionalHeader.SizeOfImage,
                         MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    printf("\nMemory allocated. Address: %x\n", (SIZE_T)mem);

    printf("\nWriting executable image into child process.\n");
    NtWriteVirtualMemory(pi.hProcess, mem, image, pNtH->OptionalHeader.SizeOfHeaders, NULL);
    for (i = 0; i < pNtH->FileHeader.NumberOfSections; i++)
    {
        pSecH = (PIMAGE_SECTION_HEADER)((LPBYTE)image + pDosH->e_lfanew +
                                        sizeof(IMAGE_NT_HEADERS) + (i * sizeof(IMAGE_SECTION_HEADER)));
        NtWriteVirtualMemory(pi.hProcess, (PVOID)((LPBYTE)mem + pSecH->VirtualAddress),
                             (PVOID)((LPBYTE)image + pSecH->PointerToRawData), pSecH->SizeOfRawData, NULL);
    }
    ctx.Eax = (SIZE_T)((LPBYTE)mem + pNtH->OptionalHeader.AddressOfEntryPoint);
    NtWriteVirtualMemory(pi.hProcess, (PVOID)(ctx.Ebx + (sizeof(SIZE_T) * 2)),
                         &pNtH->OptionalHeader.ImageBase, sizeof(PVOID), NULL);
    NtSetContextThread(pi.hThread, &ctx);
    ResumeThread(pi.hThread);
    return 0;
}
//https://github.com/idan1288/ProcessHollowing32-64/blob/master/ProcessHollowing/ProcessHollowing.c

[ build.bat ]

@ECHO OFF
WHERE cl
IF %ERRORLEVEL% NEQ 0 (
    call "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\Common7\Tools\VsDevCmd.bat"
)
cl /c /GS- hollow.cpp
link /ENTRY:main /subsystem:console hollow.obj ntdll.lib user32.lib kernel32.lib 
del *.obj


3. 분석

3.1 정적 분석

  • 모르는 함수들도 보이고 복잡하다. 차근차근 분석해보자

1) CreateProcessA()

→ 윈도우에서 새로운 프로세스를 실행시키는 함수

주요 멤버 확인) - 아래 변수를 제외한 나머지 변수에 널 값으로 입력해도 됨

  • lpApplicationName : 실행 파일명(반드시 확장자 지정)
  • lpCommandLine : 명령행 인수(확장자 생략시 .exe)
    • lpApplicationName가 널 값이라면 lpCommandLine을 통해 전달되는 문자열의 첫 번째 토큰이 실행하고자 하는 프로그램의 파일명
  • dwCreationFlags : 우선 순위 클래스와 프로세스 생성을 제어하는 플래그
    • CREATE_SUSPENDED(0x4) : 새 프로세스의 기본 스레드는 일시 중지된 상태로 생성되며 ResumeThread()가 호출될 때까지 실행되지 않음
  • lpStartupInfo : 생성시 프로세스에 대한 윈도우 스테이션, 데스크탑, 표준 핸들 및 main window의 모양을 포함하는 STARTUPINFO구조체에 대한 포인터
  • lpProcessInformation : 새로 생성된 프로세스 및 기본 스레드에 대한 정보를 포함하는 PROCESS_INFORMATION구조체에 대한 포인터

lpApplicationName 를 통해 전체 경로를 지정할 수 있으나 해당 변수가 널 값이면 lpCommandLine 을 통해 전달되는 문자열이 프로그램의 파일명이 된다.

전체 경로를 지정하지 않았기 때문에(널 값) lpCommandLine을 통해 전달된 실행 파일을 찾기 위해 순차적으로 검색을 진행한다.

자세한 내용은 다음 링크를 참고)

https://m.blog.naver.com/PostView.nhn?blogId=sol9501&logNo=70107341289&proxyReferer=https:%2F%2Fwww.google.com%2F

1-1) CreateProcessA(0, "explorer.exe", 0, 0, 0, 4, 0, 0, &StartupHandle, &ProcessHandle);

→ "explorer.exe"실행 파일을 찾아 프로세스를 중지된 상태로 생성되며 ResumeThread()가 호출될 때까지 실행되지 않는다.

2) CreateFileA(ExeName, 0x80000000, 1, 0, 3, 0, 0);

→ ExeName에 저장된 "msg.exe"문자열을 전달받아 "msg.exe"파일을 Open하여 핸들 값을 FileHandle에 저장

→ 액세스 권한은 GENERIC_READ(0x80000000, 31bit 세팅)

→ 파일공유 모드로 설정되어 있으므로 다른 프로세스 열기 권한 허가

→ 해당 위치에 파일이 존재할 경우 파일을 열고 존재하지 않는 경우 ERROR_FILE_NOT_FOUND(2) 오류를 발생시킴

3) VirtualAlloc()

→ 지정된 프로세스의 주소 공간에 메모리 할당을 해주는 VirtualAllocEx()와 다르게 VirtualAlloc()는 호출 프로세스의 가상 주소 공간에 할당해준다.

  • lpAddress : 할당할 시작 주소, 널 값인 경우 시스템이 할당 위치 결정
  • dwSize : 할당하고자 하는 메모리 크기(byte), lpAddress가 널 값인 경우 다은 페이지 경계까지 반올림
  • flAllocationType : 메모리 할당 유형
    • MEM_COMMIT(0x1000) : 물리적인 메모리를 확정
    • MEM_RESERVE(0x2000) : 물리적인 메모리 할당없이 주소 공간만 예약
  • flProtect : 할당할 페이지 영역에 대한 메모리 보호
    • PAGE_READWRITE(0x4) : read, write 액세스 활성화

3-1) ExeBaseAddress = (PVOID)VirtualAlloc(0, FileSize, 0x3000, 4);

→ GetFileSize()를 통해 "msg.exe"파일의 크기를 FileSize에 저장되어 있음

→ 호출 프로세스의 가상 주소 공간에 "msg.exe"파일 크기만큼 메모리 할당을 해준다. 이 때 read, write 액세스 권한이 활성화 되어 있음

→ 성공하면 ExeBaseAddress에 할당된 페이지 영역 주소가 저장됨

4) ReadFile()

→ 지정된 파일 또는 I/O장치에서 데이터를 읽어 포인터가 가리키는 위치에 저장하는 함수

  • hFile : 장치에 대한 핸들(ex. 파일, 파일 스트림, 볼륨, 등등..)
  • lpBuffer : 파일 또는 장치에서 읽은 데이터를 받는 버퍼에 대한 포인터
  • nNumberOfBytesToRead : 읽을 최대 바이트 수
  • lpNumberOfBytesRead : 읽은 바이트 수를 수신하는 변수에 대한 포인터
  • lpOverlapped : hFile매개 변수가 FILE_FLAG_OVERLAPPED로 열린 경우 포인터가 필요, 그렇지 않으면 널 값

4-1) ReadFile(FileHandle, ExeBaseAddress, FileSize, &v3, 0);

→ "msg.exe"파일에 대한 핸들과 할당된 주소, 파일 크기를 파라미터로 주어 ExeBaseAddress가 가리키는 위치에 "msg.exe"파일 내용을 저장한다.

5) NtGetContextThread()

→ 지정된 스레드의 컨텍스트를 검색하는 함수

NTSTATUS NtGetContextThread(
      __in HANDLE ThreadHandle,
      __inout PCONTEXT ThreadContext
);
  • ThreadHandle : 컨텍스트를 검색할 스레드에 대한 핸들
  • ThreadContext : 지정된 스레드의 컨텍스트를 받는 CONTEXT구조체 변수

5-1) NtGetContextThread(ThreadHandle, &Context);

→ ThreadHandle가 처음 사용되었으며 어떠한 스레드를 의미하는지는 아직 잘 모르겠음

성공하면 지정된 스레드의 컨텍스트에 대한 정보를 Context구조체 변수인Context에 저장될 것으로 보임

6) NtReadVirtualMemory()

→ 지정된 프로세스의 주소 범위를 읽어 버퍼에 복사하는 함수

NTSTATUS NtReadVirtualMemory(
  IN  HANDLE ProcessHandle,
  IN  PVOID BaseAddress,
  OUT PVOID Buffer,
  IN  ULONG BufferSize,
  OUT PULONG NumberOfBytesRead OPTIONAL
);
  • ProcessHandle : 프로세스에 대한 핸들
  • BaseAddress : 읽을 지정된 프로세스의 주소
  • Buffer : 지정된 프로세스 주소 공간에서 내용을 수신하는 버퍼
  • BufferSize : 버퍼에 저장할 크기(byte)
  • NumberOfBytesRead : 지정된 버퍼로 전송된 실제 byte수를 받음

6-1) NtReadVirtualMemory(ProcessHandle, (PVOID)(Context.Ebx + 8), &Buffer, 4u, 0);

→ "explorer.exe"프로세스에 대한 핸들러인 ProcessHandle을 파라미터로 넘겨준다.

→ "explorer.exe"프로세스에서 Context.Ebx + 8부터 읽어서 Buffer에 저장

7) NtUnmapViewOfSection()

→ 이전에 생성된 View를 섹션에 매핑 해제하는 함수

NTSTATUS NtWriteVirtualMemory(
  IN  HANDLE ProcessHandle,
  IN  PVOID BaseAddress
);
  • ProcessHandle : 프로세스 핸들
  • BaseAddress : 매핑을 해제할 뷰의 기본 가상 주소에 대한 포인터

7-1) NtUnmapViewOfSection(ProcessHandle, Buffer);

→ if문에서 Buffer에 저장된 값이 ((void **)v15 + 13)와 동일하면 해당 코드 실행

→ 메모리에 로드된 원본 실행 파일이 Unmap하기 위한 함수라고 한다.

8) VirtualAllocEx() → 메모리 할당

→ 다른 해당 프로세스가 가상메모리 공간을 할당받고 싶을 때 사용하는 함수

→ 성공하면 할당된 페이지 영역의 주소를 반환

  • hProcess : 프로세스에 대한 핸들
  • lpAddress : 할당할 페이지 영역에 대한 시작 주소를 지정하는 포인터, NULL인 경우 비어있는 공간에 자동 할당
  • dwSize : 할당할 메모리 영역의 크기(byte)
  • flAllocationType : 메모리 할당 유형
    • MEM_COMMIT(0x1000) : 지정된 예약 메모리 페이지에 대한 메모리 할당, 실제 물리적 페이지는 가상 주소가 액세스될 때까지 할당되지 않음
    • MEM_RESERVE(0x2000) : 페이징 파일에 실제 물리적 저장소를 할당하지 않고 프로세스의 가상 주소 공간 범위를 예약
  • flProtect : 할당할 페이지 영역에 대한 메모리 보호 설정
    • PAGE_EXECUTE_READWRITE(0x40) : 실행/읽기/쓰기 권한 활성화

8-1) BaseAddress = (PVOID)VirtualAllocEx(ProcessHandle, *((_DWORD *)v15 + 13), *((_DWORD *)v15 + 20), 0x3000, 0x40);

→ "explorer.exe"의 프로세스 핸들을 파라미터로 준다.

→ v15변수에는 (char *)ExeBaseAddress + *((_DWORD *)ExeBaseAddress + 15) 값이 들어있다.

→ ExeBaseAddress는 "msg.exe"파일에 대한 내용이 저장된 곳을 가리킨다.

→ 따라서, "explorer.exe"프로세스에 "msg.exe"파일을 올리기 위해 공간을 할당해주는 것 같다.

9) NtWriteVirtualMemory

→ 지정된 프로세스의 메모리 영역에 데이터를 쓰는 함수

NTSTATUS NtWriteVirtualMemory(
  IN  HANDLE ProcessHandle,
  OUT PVOID BaseAddress,
  IN  PVOID Buffer,
  IN  ULONG BufferSize,
  OUT PULONG NumberOfBytesWritten OPTIONAL
);
  • WriteProcessMemory()와 동일한 기능과 멤버 변수를 가짐

  • "explorer.exe"프로세스에 "msg.exe"의 어떤 부분을 작성하는 것 같다.
  • 그리고 마지막에 Context.Eax에 값을 세팅하고 한 번 더 NtWriteVirtualMemory()를 호출하게 된다.
  • 위 부분은 디버깅을 통해 자세히 알아보자

10) NtSetContextThread()

→ 지정된 스레드의 컨텍스트를 설정하는 함수

NTSTATUS NtSetContextThread(
      __in HANDLE ThreadHandle,
      __in PCONTEXT ThreadContext
);
  • ThreadHandle : 컨텍스트를 설정할 스레드에 대한 핸들
  • ThreadContext : 지정된 스레드에서 설정할 컨텍스트를 포함하는 CONTEXT구조에 대한 포인터

10-1) NtSetContextThread(ThreadHandle, &Context);

→ 위 함수를 실행시키면 Context가 설정되어 Context구조체의 Eip부분부터 지정된 Thread가 실행됨

11) ResumeThread()

→ 스레드 suspend 횟수를 감소시켜 0이 되면 스레드 실행을 재개시키는 함수

  • hThread : 다시 시작할 스레드에 대한 핸들

11-1) ResumeThread(ThreadHandle);

→ 지정된 스레드의 실행을 재개시킨다.

+) CONTEXT구조체

→ 특정 Thread의 레지스터 정보를 담고 있는 구조체

typedef struct _CONTEXT
{
ULONG ContextFlags;
ULONG Dr0;
ULONG Dr1;
ULONG Dr2;
ULONG Dr3;
ULONG Dr6;
ULONG Dr7;
FLOATING_SAVE_AREA FloatSave;
ULONG SegGs;
ULONG SegFs;
ULONG SegEs;
ULONG SegDs;
ULONG Edi;
ULONG Esi;
ULONG Ebx;
ULONG Edx;
ULONG Ecx;
ULONG Eax;
ULONG Ebp;
ULONG Eip;
ULONG SegCs;
ULONG EFlags;
ULONG Esp;
ULONG SegSs;
UCHAR ExtendedRegisters[512];
} CONTEXT, *PCONTEXT;

+) 각각의 함수들에 대응하는 ntdll함수

  • CreateProcess() / NtCreateUserProcess()
  • VirtualAllocEx() / NtAllocateVirtualMemory()
  • ReadProcessMemory() / NtReadVirtualMemory()
  • WriteProcessMemory() / NtWriteVirtualMemory()
  • GetThreadContext() / NtGetContextThread()
  • SetThreadContext() / NtSetContextThread()
  • ResumeThread() / NtResumeThread()

3.2 동적 분석

1) CreateProcessA() 호출 前, 後

① 호출 前

  • 인자 값 세팅, [ebp-0x38]에는 ProcessHandler

② 호출 後

  • 성공적으로 자식 Process를 생성
  • ProcessHandler [ebp-0x38]에 0x40값이 들어감

  • "explorer.exe"라는 자식 프로세스 생성과 동시에 SUSPEND상태
  • hollow.exe의 자식 프로세스인 "explorer.exe"의 핸들러가 0x40인 것을 확인

2) CreateFileA() 호출

  • "msg.exe"파일 Open하여 FileHandle값 0x48 [ebp-0x14]에 저장

3) VirtualAlloc() 호출

  • GetFileSize()를 통해 "msg.exe"파일의 크기만큼 호출된 프로세스 즉, "hollow.exe"의 주소 공간 할당을 받고 Base Address값을 eax에 저장
  • Base Address : 0xe0000
  • 0xe0000에 주소 공간이 할당받은 것을 알 수 있음(rw권한)

4) ReadFile() 호출 前, 後

① 호출 前

  • 파라미터로 "msg.exe"파일 핸들러, 저장되는 주소 공간 0xe0000, FileSize를 세팅한다.

② 호출 後

  • 0xe0000주소 공간에 "msg.exe"파일 내용이 저장되는 것을 확인

5) NtGetContextThread() 호출 前, 後

① 호출 前

  • [ebp-0x8]에는 "msg.exe"의 시작 주소 값이 들어 있다.
  • 위 순서대로 진행하다보면 add eax, [edx+0x3c]를 진행하게 되는데 이는 결국 "msg.exe"의 시작 주소 + ["msg.exe"의 시작 주소 + 0x3c]인데 PE파일의 DOS Header의 offset이 0x3c위치에는 e_lfanew멤버가 존재한다.
  • e_lfanew멤버에는 NT Header가 시작되는 위치의 옵셋이 저장되어 있다.
  • 최종적으로 [ebp-0x4]에는 "msg.exe"의 NT Header의 시작 주소 저장
  • ThreadHandler와 스레드 컨텍스트를 저장할 Context변수를 파라미터로 세팅
  • 현재 ThreadHandler의 값이 0x3c인 것을 확인할 수 있다.

② 호출 後

  • "explorer.exe"의 스레드 컨텍스트 값이 Context구조체 변수에 저장

6) NtReadVirtualMemory() 호출 前, 後

① 호출 前

  • "explorer.exe"의 프로세스 핸들러와 방금 전 Context구조체 변수의 Context.Ebx값 0x7ffd9000에 8을 더한 값과 마지막으로 저장할 버퍼를 파라미터로 세팅한다.
  • 참고로 Context.Ebx에는 아까 구한 PEB의 주소가 들어있다.
  • 그리고 PEB+0x8 위치에는 해당 프로세스의 Image Base주소가 들어있다.

② 호출 後

  • 호출 이후 Buffer변수에 "explorer.exe"프로세스 Image Base주소 저장
  • "explorer.exe" Image Base주소 : 0xaa0000

7) NtUnmapViewOfSection() → 원본 코드 날리는 용도

NtUnmapViewOfSection()을 호출하기 전에 먼저 if문에 걸리게 된다.

방금 구한 "explorer.exe" Image Base주소와 "msg.exe"파일의 Image Base주소가 같은지 비교하여 같으면 NtUnmapViewOfSection()을 호출한다.

  • "explorer.exe" Image Base 주소 : 0xaa0000
  • "msg.exe" Image Base 주소 : 0x400000

결국 같지 않으므로 NtUnmapViewOfSection()은 호출되지 않고 분기된다.


8) VirtualAllocEx() 호출 前, 後

① 호출 前

  • "explorer.exe"핸들러와 "msg.exe"의 Image Base주소, 등등 파라미터 세팅
  • [ebp-0x4]에는 아까 구한 "msg.exe"의 NT Header의 시작 주소가 담겨있는데 ecx에다가 넣고 다시 [ecx+0x50]의 값을 edx로 옮긴다.
  • NT_HEADERS의 시작 주소부터 offset이 0x50위치에는 메모리에 로딩될 때 가상 메모리에서 PE Image가 차지하는 크기를 나타내는 "Size of Image"멤버가 위치
  • 즉, "explorer.exe"프로세스에 "msg.exe"이미지 만큼 공간할당을 요청

② 호출 後

  • 공간 할당이 성공하고 할당된 페이지 영역의 주소를 반환

9) NtWriteVirtualMemory 호출 前, 後

9-1) NtWriteVirtualMemory(ProcessHandle, BaseAddress, ExeBaseAddress, *((_DWORD *)v15 + 21), 0);

  • 여기서도 보면 [ebp-0x4]에 있는 "msg.exe"의 NT Header의 시작 주소를 가져와 offset이 0x54인 위치의 값을 가져와 파라미터로 넣는다.
  • 해당 위치에는 PE Header의 사이즈 값이 저장된 멤버인 "Size of Header"
  • 즉, 방금 전에 VirtualAllocEx()로 할당받은 시작 주소 0x400000위치에 0x400크기만큼 "msg.exe"의 헤더 데이터 값을 쓰는 것이다.

9-2) for문

  • for문은 총 Section갯수 만큼 돌아가면서 섹션 별로 값이 들어간다.

.text 섹션)

.idata 섹션)

.reloc 섹션)

9-3) Context.Eax = (DWORD)BaseAddress + *((_DWORD *)v15 + 10);

  • "msg.exe"의 NT Header의 시작주소에 offset이 0x28떨어진 곳의 값과 0x400000을 더한 값을 Context.Eax에 저장한다.
  • NT Header의 시작주소에서 offset이 0x28에 떨어진 곳에는 "Address of Entry Point"가 존재
  • AddressOfEntryPoint : 파일이 메모리에 매핑된 후 코드 시작 주소이며 ImageBase값에 이 값을 더해 코드 시작 지점을 설정
  • Context구조체 변수의 Eax멤버에는 EntryPoint의 주소가 포함된다. 즉, 기존의 "explorer.exe"의 EntryPoint주소 대신에 "msg.exe"의 EntryPoint주소로 변경하는 과정이다.

9-4) NtWriteVirtualMemory(ProcessHandle, (PVOID)(Context.Ebx + 8), v15 + 52, 4u, 0);

  • Context.Ebx에 값에 8을 더한 값을 입력 값을 받을 주소로 사용한다.
  • Context.Ebx에는 PEB의 주소가 존재하며 PEB주소에 offset이 8만큼 떨어진 곳에는 ImageBaseAddress값이 존재한다.
  • 즉, "explorer.exe"프로세스의 PEB의 ImageBaseAddress멤버 변수에 "msg.exe"의 ImageBaseAddress값을 채워 넣는다.

10) NtSetContextThread() 호출

  • 수정된 Context구조체 변수와 explorer.exe의 스레드 핸들러를 인자로 세팅
  • 위 함수를 실행시키게 되면 Context가 설정되어 Context구조체의 Eip부분부터 지정된 Thread가 실행하게 된다.

11) ResumeThread() 호출

  • 최종적으로 ResumeThread()를 호출하여 "explorer.exe"가 SUSPEND상태에서 풀려나 실행이 재개되어 아까 심어놨던 "msg.exe"의 코드가 실행되어 MessageBox하나를 띄우게 된다.


4. 정리

4.1 Process

1) CreateProcessA()를 통해 "explorer.exe"자식 프로세스를 생성함과 동시에 SUSPENDED를 걸어놓는다.

2) 호출한 프로세스(hollow.exe)의 주소 공간에 "msg.exe"데이터를 심어 놓는다.

3) NtGetThreadContext()를 통해 "explorer.exe"의 스레드 컨텍스트를 Context구조체 변수에 저장한다.

4) "explorer.exe"와 "msg.exe"의 ImageBase를 비교하여 같으면 NtUnmapViewOfSection()를 호출한다.

5) VirtualAllocEx()를 호출하여 "explorer.exe"의 가상 메모리 공간을 할당 받는다. ("msg.exe"의 이미지 크기만큼)

6) 할당된 메모리 공간에 "msg.exe"의 헤더, 각각의 섹션을 채워넣는다.

7) Context.Eax에 "msg.exe"의 EntryPoint주소를 넣는다.

8) Context.Ebx+8(ImageBaseAddress)에 "msg.exe"의 Image Base주소를 넣는다.

9) NtSetContextThread()를 통해 수정된 Context를 적용시킨다.

10) ResumeThread()를 통해 처음에 SUSPENDED시켜놓은 "explorer.exe"의 스레드를 재개 시킨다.

11) 최종적으로 EntryPoint가 "msg.exe"의 EntryPoint를 가리키게 되어 "msg.exe"의 코드가 실행하게 된다.


4.2 Image

출처 : https://www.elastic.co/kr/blog/ten-process-injection-techniques-technical-survey-common-and-trending-process


5. 참고자료

5.1 msdn

5.2 Process Hollowing 관련

+) 추가 - Windbg를 이용해서 CONTEXT구조체 확인

dt _CONTEXT' (CONTEXT구조체 변수)


'Security > 07 Malware Technique' 카테고리의 다른 글

Dropper 3-4(Thread Injection)  (0) 2020.12.30
Dropper 3-3(PE Injection)  (0) 2020.12.29
Dropper 3-1(DLL Injection)  (0) 2020.12.29
Dropper 2(Resource)  (0) 2020.12.29
Dropper 1(Download & Execute)  (0) 2020.12.29
Comments