Windows API를 활용한 explorer.exe 프로세스 핸들 얻는 방법

Windows 시스템에서 특정 프로세스(explorer.exe)의 핸들을 얻는 것메모리 분석, 악성 코드 탐지프로세스 인젝션과 같은 작업에 매우 중요합니다. 예를 들어, 프로세스 인젝션 과정에서는 목표 프로세스에 대한 핸들 권한이 필요합니다.

 

이 글에서는 Windows API를 활용하여 explorer.exe 프로세스의 핸들을 열고, 메모리에 접근하는 방법을 설명합니다.

특히 CreateToolhelp32Snapshot, Process32First, Process32Next와 같은 함수를 사용해 시스템 내 실행 중인 프로세스를 탐색하고, 해당 프로세스에 대한 핸들을 얻는 방법을 다룹니다. 프로세스 핸들을 획득하여 메모리 조작이나 프로세스 제어와 같은 작업을 수행할 수 있게 됩니다.

 

주의: 이 글은 교육 목적으로 작성되었으며, 본 내용을 악용하여 발생하는 모든 책임은 본인에게 있습니다.

 

 

헤더파일

#include <windows.h>
#include <tlhelp32.h>
#include <stdio.h>
#include <tchar.h>
  • windows.h: Windows API 사용을 위한 필수 헤더 파일입니다. 이 파일에는 프로세스 관리, 메모리 조작, 시스템 자원 핸들 관리에 필요한 함수들이 포함되어 있습니다. 예를 들어, OpenProcess, CreateToolhelp32Snapshot과 같은 함수들이 이 헤더에 정의되어 있습니다.
  • tlhelp32.h: 스냅샷(Snapshot) 기능을 통해 현재 실행 중인 프로세스와 스레드 정보를 얻기 위한 헤더 파일입니다. 이 헤더 파일은 CreateToolhelp32Snapshot, Process32First, Process32Next와 같은 프로세스 탐색 함수들을 제공합니다.
  • stdio.h: 표준 입출력을 지원하는 헤더 파일입니다. printf, scanf 등 입출력 함수들을 사용하여 프로세스 정보와 같은 데이터를 출력할 수 있습니다.
  • tchar.h: 유니코드와 ANSI 문자열 처리를 돕는 함수들이 정의된 헤더 파일입니다. MultiByteToWideChar 함수 등을 사용할 때 유용합니다.

 

GetProcessHandle 함수

GetProcessHandle 함수는 main함수에서 설정된 특정 프로세스의 이름을 입력받아, 해당 프로세스의 핸들프로세스 ID(PID)를 얻는 기능을 합니다. 

 

자료형 설명

  • LPCWSTR processName : Windows API에서 사용하는 데이터 타입 중 하나로, Long Pointer to a Constant Wide String의 약자입니다. 유니코드 문자열에 대한 읽기전용 포인터로, Windows API 에서 유니코드 문자열을 인자로 받을 때 사용됩니다.
  • HANDLE hProcess: HANDLE은 Windows에서 파일, 프로세스 등의 시스템 자원을 참조하는 데 사용되는 자료형입니다. 여기서는 프로세스의 HANDLE을 저장합니다.
  • DWORD pid: DWORD는 32비트 정수형 데이터 타입으로, 이 변수는 찾고자하는 프로세스 ID를 저장합니다.

 

 

함수 작성 예시

BOOL GetProcessHandle(LPCWSTR processName, HANDLE* hProcess, DWORD* pID)

 

  • processName: 찾고자 하는 프로세스의 이름(유니코드 문자열)
  • hProcess: 찾은 프로세스의 핸들을 저장할 포인터
  • pID: 찾은 프로세스의 PID를 저장할 포인터
  • 반환값: 함수 실행 성공 여부 (TRUE 또는 FALSE)

 

함수 동작 설명

BOOL GetProcessHandle(LPCWSTR processName, HANDLE* hProcess, DWORD* pID)
{
	DWORD pid = 0;  // 프로세스 ID를 저장할 변수
	HANDLE hP = NULL;  // 프로세스 핸들을 저장할 변수
	HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPALL, 0);  // 현재 시스템의 모든 프로세스 정보를 스냅샷으로 찍음
	if (hSnapshot == INVALID_HANDLE_VALUE)  // 스냅샷을 찍는데 실패했는지 확인
	{
		printf("[ERROR] Invalid HANDLE to process snapshots [%d]\n", GetLastError());
		return FALSE;
	}

	PROCESSENTRY32 pe;  // 프로세스 정보를 저장할 구조체
	pe.dwSize = sizeof(pe);  // 구조체 크기 초기화

	if (!Process32First(hSnapshot, &pe))  // 스냅샷에서 첫 번째 프로세스 정보를 가져옴
	{
		printf("[ERROR] Could not enumerate processes [%d]\n", GetLastError());
		CloseHandle(hSnapshot);  // 핸들 닫기
		return FALSE;
	}

