一、结论
·
1.1技能道具
- A:0x008649B8
- S:0x00864B38
- D:0x00864CB8
- F:0x00864E38
- G:0x00864FB8
- Z:0x0085EBEC
- X:0x0085EDC0
- C:0x0085EF94
- map.x:0x004ED730
- map.y:0x004ED734
·
1.2人物数据
- 当前生命:[[0x00845618]*0x42B0+0x004ED780]
- 最大生命:[[0x00845618]*0x42B0+0x004ED784]
- 当前内力:[[0x00845618]*0x42B0+0x004ED788]
- 最大内力:[[0x00845618]*0x42B0+0x004ED78C]
- 当前体力:[[0x00845618]*0x42B0+0x004ED790]
- 最大体力:[[0x00845618]*0x42B0+0x004ED794]
- 攻击:[[0x00845618]*0x42B0+0x004ED798]
- 防御:[[0x00845618]*0x42B0+0x004ED79C]
- 身法:[[0x00845618]*0x42B0+0x004ED7A0]
- 等级:[[0x00845618]*0x42B0+0x004ED7A4]
- 经验:[[0x00845618]*0x42B0+0x004ED7B0]
- 升级:[[0x00845618]*0x42B0+0x004ED7B8]
- 人物X:[[0x00845618]*0x42B0+0x004ED7D0]
- 人物Y:[[0x00845618]*0x42B0+0x004ED7D4]
·
1.3技能call
- eax=[0x845618]
- ecx=[[0x845638]*0x180+0x845618+0x1D438+0x168]
- edi=[0x845638]*0x180+0x845618+0x1D438
- [0x845638]的取值是0x14到0x18,分别表示ASDFG技能
·
二、分析
·
2.1技能道具
- 改变ASDFGZXC这部分的值
- CE一下就能搜到绿址,且重启游戏仍有效
- (虽然人物数据也是绿址,但是重启后会变,所以再开一小节分析)·
·
2.2人物数据
- 3886=[edx],edx=0x005387E4
- edx=0x005387E4=edx+004ED784,edx=0x0004B060
- edx=0x0004B060=edx<<4,edx=0x00004B06
- edx=0x4B06=edx+esi*2,edx=0x12,esi=0x257A
- esi=0x257A=edx+esi*4,edx=0x12,esi=0x95A
- esi=0x95A=edx+esi*4,edx=0x12,esi=0x252
- esi=0x252=edx+esi,edx=0x12,esi=0x240
- esi=0x240=esi<<5,esi=0x12
- esi=0x12=edx,edx=0x12
- edx=[ecx],ecx=0x00845618,查不到这个地址,现在关键是找到谁赋给ecx
- 上一行指令0042DB97,je这里,加装备不跳进去执行下面指令,减装备跳进去
- 用OD找到0042D81C就是函数入口地址!
- ecx=0x00845618=esi,esi=0x00845618
- esi=0x00845618=ecx,ecx=0x00845618
- 找到上一个调用的函数地址:0042D695
- ecx=0x00845618=esi,esi=0x00845618
- 本函数第一条指令是0042D667,跳到结束并返回函数入口是00404087,此时ESI的值是1(不是00845618)
- 00404087的函数跳进去的第一条指令是0042D541,我们把目标定在0042D541与0042D667之间,发现一开始就传进去的就是ecx,ecx再赋给- esi的,我们在00404087的上一条00404082看到,00845618竟然是绿色地址!这下就稳了!
·
- 最终最大血条地址:[[0x00845618]*17072+0x004ED784],其中17072是十进制,转换成十六进制是42B0
- 大血条地址出来了,查看数据结构,观察附近的值就可以得到其他值了(类似之前学习CS时知道了Z就知道XY一样),代码中只需要修改后面的0x004ED784偏移即可
·
2.3技能call
- 利用技能等级改变定位到等级地址后使用技能,找到是谁访问这个地址,利用访函数返回处的指令即可找到调到他的call(就是ret的上一条指令)
·
- 技能call(0042D4CB-0042D4D5)以鼠标或人物朝向释放技能,当然有些技能是原地释放如回血
- push 00
- push eax
- push ecx
- push edi
- mov ecx,新剑侠情缘.exe+387B8
- call 新剑侠情缘.exe+15F70
·
- 然后测试了D与F技能两个call如下:
- D技能call
- push 0x00
- push 0x02
- push 0x0A
- push 0x864B50
- mov ecx,0x4E87B8
- call 0x415F70
·
- F技能call
- push 0x00
- push 0x02
- push 0x0A
- push 0x864CD0
- mov ecx,004E87B8
- call 00415F70
·
- 发现上面的第二三四个Push的参数都要找他们的基地址,即eax,ecx,edi
- 注意到0042D4CB处push 00的前两句指令
·
扫描二维码关注公众号,回复:
12666888 查看本文章

