본 게시글은 메모리에 대한 깊은 이해를 위해 에이콘 출판사의 "봇을 이용한 게임 해킹" 서적을 보고 필기한 자료입니다.
따라서 디테일한 부분에서 본 서적의 실제 내용과 다를 수 있고 글쓴이의 주관이 들어가 있음을 참고해주시기 바랍니다.
문제 시 비공개 처리하도록 하겠습니다.
코드를 작성할 때 메모리가 어떻게 동작하는지 늘 염두에 두고 있어야 한다.
게임 프로세스 식별자 알아내기
- 게임 메모리에 무언가 쓰려면 프로세스 식별자 (PID)를 알아야 한다.
- PID는 활성화된 프로세스를 구별할 수 있는 고유 번호
- 시각적으로 확인 가능한 창이라면 GetWindowThreadProcessId()로 PID를 구할 수 있다.
- 첫 번째 파라미터로 창의 핸들을 가짐
- 두 번째 파라미터로는 PID를 받을 DWORD 변수의 포인터를 받음 (여기로 값이 반환됨)
#include <iostream>
#include <Windows.h>
int main()
{
HWND myWindow =
FindWindow(NULL, L"윈도우 타이틀");
DWORD PID;
GetWindowThreadProcessId(myWindow, &PID);
}
- 만약 게임이 창으로 표시되지 않거나, 창 이름을 모른다면 모든 프로세스를 살펴봐서 찾아내야 함
- tlhelp32.h의 CreateToolhelp32SnapShot(), Process32First(), Process32Next()를 이용해서 수행함
#include <iostream>
#include <string>
#include <Windows.h>
#include <TlHelp32.h>
int main()
{
PROCESSENTRY32 entry;
entry.dwSize = sizeof(PROCESSENTRY32);
HANDLE snapshot =
CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
if (Process32First(snapshot, &entry))
{
while (Process32Next(snapshot, &entry))
{
std::wstring binPath = entry.szExeFile;
if (binPath.find(L"game.exe") != std::wstring::npos)
{
printf("game pid is %d\n", entry.th32ProcessID);
break;
}
}
}
CloseHandle(snapshot);
}
프로세스 핸들 알아내기
- PID를 알아냈다면 OpenProcess()라는 함수를 사용해 프로세스 핸들을 구할 수 있다.
- 이 함수를 사용하면 해당 프로세스의 메모리를 읽고 쓴느 접근 수준의 핸들을 가져올 수 있음
- 프로세스상에서 수행되는 모든 함수는 접근 권한을 요구하기 때문에 게임 해킹에서 핵심적인 부분임
- 첫 번째 파라미터 DesiredAccess는 프로세스 액세스 플래그임
- 다양한 플래그가 있는데 자주 사용되는 플래그는 아래와 같다.
- 두 번째 파라미터는 핸들 상속에 대한 내용인데 통상 false
- 세 번째 파라미터는 위에서 구한 PID를 전달하면 된다.
프로세스 액세스 플래그 | 설명 | 같이 사용되는 함수 |
PROCESS_VM_OPERATION | 가상 메모리 할당, 해제, 보호하기 위한 권한을 가짐 | VirtualAllocEx VirtualFreeEx VirtualProtectEx |
PROCESS_VM_READ | 가상 메모리 읽기 권한 | ReadProcessMemory |
PROCESS_VM_WRITE | 가상 메모리 쓰기 권한 | WriteProcessMemory |
PROCESS_CREATE_THREAD | 쓰레드 생성 권한 | CreateRemoteThread |
PROCESS_ALL_ACCESS | 모든 권한 | 이전 버전의 윈도우와 호환성 문제가 있으므로 사용하지 않는게 좋다. |
DWORD PID = getGamePID();
HANDLE process = OpenProcess(
PROCESS_VM_OPERATION |
PROCESS_VM_READ |
PROCESS_VM_WRITE,
false,
PID
);
if (process == INVALID_HANDLE_VALUE) {
printf("Failed to open PID %d, error code %d", PID, GetLastError());
}
- 읽고 쓰기 권한을 가진 핸들을 구하는 예제
메모리 접근
- 윈도우API에는 메모리 접근에 핵심적인 2개의 함수가 존재한다.
- ReadProcessMemory()와 WriteProcessMemory()임
- 이 두 함수를 통해 게임 메모리를 조작할 수 있다.
- 두 함수는 서로 대놓고 닮아있다.
- 그래서 이들을 활용하는 작업도 거의 동일하다.
- 첫 번째 파라미터는 프로세스의 핸들
- 두 번째 파라미터는 타깃 메모리 주소
- 세 번째 파라미터는 쓰고 있는 데이터 (Read는 내 버퍼, Write는 상대 버퍼)
- 네 번째 파라미터는 Buffer의 크기를 바이트단위로 정의
- 마지막 파라미터는 액세스된 바이트의 규모를 반환, NULL로 세팅해도 됨
DWORD val;
ReadProcessMemory(process, adr, &val, sizeof(DWORD), 0);
printf("Current mem value is %d\n", val);
val++;
WriteProcessMemory(process, adr, &val, sizeof(DWORD), 0);
ReadProcessMemory(process, adr, &val, sizeof(DWORD), 0);
printf("New mem value is confirmed as %d\n", val);
- 이 코드를 사용하기 전에 읽고 쓰고자 하는 메모리의 주소를 미리 알아내야 함
- 이 값들이 제대로 배치되면
- ReadProcessMemory() 함수는 읽어온 메모리를 val에 저장함
- val을 증가시키고 WriteProcessMemory()를 통해 원래 값을 대체함
- 그 다음 ReadProcessMemory()로 확인함
정형화된 메모리 액세스 함수 작성하기
- 위에서 읽어올 데이터가 DWORD형이란걸 이미 알고 있었다
- 다재다능한 해커는 범용 코드에 익숙해지는게 좋다.
template<typename T>
T readMemory(HANDLE proc, LPVOID adr)
{
T val;
ReadProcessMemory(proc, adr, &val, sizeof(T), NULL);
return val;
}
template<typename T>
void writeMemory(HANDLE proc, LPVOID adr, T val)
{
WriteProcessMemory(proc, adr, &val, sizeof(T), NULL);
}
- 각기 다른 타입에 대해 readMemory, writeMemory를 만들지 않아도 된다는게 큰 장점이다.
메모리 보호
- 윈도우가 게임이나 일반 프로그램에 할당되면 페이지(Page)안에 위치하게 된다.
- x86 윈도우에서 페이지는 4096바이트의 데이터 저장소임
- 모든 메모리가 페이지 안에 위치해야 하므로 최소 단위 또한 4096 바이트다.
할당된 메모리 중 사용되지 않은 충분한 공간이 있는 경우 더 적은 메모리를 설정할 수 있다.
- 운영체제에서 메모리를 할당하는 경우 할당된 페이지에 여유가 있는지부터 본다.
- 필요하면 새 페이지를 할당
- 메모리 페이지는 독특한 속성을 갖고 있음
- 메모리 관련 작업을 수행할 때 신경써야할 속성이 바로 보호 속성이다.
x86 윈도우 메모리 보호 속성 구별하기
- 우리가 배운 메모리 읽는 기술은 아주 기본적인 것임
- 우리가 살펴본 메모리는 PAGE_READWRITE 속성에 의해 보호받고 있다고 간주되었던 것
- 실제 페이지 상에 다양한 보호속성을 가진 데이터도 존재함
페이지 보호 속성 플래그 | 읽기/쓰기/실행 (권한) |
PAGE_NOACCESS | X/X/X |
PAGE_READONLY | O/X/X |
PAGE_READWRITE | O/O/X |
PAGE_WRITECOPY | O/O/X |
PAGE_EXECUTE | X/X/O |
PAGE_EXECUTE_READ | O/X/O |
PAGE_EXECUTE_READWRITE | O/O/O |
PAGE_EXECUTE_WRITECOPY | O/O/O |
- 자주 쓰이는 것들 BOLD처리함
메모리 보호 변경
- 가끔 메모리 페이지 보호로 접근이 금지된 메모리를 액세스해야 하는 경우가 있음
- 이럴 때 보호 모드를 마음대로 변경할 수 있어야 한다.
- VirtualProtectEx 함수를 사용하면 됨
- flNewProtect 인자를 통해 새로운 메모리 보호 플래그를 받고, lpflOldProtect를 통해 이전의 보호 모드를 반환 받음
- 대부분 메모리 보호는 페이지 단위로 수행됨
- 메모리 페이지 권한을 변경하는 코드도 정형화시키면 좋다.
template<typename T>
DWORD protectMemory(HANDLE proc, LPVOID adr, DWORD prot)
{
DWORD oldProt;
VirtualProtectEx(proc, adr, sizeof(T), prot, &oldProt);
return oldProt;
}
protectMemory<DWORD>(process, address, PAGE_READWRITE);
writeMemory<DWORD>(process, address, newValue);
- 메모리 보호 모드를 PAGE_READWRITE로 변경하고 메모리를 쓰는 함수를 호출하는 예제
- 게임이 침입자를 감지하지 못하게 변경된 보호속성을 최소 기간만 유지하고 원래대로 복구하는게 좋다.
DWORD oldProt =
protectMemory<DWORD>(process, address, PAGE_READWRITE);
writeMemory<DWORD>(process, address, newValue);
protectMemory<DWORD>(process, address, oldPlot);
- 전형적인 쓰기 관련 코드
- 위 코드는 페이지의 보호 속성을 변경하고, 그 메모리에 newValue를 쓴 후 보호 속성을 원래대로 복구하는 코드
메모리 조작 관련하여 중요한 사실
- 게임의 메모리를 조작하고 있을 때 게임에서도 동일 메모리에 접근할 수 있다.
- 만약 새로 설정한 보호 모드가 원래 보호 모드와 호환되지 않으면 ACCESS_VIOLATION 예외로 처리하고 크래시
- 예시로 PAGE_EXECUTE를 PAGE_READWRITE로 변경했다면 게임은 페이지 상 코드를 실행하려 하지만
- 보호속성 때문에 크래시 발생
- 이런 경우 PAGE_EXECUTE_READWRITE로 변경하면 되긴 함
주소공간 배치 난수화(ASLR)
- 바이너리가 ASLR을 지원하는 기능을 가지고 컴파일된다면 실행할 때마다 기본 주소가 달라짐
- (MSVC++ 2010과 다수 컴파일러에서 기본값)
- ASLR을 지원하지 않는 바이너리는 기본 주소 0x400000을 갖는다.
DWORD rebase(DWORD address, DWORD newBase)
{
DWORD diff = address - 0x400000;
return diff + newBase;
}
- 위 함수에서 newBase를 찾아내기 위해선 GetModuleHandle() 함수를 사용하면 됨
- 매개변수가 NULL일 경우, 이 함수는 항상 프로세스 메인 핸들을 반환함
- 이 값은 바이너리가 매핑된 주소를 가리킴
- 이 값이 기본 주소이기 때문에 이를 DWORD로 설정해서 newBase를 얻을 수 있다.
- 실제로는 다른 주소의 기본 주소를 찾아야 함
- 이를 위해 CreateRemoteThread 함수를 사용한다.
- CreateRemoteThread 함수를 통해 스레드를 생성하고 원격 프로세스상에서 코드를 수행함
- 생성된 스레드는 StartAddress를 통해 시작됨
- Param을 매개변수로 가진 함수처럼 동작하고, 스레드 종료 코드로 값을 반환함
- 스레드가 GetModuleHandle() 주소를 가리키는 StartAddress에서 시작
- WaitForSingleObject()로 스레드가 끝나기를 기다림
- GetExitCodeThread()로 결과 값(기본 주소) 반환
DWORD newBase;
HMODULE k32 = GetModuleHandle(L"kernel32.dll");
LPVOID funcAdr = GetProcAddress(k32, "GetModuleHandleA");
if (!funcAdr)
funcAdr = GetProcAddress(k32, "GetModuleHandleW");
HANDLE thread =
CreateRemoteThread(process, NULL, NULL,
(LPTHREAD_START_ROUTINE)funcAdr,
NULL, NULL, NULL);
WaitForSingleObject(thread, INFINITE);
GetExitCodeThread(thread, &newBase);
CloseHandle(thread);
- GetModuleHandle은 kernel32.dll에 포함되어 있는데
- 이 dll 주소는 모든 프로세스에서 동일하기 때문에 외부 봇에서도 동일함
- 따라서 GetProcAddress에서 가져온 주소도 타깃 프로세스의 주소와 동일함
- 내 함수가 찾은 GetProcAddress의 주소가 타깃 프로세스에서도 유효하다는 소리임
'게임 보안 > [서적] 봇을 이용한 게임 해킹' 카테고리의 다른 글
함수 포인터가 함수를 직접 가리키지 않는다!? (0) | 2022.10.02 |
---|---|
6. 코드 인젝션 (1) | 2022.10.01 |
4. 고급 메모리 포렌식 (0) | 2022.09.28 |
3. x86 어셈블리 크래시 코스 (1) | 2022.09.28 |
2. 코드에서 메모리로 - 기본 원리 (2) | 2022.09.24 |
댓글