任鸟飞逆向,FPS游戏(UE4,U3D引擎)方框绘制,骨骼透视,自瞄.子弹追踪.无后BT功能的原理 和反外挂策略

帖子简介

感谢关注公众号 任鸟飞逆向, 我们会更新更多文章 提供更多福利

QQ2217777779 5月1,远程全日制+实地班(10周年班) 正式开启,5.1假期以后正式上课,详情请咨询

2022课程目前已经更新几百课,更多内容持续更新中.所有的热门应有尽有,学逆向你最值得信任的依旧是任鸟飞,最新福利:目前VIP,在线班 全部提供 试学模式 和 学生补助以及分期计划,欢迎咨询

为了方便大家可以系统性的学习 和了解FPS游戏相关知识,

导致本帖包含的内容比较繁多.如果没有耐心全部看完的话,也可以直接跳到自己需要的知识点进行学习

下面介绍下

本帖主要内容包含:

几何基础,多种矩阵的学习,世界坐标到屏幕坐标的两种转换方法,三种绘制方框的原理,hookd3d,hookopengl,骨骼透视,主播的秘密,FPS各种BT功能的原理 和检测对抗原理,UE4引擎,U3D引擎的逆向 和实战 ,

游戏安全的建议策略方针 , 反外挂的思路和检测对抗 等等.

同时公众号 任鸟飞逆向后续会给大家专门开设一个FPS安全的版块,进行检测对抗的讨论.

学习这套课程的基础包含少量的汇编知识和编程知识,一定的数学知识和内存知识.

基础建立在 任鸟飞逆向 前100课的前提下即毫无压力的学习.(公众号有免费抽取赠送基础课的名额)

当然我们要从最简单的概念开始学习,请勿急躁

这个课题本着最简单易懂,从本质完全解析的态度,所以有任何细节不懂,哪怕是三角函数,都可以找我探讨

好,我们正式开始

向量

可能大家问为什么要学习向量, 原因是向量是矩阵的元素,而矩阵是帮助我们快速计算的朋友

所以就算不能完全掌握,了解一下是必要的.

指具有大小和方向的量.

一维向量

例如 1 对应的就是从0到1的向量,大小是1,方向X正轴方向

我们说向量只有大小和方向, 不存在其他属性,所以 一维向量 1 也可以表示 从1到2 从-1到0

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_13,color_FFFFFF,t_70,g_se,x_16

向量可以进行算数运算的.例如 1+1 =2 2个大小是1,方向X正轴方向的向量

相加等于1个大小是2,方向X正轴方向的向量

1*3 = 3 给向量放大3倍

二维向量

例如 2,3 书写格式为[2,3]

对应的是 从原点0,0 到 坐标 2,3 的向量, 大小就需要计算了

根据三角函数,大小等于 sqrt(2*2+3*3) ,同样这也是计算 二维空间2点的距离公式

(三角函数:直角三角形,斜边的平方 = 底边平方+高平方 , 知道任意2边可以计算出另外一个边的长度)

距离 = sqrt((x1-x2)*(x1-x2)+(y1-y2)*(y1-y2));

方向如图所示,我们下面再讲

向量只有大小和方向,同样二维向量 [2,3] 也可以表示 从3,0到5,3 ,可以任意位置开始

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_16,color_FFFFFF,t_70,g_se,x_16

二维向量也可以进行算数运算.

例如 [2,3]+[2,1] = [4,4]

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_15,color_FFFFFF,t_70,g_se,x_16

向量的乘法 [2,3]*[3,3] = 6+9= 15 向量的内积

向量的减法可以把空间2点的绝对坐标转化为相对坐标

[X1,Y1] - [X2,Y2]= [X1-X2, Y1 - Y2],相当于把向量移动到原点

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_16,color_FFFFFF,t_70,g_se,x_16

三角函数角度的问题

在游戏图像计算中角度是必不可少的部分

例如 我们知道了,如下三角形 高为3 底边为2

那么tanA = 3/2 我们想求A的角度怎么办呢? C++给我们提供API函数

A = atan(3,2); 这样就计算出来 A的角度了

不过atan()的返回值是弧度,我们如果想转为真正的角度 还是需要转换的

什么是弧度呢?你可以简单的理解为 正常角度如果是0-180的话 弧度就是 0- π

那么 atan(3,2) *180 / π 就把弧度转换成角度了

最终

A = atan(3,2)*180 / π;

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_8,color_FFFFFF,t_70,g_se,x_16

另外一种情况,

知道了角度,想求边长

例如一个向量[?,5] 角度是东偏北 60度

我们怎么计算向量值呢?

很简单,

tan 60 = 5/底边

底边 = 5/ tan60,当然这里的角度参数也是弧度 ,如果你的是真实角度,我们又要把角度转换成弧度

最终

底边 = 5 / tan (60*π/180) ;

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_9,color_FFFFFF,t_70,g_se,x_16

其他的 sin cos 也是同理,我们不是学习数学,所以暂时了解即可,后面用到再说.

三维向量

例如 2,1,3 格式[2,1,3]

向量写成行 或则列都可以

行向量 [2,1,3]

列向量

[ 2

1

3 ]

三维向量对应的是三维空间 2,1,3对应的是x,y,z

(注: 三维坐标系,很多书本是Y 为高度轴,切记X,Y,Z只是个符号,你可以起名叫a b c 也没问题

调转一下坐标系X,就变成了Y ,所以没有区别,不要死记名字,按照自己习惯来)

[2,1,3]就是从原点到坐标2,1,3的向量

大小计算就更加复杂一点了

先看懂下图的辅助线

根据三角函数,向量的大小等于 sqrt(1*1+2*2+3*3) ,同样这也是计算 三维空间2点的距离公式

距离 = sqrt((x1-x2)*(x1-x2)+(y1-y2)*(y1-y2)+(z1-z2)*(z1-z2));

而方向不再单纯是一个角度了,他包含水平角度 和 高低角度,这个我们后面再讲

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_20,color_FFFFFF,t_70,g_se,x_16

向量的减法可以把三维空间2点的绝对坐标转化为相对坐标

[X1,Y1,Z1] - [X2,Y2,Z2]= [X1-X2, Y1 - Y2,Z1-Z2],相当于把向量移动到原点

同样三维向量也可以进行 加法 乘法等运算

例如[x1,y1,z1] * [1,2,3] = x1+y1*2+z1*3 向量的内积

到这里是不是对几何和线性代数的基础知识不再陌生了,其实就这点东西,很简单.

矩阵

为什么要学习矩阵,对于我们研究的课题来说,就是为了方便计算以及精准计算的,当然你可以不用.

多个向量组合成一个矩阵

矩阵可以看做一个特殊的向量,而向量也可以看做一个特殊的矩阵。

只有一行的矩阵 为行向量 行矩阵

只有一列的矩阵 为列向量 列矩阵

格式为 行*列

例如 3*3 矩阵:

1   2   3

5   7   1

2   2   1

例如 3*4 矩阵

1   2   3   5

5   7   1   1

2   2   1   2

同形矩阵 可以相加减(毕竟如果不是同型的话,没有办法对应相加减 这很好理解)

稍微有点难度的是矩阵相乘除

那么大家要注意的是:

1.矩阵是 多个向量的组合,矩阵的乘除就是 向量的乘除,而不是单独元素的乘除

2.两个矩阵相乘,用第一个矩阵的行 乘 第二个矩阵的列的方式 计算

由于使用方法的区别 A*B != B*A 而且 A* 不同的矩阵 结果可能相同

3.计算结果的存放

例如 2个2*2 矩阵相乘

第一行*第一列 放到 第一行第一列

第一行*第二列 放到 第一行第二列

第二行*第一列 放到 第二行第一列

第二行*第二列 放到 第二行第二列

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_16,color_FFFFFF,t_70,g_se,x_16

m*n 矩阵 和 i*j 矩阵 由于是行 *列 所以

m*n 矩阵一行的元素 要和 i*j 矩阵一列的元素 必须相同

也就是 n == i

主要满足这个条件就可以相乘 否则不可以

 

矩阵特性

矩阵对于我们来说就是为了方便计算而生,并不是无可取代

举个例子

只有对角线为1 其他都是0的矩阵 单位矩阵

1   0   0   0

0   1   0   0

0   0   1   0

0   0   0   1

任何矩阵乘以 单位矩阵都为原来矩阵 把 1换成2 就是 放大2倍

比你一个元素一个元素的*2方便很多吧?

矩阵取一列

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_13,color_FFFFFF,t_70,g_se,x_16

单独放大某个元素

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_13,color_FFFFFF,t_70,g_se,x_16

矩阵的乘法可以实现很多的功能

看起来是不是很方便,很强大?!

 

不借助矩阵把游戏坐标转换成屏幕坐标

无论是在窗口上绘制窗体,还是画各种方框,最核心的功能就是在于如何把游戏坐标也就是世界坐标转换成屏幕坐标.

这里我们先不借助于强大好用的矩阵,单纯用几何算法转换一下坐标

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_20,color_FFFFFF,t_70,g_se,x_16

图看起来有点乱,我们慢慢来

这是游戏 上方俯视的平面图:

1.水平可视角度 一般为90度, 也就是FOV 或则第一人称视角

但是这个值只是约值,可能不精准也是导致后面不精准的原因之一

2.我们准星一直在屏幕正中心的,所以向前做一个垂线,左右各45度

3.我们把三角形补全,等腰三角形的斜边就是我们的可视范围,任何游戏中的物品 敌人和我们的连线只有和这个斜边有交单才会显示到我们的屏幕中

如图中敌人和我们连线 焦点就是红色圆圈

4.角度A 可以根据具体朝向值简单分析出来,后面数据分析的时候再说

5.红色圆圈 在 AB 这条线上的位置

就是敌人在我们屏幕X坐标的等比例位置

所以这样就可以计算出来 屏幕坐标X了

 

tanA = X差/ (AB/2);

那么 X差 = tanA*(AB/2);

X差/(AB/2) = 屏幕坐标差/ (分辨率_宽度/2)

继续替换

tanA = 屏幕坐标差/ (分辨率_宽度/2)

角度还要转换一下成弧度

最终

屏幕坐标差 = tan(A*π/180) *(分辨率_宽度/2);

屏幕坐标 = 屏幕坐标差 + 分辨率_宽度/2;

 

int 水平差 = (int)(tan(水平角度差 * 3.1416 / 180) * ((m_分辨率宽) / 2));

屏幕坐标.x = (float)(m_分辨率宽 / 2 + 水平差);

 

屏幕坐标.y 也是同样的计算方法,不过屏幕宽高是不相同的,所以可视角也是有区别的

屏幕分辨率_高/屏幕分辨率_宽 = 高低可视角度 / 水平可视角度

int 高度差 = (int)(tan(高低角度差 * 3.1416 / 180) * ((m_分辨率宽) / 2));// 这里也是m_分辨率宽

因为可视角度不是45了,而是分辨率计算出来的角度

屏幕坐标.y = (float)(m_分辨率高 / 2 + 高度差);

 

