一、结论
·
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的前两句指令
·
- 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注入金钱+1
- 寻找药品call
- 寻找道具/武功栏地址
一、DLL注入金钱+1
事实证明:注入DLL后加金钱就两行
int* money = (int*)0x00845628;
*money = *money + 1;
创建MFC DLL文件,新建资源为默认dialog,绑定类AssistMainDlg
在dll文件入口创建线程,线程再创建AssistMainDlg类对象,模态显示
之后就可以加按钮,操作与之前的差不多,但读写内存变得十分简便(因为不是第三方了)
!记得关闭辅助窗口后再卸载dll模块!
// wmgj_dll.h: wmgj_dll DLL 的主标头文件
#pragma once
#ifndef __AFXWIN_H__
#error "在包含此文件之前包含 'pch.h' 以生成 PCH"
#endif
#include "resource.h" // 主符号
// CwmgjdllApp
class CwmgjdllApp : public CWinApp{
public:
CwmgjdllApp();
// 重写
public:
virtual BOOL InitInstance();
virtual int ExitInstance();
DECLARE_MESSAGE_MAP()
};
// wmgj_dll.cpp: 定义 DLL 的初始化例程。
#include "pch.h"
#include "framework.h"
#include "wmgj_dll.h"
#include "AssistMainDlg.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#endif
BEGIN_MESSAGE_MAP(CwmgjdllApp, CWinApp)
END_MESSAGE_MAP()
CwmgjdllApp::CwmgjdllApp(){
}
CwmgjdllApp theApp;
DWORD WINAPI ShowMainDlg(LPVOID pParam) {
::MessageBox(NULL,_T("辅助模块加载成功"),L"",0);
AssistMainDlg dlg;
dlg.DoModal();
return 0;
}
BOOL CwmgjdllApp::InitInstance(){
CWinApp::InitInstance();
::CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ShowMainDlg, NULL, NULL, NULL);
return TRUE;
}
int CwmgjdllApp::ExitInstance(){
::MessageBox(NULL, _T("辅助模块卸载成功"), L"", 0);
return CWinApp::ExitInstance();
}
#pragma once
// AssistMainDlg 对话框
class AssistMainDlg : public CDialog{
DECLARE_DYNAMIC(AssistMainDlg)
public:
AssistMainDlg(CWnd* pParent = nullptr); // 标准构造函数
virtual ~AssistMainDlg();
// 对话框数据
#ifdef AFX_DESIGN_TIME
enum { IDD = IDD_MAIN_DIALOG };
#endif
protected:
virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV 支持
DECLARE_MESSAGE_MAP()
// 用户实现方法:AssistMainDlg 消息处理程序
public:
virtual BOOL OnInitDialog();
afx_msg void OnBnClickedButton1GetMoney();
};
// AssistMainDlg.cpp: 实现文件
#include "pch.h"
#include "wmgj_dll.h"
#include "AssistMainDlg.h"
#include "afxdialogex.h"
// AssistMainDlg 对话框
IMPLEMENT_DYNAMIC(AssistMainDlg, CDialog)
AssistMainDlg::AssistMainDlg(CWnd* pParent /*=nullptr*/)
: CDialog(IDD_MAIN_DIALOG, pParent){
}
AssistMainDlg::~AssistMainDlg(){
}
void AssistMainDlg::DoDataExchange(CDataExchange* pDX){
CDialog::DoDataExchange(pDX);
}
BEGIN_MESSAGE_MAP(AssistMainDlg, CDialog)
ON_BN_CLICKED(IDC_BUTTON1_GET_MONEY, &AssistMainDlg::OnBnClickedButton1GetMoney)
END_MESSAGE_MAP()
// 用户实现方法:AssistMainDlg 消息处理程序
BOOL AssistMainDlg::OnInitDialog(){
CDialog::OnInitDialog();
MessageBox(L"窗口加载完成", L"窗口加载完成", 0);
return TRUE;
}
void AssistMainDlg::OnBnClickedButton1GetMoney(){
int* money = (int*)0x00845628;//这个就是金钱地址
*money = *money + 1;
}
二、寻找药品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
三、寻找道具/武功栏地址
找到两个数量不同的物品,在物品栏第一格互换位置即可得到第一格物品数量地址,同理得第二格地址,得每格物品所占空间
找到两个级别不同的技能,在技能栏第一格互换位置即可得到第一格技能级别地址,同理得第二格技能,得每格技能所占空间
对第一格物品数量地址浏览附近内存区域,再把第一格物品换掉,观察变化的内存,易得物品栏首址
对第一格技能级别地址浏览附近内存区域,再把第一格技能换掉,观察变化的内存,易得技能栏首址