前言


本文仅供读者预览学习研究之用,不得用作商业用途。如有侵权,请告之,本人将第一时间进行处理。

作为一名计算机系的学生,我自认动手能力不错。可回首大学这几年,写的程序不少,却未发现自己有什么能向别人展示的“通俗作品”…

怎么才算“通俗”,这我不好解释。但要说什么不通俗,却简单容易得多。例如花了将近一个学期当码农参与重构Online Judge…学计算机但不搞ACM的人尚且不知道什么是OJ,更不用提那些只会用电脑看网页玩游戏的“麻瓜”了。

为了能让常人感受下编程的魅力,也为了炫耀下个人的学习成果,我就“下海”一回。

目标


前言写的那么委婉,总结起来也无非就是一句话,做款辅助显摆下。同龄人都爱玩游戏,我期待他们看到这款辅助时所展露出的“哇!”的神情,这能让我饥渴的虚荣心得到满足。

自从踏上ACM这条不归路之后我发现自己变得功利了许多,奖项并不代表一切,但付出了那么多却收获甚微,这让我抑郁不已。“拿得起,放得下!”,我最近经常在心里默念这句话,但事实上这远比它说起来难得多。那些大三大四仍在第一线的ACMer应该能理解退役是件多么困难的事,特别是那些已经投入了两三年精力的老鸟们。

最近正值赛季,学校一个接一个的举办校赛选拔新人,接着省赛东北赛。为了在这大环境中彻底脱离出来,就需要找到一个麻痹自己的方式,至少将注意力转移下。这也是深层次的原因。

目标定为了一款10年前就上市运营了的陈年老网游。为了保护运营商利益,不透露游戏名字,并且选择氛围较好的台服。

这是一款典型的泡菜网游,可以概括为“打怪-升级-打怪-升级…”。但这也比较具有代表性,思路可以被广泛参考。

Cheat Engine


这款软件的知名度不低,所以基本上所有网游都能有效屏蔽它。但可惜韩国NProtect做的不是那么到位,64位操作系统下不能有效监测出最新版CE。

这款软件的主要优点是全能,用法及原理上类似于金山游侠。

有经验的人可能会知道,辅助有两种,基于封包和基于内存修改。

服务器不会将所有验证工作都包揽于一身,那样开销太大。这就使得hacker们有机可乘。CE便是基于内存修改原理的工具。而封包工具则有著名的WPE等等。由于封包难度相对较大,本人又精力有限,因此只粗略讲解基于内存修改方式下的辅助制作。

实例:修改攻速


由于CE本身附带Tutorial,例如查找某个值在内存中的位置这类基础问题就不赘述了。

打开CE,通过CE打开游戏进程。可以通过反复装卸当前角色的武器制造一个变动的攻速值,方便确定攻速在内存中的存储位置。但这个位置并不是固定的,每次重启游戏都会变动。

程序通常是通过基址加偏移地址的方式寻找某个值,基址寄存器会通过一个固定的指针找到基址。换句话说,只要找到这个不变的指针,就可以得到基址,继而找到攻速的位置。

在地址上单击右键,选择“找出是什么改写了这个地址”,可以看到一些代码,如下图。这些代码不断向攻速地址写入,游戏通过这些代码维护攻速值。

可以看到代码通过基址寻址方式调用攻速,本例中为[ecx+60]。其中ecx此时用作基址寄存器,立即数60表示偏移地址。我们需要知道ecx的值,即基址,搜索ecx的值可以找到存储基址的指针。左键双击其中一条代码,看见下图。

在详细信息中可以看到ecx的值,本例为0D37AD48。接着我们搜索这个值,记着要勾选“十六进制”。本例结果如下。

结果中绿色的便是指针,结果可能有多个,可以多次重启游戏来确认其正确性。本例中085666B8便是我们要找的指针。点击手动添加地址,勾选指针,将我们找到的指针和偏移地址输入,在顶端就可以看到攻速地址和攻速值。本例如左图。

到目前为止,我们已经可以忽略游戏每次的重启而确定攻速的地址。理论上我们可以把它修改为任意值。但试过的朋友可能会发现,刚刚修改完,这个攻速值又会立马被还原。不要忘了还有其他代码在不断往这个地址写入,在维护这个值。对地址右键单击并选择“找出是什么改写了这个地址”,再选择“找出改写了这个指针指向的地址的代码”。全选所有代码,右键并选择“使用空指令替换(NOP)”。如下图。照做后原汇编代码将会被替换为nop,表示什么都不做。这时我们再更改这个值,就不会被还原了。