最终代码如下:

bool 绘制::世界坐标转屏幕坐标_非矩阵(坐标结构_2& 屏幕坐标, FLOAT  水平角度差, FLOAT 高低角度差)
{
           
    取窗口信息();
    FLOAT 高低可视角度 = (FLOAT)((double)atan2(m_分辨率高, m_分辨率宽)*180/3.1415);
    if (fabs(水平角度差) > 45 || fabs(高低角度差) > 高低可视角度)
    {
        return false;// 不在屏幕范围内
    }

    int 水平差 = (int)(tan(水平角度差 * 3.1416 / 180) * ((m_分辨率宽) / 2)); 
    屏幕坐标.x = (float)(m_分辨率宽 / 2 + 水平差);
    
    int 高度差 = (int)(tan(高低角度差 * 3.1416 / 180) * ((m_分辨率宽) / 2));
    屏幕坐标.y = (float)(m_分辨率高 / 2 + 高度差);

    return true;
}

但是我们发现这样计算出来的画框 是不精准的

主要2个原因一个是角度是约值,一个是计算过程中数值的溢出,尽量使用double 可以减少

也可以通过微调度数等等的方式 把他修正比较准确

 

数据部分

本文章中均以单机游戏为例,每一种功能仅提供给网络安全工作者反外挂建议和安全对抗方法.请勿用作非法用途

另外提示对于此类游戏安全和反外挂研究,单机和网络游戏的原理毫无区别,区别仅仅在于个别数据网络验证部分,如果想研讨网络游的安全防护,可观看视频版,以及公众号任鸟飞逆向探讨.

先整理cs1.6数据如下:

(属于基础范畴,任鸟飞逆向前100课即可轻松搞定这里不赘述)

 

矩阵地址 hl.exe+1820100//这个暂时先不要管,下文会有详细讲解的地方

高低朝向值 hl.exe+19E10C4 //从低到高 89 到 -89

水平朝向值 hl.exe+19E10C8 // 逆时针 从 0 到 360

 

朝向值找到以后我们发现水平转一圈 是0-360的变化规律

其中朝向算法需要我们详细分析一下

方法很简单,

把朝向写入0 然后W走路 看看坐标的变化规律发现是X增加其他不变,那么0对应X正轴

把朝向写入90 然后W走路 看看坐标的变化规律发现是Y增加其他不变,那么0对应Y正轴

把朝向写入180 然后W走路 看看坐标的变化规律发现是X减少其他不变,那么0对应X负轴

把朝向写入270 然后W走路 看看坐标的变化规律发现是Y减少其他不变,那么0对应Y负轴

最终得到结果

也就是我们不同朝向的值

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_20,color_FFFFFF,t_70,g_se,x_16

人物X坐标:hl.exe+195fe58

人物Y坐标:hl.exe+195fe5C

人物Z坐标:hl.exe+195fe60

 

周围数组

数组最大数量1F 以下n 通用 0为自己

hl.exe+1B5A5C4+24C*n+0       等于0 数组结束

hl.exe+1B5A5C4+24C*n+190 DWORD ==0 跳过 有可能数组不是顺序存放

对象X坐标 hl.exe+1B5A5C4+24C*n+18C  

对象Y坐标 hl.exe+1B5A5C4+24C*n+18C

对象Z坐标 hl.exe+1B5A5C4+24C*n+190

1为土匪   2为警察 hl.exe+62565C+n*68+4E    

血量 hl.exe+62565C+n*68+68

死亡标志位 hl.exe+62565C+n*68+60

 

得到的结果 就可以提供给我们封装数据所用了,这已经足够了

 

FPS类型的游戏安全性 和 反外挂

说到这,

我们来聊聊为什么FPS类型的游戏安全性及不高 和 反外挂

主要的2个原因

第一设计简单,数据少,通过上面的需要数据就已经知道了,真的很少

第二个原因是特性导致,透视和自瞄等功能都是服务器无法验证的本地操作

所以加大了反外挂的难度.

那么其实针对于FPS的外挂特征,反外挂可以做的事情也是不少的

第一,加大对周围数据的保护,尤其获取范围,不要在极大范围就像玩家投递全地图数据

第二,对hookd3d的检测应该是比较容易的

第三,对绘制函数的检测,当然如果是窗口的覆盖窗口那是存在一定检测难度的

第四,自瞄准星的数据写入检测

第五,鼠标准星移动轨迹的检测

第六,不定时截图上传

等等.

这些我们后面再详细探讨,现在还没有 了解 所有实现过程, 所以 无法透彻的谈 反外挂

 

数据封装

按照正常的数据封装方法,

封装代码如下,因为这里我都使用了中文命名

相信大家都可以看懂了,如果有什么不懂可以 ,可以找我(任鸟飞)探讨


struct 坐标结构_3
{
	float x, y, z;
};

struct 朝向结构_2
{
    float 水平朝向;
    float 高低朝向;
};

struct 对象结构
{
    float X_j;
    float Y_j;
    float Z_j;
    float X_H;
    float Y_H;
    float Z_H;
    int Hp;
    BYTE 死亡标志位;
    BYTE 阵营;
    朝向结构_2 角度_j;
    朝向结构_2 角度_H;
    朝向结构_2 角度差_j;
    朝向结构_2 角度差_H;
};

class 周围对象
{
public:
    对象结构  对象列表[0x100];
    DWORD  对象数量;
public:
    void 刷新周围数据_Cs();
private:
    void 计算朝向_Cs(坐标结构_3 目标, 朝向结构_2& 角度, 朝向结构_2& 角度差);
};

DWORD Cs_周围基地址 = (DWORD)GetModuleHandleA("hl.exe") + 0x1B5A5C4;
DWORD Cs_周围基地址2 = (DWORD)GetModuleHandleA("hl.exe") + 0x62565C;
void 周围对象::刷新周围数据_Cs()
{
	对象数量 = 0;
	for (int i = 1; i < 0x20; i++)//  第一个位置空出来
	{
		if (*(DWORD*)(Cs_周围基地址 + 0x24C * i + 0) == 0)// 直接结束
		{
			break;
		}
		if (*(DWORD*)(Cs_周围基地址 + 0x24C * i + 0x190) == 0)// 碰到空坐标对象 跳过
		{
			continue;
		}
           // 哪里不懂可以加我2217777779 探讨	
		对象列表[对象数量].X_j = *(FLOAT*)(Cs_周围基地址 + 0x24C * i + 0x188);
		对象列表[对象数量].Y_j = *(FLOAT*)(Cs_周围基地址 + 0x24C * i + 0x18C);
		对象列表[对象数量].Z_j = *(FLOAT*)(Cs_周围基地址 + 0x24C * i + 0x190) - 40;
		对象列表[对象数量].X_H = *(FLOAT*)(Cs_周围基地址 + 0x24C * i + 0x188);
		对象列表[对象数量].Y_H = *(FLOAT*)(Cs_周围基地址 + 0x24C * i + 0x18C);
		对象列表[对象数量].Z_H = *(FLOAT*)(Cs_周围基地址 + 0x24C * i + 0x190) + 23;
		对象列表[对象数量].阵营 = *(BYTE*)(Cs_周围基地址2 + 0x68 * i + 0x4E);
		对象列表[对象数量].Hp = *(DWORD*)(Cs_周围基地址2 + 0x68 * i + 0x68);
		对象列表[对象数量].死亡标志位 = (BYTE)*(DWORD*)(Cs_周围基地址2 + 0x68 * i + 0x60);	

		坐标结构_3 目标;
		朝向结构_2 角度;
		朝向结构_2 角度差;
		目标.x = 对象列表[对象数量].X_j;
		目标.y = 对象列表[对象数量].Y_j;
		目标.z = 对象列表[对象数量].Z_j;
		计算朝向_Cs(目标, 角度, 角度差);
		对象列表[对象数量].角度_j = 角度;
		对象列表[对象数量].角度差_j = 角度差;

		目标.x = 对象列表[对象数量].X_H;
		目标.y = 对象列表[对象数量].Y_H;
		目标.z = 对象列表[对象数量].Z_H;
		计算朝向_Cs(目标, 角度, 角度差);
		对象列表[对象数量].角度_H = 角度;
		对象列表[对象数量].角度差_H = 角度差;
		
		对象数量 += 1;
	}
}

朝向值 和角度差的计算过程,根据上面已经得到的朝向数据

我们编写如下代码,为了让理解更简单 这里我分成了4个象限来讲解

如果还是不能完全理解的话,建议翻看我们之前的 关于朝向的课程,当然朝向 有很多种 这里属于最简单的一种

大家可能问算出来的角度差是干什么用的,还记得上篇文章,不用矩阵转换屏幕坐标吗,里面我们是需要用到这个角度差的

 


void 周围对象::计算朝向_Cs(坐标结构_3 目标, 朝向结构_2& 角度, 朝向结构_2& 角度差)
{
	FLOAT FOV_x = *(FLOAT*)((DWORD)GetModuleHandleA("hl.exe") + 0x195fe58);
	FLOAT FOV_y = *(FLOAT*)((DWORD)GetModuleHandleA("hl.exe") + 0x195fe5C);
	FLOAT FOV_z = *(FLOAT*)((DWORD)GetModuleHandleA("hl.exe") + 0x195fe60);
	FLOAT 水平朝向 = *(FLOAT*)((DWORD)GetModuleHandleA("hl.exe") + 0x19E10C8);
	FLOAT 高低朝向 = *(FLOAT*)((DWORD)GetModuleHandleA("hl.exe") + 0x19E10C4);
	
	if (目标.x > FOV_x && 目标.y >= FOV_y)//第一象限
	{
		角度.水平朝向 = (FLOAT)((double)atan2(目标.y - FOV_y, 目标.x - FOV_x) * 180 / 3.1415);
	}
	if (目标.x <= FOV_x && 目标.y > FOV_y)//第二象限
	{
		角度.水平朝向 = 180 - (FLOAT)((double)atan2(目标.y - FOV_y, FOV_x - 目标.x) * 180 / 3.1415);
	}
	if (目标.x < FOV_x && 目标.y <= FOV_y)//第三象限
	{
		角度.水平朝向 = 180 + (FLOAT)((double)atan2(FOV_y - 目标.y, FOV_x - 目标.x) * 180 / 3.1415);
	}
	if (目标.x >= FOV_x && 目标.y < FOV_y)//第四象限
	{
		角度.水平朝向 = 360 - (FLOAT)((double)atan2(FOV_y - 目标.y, 目标.x - FOV_x) * 180 / 3.1415);
	}
	FLOAT 平面距离 = sqrt((目标.x - FOV_x) * (目标.x - FOV_x) + (FOV_y - 目标.y) * (FOV_y - 目标.y));
	if (目标.z > FOV_z)//上方
	{
		角度.高低朝向 = (FLOAT)(-(double)atan2(目标.z - FOV_z, 平面距离) * 180 / 3.1415);
	}
	if (目标.z < FOV_z)//下方
	{
		角度.高低朝向  = (FLOAT)((double)atan2(FOV_z - 目标.z, 平面距离) * 180 / 3.1415);
	}


	角度差.水平朝向 = 水平朝向 - 角度.水平朝向; 
	if (角度差.水平朝向 <= -180)//跨0轴的两种情况
	{
		角度差.水平朝向 += 360;
	}
	if (角度差.水平朝向 >= 180)
	{
		角度差.水平朝向 -= 360;
	}

	角度差.高低朝向 = 角度.高低朝向 - 高低朝向;
}

 

