前面介绍了使用CE和OD的简单使用:CE和OD介绍和使用CE查找阳光的教学:阳光基地址和偏移地址,下面先使用最简单的控制台程序来实现修改阳光的功能。
1.分析程序
我们的控制台程序想要修改植物大战僵尸游戏内的数据,它们不是同一个进程,因此我们就需要先找到植物大战僵尸的进程,然后跨进程访问阳光的地址,然后跨进程写入修改后的数据。
这些步骤都需要使用WindowsAPI,因此,对于复杂的函数会单独出一期介绍。
2.查找游戏窗口的句柄
可以使用FindWindows函数来查找进程的句柄,函数定义如下:
WINUSERAPI
HWND
WINAPI
FindWindowA(
_In_opt_ LPCSTR lpClassName,
_In_opt_ LPCSTR lpWindowName);
WINUSERAPI
HWND
WINAPI
FindWindowW(
_In_opt_ LPCWSTR lpClassName,
_In_opt_ LPCWSTR lpWindowName);
#ifdef UNICODE
#define FindWindow FindWindowW
#else
#define FindWindow FindWindowA
首先WINUSERAPI和WINAPI参考链接:Windows 编程之 WINUSERAPI 和 WINAPI 区别
然后HWND的H是handle句柄的意思,WND是Window窗口的意思
可以看到根据是否定义了UNICODE宏来使用不同的函数,由于默认定义了UNICODE宏,所以自动选用FindWindowW函数。
关于**In_opt** ,in表示这是输入参数,opt表示可以不设值。这个只是给编译器看的,不会参与编译。
LPCWSTR :LP长指针,C是const,WSTR是指w_char宽字符。
lpClassName:进程窗口的类名
lpWindowName:进程窗口的窗口名
要知道这两个参数,需要使用VS自带的Spy++工具:
Spy++同样也返回了窗口的句柄,但是窗口句柄是操作系统临时分配的,下次启动会改变。不可能每次使用这个工具采取获取,然后修改源代码。但是程序编译完成后,标题名和类名是固定的。
通过工具我们就可以这样调用函数:
HWND gameHandle = FindWindow(L"MainWindow", L"植物大战僵尸中文版");
if (gameHandle == NULL) {
cout << "游戏没有打开,获取窗口句柄失败" << endl;
return 0;
}
3.根据窗口句柄获取进程ID
相关的Windows API函数为:
WINUSERAPI
DWORD
WINAPI
GetWindowThreadProcessId(
_In_ HWND hWnd,
_Out_opt_ LPDWORD lpdwProcessId);
DWORD:定义为typedef unsigned long DWORD
_In_ :表示这个参数是输入参数
_Out_opt_ :out表示是输出参数,opt表示可选。
因为有传出参数了,就不需要返回值了。
函数官方链接
因此可以这样写:
DWORD dwPID= 0;
GetWindowThreadProcessId(gameHandle, &dwPID);
if (dwPID == 0) {
cout << "获取游戏窗口PID失败" << endl;
return 0;
}
4.根据进程ID获取进程句柄
相关API:
WINBASEAPI
HANDLE
WINAPI
OpenProcess(
_In_ DWORD dwDesiredAccess,
_In_ BOOL bInheritHandle,
_In_ DWORD dwProcessId
);
函数官网链接
dwDesiredAccess:官网参考链接选用值PROCESS_ALL_ACCESS ,表示进程对象的所有可能的访问权限
bInheritHandle:如果此值为 TRUE,则此进程创建的进程将继承句柄。 否则,进程不会继承此句柄。
dwProcessId:要打开的本地进程的标识符。
返回值:如果函数成功,则返回值是指定进程的打开句柄。如果函数失败,则返回值为 NULL。
因此代码如下:
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, false, dwPID);
if (hProcess == nullptr) {
cout << "获取游戏进程句柄失败" << endl;
return 0;
}
5.读取游戏进程内存
相关API:
WINBASEAPI
_Success_(return != FALSE)
BOOL
WINAPI
ReadProcessMemory(
_In_ HANDLE hProcess,
_In_ LPCVOID lpBaseAddress,
_Out_writes_bytes_to_(nSize,*lpNumberOfBytesRead) LPVOID lpBuffer,
_In_ SIZE_T nSize,
_Out_opt_ SIZE_T* lpNumberOfBytesRead
);
hProcess:想要读取的进程句柄。
lpBaseAddress: 要读取进程的基地址
lpBuffer:参出参数,表示从指定进程读取的值存放地址
nSize:要从指定进程读取的字节数。
lpNumberOfBytesRead:参出参数,表示实际读取字节数
返回值:如果该函数成功,则返回值为非零值。如果函数失败,则返回值为 0(零)。 要获得更多的错误信息,请调用 GetLastError。
函数官网链接
我们现在就要通过这个函数访问到阳光的基地址了,从之前的博客CE找阳光基地址得到阳光的基地址为:006A9EC0 ,一级偏移量0x768,二级偏移量为0x5560。
因此可以通过以下代码访问到动态的阳光地址:
//读取游戏进程内存
DWORD SunBaseAddress = 0x006A9EC0; //要读取的地址
DWORD SunBaseAddressValue = 0; //读取存放的值
DWORD Size = 0; //实际读取到的字节大小
if (ReadProcessMemory(hProcess, (void*)SunBaseAddress, &SunBaseAddressValue, sizeof(DWORD), (SIZE_T*)&Size) == 0) {
cout << "读取基地址失败" << GetLastError() << endl;
return 0;
}
DWORD SunOffsetFirst = 0x768; //一级偏移量
DWORD SunOffsetFirstValue = 0; //一级偏移量中存放的值
if (ReadProcessMemory(hProcess, (void*)(SunBaseAddressValue + SunOffsetFirst), &SunOffsetFirstValue, sizeof(DWORD), (SIZE_T*)&Size) == 0) {
cout << "读取一级偏移地址失败" << GetLastError() << endl;
return 0;
}
DWORD SunOffsetSecond = 0x5560; //一级偏移量
DWORD SunValue = 0; //一级偏移量中存放的值:阳光值
if (ReadProcessMemory(hProcess, (void*)(SunOffsetFirstValue + SunOffsetSecond), &SunValue, sizeof(DWORD), (SIZE_T*)&Size) == 0) {
cout << "读取阳光地址失败" << GetLastError() << endl;
return 0;
}
cout << "现在的阳光是:" << SunValue << endl;
运行发现抱错:
解决方法:可以在“ 项目 ---- 属性 ---- 配置属性 ---- C/C++ ---- 代码生成 ---- 基本运行时检查:”设置为默认值,点击应用,确定后即可。参考链接
运行结果如下,成功获取当前阳光值:
6.修改游戏进程内存
相关API:
WINBASEAPI
_Success_(return != FALSE)
BOOL
WINAPI
WriteProcessMemory(
_In_ HANDLE hProcess,
_In_ LPVOID lpBaseAddress,
_In_reads_bytes_(nSize) LPCVOID lpBuffer,
_In_ SIZE_T nSize,
_Out_opt_ SIZE_T* lpNumberOfBytesWritten
);
hProcess:要修改的进程内存的句柄。
lpBaseAddress:要写入进程的内存地址
lpBuffer:要写入的数据地址
nSize:要写入的字节大小
lpNumberOfBytesWritten:实际写入的字节数。
返回值:如果该函数成功,则返回值为非零值。如果函数失败,则返回值为 0(零)。 要获得更多的错误信息,请调用 GetLastError。
官网链接
代码如下:
cout << "想要修改的阳光值:" << endl;
cin >> SunValue;
if (WriteProcessMemory(hProcess, (void*)(SunOffsetFirstValue + SunOffsetSecond), &SunValue, sizeof(DWORD), (SIZE_T*)&Size) == 0) {
cout << "写入地址失败" << endl;
}
效果如下: