Python还可以写游戏外挂?实操一下给你看看

相关阅读

视频演示

图文教程

上一次写了一个关于游戏逆向的文章,只讲了一些思路,并没有用代码体现出来,总感觉有些不完整

杂谈

所以这次就来一个完整版,从分析到写代码,一步步做出外挂~~

内容可能有些长,基础讲的比较多,但都是我对游戏逆向的理解,文中有描述不对或错误的地方请高手指正

本篇主要是面向广大网友写的,不是面向大佬写的,所以内容基础

其实做外挂不是目的,明白其中的原理和知识才是最重要的

我想多说几句:

有些人可能认为做外挂很简单,毕竟有 CE 这样的神器,普通小白就能修改,只要再学一些编程,基本就能上手了

但是我想说,没有那么容易~~

很多东西当你真的深入进去研究的时候,会发现根本没有想象的那么简单,只会使用工具那是皮毛……….

就拿这次做外挂举例,我遇到的一些问题:

  • python能否对程序内存读写?内存读写32位和64位有什么区别?如何用python动态获取模块基址?python调用API和C有什么不同?
  • 模块基址变化应该怎么解决?怎么通过一个数据找到其它数据?编译器里运行和打包后运行,正则匹配错误怎么办?……..

看似很简单的外挂,制作过程中会遇到很多问题,甚至有些问题网上几乎找不到解决方法,只能自己尝试摸索解决…….

总之一句话,实践才是检验真理的唯一标准

python主要是用来写脚本的,网上很多外挂是用C、C++、易语言写的,很少见到用python写外挂的

因为C/C++更接近底层,指针的存在让C/C++对内存操作有着天然的优势,所以外挂用C/C++的多

但是,作为猪头网站Python领域优质作者,我决定用python写(手动滑稽~~)

经过几天的摸索,最终我也是用python写出来了

本篇文章既是分享,也是对自己的一个总结。

正文

下面进入正题:

很多人都知道,用CE先找到动态地址,再根据动态地址找偏移,最后找到基址

问题就来了,什么是动态地址、静态地址?

下面我用C++举例,从代码层面浅析一下,可能有描述的不到位或者错误的地方,还请路过的大佬帮忙指正

首先创建一个全局变量a,和局部变量b,并且打印它们的地址

图片[1]-Python还可以写游戏外挂?实操一下给你看看-FancyPig's blog

第一次打印地址:

图片[2]-Python还可以写游戏外挂?实操一下给你看看-FancyPig's blog

可以看到 全局变量a 地址为004DC000,局部变量b 地址为0137F9E8

然后我们再打印一次地址:

图片[3]-Python还可以写游戏外挂?实操一下给你看看-FancyPig's blog

第二次打印,a的地址没变,b的地址变为005EFBF4

那这是不是就说明了,程序重新运行的时候,全局变量的地址不变,局部变量地址会改变

那就多打印几次,看看是否和猜想的一样,经过多次打印输出,发现和前两次情况一样,就表明猜想是对的

那为什么每次重新运行,全局变量地址不变,而局部变量地址改变呢?

因为局部变量 b 是在main函数里的,程序启动调用main函数,就会给main函数分配一个栈空间用来存储函数里的数据,当main函数调用完之后变量被销毁、栈空间会被回收,当下次再运行程序时就会重新给main函数分配一个栈空间,栈空间是随机分配的,也就是说,前后两次分配的栈空间不一定在同一段内存区域,既然栈空间不是在同一内存区域,那栈里的数据地址也都不一样了,所以,这就是局部变量b地址变化的原因

全局变量 a 没有声明在任何一个函数内,作用范围在程序运行始终存在,全局变量在编译时就已经分配地址了,而且这个地址是固定的线性地址,不管程序运行多少次,运行多少个实例,它的地址始终是确定的,而且是唯一的线性地址,这就是全局变量a地址不变的原因

需要注意的是,程序的局部变量存在于堆栈中,全局变量存在于静态区中,动态申请数据存在于堆中。

既然全局变量地址不变,局部变量地址一直变化,那 a 和 b 的地址在汇编中又是如何体现的?

我们在a=20处下个断点,看一下汇编代码

图片[4]-Python还可以写游戏外挂?实操一下给你看看-FancyPig's blog
图片[5]-Python还可以写游戏外挂?实操一下给你看看-FancyPig's blog

可以看到,int b=10 在汇编中是 dword ptr [ebp-0Ch] , 0Ah 0Ah就是十进制的10,将 0Ah 传递到了 [ebp-0Ch] 地址里

a=20 在汇编中是 dword ptr ds:[0041C144h] , 14h 14h就是十进制20,将14h传递到了 [0041C144h] 地址里

通过对比可以发现,变量b的地址是 [ebp-0Ch],变量a的地址是 [0041C144h]

变量b地址中,ebp是寄存器(基址指针寄存器),ebp-0Ch 指向的就是变量b的内存地址,0Ch就是偏移,ebp始终指向系统栈最上面一个栈帧的底部,也是就通常所说的栈底指针,因为函数调用和结束会伴随栈的开辟和回收,ebp作为栈底指针也是在不断的变化,这也说明了 局部变量b 地址变化的原因

全局变量a 的地址并没有其它寄存器,而是直接一个4字节地址,也没有偏移,这就说明 全局变量a 的地址是 “写死” 的,一直就是这个值不会变。

而我们最后通过对游戏的分析,就是要找到像 全局变量a 这样 “写死” 的地址。

问题又来了,怎么找到这样的地址?

既然是游戏逆向,我们就要从游戏开发者的角度去思考,去试着猜想一下游戏是怎么写出来的,内部结构是怎样的?

游戏开发应该都是面向对象,前面我们说过,局部变量在函数调用完成后就会被销毁,栈空间也会被回收

游戏中有些数据,比如人物经验、等级、金币,这些东西从打开游戏开始就一直存在,也没有消失

所以,这些数据不是局部变量,全局变量基本也不可能,那有可能是什么呢?

我们可以猜想,既然是面向对象,游戏开发者可能是通过指针创建对象,通过对象来访问类中的属性和方法

猜想游戏结构:

#include <iostream>
using namespace std;
​
// 人物属性类
struct Person
{
    int gold;      //金币数量
    int EXP;       //经验值  
    int grade;     //等级
    int skill;     //技能
};
​
// 武器属性类
struct Weapon
{
    int bullet;    //子弹数量
    int accuracy;  //精准度
    int power;     //威力
    int distance;  //射程
};
​
// 游戏数据
struct GameData
{
    int temp1;    //数据一
    int temp2;    //数据二
    int temp3;    //数据三
​
    Person *person;   //人物属性指针
    Weapon *weapon;   //武器属性指针
};
​
//游戏数据指针
GameData *gamedata;
​
int main()
{
    gamedata= new GameData();         //创建类GameData对象
    gamedata->person = new Person();  //创建类Person对象
    gamedata->weapon = new Weapon();  //创建类Weapon对象
​
    system("pause");
    return 0;
}

上面可以看到,定义Person、Weapon、GameData三个类,GameData类中又定义了Person、Weapon类指针

又通过指针gamedata去指向 GameData类 (gamedata在全局变量中),这样可以方便游戏开发者通过指针访问类中的属性和方法

然后又在main函数中,通过指针 gamedata 创建对象,通过对象对类属性进行操作

new分配的空间在堆上,用完之后堆肯定是要释放的,那么new出来的对象 空间首地址肯定会发生变化

但是,gamedata是一个全局变量,gamedata指针指向 GameData 类,而GameData类中又定义了Person、Weapon类指针

我们可以打印一下,观察 gamedata 指针本身的地址和 gamedata 指针里存放的地址,是否发生变化

第一次打印:

图片[6]-Python还可以写游戏外挂?实操一下给你看看-FancyPig's blog

第二次打印:

图片[7]-Python还可以写游戏外挂?实操一下给你看看-FancyPig's blog

可以看到,指针 gamedata 的地址不变,而它所指向的地址发生改变

那么,我们可以想到什么?