绘制类

万事俱备,我们来写个绘制类

数据全部完毕我们开始写代码绘制了,这部分是固定的代码,很简单

完全中文编程,代码已经很清晰了

大家跟着我敲写一遍即可

当然第一种 是最粗糙的, 由奢入简易 由俭入奢易难 我们先来粗糙的

struct 坐标结构_2
{
    float x, y;
};
struct 坐标结构_3
{
    float x, y, z;
};
struct 坐标结构_4
{
    float x, y, z, w;
};

class 绘制
{
public:
    HWND m_窗口句柄;
    RECT m_窗口;
    int m_分辨率宽;
    int m_分辨率高;
    RECT m_外窗口;
    int m_外窗口宽;
    int m_外窗口高;
    float m_矩阵[16];
    DWORD m_矩阵地址; 
public:
    绘制(HWND 窗口句柄, DWORD 矩阵地址);
    绘制()
    {
    }
private:
    void 取窗口信息();
public:
    void 绘制矩形(HDC HDC句柄, HBRUSH 画刷句柄, int x, int y, int w, int h);
    void 画框(HDC HDC句柄, HBRUSH 画刷句柄, int x, int y, int w, int h, int 厚度);
    void 画线(HDC HDC句柄, int X, int Y);//Q2217777779探讨
    void 绘制字符串(HDC HDC句柄, int x, int y, COLORREF color,  const char* text);
    //还没讲矩阵绘制  这个函数请忽略
    //bool 世界坐标转屏幕坐标(坐标结构_3 游戏坐标, 坐标结构_2& 屏幕坐标);
    bool 世界坐标转屏幕坐标_非矩阵(坐标结构_2& 屏幕坐标, FLOAT  水平角度差, FLOAT 高低角度差);//传递真实角度
};
绘制::绘制(HWND 窗口句柄, DWORD 矩阵地址)
{
    m_窗口句柄 = 窗口句柄;
    m_矩阵地址 = 矩阵地址;
    memcpy(&m_矩阵, (PBYTE*)(m_矩阵地址), sizeof(m_矩阵));
    取窗口信息();
}

void 绘制::取窗口信息()
{
    GetClientRect(m_窗口句柄, &m_窗口);
    m_分辨率宽 = m_窗口.right - m_窗口.left;
    m_分辨率高 = m_窗口.bottom - m_窗口.top;
    GetWindowRect(m_窗口句柄, &m_外窗口);  //含有边框及全屏幕坐标
    m_外窗口宽 = m_外窗口.right - m_外窗口.left;
    m_外窗口高 = m_外窗口.bottom - m_外窗口.top;
}

void 绘制::绘制矩形(HDC HDC句柄, HBRUSH 画刷句柄, int x, int y, int w, int h)
{
    RECT 矩形 = { x,y,x + w,y + h };
    FillRect(HDC句柄, &矩形, 画刷句柄);//绘制矩形
}

void 绘制::画框(HDC HDC句柄, HBRUSH 画刷句柄, int x, int y, int w, int h, int 厚度)
{
    绘制矩形(HDC句柄, 画刷句柄, x, y, w, 厚度);//顶边
    绘制矩形(HDC句柄, 画刷句柄, x, y + 厚度, 厚度, h - 厚度);//左边
    绘制矩形(HDC句柄, 画刷句柄, (x + w) - 厚度, y + 厚度, 厚度, h - 厚度);//右边
    绘制矩形(HDC句柄, 画刷句柄, x + 厚度, y + h - 厚度, w - 厚度 * 2, 厚度);//底边
}

void 绘制::画线(HDC HDC句柄, int X, int Y)
{
    取窗口信息();
    MoveToEx(HDC句柄, m_分辨率宽 / 2, m_分辨率高, NULL);
    LineTo(HDC句柄, X, Y);
}

HFONT 字体属性;
void 绘制::绘制字符串(HDC HDC句柄, int x, int y, COLORREF color, const char* text)
{
    SetTextAlign(HDC句柄, TA_CENTER | TA_NOUPDATECP);
    SetBkColor(HDC句柄, RGB(0, 0, 0));
    SetBkMode(HDC句柄, TRANSPARENT);
    SetTextColor(HDC句柄, color);
    SelectObject(HDC句柄, 字体属性);
    TextOutA(HDC句柄, x, y, text, strlen(text));
    DeleteObject(字体属性);
}


//还没讲矩阵绘制  这个函数请忽略
/*bool 绘制::世界坐标转屏幕坐标(坐标结构_3 游戏坐标, 坐标结构_2& 屏幕坐标)
{
    取窗口信息();
    memcpy(&m_矩阵, (PBYTE*)(m_矩阵地址), sizeof(m_矩阵));
    坐标结构_4 裁剪坐标;
    裁剪坐标.x = 游戏坐标.x * m_矩阵[0] + 游戏坐标.y * m_矩阵[4] + 游戏坐标.z * m_矩阵[8] + m_矩阵[12];
    裁剪坐标.y = 游戏坐标.x * m_矩阵[1] + 游戏坐标.y * m_矩阵[5] + 游戏坐标.z * m_矩阵[9] + m_矩阵[13];
    裁剪坐标.z = 游戏坐标.x * m_矩阵[2] + 游戏坐标.y * m_矩阵[6] + 游戏坐标.z * m_矩阵[10] + m_矩阵[14];
    裁剪坐标.w = 游戏坐标.x * m_矩阵[3] + 游戏坐标.y * m_矩阵[7] + 游戏坐标.z * m_矩阵[11] + m_矩阵[15];

    if (裁剪坐标.w < 0.0f)
        return false;

    坐标结构_3 NDC;
    NDC.x = 裁剪坐标.x / 裁剪坐标.w;
    NDC.y = 裁剪坐标.y / 裁剪坐标.w;
    NDC.z = 裁剪坐标.z / 裁剪坐标.w;

    屏幕坐标.x = (m_分辨率宽 / 2 * NDC.x) + m_分辨率宽 / 2;
    屏幕坐标.y = -(m_分辨率高 / 2 * NDC.y) + m_分辨率高 / 2;
    return true;
}*/

