sword 2逆向分析

基础

OD使用

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;
}

HOOK

清零 5bytes mov edi,0 2bytes xor edi,edi

sub -> nop /add /jmp 但是这种修改后所有对象都无敌了,因为都是调用的一个攻击函数

此时引入hook,先跳转执行我们的代码,再跳回来执行原本的代码

  1. 修改逻辑
  2. 因为用了长跳转,可能会破坏原本逻辑,就需要再跳转后恢复逻辑
  3. 原本代码中有的需要跳转到修改后的逻辑中,就需要再修改原代码中的跳转
  4. 如果像这样,修改成本就比较大,考虑其他点

利用字符串改写内存,jmp的相对地址 = dest - next

  1. 修改已有的代码需要先获取权限
  2. 自己新增代码需要先分配空间

技术模型

#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处的值了

sword 2逆向分析_第1张图片

call

网络游戏中修改本地数据一般无法达到效果

用自己的参数去调用游戏的函数

  • 实现自杀

在其他进程里创建线程

DWORD Tid;
CreateRemoteThread(hProcess,NULL,0,(LPTHREAD_START_ROUTINE)call_kill,0,0,&Tid);

当游戏跳到该线程后,就会没有终点,继续往下执行

空闲的代码会填充INT3 也就是CC 也就是 代码执行到这就会报错

此时我们需要在call代码中加上RET

  • 模块 sword2->查看内存->双击项目

通过字符串和指针来修改内存

_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

代码生成 -> 关闭安全检查

  • 写入的汇编如下
sword 2逆向分析_第2张图片

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搜索会比较麻烦,换思路,在显示过程中修改

显示的基本知识

目前主流的显示技术(本质是个类库)

  • Gdi/Gdi+ 微软公司提供,比较老,性能较低,2D,一般都会使用 GDI32.dll
  • OpenGL 开源,linux下使用较多
  • DirectX 微软公司提供,性能更高,3D D7 Ddraw.dll D9 d3d9_x.dll

游戏引擎的显示也是基于这三者开发的,我们可以通过看引用的库区分这三者

修改思路

查看库文档,找到创建窗口的函数(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,命令行通过一个循环内多个条件判断实现

然后直接跟随入口点,命令行肯定是第一个处理的东西

UI修正

利用面向对象

通过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框架和内部的小按钮元素

多开检测

检测类型

事前检测

事中检测

事后检测

  1. ip(真假ip)
  2. 硬件信息
  3. 多账号行为关系
  4. 游戏行为

进程枚举

枚举当前进程,看是否有同名进程,利用进程快照

破解方法 : 重命名文件

#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);
...
}

cpp实现多开器

从入口点开始,每个call下断点,看在哪退出

一个程序正常运行后会到这个函数,多开处理一定在此之前

// 主消息循环:
while (GetMessage(&msg, nullptr, 0, 0))
{
    if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
}

信息显示

数据搜索

可见数据

类型选择: int float double

搜得到

直接修改

但地址每次都变

搜到多个

使用的数据副本

搜不到

数据类型错误

数据加密,通过右值显示(无固定内存地址)

不可见数据

  1. 模糊搜索
  2. 面向对象

如坐标这种数据应该在人物类中,先通过血量等找出一个数据

  • 然后由this指针访问找到数据头

访问时通过寄存器(一般为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,让更换存档的时候才修改地址

搜索技巧

  1. 先把数据搜索到
  2. 找改写或访问的代码,找到类的访问方式
  3. 搜索this指针,然后找谁访问了该指针,即二级地址

游戏加载

由之前得到修改数组下标的函数开始,一层层往上,得到加载和保存进度的函数

由分析得到

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();
}

脚本函数的hook

前面涉及通过二分查找到当前脚本下标

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) 通过二维数组计算坐标,并与内存中的地图表进行比较

你可能感兴趣的:(汇编,游戏)