—— besterChen
其实说实话,我写这类文章有些牵强,因为我也是新手。刚学习这个技术所以我所了解的东西实在是太少,没有什么太多的东西分享给大家,只希望写这点东西能帮助到那些跟我一样或者比我还菜的新手朋友。
做外挂,貌似是很多高手所不齿的一个偏行,但似乎又是在现在这个游戏横行年代中很流行的技术话题,但若是我们仔细探究其根源:做外挂似乎也没什么太高的技术含量,说到底就是编程。
今天,就由我来带领大家了解一下做外挂这个“技术”的方方面面吧。
一、 引子
由于我在外挂方面也是一个刚入门的新人,这里所讲的也仅仅是我所接触层面上的一些知识,肯定是不全面的,因此,我在这里就尽量把我所了解到的知识讲的尽可能详细,以弥补我知识面上的欠缺,希望大家能够谅解,也希望大家莫要笑话我……
本文,我将从基础的数据分析开始着手,到程序实现,再到过保护,再到节省系统资源等,以这样层层递进的方式来讲述我要展现给大家的东西。
在每个知识点讲解的时候,我会尽量的分好每一个小节,争取让它更有条理,希望这样讲述大家能够接受也希望大家在技术上有所突破。
在正式开始我们的旅程之前,希望大家能够提前了解下如下一些知识:
1、 OD的使用方法。
2、 了解些内存搜索工具的用法。
3、 掌握些基础的编程思想
希望大家在思考所有问题的时侯,都能从编程的角度去考虑,因为这样就可以有更多的思路、更多的分析方法,更多的技巧,这些是那些不懂编程的朋友永远得不到的强处。
二、 关于数据分析
在网上,尤其像广海,断点这样的外挂论坛,上面有很多关于数据分析的教程,其中很多文章讲的就是用搜索工具查找数据指针,而且似乎很多人都不厌其烦的在讲,在写,在讨论,但是当我仔细去问究为什么的时候,似乎没几个人能给一个令我满意的答案。
相信看这个文章的读者肯定看过很多像我刚才说的那样的文章了,我也相信很多的朋友都会也知道为什么用搜索工具搜数据指针。当然也有很多的朋友不知道的,这里我就从最基础的开始说。
1、基础属性数据的分析
A. 需求分析
当我们在做挂的时候,我们需要根据角色的血量,魔法量的多少来吃药,以保证角色不死掉来提高挂机的效率。在挂机的时候,怕角色乱跑,所以我们需要根据坐标来定一个挂机范围,因此,我们需要分析角色的基础属性。
B. 起始思路
找基础属性,有这么几个方法,思路是这样的:
i. 想很多教程中写的,根据血量、等级、经验、攻击等任何一个属性值的变化,来搜索变化的值,然后用CE找数据指针。
ii. 从编程角度来考虑,任何一个在屏幕上显示的数据都是字符,游戏中显示的属性信息都是字符,比如血量在游戏的数据结构中应该是整型数据,在显示到屏幕上需要进行类型转换,调用类似sprintf这样的函数,比如:
004E8E01 |. 8B86 B0020000 mov eax, dword ptr [esi+0x2B0] ; 取出当前血量
004E8E07 |. 8B8E 78020000 mov ecx, dword ptr [esi+0x278] ; 取出最大血量
004E8E0D |. 50 push eax
004E8E0E |. 51 push ecx
004E8E0F |. 8D5424 58 lea edx, dword ptr [esp+0x58]
004E8E13 |. 68 88BE9C00 push 009CBE88 ; UNICODE "%d / %d"
004E8E18 |. 52 push edx
004E8E19 |. FFD5 call ebp ; sprintf函数
这样,我们就可以通过在OD中搜索相应的字符串来定位到如上这样的代码,在仔细分析ESI的来历,这样我们就可以得到相应数据结构的指针。
C. 调试过程
这里我举一个例子,就像我前些日子分析的口袋西游。
内存搜索,找力量值,得到结果如下:
106CC99C 6
内存访问断点,来到如下代码:
004E8F01 |. 8B86 18030000 mov eax, dword ptr [esi+0x318] ; 最大力量
004E8F07 |. 8B8E 14030000 mov ecx, dword ptr [esi+0x314] ; 最小力量
004E8F0D |. 50 push eax
004E8F0E |. 51 push ecx
004E8F0F |. 8D5424 58 lea edx, dword ptr [esp+0x58]
004E8F13 |. 68 24969C00 push 009C9624 ; UNICODE "%d-%d"
004E8F18 |. 52 push edx
004E8F19 |. FFD5 call ebp ; 显示出来
这些代码由此向下,有很多的数据都是这个结构体的成员,具体的含义大家可以跟一下,对比游戏中显示的数据猜出其具体含义。
004E8B19 |. E8 3249FEFF call 004CD450
{
004CD450 /$ A1 4C28A200 mov eax, dword ptr [0xA2284C]
004CD455 |. 8B48 1C mov ecx, dword ptr [eax+0x1C]
004CD458 |. 8B41 28 mov eax, dword ptr [ecx+0x28]
004CD45B \. C3 retn
}
004E8B1E |. 8BF0 mov esi, eax
下面总结一下:
0xA2284C]+0x1C]+0x28]
+3C X
+40 Z
+44 Y
+264: 角色ID
+26C: 角色职业 1:灵剑,2:日羽,3:枪侠,4:萨满,5:法皇,6:药王
+270: 角色等级
+278: 当前血量
+27C: 当前魔法
+280: 当前异能
+284: 当前精力
+2B0: 最大血量
+2B4: 最大魔法
+2B8: 最大异能
+2BC: 最大精力
+348: 背包金钱
+434: 名字
+704: 移动速度
+8F4: 选择的目标ID
这样我们的角色基础属性就算是分析完成了,可以根据编程需要将它整理成对应的结构体。比如:
// 个人属性信息结构
typedef struct _GAME_CHAR_INFO
{
DWORD UnKnown1[15]; // 未知 offset 0
float fX; // X坐标 offset 0x3C
float fZ; // Z坐标 offset 0x40
float fY; // Y坐标 offset 0x44
DWORD UnKnown2[135]; // 未知 offset 0x48
DWORD dwSID; // 角色ID offset 0x264
DWORD UnKnown3; // 未知 offset 0x268
DWORD dwZhiYe; // 职业 offset 0x26C 1:灵剑,2:日羽,3:枪侠,4:萨满,5:法皇,6:药王
DWORD dwLv; // 等级 offset 0x270
DWORD UnKnown4; // 未知 offset 0x274
DWORD dwCurHP; // 当前HP offset 0x278
DWORD dwCurMP; // 当前MP offset 0x27C
DWORD dwCurYN; //当前异能 offset 0x280
DWORD dwCurJL; //当前精力 offset 0x284
DWORD UnKnown5[10]; // 未知 offset 0x28C
DWORD dwMaxHP; // 最大HP offset 0x2B0
DWORD dwMaxMP; // 最大MP offset 0x2B4
DWORD dwMaxYN; //最大异能 offset 0x2B8
DWORD dwMaxJL; //最大精力 offset 0x2BC
DWORD UnKnown6[34]; // 未知 offset 0x2C0
DWORD dwMoney; // 金钱 offset 0x348
DWORD UnKnown7[58]; // 未知 offset 0x34C
wchar_t* strName; //人物名称 offset 0x434
DWORD UnKnown8[0x12F]; // 未知 offset 0x438
DWORD dwTSID; // 目标实例offset 0x8F4
DWORD UnKnown9[0x2B]; // 未知 offset 0x8F8
PGAME_BAG_FIRST pGBF; // 背包结构指针offset 0x9A4
PGAME_BAG_FIRST pGBF; // 装备结构指针offset 0x9A8
DWORD UnKnown10[0x28]; // 未知 offset 0x9AC
PGAME_SKILL_LIST pGSL; // 技能列表 offset 0xA4C
DWORD nCurSkillNum; // 当前技能数量offset 0xA50(基础是x17,学一个技能加)
DWORD nMaxSkillNum; // 最大技能数量offset 0xA54
} GAME_CHAR_INFO, *PGAME_CHAR_INFO;
到这里,无论是从分析的角度还是从编程的角度,这个数据的分析就算是完工了。下面我们来看一下怎么用代码来操作这些数据。
D. 代码实现
关于读取内存的代码几乎不同的人都有不同的写法,当然各有利弊,我这里写我比较习惯的方法吧。
关于定位数据结构的首地址:
/************************************************************************/
/* 获取人物信息指针 */
/* [g_dwBasePointAddr]+0x1C]+0x28] */
/************************************************************************/
__declspec(naked) PGAME_CHAR_INFO WINAPI GetCharInfoPoint()
{
__asm
{
mov eax, dword ptr [g_dwBasePointAddr]
test eax, eax
je NULL_POINT
mov eax, dword ptr [eax]
test eax, eax
je NULL_POINT
mov eax, dword ptr [eax+0x1C]
test eax, eax
je NULL_POINT
mov eax, dword ptr [eax+0x28]
NULL_POINT:
retn
}
}
这样,只要我们调用这个函数就可以定位到PGAME_CHAR_INFO结构,关于这个结构的定义和分析参考本数据的分析部分。
具体的使用,我给出我的MFC的显示函数:
/************************************************************************/
/* 功能:显示人物信息 */
/* 参数:无 */
/* 返回:无 */
/************************************************************************/
void CFirstTask::ShowRoleInfo()
{
wchar_t szTemp[512] = {0};
PGAME_CHAR_INFO pGCI = GetCharInfoPoint();
SetDlgItemTextW(IDC_MYNAME,pGCI->strName);
switch (pGCI->dwZhiYe)
{
case 1:
lstrcpyW(szTemp, L"灵剑\0");
break;
case 2:
lstrcpyW(szTemp, L"日羽\0");
break;
case 3:
lstrcpyW(szTemp, L"枪侠\0");
break;
case 4:
lstrcpyW(szTemp, L"萨满\0");
break;
case 5:
lstrcpyW(szTemp, L"法皇\0");
break;
case 6:
lstrcpyW(szTemp, L"药王\0");
break;
default:
lstrcpyW(szTemp, L"未知\0");
break;
}
SetDlgItemTextW(IDC_MYZHIYE, szTemp);
wsprintfW(szTemp, L"%d/%d", pGCI->dwCurHP,pGCI->dwMaxHP);
SetDlgItemTextW(IDC_MYHP, szTemp);
wsprintfW(szTemp, L"%d/%d", pGCI->dwCurMP,pGCI->dwMaxMP);
SetDlgItemTextW(IDC_MYMP, szTemp);
wsprintfW(szTemp, L"%d/%d", pGCI->dwCurYN,pGCI->dwMaxYN);
SetDlgItemTextW(IDC_MYYN, szTemp);
wsprintfW(szTemp, L"%d/%d", pGCI->dwCurJL,pGCI->dwMaxJL);
SetDlgItemTextW(IDC_MYJL, szTemp);
swprintf(szTemp, L"%.0f,%.0f,%.0f↑", pGCI->fX, pGCI->fY, pGCI->fZ);
SetDlgItemTextW(IDC_MYXYZ, szTemp);
wsprintfW(szTemp, _T("%d金%d银%d铜"),
pGCI->dwMoney / 10000, (pGCI->dwMoney % 10000) / 100, (pGCI->dwMoney % 10000) % 100);
SetDlgItemTextW(IDC_MYMONEY, szTemp);
wsprintfW(szTemp, L"%d", pGCI->dwLv);
SetDlgItemTextW(IDC_MYLEVEL, szTemp);
}
贴一下显示的效果: