工具的使用教程网上太多了,这里就不一一赘述了.
此处省略千言万语...
我们碰到问题去百度 google的时候搜出来的大部分都是别人的博客笔记.
养成记录学习的过程是一个好习惯.
前言:
分析游戏一般是选择可见值可推测状态等等相关回馈信息着手.不少游戏都会采取数值加密,尽可能的隐藏内存中可见值来妨碍肉眼分析过程.
因为LostArk是UE3Unreal Engine
引擎开发,所以相关词汇采用UE引擎为准(其他引擎也是大同小异).
3D游戏的图形世界中我们可操控的对象(LocalPlayer
)由两个部分组成Camera
与ModelObject
绑定共同构成.
其中相机是隐藏的不是真实存在的模型实体,只有模型会反馈给玩家从而绘制在屏幕上.
PS:我在BB些什么呢
开始实战讲解部分:
掏出咋们的万能工具Cheat Engine
对着可见血量进行4字节一顿筛查搜索(CE的使用可以B站搜索视频学习有很多UP主都出过
)最后筛查出最后两个.
从上面我可以看出来两个地址非常的近 很明显是当前血与最大血值的存储地址
???不会吧不会吧这么简单?
好了看一下有那些代码在访问此地址.
下访问断点后我们可以发现只有改变血量才会有访问代码
很明显两个访问地址在一个函数内一个在上一个在下.切到反汇编窗口过去看看.
从上面的汇编代码可以看出在与CALL_2142D00的返回值进行了一些比较,然后再将返回值写入R15+4F0(这里其实就一眼便知CALL_2142D00返回值才是真正的血量来源
).
仔细观察代码与R15对象结构下的其他信息就能发现并不是人物自身对象,而是UI标签(血量是解密以后填充到UI标签中让我们可以在UI上看见真实血量值
).
分析0x2142D00这个CALL,首先确定参数,从CALL外部来看有两个参数,RCX是一个人物对象,RDX是一个索引.然后继续观察CALL内部的过程.
进来CALL内部可以看到RCX+0x34
紧接着将dl也就是索引拷贝给ebx.继续跟进去看CALL_EA7DA0.
这个CALL就是解密人物对象自身信息关键代码了,很明显是异或加密了对象信息数值.
省略掉基址的查找了,大家可以自己在CALL_2142D00往上跟一下就能看到基址来源.
我们打印一下当前对象所有信息看看都是什么.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | void DumpData()
{
uint64_t info = 0 ;
uint64_t result = 0 , address = 0 ; / / rax
uint64_t InfoTmp = mem.RPM<uint64_t>(mem.BaseAddrss + 0x447FE38 );
InfoTmp = mem.RPM<uint64_t>(InfoTmp + 0xDC ) + 0x34 ;
for (size_t i = 0 ; i < 0x98 ; i + + )
{
/ / info = DecryptObjInfo(InfoTmp, i);
if (mem.RPM<uint32_t>(InfoTmp + 0x5A8 ) > 1 )
result = mem.RPM<uint8_t>(i + InfoTmp + 0x510 );
info = mem.RPM<uint64_t>(InfoTmp + 8 * ( int )result) ^ mem.RPM<uint64_t>(InfoTmp + 8 * i + 0x50 );
printf( "Address:%llX\t Info:%-4d\r\n" ,
InfoTmp + 8 * i + 0x50 ,
info);
}
}
|
其实想偷懒的话直接IDA F5就好了 F5的结果并不是一定准确只能作为伪代码参考(不过我是直接C+V舒服了)
然后修改一下代码成为写入加密数值.思密达的游戏特色很多数据放在本地判断.
修改攻速搭配蓄力技能让游戏体验加倍增长~~~
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | void DecryptObjInfo( int Index, int value)
{
uint64_t result = 0 , address = 0 , info = 0 ; / / rax
uint64_t InfoTmp = mem.RPM<uint64_t>(mem.BaseAddrss + 0x447FE38 );
InfoTmp = mem.RPM<uint64_t>(InfoTmp + 0xDC ) + 0x34 ;
if (mem.RPM<uint32_t>(InfoTmp + 0x5A8 ) > 1 )
result = mem.RPM<uint8_t>(Index + InfoTmp + 0x510 );
info = mem.RPM<uint64_t>(InfoTmp + 8 * ( int )result) ^ mem.RPM<uint64_t>(InfoTmp + 8 * Index + 0x50 );
if (info ! = value) {
int Decvalue = mem.RPM<uint64_t>(InfoTmp + 8 * ( int )result) ^ value; / / 取出当前加密值跟与写入的值进行异或
mem.WPM<uint64_t>(InfoTmp + 8 * Index + 0x50 , Decvalue);
}
}
/ / 1 是当前血
/ / 27 是最大血
/ / 2 是当前蓝
/ / 28 是最大蓝
/ / 77 是攻速
/ / 还有一些其他信息可以自行打印观看
DecryptObjInfo( 77 , 500 ); / / 500 = = 500 %
|
此处结构只有人物的基础信息并非完整的存在.真正的人物对象在另外一个结构通过一个ID跟此结构进行关联.还是比较少见的分开存储的方式可能不止一种方式去获取其他的方法没有去深入观察
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | / / 写法拉跨大家凑合看
int ARKObject::GetObjinfovalue(uint64_t UId, uint64_t Index)
{
if (this - >GetObjType() = = ObjType::Moster || this - >GetObjType() = = ObjType::Role)
{
auto uBase = mem.RPM<uint64_t>(g_BaseModule + GlobalBaseInfo);
auto size = mem.RPM<uint32_t>(uBase + 0x9C );
uBase = mem.RPM<uint64_t>(uBase + 0x94 );
for (size_t i = 0 ; i < size; i + + )
{
uint64_t EFUId = mem.RPM<uint64_t>(uBase + 0x18 * i);
if (EFUId = = UId) {
/ / 解密从上面的函数修改而来
return DecryptObjInfo(mem.RPM<uint64_t>(uBase + 0x18 * i + 8 ), Index);
}
}
}
return 0 ;
}
|
微软提供了非常强大的调试工具Windbg(新版本是WindbgX
).不废话了直接上手搭建双机调试的环境吧.
推荐环境为:VMWare+VirtualKD-3.0(此工具需要去Github上下载一个最新版本才能支持最新VMWare16等新版本)
这样搭建双机调试不容易出错,也很简单.
VMWare安装系统步骤就省略了.
系统安装完成以后将下载好的VirtualKD-3.0\target64
或者VirtualKD-3.0\target32
你的目标虚拟机系统是多少位就复制相对应到C盘根目录.
完成上述步骤以后就可以直接在虚拟机中运行vminstall.exe
点击Install按钮.
系统会被强制重启,出现以下画面.
Win10或更高需要禁用强制驱动签名才能完整跑起来VirtualKD.这个暂时不确定是我个人问题还是此工具问题
然后我们再配置本机的VirtualKD-3.0的设置
.本机系统是多少位就运行对应的vmmon32.exe
或vmmon64.exe
.
现在的新版本支持选择Windbg Preview也就是WindbgX
.直接选择对应目录打开对应的exe就好了.
老版本的需要选择Curstom
配置:你的工具目录\windbgx1.27DbgX.Shell.exe /k com:pipe,resets=0,reconnect,port=$(pipename) -y "cache*D:\localsymbols;srv*https://msdl.microsoft.com/download/symbols;"
还有你的symbols也就是PDB选择保存在那个目录上面.PDB的下载问题需要科学上网解决
可能有的时候会出现一个问题全部都配置好了.但是发现还是无法双机调试启动成功.
出现这个情况需要检查虚拟机的调试模式是否开启.
cmd
中输入msconfig
查看
强烈推荐巧妙运用VMware的快照功能,初始化环境后将需要的工具拷贝进虚拟机,然后可以备份一个快照.防止出情况后续再重新配置.
到这里基本配置就OK了.
接下就是IDA与WindbgX搭配进行静态分析与动态调试了.
我这边举例一个保护进程的功能,也是现在流传最为广泛的一个.
我们先把虚拟机的ntoskrnl.exe
搞出来用IDA静态分析一手.
IDA也需要设置一下PDB的下载与加载问题.打开IDA的根目录找到IDA Pro\cfg\pdb.cfg
打开后找到_NT_SYMBOL_PATH = "SRV*D:\\localsymbols*http://msdl.microsoft.com/download/symbols";
进行设置即可.记得换成自己的目录
把ntos丢进IDA让他自动跑完分析保存成.i64文件即可.分析其他内核模块也是差不多这个步骤
开始我们的调试内核之旅:
在虚拟机中打开一个记事本,再使用WindbgX去暂停虚拟机.
再命令行输入:!process 0 0 notepad.exe
出现以下信息
PROCESS
:为进程对应的内核EPROCESS
对象首地址.
1 2 3 4 | PROCESS ffffca83623e8080
SessionId: 1 Cid: 17d4 Peb: 4d6ad51000 ParentCid: 0ee8
DirBase: 3532e002 ObjectTable: ffff9e829eca2980 HandleCount: 282.
Image: notepad.exe
|
拿到EPROCESS以后呢.直接按照-0x30其实就是EPROCESS地址-sizeof(_OBJECT_HEADER)
方法去拿_OBJECT_HEADER
结构所在位置.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 0 : kd> dt _object_header ffffca83623e8080 - 0x30
nt!_OBJECT_HEADER
+ 0x000 PointerCount : 0n196247
+ 0x008 HandleCount : 0n6
+ 0x008 NextToFree : 0x00000000 ` 00000006 Void
+ 0x010 Lock : _EX_PUSH_LOCK
+ 0x018 TypeIndex : 0xb4 ''
+ 0x019 TraceFlags : 0 ''
+ 0x019 DbgRefTrace : 0y0
+ 0x019 DbgTracePermanent : 0y0
+ 0x01a InfoMask : 0x88 ''
+ 0x01b Flags : 0 ''
+ 0x01b NewObject : 0y0
+ 0x01b KernelObject : 0y0
+ 0x01b KernelOnlyAccess : 0y0
+ 0x01b ExclusiveObject : 0y0
+ 0x01b PermanentObject : 0y0
+ 0x01b DefaultSecurityQuota : 0y0
+ 0x01b SingleHandleEntry : 0y0
+ 0x01b DeletedInline : 0y0
+ 0x01c Reserved : 0
+ 0x020 ObjectCreateInfo : 0xffffca83 ` 605e58c0 _OBJECT_CREATE_INFORMATION
+ 0x020 QuotaBlockCharged : 0xffffca83 ` 605e58c0 Void
+ 0x028 SecurityDescriptor : 0xffff9e82 `a116906c Void
+ 0x030 Body : _QUAD
|
+0x01b Flags
关键就是这个flags了.现在普遍用的是写入4
对应将+0x01b KernelObject : 0y0
置为1
.此方法源于ydark
还有一个就是看雪上发过的设置成退出标志位0x71
.对应的即是如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | > 0 : kd> eb ffffca83623e8080 - 0x30 + 0x1b 0x71
0 : kd> dt _object_header ffffca83623e8080 - 0x30
nt!_OBJECT_HEADER
....
+ 0x01b Flags : 0x71 'q'
+ 0x01b NewObject : 0y1
+ 0x01b KernelObject : 0y0
+ 0x01b KernelOnlyAccess : 0y0
+ 0x01b ExclusiveObject : 0y0
+ 0x01b PermanentObject : 0y1
+ 0x01b DefaultSecurityQuota : 0y1
+ 0x01b SingleHandleEntry : 0y1
+ 0x01b DeletedInline : 0y0
....
|
他们是怎么确定退出标志位是0x71
呢?调试吧?
那我们也调试一下吧.
对ffffca83623e8080-0x30+0x1b
下写入断点
1 2 3 | 0 : kd> ba w1 ffffca83623e8080 - 0x30 + 0x1b
0 : kd> bl
0 e Disable Clear ffffca83` 623e806b w 1 0001 ( 0001 )
|
退出进程并没有任何反应???是我操作不对吗.我在尝试一次.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | > 0 : kd> ba w4 ffffca8363233080 - 0x30 + 0x1b
Data breakpoint must be aligned
^ Syntax error in 'ba w4 ffffca8363233080-0x30+0x1b'
0 : kd> ba w8 ffffca8363233080 - 0x30 + 0x1b
Data breakpoint must be aligned
^ Syntax error in 'ba w8 ffffca8363233080-0x30+0x1b'
0 : kd> ba w8 ffffca8363233080 - 0x30 + 0x18
0 : kd> g
/ / 移除对象了从偏移来看也就这里有点关系了
nt!ObpRemoveObjectRoutine + 0x70 :
fffff803` 6e283cec 804b1b80 or byte ptr [rbx + 1Bh ], 80h
fffff803` 6e283cf0 488b8788000000 mov rax,qword ptr [rdi + 88h ]
/ / 看API的名字也知道释放对象了 偏移位置不对操作的也是byte
nt!ObpFreeObject + 0x17d :
fffff803` 6e283ed9 40887b18 mov byte ptr [rbx + 18h ], dil: 2
fffff803` 6e283edd 4885ed test rbp, rbp
/ / 到这个API出现基本就没我们目前什么事了说明已经被清理了
nt!memset + 0xb3 :
fffff803` 6e01b433 0f2941e0 movaps xmmword ptr [rcx - 20h ],xmm0
/ / 到这个API出现基本就没我们目前什么事了说明已经被清理了
nt!MiClearPteAccessed + 0x6de :
fffff803` 6de5675e ff470c inc dword ptr [rdi + 0Ch ]
|
Data breakpoint must be aligned
数据断点必须要对齐咯.那咱们就从0x18下8字节写入.
所以网上的0x71
到底从何而来呢,目前还是不知道呢.估计是根据对象结构去自行测试出来的吧.
还有就是此方法只针对R3的Openprocess有效
驱动直接用PsLookupProcessByProcessId
获取EProcess
完全没问题.
所以这个保护进程其实也就是和置位EProcess
下+0x87a Protection : _PS_PROTECTION
差不多一个效果.
好了动态调试既然没办法找到0x71
这个写入的来源,接下来就可以使用IDA了.我们可以打开IDA分析好的ntos.i64
文件了.
然后在IDA函数名字列表搜索OBJECT_
为什么要搜索这个呢?结构体就叫这个呀.别问,问就单词联动+肉眼观察法
我们可以看到有四个OBJECT_HEADER_TO
什么什么的.对我们来说关键的是TO_PROCESS
因为分析的这个功能是进程保护.
然后我们双击转到这个函数的反汇编窗口直接F5
是的就这么直接.有PDB加载还看锤子的汇编代码不要学我,大家好好学汇编.这不是一个好习惯
.
这个函数啥啊就这么点东西,a1推测(IDA F5出来的结果不要过度相信依赖.因为是静态分析别说太绝对给自己留个退路不然容易啪啪打脸)应该是OBJECT_HEADER对象
目前不是我们所需要关心的.因为这是Windows句柄表相关的知识
这里就不多废话啦.我们主要的目的是去分析他们的0x71
到底从何而来.
点击函数名字按X键交叉引用(xrefs)找到上层调用函数
此函数有四处调用,分别来自两个函数.其中有一个函数调用了三次.
我们先看第一个双击跳过去.然后拉到函数起始也就是头部,开始一点点分析.
v8
就是OBJECT_HEADER对象
因为它有一个明显的-0x30特征存在,a4就是EPROCESS
.我没有去N键重命名
既然知道了v8就是OBJECT_HEADER对象
那这个v8+27(十进制的0x1b)
就是我们要找的Flags
我们将鼠标点到27
上去高亮.然后可以发现当前函数中有五处操作了v8+27
并且都&按位与
某个数.不同的数值其实就是对应着要操作的Flags的不同bit位
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | union / / 0x001B ; 2 elements; 0x0001 Bytes
{
UINT8 Flags; / / 0x001B ; 0x0001 Bytes
struct / / 0x001B ; 8 elements; 0x0001 Bytes
{
UINT8 NewObject : 1 ; / / 0x001B ; Bit: 0
UINT8 KernelObject : 1 ; / / 0x001B ; Bit: 1
UINT8 KernelOnlyAccess : 1 ; / / 0x001B ; Bit: 2
UINT8 ExclusiveObject : 1 ; / / 0x001B ; Bit: 3
UINT8 PermanentObject : 1 ; / / 0x001B ; Bit: 4
UINT8 DefaultSecurityQuota : 1 ; / / 0x001B ; Bit: 5
UINT8 SingleHandleEntry : 1 ; / / 0x001B ; Bit: 6
UINT8 DeletedInline : 1 ; / / 0x001B ; Bit: 7
};
};
|
我上图只分析了第一个NewObject
大家在学习的过程中可以去把下面的也分析了 大家就能得出来为什么他们有修改成4
或者0x71
了.
通过修改对应bit位判断
让此函数内部判断失效返回错误值.拼凑出来了这个"0x71"
最后我再把这个函数的调用层级带大家一起看一下.
NtOpenPorcess->PsOpenProcess->ObOpenObjectByPointer->ObpCreateHandle->ObpIncrementHandleCountEx(我们返回到的函数)
上述调用链中的函数大部分过程都是很繁琐的即是IDA F5以后观看起来也很头皮发麻.
NtOpenPorcess
要打开进程就要在请求进程自身创建一个Process的Handle
来存储着被打开的进程一些信息.
通过修改了Flags bit位
的判断实现了让来自NtOpenPorcess
内部调用链出现错误返回.从而达到了保护的目的.(这种方法只能防住R3的调用,驱动有驱动的玩法了后面有机会在BB)