주요 동작

  • HANDLE: Windows에서 시스템 자원(파일, 프로세스 등)을 참조할 때 사용하는 값입니다.
  • CreateToolhelp32Snapshot: 현재 시스템에서 실행 중인 모든 프로세스와 스레드 정보를 스냅샷으로 찍습니다. 이 함수는 프로세스, 스레드, 모듈 등의 정보를 스냅샷으로 캡처하며, hSnapshot 변수에 그 핸들을 저장합니다.
  • Process32First: 스냅샷에서 첫 번째 프로세스 정보를 가져옵니다.

 

함수의 동작을 살펴보면, 먼저 CreateToolhelp32Snapshot 함수를 사용하여 현재 실행중이 모든 프로세스 정보의 스냅샷을 찍어 hSnapshot 변수에 프로세스핸들을 저장합니다.

 

만약 CreateToolhelp32Snapshot 함수가 실패하면 hsnapshotINVALID_HANDLE_VALUE가 됩니다. 이 값은 -1이므로, 이를 통해 실패 여부를 확인할 수 있습니다. 실패했을 때는 GetLasError()를 터미널에 출력하여 실패 원일을 자세히 알 수 있습니다.

 

그 다음, Process32First 함수를 사용하여 현재 스냅샷에 저장된 첫 번째 프로세스 정보를 가져옵니다. 이 정보는 PROCESSENTRY32 구조체에 저장되며, pe 포인터에 첫 번째 프로세스의 정보가 채워집니다.

!연산자를 통해 함수가 실패하였을 경우에는 오류 메시지를 출력하고, 핸들(hSnapshot)을 닫아 리소스 낭비를 방지하며 함수가 종료합니다.

 

 

프로세스 비교 및 핸들 열기

프로세스를 순차적으로 불러와 프로세스의 핸들을 읽는 do-while문 코드에 대해서 단계별로 설명하고 전체코드를 제공하겠습니다.

 

do {
    // 내부 코드
} while (Process32Next(hSnapshot, &pe));  // 스냅샷에서 다음 프로세스를 읽음

do-while 루프 do 블록의 코드를 실행한 후, Process32Next 함수를 통해 다음 프로세스를 탐색합니다.

 

첫 번째 프로세스는 이미 Process32First로 가져왔기 떄문에, Process32Next 함수는 두 번째 프로세스로부터 마지막 프로세스까지 순차적으로 읽어옵니다.

 

 

WCHAR exeFileName[MAX_PATH];  // 유니코드 형식의 문자열을 저장하는 배열
MultiByteToWideChar(CP_ACP, 0, pe.szExeFile, -1, exeFileName, MAX_PATH);

WCHAR: 유니코드 형식의 문자열 배열을 선언할 때 사용하는 자료형입니다. Windows에서는 MAX_PATH 상수를 사용하여 배열의 크기를 제한하는데, 이 상수에는 최대 260글자까지 저장가능합니다.

 

MultiByteToWideChar: 이 함수는 ANSI 형식의 문자열을 유니코드 문자열로 변환하는 함수입니다. pe.szExeFile에 저장된 프로세스 이름은 ANSI 형식이므로, 이를 유니코드로 변환하여 프로그램의 유니코드 기반 문자열과 비교할 수 있도록 합니다.

 

 

if (_wcsicmp(processName, exeFileName) == 0) // 대소문자 구분 없이 유니코드 문자열을 비교, 일치하면 0 반환
{
	pid = pe.th32ProcessID;
	printf("[!] Trying to open handle on %ls, on pid %d\n", processName, pid);

	hP = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, FALSE, pid);
	if (hP == NULL)  // 핸들 열기 실패
	{
		printf("[X] Could not open handle on %d, continuing\n", pid);
	}
	else  // 핸들 열기 성공
	{
		printf("[+] Successfully got handle on %d\n", pid);
		*pID = pid;
		*hProcess = hP;
		CloseHandle(hSnapshot);
		return TRUE;  // 일치하는 프로세스를 찾았으므로 함수 종료
	}
}

변환된 유니코드 문자열(exeFileName)과 찾고자 하는 유니코드 문자열(processName)을 비교합니다. 이때 _wcsicmp 함수는 대소문자를 구분하지 않고 문자열을 비교합니다. 두 문자열이 일치하면 0을 반환하고, 그 경우 해당 프로세스의 ID핸들을 얻을 수 있습니다.

 

OpenProcess 함수를 사용하여, 해당 프로세스에 대한 핸들을 열고 여러 권한을 요청합니다. 요청하는 권한에는 스레드 생성, 프로세스 정보 조회, 메모리 작업(읽기, 쓰기, 실행) 등이 포함됩니다.

 

