일전에
Visual Studio 에서 Intel Pin 프로젝트를 설정하는 방법을 올린 적이 있습니다.
이번에는 응용차원에서 Pin 을 활용하는 방법을 올려봅니다. @_@
주 업무분야가 '게임보안' 인만큼 '게임 해킹툴 분석' 측면으로 접근해봤습니다... ^^;;
Pin 을 이용한 메모리 조작 분석
캐릭터의 공격력을 높이기 위해 '공격 데미지 값' 이라는
데이터를 조작하거나...
캐릭터가 받는 데미지를 없애기 위해 '데미지 처리 로직' 이라는
코드를 조작하거나...
게임 해킹툴이 게임 치팅을 위해 가장 많이 사용하는 방식 중 하나가
'메모리 조작' 입니다.
'메모리 조작' 은 조작하는 방식에 따라 아래와 같이 크게 두 종류로 나눌 수 있습니다.
- 게임 프로세스 외부의 다른 프로세스에서 WriteProcessMemory 로 조작.
- 게임 프로세스 내부에 DLL 을 인젝션 시켜서 직접 값을 조작.
여기서는 DLL 을 인젝션 시켜서 직접 값을 조작하는 방식을 대상으로
Pin 을 활용해보도록 하겠습니다.
테스트 편의를 위해 Sample.exe 와 SampleDll.dll 을 간단하게 구현했습니다.
[ Sample.zip 다운로드 ]
- Sample.exe : 게임 프로세스
- SampleDll.dll : 게임 프로세스에 인젝션되는 게임 해킹툴
Sample.exe 가 실행될 때 SampleDll.dll 을 로드하고...
DLL 이 로드된 후, 'F1' 키를 누르면~ SampleDll.dll 이 Sample.exe 의 메모리를 조작합니다.
|
Sample.exe 실행 |
'F1' 키를 눌렀을 때... 실행되는 SampleDll.dll 의 코드는 아래와 같습니다.
게임 해킹툴이 메모리 조작 시 자주 사용하는 형태입니다.
|
SampleDll.dll 의 메모리 조작 코드 (소스코드) |
참고로 hModule 은 Sample.exe 프로세스의 베이스 주소입니다.
|
SampleDll.dll 의 메모리 조작 코드 (디스어셈블 코드) |
우리의 목표는 SampleDll.dll 에서 Sample.exe 의 메모리를 0x90 으로 조작하는 노란 박스의 코드를...
Pin 을 이용해서 찾아내는 겁니다. :)
핵심 API 는
"INS_MemoryOperandIsWritten" 입니다.
메모리 쓰기가 발생하는 경우 이 함수의 리턴값이 TRUE 가 됩니다.
"MOV", "AND", "SUB" 등의 명령으로 메모리 주소에 값이 써지는 경우는 물론...
"PUSH", "CALL" 등의 명령으로 스택 메모리에 값이 써지는 경우도 포함됩니다. @_@;;;
이 함수를 잘 이용하면 DLL 에서 게임 프로세스의 메모리를 조작하는 코드를 찾을 수 있습니다.
아래는 Pin Tool 예제 코드입니다
// MemTrace.cpp
#include "pin.H"
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
ofstream TraceLog;
UINT32 SampleBaseAddr = 0;
UINT32 SampleMappedSize = 0;
UINT32 DllBaseAddr = 0;
UINT32 DllMappedSize = 0;
//-------------------------------------------------------------------
INT32 Usage()
{
return -1;
}
VOID Fini(INT32 code, VOID *v)
{
TraceLog << endl << "#eof..." << endl;
if (TraceLog.is_open()) TraceLog.close();
}
VOID ImageLoad(IMG img, VOID *v)
{
// 메인 프로세스 : Sample.exe
if (IMG_IsMainExecutable(img) == TRUE) {
SampleBaseAddr = IMG_StartAddress(img);
SampleMappedSize = IMG_SizeMapped(img);
TraceLog << "* Process Name : " << IMG_Name(img) << endl;
TraceLog << " StartAddress : " << hexstr(SampleBaseAddr)
<< ", MappedSize : " << hexstr(SampleMappedSize) << endl;
}
// SampleDll.dll
if (IMG_Name(img).find("SampleDll.dll") != string::npos) {
DllBaseAddr = IMG_StartAddress(img);
DllMappedSize = IMG_SizeMapped(img);
TraceLog << "* Sample Dll : " << IMG_Name(img) << endl;
TraceLog << " StartAddress : " << hexstr(DllBaseAddr)
<< ", MappedSize : " << hexstr(DllMappedSize) << endl;
}
}
VOID Log_MemWrite(VOID *ip, string &MemWrite, VOID *addr)
{
UINT32 target = UINT32(addr);
if ((target >= SampleBaseAddr) &&
(target < (SampleBaseAddr + SampleMappedSize))) {
TraceLog << MemWrite << endl;
}
}
VOID Instruction(INS ins, VOID *v)
{
ADDRINT Address = INS_Address(ins);
if ((Address >= DllBaseAddr) &&
(Address <= (DllBaseAddr + DllMappedSize))) {
UINT32 memOperands = INS_MemoryOperandCount(ins);
for (UINT32 memOp = 0; memOp < memOperands; memOp++) {
if (INS_MemoryOperandIsWritten(ins, memOp)) {
string MemWrite = "- eip: " + hexstr(Address) +
" [WriteMem] " + INS_Disassemble(ins);
INS_InsertPredicatedCall(ins, IPOINT_BEFORE, (AFUNPTR)Log_MemWrite,
IARG_INST_PTR,
IARG_PTR, new string(MemWrite),
IARG_MEMORYOP_EA, memOp,
IARG_END);
}
}
}
}
//-------------------------------------------------------------------
int main(int argc, char *argv[])
{
PIN_InitSymbols();
if (PIN_Init(argc, argv)) return Usage();
TraceLog.open("MemTrace_Log.txt", ofstream::out);
TraceLog << "### Memory Trace Log ###" << endl << endl;
IMG_AddInstrumentFunction(ImageLoad, NULL);
INS_AddInstrumentFunction(Instruction, NULL);
PIN_AddFiniFunction(Fini, NULL);
PIN_StartProgram();
return 0;
}
빌드해서 생성된 MemTrace.dll 을 이용해보면~~
|
Sample.exe 실행 (with Pin - MemTrace.dll) |
|
MemTrace.dll 에서 남긴 로그 |
SampleDll.dll 에서 메모리 조작을 하는 코드가 깔끔하게(?) 로그 파일에 기록되었습니다~ :)
마치며
일반적으로 Pin 으로 프로그램을 실행시키면 그냥 실행시키는 것보다 속도가 느립니다.
게다가 Instrumentation 하는 범위가 넓을 수록... 로깅 위치가 많을 수록...
엄~~~청나게 느려집니다. -_-;;;;;;
Instrumentation 범위 조절과 로깅 위치 조절만 하더라도 실행 속도를 올릴 수 있으니...
적절하게 필터링을 해서 쓰는 걸로~ :)
5년 전에 회사에서 이것저것 시도하다 한계에 부딪혔던걸...
지금 올리네요... -_-;;;;;