bool 绘制::世界坐标转屏幕坐标_非矩阵(坐标结构_2& 屏幕坐标, FLOAT  水平角度差, FLOAT 高低角度差)
{
           
    取窗口信息();
    FLOAT 高低可视角度 = (FLOAT)((double)atan2(m_分辨率高, m_分辨率宽)*180/3.1415);
    if (fabs(水平角度差) > 45 || fabs(高低角度差) > 高低可视角度)
    {
        return false;// 不在屏幕范围内
   

    int 水平差 = (int)(tan(水平角度差 * 3.1416 / 180) * ((m_分辨率宽) / 2)); 
    屏幕坐标.x = (float)(m_分辨率宽 / 2 + 水平差);
    // 哪里不懂可以v  hdlw312
    int 高度差 = (int)(tan(高低角度差 * 3.1416 / 180) * ((m_分辨率宽) / 2));
    屏幕坐标.y = (float)(m_分辨率高 / 2 + 高度差);

    return true;
}

到这里 非矩阵绘制的所有准备工作都已经完毕了 ,就看你要怎么调用了

 

非矩阵绘制方框

编写循环绘制代码

通过上面的编写 我们终于可以调用观看效果了


HBRUSH 画刷句柄;
HDC HDC句柄;
COLORREF 文本颜色_亮红 = RGB(255, 0, 0);
COLORREF 文本颜色_红 = RGB(128, 0, 0);
COLORREF 文本颜色_黄 = RGB(200, 200, 0);
COLORREF 文本颜色_黑 = RGB(0, 0, 0);
COLORREF 文本颜色_白 = RGB(255, 255, 255);
COLORREF 文本颜色_常用 = RGB(158, 255, 0);
坐标结构_2 屏幕坐标_j;
坐标结构_2 屏幕坐标_H;
周围对象  周围;

int mainThread()
{

    绘制 FPS_绘制(FindWindow(L"Valve001", 0), (DWORD)GetModuleHandleA("hl.exe") + 0x1820100);


    while (true)
    {
        HDC句柄 = GetDC(FPS_绘制.m_窗口句柄);
        周围.刷新周围数据_Cs();
        for (int i = 0; i < (int)周围.对象数量; i++)
        {
            if (周围.对象列表[i].死亡标志位 == 1)
            {
                continue;
            }
            if (FPS_绘制.世界坐标转屏幕坐标_非矩阵(屏幕坐标_j, 周围.对象列表[i].角度差_j.水平朝向, 周围.对象列表[i].角度差_j.高低朝向))
            {
                if (FPS_绘制.世界坐标转屏幕坐标_非矩阵(屏幕坐标_H, 周围.对象列表[i].角度差_H.水平朝向, 周围.对象列表[i].角度差_H.高低朝向))
                {
                    if (周围.对象列表[i].阵营 == 2)
                    {
                        画刷句柄 = CreateSolidBrush(文本颜色_常用);
                    }
                    else
                    {
                        画刷句柄 = CreateSolidBrush(文本颜色_亮红);
                    }

                    float 高度 = 屏幕坐标_j.y - 屏幕坐标_H.y;
                    float 宽度 = 高度 / 2;                       
                    FPS_绘制.画框(HDC句柄, 画刷句柄, (int)(屏幕坐标_H.x - 宽度/2), (int)屏幕坐标_H.y-2, (int)宽度, (int)(高度+4), 1);
                    DeleteObject(画刷句柄);

                    char healthChar[255];
                    sprintf_s(healthChar, sizeof(healthChar), "%d", 周围.对象列表[i].Hp);
                    FPS_绘制.绘制字符串(HDC句柄, (int)屏幕坐标_j.x, (int)屏幕坐标_j.y, 文本颜色_常用, healthChar);
                    if (GetKeyState(VK_F2) & 1)
                    {
                        FPS_绘制.画线(HDC句柄, (int)屏幕坐标_j.x, (int)屏幕坐标_j.y);
                    }
                }
            }
        }
        Sleep(1);
        DeleteObject(HDC句柄);
    }
}

BOOL APIENTRY DllMain(HMODULE hModule,
    DWORD  ul_reason_for_call,
    LPVOID lpReserved
)
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        DisableThreadLibraryCalls(hModule);
        CreateThread(0, 0, (LPTHREAD_START_ROUTINE)mainThread, 0, 0, 0);

    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

if (GetKeyState(VK_F2) & 1)

这里面学习一个快捷键指令 GetKeyState

每次按下切换他的状态 大于0 或则小于0

从而可以实现开关的功能

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_20,color_FFFFFF,t_70,g_se,x_16

微调

发现效果是有了,但是并不精准,

优点是:不需要矩阵 ,完全的数学公式可以计算出来,而且公式是固定的,

缺点:

但是不精准,

有可视角的关系,运算的误差等等导致的,算出来的误差虽然不是特别大,

而且也可以通过调整的方式让其精确,但是不够完美。

 

我们可以选择微调可视角度,微调屏幕X,Y坐标等等方式来处理

反正这种方法想要完全的精密是不太可能的

但是达到基本效果是没问题的,虽然达到精密没有那么的重要,但是完美主义者

是一定会使用矩阵来计算的

 

那么我们继续学习矩阵,用矩阵的方式 绘制方框

a767bb8783c139b1b69a1b468c1e74ac.png

 

绘制的几种方法和反外挂建议

我们非矩阵的方式绘制 是没有那么的精确的

在学习矩阵绘制之前,我们先来了解下绘制的几种方法

第一种 hook d3d/opengl

优点:不闪 ,代码简单

缺点:非常容易被检测

第二种 窗口上自行绘制,但是会闪

优缺点适中

第三种 自建透明窗口,覆盖游戏窗口,透明窗口上绘制

优点:稳定

确定:代码复杂,会闪

反外挂:无非就是针对外挂使用的函数进行检测

第四种,分屏透视,一个窗口正常游戏,另外一个窗口显示透视信息,主播一般用的

不要说很少一部分,我对陪玩代练主播等了解较多,大部分都是这样.

反外挂思路,同样是针对实现原理,很轻松的应对

只是后2种 属于外部绘制,检测比较困难

下面我们对几种方法一一了解

 

深入学习矩阵 (总结所有游戏矩阵特点 找到矩阵)

对象的世界坐标列向量

x

y

z

w(w为了兼容4*4矩阵以及可以NDC坐标转化而设计存在的,大家可以暂且不管)

可以通过被一个游戏矩阵(以后我们就叫他游戏矩阵吧) 乘 从而获得 剪辑坐标,也可以叫裁剪坐标,都是翻译而来(暂且当成屏幕坐标也没问题,因为他到屏幕坐标转换及其简单)

在学习这个矩阵之前呢我们先来了解

行主序和列主序

行主序就是拿4*4矩阵的行来* 后面的矩阵,我们前面的例子 都是这样

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_14,color_FFFFFF,t_70,g_se,x_16

 

而列主序就是 拿4*4 矩阵的列 来* 后面的矩阵,这个前面没有出现过

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_13,color_FFFFFF,t_70,g_se,x_16

 

那么我们现在了解下这个游戏矩阵都能对我们干嘛,给他拆分成几个功能

最后他是这几个功能的结合体而已

 

缩放位移矩阵

如果是行主序矩阵的话就是

350d5020a78350760ed2f0d2f12a8fcd.png

(1不是固定的 应该写成Tw )

如果是列主序矩阵的话就是

Sx    0     0     0

0     Sy    0     0

0      0     Sz   0

Tx    Ty   Tz  Tw

x

y

z

w

等于

x*Sx+w*Tx

y*Sy+w*Ty

z*Sz+w*Tz

w*Tw

对矩阵 x y z w 进行了 缩放和位移

所以我们在这里知道 矩阵移动影响的主要是

Tx Ty Tz Tw

 

结论一: 我们在走路做位移的情况下,行主序最后一列 列主序最后一行 Tx Ty Tz Tw 会改变

当然不一定只有走路的时候会变

原因很简单,这个矩阵是各种操作的结合体

如以下例子:

只改变Y的情况下

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_15,color_FFFFFF,t_70,g_se,x_16

只改变X的情况下

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_15,color_FFFFFF,t_70,g_se,x_16

只改变Z的情况下

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_13,color_FFFFFF,t_70,g_se,x_16

XYZ混合改变的情况下

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_13,color_FFFFFF,t_70,g_se,x_16

 

旋转矩阵

简单证明下结论watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_17,color_FFFFFF,t_70,g_se,x_16

 

假设旋转45度watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_20,color_FFFFFF,t_70,g_se,x_16

 

用极限验证法,验证结果正确

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_20,color_FFFFFF,t_70,g_se,x_16

围绕Z轴 转动

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_12,color_FFFFFF,t_70,g_se,x_16

 

结论二:只转动水平朝向的时候 行主序第三列不变 ,列主序第三行 不变化

列主序如下

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_12,color_FFFFFF,t_70,g_se,x_16

围绕X 轴转动

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_17,color_FFFFFF,t_70,g_se,x_16

结论三:只转动高低朝向的时候 行主序第一行不变 ,列主序第一列 不变化

列主序如下

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_13,color_FFFFFF,t_70,g_se,x_16

围绕Y 轴转动 我们没有这种情况的时候

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_12,color_FFFFFF,t_70,g_se,x_16

开倍镜

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_12,color_FFFFFF,t_70,g_se,x_16

总结矩阵6条结论

第一结论: 行主序最后一列 列主序最后一行 走路 跳的状态会改变,不代表别的动作不改变

第二结论: 水平转动的情况 行主序第三列不变 列主序的话第三行不变

第三结论: 高低朝向改变的时候 行主序第一行不变 列主序的第一列不变

第四结论: 矩阵第一个值 -1 到1 的, 这个绝对吗 不绝对!

第五结论: 行主序 第一个行第3个元素 是固定的0 列主序 第一列的第三个元素是0 不绝对!

第六结论: 我们开倍镜 第一个值 会* 相应的倍数 不绝对!

 

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_14,color_FFFFFF,t_70,g_se,x_16

通过找到的矩阵可以进一步完善结论

通过以上的规律 CE 搜到如下矩阵地址

矩阵地址

hl.exe+1820100

这个过程文字很啰嗦,只能视频里见了,无非根据以上结论 扫描而已

如果这个都扫不到,说明CE基础薄弱,看看公众号任鸟飞逆向学习下免费CE基础

 

世界坐标 ->剪辑坐标->NDC坐标->屏幕坐标

那么我们来了解下坐标的转换过程

通过以上的矩阵

世界坐标 ----> 剪辑坐标

矩阵乘法

a0      a1      a2     a3

a4      a5      a6     a7

a8      a9      a10   a11

a12    a13    a14   a15

*

x

y

z

w

得到:

剪辑坐标 x = a0*x +a4*y + a8*z + a12*w

剪辑坐标 y = a1*x +a5*y + a9*z + a13*w

剪辑坐标 z = a2*x +a6*y + a10*z + a14*w

剪辑坐标 w = a3*x +a7*y + a11*z + a15*w

 

世界坐标按照我们之前的矩阵法则转换成了平面的剪辑坐标,

但是剪辑坐标是和分辨率没有直接关系的 ,需要我们进一步转换。

剪辑坐标坐标系如下,正中心为0,0

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_20,color_FFFFFF,t_70,g_se,x_16

剪辑坐标---->NDC坐标

矩阵的设计中w 是可以让剪辑坐标范围到-1和1的 也就成了NDC坐标

所以NDC坐标很好理解,就是 -1到1的平面坐标系 中心点为0,0

NDC .x = 剪辑坐标 x/剪辑坐标 w

NDC.y =剪辑坐标y/剪辑坐标 w

NDC.z =剪辑坐标z/剪辑坐标 w

 

DNC坐标---->屏幕坐标

有了DNC坐标我们可以很容易转换成屏幕坐标了

当然屏幕坐标不止一种形式

我们现在只以CS为例

后面会有其他类型

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_20,color_FFFFFF,t_70,g_se,x_16

DNC.x / 1 = 屏幕坐标差.x / 分辨率_宽 /2

屏幕坐标差.x = (分辨率_宽 /2) * DNC.x

屏幕坐标.x = (分辨率_宽 /2) * DNC.x + 分辨率_宽/2

 

DNC.y / 1 = 屏幕坐标差.y / 分辨率_高 /2

屏幕坐标差.y = -(分辨率_高 /2)*DNC.y

屏幕坐标.y= -(分辨率_高 /2)*DNC.y + 分辨率_高 /2

 

矩阵绘制

最终代码如下

(再用之前的代码,只把世界坐标转屏幕坐标_非矩阵(),改成世界坐标转屏幕坐标(),就可以实现功能了,其实也是我们之前的代码设计的比较合理。这样,只要替换一句命令,其他指令完全不影响)

bool 绘制::世界坐标转屏幕坐标(坐标结构_3 游戏坐标, 坐标结构_2& 屏幕坐标)
{
    取窗口信息();
    memcpy(&m_矩阵, (PBYTE*)(m_矩阵地址), sizeof(m_矩阵));
    坐标结构_4 裁剪坐标;
    裁剪坐标.x = 游戏坐标.x * m_矩阵[0] + 游戏坐标.y * m_矩阵[4] + 游戏坐标.z * m_矩阵[8] + m_矩阵[12];
    裁剪坐标.y = 游戏坐标.x * m_矩阵[1] + 游戏坐标.y * m_矩阵[5] + 游戏坐标.z * m_矩阵[9] + m_矩阵[13];
    裁剪坐标.z = 游戏坐标.x * m_矩阵[2] + 游戏坐标.y * m_矩阵[6] + 游戏坐标.z * m_矩阵[10] + m_矩阵[14];
    裁剪坐标.w = 游戏坐标.x * m_矩阵[3] + 游戏坐标.y * m_矩阵[7] + 游戏坐标.z * m_矩阵[11] + m_矩阵[15];

    if (裁剪坐标.w < 0.0f)
        return false;

    坐标结构_3 NDC;
    NDC.x = 裁剪坐标.x / 裁剪坐标.w;
    NDC.y = 裁剪坐标.y / 裁剪坐标.w;
    NDC.z = 裁剪坐标.z / 裁剪坐标.w;

    屏幕坐标.x = (m_分辨率宽 / 2 * NDC.x) + m_分辨率宽 / 2;
    屏幕坐标.y = -(m_分辨率高 / 2 * NDC.y) + m_分辨率高 / 2;
    return true;
}

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_20,color_FFFFFF,t_70,g_se,x_16

这次不用微调就已经 精确了吧?

所以证明矩阵确实算的比较准确而且帮我们省掉了大量计算的时间。

到这里,你的基础算过了

迎接后面真正的学习吧!

 

单机口袋西游和单机突袭进一步熟悉相关数据,矩阵,以及绘制

口袋西游:

用到的数据很简单,跟着2020的课程很容易学会。

这是一款普通的RPG类游戏,不设计到射击相关的功能,我们只是拿来做画框绘制练习。

人物数据:

[[[单机版基地址]+1C]+28]+3C X

[[[单机版基地址]+1C]+28]+40 Y

[[[单机版基地址]+1C]+28]+44 Z

[[[单机版基地址]+1C]+28]+560 X中心点

[[[单机版基地址]+1C]+28]+564 Y中心店

[[[单机版基地址]+1C]+28]+568 Z中心点

[[[单机版基地址]+1C]+28]+56C X中心点到对顶点的X差

[[[单机版基地址]+1C]+28]+570 Y中心点到对顶点的Y差

[[[单机版基地址]+1C]+28]+574 Z中心点到对顶点的Z差

[[[单机版基地址]+1C]+28]+578 立方体下顶点X坐标

[[[单机版基地址]+1C]+28]+57C 立方体下顶点Y坐标

[[[单机版基地址]+1C]+28]+580 立方体下顶点Z坐标

[[[单机版基地址]+1C]+28]+584 立方体上顶点X坐标

[[[单机版基地址]+1C]+28]+588 立方体上顶点Y坐标

[[[单机版基地址]+1C]+28]+58C 立方体上顶点Z坐标

周围遍历

[[[[[单机版基地址]+1C]+8]+20]+5C] 对象数量

[[[[[[[单机版基地址]+1C]+8]+20]+58]+N*4]+4]+138 周围对象血量(未被攻击过是0)

[[[[[[[单机版基地址]+1C]+8]+20]+1C]+N*4]+4]+3C 周围对象X

[[[[[[[单机版基地址]+1C]+8]+20]+1C]+N*4]+4]+40 周围对象Z

[[[[[[[单机版基地址]+1C]+8]+20]+1C]+N*4]+4]+44 周围对象Y

[[[[[[[单机版基地址]+1C]+8]+20]+1C]+N*4]+4]+ED 死亡标志1为死亡BYTE

[[[[[[[单机版基地址]+1C]+8]+20]+1C]+N*4]+4]+2C4 X中心点

[[[[[[[单机版基地址]+1C]+8]+20]+1C]+N*4]+4]+2C8 Z中心店

[[[[[[[单机版基地址]+1C]+8]+20]+1C]+N*4]+4]+2CC Y中心点

[[[[[[[单机版基地址]+1C]+8]+20]+1C]+N*4]+4]+2D0 X中心点到对顶点的X差

[[[[[[[单机版基地址]+1C]+8]+20]+1C]+N*4]+4]+2D4 Y中心点到对顶点的Y差

[[[[[[[单机版基地址]+1C]+8]+20]+1C]+N*4]+4]+2D8 Z中心点到对顶点的Z差

[[[[[[[单机版基地址]+1C]+8]+20]+1C]+N*4]+4]+2DC 立方体下顶点X坐标

[[[[[[[单机版基地址]+1C]+8]+20]+1C]+N*4]+4]+2E0 立方体下顶点Y坐标

[[[[[[[单机版基地址]+1C]+8]+20]+1C]+N*4]+4]+2E4 立方体下顶点Z坐标

[[[[[[[单机版基地址]+1C]+8]+20]+1C]+N*4]+4]+2E8 立方体上顶点X坐标

[[[[[[[单机版基地址]+1C]+8]+20]+1C]+N*4]+4]+2EC 立方体上顶点Y坐标

[[[[[[[单机版基地址]+1C]+8]+20]+1C]+N*4]+4]+2F0 立方体上顶点Z坐标

突袭:

这是一款类似于CS的单机游戏,在设计上相对于CS更加简单一些,数据也比较简单,适合新手练习。

这款游戏也可以联网,有一些数据需要在联网状态下才能精确的分析出来,在新课程中也做了详细的讲解。

人物数据:

[0x00587C0C]+28   X   

[0x00587C0C]+2C  Y

[0x00587C0C]+30   Z

[0x00587C0C]+34   水平朝向顺时针递增360  

[0x00587C0C]+38   竖直朝向-90到90   从下到上

[0x00587C0C]+EC    角色血量

周围遍历

00587C18     玩家数量

[[00587C10]+n*4]+4   X_head

[[00587C10]+n*4]+8  Y_head

[[00587C10]+n*4]+C   Z_head

[[00587C10]+n*4]+28   X   

[[00587C10]+n*4]+2C  Y

[[00587C10]+n*4]+30   Z

[[00587C10]+n*4]+50   身高

[[00587C10]+n*4]+34   水平朝向顺时针递增360  

[[00587C10]+n*4]+38   竖直朝向-90到90   从下到上

[[00587C10]+n*4]+EC    角色血量

在周围遍历中我们发现多了一个头部坐标,这也是其相对于CS设计的更加完善的地方。

 

游戏不分类型

RPG也好,回合制也好,FPS也好,其实只是一个操作的区别而已,本质没有任何区别。

FPS的自瞄相当于Rpg的朝向怪物,FPS的绘制相当于RPG的计算碰撞体 以方便躲避 或则自写寻路等等。

通过已有的数据,我们可以实现以下效果。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_20,color_FFFFFF,t_70,g_se,x_16

看到RPG游戏这样意外吗?

因为游戏很古老,引擎也很古老,所以能够得到资源只允许我们做一个3D的立方体模型,

但是不管如何,RPG和FPS的本质是一样的,没有任何区别,只是我们研究的角度不同,要达到的效果也不同。

 

矩阵

下面我们来看看这两款游戏的矩阵数据

口袋西游矩阵 [D2E5D0]+E8

2bc2eede6ff0b697a78749c4785cdd6c.png

突袭矩阵 57AFE0

06ff76b2d87bd2d57aee1b0a4de0b88d.png

这都是标准的4*4矩阵,具体的分析方法在上面我们已经提到了,下面我们来看看封装数据的过程。

 

数据封装

DWORD 口袋西游_周围基地址 = 0xD0DF1C;
void 周围对象::刷新周围数据_口袋西游()
{
	DWORD Temp = *(DWORD*)(*(DWORD*)(*(DWORD*)(*(DWORD*)口袋西游_周围基地址 + 0x1C) + 0x8) + 0x20);
	对象数量 = *(DWORD*)(Temp +  0x5C);
	DWORD 怪物基地址 = *(DWORD*)(Temp + 0x58);
	for (int i = 0; i < (int)对象数量; i++)
	{
		对象列表[i].X_j = *(FLOAT*)(*(DWORD*)(怪物基地址 + 0x4 * i) + 0x2DC);
		对象列表[i].Y_j = *(FLOAT*)(*(DWORD*)(怪物基地址 + 0x4 * i) + 0x2E0);
		对象列表[i].Z_j = *(FLOAT*)(*(DWORD*)(怪物基地址 + 0x4 * i) + 0x2E4);
		对象列表[i].X_H = *(FLOAT*)(*(DWORD*)(怪物基地址 + 0x4 * i) + 0x2E8);
		对象列表[i].Y_H = *(FLOAT*)(*(DWORD*)(怪物基地址 + 0x4 * i) + 0x2EC);
		对象列表[i].Z_H = *(FLOAT*)(*(DWORD*)(怪物基地址 + 0x4 * i) + 0x2F0);
		对象列表[i].阵营 = 1;
		对象列表[i].Hp = *(DWORD*)(*(DWORD*)(怪物基地址 + 0x4 * i) + 0x138);
		对象列表[i].死亡标志位 = 0;
		
	}
	对象数量++;
	对象列表[对象数量-1].X_j = *(FLOAT*)(*(DWORD*)(*(DWORD*)(*(DWORD*)0xD0DF1C + 0x1C) + 0x28) + 0x578);
	对象列表[对象数量-1].Y_j = *(FLOAT*)(*(DWORD*)(*(DWORD*)(*(DWORD*)0xD0DF1C + 0x1C) + 0x28) + 0x57C);
	对象列表[对象数量-1].Z_j = *(FLOAT*)(*(DWORD*)(*(DWORD*)(*(DWORD*)0xD0DF1C + 0x1C) + 0x28) + 0x580);
	对象列表[对象数量-1].X_H = *(FLOAT*)(*(DWORD*)(*(DWORD*)(*(DWORD*)0xD0DF1C + 0x1C) + 0x28) + 0x584);
	对象列表[对象数量-1].Y_H = *(FLOAT*)(*(DWORD*)(*(DWORD*)(*(DWORD*)0xD0DF1C + 0x1C) + 0x28) + 0x588);
	对象列表[对象数量-1].Z_H = *(FLOAT*)(*(DWORD*)(*(DWORD*)(*(DWORD*)0xD0DF1C + 0x1C) + 0x28) + 0x58C);
	对象列表[对象数量-1].阵营 = 2;
	对象列表[对象数量-1].Hp = *(DWORD*)(*(DWORD*)(*(DWORD*)(*(DWORD*)0xD0DF1C + 0x1C) + 0x28) + 0x288);
	对象列表[对象数量-1].死亡标志位 = 0;
}

DWORD 突袭_周围基地址 = 0x00587C10;
void 周围对象::刷新周围数据_突袭()
{
	对象数量 = *(DWORD*)0x00587C18;
	DWORD 怪物基地址 = *(DWORD*)突袭_周围基地址;
	for (int i = 1; i < (int)对象数量; i++)
	{
		对象列表[i].X_j = *(FLOAT*)(*(DWORD*)(怪物基地址 + 0x4 * i) + 0x28);
		对象列表[i].Y_j = *(FLOAT*)(*(DWORD*)(怪物基地址 + 0x4 * i) + 0x2C);
		对象列表[i].Z_j = *(FLOAT*)(*(DWORD*)(怪物基地址 + 0x4 * i) + 0x30);
		对象列表[i].X_H = *(FLOAT*)(*(DWORD*)(怪物基地址 + 0x4 * i) + 0x4);
		对象列表[i].Y_H = *(FLOAT*)(*(DWORD*)(怪物基地址 + 0x4 * i) + 0x8);
		对象列表[i].Z_H = *(FLOAT*)(*(DWORD*)(怪物基地址 + 0x4 * i) + 0xC);
		对象列表[i].阵营 = 2;
		对象列表[i].Hp = 100;
		对象列表[i].死亡标志位 = 0;
	}
}


基本数据的封装,我们单独来完成,而后面的整体调用我们也进行了设计和修改,可以同时支持西游,CS和突袭这3款游戏。

HBRUSH 画刷句柄;
HDC HDC句柄;
COLORREF 文本颜色_亮红 = RGB(255, 0, 0);
COLORREF 文本颜色_红 = RGB(128, 0, 0);
COLORREF 文本颜色_黄 = RGB(200, 200, 0);
COLORREF 文本颜色_黑 = RGB(0, 0, 0);
COLORREF 文本颜色_白 = RGB(255, 255, 255);
COLORREF 文本颜色_常用 = RGB(158, 255, 0);
坐标结构_2 屏幕坐标_j;
坐标结构_2 屏幕坐标_H;
周围对象  周围;
DWORD 编号 = 0;
绘制  FPS_绘制;



int mainThread()
{  
    if (FindWindow(L"Valve001", 0) != 0)
    {
        绘制 FPS_绘制_2(FindWindow(L"Valve001", 0), (DWORD)GetModuleHandleA("hl.exe") + 0x1820100);
        FPS_绘制 = FPS_绘制_2;
        编号 = 1;
    }
    else if (FindWindow(L"XYElementClient Window", 0) != 0)
    {
        绘制 FPS_绘制_2(FindWindow(L"XYElementClient Window", 0), *(DWORD*)0xD2E5D0 + 0xE8);
        FPS_绘制 = FPS_绘制_2;
        编号 = 2;
    }
    else
    {
        绘制 FPS_绘制_2(FindWindow(0, L"AssaultCube"), 0x57AFE0);
        FPS_绘制 = FPS_绘制_2;
        编号 = 3;
    }
    while (true)
    {
        HDC句柄 = GetDC(FPS_绘制.m_窗口句柄);
        if (编号 == 1)
        {
            周围.刷新周围数据_Cs();
        }
        if (编号 == 2)
        {
            周围.刷新周围数据_口袋西游();
        }
        if (编号 == 3)
        {
            周围.刷新周围数据_突袭();
        }
        for (int i = 0; i < (int)周围.对象数量; i++)
        {
            if (周围.对象列表[i].死亡标志位== 1)
            {
                continue;
            }
            坐标结构_3 坐标_j = { 周围.对象列表[i].X_j ,周围.对象列表[i].Y_j ,周围.对象列表[i].Z_j };
			if (FPS_绘制.世界坐标转屏幕坐标(坐标_j, 屏幕坐标_j))
            //if (FPS_绘制.世界坐标转屏幕坐标_非矩阵(屏幕坐标_j, 周围.对象列表[i].角度差_j.水平朝向, 周围.对象列表[i].角度差_j.高低朝向))
			{             
                坐标结构_3 坐标_H = { 周围.对象列表[i].X_H ,周围.对象列表[i].Y_H ,周围.对象列表[i].Z_H };
				if (FPS_绘制.世界坐标转屏幕坐标(坐标_H, 屏幕坐标_H))
               // if (FPS_绘制.世界坐标转屏幕坐标_非矩阵(屏幕坐标_H, 周围.对象列表[i].角度差_H.水平朝向, 周围.对象列表[i].角度差_H.高低朝向))
				{               
                   
                    if (周围.对象列表[i].阵营 == 2)
                    {
                        画刷句柄 = CreateSolidBrush(文本颜色_常用);
                    }
                    else
                    {
                        画刷句柄 = CreateSolidBrush(文本颜色_亮红);
                    }

                    if (编号 == 1 || 编号 == 3)
                    {
						float head = 屏幕坐标_H.y - 屏幕坐标_j.y;//  负数高度
						float width = head / 2;   //  负数宽度
						float center = width / -2;  //  一半宽度
						float extra = head / -6;   //  三分之一 宽度
						FPS_绘制.画框(HDC句柄, 画刷句柄, (int)(屏幕坐标_j.x + center), (int)屏幕坐标_j.y, (int)width, (int)(head - extra), 1);
                    }
                    if (编号 == 2)
                    {
                        口袋西游模型
                        坐标结构_2 屏幕点1, 屏幕点2, 屏幕点3, 屏幕点4, 屏幕点5, 屏幕点6, 屏幕点7, 屏幕点8;
                        //点求的顺序不能乱,我就不简化代码了
                        坐标结构_3 点1 = { 周围.对象列表[i].X_j ,周围.对象列表[i].Y_j ,周围.对象列表[i].Z_j };
                        坐标结构_3 点2 = { 周围.对象列表[i].X_H ,周围.对象列表[i].Y_j ,周围.对象列表[i].Z_j };
                        坐标结构_3 点3 = { 周围.对象列表[i].X_H ,周围.对象列表[i].Y_H ,周围.对象列表[i].Z_j };
                        坐标结构_3 点4 = { 周围.对象列表[i].X_j ,周围.对象列表[i].Y_H ,周围.对象列表[i].Z_j };
                        坐标结构_3 点5 = { 周围.对象列表[i].X_j ,周围.对象列表[i].Y_j ,周围.对象列表[i].Z_H };
                        坐标结构_3 点6 = { 周围.对象列表[i].X_H ,周围.对象列表[i].Y_j ,周围.对象列表[i].Z_H };
                        坐标结构_3 点7 = { 周围.对象列表[i].X_H ,周围.对象列表[i].Y_H ,周围.对象列表[i].Z_H };
                        坐标结构_3 点8 = { 周围.对象列表[i].X_j ,周围.对象列表[i].Y_H ,周围.对象列表[i].Z_H };
                        if (FPS_绘制.世界坐标转屏幕坐标(点1, 屏幕点1) == 0) continue;
                        if (FPS_绘制.世界坐标转屏幕坐标(点1, 屏幕点1) == 0) continue;
                        if (FPS_绘制.世界坐标转屏幕坐标(点2, 屏幕点2) == 0) continue;
                        if (FPS_绘制.世界坐标转屏幕坐标(点3, 屏幕点3) == 0) continue;
                        if (FPS_绘制.世界坐标转屏幕坐标(点4, 屏幕点4) == 0) continue;
                        if (FPS_绘制.世界坐标转屏幕坐标(点5, 屏幕点5) == 0) continue;
                        if (FPS_绘制.世界坐标转屏幕坐标(点6, 屏幕点6) == 0) continue;
                        if (FPS_绘制.世界坐标转屏幕坐标(点7, 屏幕点7) == 0) continue;
                        if (FPS_绘制.世界坐标转屏幕坐标(点8, 屏幕点8) == 0) continue;
                        //  我连的脑袋疼,就不简化代码了
                        FPS_绘制.两点画线(HDC句柄, 屏幕点1.x, 屏幕点1.y, 屏幕点2.x, 屏幕点2.y);
                        FPS_绘制.两点画线(HDC句柄, 屏幕点2.x, 屏幕点2.y, 屏幕点3.x, 屏幕点3.y);
                        FPS_绘制.两点画线(HDC句柄, 屏幕点3.x, 屏幕点3.y, 屏幕点4.x, 屏幕点4.y);
                        FPS_绘制.两点画线(HDC句柄, 屏幕点4.x, 屏幕点4.y, 屏幕点1.x, 屏幕点1.y);
                        FPS_绘制.两点画线(HDC句柄, 屏幕点5.x, 屏幕点5.y, 屏幕点6.x, 屏幕点6.y);
                        FPS_绘制.两点画线(HDC句柄, 屏幕点6.x, 屏幕点6.y, 屏幕点7.x, 屏幕点7.y);
                        FPS_绘制.两点画线(HDC句柄, 屏幕点7.x, 屏幕点7.y, 屏幕点8.x, 屏幕点8.y);
                        FPS_绘制.两点画线(HDC句柄, 屏幕点8.x, 屏幕点8.y, 屏幕点5.x, 屏幕点5.y);
                        FPS_绘制.两点画线(HDC句柄, 屏幕点1.x, 屏幕点1.y, 屏幕点5.x, 屏幕点5.y);
                        FPS_绘制.两点画线(HDC句柄, 屏幕点2.x, 屏幕点2.y, 屏幕点6.x, 屏幕点6.y);
                        FPS_绘制.两点画线(HDC句柄, 屏幕点3.x, 屏幕点3.y, 屏幕点7.x, 屏幕点7.y);
                        FPS_绘制.两点画线(HDC句柄, 屏幕点4.x, 屏幕点4.y, 屏幕点8.x, 屏幕点8.y);
                        ///
                    }

                    DeleteObject(画刷句柄);

					char healthChar[255];
					sprintf_s(healthChar, sizeof(healthChar), "%d", 周围.对象列表[i].Hp);
                    FPS_绘制.绘制字符串(HDC句柄,(int)屏幕坐标_j.x, (int)屏幕坐标_j.y, 文本颜色_常用,  healthChar);               
					if (GetKeyState(VK_F2) & 1)
					{
                        FPS_绘制.画线(HDC句柄, (int)屏幕坐标_j.x, (int)屏幕坐标_j.y);
					}
				}
			}     
        }
        Sleep(1);
        DeleteObject(HDC句柄);
    }
}

骨骼绘制和模型绘制

不同的游戏,不同的引擎,骨骼的设计理念也不同,找到的数据和最终达到的效果也不同。

新款的引擎相对于老款引擎设计的骨骼也更加的细腻,完善。

骨骼不一定有绝对坐标,但是一般都有相对坐标。

比如口袋西游,Y坐标是高度,

他的骨骼由一个中心点,和两个固定的顶点组成,还需要结合朝向来决定立方体在空间的方位

中心点 X0,Y0,Z0

底点 X1,Y1,Z1

顶点 X2,Y2,Z2

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_20,color_FFFFFF,t_70,g_se,x_16

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_19,color_FFFFFF,t_70,g_se,x_16

下面是定点坐标的计算公式推算

距离 = sqrt((x1-x2)*(x1-x2)+(y1-y2)*(y1-y2));

蓝 = 距离/2

tanA = (y2-y1)/(x2-x1) = 绿/蓝

绿 = 距离/2*(y2-y1)/(x2-x1)

粉 = sqrt(蓝平方 + 绿平方 )

紫 = 蓝 - 绿

橙 = 紫* 蓝/粉

黑 = 绿* 橙/ 蓝

 

右下坐标:

x = 粉+黑

y = - 橙

灰 / (距离/2 + 绿) = 蓝/ 粉

灰 = (距离/2 + 绿) * 蓝/ 粉

勾股定理求粉的右部分

黄 = 粉 - 粉的右分部

左上坐标:

x = 黄

y = 灰

 

但是这个里面不用这么复杂的计算,如果仔细的观察数据变化,可以发现它是X,Y等比增量 说明他是 45°角度固定的

c92b165d630cfdcc1d02c31faa4cc2f3.png

所有 8个点

逆时针顺序分别是

底点 (X1,Y1,Z1)

点1:X1,Y1,Z1

点2:X2,Y1,Z1

点3:X2,Y2,Z1

点4:X1,Y2,Z1

顶点 (X2,Y2,Z2)

点5:X1,Y1,Z2

点6:X2,Y1,Z2

点7:X2,Y2,Z2

点8:X1,Y2,Z2

连线方式

1-2 2-3 3-4 4-1

5-6 6-7 7-8 8-5

1-5 2-6 3-7 4 -8

这样做起来就容易多了。

 

自瞄

自瞄第一步,自然是分清敌我,

绘制和瞄准队友是毫无意义的。

在前面代码中添加一断阵营判断,如下

DWORD 自己阵营 = *(BYTE*)(Cs_周围基地址2 + 0x68 * 0 + 0x4E);

		对象列表[对象数量].阵营 = *(BYTE*)(Cs_周围基地址2 + 0x68 * i + 0x4E);
		if (对象列表[对象数量].阵营 == 自己阵营)
		{
			对象列表[对象数量].阵营 = 2;
		}
		else// 公众号任鸟飞逆向
		{
			对象列表[对象数量].阵营 = 1;
		}

常量是几 ,自己定,方便调用即可

 

第二步,自瞄应该设置成距离准星最近的敌人,而不是距离人物最近的

所以我们要计算一下 准星最近敌人

添加一个成员

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_9,color_FFFFFF,t_70,g_se,x_16

这里面我们就粗略计算,大家可以自己用三角函数计算的更精确,不赘述了

0f24ecbcf8eb4f778e88ab40f9a7028d.png

if (fabs(对象列表[对象数量].角度差_H.水平朝向) < 45 && fabs(对象列表[对象数量].角度差_H.高低朝向) < 35&& 对象列表[对象数量].阵营==1&& 对象列表[对象数量].死亡标志位!=1)
{
  if (fabs(对象列表[对象数量].角度差_H.水平朝向) + fabs(对象列表[对象数量].角度差_H.高低朝向) < 最小角度和)
  {
     最小角度和 = fabs(对象列表[对象数量].角度差_H.水平朝向) + fabs(对象列表[对象数量].角度差_H.高低朝向);
     准星最近对象 = 对象列表[对象数量];
			
   }
		
  }

这样最近的就计算出来了,当然我们只是简单的算和, 应该根据角度 算距离的

第三步,我们就可以到 循环线程加入自瞄代码了

把计算出来的最近朝向写入到 我们的准星数据中

高低朝向值 hl.exe+19E10C4 //从低到高 89 到 -89

水平朝向值 hl.exe+19E10C8 // 逆时针 从 0 到 360

if (GetKeyState(VK_F3) & 1)//启动自瞄 { if (编号 == 1) { *(FLOAT*)((DWORD)GetModuleHandleA("hl.exe") + 0x19E10C4) = 周围.准星最近对象.角度_H.高低朝向; * (FLOAT*)((DWORD)GetModuleHandleA("hl.exe") + 0x19E10C8) = 周围.准星最近对象.角度_H.水平朝向; } } Sleep(1); DeleteObject(HDC句柄);

但是准星秒在敌人头顶了

1c9dc6937f8284d9d3ad99ac735b8502.png

我们之前为了画框 其实给的坐标 是不够准确的

我们调整下,自瞄的坐标

df4320705b1ad0e5b662995e0be7d902.png

另外视野内没有敌人,我们也要判断一下

c585a0b36f253da88d0c25fd3b15c399.png

2a64f16f5ac477c2a747e8a299752cd9.png

这样就可以枪枪爆头,甩狙等等骚操作都可以了

 

UE4引擎学习

根据ue4引擎世界对象数组的特点,我们可以采取以下方式来进行扫描,

打中敌人数组对象+1,拿出手雷+1或者+2,

也就是说出现新物品+不定数量,例如手雷增加,原本手雷不在模型上显示,打出的子弹也是有对象的,所以子弹打中某个碰撞体也会增加。

[["BattleRoyaleTrainer-Win64-Shipping.exe"+2AF0FB8] +138 ]+ b8

[["BattleRoyaleTrainer-Win64-Shipping.exe"+2AF0FB8 ] +30 ]+ b8

 

坐标数组

7cbb9123c2d5d8bfbf73fe9d19ba2b89.png

CTRL+A

acd4cbd5744c5f9a13f71871715eef0d.png

X Y Z 00000000

3F800000 3F800000 3F800000 00000000

Q2217777779

?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? 00 00 00 00 00 00 80 3F 00 00 80 3F 00 00 80 3F 00 00 00 00

UE4 矩阵特征

99a775fcbefd458a2bb9cac5cf2d2851.png

00 00 00 00 00 00 00 00 00 00 80 3F 00 00 80 3F

00 00 80 3F 00 00 80 3F 00 00 00 00 00 00 00 00

00 00 80 3F 00 00 00 00 00 00 00 00 00 00 80 3F

00 00 80 3F 00 00 00 00

UE4的矩阵有他自己的特点

下访问断

r8+280

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_20,color_FFFFFF,t_70,g_se,x_16

返回

rbx+280

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_20,color_FFFFFF,t_70,g_se,x_16

来源于上面某个CALL

F7 往里追

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_20,color_FFFFFF,t_70,g_se,x_16

[["BattleRoyaleTrainer-Win64-Shipping.exe"+2ADA268]+24D08*8+8] +280

也可以扫描 rbx+280的 rbx 然后挨个下断

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_20,color_FFFFFF,t_70,g_se,x_16

"BattleRoyaleTrainer-Win64-Shipping.exe"+2887630

[["BattleRoyaleTrainer-Win64-Shipping.exe"+2887630]+ 3*8 +8]+280

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_20,color_FFFFFF,t_70,g_se,x_16

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_20,color_FFFFFF,t_70,g_se,x_16

深入继续学习可以 公众号 任鸟飞逆向 进行讨论学习

跳或则沿着 XY轴走,2个值改变,乱走路3个值改变。

第一个值范围是-1.19到1.19,准星可以放大绝对值是否确定?

这是一个列主序的矩阵,

第三列 0 0 0 1,其中的1可能是其他数值,

第一列的第三3个数值为0。

 

Unity3D游戏

u3d游戏的数据有一个特点,那就是公式特别长,所以很多人都不习惯直接取分析,而是通过dnspy等工具去正向分析。这类分析方式我们放到下一个专题来说,下面我们看看u3d数据。

1.朝向

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_19,color_FFFFFF,t_70,g_se,x_16

这是从无模块的代码开始分析出来的角色朝向,简单的一个朝向数据,用了一个二叉树,和十多层的偏移。

在分析过程中还有各种坑需要我们跳,比如在函数头部加入一些远跳转,以达到隐藏偏移的目的。

2.周围对象及骨骼遍历

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_20,color_FFFFFF,t_70,g_se,x_16

这款u3d单机游戏的骨骼是以数组的形式挂在周围对象下的,通过一些字符串以及ID来确定骨骼的类型,并且其坐标是绝对坐标,而非像UE4一样的相对坐标。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_20,color_FFFFFF,t_70,g_se,x_16

这里是获取骨骼刚体信息的关键代码,而周围对象的遍历方式也是以数组套数组层层嵌套的方式获取的,这也是u3d代码的特点。

在每一层数组对象的固定偏移内,会有该对象的信息字符串,可能是名字,也可能是类型

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_16,color_FFFFFF,t_70,g_se,x_16

只要找到了基地址,我们就可以通过这些字符串来确定每一层对象的信息,并完成整体的遍历。

u3d的数据我们做一个简单的了解即可,目前主流的引擎更加偏向于UE4等虚幻引擎。

 

第二种透视方式 HOOK OPENGL/ D3D 方式 透视

了解OpenGL 和D3D

OpenGL 开放式图形库, 具有非常强的可移植性,CS中 OpenGL 模式比D3D模式效果好

Direct3D 简称D3D,由微软公司所制定的3D规格界面 与windows兼容性更好,微软亲儿子,优点 游戏运行效率更高

3D绘图 和渲染 用到他们其中之一

/* BeginMode */

#define GL_POINTS 0x0000

#define GL_LINES 0x0001

#define GL_LINE_LOOP 0x0002

#define GL_LINE_STRIP 0x0003

#define GL_TRIANGLES 0x0004

#define GL_TRIANGLE_STRIP 0x0005

#define GL_TRIANGLE_FAN 0x0006

#define GL_QUADS 0x0007

#define GL_QUAD_STRIP 0x0008

#define GL_POLYGON 0x0009

GL_POINTS:把每一个顶点作为一个点进行处理,顶点n即定义了点n,共绘制N个点

GL_LINES:把每一个顶点作为一个独立的线段,顶点2n-1和2n之间的是第n条线段,总共绘制N/2条线段

GL_LINE_STRIP:绘制从第一个顶点到最后一个顶点依次相连的一组线段,第n和n+1个顶点定义了第n线段,总共绘制 N-1条线段

GL_LINE_LOOP:绘制从第一个顶点到最后一个顶点依次相连的一组线段,然后最后一个顶点和第一个顶点相连,第n和n+1个顶点定义了线段n,总共绘制N条线段

GL_TRIANGLES:把每个顶点作为一个独立的三角形,顶点3n-2、3n-1和3n定义了第n个三角形,总共绘制N/3个三角形

GL_TRIANGLE_STRIP:绘制一组相连的三角形,对于奇数n,顶点n、n+1和n+2定义了第n个三角形;对于偶数n,顶点n+1、n和n+2定义了第n个三角形,总共绘制N-2个三角形

GL_TRIANGLE_FAN:绘制一组相连的三角形,三角形是由第一个顶点及其后给定的顶点确定,顶点1、n+1和n+2定义了第n个三角形,总共N-2个三角形

GL_QUADS:绘制由四个顶点组成的一组单独的四边形。顶点4n-3、4n-2、4n-1和4n定义了第n个四边形。总共绘制N/4个四边形

GL_QUAD_STRIP:绘制一组相连的四边形。每个四边形是由一对顶点及其后给定的一对顶点共同确定的。顶点2n-1、2n、2n+2和2n+1定义了第n个四边形,总共绘制N/2-1个四边形

GL_POLYGON:绘制一个凸多边形。顶点1到n定义了这个多边形。

#define GL_DEPTH_TEST 0x0B71

如果启用,则进行深度比较并更新深度缓冲区。参见glDepthFunc和glDepthRange。

 

拿OPENGL 为例

首先知道两个函数  openGL32.DLL 中的 glBegin(枚举模式) ,glDisable(ID) 关闭渲染,关闭服务器端GL功能。我们 Hook glBegin ,判断是否是人物ID,如果是, 直接让他调用 glDisable 关闭渲染,就可以实现透视。

OD 附加 CS

直接跳到opengl32模块的glBegin函数

参数是渲染ID

当ID等于5或则6的时候是 人物,当然你可以每个ID都测试下,看看是什么,种类并不多。

glDisable 关闭渲染函数

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_20,color_FFFFFF,t_70,g_se,x_16

hook 的位置

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_20,color_FFFFFF,t_70,g_se,x_16

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_20,color_FFFFFF,t_70,g_se,x_16

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_20,color_FFFFFF,t_70,g_se,x_16

效果

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_11,color_FFFFFF,t_70,g_se,x_16

 

第三种和第四种透视方式 外部覆盖窗口

1.创建一个透明窗口

2.同步分辨率

3.透明窗口跟随游戏移动

4.置顶透明窗口

5.自己的窗口上画框

 

实现两种效果

不闪,不容易检测更稳定的效果

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_18,color_FFFFFF,t_70,g_se,x_16

主播的秘密

主播:我能看到两个窗口,你们只能到右边的。

831faddf9c730582f8c53972ca27bc8d.png

实现的代码和原来一模一样,唯一的区别就是增加了自己创建窗口的步骤。



void  透明窗口绘制()
{

    RECT 窗口1;
    GetClientRect(hWnd, &窗口1);
    //InvalidateRect(hWnd, &窗口1, TRUE);
    //UpdateWindow(hWnd);//更新窗口 

    HDC HDC句柄 = GetDC(hWnd);
    HBRUSH 画刷句柄 = CreateSolidBrush(RGB(0, 0, 0));
    FillRect(HDC句柄, &窗口1, 画刷句柄);//绘制矩形
    DeleteObject(画刷句柄);
    DeleteObject(HDC句柄);


    绘制 FPS_绘制_2(FindWindow(L"Valve001", 0), (DWORD)GetModuleHandleA("hl.exe") + 0x1820100);
    FPS_绘制 = FPS_绘制_2;
    FPS_绘制.取窗口信息();
    MoveWindow(hWnd, FPS_绘制.m_外窗口.left + (FPS_绘制.m_外窗口宽 - FPS_绘制.m_分辨率宽) / 2, FPS_绘制.m_外窗口.top + (FPS_绘制.m_外窗口高 - FPS_绘制.m_分辨率高 - (FPS_绘制.m_外窗口宽 - FPS_绘制.m_分辨率宽) / 2), 640, 480, 1);
    HDC句柄 = GetDC(hWnd);
    周围.刷新周围数据_Cs();

    
    FPS_绘制.绘制字符串(HDC句柄, FPS_绘制.m_分辨率宽 / 2, FPS_绘制.m_分辨率高 / 2, 文本颜色_常用, "准星");

    for (int i = 0; i < (int)周围.对象数量; i++)
    {
        if (周围.对象列表[i].死亡标志位 == 1)
        {
            continue;
        }
        坐标结构_3 坐标_j = { 周围.对象列表[i].X_j ,周围.对象列表[i].Y_j ,周围.对象列表[i].Z_j };
        if (FPS_绘制.世界坐标转屏幕坐标(坐标_j, 屏幕坐标_j))
        {
            坐标结构_3 坐标_H = { 周围.对象列表[i].X_H ,周围.对象列表[i].Y_H ,周围.对象列表[i].Z_H };
            if (FPS_绘制.世界坐标转屏幕坐标(坐标_H, 屏幕坐标_H))
            {
                if (周围.对象列表[i].阵营 == 2)
                {
                    画刷句柄 = CreateSolidBrush(文本颜色_常用);
                }
                else
                {
                    画刷句柄 = CreateSolidBrush(文本颜色_亮红);
                }

                float head = (float)屏幕坐标_H.y - (float)屏幕坐标_j.y;//  负数高度
                float width = head / 2;   //  负数宽度
                float center = width / -2;  //  一半宽度
                float extra = head / -6;   //  三分之一 宽度

                FPS_绘制.画框(HDC句柄, 画刷句柄, (int)(屏幕坐标_j.x + center), (int)屏幕坐标_j.y, (int)width, (int)(head - extra), 1);

                DeleteObject(画刷句柄);

                char healthChar[255];
                sprintf_s(healthChar, sizeof(healthChar), "%d", 周围.对象列表[i].Hp);
                FPS_绘制.绘制字符串(HDC句柄, (int)屏幕坐标_j.x, (int)屏幕坐标_j.y, 文本颜色_常用, "任鸟飞");
                if (GetKeyState(VK_F2) & 1)
                {
                    FPS_绘制.画线(HDC句柄, (int)屏幕坐标_j.x, (int)屏幕坐标_j.y);
                }

            }
        }
    }
    DeleteObject(HDC句柄);
     
}


//回调函数 Q2217777779
LRESULT CALLBACK windowProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) 
{
        switch (msg)
        {
        case WM_PAINT:
            通用_输出调试信息("WM_PAINT消息");
            透明窗口绘制();
            return 0;
        case WM_SIZE:
            break;
        case WM_CLOSE:
        case WM_DESTROY:
            PostQuitMessage(0);
            break;
        default:
            break;
        }
        return DefWindowProc(hWnd, msg, wParam, lParam);
}