我们已经成功修改了攻速,但是每次启动游戏都要开CE用NOP替换再修改?这样也太繁琐了。这是辅助制作教程不是网游修改教程。我们已经知道了需要攻速地址所需的指针,还需要一点C/C++和Windows API知识即可完成一个辅助。

当一个程序要修改其他进程时,首先需要打开这个进程。这就需要调用OpenProcess()函数,但这个函数需要被打开进程的pid。我们通常可以从任务管理器中看到pid。调用GetWindowThreadProcessId()函数可以获取指定进程的pid,但它需要指定进程的窗口句柄作为参数。而获得窗口句柄又需要调用FindWindow()函数,它需要窗口名作为参数。

HWND__* hwnd=FindWindow(NULL,"MU");
DWORD pid;
GetWindowThreadProcessId(hwnd,&pid);
HANDLE calcProcess;
calcProcess = OpenProcess(PROCESS_ALL_ACCESS, false,pid);

这里还有一个问题,正常情况下OpenProcess()应当返回进程句柄,但很多时候会返回NULL。若此时GetLastError()的返回值为5,那么我们就需要外加一段代码来提升权限。

bool EnableDebugPriv() 
{ 
	HANDLE hToken;
	LUID sedebugnameValue; 
	TOKEN_PRIVILEGES tkp; 

	if (!OpenProcessToken(GetCurrentProcess(),TOKEN_ALL_ACCESS,&hToken)) 
		return false; 

	if (!LookupPrivilegeValue(NULL,SE_DEBUG_NAME,&sedebugnameValue)) 
	{ 
		CloseHandle( hToken );
		return false; 
	}
	
	tkp.PrivilegeCount = 1; 
	tkp.Privileges[0].Luid = sedebugnameValue; 
	tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; 
	if (!AdjustTokenPrivileges(hToken,FALSE,&tkp,sizeof tkp,NULL,NULL)) 
	{
		CloseHandle( hToken );
		return false;
	}
	
	return true;
}

在拥有进程句柄的情况下,我们可以通过ReadProcessMemory()和WriteProcessMemory()对指定进程的内存进行读写。拥有对内存的读写能力,我们就可以模拟之前在CE上的操作。

首先是将维护攻速的代码替换为nop。在之前的图片中可以看到本例需要替换的代码为5处”mov [ecx+60],ax(cx)”,相应的十六进制代码为66 89 41(48) 60,这是四个字节。需要注意的是,nop指令为一个字节。这也意味着,一条维护指令将被4条nop指令替换。nop的十六进制代码为90,四个字节的十六进代码就是90909090,十进制下就是2425393296。我们只要将2425393296写入正确的位置即可,代码在内存中的位置在之前的图片上可以看到。

unsigned int writeData=2425393296;
WriteProcessMemory(calcProcess,(LPVOID)0x005a690d,&writeData,4,readByte);
WriteProcessMemory(calcProcess,(LPVOID)0x005a6acf,&writeData,4,readByte);
WriteProcessMemory(calcProcess,(LPVOID)0x005a6e66,&writeData,4,readByte);
WriteProcessMemory(calcProcess,(LPVOID)0x005a7327,&writeData,4,readByte);
WriteProcessMemory(calcProcess,(LPVOID)0x005a64e3,&writeData,4,readByte);

接下来是更改攻速,方法与上面类似,唯一需要注意的是先读取指针中的基址,在通过基址加偏移地址算出新地址来更改攻速。我们知道十六进制的偏移地址是60,十进制下为96。LPVOID是4字节空类型的指针,因此偏移96/4=16才对。下面的代码会将本例的攻速由40改为300。

int* dataAddress=0;
int speed=300;
ReadProcessMemory(calcProcess,(LPCVOID)0x085666b8,&dataAddress,4,readByte);
WriteProcessMemory(calcProcess,(LPVOID)(dataAddress+24),&speed,2,readByte);

至此,辅助算是大功告成。授人鱼不如授人以鱼,这实际上是在演示一种通用思路。各个游戏的内存辅助均类似于此,大同小异。

实例:引怪


为保护运营商权益,此部分教程不公开。