게임해킹/커널 게임 해킹

[게임해킹] 커널에서 PEB로 베이스주소 가져오는 방법

cp_good: 2023. 1. 1. 20:46
반응형

기본적인 게임해킹 중 커널 안티치트를 피하기 위해 필요한 과정으로

커널에서 베이스주소를 가져오는 방법입니다.

 

안티치트 우회이기 때문에 data ptr 후킹이 사용됩니다.(참고)

 


소스코드

 

아래 소스코드는 베이스주소를 가져오는데 필요한 헤더와 구조체 정의를 한 Includes.h입니다.

#include <ntdef.h>
#include <ntifs.h>
#include <windef.h>

extern "C" NTKERNELAPI
PPEB
PsGetProcessPeb(
	IN PEPROCESS Process
);

typedef struct _PEB_LDR_DATA {
	ULONG Length;
	BOOLEAN Initialized;
	PVOID SsHandle;
	LIST_ENTRY ModuleListLoadOrder;
	LIST_ENTRY ModuleListMemoryOrder;
	LIST_ENTRY ModuleListInitOrder;
} PEB_LDR_DATA, * PPEB_LDR_DATA;

typedef struct _LDR_DATA_TABLE_ENTRY {
	LIST_ENTRY InLoadOrderModuleList;
	LIST_ENTRY InMemoryOrderModuleList;
	LIST_ENTRY InInitializationOrderModuleList;
	PVOID DllBase;
	PVOID EntryPoint;
	ULONG SizeOfImage;
	UNICODE_STRING FullDllName;
	UNICODE_STRING BaseDllName;
	ULONG Flags;
	USHORT LoadCount;
	USHORT TlsIndex;
	LIST_ENTRY HashLinks;
	PVOID SectionPointer;
	ULONG CheckSum;
	ULONG TimeDateStamp;
} LDR_DATA_TABLE_ENTRY, * PLDR_DATA_TABLE_ENTRY;

typedef struct _RTL_USER_PROCESS_PARAMETERS {
	BYTE Reserved1[16];
	PVOID Reserved2[10];
	UNICODE_STRING ImagePathName;
	UNICODE_STRING CommandLine;
} RTL_USER_PROCESS_PARAMETERS, * PRTL_USER_PROCESS_PARAMETERS;

typedef void(__stdcall* PPS_POST_PROCESS_INIT_ROUTINE)(void);

typedef struct _PEB {
	BYTE Reserved1[2];
	BYTE BeingDebugged;
	BYTE Reserved2[1];
	PVOID Reserved3[2];
	PPEB_LDR_DATA Ldr;
	PRTL_USER_PROCESS_PARAMETERS ProcessParameters;
	PVOID Reserved4[3];
	PVOID AtlThunkSListPtr;
	PVOID Reserved5;
	ULONG Reserved6;
	PVOID Reserved7;
	ULONG Reserved8;
	ULONG AtlThunkSListPtr32;
	PVOID Reserved9[45];
	BYTE Reserved10[96];
	PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;
	BYTE Reserved11[128];
	PVOID Reserved12[1];
	ULONG SessionId;
} PEB, * PPEB;

 

간단히 과정을 정리하면

 

1. PsLookupProcessByProcessId() 함수로 EPROCESS를 얻어옵니다.

 

2. 얻어온 EPROCESS을 인자로 PsGetProcessPeb() 함수로 PEB를 가져옵니다.

 

3. KeStackAttachProcess() 함수로 베이스주소를 가져올 프로세스에 연결합니다.

  • 연결해주지 않으면 블루스크린이 뜹니다.

 

4. Includes.h에 정의된 PEB 구조체의 "PPEB_LDR_DATA Ldr" 멤버를 참조해 모듈정보를 가져옵니다.

  • _PEB는 각 프로세스마다 생성된다.
  • Ldr은 _PEB_LDR_DATA 구조체를 가지고, 이 구조체는 현재 프로세스에 로드한 모듈 정보를 가지고 있다.
  • 프로세스에 로딩되는 DLL마다 _LDR_DATA_TABLE_ENTRY 구조체가 생성되고 연결방법이 세 가지이다. LIST_ENTRY InLoadOrderModuleList;
    LIST_ENTRY InMemoryOrderModuleList;
    LIST_ENTRY InInitializationOrderModuleList;
  • _PEB_LDR_DATA 구조체 ModuleListLoadOrder를 _LDR_DATA_TABLE_ENTRY 구조체로 참조하면 DLL의 정보를 가지고 있다.

 