不管指针里的地址怎么变,只要指针本身的地址不变,就可以通过 “不变” 找到 “变” 的地址

只要找到 gamedata 指针的地址,我们就可以通过指针和偏移间接访问类中的所有属性

思路图:

图片[8]-Python还可以写游戏外挂?实操一下给你看看-FancyPig's blog

这就解释了为什么可以通过静态地址找到动态地址

我们还知道,一个函数或结构体里的多个变量在内存区域中的地址是连续的,也就是说,我只要找到了类里的其中一个属性

就可以通过内存地址连续的特性,挖掘出游戏里的其他数据,我们可以打印一下Person类中的所有属性地址来看一下

#include <iostream>
using namespace std;

// 人物属性类
struct Person
{
	int gold;      //金币数量
	int EXP;       //经验值  
	int grade;     //等级
	int skill;     //技能
};

// 武器属性类
struct Weapon
{
	int bullet;    //子弹数量
	int accuracy;  //精准度
	int power;     //威力
	int distance;  //射程
};

// 游戏数据
struct GameData
{
	int temp1;    //数据一
	int temp2;    //数据二
	int temp3;    //数据三

	Person *person;   //人物属性类指针
	Weapon *weapon;   //武器属性类指针
};


GameData *gamedata;

int main()
{
	gamedata= new GameData();         //创建类GameData对象
	gamedata->person = new Person();  //创建类Person对象
	gamedata->weapon = new Weapon();  //创建类Weapon对象


	cout <<  "人物金钱地址:"<<&gamedata->person->gold << endl;   //打印人物金钱地址
	cout <<  "人物经验地址:"<<&gamedata->person->EXP << endl;    //打印人物经验地址
	cout <<  "人物等级地址:"<<&gamedata->person->grade << endl;  //打印人物等级地址
	cout <<  "人物技能地址:"<<&gamedata->person->skill << endl << endl; //打印人物技能地址


	system("pause");
	return 0;
}
图片[9]-Python还可以写游戏外挂?实操一下给你看看-FancyPig's blog

可以看到,每个地址都只相差4个字节(32位),我们只要找到其中任何一个地址,通过4字节的偏移就可以挖掘出其他游戏数据

相同的,在Person类中,我们只要找到Person类的首地址,就可以通过计算偏移,来找到我们需要的那个属性地址

举个例子,Person类首地址为 0x100,我想找到 grade 地址,那么只需要在 0x100 的基础上再加上8个字节的偏移就是 grade 地址

不管 grade 地址怎么变,我只需要知道和它关联的其它地址,我就一定能通过偏移找到它。

以上就是对游戏整体结构的简单分析。

接下来就是,找基址的过程~~

CE 大家应该都会用,就直接跳过了,我们在 CE 找到动态地址之后,通过x64dbg进行分析

用CE找到的金币动态地址:0x1196645E368

我们在x64dng右下方的地址栏点击转到,跳转到改地址,查看数值

图片[10]-Python还可以写游戏外挂?实操一下给你看看-FancyPig's blog
图片[11]-Python还可以写游戏外挂?实操一下给你看看-FancyPig's blog

跳转之后,可以看到我们游戏里的金币数值,如果跳转后是十六进制,转换一下就行了

图片[12]-Python还可以写游戏外挂?实操一下给你看看-FancyPig's blog

此时,我们在刚才的地址下一个 硬件访问断点,用来追踪地址数据来源,因为游戏是64位的,所以选择 四字(Q)选项

图片[13]-Python还可以写游戏外挂?实操一下给你看看-FancyPig's blog

下好 硬件断点之后,x64dbg的汇编区会自动跳转到访问 0x1196645E368 地址最近的一行汇编代码的下一行

断点的上一行汇编,就是我们要找的关键代码,也就是一级偏移处

图片[14]-Python还可以写游戏外挂?实操一下给你看看-FancyPig's blog