int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd) 
{
	::WNDCLASSEXA winClass;//欢迎公众号 任鸟飞逆向 学习讨论
	winClass.lpszClassName = "zijiandialog";
	winClass.cbSize = sizeof(::WNDCLASSEX);
    winClass.style =  CS_HREDRAW | CS_VREDRAW | CS_OWNDC | CS_DBLCLKS;
	winClass.lpfnWndProc = windowProc;//回调函数
	winClass.hInstance = hInstance;
	winClass.hIcon = 0;
	winClass.hIconSm = 0;
	winClass.hCursor = LoadCursor(NULL, IDC_ARROW);
    winClass.hbrBackground = NULL;//(HBRUSH)(BLACK_BRUSH); // 背景颜色
	winClass.lpszMenuName = NULL;
	winClass.cbClsExtra = 0;
	winClass.cbWndExtra = 0;

	RegisterClassExA(&winClass);//注册窗口类

	//创建窗口
    hWnd = CreateWindowExA(
        128 | 32 | 8 | WS_EX_LAYERED,// 扩展风格  透明窗口 WS_EX_LAYERED
		"zijiandialog",//类名指针
		"自建窗口",//窗口名指针
		WS_EX_LAYERED| 0 | 0x10000 | 0x20000 | 0x2000000 |WS_POPUP,//窗口的风格 
		100,//水平位置
		100,//垂直位置
		GetSystemMetrics(SM_CXSCREEN) * 640 / 2560,//宽高
		GetSystemMetrics(SM_CYSCREEN) * 480 / 1440,//高度
		0,
		0,
		hInstance,//应用程序实例的句柄  
		0//用户自定义的变量
	);

	SetWindowPos(hWnd, (HWND)-1, 100, 100, 0, 0, 19);
	ShowWindow(hWnd, SW_NORMAL);//显示窗口
	UpdateWindow(hWnd);//更新窗口
	SetWindowLongA(hWnd, -20, 589992);
	SetLayeredWindowAttributes(hWnd, 0, 1, 2);
	SetLayeredWindowAttributes(hWnd, 0, 0, 1);

	//window 消息循环
	MSG msg = { 0 };
	while (true)
	{
        //置顶窗口
        ::SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE);
        //读取当前的主框架的style
        DWORD dwStyle = ::GetWindowLong(hWnd, GWL_EXSTYLE);
        if (!((dwStyle & WS_EX_TOPMOST) == WS_EX_TOPMOST))
        {
            //保证主框架前置,然后再恢复到正常状态
            SetWindowPos(hWnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE);
        }

		if (msg.message == WM_ERASEBKGND)
		{
			break;
		}

		if (msg.message == WM_DESTROY || msg.message == WM_CLOSE || msg.message == WM_QUIT)
		{
			break;
		}
		if (PeekMessage(&msg, hWnd, 0, 0, PM_REMOVE))
		{
			DispatchMessage((&msg));//分发消息
			TranslateMessage(&msg);//解释消息	
		}
		Sleep(10);     
	}
	return 0;
}