- mov eax,[esi]
- mov ecx,[edi+00000168]
- 那也就是说,eax与ecx由esi与edi决定
·
- 追溯到本函数头0042D480,从0042D481-0042D48D,这里用传入的ecx决定了esi,再通过eax作为临时变量,用esi决定了edi,之后都没再对edi与esi作出更改,这是值得高兴的第一个地方
·
- 其中mov esi,ecx,此时ecx=0x00845618,这是值得高兴的第二个地方,不用再往回找了,因为他就是一个基地址!
·
- mov esi,ecx,ecx=0x00845618
- mov eax,[esi+20]
- lea eax,[eax+eax*2]
- shl eax,07
- lea edi,[eax+esi+0001D438]
·
- 整合即可得到结论,以D技能测试为例:
- eax=[0x845618]
- ecx=[[0x845638]*384+0x845618+0x1D438+0x168],十进制384是十六进制180
- edi=[0x845638]*384+0x845618+0x1D438,十进制384是十六进制180
- 我们可以通过上式计算出0x02,0x0A,0x864B50这三个参数值
·
- 最后的问题就是如何区分当前使用的技能?
- 因为传进来的就只有ecx这个基地址呀!
- 我们可以发现不同的技能释放时唯一的参数不同就是edi,而edi表达式中只有[0x845618+0x20]个这是变的,我们尝试使用不同技能记下他的值
·
-
经测试可以发现A-G就是0x14到0x18,凭直觉感觉这个值可以直接用了。
-
当然保险起见,还是查看一下是什么访问了这个内存地址吧
-
我们发现原来设置他的位置就在上面0x0042D469处!
· -
mov eax,[esp+18],esp=0x0019FDFC
-
mov [esi+20],eax,esi=0x00845618
·
- 现在问题就转变为求esp+18
- 我们知道esp指向的永远是栈顶,这里的+18要往回看第6个push的值(因为每个push占4B)
- 一直数可以得到0042D3E0处有个push eax,这里的值就是后面的[esp+18],现在转变为求eax,然后上一行lea eax,[esp+0C],理论上应该是往回看第3个push的值,但是此函数开始处(0042D3D0处)有个sub esp,10,这时就要看调用这个函数的call的最后一个传参了!
·
- 在右下角的堆栈窗口中点第一个跳到堆栈区一眼可以看到push的数14-18,我们前面的判断是没有错的,然后ecx的初始赋值也是绿色地址!
·
- 最后我们整理一下会发现:
- push 00
- push eax
- push ecx
- push edi
- 这四个push中,第一和第三个push是可以直接设值的,第二和第四个push也可能通过一次寻址得到
·
- 至此搜索结束,打开VS写游戏辅助吧(特别注意一定要是Release版本)!
·
三、代码:
- 按钮一:不传参且编译器不优化的测试函数
- 按钮二:传参且优化的测试函数
- 按钮三:开定时器五个技能轮流放
- 按钮四:所有人物属性/技能级别/道具数增加1
//按钮一:不传参且编译器不优化的测试函数
DWORD FindGageProcessIdByWndTitle(CString strTitle) {
HWND hWnd = ::FindWindow(NULL, strTitle.GetBuffer());
DWORD dwPid = 0;
GetWindowThreadProcessId(hWnd, &dwPid);
return dwPid;
}
__declspec(naked) void D_call() {
__asm {
pushad
push 0x00000000
mov eax, ds:[0x00845618]
push eax
mov ecx, ds:[0x00864B50+0x168]
push ecx
push 0x00864B50
mov ecx, 0x004E87B8
mov edx, 0x00415F70
call edx
popad
ret
}
//改变0x00864B50可以改变释放的技能
}
void CxjxqygameDlg::OnBnClickedButton1(){
DWORD dwPid = FindGageProcessIdByWndTitle(_T("Sword Window"));
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);
if (!hProcess) {
MessageBox(_T("打开程序失败"), NULL, 0);
return;
}
LPVOID ThreadFunAdd = VirtualAllocEx(hProcess, NULL, 4096, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (!ThreadFunAdd) {
MessageBox(_T("分配内存失败"), NULL, 0);
CloseHandle(hProcess);
return;
}
DWORD tmpWrite = 0;
DWORD tmpSize = 4096;
BOOL wpm = WriteProcessMemory(hProcess, ThreadFunAdd, D_call, tmpSize, &tmpWrite);
if (!wpm) {
MessageBox(_T("写入代码失败"), NULL, 0);
CloseHandle(hProcess);
return;
}
HANDLE hThread = CreateRemoteThread(hProcess, NULL, NULL, (LPTHREAD_START_ROUTINE)ThreadFunAdd, NULL, NULL, NULL);
if (!hThread) {
MessageBox(_T("远程调用失败"), NULL, 0);
CloseHandle(hProcess);
return;
}
DWORD dwWait = WaitForSingleObject(hThread, INFINITE);
VirtualFreeEx(hProcess, hThread, 0, MEM_RELEASE);
CloseHandle(hProcess);
}
//按钮二:传参且优化的测试函数
typedef struct _ASDFG_call_parame {
DWORD ASDFG;
DWORD ASDFG_add168;
}ASDFG_call_parame, *PASDFG_call_parame;
DWORD __stdcall _ASDFG_call(LPVOID lpThreadParame) {
PASDFG_call_parame pParame = (PASDFG_call_parame)lpThreadParame;
DWORD ASDFG = pParame->ASDFG;
DWORD ASDFG_add168 = pParame->ASDFG_add168;
__asm {
pushad
push 0x00000000
mov eax, ds: [0x00845618]
push eax
mov ecx, ds : [ASDFG_add168]
push ecx
push ASDFG
mov ecx, 0x004E87B8
mov edx, 0x00415F70
call edx
popad
}
return 0;
}
void CxjxqygameDlg::OnBnClickedButton2(){
ASDFG_call_parame parame;
parame.ASDFG=0x00864B50;
parame.ASDFG_add168 = 0x00864B50 + 0x168;
DWORD dwPid = FindGageProcessIdByWndTitle(_T("Sword Window"));
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);
if (!hProcess) {
MessageBox(_T("打开程序失败"), NULL, 0);
return;
}
LPVOID ThreadFunAdd = VirtualAllocEx(hProcess, NULL, 4096, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (!ThreadFunAdd) {
MessageBox(_T("分配内存失败"), NULL, 0);
CloseHandle(hProcess);
return;
}
DWORD tmpWrite = 0;
BOOL wpm1 = WriteProcessMemory(hProcess, ThreadFunAdd, _ASDFG_call, 4096, &tmpWrite);
if (!wpm1) {
MessageBox(_T("写入代码失败"), NULL, 0);
CloseHandle(hProcess);
return;
}
LPVOID ParamAdd = VirtualAllocEx(hProcess, NULL, 4096, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (!ParamAdd) {
MessageBox(_T("分配内存失败"), NULL, 0);
CloseHandle(hProcess);
return;
}
BOOL wpm2 = WriteProcessMemory(hProcess, ParamAdd, ¶me, sizeof(parame), &tmpWrite);
if (!wpm2) {
MessageBox(_T("写入数据失败"), NULL, 0);
CloseHandle(hProcess);
return;
}
HANDLE hThread = CreateRemoteThread(hProcess, NULL, NULL, (LPTHREAD_START_ROUTINE)ThreadFunAdd, ParamAdd, NULL, NULL);
if (!hThread) {
MessageBox(_T("远程调用失败"), NULL, 0);
CloseHandle(hProcess);
return;
}
DWORD dwWait = WaitForSingleObject(hThread, INFINITE);
VirtualFreeEx(hProcess, hThread, 0, MEM_RELEASE);
CloseHandle(hProcess);
}
//按钮三:开定时器五个技能轮流放
int OnTimerCnt = 0;
void CxjxqygameDlg::OnTimer(UINT_PTR nIDEvent) {
switch (nIDEvent) {
case 1: {
ASDFG_call_parame parame;
OnTimerCnt = (OnTimerCnt + 1) % 5;
if (OnTimerCnt == 0) {
parame.ASDFG = 0x00864850;
parame.ASDFG_add168 = 0x00864850 + 0x168;
}else if (OnTimerCnt == 1) {
parame.ASDFG = 0x008649D0;
parame.ASDFG_add168 = 0x008649D0 + 0x168;
}else if (OnTimerCnt == 2) {
parame.ASDFG = 0x00864B50;
parame.ASDFG_add168 = 0x00864B50 + 0x168;
}else if (OnTimerCnt == 3) {
parame.ASDFG = 0x00864CD0;
parame.ASDFG_add168 = 0x00864CD0 + 0x168;
}else if (OnTimerCnt == 4) {
parame.ASDFG = 0x00864E50;
parame.ASDFG_add168 = 0x00864E50 + 0x168;
}
DWORD dwPid = FindGageProcessIdByWndTitle(_T("Sword Window"));
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);
if (!hProcess) {
MessageBox(_T("打开程序失败"), NULL, 0);
return;
}
LPVOID ThreadFunAdd = VirtualAllocEx(hProcess, NULL, 4096, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (!ThreadFunAdd) {
MessageBox(_T("分配内存失败"), NULL, 0);
CloseHandle(hProcess);
return;
}
DWORD tmpWrite = 0;
BOOL wpm1 = WriteProcessMemory(hProcess, ThreadFunAdd, _ASDFG_call, 4096, &tmpWrite);
if (!wpm1) {
MessageBox(_T("写入代码失败"), NULL, 0);
CloseHandle(hProcess);
return;
}
LPVOID ParamAdd = VirtualAllocEx(hProcess, NULL, 4096, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (!ParamAdd) {
MessageBox(_T("分配内存失败"), NULL, 0);
CloseHandle(hProcess);
return;
}
BOOL wpm2 = WriteProcessMemory(hProcess, ParamAdd, ¶me, sizeof(parame), &tmpWrite);
if (!wpm2) {
MessageBox(_T("写入数据失败"), NULL, 0);
CloseHandle(hProcess);
return;
}
HANDLE hThread = CreateRemoteThread(hProcess, NULL, NULL, (LPTHREAD_START_ROUTINE)ThreadFunAdd, ParamAdd, NULL, NULL);
if (!hThread) {
MessageBox(_T("远程调用失败"), NULL, 0);
CloseHandle(hProcess);
return;
}
DWORD dwWait = WaitForSingleObject(hThread, INFINITE);//等到线程执行完才返回,固定值则最多等待该值(ms),0是立即返回
VirtualFreeEx(hProcess, hThread, 0, MEM_RELEASE);
CloseHandle(hProcess);
break;
}
default: {
break;
}
}
CDialogEx::OnTimer(nIDEvent);//nIDEvent号计时器重新计时
}
int TimerOnOrOff = 0;
void CxjxqygameDlg::OnBnClickedButton3(){
if (!TimerOnOrOff)SetTimer(1, 1000, NULL);//1号计时器每1秒调用一次OnTimer
else KillTimer(1);
TimerOnOrOff = (TimerOnOrOff + 1) % 2;
}
//按钮四:所有人物属性/技能级别/道具数增加1
void allAddOne(HANDLE hProcess,DWORD address) {
DWORD tmpRead = 0;
DWORD tmpWrite = 0;
DWORD getData = 0;
BOOL rpm = ReadProcessMemory(hProcess, (LPCVOID)address, &getData, 4, &tmpRead);
getData += 1;
BOOL wpm = WriteProcessMemory(hProcess, (LPVOID)address, &getData, 4, &tmpWrite);
}
void CxjxqygameDlg::OnBnClickedButton4(){
DWORD dwPid = FindGageProcessIdByWndTitle(_T("Sword Window"));
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);
if (!hProcess) {
MessageBox(_T("打开程序失败"), NULL, 0);
return;
}
allAddOne(hProcess, 0x008649B8);//A
allAddOne(hProcess, 0x00864B38);//S
allAddOne(hProcess, 0x00864CB8);//D
allAddOne(hProcess, 0x00864E38);//F
allAddOne(hProcess, 0x00864FB8);//G
allAddOne(hProcess, 0x0085EBEC);//Z
allAddOne(hProcess, 0x0085EDC0);//X
allAddOne(hProcess, 0x0085EF94);//C
DWORD tmpRead = 0;
DWORD getData = 0;
BOOL rpm = ReadProcessMemory(hProcess, (LPCVOID)0x00845618, &getData, 4, &tmpRead);
allAddOne(hProcess, getData * 0x42B0 + 0x004ED780);//当前生命
allAddOne(hProcess, getData * 0x42B0 + 0x004ED784);//最大生命
allAddOne(hProcess, getData * 0x42B0 + 0x004ED788);//当前内力
allAddOne(hProcess, getData * 0x42B0 + 0x004ED78C);//最大内力
allAddOne(hProcess, getData * 0x42B0 + 0x004ED790);//当前体力
allAddOne(hProcess, getData * 0x42B0 + 0x004ED794);//最大体力
allAddOne(hProcess, getData * 0x42B0 + 0x004ED798);//攻击
allAddOne(hProcess, getData * 0x42B0 + 0x004ED79C);//防御
allAddOne(hProcess, getData * 0x42B0 + 0x004ED7A0);//身法
allAddOne(hProcess, getData * 0x42B0 + 0x004ED7A4);//等级
allAddOne(hProcess, getData * 0x42B0 + 0x004ED7B0);//经验
allAddOne(hProcess, getData * 0x42B0 + 0x004ED7B8);//升级
CloseHandle(hProcess);
}
写在最后完善的功能:
- 实现DLL注入:按键代替按钮
- 寻找药品call:血量低自动吃
- 寻找敌人/NPC坐标:自动打怪/聊天
- 寻找道具/武功栏地址:代码换物品
二、寻找药品call
//吃药call(0x4175B开始)
//说明:这里的吃药call是有消耗的
//用z药
push 0x0
push 0xDD
mov ecx,0x845618
call 0x42D540
//用x药
push 0x0
push 0xDE
mov ecx,0x845618
call 0x42D540
//用c药
push 0x0
push 0xDF
mov ecx,0x845618
call 0x42D540
//要吃药无消耗方法:
//把0x0041CEB6处的指令mov [edi+00000198],ecx改成nop即可
//技能call
//说明:这里释放技能是最开始的入口,会先播放动画及判断内力够不够,最后再跳到执行call(就是上一篇笔记中技能调用的call)
//当然下面的主call也是可以用的,但是有消耗有延时,所以用回上一篇笔记的技能释放call就可以了,而上面吃药本身就没有延时,所以直接用吃药的主call吧
//用A技能
push 14
mov ecx,0x845618
call 0x82D3D0
//用S技能
push 15
mov ecx,0x845618
call 0x82D3D0
//用D技能
push 16
mov ecx,0x845618
call 0x82D3D0
//用F技能
push 17
mov ecx,0x845618
call 0x82D3D0
//用G技能
push 18
mov ecx,0x845618
call 0x82D3D0