F2添加断点
F6切换窗口
F7步进F8步过(进步进入函数)
F9执行到断点 ctrl+F9执行到返回,然后就可以回到上一级的call
- 回到当前执行位置
查找字符串 : 插件 -> 超级字符串查找++ Pre/Next进行前后查找
跟随入口点 : 可执行模块->跟随入口
保存修改 : 右键->复制到可执行文件
恢复修改 : 补丁->恢复原始代码
右键断点->条件断点 : 如 `esi == 4ce60c`
断点->内存的写入 断点通过修改内存访问属性,bu'huo'y
找到一个值后,其他的应该在这个值附近
找坐标的思路,应该是在看得到的数据附近,找到数据查看内存(显示类型dec),根据变化看闪红的
生命值,体力值,内力值,其当前值和最大值,其内存为int线性排列
位置 : 坐标,目的坐标,窗口坐标,
状态 : 方向 0-7(2D游戏,八个方向) ; 模式 0,1,2,3 静止,走动,跑动,技能 ; 速度
推测数据的指针,找到一串数据的头
理论 : 成员函数是通过this指针来访问成员变量的如[ecx+10]
查看改变该变量的成员函数,有的改变不是通过成员函数实现的
就会发现改变生命值的汇编中有,[esi+10],即当前数据-10就是this指针的位置了
数据分析
拆解
根据大小拆解
根据数据样式拆解,内存对齐(网络数据包需要节约,压缩,可以不遵内存对齐)
char:
C4 CF B9 AC | B7 C9 D4 C6 | 00 00 00 00 | 00 00 00 00 | 00 00 00 00 | 00 00 00 00
翻译过来是"南宫飞云",由此可推测制作者命名时应该都是char name[0x20]
三个作为一个翻译单元(即一个16进制数+空格,如C4_
char str[] = "C4 CF B9 AC B7 C9 D4 C6 ";
int n = strlen(str)+1;//+1是为了防止最后没输空格
char strs[0x20]{};
for (int i = 0; i < n/3; i++)
{
for (int j = 0; j < 2; j++)
{
if (str[3 * i + j] >= 'A')
str[3 * i + j] -= 55;
else str[3 * i + j] -= 48;
}
strs[i] = str[3 * i] * 16 + str[3 * i + 1];
}
std::cout << strs;
调用api版
#include
#include
int main() {
std::string str = "C4 CF B9 AC B7 C9 D4 C6 ";
std::stringstream ss(str);
std::string hex;
std::string result;
while (ss >> hex) {
int value;
std::stringstream(hex) >> std::hex >> value;//std::hex会将字符串解释为16进制数,然后赋值给value
result += static_cast<char>(value);
}
std::cout << result << std::endl;
return 0;
}
wchar_t 特征
66 00 67 00 68 00 00 00
两个一位,中间有很多0,最后以两个00结尾
如果是字符指针应该四个字节
CE工具 -> 分析数据/结构, 将数据列出,便于标记
以管理员身份运行!!!
猜测好this指针的成员变量后,自己写结构体
打开进程,读写数据
打开进程,获得句柄->通过句柄,分配内存->读写内存,修改内存
#include
#include
typedef struct
{
int unknow1[4];
int hp[2];
int tp[2];
int mp[2];
int atk;
int def;
int sf;//身法
int lv;
int unknow2[1];
int exp[2];
int speed;
int unknow3[4];
int x;
int y;
int dy;
int dx;
int unknow4[34];
char Name[0x20];
}Role;
HANDLE hProcess;
unsigned address= 0x4CDD10;
BOOL ReadRole(unsigned addr, Role* role)
{//修改接口
return ReadProcessMemory(hProcess, (LPCVOID)addr, (LPVOID)role, sizeof(Role), NULL);
}
int main()
{
DWORD Pid;
Role role;
ReInput:
//打开进程
std::cout << "请输入游戏进程ID: ";
std::cin >> Pid;
hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Pid);
if (!hProcess) {std::cout << "Open process failed";goto ReInput;}
while (1)
{
//读
if (ReadRole(address, &role))
{
system("cls");
std::cout <<"玩家 : " <<role.Name << std::endl;
std::cout <<"hp : " << role.hp[0] << std::endl;
std::cout <<"坐标 : " << role.x << " " << role.y << std::endl;
//写
int hp = 1562;
WriteProcessMemory(hProcess, (LPVOID)(address+0x10), (LPCVOID)& hp, sizeof(hp), NULL);
Sleep(200);
}
else { std::cout << "read failed\n"; break; }
}
system("pause");
}
main.cpp
#include
#include "GameCheat.cpp"//LNK2019
using namespace std;
int main()
{
cout << "请输入进程ID: ";
int Pid;
cin >> Pid;
GameCheat<JXRole> gcheat(Pid, 0x4CDD10);
gcheat.setData((void*)&gcheat.Data().atk, 999);
cout << gcheat.Data().Name<<endl;
}
GameCheat.h
#pragma once
#include "windows.h"
typedef struct
{
int unknow1[4];
int hp[2];
int tp[2];
int mp[2];
int atk;
int def;
int sf;//身法
int lv;
int unknow2[1];
int exp[2];
int speed;
int unknow3[4];
int x;
int y;
int dy;
int dx;
int unknow4[34];
char Name[0x20];
}JXRole;
template<class Tc1>
class GameCheat
{
public:
GameCheat(unsigned pid, unsigned _baseAddr, unsigned _reatTime = 100);
Tc1& Data(bool isCheckTime = true);//读取数据,考虑到实时更新数据,所以引入时间间隔,有的时候不需要更新,所以引入参数
template<typename Tf1>
void setData(void* dataAddr, Tf1 x);
private:
Tc1 data;
HANDLE hProcess;
unsigned baseAddr;
unsigned lastRead;
unsigned readRate;
};
template<class Tc1>
template<typename Tf1>
inline void GameCheat<Tc1>::setData(void* dataAddr, Tf1 x)
{
LPVOID destAddr = (LPVOID)((unsigned)dataAddr - (unsigned)this + baseAddr);
WriteProcessMemory(hProcess, (LPVOID)destAddr, (LPCVOID)&x, (SIZE_T)sizeof(Tf1), NULL);
}
GameCheat.cpp
#include "GameCheat.h"
template<class Tc1>
GameCheat<Tc1>::GameCheat(unsigned pid, unsigned _baseAddr, unsigned _reatRate)
{
baseAddr = _baseAddr;
readRate = _reatRate;
hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
}
template<class Tc1>
Tc1& GameCheat<Tc1>::Data(bool isCheckTime)
{
if (!isCheckTime)return data;
unsigned dTickNow = GetTickCount64();
if ((dTickNow - lastRead) > readRate)
{
lastRead = dTickNow;
ReadProcessMemory(hProcess, (LPCVOID)baseAddr, this, sizeof(Tc1), NULL);
}
return data;
}
清零 5bytes mov edi,0
2bytes xor edi,edi
sub -> nop /add /jmp 但是这种修改后所有对象都无敌了,因为都是调用的一个攻击函数
此时引入hook,先跳转执行我们的代码,再跳回来执行原本的代码
利用字符串改写内存,jmp的相对地址 = dest - next
#include
#include
int main()
{
std::cout<<"请输入进程ID: ";
DWORD Pid;
HANDLE hProcess;
std::cin >>Pid;
hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Pid);
if (hProcess)
{
LPVOID lCode = VirtualAllocEx(hProcess, NULL, 1000, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (lCode)
{
//几个变量
unsigned pHook = 0x41fd61;//跳转的位置
unsigned pUser = 0x4CE60C;//角色的this指针
int* mid;//作为左值,用于中转要修改的地址的值
//修改原本的代码
char memHook[]{0xe9,0x00,0x00,0x00,0x00,0x90};//jmp 0 0 0 0 nop
mid = (int*)(memHook + 1);
mid[0] = (unsigned)lCode - pHook - 5;
VirtualProtectEx(hProcess, (LPVOID)pHook, 6, PAGE_EXECUTE_READWRITE, NULL);
WriteProcessMemory(hProcess, (LPVOID)pHook, memHook, 0x6, NULL);
//增加我们的代码
char memRevise[]
{
//当对象为我们时,将对手攻击清0 源代码大概像这样user.atk(wolf,atk);
0x81,0xFE,0x00,0x00,0x00,0x00,//cmp esi 4ce60c
0x75,0x02,//jne 02
0x31,0xFF,//xor edi,edi
//还原代码
0x8b,0xd0,//mov edx,eax
0x29,0xfa,//sub edx,edi
0x39,0xca,//cmp edx,ecx
//跳回去
0xe9,0x00,0x00,0x00,0x00//jmp return
};
mid = (int*)(memRevise + 0x2);
mid[0] = pUser;
mid = (int*)(memRevise + 0x11);
mid[0] = (pHook + 6) - ((unsigned)lCode + 0x10) - 5;
WriteProcessMemory(hProcess, lCode, memRevise, sizeof(memRevise), NULL);
}
else std::cout << "内存分配失败";
}
}
改进 : 开/关无敌
实现方案1(傻白甜式),将跳转处的代码在进行来回修改
char memHook[]{ 0xe9,0x00,0x00,0x00,0x00,0x90 };
char memOld[6]{ 0x8b,0xd0,0x29,0xfa,0x39,0xca };
实现方案2,因为会频繁修改运行的代码,所以我们可以设置一个全局变量,用来切换状态然后进行比较
将上面的跳转后的代码改成这样就行,然后每次的write只需要修改008B0000
处的值了
网络游戏中修改本地数据一般无法达到效果
用自己的参数去调用游戏的函数
在其他进程里创建线程
DWORD Tid;
CreateRemoteThread(hProcess,NULL,0,(LPTHREAD_START_ROUTINE)call_kill,0,0,&Tid);
当游戏跳到该线程后,就会没有终点,继续往下执行
空闲的代码会填充INT3 也就是CC
也就是烫
代码执行到这就会报错
此时我们需要在call代码中加上RET
通过字符串和指针来修改内存
_asm
DWORD WINAPI cide()
{
unsigned index = 2;//序号,角色为2 狼是0x28
unsigned damage = 999999;
unsigned dcall = 0x41fd40;
unsigned dBeAtker = 0x4ce60c;
_asm
{
push index
push damage
mov ecx, dBeAtker
call dcall
}
}
//将我们的代码写入对方内存之中
WriteProcessMemory(hProcess, (LPVOID)((int)lCode + 500),cide,0x200,NULL );
这个会失败,会拷贝一堆jmp
代码生成 -> 关闭安全检查
c++
思路 : 在解读了汇编后,构造类,模拟原本函数调用的过程,传入自己的参数
写入对方内存的代码一定不要使用全局变量,试了三个小时bug的忠告
class Role{};
DWORD __stdcall cideNoASM(LPCVOID param)
{
unsigned dcall = 0x41fd40;
unsigned dBeAtker = 0x4ce60c;
//函数指针指向被call的位置
void (Role:: * BeAtk)(int damage, int index);
int *mid = (int*)&BeAtk;
mid[0] = dcall;
//传入自己需要的三个参数,模拟调用
Role* role = (Role*)dBeAtker;
(role->*BeAtk)(99999, 2);
return 1;
}
主要是显示方面的改造,但一旦分析清楚显示就可以实现透视,穿墙等功能
由1024*768(0x400*0x300)
修改成1280*960(0x500*3C0)
因为是不变的数据,用CE搜索会比较麻烦,换思路,在显示过程中修改
目前主流的显示技术(本质是个类库)
游戏引擎的显示也是基于这三者开发的,我们可以通过看引用的库区分这三者
查看库文档,找到创建窗口的函数(DirectDrawCreate)并下断点(不直接到设置大小的函数,因为该函数是虚函数,需要另外计算地址)
可利用ascii字符串来进行分析,某些调试用的消息
找到后,由于事静态的,需要通过PE方式修改
找到SetDisplayMode地址
因为动态链接库是共享的,所以SetDisplayMode的地址可以通过我们自己调用得到
#include
#pragma comment(lib,"ddraw.lib")
#pragma comment(lib,"dxguid.lib")//定义了IID_IDirectDraw7
int main()
{
LPVOID ldx;
DirectDrawCreateEx(NULL, &ldx, IID_IDirectDraw7,NULL);
if (ldx) {
LPDIRECTDRAW7 ldx7 = (LPDIRECTDRAW7)ldx;
ldx7->SetDisplayMode(1, 2, 3, 4, 5);
//该函数使用_stdcall而非thiscall,则在汇编上需要多一个参数来传递this指针
}
}
sword自带命令行-window窗口化
利用GetCommandLine 来获取命令行参数或利用PEB结构
然后分割字符串与支持的命令行进行比较REPE CMPS
Repeat Compare String
查找字符串window,命令行通过一个循环内多个条件判断实现
然后直接跟随入口点,命令行肯定是第一个处理的东西
利用面向对象
通过F9可以隐藏UI这个特性,即一个判断语句,直接使用CE搜0,1变化,找到开关,
然后找到两个,则UI是类的不同实例,直接找到ecx,通过改变窗口位置来定位类中坐标在哪
利用绘图函数
而UI计算应该为相对位置,有个固定偏移量
00407A20 8B41 04 MOV EAX,DWORD PTR DS:[ECX+4]
00407A23 85C0 TEST EAX,EAX
00407A25 74 09 JE SHORT Sword2.00407A30;典型if语句
00407A27 8B41 08 MOV EAX,DWORD PTR DS:[ECX+8]
00407A2A 83C1 08 ADD ECX,8
00407A2D FF60 08 JMP DWORD PTR DS:[EAX+8];多重继承,重算指针
00407A30 C3 RETN
;取出ecx+8这个成员变量后,然后直接根据这个地址跳转,调用虚函数
;因为不涉及栈,所以用jmp调用函数,效率更高
正常调用成员函数,先传递指针,再call
00416AA4 B9 88BF4400 MOV ECX,Sword2.0044BF88
00416AA9 E8 720FFFFF CALL Sword2.00407A20
还原的cpp
class data{
unsigned unkown;
unsigned show;
};
class ui{
public virtual void func1();
public virtual void func2();
public virtual void _show();
};
class UIEx:public ui,public data
{
void showUi(show) {
if(show){
_show;//为了能够直接调用,只能是继承于父类的虚函数
}
}
};
对于UI而言,这种嵌套调用多半是大ui框架和内部的小按钮元素
事前检测
事中检测
事后检测
枚举当前进程,看是否有同名进程,利用进程快照
破解方法 : 重命名文件
#include
bool CmulcheatDlg::TestMulByProcess()
{
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnap == INVALID_HANDLE_VALUE) {
AfxMessageBox(L"获取进程列表失败,请检测是否具有管理员权限!");
}
int mulcount = 0;
PROCESSENTRY32W pe{ sizeof(pe) };
BOOL bRet = Process32First(hSnap, &pe);
while (bRet) {
CString txt= pe.szExeFile;
if (txt.MakeLower() == "mulcheat.exe")mulcount++;
bRet = Process32Next(hSnap, &pe);
}
if (mulcount > 1)return true;
return false;
}
调用函数
//在进程初始化时
BOOL CmulcheatDlg::OnInitDialog()
{
CDialogEx::OnInitDialog();
if (TestMulByProcess())ExitProcess(0);
...
}
是操作系统拥有的,可以跨进程操作
bool CmulcheatDlg::TestMulByMutex()
{
HANDLE hMutex = CreateMutex(NULL, FALSE, L"MulCheat");//互斥量创建
if (GetLastError() == ERROR_ALIAS_EXISTS) return true;
return false;
}
破解就直接关闭互斥量就行
void MulCraker()
{
HANDLE hMutex = OpenMutex(MUTEX_ALL_ACCESS, FALSE, L"MulCheat");
if (hMutex != NULL) CloseHandle(hMutex);
}
可控制多开个数
初始计数为0,每次release+1
bool CmulcheatDlg::TestMulBySempore()
{
auto hMulSem = OpenSemaphore(SEMAPHORE_ALL_ACCESS, 0, L"SemCheat");
if(!hMulSem)hMulSem = CreateSemaphore(0,0,2, L"SemCheat");
if (!ReleaseSemaphore(hMulSem, 1, 0)) return true;
return false;
}
或者初始为2,每次wait-1
bool CmulcheatDlg::TestMulBySempore()
{
auto hMulSem = OpenSemaphore(SEMAPHORE_ALL_ACCESS, 0, L"SemCheat");
if(!hMulSem)hMulSem = CreateSemaphore(0,2,2, L"SemCheat");
if (WaitForSingleObject(hMulSem,0)==WAIT_TIMEOUT) return true;
return false;
}
破解就反着来就行了,比如最后一个
void MulCraker()
{
auto hMulSem = OpenSemaphore(SEMAPHORE_ALL_ACCESS, 0, L"SemCheat");
if (!hMulSem)hMulSem = CreateSemaphore(0, 2, 2, L"SemCheat");
ReleaseSemaphore(hMulSem, 1, 0);
}
bool CmulcheatDlg::TestMulByWnd()
{
auto hWnd = FindWindow(L"#32770", L"MulCheat");//类名,窗口名spy++
return hWnd;
}
这个应该放在窗口创建之前
BOOL CmulcheatApp::InitInstance()
{
CWinApp::InitInstance();
...
CmulcheatDlg dlg;
m_pMainWnd = &dlg;
if (dlg.TestMulByWnd())ExitProcess(0);//添加到这
INT_PTR nResponse = dlg.DoModal();
...
}
破解
void MulCraker()
{
auto hWnd = FindWindow(L"#32770", L"MulCheat");
SetWindowText(hWnd, L"mulCracker");
}
#pragma data_seg("_hdata")
//这一段的数据都会被共享
int gamecount = 0;
#pragma data_seg
#pragma comment(linker,"/SECTION:_hdata,RWS")//read write share
//在进程初始化时
BOOL CmulcheatDlg::OnInitDialog()
{
CDialogEx::OnInitDialog();
gamecount++;
if (gamecount > 1)ExitProcess(0);
...
}
从入口点开始,每个call下断点,看在哪退出
一个程序正常运行后会到这个函数,多开处理一定在此之前
// 主消息循环:
while (GetMessage(&msg, nullptr, 0, 0))
{
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
类型选择: int float double
搜得到
直接修改
但地址每次都变
搜到多个
使用的数据副本
搜不到
数据类型错误
数据加密,通过右值显示(无固定内存地址)
如坐标这种数据应该在人物类中,先通过血量等找出一个数据
访问时通过寄存器(一般为ecx或esi)+一个比较小的数(0-FFFF)
0041FDB2 - 29 7E 10 - sub [esi+10],edi
如果该对象通过数组来访问(找sizeof无法确定,会在编译时优化为常量)
CE按住shift可选择一段数据复制
;004257E0处,ce下断点后可以查看寄存器的值
mov eax,[ecx] ;eax = x
push esi
lea edx,[eax+eax*8] ;edx = eax*9
shl edx,06 ;edx<<=6
sub edx,eax ;edx-=eax 即edx = 575x
mov esi,[edx*4+004B51D0] ;[2300x + 基址] 2300 = 0x8FC,算出数组大小
lea eax,[edx*4+004B51D0]
mov edx,[esp+08]
add esi,edx
mov [eax],esi
因为只是显示消息,使用MFC的EDIT空间,设置样式为无边框,行为多行文本和禁止编辑
主要是涉及一个多页面和架构的问题(层层调用)
底层随游戏更新而更新,逻辑随功能改变而改变,窗口随开发环境而改变
三个部分相互隔离,在代码迁移时成本更低
CGame
对接底层数据
CGame::CGame()
{
player = (PAIM)0x4CE60C;
}
CGLogic
实现我们的逻辑
#include "pch.h"
#include "CGLogic.h"
CGLogic* logicThis;
void _stdcall TimerCallBack(HWND, UINT, UINT_PTR, DWORD)
{
if (logicThis)logicThis->ShowInfo();
}
CGLogic::CGLogic(CWndMain* wndMain)
{
m_wndMain = wndMain;
logicThis = this;
m_game = new CGame();//调用数据
if (wndMain) SetTimer(m_wndMain->m_hWnd, 10000, 100, TimerCallBack);
}
void CGLogic::ShowInfo()
{
if (m_game == nullptr || m_game->player == nullptr)return;
AIM* p = m_game->player;
CStringA infos;
infos.AppendFormat("昵称:[%s]\r\n", p->Name);
infos.AppendFormat("生命值: %d / %d\r\n", p->HP, p->HPMax);
infos.AppendFormat("体力: %d / %d\r\n", p->TP, p->TPMax);
infos.AppendFormat("内力: %d / %d\r\n", p->MP, p->MPMax);
infos.AppendFormat("攻击: %d\r\n", p->ATK);
infos.AppendFormat("防御: %d\r\n", p->DEF);
infos.AppendFormat("身法: %d\r\n", p->SF);
infos.AppendFormat("等级: %d\r\n", p->LV);
infos.AppendFormat("经验: %d / %d\r\n", p->EXP, p->EXPMax);
infos.AppendFormat("朝向: %d\r\n", p->ward);
infos.AppendFormat("坐标: (%d, %d)\r\n", p->x, p->y);
CString winfos;
winfos = infos;
m_wndMain->ShowInfos(winfos.GetBuffer());
}
CWndMain
通过主窗口分发
void CWndMain::ShowInfos(wchar_t* val)
{
CWnd_0 *Wnd0 = (CWnd_0*)m_arrPages[0];
Wnd0->ShowPlayerInfos(val);
}
CWnd0
终端窗口实现
void CWnd_0::ShowPlayerInfos(wchar_t* val)
{
RoleInfo = val;
UpdateData(FALSE);
}
全局变量
在内存中的固定地址,PE文件加载时,是基址+偏移,因为确定的地址会因为环境而出问题,在CE内数据显示为绿色,常量和静态变量类似于全局变量
局部变量
函数内通过压栈分配局部变量[sub esp,1C]
,每个线程独有自己的栈,所以内存地址不固定
动态分配
分配在堆区,内存也不固定,但可以找到其声明的指针地址
//对于很多全局变量,让搜索者无法直接搜出来
int *x;//但这个指针的地址是固定的,可以逆向找这个地址
void init()
{
x = new int[200];
int *arr = new int[200];//而这种就完全没有固定地址了
}
对于嵌套类,涉及二级基址,会更加复杂
如果用了游戏引擎,由于层层封装,可能使用18级基址
class role
{
public:
int Hp;
int Atk;
};
class Engine
{
public:
int version;
role* player;
};
Engine* engine;
void init()
{
engine = new Engine;
}
void login()
{
engine->player = new role;
}
void UI()
{
engine->player->Hp = 1000;
}
有的东西可能没有基址,如NPC,只在部分地区临时产生,用的多的数据一般会有基址,比如人物
逆向分析的思路是找最快的一条路
找到数据后,找出通过this指针访问的形式,找到this地址,然后再搜索该指针
如果找到很多个,则可以尝试更换行为
行为的选择,一般而言,攻击是很复杂的,但显示血量就很简单
找出全局变量后,减去模块地址,找到偏移量,就大功告成
CE指针扫描
找到数据后,右键对这个地址进行指针扫描,将扫描结果保存到一个文件夹中,然后退出重进使用指针扫描器->重新扫描内存,如此反复,找到基址
更新后通过特征码定位新基址
0x4CE61C
;读可以得到人物数组基址偏移为0xB51D0
;由上面不可见数据算出每个元素大小为2300,然后看寄存器得到数组下标的地址
mov esi,[edx*4+Sword2.exe+B51D0]
;写可以得到血量的偏移为0x10
Sword2.exe+1FDB2 - sub [esi+10],edi
通过监管数组下标变化,找到加载存档的函数,然后下hook,让更换存档的时候才修改地址
由之前得到修改数组下标的函数开始,一层层往上,得到加载和保存进度的函数
由分析得到
void CWndMain::OnBnClickedButton1()
{
unsigned decx = 0x44A238;
unsigned loadgame = 0x406940;
unsigned* read = (unsigned*)decx;
read[0x55c / 4] = 0x6d;//6e表示加载,6d表示存档
read[0x6cc / 4] = 0x6;
_asm
{
push 6dh
mov ecx,decx
call loadgame
}
}
跳过开场视频的思路
模拟按键esc跳过
通过api播放
通过文件路径,直接ce搜播放视频文件名,一直跟随,期间出现了拼接完整路径,然后播放
在分析过程中发现播放开场视频是通过脚本加载的
为了让游戏的可编辑性更强,将操作打包成脚本,通过脚本系统调用
破解后可直接利用脚本操作游戏
void CGame::PrintScriptTable()
{
CStringA tmp;
std::ofstream ofs(TableFile);
if (!ofs.bad())
{
int count = GetMaxScript();
for (int i = 0; i < count; i++)
{
tmp.Format("[%03d]. 函数地址[% 08X] 函数名[%s]\n",i ,ScriptTable[i].FuncAddress,ScriptTable[i].FuncName);
ofs << tmp.GetBuffer();
}
}
AfxMessageBox(L"游戏脚本表打印成功");
ofs.close();
}
在执行脚本处hook取到edi调用下面函数输出到文件中(由逆向可得脚本的指针会加载到edi中)
void CGame::PrintScriptRun(const char* _scriptTxt)
{
std::ofstream ofs(ExecFile,std::ios::app);
if (!ofs.bad())
{
ofs << _scriptTxt<<std::endl;
}
ofs.close();
}
前面涉及通过二分查找到当前脚本下标
bool CGame::SetScriptHook(const char* name, PROC_VOID_0 proc)
{
if (!ScriptAddress) ScriptAddress = new unsigned[GetMaxScript()] {};
int index = GetScriptProcID(name);
if ((index > -1) && (index < GetMaxScript()))
{
ScriptAddress[index] = ScriptTable[index].FuncAddress;
ScriptTable[index].FuncAddress = (unsigned)proc;
return true;
}
return false;
}
可实现穿墙等功能
移动实现过程,循环等待鼠标消息,确定目的地坐标,判断该点是否可走,然后自动寻路
判断点击的点,可走,不可走,可交互
(width*y+x)
通过二维数组计算坐标,并与内存中的地图表进行比较