各种变态功能实现的原理

FPS类的变态功能实现原理都是比较简单的,一般就是篡改游戏的执行流程或则直接修改游戏数据,由于验证不严格,直接可以实现效果。

另外一种就是封包实现,这样被检测几率相对小一些,我们举几个简单例子。

 

1.AK无限子弹

CE扫描子弹很容易扫描出来一堆

批量修改的方法确定出真实地址

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_18,color_FFFFFF,t_70,g_se,x_16

进行锁定,达到无限子弹的效果

也可以OD里子弹下写入断,

断到扣子弹的射击call

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_20,color_FFFFFF,t_70,g_se,x_16

修改扣子弹代码 dec eax 每次扣一发子弹,

我们改成 inc,每次加一发子弹或则nop不改变子弹。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_17,color_FFFFFF,t_70,g_se,x_16

这样,无限子弹就实现了

2.AK速射

在扣子弹代码下断,调用他的函数必然是射击call。

我们ctrl+F9 返回到射击call,分析以及修改射击参数实现速射。

( 速射还有用频繁调call 发包的方式实现)

发现是3个参数

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_20,color_FFFFFF,t_70,g_se,x_16

挨个修改参数 看实现效果

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_20,color_FFFFFF,t_70,g_se,x_16

子弹横飞了,瞬间打出几十上百发

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_13,color_FFFFFF,t_70,g_se,x_16