此时我们看到,关键代码处为:mov r8d,dword ptr ds:[rdi+368],[rdi+368] 的地址里的值会传递给 r8d,可以看出 rdi+368 是一个动态地址,因为 368 是固定的,所以我们就需要找到 rdi寄存器 地址的来源,找到 rdi 看是否还有变化的地址,如果有就继续往上找,一直找到不会变动的地址为止。

图片[15]-Python还可以写游戏外挂?实操一下给你看看-FancyPig's blog

这个游戏还是比较简单的,只找了一次就找到了,qword ptr ds:[7FF6F9E73198] 是不是感觉和上面所讲的全局变量 a 的汇编代码有异曲同工之妙,没错,这就是 “静态地址”(为什么会加引号,往下看就明白了)

当我把游戏重新打开的时候,再重复上面的操作,发现那个 “静态地址” 没变

也就是说,[7FF6F9E73198]+368——>金币动态地址

正当我以为要成功的时候,挑战才刚刚开始,问题也接踵而至~~~

当我关闭电脑,重新开启电脑、打开游戏,发现 “静态地址” 竟然改变了

图片[16]-Python还可以写游戏外挂?实操一下给你看看-FancyPig's blog

从 7FF6F9E73198 变为了 7FF6CBD43198,本以为这样就成功了,看来还是想的太简单了

这个地址已经不能再往上找了,因为这地址是 “写死” 的,没有偏移,不像 [rdi+368] 那样可以找 rdi 里的地址值

这也是为什么上文我会在 “静态地址” 上加引号,因为这不是真正意义上的静态

那该怎么办?

于是我又重新打开了CE,在CE上找一下灵感,我发现这个地址是来源一个 [模块+偏移] 的地址中

图片[17]-Python还可以写游戏外挂?实操一下给你看看-FancyPig's blog
图片[18]-Python还可以写游戏外挂?实操一下给你看看-FancyPig's blog

[theHunterCotW_F.exe + 2113198] 里存放的地址,就是我们刚才找到的 “静态地址”

theHunterCotW_F.exe 是一个游戏模块

那么,如果我能知道 [模块+偏移] 的地址,那不就间接知道 那个“静态地址” 了吗,不管那个 “静态地址” 怎么变

我只要知道 [模块+偏移] 的地址就可以找到 “静态地址”

关键是怎么找 [模块+偏移] 的地址?

于是,我猜测,一般 [模块+偏移] 的地址中,模块一般都是取的基地质,[模块+偏移] == [模块基址+偏移]

那么我只需要在 x64dbg 上找到该模块的基址不就行了吗,x64dbg上按住 Alt + E,可以看到游戏运行所需要的模块

图片[19]-Python还可以写游戏外挂?实操一下给你看看-FancyPig's blog

很轻松和就找到了模块基址,那真正的金币地址应该是这样的:

[7FF6C9C30000+2113198]+368 ==金币地址

但是问题又来了,我们上文知道,那个 “静态地址” 是会改变的,而 “静态地址” == [模块基址+2113198]

既然 “静态地址” 会改变,那模块基址也会改变

我们如何动态的获取模块基址,是问题的关键!这个留到代码部分会有说明

当我们找到了动态获取模块基址的方法后,那又如何去挖掘其它数据呢?

还记得我上面讲过,同一个函数或结构体中,相邻的变量内存地址也是相邻的,我们只需要找到一个变量的地址

加上4字节或8字节的偏移,就可以找到其它变量的地址

如下:

图片[20]-Python还可以写游戏外挂?实操一下给你看看-FancyPig's blog

可以发现,地址基本都是连续的,相差4字节或8字节

编写python代码

接下来就是python写代码阶段了~~

在用 python 写代码之前需要了解一些API函数、句柄、内存操作的一些知识

什么是Windows API?

Windows 这个多作业系统除了协调应用程序的执行、分配内存、管理资源之外, 它同时也是一个很大的服务中心,调用这个服务中心的各种服务(每一种服务就是一个函数),可以帮应用程式达到开启视窗、描绘图形、使用周边设备等目的,由于这些函数服务的对象是应用程序(Application), 所以便称之为 Application Programming Interface,简称 API 函数。——来源百度

简单来时,API就是接口,你可以用程序调用这些接口来达到一些功能