만약 OpenProcess 함수가 실패하여 NULL 값을 반환하면, 핸들을 여는 데 실패한 것이므로 다음 프로세스로 넘어가 이 과정을 반복합니다.

 

hP 값이 NULL이 아닌 경우 핸들이 성공적으로 열렸음을 의미합니다. 이때 PID와 프로세스 핸들을 저장하고, 더 이상 반복하지 않고 함수를 TRUE로 반환하며 종료합니다.

 

 

} while (Process32Next(hSnapshot, &pe));  // 스냅샷에서 다음 프로세스를 읽음

CloseHandle(hSnapshot);
return FALSE;  // 프로세스를 찾지 못하면 오류 메시지를 출력하고 FALSE 반환

루프가 끝날 때까지 일치하는 프로세스를 찾지 못하면, 스냅샷 핸들(hSnapshot)을 닫아 리소스 낭비를 방지하고 함수는 FALSE를 반환합니다.

 

 

프로세스 비교 및 핸들 열기 전체 코드

	do {
		// 프로세스 이름을 ANSI에서 유니코드로 변환
		WCHAR exeFileName[MAX_PATH];  // 유니코드 형식의 문자열을 저장하는 배열
		MultiByteToWideChar(CP_ACP, 0, pe.szExeFile, -1, exeFileName, MAX_PATH);  // ANSI에서 유니코드로 변환

		// 찾는 프로세스 이름과 변환된 프로세스 이름 비교
		if (_wcsicmp(processName, exeFileName) == 0)  // 일치하는 프로세스를 찾았는지 확인
		{
			pid = pe.th32ProcessID;  // 일치하는 프로세스의 ID 저장
			printf("[!] Trying to open handle on %ls, on pid %d\n", processName, pid);

			// 해당 프로세스의 핸들 열기 (여러 권한을 요청)
			hP = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, FALSE, pid);
			if (hP == NULL)
			{
				printf("[X] Could not open handle on %d, continuing\n", pid);  // 핸들 열기 실패 시 다음 프로세스로 넘어감
			}
			else
			{
				printf("[+] Successfully got handle on %d\n", pid);  // 핸들 열기 성공
				*pID = pid;  // 프로세스 ID 저장
				*hProcess = hP;  // 핸들 저장
				CloseHandle(hSnapshot);  // 리소스 누수 방지를 위해 스냅샷 핸들 닫기
				return TRUE;  // 프로세스를 찾았으므로 함수 종료
			}
		}
	} while (Process32Next(hSnapshot, &pe));  // 다음 프로세스를 계속 탐색
    
    
	CloseHandle(hSnapshot);
	return FALSE; //프로세스를 찾지 못하면 오류 메시지를 출력합니다.

 

 

main함수

int main()
{
	HANDLE hProcess;  // 프로세스 핸들을 저장할 변수
	DWORD pid;  // 프로세스 ID를 저장할 변수
	LPWSTR targetProcess = L"explorer.exe";  // 찾을 프로세스 이름 (유니코드 문자열)

	if (GetProcessHandle(targetProcess, &hProcess, &pid))  // 프로세스 핸들을 얻으면 성공
	{
		printf("[+] Process handle opened successfully! PID: %d\n", pid);  // 성공 메시지 출력
		CloseHandle(hProcess);  // 프로세스 핸들 닫기
	}
	else  // 프로세스를 찾지 못하거나 핸들 열기 실패
	{
		printf("[X] Failed to find or open the process\n");  // 실패 메시지 출력
	}

	system("pause");
	return 0;
}

hProcess pid 프로세스 핸들 프로세스 ID(PID)를 저장할 변수로 선언됩니다. 이 변수들은 초기에 빈 값으로 선언되지만, GetProcessHandle함수가 실행되는 부분에 각변수의 주소(&)를 넘겨 프로세스 핸들과 프로세스 PID에 해당 값들이 저장됩니다.

 

targetProcess"explorer.exe"라는 유니코드 문자열을 저장하는 변수로, 찾고자 하는 프로세스 이름을 나타냅니다.

 

GetProcessHandle 함수가 성공하면, 프로세스 핸들이 성공적으로 열렸다는 메시지와 함께 PID가 출력됩니다. 이후 핸들CloseHandle 함수로 닫아 리소스 누수를 방지합니다.

반면에, 프로세스를 찾지 못하거나 핸들을 열지 못하면 실패 메시지가 출력됩니다.

 

 

테스트 하기

코드를 작성시켜보면 explorer.exe 프로세스에대한 프로세스 핸들을 성공적으로 취득하여 값이 반환되는걸 확인할 수 있습니다.

 

 

전체코드

 

GitHub - Blue-B/Process_Injection

Contribute to Blue-B/Process_Injection development by creating an account on GitHub.

github.com

 

 

참고자료

Microsoft lgnite

Red Teaming @Lsecqt

 

Top