5. for문으로 LIST_ENTRY를 순회하면서 원하는 모듈을 찾습니다. 찾으면 CONTAINING_RECORD() 매크로로  _LDR_DATA_TABLE_ENTRY. 구조체를 가져옵니다.

  • LIST_ENTRY 연결 리스트 구조를 이용해 for문으로 처음 연결리스트와 같으면 for문을 빠져나옵니다.
  • CONTAINING_RECORD()는 상위구초제를 가져오는 매크로 함수입니다, LIST_ENTRY형태로 돼있으니 _LDR_DATA_TABLE_ENTRY를 가져와야 BaseDllName을 찾을 수 있습니다.

6. 유니코드 스트링을 비교해 원하는 모듈이면 베이스주소를 가져와 KeUnstackDetachProcess() 후 리턴합니다.

 


아래 소스코드는 베이스주소를 얻어오는 함수와 DriverEntry입니다.

#include "Includes.h"

// 베이스주소 얻어오는 함수
ULONG64 get_module_base_x64(PEPROCESS proc, UNICODE_STRING module_name)
{
	// PEB 가져오기
	PPEB pPeb = PsGetProcessPeb(proc);

	if (!pPeb)
	{
		return NULL;
	}

	KAPC_STATE state;
	// 프로세스 연결(중요)
	KeStackAttachProcess(proc, &state);
	// Ldr 참조
	PPEB_LDR_DATA pLdr = (PPEB_LDR_DATA)pPeb->Ldr;

	if (!pLdr)
	{
		KeUnstackDetachProcess(&state);
		return NULL;
	}
	// 연결리스트 순환
	for (PLIST_ENTRY list = (PLIST_ENTRY)pLdr->ModuleListLoadOrder.Flink; list != &pLdr->ModuleListLoadOrder; list = (PLIST_ENTRY)list->Flink)
	{
		// 상위 구조체 가져오는 매크로로 LDR_DATA_TABLE_ENTRY 구조체 가져오기
		PLDR_DATA_TABLE_ENTRY pEntry = CONTAINING_RECORD(list, LDR_DATA_TABLE_ENTRY, InLoadOrderModuleList);
		// 문자열 비교
		if (RtlCompareUnicodeString(&pEntry->BaseDllName, &module_name, TRUE) == NULL)
		{
			// 베이스주소 리턴
			ULONG64 baseAddr = (ULONG64)pEntry->DllBase;
			KeUnstackDetachProcess(&state);
			return baseAddr;
		}
	}

	KeUnstackDetachProcess(&state);
	return NULL;
}

// 드라이버 엔트리
NTSTATUS DriverEntry(PVOID lpBaseAddress, DWORD32 dwSize) {
	UNREFERENCED_PARAMETER(lpBaseAddress);
	UNREFERENCED_PARAMETER(dwSize);
	
	// 베이스주소를 가져올 프로세스 이름
	// 유니코드 스트링으로 바꾸는 과정
	RtlInitAnsiString(&AS, "notepad.exe");
	RtlAnsiStringToUnicodeString(&ModuleName, &AS, TRUE);
	PEPROCESS process;
	// 프로세스 아이디로 EPROCESS가져오기
    PsLookupProcessByProcessId((HANDLE)pid, &process);
	ULONG64 base_address64 = NULL;
	// EPROCESS와 모듈이름 넘기기
	base_address64 = get_module_base_x64(process, ModuleName);

	return 0;
}

data ptr 후킹은 링크 참조하세요.

data ptr 후킹이 이해되지 않으면 기본적인 게임해킹 방법 참고

 

유저모드 소스코드
#include <Windows.h>
#include <iostream>

// 요청이 뭔지 체크용
// 임의로 지정하면됨
#define Base 0x15881588
// 예시) Write는 메모리쓰기 요청, Read는 메모리읽기 요청, Base는 베이스주소 요청
// 게임해킹에는 메모리읽기, 쓰기, 베이스주소등 한가지 요청만 하는게 아니기 때문에
// 후킹한 함수에서 0x15881588 같이 지정된 숫자로 무슨요청인지 구분후
// 요청에 맞게 처리해줍니다.
// #define Write 0x15991599
// #define Read 0x16001600
// 만약 내가 만든프로그램에서 요청한것이 아니면 원본함수로 넘겨버릴수도 있겠죠

PVOID(__fastcall* FunctionPTR)(INT64, INT64, PVOID, INT, UINT, INT) = NULL;
uintptr_t base_address;

int main()
{
	// data ptr 후킹한 함수를 호출하는 함수포인터 입니다.
    FunctionPTR(0, 0, &base_address, pid, Base, 0);
	// base_address : 베이스주소를 받아올 변수
	// Base : 내가 만든 프로그램에서 무엇을 요청했는지 체크용 숫자
	
	return 0;
}
반응형