什么是句柄?

句柄 ( handle )是 Windows操作系统 用来标识被应用程序所建立或使用的对象的整数。其本质相当于带有引用计数的智能指针 。当一个 应用程序 要引用其他系统(如数据库 、 操作系统)所管理的内存块或对象时,可以使用句柄。

打个比方,切菜的刀,你只有握住了刀把,才能使用这个刀去切菜,刀把 就相当于 句柄。

为什么要进行内存操作?

游戏的数据都在内存中,我们经过上面的步骤找到数据所在的内存地址,那么就要去修改地址里的数据,这就是外挂的本质

本次python需要的模块为:

mport win32gui       #获取窗口句柄
import win32api       #创建进程句柄与关闭进程句柄
import win32process   #通过窗口句柄获取进程ID
from win32con import PROCESS_ALL_ACCESS #Opencress 权限
import ctypes     #外部函数库,提供了与 C 兼容的数据类型,并允许调用 DLL 或共享库中的函数
import re         #正则

有些萌新可能不知道,什么是窗口句柄、进程句柄、进程ID?

我就来简单的说一下:

窗口句柄就是窗口的钥匙,每个窗口在被创建出来之后就会被赋予一个句柄,该句柄(句柄实则上是一个指针)指向一个数据结构体,结构体里明确表示着该窗口的各种信息,窗口大小,窗口名等,当我们得到这个句柄时就可以请求操作系统对它做一系列操作,列如:移动窗口,关闭窗口,最小化最大化等,也就是说,得到了窗口句柄我们就有权限去操作这个窗口

进程ID是当一个进程被创建出来时系统内核为其分配的一个名字/绰号,任务管理器中就可以看到所有进程的ID号(PID)

进程分配的首地址在GDT(局域的叫LDT)表中,进程ID会被保存到该进程的PCB进程控制块中

进程句柄指向进程下的PCB进程控制块,当我们要对进程进行I/O操作时候时候需要知道进程的堆栈地址范围以及状态才能的值对应的LDT/GDT并转化为物理地址(或通过段选择符进行转换),操作系统才能为我们对该进程进行读写操作,所以一般我们会通过进程ID来获取进程句柄(临时的),来对进程进行操作。

简单来说:

人都有一个身体和大脑,身体就相当于窗口句柄,大脑就是进程句柄

身体是对外展示的,而大脑是在控制身体应该怎样对外展示,所以窗口就是对用户进行可视化界面交互的,而进程里的数据和指令在控制着窗口应该怎样交互!

所以,流程就是 窗口句柄——>进程ID——>进程句柄——>获取权限——>内存改写

我们还需要引入一个Windows内核级文件——kernel32.dll

什么是kernel32.dll?

kernel32.dll是Windows 中非常重要的32位动态链接库文件,属于内核级文件。它控制着系统的内存管理、数据的输入输出操作与中断处理,当Windows启动时,kernel32.dll就驻留在内存中特定的写保护区域,使别的程序无法占用这个内存区域。

在Windows系统中,每个.exe文件在双击打开时都会加载 kernel32.dll 这个系统模块

内存操作肯定少不了kernel32.dll

还有一些API函数也需要说明一下

WriteProcessMemory():从英文也可以看出来,就是写进程内存,对进程内存进行写入修改操作

BOOL WriteProcessMemory(
  [in]  HANDLE  hProcess,
  [in]  LPVOID  lpBaseAddress,
  [in]  LPCVOID lpBuffer,
  [in]  SIZE_T  nSize,
  [out] SIZE_T  *lpNumberOfBytesWritten
);

对于其中的参数用法,可以查看文档:WriteProcessMemory function (memoryapi.h) – Win32 apps |微软文档 (microsoft.com)

ReadProcessMemory():读进程内存,对进程内存进行读取

BOOL ReadProcessMemory(
  [in]  HANDLE  hProcess,
  [in]  LPCVOID lpBaseAddress,
  [out] LPVOID  lpBuffer,
  [in]  SIZE_T  nSize,
  [out] SIZE_T  *lpNumberOfBytesRead
);