3.AK无后坐力

后坐力其实就是射击的时候修改了人物朝向,导致枪抖动,

我们直接nop掉射击call,发现不抖动了,但是一会就不射击了,

说明里面有抖动的call,也有射击的call。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_17,color_FFFFFF,t_70,g_se,x_16

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_20,color_FFFFFF,t_70,g_se,x_16

如果修改掉还抖动的话怎么办呢?

说明抖动的call还在外层,我们再返回测试就可以了。

这样的话,我们进call,F8一直走,返回挨个call nop测试即可

最后发现nop,掉这个call彻底不抖动了

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_20,color_FFFFFF,t_70,g_se,x_16

4.无限雷

同理于无限子弹

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_20,color_FFFFFF,t_70,g_se,x_16

5.锁血无敌

扫描浮点型血量,先锁定血量无敌 防止被自己雷炸死

彻底无敌要

返回的扣血call 直接nop掉

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_20,color_FFFFFF,t_70,g_se,x_16

6.雷爆

对雷数量下断,断到的投雷代码

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_20,color_FFFFFF,t_70,g_se,x_16

将2个可以跳过 投雷的代码改成nop 这样不会跳过 不需要自己释放 无限释放

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_20,color_FFFFFF,t_70,g_se,x_16

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_20,color_FFFFFF,t_70,g_se,x_16

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA5Lu76bif6aOe6YCG5ZCRfg==,size_15,color_FFFFFF,t_70,g_se,x_16