对于其中的参数用法,可以查看文档:ReadProcessMemory function (memoryapi.h) – Win32 apps | Microsoft Docs

FindWindow():直译为 “寻找窗口” ,就是获取窗口句柄

HWND FindWindowA(
  [in, optional] LPCSTR lpClassName,
  [in, optional] LPCSTR lpWindowName
);

对于其中的参数用法,可以查看文档:FindWindowA function (winuser.h) – Win32 apps | Microsoft Docs

GetWindowThreadProcessId():直译为 “获取窗口线程进程Id”,就是获取窗口的进程ID和线程ID

DWORD GetWindowThreadProcessId(
  [in]            HWND    hWnd,
  [out, optional] LPDWORD lpdwProcessId
);

对于其中的参数用法,可以查看文档:GetWindowThreadProcessId function (winuser.h) – Win32 apps | Microsoft Docs

OpenProcess():直译为 “打开进程”,就是获取进程的读写权限

HANDLE OpenProcess(
  [in] DWORD dwDesiredAccess,
  [in] BOOL  bInheritHandle,
  [in] DWORD dwProcessId
);

对于其中的参数用法,可以查看文档:OpenProcess function (processthreadsapi.h) – Win32 apps | Microsoft Docs

LoadLibrary():直译为 “加载、调用”,就是将指定的模块加载到调用进程的地址空间中

HMODULE LoadLibraryA(
  [in] LPCSTR lpLibFileName
);

对于其中的参数用法,可以查看文档:LoadLibraryA function (libloaderapi.h) – Win32 apps |微软文档 (microsoft.com)

了解这些API函数用法之后,就可以用python写代码了

首先导入需要的模块

import win32gui       #获取窗口句柄
import win32api       #创建进程句柄与关闭进程句柄
import win32process   #通过窗口句柄获取进程ID
from win32con import PROCESS_ALL_ACCESS #Opencress 权限
import ctypes     #外部函数库,提供了与 C 兼容的数据类型,并允许调用 DLL 或共享库中的函数
import re         #正则

获取进程句柄

    hwnd = win32gui.FindWindow(None, "theHunter: Call of the Wild")  # 获取窗口句柄
    pid = win32process.GetWindowThreadProcessId(hwnd)[1]             # 获取进程id
    hProcess = win32api.OpenProcess(PROCESS_ALL_ACCESS, False, pid)  # 获取进程句柄

然后就是获取动态模块基址

上面说到,这个游戏的模块基址是变化的,那怎么用代码动态的获取呢? 我在网上也找了一些获取模块基址的方法,但都是C/C++的,没有python的方法

于是,我灵机一动,上面不是有一个API函数吗——LoadLibrary()

LoadLibrary()函数可以加载模块到进程空间,并且返回模块句柄,而模块句柄里就含有模块基址,那我再用一次正则把模块基址匹配出来不就行了嘛,我真是大聪明(嘿嘿嘿~~~)

    #获取模块基址
    mokuai = str(ctypes.windll.LoadLibrary(r"F:\荒野的召唤\theHunterCallOfTheWild\theHunterCotW_F.exe")) #模块路径
    b = mokuai.split()[-3]              #正则匹配
    c = int(b, 16)                      # 将16进制字符串转换成整数

获取模块基址之后,就可以获取 “静态地址” 了

    # “静态地址”
    module_baseaddr = c + 0x2113198
    addr_base = ReadProcessMemory64(hProcess, module_baseaddr)

定义一个读内存的函数,方便后续调用

def ReadProcessMemory64(hProcess, addr):
    # 这里定义一个函数来读取,传入两个参数,第一个是进程句柄,第二个是我们要读取的地址,长度默认为8
    addr = ctypes.c_ulonglong(addr)
    ret = ctypes.c_ulonglong()
​
    kernel32.ReadProcessMemory(int(hProcess), addr, ctypes.byref(ret), 8, 0)
    return ret.value

同理定义一个写内存的函数,方便后续调用