当然,以上的功能大多是指能在局域网中作为服务器的一方才能实现的,当你修改完AK的射速代码后,可能会发现对手的AK也被强化了。

写在最后

反外挂检测方法

以上只是我们对一些变态原理的实现举出的例子,并不是所有的游戏的变态方式都是通用的,但是为了游戏的安全,我们必须要了解这些手段。

我们不从挖掘漏洞者的角度去看待问题,是很难有更多的思路的。

想要给出反外挂建议,如果不从外挂的角度去了解,根本不可能知道这些反外挂的高效办法。

常用的针对性的反外挂方式

1.CRC检测

对于篡改代码直接CRC检测,可以添加多层嵌套,防止被处理掉。

并且值得注意的是,不进行实时检测,才是真正的最好检测, 立刻通知别人被检测到,无非是暴露了自己

不妨尝试下不定时检测,偷偷发包,再进行服务器统一封号是否很不错呢?

2.内存本地校验

对于修改内存可以加入多层数据相互验证,在服务器压力不大的情况下也可以结合服务器数据进行校验。

毕竟FPS 需要利用的数据较少,我们通过以上的学习,知道无非就是那几个数据

那么加大对此类的数据保护即可

3.针对相关绘制函数进行检测保护

通过上面的学习,我们可以知道 都需要使用哪些函数,其实还有更多方法

那么需要自己进行拓展了

4.行为检测

这个无需多言, 每个人有自己的判断尺度。

 

好了就说这么多吧

不知不觉 3万多字了,实际上录制的视频内容更多,本来只想做一个小小的专题课,和我其他大型课程比应该少的多,但是没想到就这样也已经100-200课了。

只是想把更多的东西分享给大家,无奈文字限制太大,很多一些想说的想讲的还是不能达到面面俱到。

我们下次见,感兴趣的同学可以关注 公众号任鸟飞逆向 欢迎探讨学习。

 

  • 28
    点赞
  • 165
    收藏
    觉得还不错? 一键收藏
  • 13
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值