def WriteProcessMemory64(hProcess,addr, revamp):
    addr = ctypes.c_ulonglong(addr)
    ret = ctypes.c_ulonglong(revamp)
    kernel32.WriteProcessMemory(int(hProcess), addr, ctypes.byref(ret), 4, 0)
    return ret.value

定义一些函数来修游戏对应的数值

def grade(value):
    # 计算等级地址
    offset1 = addr_base + 0x2D4    #静态地址+偏移
    ReadProcessMemory64(hProcess,offset1)
    WriteProcessMemory64(hProcess, offset1,value)
    print("修改成功!")
def gold(value):
    # 计算金币地址
    offset1 = addr_base + 0x368  #静态地址+偏移
    ReadProcessMemory64(hProcess,offset1)
    WriteProcessMemory64(hProcess, offset1,value)
    print("修改成功!")
def EXP(value):
    # 计算经验地址
    offset1 = addr_base + 0x2D8  #静态地址+偏移
    ReadProcessMemory64(hProcess,offset1)
    WriteProcessMemory64(hProcess, offset1,value)
    print("修改成功!")

然后给上面的函数传参

    #修改等级
    grade_value=int(input("请输入要修改的等级:"))
    grade(grade_value)
​
    #修改金币
    gold_value=int(input("请输入要修改的金币:"))
    gold(gold_value)
​
    #修改经验
    EXP_value=int(input("请输入要修改的经验值:"))
    EXP(EXP_value)

基本上就做好外挂了

下面是完整代码

import win32gui       #获取窗口句柄
import win32api       #创建进程句柄与关闭进程句柄
import win32process   #通过窗口句柄获取进程ID
from win32con import PROCESS_ALL_ACCESS #Opencress 读写权限
import ctypes         #外部函数库,提供了与 C 兼容的数据类型,并允许调用 DLL 或共享库中的函数
import re             #正则
​
kernel32 = ctypes.windll.LoadLibrary(r"C:\Windows\System32\kernel32.dll")
​
# 读内存函数64位
def ReadProcessMemory64(hProcess, addr):
    # 这里定义一个函数来读取,传入两个参数,第一个是进程句柄,第二个是我们要读取的地址,长度默认为8
    addr = ctypes.c_ulonglong(addr)
    ret = ctypes.c_ulonglong()
    kernel32.ReadProcessMemory(int(hProcess), addr, ctypes.byref(ret), 8, 0)
    return ret.value
​
# 写内存函数64位
def WriteProcessMemory64(hProcess,addr, revamp):
    addr = ctypes.c_ulonglong(addr)
    ret = ctypes.c_ulonglong(revamp)
    kernel32.WriteProcessMemory(int(hProcess), addr, ctypes.byref(ret), 4, 0)
    return ret.value
​
​
def grade(value):
    # 计算等级地址
    offset1 = addr_base + 0x2D4
    ReadProcessMemory64(hProcess,offset1)
    WriteProcessMemory64(hProcess, offset1,value)
    print("修改成功!")
​
def gold(value):
    # 计算金币地址
    offset1 = addr_base + 0x368
    ReadProcessMemory64(hProcess,offset1)
    WriteProcessMemory64(hProcess, offset1,value)
    print("修改成功!")
​
def EXP(value):
    # 计算经验地址
    offset1 = addr_base + 0x2D8
    ReadProcessMemory64(hProcess,offset1)
    WriteProcessMemory64(hProcess, offset1,value)
    print("修改成功!")
​
def People_skills(value):
    # 计算人物技能点地址
    offset1 = addr_base + 0x2DC
    ReadProcessMemory64(hProcess,offset1)
    WriteProcessMemory64(hProcess, offset1,value)
    print("修改成功!")
​
def weapon(value):
    # 计算武器技能点地址
    offset1 = addr_base + 0x2E0
    ReadProcessMemory64(hProcess,offset1)
    WriteProcessMemory64(hProcess, offset1,value)
    print("修改成功!")
​
def rifle(value):
    # 计算步枪熟练度地址
    offset1 = addr_base + 0x380
    ReadProcessMemory64(hProcess,offset1)
    WriteProcessMemory64(hProcess, offset1,value)
    print("修改成功!")
​
def pistol(value):
    # 计算手枪熟练度地址
    offset1 = addr_base + 0x384
    ReadProcessMemory64(hProcess,offset1)
    WriteProcessMemory64(hProcess, offset1,value)
    print("修改成功!")
​
def shotgun(value):
    # 计算霰弹枪熟练度地址
    offset1 = addr_base + 0x388
    ReadProcessMemory64(hProcess,offset1)
    WriteProcessMemory64(hProcess, offset1,value)
    print("修改成功!")
​
def bow_arrow(value):
    # 计算弓箭熟练度地址
    offset1 = addr_base + 0x38C
    ReadProcessMemory64(hProcess,offset1)
    WriteProcessMemory64(hProcess, offset1,value)
    print("修改成功!")
​
if __name__ == '__main__':
    hwnd = win32gui.FindWindow(None, "theHunter: Call of the Wild")  # 获取窗口句柄
    pid = win32process.GetWindowThreadProcessId(hwnd)[1]  # 获取线程id
    hProcess = win32api.OpenProcess(PROCESS_ALL_ACCESS, False, pid)
​
    #获取模块基址
    mokuai = str(ctypes.windll.LoadLibrary(r"F:\荒野的召唤\theHunterCallOfTheWild\theHunterCotW_F.exe"))#输入模块路径
    b = mokuai.split()[-3]   #正则匹配
    c = int(b, 16)  # 将16进制字符串转换成整数
​
    # “静态地址”
    module_baseaddr=c+0x2113198
    addr_base=ReadProcessMemory64(hProcess,module_baseaddr)
​
    #修改等级
    grade_value=int(input("请输入要修改的等级:"))
    grade(grade_value)
​
    #修改金币
    gold_value=int(input("请输入要修改的金币:"))
    gold(gold_value)
​
    #修改经验
    EXP_value=int(input("请输入要修改的经验值:"))
    EXP(EXP_value)
​
    #修改人物技能点
    People_skills_value=int(input("请输入要修改的人物技能点:"))
    People_skills(People_skills_value)
​
    #修改武器技能点
    weapon_value=int(input("请输入要修改的武器技能点:"))
    weapon(weapon_value)
​
    #修改步枪熟练度
    rifle_value=int(input("请输入要修改的步枪熟练度:"))
    rifle(rifle_value)
​
    #修改手枪熟练度
    pistol_value=int(input("请输入要修改的手枪熟练度:"))
    pistol(pistol_value)
​
    #修改霰弹枪熟练度
    shotgun_value=int(input("请输入要修改的霰弹枪熟练度:"))
    shotgun(shotgun_value)
​
    #修改弓箭熟练度
    bow_arrow_value=int(input("请输入要修改的弓箭熟练度:"))
    bow_arrow(bow_arrow_value)
    
    # 关闭句柄
    win32api.CloseHandle(hProcess)

到这里基本也快结束了

从分析到写代码,一步一步用 python 写出来

可以看到,涉及到很多知识,比如Windows API的调用、句柄的获取、汇编的分析、代码的编写

这期间会遇到很多意想不到的问题,比如模块基址变化怎么解决?64位程序内存操作应该怎么写?打包运行出错怎么办?…….

这还算一个简单的游戏,偏移少,也基本没保护,像那种大型游戏估计更难了

总结

最后还想说:

只用工具谁都会,关键是懂背后的原理,不要仅仅局限于会用工具就行了,工具是辅助可以用,但是也要知道原理

希望有条件的小伙伴,可以自己动手实际操作一遍,真正感受一下外挂开发从开始到结束的过程

深入进去研究真的可以学到不少知识~~~~

可能对于逆向大佬来说,这些都是小意思,但是对我来说还是学到了很多,也是第一次用Python写外挂

有路过的大佬如果看到文章里有错误,可以帮忙指正修改哦,写文章也是一种学习~~~

© 版权声明
THE END
喜欢就支持一下吧
点赞69 分享
评论 共2条

请登录后发表评论