尚未完成,敬请期待
前言
ida中Func_
开头的函数,Stru_
开头的结构体。
基础知识
IDA中的结构体:从入门到进阶
在逆向大型工程的时候,我们不可避免地会遇到大量复杂的结构体。合理地利用ida
定义结构体的功能,可以让我们的分析效率大大提高。本节我们从零开始,介绍如何在ida
中定义结构体,并深入展开介绍相关进阶操作。
定义结构体√
新建、删除与重命名
Structures
界面内(可通过shift+f9
打开)右键并点击Add struct type...
(或直接按Insert
键)即可创建新结构体:
在弹窗中输入要创建的结构体的名称:
此时结构体为空:
按Delete
键可以删除当前选中的结构体:
按n
可以更改当前选中的结构体或变量的名称:
基本数据类型
按d
,可以新增结构体内的变量,并可切换设置数据类型为BYTE
, WORD
, DWORD
, QWORD
:
按a
,可以设置字符串:
按*
键,可以设置数组。注意默认会是BYTE
数组,如果需要创建WORD
, DWORD
, QWORD
的数组,则需要先创建一个WORD
, DWORD
, QWORD
变量,选中这个变量后,再按*
设置数组:
变量的删除与结构体的收缩
按;
键可以为结构中的某一行添加注释,这里就不演示了。
按u
可删除结构体内的变量。如果该变量不是结构体的最后一个变量,那么结构体大小不变,该变量对应位置变为undefined
(比如我们删除了byData2
):
当被删除的变量下面没有其他的变量的时候,结构体大小会自动收缩(比如我们在上图的基础上又删除了szString
和adwData
):
数据显示格式
按h
,可以切换设置该变量在IDA主窗口中显示为十进制或十六进制,按b
设置为二进制:
按ctrl+o
,可以设置该变量在IDA主窗口中显示为一个偏移(如果此偏移地址在IDA中是一个有效的地址或标签,则双击时可以跳转到对应的偏移地址处):
比如下图中的结构体中红框里面的变量显示的就是一个偏移:
如果我们不将此变量的数据格式设置为偏移,就会出现下图红框中的情形,不利于我们快速跳转过去(另外,没想到吧,ida主窗口中的结构体是可以通过点击左侧的箭头来向下展开的):
函数指针
按y
,可以修改成员变量的声明(即修改其数据类型),不仅可以修改成常规的数据类型,也可以修改成函数类型、结构体类型等,比如:
上图结构体中的Func_Wrapper_malloc_1400A6410
变量其实是一个函数指针,但是目前并没有设置数据类型(所以默认应该是__int64
类型),因而伪代码中需要先进行强制类型转换,转换成通过识别参数和返回值来确定出的函数类型,然后才能调用函数。
我们可以按y
修改结构体中Func_Wrapper_malloc_1400A6410
变量的声明。
复制上图伪代码中自动推断出的__int64 (__fastcall *)(__int64, __int64)
作为结构体中变量的声明:
也可以找到实际的Func_Wrapper_malloc_1400A6410
函数的定义:
将void *__fastcall Func_Wrapper_malloc_1400A6410(__int64 a1, size_t a2)
修改为void *__fastcall (*Func_Wrapper_malloc_1400A6410)(__int64 a1, size_t a2)
,并作为结构体中变量的声明:
转换后的结果:
应用结构体√
主窗口中
在ida
主窗口中,将鼠标光标放在结构体开始的地方(以此处为例):
按alt+q
,选择对应的结构体:
完成转换:
从而将这里的数据转换为:
伪代码中
在下图的伪代码中,我们经过分析(此类分析过程留待后续专门章节,本节不过多介绍),确定了off_143DE40C0
是一个Stru_DirInfoOfResourcesInExe
结构体的数组,v1
是指向一个Stru_DirInfoOfResourcesInExe
结构体的指针。
因此,我们可以把v1
的数据类型从char **
转换成Stru_DirInfoOfResourcesInExe *
。
右键v1
,点击Convert to struct *...
:
选择相应的结构体:
即可完成转换:
也可以右键v1
,选择Set lvar type...
(或者直接按y
):
将v1
的声明从char **v1
改为Stru_DirInfoOfResourcesInExe *v1
:
这样同样可以设置变量的数据类型。
如果不想把v1
设置成结构体了,可以右键v1
,并点击Reset pointer type
:
即可将v1
重置回万能的__int64
:
导出与导入√
Local Types窗口与Structures窗口
在描述如何导出或导入结构体信息之前,我们需要首先了解一些前置知识。
为了给types提供直观且强大的接口,ida
提供了两种类型的types,分别为Assembler level types
和C level types
。
Assembler level types
是在Structures
或Enums
窗口中定义的类型。由于我们必须手动指定成员变量的偏移量和其他属性,因此ida认为成员变量的偏移量是固定的,并且ida永远不会改变此结构体的成员变量。
C level types
是在Local Types
窗口中定义的类型。对于它们,ida会自动计算成员变量的偏移量,如有必要,可能会移除成员变量并更改结构体的总大小。
在我们手动编辑了C level types
的type后,IDA会将该type视为Assembler level types
。
需要注意是是,Structures
窗口中不只有Assembler level types
,也含有以灰色显示的C level types
;Local Types
窗口中不只有C level types
,也含有以灰色显示的Assembler level types
。灰色用来间接表示该type“不属于”此窗口,实际上是另一个窗口中的副本。具体显示颜色可见下表。
所属types | Structures窗口中显示 | Local Types窗口中显示 |
---|---|---|
Assembler level types | 黑色 | 灰色 |
C level types | 灰色 | 黑色 |
Structures
窗口中的TestStruct
:
Local Types
窗口中的TestStruct
:
此外,需要注意的是,Structures
中的结构体一定会显示在Local Types
窗口中,但是Local Types
窗口中的结构体不一定会显示在Structures
窗口中。
推荐阅读:
- IDA Help: Structures window
- IDA Help: Local types window
- IDA Help: Assembler level and C level types
- What is the point to have 2 different places for structures: Local Types and Structures in IDA?
导出
依次选择File
-> Produce file
-> Create C header file...
:
从而可以将Local Types
界面中的全部type导出为一个.h
头文件:
导入
导入整个.h
头文件
上一环节我们从ida
中导出了全部type(不止局限于结构体)的.h
文件,自然也是可以将任意的.h
头文件导入到ida
中的。
依次选择File
-> Load file
-> Parse C header file
(或者直接按ctrl+f9
):
选择相应的.h
头文件,即可完成导入:
注意,这个是导入到Local Types
窗口中,如果需要同步到Structures
窗口中,则可以在Local Types
中双击对应行并点击确定,即可同步到Structures
窗口中:
也可以在对应行(可多选)右键,并点击Synchronize to idb
:
又或者可以在Structures
窗口中按Insert
,点击Add standard structure
:
通过关键词搜索(ctrl+f
),找到想要同步的结构体并点击确定即可同步:
Local Types
窗口中的type有没有同步显示在Structures
窗口中,可以通过下图中的Sync
一列来判断。Auto
的就是已经同步显示的。
导入一个或几个结构体
如果我们只需要导入一两个结构体,可以在Local Types
窗口中,按Insert
键,粘贴并导入我们的结构体:
需要注意的时候,结构体内部使用的数据类型如图中QWORD,必须是ida
中定义过的,否则要通过#define或typedef来定义QWORD,不然会导入失败。
同样,这样只是将结构体导入到了ida
的Local Types
中,还没有同步到当前IDA文件的Structures
窗口中,可以按照上面介绍过的几个方法来同步。
进阶操作
示例一
示例二
关于Set call type
和Force call type
变量合并
内存对齐
函数
windows平台编码方案辨析
x64dbg
字符串断点
老版本
老版本的x64dbg的条件断点想要设置字符串的话,并不是很方便,一般是通过比较QWORD, DWORD等数值来间接实现。字符串短一点还好,如果很长的话,手动转换出合适的断点条件是比较繁琐的。
于是我简单写了一个小脚本,用于将n字节的字符串转换为$\lceil\frac{n}{8}\rceil$个QWORD、$\lceil\frac{n}{4}\rceil$DWORD、$\lceil\frac{n}{2}\rceil$个WORD或$n$个BYTE。使用的时候修改path
为目标字符串,block_size
为1, 2, 4, 8。另外,使用时可以根据utf16(unicode), gbk(ansi), ascii等编码细调。
import struct
def str2utf16bytes(s : str) -> bytes:
# utf-16-be big endian no BOM
# utf-16-le little endian no BOM
# utf-16 little endian + BOM
return s.encode('utf-16-le')
def utf16bytes2str(b : bytes) -> str:
return b.decode('utf-16-le')
def bytes2blocks(b : bytes, block_size : int) -> list:
result = []
block_num = len(b) // block_size
if len(b) % block_size:
block_num += 1
for block_idx in range(block_num):
block_bytes = b[block_idx * block_size : (block_idx+1) * block_size]
block_bytes += b'\x00' * (block_size - len(block_bytes))
if block_size == 1:
mode = '<B'
elif block_size == 2:
mode = '<H'
elif block_size == 4:
mode = '<I'
elif block_size == 8:
mode = '<Q'
else:
raise Exception("Block Size must be 1, 2, 4 or 8.")
block_data = struct.unpack(mode, block_bytes)[0]
result.append((block_idx, block_data, block_bytes))
return result
if __name__ == '__main__':
#path = r'd:\Game\GujianOL/data'
path = r'd:\Game\GujianOL/data/_index/708.idx'
block_size = 8
print(path)
utf16 = str2utf16bytes(path)
results = bytes2blocks(utf16, block_size)
for block in results:
idx, data, bytes = block
print('{:<8s}{:<16} {}'.format(hex(idx * block_size), hex(data), utf16bytes2str(bytes)))
path
为d:\Game\GujianOL/data
, block_size
为 8
时,上面脚本的运行结果为:
d:\Game\GujianOL/data/_index/708.idx
0x0 0x47005c003a0064 d:\G
0x8 0x5c0065006d0061 ame\
0x10 0x69006a00750047 Guji
0x18 0x4c004f006e0061 anOL
0x20 0x7400610064002f /dat
0x28 0x69005f002f0061 a/_i
0x30 0x7800650064006e ndex
0x38 0x3800300037002f /708
0x40 0x7800640069002e .idx
因此,如果我们希望在rcx
指向d:\Game\GujianOL/data/_index/708.idx
时断下断点,可以选择性地设置这样的条件:判断rcx+0x20
处的QWORD等于utf16编码的字符串/dat
对应的小端QWORD,如下图所示。当然我们也可以根据需要来决定是否采用更多的QWORD进行匹配来避免条件断点的精确性。
新版本
byte:[]之类
新函数
条件断点中的字符串以及一些log命令等
addr
module.base
" - ".base
CRC16
虚幻OodleLZ库
逆向分析前置
游戏文件目录简述√
官服
游戏根目录D:\Game\GujianOL
下有:
目录 | 含义 |
---|---|
bin64 | 包含gujianol.exe 及与之运行相关的一些exe、dll文件。重点有:gujianol.exe , oo2core_6_win64.dll , browserhost.exe , crashpad_handler.exe , sentry.dll |
data | 游戏的资源文件,从data000 到data116 ,每个文件最大为1G。另有_index 文件夹存放着708.idx , 753.idx 等索引文件。 |
ExportFace | 游戏内角色的捏脸数据 |
ExportPrefab | 游戏内仙府的建造方案 |
Interface | |
screenshots | 游戏截图 |
settings | 跟随游戏账号的设置 |
tools | 动画编辑器、音乐编辑器 |
.sentry-native | The Sentry Native SDK is an error and crash reporting client for native applications, optimized for C and C++. Sentry allows to add tags, breadcrumbs and arbitrary custom context to enrich error reports. |
文件 | 含义 |
---|---|
GameLauncher.exe | (不是真正的)游戏启动器 |
branch-version | 游戏版本号 |
launcher.ini | 启动器的配置文件,存储了APP_ID , APP_NAME , LAUNCHER_PATH 等信息,以及UPGRADE , LOG , AUTH , SENTRY 的网址 |
gamelauncher.db | json格式,存储了一些文件的md5,比如GameLauncher.exe , launcher.ini |
union.db | 配置文件,APPID=26 ,ID=wangyuan |
sentry.dll | Sentry Native SDK |
crashpad_handler.exe | crashpad_handler 是Google开发的一款用于收集和处理应用程序崩溃报告的工具 |
log_launcher_20230826.log | Launcher启动失败时的log文件 |
skins.skin | 应该是启动器的UI皮肤文件 |
game.ico | 图标 |
title.png | logo |
%LocalAppData%\WangYuan\GameCenterLite
目录下(%LocalAppData%
即C:\Users\UserName\AppData\Local
)有:
目录 | 含义 |
---|---|
.sentry-native | Sentry Native SDK |
文件 | 含义 |
---|---|
Launcher.exe | 真正的游戏启动器 |
Agent.exe | 收发一些跟流量相关的数据,并通过管道与Launcher.exe进行数据传输 |
wy.login_sdk_x64.dll | 网元账号登录的SDK |
wke.dll | BlzFans/wke: 3D Web UI. Web and Flash Embedded in 3D games, based on WebKit |
sentry.dll | Sentry Native SDK |
oo2core_6_win64.dll | Oodle的动态链接库 |
launcher.db | json格式,存储了一些文件的md5,比如Agent.exe , Launcher.exe |
crashpad_handler.exe | crashpad_handler 是Google开发的一款用于收集和处理应用程序崩溃报告的工具 |
log_launcher_20231029.log | Launcher启动失败时的log文件 |
error.html | 启动器加载网页失败时展示的页面 |
skins.skin | 应该是启动器的UI皮肤文件 |
steam服
基本同上,只是没有了%LocalAppData%\WangYuan\GameCenterLite
这个文件夹,且游戏根目录下没有了启动器、配置信息等文件(因为登录信息是调用steam的接口)
启动游戏时的进程调用链√
官服
1. GameLauncher.exe
游戏根目录下的GameLauncher.exe
实际上是一个假的启动器。
使用x32dbg
动调(为避免唠叨,本文后续所有的动调都默认需要管理员权限,因为游戏本身和启动器都是管理员权限运行的),在CreateProcessA
和CreateProcessW
下断点,f9
运行后断在CreateProcessW
,[esp+4]
(即args1)为D:\Game\GujianOL\crashpad_handler.exe
。检索一下,得知crashpad_handler
是Google开发的一款用于收集和处理应用程序崩溃报告的工具。跟主线任务无关,直接无视。
继续f9
,这次CreateProcessW
的args1为C:\Users\iyzyi\AppData\Local\WangYuan\GameCenterLite\launcher.exe
,栈中函数的全部参数为:
对照CreateProcessW
的函数声明,我们可以发现,lpCurrentDirectory
设置为游戏的根目录。
BOOL CreateProcessW(
[in, optional] LPCWSTR lpApplicationName,
[in, out, optional] LPWSTR lpCommandLine,
[in, optional] LPSECURITY_ATTRIBUTES lpProcessAttributes,
[in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes,
[in] BOOL bInheritHandles,
[in] DWORD dwCreationFlags,
[in, optional] LPVOID lpEnvironment,
[in, optional] LPCWSTR lpCurrentDirectory,
[in] LPSTARTUPINFOW lpStartupInfo,
[out] LPPROCESS_INFORMATION lpProcessInformation
);
2. Launcher.exe
Launcher.exe
是真正的游戏启动器,不在游戏根目录中,而是在%LocalAppData%\WangYuan\GameCenterLite
目录下,初步推测应该适用于该游戏厂商旗下的所有游戏的启动。
但如果我们直接运行此程序,会立马闪退,并在%LocalAppData%\WangYuan\GameCenterLite
目录下生成形如log_launcher_20231102.log
的日志文件,内容为:
21:43:45:448 [8644:17568] [INFO] Launcher Start
21:43:45:493 [8644:17568] [ERROR] GetGamePath failed.
21:43:45:493 [8644:17568] [INFO] Launcher Exit
不应该啊,明明上一步GameLauncher.exe
中通过CreatePorcessW
调用此程序时,没有其他的参数,只是单纯传递了此程序的绝对路径。为什么不能成功运行呢?难道是因为直接在资源管理器中启动此程序时,没有将lpCurrentDirectory
设置为游戏的根目录吗?
然而尝试使用cmd,进入游戏根目录下,再通过绝对路径的方式启动此程序,依然无法成功运行:
也尝试使用x64dbg
打开此程序(但先不运行),使用命令chd "D:\Game\GujianOL"
改变当前目录(chd - SetCurrentDirectory),在运行程序,也是无法成功运行:
没办法,使用ida静态分析一下Lanucher.exe
吧。
因为前面的报错日志中提到了GetGamePath failed
,所以尝试直接搜索这个字符串,定位到sub_14000D710
:
发现了相当显眼的get_env(),这里Lanucher.exe
尝试从环境变量中获取GAME_PATH
和GAME_ID
,难道参数是通过环境变量来传递的吗?
还没来得及多加思考,紧跟其后,我们可以发现-gamepath=
和-gid=
这两个明显是命令行参数的字符串。
回想起我们在游戏根目录下发现的union.db文件中就存储着:
[UNION]
APPID=26
ID=wangyuan
从而推测-gid=26
。
而-gamepath=
的值我们首先猜测为游戏根目录D:\Game\GujianOL
。
现在我们尝试通过%LocalAppData%\WangYuan\GameCenterLite\Launcher.exe -gid=26 -gamepath=D:\Game\GujianOL
来运行此程序。成功运行!
接下来,我们就可以使用x64dbg
动调此程序了:
同样在CreateProcessW
下断点。
第 i 次触发断点 | 创建进程路径 |
---|---|
1 | C:UsersiyzyiAppDataLocalWangYuanGameCenterLitecrashpad_handler.exe |
2 | C:UsersiyzyiAppDataLocalWangYuanGameCenterLiteAgent.exe |
null | 游戏启动器中会弹出登录框,需要完成账号登录,并点击开始游戏按钮,才能继续调试 |
3 | C:UsersiyzyiAppDataLocalWangYuanGameCenterLiteAgent.exe |
4 | D:GameGujianOLbin64GujianOL.exe --fs=data:_index/708.idx:_index/708.idx |
有一点我想吐槽我自己,深夜写博客,脑子不清醒了,把文件union.db
中的wangyuan
当成了-gamepath
的值,于是在触发了第3次断点并继续运行后,被卡在更新游戏资源文件这一步,多次动调都会这样(然而又尝试正常在资源管理器中运行游戏启动器并点击开始游戏,而不通过调试器或cmd启动,是不会卡在这个界面的):
我就纳闷了,以前不是可以通过命令行参数成功启动嘛,怎么就今天写博客的时候不行了呢?走了很多弯路,大脑也浑浑噩噩的,但其实并没有什么高深的原因,问题就出在-gamepath
的参数是错误的,仅此而已。
另外,如果有读者认为,我们主要是靠猜测拼凑出的-gid=
和-gamepath
,一点也不优雅。那我们还可以在GameLauncher.exe
通过CreateProcessW
启动Launcher.exe
的时候,使用procexp
(需管理员权限,因为目标进程也是管理员权限)查看此时GameLauncher.exe
的环境变量:
3. gujianol.exe
前面都是在真假启动器上做工作,总算分析到游戏程序本身了。
尝试命令行启动D:\Game\GujianOL\bin64\GujianOL.exe --fs=data:\_index/708.idx:\_index/708.idx
,不出意料地没能成功运行,反倒运行了Launcher.exe
,要求我们进行登录。这一点倒是可以预料的,毕竟命令行参数中并没有账号信息,只是简单地给出了索引文件的名称(708.idx
是太华山
服务器对应的索引文件,后续章节会继续讨论)。
因为前面出现过通过环境变量在父子进程间传递信息,所以这里我们很容易联想到账号信息等数据,有可能是通过环境变量,从游戏启动器传递给游戏程序的。
使用ida打开Lanucher.exe
,随手搜了一些字符串,比如start
,就发现了Start game success
,并通过交叉引用定位到sub_14001AA30
:
再往上翻两下,就找到了CreateProcessW
:
再往上一翻:
很明显,这里调用wputenv_s
函数,将gates
, gj3_gates
, gj3_ticket
, gj_jates
, gj_ticket
等信息存入当前进程的环境变量。然后调用了CreateProcessW
来启动游戏进程:
BOOL CreateProcessW(
[in, optional] LPCWSTR lpApplicationName,
[in, out, optional] LPWSTR lpCommandLine,
[in, optional] LPSECURITY_ATTRIBUTES lpProcessAttributes,
[in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes,
[in] BOOL bInheritHandles,
[in] DWORD dwCreationFlags,
[in, optional] LPVOID lpEnvironment,
[in, optional] LPCWSTR lpCurrentDirectory,
[in] LPSTARTUPINFOW lpStartupInfo,
[out] LPPROCESS_INFORMATION lpProcessInformation
);
其中,倒数第4个参数lpEnvironment
为NULL时,创建的子进程将使用父进程的环境变量。
[in, optional] lpEnvironment
A pointer to the environment block for the new process. If this parameter is NULL, the new process uses the environment of the calling process..
因而,gujianol.exe
继承了Lanucher.exe
设置的环境变量。
我们可以使用procexp64
(同样需要管理员权限运行,因为目标进程也是管理员权限)来查看一下相关进程的环境变量:找到目标进程,右键菜单点击Properties...
,在新窗口中点击Environment
选项卡即可。以下截图我截去了无关的默认环境变量。
Launcher.exe
登录前:
Launcher.exe
登录后,但未启动游戏,此时环境变量没有变化:
启动游戏后,Launcher.exe
的环境变量增加了许多诸如登录票据、服务端地址等信息:
接下来启动的gujianol.exe
的环境变量继承了上一步的Launcher.exe
的环境变量:
完整的进程调用链
以上我们便完成了对官服启动游戏时的进程调用链的分析,这里我们使用Procmon64
记录一下全部有关进程创建的流程。
首先我们需要设置一下Procmon64
的Filter,不然记录超级多,没法看。这里我们只关心进程的创建,所以可以添加一个Operation
为Process Create
的Filter:
记录如下:
steam服
steam服没有官方启动器,而是通过steam来完成信息认证。
直接运行gujianol.exe
并不会立即启动游戏,而是通过steam来启动:"D:\Game\Steam\steam.exe" steam://run/1541840
因为后续工作主要集中在官服,所以这里我们就不过多探索了。再逆下去其实就是逆steam通用的登录认证与启动游戏的流程了。
下文中如果不特别指出,均为对官服的逆向分析。
如何动调游戏进程⚪
直接附加游戏进程
传统方法,Launcher.exe
登录并点击开始游戏后,直接使用x64dbg
附加gujianol.exe
进程,手速越快,错过的初始化逻辑越少。
如果是老版本的x64dbg
(我用的是Jul 2 2019
版本),附加成功后,会自动断在此处:
再接下来就可以正常调试后续的代码。
要想附加进程后自动断下断点,需要我们在设置中勾选附加断点
这一选项:
但是较新版本中,比如Oct 5 2023
版本的x64dbg
,是没有附加断点
这个选项的:
在 No "Attach Breakpoint" Event Option in snapshot_2020-11-12_05-12 · Issue #2528 · x64dbg/x64dbg 这个issue中,开发者解释说:
No, that feature has been removed. The reason is that the breakpoint requires a thread to be created in the debuggee and this is a way to detect the debugger.
If you want to stop the execution for some reason, you can press the pause button. What is your use case for the attach breakpoint?
大意是x64dbg
为了支持附加断点
这个功能,就需要在目标进程中创建一个远程线程,这可能会导致目标进程能够检测到有调试器的存在,出于隐藏调试器的考虑,此后不再支持附加断点
。
我刚提了一个feature issue询问开发者是否可以考虑增加一个附加选项,在逆向人员知道开启附件断点
这个功能可能导致的后果时,自行决定是否继续启用。但我估计应该不会有后续。
所以说在较新版本的x64dbg
中,当目标进程启动后,不仅需要我们快速地附加到此进程,而且附加到进程后也需要我们迅速地按下F12
Pause暂停键,不让程序继续运行下去(全都运行完了我还分析啥逻辑哦)。
其实删除掉附加断点
这个功能,只是强迫症(比如我)难受一点,实际上影响并不是很大。我一开始认为,如果没有在附加到进程后自动断下断点,可能会错过一些逻辑。但实际上,本来等程序运行起来后再附加到进程这个操作,哪怕手速再快,也会错过很多逻辑。现在只不过多了一步手动操作,看起来有些繁琐罢了。
补充:x64dbg的开发者回复我的issue了,并告知我可以通过编写注册了CB_ATTACH事件回调的插件来实现我的需求。有时间再写这个插件吧。
You should be able to call CreateRemoteThread from a plugin that registers the CB_ATTACH callback.
Callback Structures — x64dbg documentation
x64dbg 插件开发SDK环境配置 - lyshark - 博客园 (cnblogs.com)
【c++】x64dbg插件开发,摸索前进:第二章第一个正式插件 - 『编程语言区』 - 吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn
x64dbg/PluginTemplate: Plugin template for x64dbg. (github.com)
How Windows Debuggers Work | Microsoft Press Store
DebugBreakProcess function (winbase.h) - Win32 apps | Microsoft Learn
源码中搜索:cbDebugDetach
DbgChild: 自动附加子进程√
直接附加到游戏进程只能跟踪后续执行的相关逻辑,不可避免地无法跟踪一些游戏初始化的相关代码。此时我们可以考虑使用DbgChild
。DbgChild是一个x64dbg
插件,可用于自动附加到子进程并进行调试。
初始化插件
从Releases页面下载并解压后,将下列文件复制到x64dbg
的根目录(即x96dbg.exe
所在目录)以及对应子目录。注意不是按照插件的惯例把全部文件都放到x32\plugins
或x64\plugins
文件夹中。
NewProcessWatcher.exe
x64_post.unicode.txt
x64_pre.unicode.txt
x86_post.unicode.txt
x86_pre.unicode.txt
x32\plugins\DbgChild.dp32
x32\CreateProcessPatch.exe
x32\DbgChildHookDLL.dll
x32\NTDLLEntryPatch.exe
x32\CPIDS\
x64\plugins\DbgChild.dp64
x64\CreateProcessPatch.exe
x64\DbgChildHookDLL.dll
x64\NTDLLEntryPatch.exe
x64\CPIDS\
工作原理
作者在本项目的readme中只是简略地一笔带过该插件的实现思路,但其中有很多实现细节确实值得学习,因此我阅读了此插件完整的源代码,并在此介绍一下相关实现。
1. CreateProcessPatch.exe
总结:CreateProcessPatch.exe
填写payload的参数并将其拷贝到目标父进程,并通过跨进程修改内存数据来Hook目标父进程的ZwCreateUserProcess
函数,使其跳转执行拷贝的payload。payload核心功能为加载DbgChildHookDLL.dll
并调用其中自定义的ZwCreateUserProcess
函数。
主体逻辑在CreateProcessPatch
函数中实现。
首先通过OpenProcess
函数打开了目标父进程的句柄hProcess
,然后通过VirtualAllocEx
函数在hProcess
对应的进程中创建了一块空间,记作payload
。
在FillPayload
函数中通过memcpy
函数填写完payload_ep
(ep的含义是entry point)中空缺部分的数据(比如payload_dll_func_name
等)。注意,payload
是目标父进程中的内存空间,payload_ep
是CreateProcessPatch.exe
中的payload.asm
汇编代码经过编译后标签init_payload
的地址。
然后通过PatchCode
函数远程修改目标父进程的内存数据。该函数在com_patch.cpp
中定义,通过VirtualProtectEx
, NtSuspendProcess
, WriteProcessMemory
, FlushInstructionCache
, NtResumeProcess
等函数,来实现修改远程进程中的数据。
不止一处调用PatchCode
哦,比如:
// 把CreateProcessPatch.exe中的payload_ep(已填充好空缺部分)拷贝到hProcess进程中的payload中
PatchCode(hProcess, payload, payload_ep, payload_size, NULL, 0, NULL, 0);
又比如(简化代码):
// 获取系统库函数ZwCreateUserProcess的地址
void* ZwCreateUserProcess_f = (void*)GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "ZwCreateUserProcess");
// 用于实现Hook时jmp跳走的汇编指令
#ifdef _WIN64
unsigned char pushret_relative_ref[] = { 0xFF, 0x35, 0x00, 0x00, 0x00, 0x00, 0xC3,
0x90, 0x90, 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90,
0x90, 0x90, 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90
};
DWORD* jmp_dest = (DWORD*)&pushret_relative_ref[2];
#else
unsigned char pushret_relative_ref[] = { 0x68, 0x00, 0x00, 0x00, 0x00, 0xC3,
0x90, 0x90, 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90,
0x90, 0x90, 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90 , 0x90
};
#endif
// 修改jmp指令跳转到的地址为payload的起始地址
#ifdef _WIN64
PatchCode(hProcess, ntdll_dos_stub, &payload, sizeof(payload), NULL, 0, NULL, 0);
*jmp_dest = (DWORD)(ntdll_dos_stub - (((unsigned char*)ZwCreateUserProcess_f) + (size_pushret - 1)));
#else
memcpy(&(pushret_relative_ref[1]), &payload, sizeof(payload));
#endif
// Hook hProcess进程中的ZwCreateUserProcess函数,从而将控制流劫持到payload的起始地址
PatchCode(hProcess, ((unsigned char*)ZwCreateUserProcess_f), pushret_relative_ref, total_bytes, code_before_patch, sizeof(code_before_patch), code_after_patch, sizeof(code_after_patch));
上面代码中,根据x64和x86的不同,用于实现Hook时跳转到payload的汇编代码也不同。
在x86中,首先0x68 dword
表示push payload_addr
,然后0xc3
表示ret
,这样完成调用payload。
在x64中,因为payload的地址是8字节,无法直接跳转。所以先通过PatchCode
将payload_addr
存放到目标父进程的ntdll_dos_stub
地址处(此处为ntdll.dll
的DOS STUB,存有This program cannot be run in MS-DOS mode.
),然后通过0xff 0x35 int
来push [rip+int]
,将payload地址入栈,最后通过0xc3
ret
到payload。
下图为x64中的跳转实现:
其中push时,具体的rip+int
的计算是这样的:
另外,此前我们多次提到payload,但还没分析它是怎么使用汇编代码实现的,又实现了什么功能,现在我们一起来看一下。
实际上payload.asm
的实现相当巧妙,绝对值得后续继续学习。payload.asm
总体上可主要分为5大部分。
① 根据不同架构,将
CAX
定义为RAX
或EAX
,方便编写最后一部分x64和x86架构都通用的汇编代码。IFDEF RAX CAX EQU RAX CBX EQU RBX ELSE CAX EQU EAX CBX EQU EBX .486 .model flat, C option casemap:none ENDIF
② 空缺参数部分。用于存储payload所需的一些参数,会在前面提到过的
FillPayload
函数中填充。ldr_get_procedure_address_lbl:: ldr_get_procedure_address db 8 dup(?) ; ldr_load_dll_sym_lbl:: ldr_load_dll_sym db 8 dup(?) ; dll_unicode_string_lbl:: dll_unicode_string db 16 dup(?) ; dll_ansi_string_lbl:: dll_ansi_string db 16 dup(?) ; dll_full_path_lbl:: dll_full_path db 520 dup (?) ; dll_func_name_lbl:: dll_func_name db 520 dup (?) ; ............
- ③ x64 PAYLOAD。实现了x64架构下的相关逻辑。
- ④ x86 PAYLOAD。功能同x64,只不过是x86架构的实现。
⑤ 通用汇编部分。主要是编写了一些对外的函数接口,如:
get_payload_size PROC PUSH CBX LEA CAX, OFFSET end_payload LEA CBX, OFFSET init_payload SUB CAX, CBX POP CBX RET get_payload_size ENDP get_payload_ep PROC LEA CAX, OFFSET init_payload RET get_payload_ep ENDP get_payload_dll_str PROC LEA CAX, OFFSET dll_full_path_lbl RET get_payload_dll_str ENDP
其中,第③部分和第④部分实现的核心逻辑是这样的:
调用
ldr_load_dll_sym
,实际该值为ntdll.LdrLoadDll
,用于加载DbgChildHookDLL.dll
。NTSYSAPI NTSTATUS NTAPI LdrLoadDll( IN PWCHAR PathToFile OPTIONAL, IN ULONG Flags OPTIONAL, IN PUNICODE_STRING ModuleFileName, OUT PHANDLE ModuleHandle);
- 调用
ldr_get_procedure_address
,实际该值为ntdll.LdrGetProcedureAddress
,用于获取DbgChildHookDLL.dll
中的ZwCreateUserProcess
函数的地址。
注意,DbgChildHookDLL.dll
中的ZwCreateUserProcess
函数,不是系统库函数,而是自己编写的、用于Hook后跳转到payload中被调用的封装函数。
NTSYSAPI
NTSTATUS
NTAPI
LdrGetProcedureAddress(
_In_ PVOID DllHandle,
_In_opt_ PANSI_STRING ProcedureName,
_In_opt_ ULONG ProcedureNumber,
_Out_ PVOID *ProcedureAddress);
同时需要注意的是,LdrGetProcedureAddress
的第二个参数,不是函数名称字符串,而是ANSI_STRING结构体的指针:
typedef struct _STRING
{
USHORT Length;
USHORT MaximumLength;
_Field_size_bytes_part_opt_(MaximumLength, Length) PCHAR Buffer;
} STRING, *PSTRING, ANSI_STRING, *PANSI_STRING, OEM_STRING, *POEM_STRING;
在payload.asm
中,我们可以注意到第二个参数rdx
的值为dll_ansi_string
,不得不说,插件开发者使用的这个名称确实有误导性:
dll_ansi_string
这个值,通过get_payload_dll_ansi_string
向外传出:
dll_ansi_string_lbl::
dll_ansi_string db 16 dup(?) ;
............
get_payload_dll_ansi_string PROC
LEA CAX, OFFSET dll_ansi_string_lbl
RET
get_payload_dll_ansi_string ENDP
在FillPayload
函数中完成此指针指向的结构体的赋值(简化代码):
#define API_NAME_A ("ZwCreateUserProcess")
// 对应字符串为API_NAME_A
void* payload_dll_func_name = get_payload_dll_func_name();
// ANSI_STRING结构体的指针,是LdrGetProcedureAddress的第二个参数
PANSI_STRING payload_dll_ansi_string = (PANSI_STRING)get_payload_dll_ansi_string();
// 通过相对偏移,计算出的结果就是远程进程中的payload中payload_dll_func_name的地址
payload_dll_ansi_string->Buffer = (PCHAR)(remote_payload + (((unsigned char*)payload_dll_func_name) - ((unsigned char*)payload_ep)));
payload_dll_ansi_string->MaximumLength = sizeof(API_NAME_A);
payload_dll_ansi_string->Length = sizeof(API_NAME_A) - 1;
将真正的
ZwCreateUserProcess
函数地址(实际上是trampoline
的地址,通过跳转指令最终把汇编代码拼接成了完整的真正的ZwCreateUserProcess
)赋值给rax
或eax
,用于配合下一步中DbgChildHookDLL.dll
中的ZwCreateUserProcess
函数在函数内部,获取真正的ZwCreateUserProcess
函数地址。; x64 lea rax, trampoline ; x86 lea eax, [ebp+trampoline]
- 调用第二步获取的函数,即
DbgChildHookDLL.dll
中的ZwCreateUserProcess
函数。
另外,必须提及的是,我们拷贝到远程进程中的payload,只是上面分析的第②③④部分,不是全部的payload.asm
中的代码。payload总体上是这样的汇编代码框架:
payload PROC
init_payload::
nop
jmp payload_ep_lbl
; 二 (空缺)参数部分
payload_ep_lbl:
IFDEF RAX
; 三 x64 PAYLOAD
ELSE
; 四 x86 PAYLOAD
ENDIF
end_payload::
payload ENDP
2. DbgChildHookDLL.dll
总结:目标父进程每次运行到ntdll.ZwCreateUserProcess
时,都会跳转执行DbgChildHookDLL.dll
中的自定义的ZwCreateUserProcess
函数,从而将新创建子进程的pid写入CPIDS
文件夹。
目标父进程运行到原生的ZwCreateUserProcess
函数时,由于被Hook了,因而跳转执行payload,其中payload内部调用的第三个函数就是DbgChildHookDLL.dll
中ZwCreateUserProcess
函数(共计11个参数会在payload入口处就入栈保存,等调用这个函数时再恢复原样),这个函数并不是ntdll.ZwCreateUserProcess
函数,而是插件开发者自定义的。
// 返回rax或eax
void GetCax(void) {
return;
}
// 自定义的ZwCreateUserProcess
DBGCHILDHOOKDLL_API NTSTATUS WINAPI ZwCreateUserProcess(
PHANDLE ProcessHandle,
PHANDLE ThreadHandle,
ACCESS_MASK ProcessDesiredAccess,
ACCESS_MASK ThreadDesiredAccess,
POBJECT_ATTRIBUTES ProcessObjectAttributes,
POBJECT_ATTRIBUTES ThreadObjectAttributes,
ULONG ProcessFlags,
ULONG ThreadFlags,
PRTL_USER_PROCESS_PARAMETERS ProcessParameters,
PVOID CreateInfo,
PVOID AttributeList)
{
#pragma comment(linker, "/EXPORT:" __FUNCTION__"=" __FUNCDNAME__)
// payload在调用此函数前,将payload中trampoline的地址赋值给了rax或eax
// trampoline可以看作真正的ZwCreateUserProcess函数的入口点
ZwCreateUserProcess_f = (ZwCreateUserProcess_t)((void* (*)(void))GetCax)();
// 查看payload的汇编代码,可以发现,dll_work_full_path就在trampoline地址+80
work_path = (WCHAR*)(((unsigned char*)ZwCreateUserProcess_f) + 80);
// 核心的自定义逻辑在此实现
return _ZwCreateUserProcess(ProcessHandle,
ThreadHandle,
ProcessDesiredAccess,
ThreadDesiredAccess,
ProcessObjectAttributes,
ThreadObjectAttributes,
ProcessFlags,
ThreadFlags,
ProcessParameters,
CreateInfo,
AttributeList);
}
进而在_ZwCreateUserProcess
函数中,通过NtQueryInformationProcess
函数(args1为FileHandle
,是新创建子进程的句柄,亦即ZwCreateUserProcess
的第一个参数(PHANDLE)指向的句柄;args2为ProcessBasicInformation
的枚举值)获取了ProcessInformation
结构体,其中的UniqueProcessId
就是新创建子进程的pid
。然后在CPIDS
文件夹中创建一个名称为此pid
的文件。
3. NewProcessWatcher.exe
总结:NewProcessWatcher.exe
监视CPIDS
文件夹中的新创建的文件,以获取刚创建的子进程的pid
,然后通过命令行调用NTDLLEntryPatch.exe
来patch目标子进程的ntdll.LdrInitializeThunk
(即ntdll的EntryPoint入口点),间接实现暂停子进程的继续运行,然后通过命令行调用 x64dbg.exe
或x32dbg.exe
来附加子进程。
main
函数中创建了两条线程,分别监视x64和x86的CPIDS
文件夹中有无新文件(文件名为新创建子进程的pid
),线程函数为NewProcessWatcher
,线程函数的参数是CPIDS
文件夹"x??\\CPIDS"
(??为64或32)的绝对路径。
NewProcessWatcher
线程函数中,在死循环里面,如果发现了新创建文件,则将(新创建文件名, CPIDS文件夹绝对路径)
作为线程函数的参数,在新线程中运行线程函数ProcesCreated
。
ProcesCreated
线程函数中,首先将参数拼接成新创建文件名的绝对路径,然后通过GetPreResumedCmd
函数,获取所谓的PreResumedCmd
命令行参数。
其中,GetPreResumedCmd
函数的核心逻辑就是读取x64_pre.unicode.txt
和x86_pre.unicode.txt
,将文件内容x64\NTDLLEntryPatch.exe 4294967295 p
和x32\NTDLLEntryPatch.exe 4294967295 p
中的4294967295
替换成新创建子进程的pid。
获取PreResumedCmd
命令行参数之后,使用CreateProcessW
运行形如x64\NTDLLEntryPatch.exe 624 p
的进程。NTDLLEntryPatch.exe
通过patch目标子进程的ntdll.LdrInitializeThunk
(即ntdll的EntryPoint入口点),间接实现暂停子进程的继续运行。
最后调用PostProcess
函数。此函数在循环中每500ms判断一次子进程的主线程是否处于暂挂suspended
状态,若否,则跳出循环。然后读取x64_post.unicode.txt
和x86_post.unicode.txt
中,同样将x64\x64dbg.exe -a 4294967295 -e 4294967295
和x32\x32dbg.exe -a 4294967295 -e 4294967295
中的4294967295
替换成新创建子进程的pid,即为所谓的PostResumedCmd
命令行参数。最后使用CreateProcessW
运行形如x64\x64dbg.exe -a 624 -e 624
的进程,从而将启动一个新的调试器,且附加到目标子进程上。
成功附加目标子进程后,需要手动unpatch ntdll.LdrInitializeThunk
,使得子进程继续运行下去。或者勾选Auto From x32dbg|x64dbg Unpatch NTDLL Entry
,当成功附加目标子进程后,插件会自动unpatch ntdll.LdrInitializeThunk
。
顺便一提的是,上面使用的x64dbg的命令行参数-a PID -e PID
,我并没有找到明确的手册解释其含义是附加调试器到进程。文档里面只提到了 -p PID
表示附加到PID
,没有写明-a
和-e
。最后我是在源码中得知其含义:
4. NTDLLEntryPatch.exe
就干了一件事,通过PatchCode
函数,将目标子进程的ntdll.LdrInitializeThunk
的前两个字节,修改成了0xEB, 0xFE
(0xeb
表示jmp
,0xfe
即-2
,这条指令长度也是2),也就是说执行完这条指令后又跳转到这条指令继续执行。除非unpatch此处,恢复原来的两个字节,否则目标子进程将“阻塞”在此处,不会继续往下运行。
关于LdrInitializeThunk
:
- 除
ntdll.dll
外的其他dll的加载和连接是通过ntdll.LdrInitializeThunk
实现的。在进入这个函数之前,目标EXE映像已经被映射到当前进程的用户空间,ntdll.dll
的映像也已经被映射,但是并没有在EXE映像与ntdll.dll
映像之间建立连接。LdrInitializeThunk
是ntdll.dll
中不经连接就可进入的函数,实质上就是ntdll.dll
的入口。 - 用户模式下的所有线程都从
LdrInitializeThunk
开始执行。进程中的第一个线程调用LdrpInitializeProcess
,其他线程都调用LdrpInitializeThread
。
工作流程
用visio简单画了个流程图,凑合看吧:
插件菜单
该插件具有以下操作:
介绍一下使用此插件时,需要用到的几个操作:
后文编号 | 功能 | 作用 |
---|---|---|
① | Hook Process Creation | 手动启动自动附加子进程 的功能 |
② | Auto from x32dbg/x64dbg Hook Process Creation | (选中后)在调试程序的时候自动启动自动附加子进程 的功能。 |
③ | Unpatch NTDLL Entry | 手动取消对目标子进程的”阻塞“ |
④ | Auto From x32dbg/x64dbg Unpatch NTDLL Entry | (选中后)在成功附加新创建的子进程后,自动取消对目标子进程的”阻塞“ |
使用说明
使用x64dbg打开或附加目标父进程后,可以设置是否选中下图中的②和④。
如果不选中②,则在启动想要跟踪的子进程之前(比如触发对应进程的CreateProcessW
断点的时候),必须手动点击①,来开启自动跟踪子进程
的功能。
如果不选中④,则在新创建的调试器附加到想要跟踪的子进程后,必须手动点击③,来取消子进程”阻塞“在ntdll.LdrInitializeThunk
,使得子进程能够继续运行下去。
(②和④是否选中,需要根据根据实际需要来决定。比如说目标父进程中启动了多个进程,而我们只关心其中的一个,那么可以不选中②,而是在CreateProcess
下断点,当断在想要继续调试的子进程时,再手动点击①。)
手动点击①或通过②自动开启功能时,会弹出下面弹窗,点击是即可,同时在调试期间不要关闭NewProcessWatcher.exe
:
完成以上配置或操作,接下来就可以正常调试父进程了,遇到子进程时会另起一个调试器并附加到子进程,当然这个时间可能有点长,大概需要几分钟才能成功附加到子进程,多耐心等待一会。(经过分析这个时间浪费在NewProcessWatcher
中的GetMainTIDFromPID
函数中,这个函数在成功获取到目标子进程的主线程之前不会跳出死循环。)
然而,有一点需要注意,前面在直接附加游戏进程的一小节中提到过,较老版本的x64dbg是支持附加断点的,也就是说附加到子进程后,不管有没有选中④,最终都会断在附件断点的(选了④,直接断在附加断点;没选④,手动点击③后会断在附加断点):
如果使用的是较新版本的x64dbg,且选中了④自动移除对子进程的”阻塞“,那么会断在异常,f9
就可以继续正常运行:
如果使用的是较新版本的x64dbg,且没选中④自动移除对子进程的”阻塞“,那么不会断下,而是一直阻塞在ntdll.LdrInitializeThunk
,所以界面中一直显示运行中,没有断下(因为程序在运行中没有断下,所以图中rip等寄存器都是不正确的):
按下F12
暂停程序,发现rip确实处于ntdll.LdrInitializeThunk
,且第一条指令为跳转到自身,该函数的执行被”阻塞“了:
手动点击插件的③Unpatch NTDLL entry
,取消阻塞,此函数第一条指令恢复为原本的指令:
接下来就可以正常调试子进程了。
自动跟踪本游戏进程的操作步骤
DbgChild
的菜单中选中了④,没有选中②:
首先打开游戏启动器并登录,然后使用x64dbg
附加到Launcher.exe
,并在CreateProcessW
下断点。
然后点击启动器中的开始游戏,第一次断在CreateProcessW
,子进程为C:\Users\iyzyi\AppData\Local\WangYuan\GameCenterLite\Agent.exe
,不关心,继续运行程序。但注意DbgChild
的菜单中一定不要选中②Auto from x64dbg Hook process creation
,不然也会另起一个调试器跟踪Agent.exe
这个进程。
第二次断在CreateProcessW
,子进程为D:\Game\GujianOL\bin64\GujianOL.exe --fs=data:_index/708.idx:_index/708.idx
。此时点击①Hook process creation
:
此时Launcher.exe
被暂停了,f9
继续运行下去:
然后等待两三分钟,就启动了一个附加到子进程的新的调试器窗口:
f9
就可以正常调试游戏进程了。
在这个过程中,NewProcessWatcher.exe
的log为:
总结
上一节我们分析了启用游戏时的进程调用链,最后得知,gujianol.exe
的启动需要继承父进程Launcher.exe
设置的一些有关登录信息的环境变量,并不能简单地通过命令行参数来启动,所以我们无法通过在x64dbg
中直接打开gujianol.exe
并修改命令行参数来实现从头开始跟踪的动态调试。
gujianol.exe
必须是通过Launcher.exe
完成登录并启动的,因此在本节中我们介绍了两种方法动调游戏进程的方法,应用场景分别为:
- DbgChild:当我们需要对游戏初始化的相关逻辑进行分析时(比如说初始化时磁盘中的资源文件是如何处理成一个索引、以便后续根据需要来具体加载对应的资源文件的),直接附加到游戏进程极大概率会错过此类代码,因此必须通过
DbgChild
插件来从头开始跟踪子进程(即游戏进程)。但是该插件的缺点也十分明显,使用这个插件,动态打开新的调试器窗口并附加子进程的这一过程相当慢,所以如无必要,还请尽可能直接附加到游戏进程。 - 直接附加:当我们需要分析的相关操作并不在游戏初始化的阶段时(比如说实时的通信流量是如何加解密的),直接附加到游戏进程就可以了,简单粗暴。
动静结合时的地址转换问题⚪
一般在做逆向分析的时候,我都是通过ida
静态分析和x64dbg
动态调试相结合。
但是同一个函数,ida
中显示的地址和x64dbg
动调时的地址大概率是不一样的,这给我们的逆向分析(比如在ida
中找到一处关键逻辑后,想要找到它在x64dbg
中的地址,进而调试分析;或者在x64dbg
中通过调试确定某个函数后,想要在ida
中查看这个函数的交叉引用)带来一些困难。
此外,由于游戏程序每周都会更新,二进制文件会相应发生改变,
本节就针对以上两种情形,给出对应的处理思路。
基址重定位与ASLR导致动态基址
基址重定位√
向进程的虚拟内存加载PE文件(包含EXE, DLL, OCX, SYS等)时,该PE文件会被加载到PE头的ImageBase
所指的地址处,如下图所示。
但是在加载过程中,如果当前要加载的PE文件的ImageBase
所指的地址处,已经在此之前加载了其他的PE文件,此时就需要通过基址重定位,更改当前要加载的PE文件加载到内存中的基址。
由于进程使用虚拟内存,且创建好进程后EXE会首先加载进内存,所以EXE本身无需考虑基址重定位问题。对于windows的系统DLL文件,微软根据不同版本分别赋予了不同的ImageBase
,例如同一系统的kernel32.dll
和user32.dll
的ImageBase
固定且不相同,因此也无需考虑基址重定位问题。所以,基址重定位一般发生在用户DLL中,如果有固定此模块基址的需要,可以手动修改ImageBase
为所在进程中其他模块没有使用的地址(同时也需要禁用ASLR)。
因为在本项目中,我们基本没有动调时固定住dll基址的需求,所以此处就不展开深入讨论了。这部分只是为了告知读者,不只有ASLR会导致动态基址,避免被我后续有关ASLR的讨论误导。
ASLR
基本介绍
ASLR (Address Space Layout Randomization) 是一种计算机安全机制,用于增加操作系统和应用程序的安全性。通过在每次运行程序时随机化内存地址布局,减少缓冲区溢出和其他内存攻击的成功率,使攻击者难以预测和利用特定的内存地址来执行恶意代码。尽管本意是好的,但不得不说也给我们逆向人员带来了一些麻烦(废话,防的就是我们这群人)。
ASLR一般分为映像随机化、堆栈随机化、PEB与TEB随机化。本文不涉及如何编写针对特定漏洞的payload,所以这里主要关注映像随机化(指PE文件加载到虚拟内存时会采用一个随机的基址),因为其中牵扯到如何定位到目标代码。
编译时启用
ASLR机制可以在链接时添加/DYNAMICBASE
选项来启用:
如果启用了ASLR机制,则会在PE文件头中设置IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE
标志,具体的位置是:
IMAGE_NT_HEADERS
IMAGE_OPTIONAL_HEADER32/64
DLL_CHARACTERISTICS
IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE
在010editor中安装了exe文件的模板后,可以方便查看PE文件的格式。
32位程序:
64位程序:
系统设置
从Windows 10 1709、Windows 11和Windows Server 1803开始支持Exploit Protection
,Exploit Protection
将许多漏洞利用缓解技术应用于操作系统进程和应用(在以上版本的windows系统之前,此类防护方案由Enhanced Mitigation Experience Toolkit
提供,不过现已废弃),可在Windows设置
-> 更新和安全
-> Windows安全中心
-> 应用和浏览器控制
中找到Exploit Protection设置
:
在这里面可以设置windows系统是否启用ASLR:
上图设置里面同时也支持对特定程序(可以选择匹配程序名或程序路径)设置专门的方案,以决定是否针对特定程序,来启用包括ASLR在内的各种安全方案。
上图设置中有关ASLR的防护选项有:
防护选项 | 含义 | 系统设置的默认值 | 涉及到的编译标志 |
---|---|---|---|
强制映像随机化(强制性ASLR) | 无视PE文件中有无/DYNAMICBASE 对应的标志,强制性使用ASLR。 | 关闭 | |
随机化内存分配(自下而上ASLR) | 常规的ASLR | 开启 | /DYNAMICBASE (默认启用此标志) |
高熵ASLR | 在常规的ASLR的基础上,利用更大的64 位地址空间来随机化映像加载的基址。此选项的生效需启用随机化内存分配(自下而上ASLR) 作为前置。 | 开启 | /HIGHENTROPYVA (默认为64位可执行映像启用此标志) |
系统设置(随机化内存分配 自下而上ASLR) | PE文件标志(IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE) | 结果 |
---|---|---|
启用 | 启用 | 启用 |
启用 | 关闭 | 关闭 |
关闭 | 启用 | |
关闭 | 关闭 | 关闭 |
基本的知识介绍完了,下面我们介绍几种可以用于动静结合来逆向程序时定位到相关地址的方案。
方案1:禁用ASLR机制来固定x64dbg中的基址
禁用ASLR机制可以使得程序在运行时不再采用动态随机的基址,所以在x64dbg中动调的时候,目标程序的基址将是固定的。而且ida
和x64dbg
中的对应地址会是完成一致的,因为二者分析的映像的基址都是PE文件头中的ImageBase
。
接下来介绍几种可以考虑的禁用ASLR机制的方法。
法① 修改PE文件头√
将PE文件头中的IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE
这1bit从1修改为0。
前面提到过在010editor中使用exe模板,就能便捷地解析PE文件格式,将下图箭头处(这个标志在PE文件中的相对位置前面也有介绍)的中的1改成0即可:
其中关于DllCharacteristics
这16bit标志位的具体含义,可参考DllCharacteristics Enum。
法② 设置中关闭系统的ASLR机制
1) win7
- 打开注册表编辑器。可以按下
Win+R
键,输入regedit
,然后按 Enter 键来打开注册表编辑器。 - 导航到以下注册表键:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management
。 - 在右侧窗格中,查找名为
MoveImages
的项。如果不存在,可以右键单击空白处,选择新建
->DWORD (32位)值
,并将其命名为MoveImages
。 - 双击
MoveImages
,将数值数据设置为0
,然后点击确定
。 - 关闭注册表编辑器。
- 重新启动计算机以使更改生效。
注册表中的操作:
以win7中默认的cmd.exe
为例。关闭ASLR前:
关闭ASLR后:
而此程序的PE文件中,ImageBase
就是上图中的4AD00000
:
2) win10
直接在前面提到过的Exploit Protection设置
中,将随机化内存分配
设置为默认关闭
,然后重启电脑。
这样一来,系统中所有的程序,如果没有单独设置程序的安全方案,都不会启用ASLR。所以说还是具有一定的风险的。
法③ 设置中关闭特定程序的ASLR机制
方案2:更改ida中的基址为x64dbg中的基址√
如果出于种种原因(比如反反调试),不想禁用ASLR机制时,也可以将ida
中映像的基址改为x64dbg
动调时映像的基址。这样两者也会保持地址的一致,便于逆向分析。
ida
中依次点击Edit
-> Segments
-> Rebase program...
:
将此处的基址,修改为映像在x64dbg
中对应的基址即可:
这个方法挺简单的,而且程序会原封不动地按照原有逻辑运行,不会有任何文件中的、内存中的、注册表中的、等等的改动,不会被目标程序中的反调试、不兼容性等原因干扰。但是每次x64dbg
动调时目标程序的基址变了,都需要手动在ida
中对应地改一下。
方案3:通过RVA换算ida和x64dbg中的地址√
VA与RVA
VA:PE文件被加载器映射到内存中后,并没有直接使用物理地址,而是使用虚拟地址(Virtual Address, VA)。操作系统的内存管理单元负责将VA映射到真实的物理地址,以实现进程对内存的访问。这样一来,每个进程有一个抽象出来的独立的地址空间。
RVA:尽管PE文件头中规定了程序加载到内存中的基址,但是由于基址重定位、ASLR等机制,每次运行程序时,映像加载到内存中的基址都可能不同,我们很难通过一个确定的VA来描述某处逻辑的具体位置,但是此处距离映像基址的偏移是固定不变的,这就是相对虚拟地址(Relative Virtual Address, RVA)。
即我们有:
$$ RVA = VA - ImageBase $$
举个例子,如果某个函数的地址VA为0x401000
,映像的基址为0x400000
,此函数地址相对于基址的偏移为0x1000
,那么我们就说这个函数的RVA
为0x1000
。
按照以上思路,我们可以通过RVA来转换同一个映像在基于不同的基址时的同一逻辑的地址。具体应用的时候有以下两种做法,其实原理是完全一样的,只是具体操作上有一点点小差异而已。
法①:x64dbg中获取RVA
在x64dbg
中,可以直接获取某个VA对应的RVA。
在想要转换的VA处(图中为0x00007FF792AC200E
),右键菜单中选择复制
-> RVA
:
此时RVA = 200E
就被拷贝到剪切板中了。
再手动加上ida
中的映像基址,就可以轻松计算出该地址在ida
中对应的地址了。
但是很可惜,原生的ida
中没有直接复制对应RVA的功能。不过检索发现好像有相关的插件,毕竟实现起来也很简单,感兴趣的话可以参考:使用IDAPython开发复制RVA的插件。
法②:ida和x64dbg地址转换脚本
对于同一个PE文件,不管是在ida
中,还是在x64dbg
中,同一逻辑所处的虚拟地址相对于模块的基址肯定是一样的。下面的python脚本就可以用于协助计算二者地址的转换。
使用时首先需要给出ida
和x64dbg
中模块的基址,然后可以使用get_x64dbg_pos
将ida
中的地址转换成x64dbg
中对应的地址,也可以使用get_ida_pos
将x64dbg
中的地址转换成相应的ida
中的地址。
# convert_address.py
ida_base = 0x140000000
x64dbg_base = 0x00007FF7E0790000
def get_x64dbg_pos(ida_pos):
print(hex(ida_pos - ida_base + x64dbg_base))
def get_ida_pos(x64dbg_pos):
print(hex(x64dbg_pos - x64dbg_base + ida_base))
get_x64dbg_pos(0x1400B0AE2)
get_ida_pos(0x0000007FF7E0840AE2)
游戏更新导致二进制文件变化√
问题描述
上一小节介绍的是同一个可执行文件在ida
中和在x64dbg
中地址的转换问题。但是在这个项目中,我们面临着一个新的问题:由于游戏每周都会更新,更新后的游戏程序会因为源代码(细微)改动、嵌入新的Resources数据、重新进行编译等等原因,导致汇编代码、函数地址、变量布局等等信息,都与上一周的游戏程序中的有所差异。
所以我们每周都要重新使用ida
分析一次本周的游戏程序,这样才能和x64dbg
动调时的本周的游戏程序对应起来。不然我们拿着上一周ida
的分析结果,使用x64dbg
对照着这一周的游戏程序进行动调,是没法完成ida
和x64dbg
中地址的一对一转化的。
其核心原因在于二者基于的不是同一个PE文件,因此同一个函数的RVA是否相同全是玄学。试以20230817~20230823
版本的游戏程序和20231012~20231018
版本的游戏程序对比为例。
20230817~20230823
版本的游戏程序中,main
函数的地址为0x14025B950
,调用memset
时的地址与memset
的地址之间的偏移为0x898C38+5
:
而20231012~20231018
版本的游戏程序中,main
函数的地址为0x14025B250
,调用memset
时的地址与memset
的地址之间的偏移为0x897E58+5
:
可以明显看出,两个不同时间段的游戏程序,是有略微差异的。因而我们不能通过上一节介绍的几种方法,将上一周的ida分析文件中某个逻辑的地址,转化成这一周x64dbg动调时的地址。
但是在逆向这种大型项目的过程中,我们很难在一周之内就能完整地将需要分析的逻辑都整理出来。而每周的ida分析文件中都包含了大量我们逆向时的相关记录,难道每周都要重新分析一遍曾经逆过的逻辑(至少也得是要将关键性的逻辑更新到新一周的ida分析文件中)吗?这听起来工作量太大了,有什么优雅的解决方案吗?
解决方案
在逆向工程领域,特征码,或称匹配特征,是一种用于在二进制代码中搜索和匹配特定模式或标识符的特征组合,通常用于分析和调试程序时定位到目标代码,有助于查找代码中的关键信息,标记特定函数、数据结构或算法,或者识别恶意软件特征。
我们以上面展示的main
函数为例,介绍如何通过特征码,从ida
分析结果中旧版本的main
函数,定位到x64dbg
中新版本的main
函数。
我们首先从ida分析结果(下图为20231012~20231018
版本)中的main
函数内部找到一段合适的特征码,比如说:
然后在x64dbg
中,首先需要在反汇编窗口中转到游戏进程的主模块。
可以在内存布局窗口,右键箭头指向的这一行,并选择在反汇编中转到
:
也可以在反汇编窗口中,按ctrl+g
,输入gujianol.base
,最后回车就可以跳转到主模块的基址处了(其中gujianol
是游戏进程的主模块的名称,如果该名称中有空格,则需要使用双引号包裹模块名称,后面再跟.base
)。:
在反汇编窗口中转到游戏进程的主模块之后,我们在反汇编窗口中,右键,依次选择搜索
-> 当前模块
-> 匹配特征
(或者直接快捷键ctrl+shift+b
):
上图中的当前区域
是指一个模块中的一个区段,比如下图中,gujianol.exe
是一个区域,.text
是另一个区域,BIND
又是另一个区域:
我们在匹配特征的时候,一般不选择所有模块
,因为匹配结果可能会超级多。
在弹出的窗口中填写我们刚才选取的特征码:
搜索后,刚好只找到一处符合匹配特征的数据:
双击过去看一下,确实是我们要找的main
函数:
而在ida
当中,同样也支持匹配特征,可以在工具栏中找到。:
也是支持用??
表示模糊匹配的。
匹配结果:
特征码选取原则
特征码不是随便选取的,不合适的特征码可能会没有匹配结果,也可能匹配出超级多的结果,影响我们的判断。从经验上来说,我认为选取特征码的时候,需要尽可能满足以下几个条件。
- 不要选取共性的汇编代码
共性是指不仅目标函数中会包含,而且其他很多函数中也会包含。比如说函数开头的那几条负责处理栈的汇编代码,基本上所有的函数都遵循大致相同的特征。
还是以上面的main
函数为例,如果我们选取的特征码是函数开头的那三条汇编指令:
那么将会搜索出好几处符合匹配特征的地址:
- 避开涉及绝对地址或相对偏移的汇编指令
没有任何一个编译器能够保证可以用相同的参数编译相同的代码两次却还能得到两份一模一样的可执行文件,更何况游戏更新时一般都会更改一些源代码、嵌入一些Resources数据。我们前面就以两个版本的main
函数为例论证过这个问题了。
因此我们在选取特征码的时候,一定要避开汇编代码中涉及到绝对地址或相对偏移的指令,比如call指令、jmp指令等等。如果无法完全避开,则需要使用??
进行模糊匹配,比如:
当然也不是绝对不能用哈。如果是函数内部的相对跳转,或许也可以选用,比如下图中的函数内部的近跳转,版本更新的时候一般也不会改变:
密码算法扫描插件√
findcrypt-yara这个IDA插件可以用于扫描一些密码算法的魔数常量,便于我们分析程序使用了哪些密码算法。
从Github下载后,将findcrypt3.py
和findcrypt3.rules
放入IDA的plugins
文件夹,然后python -m pip install yara-python==4.2.3
安装yara-python
。
使用快捷键ctrl+alt+f
即可调用此插件,或者:
需要注意的是,如果运行插件,出现下图所示的报错:
是因为yara-python的最新版本更改了yara.StringMatch
的定义,改用4.2.3版本的yara-python就可以了:
运行插件后可以观察到:
这个分析结果只能作为一个参考,并不完全准确,比如说就没有正确分析出crc16的常量表。
某类函数批量重命名√
在对游戏程序进行初步逆向分析的时候,偶然间我发现在.data
段和.rdata
区段中,出现了很多类似这样的数据:
.data
段:
.rdata
段:
稍微分析一下,就能看出实际上每两个QWORD为一组,第一个QWORD是函数名称的字符串地址,第二个QWORD是函数的地址。这样的数据结构实际上与Lua有关,我们会在后续关于Lua的章节中详细介绍,本节暂且不过多涉及。
我们可以手动将这些函数重命名为对应的字符串,便于后续分析时能够更加清晰地查看函数的交叉引用,从而提高逻辑分析的效率。但是实在是太多了,一个一个手动重命名相当繁琐,不太现实。
我写了一个idapython
脚本,可以批量将此类函数重命名。使用时需要修改.code
(即.text
), .data
, .rdata
区段的开始地址和结束地址,可在ida中按g
查看:
# rename_functions.py
# idapython scripts
code_section_begin_addr = 0x140001000
code_section_end_addr = 0x14117E000
data_section_begin_addr = 0x143D1F000
data_section_end_addr = 0x14488B000
rdata_section_begin_addr = 0x141180E88
rdata_section_end_addr = 0x143D1F000
cnt = 0
def belong_code_section(addr):
return code_section_begin_addr <= addr <= code_section_end_addr
def auto_rename_func_name(begin_pos, end_pos):
pos = begin_pos
while True:
if pos + 16 > end_pos:
break
func_name_pos = struct.unpack('Q', get_bytes(pos, 8))[0]
if not is_loaded(func_name_pos) or belong_code_section(func_name_pos):
pos += 8
continue
func_name = get_strlit_contents(func_name_pos, -1, STRTYPE_C)
if not func_name:
pos += 8
continue
func_offset = struct.unpack('Q', get_bytes(pos+8, 8))[0]
if not is_loaded(func_offset) or not belong_code_section(func_offset):
pos += 8
continue
func_name = '%s_%s' % (func_name.decode(), hex(func_offset)[2:])
print(hex(func_offset), func_name)
# https://hex-rays.com/products/ida/support/idadoc/203.shtml
success = ida_name.set_name(func_offset, func_name)
if success:
global cnt
cnt += 1
pos += 16
auto_rename_func_name(data_section_begin_addr, data_section_end_addr)
auto_rename_func_name(rdata_section_begin_addr, rdata_section_end_addr)
print('共重命名%d个函数' % cnt)
IDA Help: Alphabetical list of IDC functions (hex-rays.com)
函数 功能 文档 is_loaded(ea) 字节是否已初始化(通俗一点就是ea是否是ida中的有效地址) click get_strlit_contents(long ea, long len, long type) 获取字符串内容(返回字符串内容或空字符串) click set_name(long ea, string name, long flags=SN_CHECK) 重命名一个地址 click
脚本的原理其实很简单。分别从.data
和.rdata
区段的开始地址开始,首先判断第一个QWORD的值是否是ida中的有效地址,若是则继续判断该值是否是一个字符串的起始地址,若是则将其记作函数名称,继续判断下一个QWORD的值是否是ida中的有效地址且位于.text
区段(因为函数代码必定位于.text
区段),若是,则将此值对应的函数重命名。以上判定的任何一处为否定,则指针后移8个字节,重新进行以上算法,直到到达.data
和.rdata
区段的结束地址。
运行脚本后,可能会有弹窗:
这是因为对应的字符串含有不支持用于函数名称的字符,比如图中的这个是因为函数名称不能以!
字符开头。勾选图中的Don't display this message again
再点击OK
就不再弹窗了(当然这类函数也不会被重命名,如果需要可以修改前面的脚本,把字符串做进一步的动态更改,以满足ida中函数命名规范,我这里因为用不太到,所以就不修改了)。
脚本运行成功后输出一览:
1400多个函数,还好直接上脚本了,不然手动重命名肯定烦死,而且也很难全部发现此类函数,因为这些数据不是聚在一个地方,而是分散在.data
和.rdata
区段中。
通过脚本能够查找到对应字符串的这类sub开头的函数,都被重命名了:
.data
段:
.rdata
段:
新User多开游戏进程分析
资源文件的提取
本章主要聚焦于资源文件的提取,即通俗意义上的解包。
其实资源文件主要是指存储于磁盘当中、高达百G大小的游戏资源的打包文件(本章将之划分并称为索引文件和磁盘文件),但是由于加解密逻辑的相似性,因而我把对内嵌在exe文件中的Resources文件(本章将之称为内存文件)的提取也整合在本章当中。
另外需要说明的是,由于处理资源文件的索引关系、以及将必要的资源文件加载到内存中,是在游戏初始化的时候进行的,因此本章的大多数时候,都需要我们通过使用前面介绍过的DbgChild
插件,来从头开始附加到游戏进程上。由于本章动调的地方比较多,所以后续不会每次动调时都强调这一点了。
初步探索
在前面展示游戏文件目录的时候,我们就已经介绍过了该游戏的资源文件存储在data
文件夹中:
1) 磁盘文件:data
文件夹中存放着data000
~ data116
,每个文件约1G。
2) 索引文件:data/_index
文件夹中存放着708.idx
, 753.idx
等文件,大小在8M~9M左右,初步推测是文件索引。
因为游戏在初始化的时候,必定会加载存储在磁盘中的资源文件(至少也会处理一下基本的索引关系,以便后续在游戏运行过程中,能够通过这个索引,来加载具体的文件),所以我们可以在x64dbg
中,尝试在CreateFileA
和CreateFileW
下断点。
经过测试,主要是CreateFileW
被大量触发,偶尔触发的CreateFileA
基本上是系统DLL或第三方DLL调用的,比如:
接下来我们在CreateFileW
的断点中加上日志,以便观察操作了哪些文件:
由于CreateFileW
涉及到的不只有游戏目录下的一些文件:
也会涉及一些系统盘中的文件,比如:
我们不关心的这类文件太多了,影响分析,加上一个日志条件进行过滤,这里我们简单地用第一个字节是不是D
或d
来排除(游戏存放在D盘):
这次日志就清晰很多了,从启动游戏一直到进入游戏的开始界面为止,共有614条CreateFileW
记录,这里只介绍和处理资源文件相关的日志。
CreateFileW: d:\Game\GujianOL\bin64/gujianol.log
CreateFileW: d:\Game\GujianOL/data/data000
CreateFileW: d:\Game\GujianOL/data/data001
CreateFileW: d:\Game\GujianOL/data/data002
CreateFileW: d:\Game\GujianOL/data/data003
CreateFileW: d:\Game\GujianOL/data/data004
......(省略的dataxxx都是一条记录)......
CreateFileW: d:\Game\GujianOL/data/data253
CreateFileW: d:\Game\GujianOL/data/data254
CreateFileW: d:\Game\GujianOL/data/data255
CreateFileW: d:\Game\GujianOL/data/data000
CreateFileW: d:\Game\GujianOL/data/data000
CreateFileW: d:\Game\GujianOL/data/data001
CreateFileW: d:\Game\GujianOL/data/data001
CreateFileW: d:\Game\GujianOL/data/data002
CreateFileW: d:\Game\GujianOL/data/data002
......(省略的dataxxx都是两条记录)......
CreateFileW: d:\Game\GujianOL/data/data115
CreateFileW: d:\Game\GujianOL/data/data115
CreateFileW: d:\Game\GujianOL/data/data116
CreateFileW: d:\Game\GujianOL/data/data116
CreateFileW: d:\Game\GujianOL/data/_index/708.idx
CreateFileW: d:\Game\GujianOL\bin64/../settings/LanguageSetting.dat
CreateFileW: d:\Game\GujianOL\bin64/../VERSION
CreateFileW: D:\Game\GujianOL\bin64\NvCamera\ShotWithGeforce518x32.rgba
CreateFileW: D:\Game\GujianOL\bin64/Log_GujianOL.htm
CreateFileW: D:\Game\GujianOL\bin64\GujianOL.exe
很明显观察到,CreateFileW
函数,首先从data000
到data255
处理了一遍,然后又从data000
到data116
处理了两遍,最后处理了_index/708.idx
。
索引文件
逻辑定位
我们十分关注上述CreateFileW
函数在哪里被调用(来打开dataxxx
文件以及idx
文件的句柄),以及之后游戏进程又进行了哪些文件操作,所以可以考虑将此断点的暂停条件设置为rcx
指向的字符串等于d:\Game\GujianOL/data/_index/708.idx
。
为啥不选择d:\Game\GujianOL/data/data000
呢?这是因为dataxxx
文件总计一百多G,肯定不会全部加载到内存中,而idx
文件根据名称我们初步判断是索引文件,而且只有9M大小,应该会在游戏初始化的时候,加载到内存中。(然而实际上其实我都有尝试过,写博客时觉得先写索引文件的逆向分析过程比较好,逻辑上一脉相承)
另外注意上面的这个字符串中,data
前面的斜杠是/
而非\
,且开头的盘符是d
而非D
,这是我们通过上一步的日志发现的。如果我们按照习惯,设置的条件一般是D:\Game\GujianOL\data\_index\708.idx
,是断不下来的。
接下来按照前面介绍过的老版本中字符串断点的实现,我们为CreateFileW
的断点设置下面的条件:
d:\Game\GujianOL/data/_index/708.idx
0x0 0x47005c003a0064 d:\G
0x8 0x5c0065006d0061 ame\
0x10 0x69006a00750047 Guji
0x18 0x4c004f006e0061 anOL
0x20 0x7400610064002f /dat
0x28 0x69005f002f0061 a/_i
0x30 0x7800650064006e ndex
0x38 0x3800300037002f /708
0x40 0x7800640069002e .idx
从而断在:
此时堆栈为下图所示:
其中的第一个数据,亦即CreateFileW
的返回地址,为0x00007FF7D5153E30
:
此地址所位于的函数,对应ida
中的Func_CreateFileHandle_1400A3D00
:
(注意,逆向过程中,我们不可能在一开始就能分析出每个函数的功能,并重命名为一个清晰的名称。但是为了便于记录和理解,本文中涉及到的函数,如果是我曾分析并重命名的函数,均直接给出由我重命名后的函数名称。不然文中出现一堆sub开头的函数名,肯定会特别混乱)
x64dbg
中动调跳出这个函数(ctrl+f9
直达函数ret
,f8
单步步过)后,会立马来到:
此处对应ida
中的Func_LoadFileHandle_1400A3BE0
:
继续跳出当前函数,来到:
此处对应ida
中的Func_GetFileData_1400A9E90
:
接下来我们就分析一下这三个函数,以及其内部调用的其他关键函数。
逆向分析
1 Func_CreateFileHandle_1400A3D00
总结
根据iType
(args2
) 决定CreateFileW
的相关标志,并通过CreateFileW
打开pwszFilePath
(args1
) 文件的句柄,最后返回Stru_FileHandle
结构体(该结构体中包含文件句柄、与文件操作相关的函数数组这两个成员变量)。
伪代码
这里附上经过我分析后的伪代码,以便后续分析时可以对照着它来理解具体的实现细节。函数内部的关键函数和变量均已重命名,同时对于涉及到的结构体,已经尽可能地分析出了具体的结构。本章后续的分析我都会按照这个模式来布局。
Stru_FileHandle *__fastcall Func_CreateFileHandle_1400A3D00(WCHAR *pwszFilePath, int iType)
{
DWORD dwFlagsAndAttributes; // ebp
WCHAR *pwszFilePath_1; // rdi
DWORD dwCreationDisposition; // esi
DWORD dwDesiredAccess; // r14d
int v7; // ebx
int v8; // ebx
int v9; // ebx
DWORD FileAttributesW; // eax
HANDLE hFile; // rbx
Stru_FileHandle *pSFileHandle; // rax
WCHAR wszTempPath[2048]; // [rsp+40h] [rbp-2028h] BYREF
WCHAR wszTempFileName[2048]; // [rsp+1040h] [rbp-1028h] BYREF
dwFlagsAndAttributes = 0;
pwszFilePath_1 = pwszFilePath;
dwCreationDisposition = 3; // OPEN_EXISTING
dwDesiredAccess = 0xC0000000; // READ and WRITE
if ( !pwszFilePath ) // 如果参数pwszFilePath为NULL,则创建一个Temp文件
// 此时需iType = 3
{
if ( iType != 3
|| GetTempPathW(0x800u, wszTempPath) - 1 > 0x7FF
|| !GetTempFileNameW(wszTempPath, L"_tmp_", 0, wszTempFileName) )
{
return 0i64;
}
pwszFilePath_1 = wszTempFileName;
}
if ( !iType )
{
dwDesiredAccess = 0x80000000; // iType = 0 READ
// https://learn.microsoft.com/en-us/windows/win32/secauthz/access-mask-format
if ( (unsigned int)off_143E093C8(pwszFilePath_1) )
goto LABEL_18;
return 0i64;
}
v7 = iType - 1;
if ( !v7 )
{
dwCreationDisposition = 4; // iType = 1
// OPEN_ALWAYS
if ( (unsigned int)off_143E093C8(pwszFilePath_1) )
{
sub_1400A3CC0(pwszFilePath_1);
goto LABEL_18;
}
return 0i64;
}
v8 = v7 - 1;
if ( v8 )
{
v9 = v8 - 1;
if ( !v9 )
{
dwCreationDisposition = 2; // iType = 4
// CREATE_ALWAYS
dwFlagsAndAttributes = 0x4000002;
goto LABEL_18;
}
if ( v9 != 1 )
goto LABEL_18; // iType = 4
}
dwCreationDisposition = 2; // iType = 2
FileAttributesW = GetFileAttributesW(pwszFilePath_1);
if ( FileAttributesW != -1 && (FileAttributesW & 1) != 0 )
SetFileAttributesW(pwszFilePath_1, FileAttributesW & 0xFFFFFFFE);
LABEL_18:
hFile = CreateFileW(pwszFilePath_1, dwDesiredAccess, 3u, 0i64, dwCreationDisposition, dwFlagsAndAttributes, 0i64);//
// 1-动调CreateFileW 708.idx
// 1-动调CreateFileW 第二轮data000
if ( hFile == (HANDLE)-1i64 )
return 0i64;
pSFileHandle = (Stru_FileHandle *)g_pSMemoryFuncs->Func_Wrapper_malloc_1400A6410(g_pSMemoryFuncs->qwZero, 16i64);// malloc(16)
if ( !pSFileHandle )
{
CloseHandle(hFile);
return 0i64;
}
pSFileHandle->hFile = hFile;
pSFileHandle->pFuncs = &Stru_FileFuncs1_143E09410;
return pSFileHandle;
}
Part1
根据参数iType
来设置CreateFileW
的dwDesiredAccess
, dwCreationDisposition
和 dwFlagsAndAttributes
这三个标志,然后调用CreateFileW
来打开pwszFilePath
的句柄。
我们其实目前并不关心这些标志分别代表什么含义,无非是诸如文件不存在时要不要创建新的文件等等,所以暂且略过。
Part2
通过封装的malloc
来创建一个16字节的空间。
上面的伪代码截图是经过我分析过后的,而ida
中的原版其实是这样的,分析的时候,头一下子就大了起来:
其实主要的难点在于off_143E093A0
。经过观察,指针关系为off_143E093A0
-> off_143E094A0
-> sub_1400A6410
。
因此*off_143E093A0()
即off_143E094A0()
,而off_143E094A0()
其实就是call sub_1400A6410
(如果感觉反直觉的话,可以仔细想想pFunc()
调用的pFunc
的地址,还是pFunc
的值)。
off_143E093A0[3]
即*((*off_143E093A0) + 0x18)
即*(0x143E094A0 + 0x18)
即*(0x143E094B8)
,也就是0
。
如果还是难以理解,可以借助此处的汇编代码来理解:
顺便说一下,从off_143E094A0
开始的32个字节,存储的是3个函数和一个0,三个函数分别封装了malloc
, free
和free + realloc
,都通过mov rcx, rdx
将传入的第一个参数丢弃掉(可见上图中的汇编代码),我们分别将之重命名为Func_Wrapper_malloc_1400A6410
, Func_Wrapper_free_1400A6420
和Func_Wrapper_realloc_1400A6430
。
关系捋清了之后,为了伪代码中能够清晰地反映出调用了哪个函数,我们可以创建如下结构体Stru_MemoryFuncs
:
然后将off_143E094A0
开始的32字节转换为此结构体,并重命名为Stru_MemoryFuncs_143E094A0
:
然后将off_143E093A0
重命名为g_pSMemoryFuncs
:
ida
可能会进行自动类型推断,根据Stru_MemoryFuncs_143E094A0
是Stru_MemoryFuncs
结构体,从而将g_pSMemoryFuncs
自动判断为Stru_MemoryFuncs *
。但是也有可能没有,这就需要我们光标选中g_pSMemoryFuncs
后按y
修改变量的声明:
由于这是我们手动声明的,ida
会默认这就是绝对正确的,不会再对它进行自动类型推断,因而主窗口中会显示一行注释,表明此处的变量类型:
我们再回到伪代码中:
可以发现这里已经变得清晰多了。如果想要伪代码更简洁一点,可以参考前面结构体一节中的函数指针来优化成:
Part3
在刚创建的16字节空间中,前8字节为一个全局的地址0x143E09410
(是另一个存储着一些函数地址的数组),后8字节存储文件的句柄:
因此我们定义Stru_FileHandle
的结构为:
其中,Stru_FileFuncs1_143E09410
处的函数有:
可以依次分析并重命名。
Func_Wrapper_CloseHandle_1400A3450
关闭文件句柄:
Func_Wrapper_FlushFileBuffers_1400A3490
刷新文件缓冲区:
Func_Wrapper_ReadFile_1400A34B0
读取文件:
Func_Wrapper_WriteFile_1400A34E0
写入文件:
Func_Wrapper_SetFilePointerEx_1400A3510
设置文件指针:
其中的LARGE_INTEGER
其实是一个union:
typedef __int64 LONGLONG;
typedef union _LARGE_INTEGER {
struct {
ULONG LowPart;
LONG HighPart;
};
struct {
ULONG LowPart;
LONG HighPart;
} u;
LONGLONG QuadPart;
} LARGE_INTEGER;
Func_GetFileSizeAndTime_1400A3570
获取文件大小和时间:
经过分析,我们可以创建一个结构体Stru_FileFuncs1
方便后续伪代码中的显示:
同时需要将Stru_FileHandle
中的pFuncs
的数据类型从默认的__int64
更改为Stru_FileFuncs1 *
,这样才能在伪代码中显示出这样的效果:
注意,此时Stru_FileFuncs1
结构体内的成员变量,看起来像函数,但实际上的类型只是__int64
。同时因为ida
自动推断函数参数信息的失误,因此可能会有下面这种显示:
而我们经过前面的分析,可以知道Func_Wrapper_free_1400A6420
其实是有两个参数的,上图却只显示了一个参数。
我们可以选择在红框中的任意位置右键,并选择Set call type...
:
设置正确的函数原型:
于是就有了正确的显示:
我们也可以先修改结构体中Func_Wrapper_free_1400A6420
这个成员变量的类型为:
然后在同样的右键菜单中,选择Force call type
,这样就能够在此处强制性采用g_pSMemoryFuncs->Func_Wrapper_free_1400A6420
的函数类型了,能够实现同样的效果。
但是,如果没有把g_pSMemoryFuncs->Func_Wrapper_free_1400A6420
的数据类型手动修改为对应的函数原型,还是默认的__int64
的话,那么同样的右键菜单中是不会出现Force call type
这个子选项的。
2 Func_LoadFileHandle_1400A3BE0
总结
套壳Func_CreateFileHandle_1400A3D00
。
伪代码
分析
这个函数结构上相对简单。
首先通过Func_Ansi2Utf16_1400A3010
将szFilePath
转换为pwszFilePath
。具体转换逻辑我分析了一些,但和当前的主线任务关系不大,这里就不介绍了。
然后调用Func_CreateFileHandle_1400A3D00
获取pSFileHandle
并返回。
3 Func_GetFileData_1400A9E90
总结
- 传入索引文件的路径
- 打开索引文件句柄
Stru_FileHandle
- 读取索引文件前32字节,解析头部,完成crc16校验,设置解压算法
- 获取文件大小和文件时间
- 读取索引文件数据并解压
- 解析索引文件的具体数据,实现相关初始化操作。
伪代码
__int64 __fastcall Func_GetFileData_1400A9E90(
__int64 a1,
const char *szDataDirPath,
char *szFileRelativePath,
void (__fastcall *pfnDecompressFuncOrNull)(__int64, _QWORD, __int64 (__fastcall **)()))
{
int v4; // r15d
char *v6; // rbx
const char *v7; // rdi
unsigned int v9; // ebp
__int64 v11; // rax
__int64 v12; // rcx
char *v13; // rax
CHAR *szFilePath; // rsi
Stru_FileHandle *pSFileHandle; // rbx
Stru_PackFileInfoWithDecOrWithNoDec *pSPackFileInfoWithDecOrWithNoDec_1; // rax
Stru_PackFileInfoWithDecOrWithNoDec *pSPackFileInfoWithDecOrWithNoDec; // rbx
__int64 pFuncs; // rax
__int64 dwPlainLen; // rsi
void *pbFilePlain; // rdi
Stru_PackFileInfoWithDecOrWithNoDec *v21; // rdi
__int64 v22; // rax
size_t dwPlainLen_1; // [rsp+50h] [rbp+8h] BYREF
v4 = 0;
v6 = szFileRelativePath;
v7 = szDataDirPath;
v9 = 1;
if ( !a1 || *(__int64 (__fastcall ***)())(a1 + 64) != &off_143E095A0 || !szFileRelativePath )
return 0i64;
if ( *szFileRelativePath == '*' )
{
v6 = szFileRelativePath + 1;
v4 = 1;
}
if ( szDataDirPath )
{
v11 = -1i64;
v12 = -1i64;
do
++v12;
while ( szDataDirPath[v12] );
do
++v11;
while ( v6[v11] );
v13 = (char *)malloc(v11 + 8 + v12);
szFilePath = v13;
if ( !v13 )
return 0i64;
if ( *v7 == '*' )
++v7;
sprintf(v13, "%s/%s", v7, v6); // 拼接出完整的文件路径
pSFileHandle = Func_LoadFileHandle_1400A3BE0(szFilePath, 0);// 3-动调CreateFileW 708.idx
free(szFilePath);
}
else
{
pSFileHandle = Func_LoadFileHandle_1400A3BE0(v6, 0);
}
if ( !pSFileHandle )
return 0i64;
pSPackFileInfoWithDecOrWithNoDec_1 = Func_ParsePackFileHeaderAndSetDecompressFunc_1400A8000(
pSFileHandle,
0i64,
0i64,
pfnDecompressFuncOrNull);
pSPackFileInfoWithDecOrWithNoDec = pSPackFileInfoWithDecOrWithNoDec_1;
if ( !pSPackFileInfoWithDecOrWithNoDec_1 )
return 0i64;
if ( v4 )
{
pFuncs = pSPackFileInfoWithDecOrWithNoDec_1->pFuncs;//
// g_pFuncs_PackFileWithNoDec_143E09620 or
// g_pFuncs_PackFileWithDec_143E09558
dwPlainLen_1 = 0i64;
if ( (*(unsigned int (__fastcall **)(Stru_PackFileInfoWithDecOrWithNoDec *, size_t *, _QWORD))(pFuncs + 40))(
pSPackFileInfoWithDecOrWithNoDec,
&dwPlainLen_1,
0i64) ) // Func_GetFilePlainLenAndTimeWithNoDec_1400BEF00 or
// Func_GetFilePlainLenAndTimeWithDec_1400A7CB0
{
dwPlainLen = dwPlainLen_1;
pbFilePlain = malloc(dwPlainLen_1);
if ( !pbFilePlain
|| (*(__int64 (__fastcall **)(Stru_PackFileInfoWithDecOrWithNoDec *, void *, __int64))(pSPackFileInfoWithDecOrWithNoDec->pFuncs
+ 16))(//
// *(_QWORD *)pSPackFileInfoWithDecOrWithNoDec即为pFuncs
// pFuncs + 16为
// Func_ReadFileDataWithNoDec_1400BEE30 or
// Func_ReadFileDataWithDec_1400A7B00
pSPackFileInfoWithDecOrWithNoDec,
pbFilePlain,
dwPlainLen) != dwPlainLen
|| !(unsigned int)sub_1400AADD0(a1, (__int64)pbFilePlain, dwPlainLen) )
{
v9 = 0;
}
free(pbFilePlain);
}
}
else
{
v21 = (Stru_PackFileInfoWithDecOrWithNoDec *)sub_1400A6F80((__int64)pSPackFileInfoWithDecOrWithNoDec_1, 0x40000u);
v22 = pSPackFileInfoWithDecOrWithNoDec->pFuncs;
if ( v21 )
{
(*(void (__fastcall **)(Stru_PackFileInfoWithDecOrWithNoDec *))v22)(pSPackFileInfoWithDecOrWithNoDec);
pSPackFileInfoWithDecOrWithNoDec = v21;
}
else
{
(*(void (__fastcall **)(Stru_PackFileInfoWithDecOrWithNoDec *, _QWORD, _QWORD))(v22 + 32))(
pSPackFileInfoWithDecOrWithNoDec,
0i64,
0i64);
}
sub_1400A9860(a1, pSPackFileInfoWithDecOrWithNoDec, 0i64);
}
if ( pSPackFileInfoWithDecOrWithNoDec )
(*(void (__fastcall **)(Stru_PackFileInfoWithDecOrWithNoDec *))pSPackFileInfoWithDecOrWithNoDec->pFuncs)(pSPackFileInfoWithDecOrWithNoDec);
return v9;
}
Part1
分了两种情况,但最终都要调用Func_LoadFileHandle_1400A3BE0
加载文件句柄并返回Stru_FileHandle
的指针。
Part2
将上一步获取的pSFileHandle
传入Func_ParsePackFileHeaderAndSetDecompressFunc_1400A8000
,解析文件32字节的头部并设置了解压算法。
需要注意的是,根据实际情况,该函数返回的指针指向的数据的类型实际上不唯一,有可能是两种结构体,Stru_PackFileInfoWithNoDec
(对应无需解压的数据)和Stru_PackFileInfoWithDec
(对应需要解压的数据),分别由该函数内部调用的Func_SetPackFileInfoWithNoDec_1400BEF30
和Func_SetPackFileInfoWithDec_1400A7CD0
返回。尽管二者大部分成员变量都不同,但是+0
处的成员变量都是pFuncs
,我根据二者共有的成员变量,定义了共用的Stru_PackFileInfoWithDecOrWithNoDec
,以便在伪代码中显示出通用的pFuncs
字段。
Part3
Func_ParsePackFileHeaderAndSetDecompressFunc_1400A8000
的不同结构体指针类型的返回值、对应的+0
成员变量pFuncs
(一个函数数组)、以及后续伪代码中涉及到的pFuncs
中的两个函数可见下表:
上图伪代码中的pFuncs + 40
,根据返回值指向的结构体类型的不同,将分别调用Func_GetFilePlainLenAndTimeWithNoDec_1400BEF00或Func_GetFilePlainLenAndTimeWithDec_1400A7CB0。这两个函数均用于获取文件的明文长度和文件的时间。
Part4
伪代码中的pFuncs + 16
,根据返回值指向的结构体类型的不同,将分别调用Func_ReadFileDataWithNoDec_1400BEE30和Func_ReadFileDataWithDec_1400A7B00。这两个函数均用于获取文件明文数据,前者直接读取文件数据并返回,后者需要读取并解压所有chunk再返回。
Part5
解压出索引文件的明文数据后,通过这个函数做最后的处理:
我推测应该就是将索引文件的信息处理成一个关于索引的数据结构,用于检索磁盘文件。
因为和解包的主线任务关系不大,所以这个函数就不分析了。
3-1 Func_ParsePackFileHeaderAndSetDecompressFunc_1400A8000
总结
本章最最最关键的函数,应该没有之一!
- 读取
pSFileHandle
(args1
) 当前文件指针处的前32字节,解析并进行crc16校验。 - 根据上一步解析结果中的
wType
,设置不同的解压函数或NULL。 - 根据是否设置解压函数,调用相应函数将目标文件的相关信息整合到Stru_PackFileInfoWithNoDec或Stru_PackFileInfoWithDec中。
伪代码
Stru_PackFileInfoWithDecOrWithNoDec *__fastcall Func_ParsePackFileHeaderAndSetDecompressFunc_1400A8000(
Stru_FileHandle *pSFileHandle,
__int64 liDistanceToMove,
__int64 dwCipherLen,
void (__fastcall *pfnDecompressFuncOrNull)(__int64, _QWORD, __int64 (__fastcall **)()))
{
__int64 wType; // rcx
unsigned int (__fastcall *pfnDecompressFunc)(Stru_DecompressContext *, Stru_DecompressContext *); // rcx
Stru_PackFileInfoWithDec *v10; // rdi
unsigned int (__fastcall *pfnDecompressFunc_1)(Stru_DecompressContext *, Stru_DecompressContext *); // [rsp+30h] [rbp-68h] BYREF
Stru_FileStorageHeaderWhenPreprocess pSHeader; // [rsp+38h] [rbp-60h] BYREF
char pbFileHeader32B[32]; // [rsp+58h] [rbp-40h] BYREF
pfnDecompressFunc_1 = 0i64;
if ( !pSFileHandle )
return 0i64; // 注意,pSFileHandle结构体的pFuncs,
// 我默认指向了Stru_FileFuncs1_143E09410,
// 但不是百分百是这样的,
// 也可能是Stru_MemFileFuncs_143E09528 等
if ( ((__int64 (__fastcall *)(Stru_FileHandle *, __int64, _QWORD))pSFileHandle->pFuncs->Func_Wrapper_SetFilePointerEx_1400A3510)(// 此函数也可能是sub_1400A6C00
pSFileHandle,
liDistanceToMove,
0i64) != liDistanceToMove
|| pSFileHandle->pFuncs->Func_Wrapper_ReadFile_1400A34B0(pSFileHandle, pbFileHeader32B, 32i64) != 32// 此函数也可能是sub_1400A6AA0
// 读取PackFile开头32字节
// 动调708.idx, data111都可能走这
|| !(unsigned int)Func_Crc16AndParseFileStorage_1400A7E90((__int64)pbFileHeader32B, &pSHeader)
|| dwCipherLen && dwCipherLen != pSHeader.qwCipherLen )
{
pSFileHandle->pFuncs->Func_Wrapper_CloseHandle_1400A3450(pSFileHandle);
return 0i64;
}
wType = (unsigned __int16)pSHeader.wStorageType;
if ( pSHeader.wStorageType )
{
if ( pSHeader.wStorageType == 1 || pSHeader.wStorageType == 2 )
{
pfnDecompressFunc = Func_DecompressType1or2_1400A7270;
pfnDecompressFunc_1 = Func_DecompressType1or2_1400A7270;
goto LABEL_14;
}
if ( pSHeader.wStorageType == 3 )
{
pfnDecompressFunc = Func_DecompressType3_1400A7410;
pfnDecompressFunc_1 = Func_DecompressType3_1400A7410;
goto LABEL_14;
}
if ( pfnDecompressFuncOrNull ) // 动调查看rbp即pfnDecompressFuncOrNull
// 为Func_SetOodleCompressAndDecompressFunc_140227F90
// wStorageType = 4, 5, 6, 7时设置Oodle解压函数
// wStroageType >= 8时解压函数为NULL
{
LOWORD(wType) = pSHeader.wStorageType - 4;
pfnDecompressFuncOrNull(wType, 0i64, (__int64 (__fastcall **)())&pfnDecompressFunc_1);
}
}
pfnDecompressFunc = pfnDecompressFunc_1;
LABEL_14:
if ( !pfnDecompressFunc ) // 没有设置解压函数时执行此段代码
return (Stru_PackFileInfoWithDecOrWithNoDec *)Func_SetPackFileInfoWithNoDec_1400BEF30(
pSFileHandle,
pSHeader.qwTime,
liDistanceToMove + 32,
pSHeader.qwPlainLen,
1);
v10 = Func_SetPackFileInfoWithDec_1400A7CD0( // 设置了解压函数时执行此段代码
pSFileHandle,
pSHeader.qwCipherLen,
pSHeader.qwTime,
pSHeader.qwPlainLen,
pfnDecompressFunc,
pSHeader.dwChunkPlainLen);
if ( !v10 )
{
pSFileHandle->pFuncs->Func_Wrapper_CloseHandle_1400A3450(pSFileHandle);
return 0i64;
}
return (Stru_PackFileInfoWithDecOrWithNoDec *)v10;
}
Part1
设置文件的指针,读取文件中从liDistanceToMove
处开始的32字节,然后通过Func_Crc16AndParseFileStorage_1400A7E90
解析这32字节的数据并进行crc16校验。
在Func_CreateFileHandle_1400A3D00
中,我们定义了Stru_FileHandle
结构体,但是当时还不知道其中的pFuncs
成员的用处。这里就展示了调用pFuncs
中的Func_Wrapper_SetFilePointerEx_1400A3510
和Func_Wrapper_ReadFile_1400A34B0
的代码。
伪代码中的注释中说的Stru_FileHandle
的pFuncs
不一定指向我们前面分析的Stru_FileFuncs1_143E09410
,我们留待后续讨论。
Part2
根据上一步从文件的前32字节解析而来的pSHeader
中的wType
,选择不同的解压函数。
wType | 解压函数 |
---|---|
1~2 | Func_DecompressType1or2_1400A7270 |
3 | Func_DecompressType3_1400A7410 |
4~7 | Func_SetOodleCompressAndDecompressFunc_140227F90里面设置具体的解压函数 |
其他 | NULL |
前两个解压函数很容易分析出来,不多赘述。
wType
不为1, 2, 3时,都将执行Func_SetOodleCompressAndDecompressFunc_140227F90
,但是在该函数内部,只有wType
为4~7时会设置相应的解压函数。因而其他情况的wType
对应的pfnDecompressFunc
均为NULL。
但是问题是,Func_SetOodleCompressAndDecompressFunc_140227F90
并没有出现在上图代码中,是怎么被我们发现的呢?
注意看,上图第54行,调用了当前所处函数的第四个参数pfnDecompressFuncOrNull
,这个函数我们有两种方法来确定。
动调
动调到当前函数Func_ParsePackFileHeaderAndSetDecompressFunc_1400A8000
:
查看第四个参数r9
为0x00007FF604337F60
:
即对应ida
中的Func_SetOodleCompressAndDecompressFunc_140227F90
。
静态
查看当前函数Func_ParsePackFileHeaderAndSetDecompressFunc_1400A8000
的交叉引用:
这里的pfnDecompressFuncOrNull
同样是Func_GetFileData_1400A9E90
的第四个参数,因此继续查看Func_GetFileData_1400A9E90
的交叉引用:
调用Func_GetFileData_1400A9E90
的函数不多,挨个看一下。其中第三个函数,就有:
由此可确定pfnDecompressOrNull
是(至少在某种情况下会是)Func_SetOodleCompressAndDecompressFunc_140227F90
。
Part3
类型 | 调用 | 返回 |
---|---|---|
未设置解压函数 | Func_SetPackFileInfoWithNoDec_1400BEF30 | Stru_PackFileInfoWithNoDec 指针 |
设置了解压函数 | Func_SetPackFileInfoWithDec_1400A7CD0 | Stru_PackFileInfoWithDec 指针 |
由于此处返回的结构体可能有两种形态,因而我找出二者的共同含义的字段,定义了通用性的Stru_PackFileInfoWithDecOrWithNoDec
:
3-1-1 Func_Crc16AndParseFileStorage_1400A7E90
总结
- 将传入的文件的前32个字节解析为
Stru_FileStorageHeaderWhenPreprocess
结构。 - 计算前30个字节的crc16,并与第31~32字节处的crc16校验和进行比对。
- 若
args2
不为NULL,则解析出的结构体通过args2
这个指针传出。
伪代码
__int64 __fastcall Func_Crc16AndParseFileStorage_1400A7E90(
__int64 pbFileHeader32B,
Stru_FileStorageHeaderWhenPreprocess *pSHeader_1)
{
Stru_FileStorageHeaderWhenPreprocess *pSHeader; // rax
unsigned __int8 *pbInputAdd1; // r11
unsigned __int16 wCalcCheckNum; // r10
__int64 i; // rbx
__int16 wCheckNum; // di
unsigned __int8 v7; // dl
unsigned __int16 v8; // r8
__int64 v9; // rcx
unsigned __int16 v10; // r8
unsigned __int16 v11; // r8
int dwChunkPlainLen; // ecx
__int64 result; // rax
char v14; // [rsp+0h] [rbp-48h] BYREF
pSHeader = (Stru_FileStorageHeaderWhenPreprocess *)&v14;
if ( pSHeader_1 )
pSHeader = pSHeader_1;
pSHeader->qwPlainLen = *(_QWORD *)pbFileHeader32B;
pbInputAdd1 = (unsigned __int8 *)(pbFileHeader32B + 1);
pSHeader->qwTime = *(_QWORD *)(pbFileHeader32B + 8);
wCalcCheckNum = 0;
pSHeader->qwCipherLen = *(_QWORD *)(pbFileHeader32B + 16);
i = 5i64;
pSHeader->dwChunkPlainLen = *(_DWORD *)(pbFileHeader32B + 24);
pSHeader->wStorageType = *(_WORD *)(pbFileHeader32B + 28);
wCheckNum = *(_WORD *)(pbFileHeader32B + 30);
do
{
v7 = wCalcCheckNum ^ *(pbInputAdd1 - 1);
pbInputAdd1 += 6; // 每次循环迭代6B,共循环5次,因而crc16共计算30B
v8 = HIBYTE(wCalcCheckNum) ^ pwCrc16Table_143B0BD30[v7];
v9 = (unsigned __int8)(HIBYTE(wCalcCheckNum) ^ LOBYTE(pwCrc16Table_143B0BD30[v7]) ^ *(pbInputAdd1 - 6));
v10 = ((unsigned __int16)(HIBYTE(v8) ^ pwCrc16Table_143B0BD30[v9]) >> 8) ^ pwCrc16Table_143B0BD30[(unsigned __int8)(HIBYTE(v8) ^ LOBYTE(pwCrc16Table_143B0BD30[v9]) ^ *(pbInputAdd1 - 5))];
v11 = ((unsigned __int16)(HIBYTE(v10) ^ pwCrc16Table_143B0BD30[(unsigned __int8)(v10 ^ *(pbInputAdd1 - 4))]) >> 8) ^ pwCrc16Table_143B0BD30[(unsigned __int8)(HIBYTE(v10) ^ LOBYTE(pwCrc16Table_143B0BD30[(unsigned __int8)(v10 ^ *(pbInputAdd1 - 4))]) ^ *(pbInputAdd1 - 3))];
wCalcCheckNum = HIBYTE(v11) ^ pwCrc16Table_143B0BD30[(unsigned __int8)(v11 ^ *(pbInputAdd1 - 2))];
--i;
}
while ( i );
if ( wCheckNum != wCalcCheckNum ) // 比较计算出的和存储的crc16校验和是否相等
return 0i64;
if ( pSHeader->qwCipherLen < 32 )
return 0i64;
if ( pSHeader->qwPlainLen < 0 )
return 0i64;
dwChunkPlainLen = pSHeader->dwChunkPlainLen;
result = 1i64;
if ( (unsigned int)(dwChunkPlainLen - 128) > 0x1FFF80 )
return 0i64;
return result;
}
Part1
如果此函数传入的参数2pSHeader_1
不为空,则pSHeader
指向参数2指向的空间,否则指向当前函数栈内的某处空间,此空间用于存储将文件前32个字节的数据转换成的Stru_FileStorageHeaderWhenPreprocess
结构体,具体结构如下:
Part2
这里以6个字节为一组,进行crc16的迭代计算,共计5轮循环,因此是将文件的前30个字节进行计算。然后判断计算结果是否与文件的第31~32字节处存储的crc16的校验和相等,如果不等则校验失败。
其中pwCrc16Table_143B0BD30
为:
一直不知道这里是啥算法,直到搜索了一下这个数组里面的第二个WORD
(因为第一个WORD
是0),发现是crc16:
具体有关crc16算法的相关内容,可以跳转CRC16一节查阅。
Part3
一些其他的校验,比如:
3-1-2 Func_DecompressType1or2_1400A7270
Func_DecompressType1or2_1400A7270
的两个参数,都可定义为如下的结构体Stru_DecompressContext
:
然后封装调用了sub_1400BC8E0
:
这个函数很难分析,因为涉及到具体的压缩算法,如果不能通过特征识别出具体的算法名称,靠自己肉眼逆是很难逆出来的。
这里先放张截图给大家留个印象,后面写解包代码的时候我们在讨论如何处理这个函数。
3-1-3 Func_DecompressType3_1400A7410
总结
这个函数不出意外应该也是一个解压算法,参数也和Func_DecompressType1or2_1400A7270
的参数完全一样,但是本项目中似乎没用到这个解压函数,因此就不分析了。放在这里只是为了行文逻辑的完整性。
伪代码
3-1-4 Func_SetOodleCompressAndDecompressFunc_140227F90
总结
根据wType
(args1
) 设置不同的OodleLZ
变体算法,通过指针分别将压缩算法和解压缩算法的函数地址传给args2
和args3
。
伪代码
参数1的wType
其实是Func_ParsePackFileHeaderAndSetDecompressFunc_1400A8000
中的wType - 4
:
然后根据wType
为0, 1, 2, 3时(所以实际对应Func_ParsePackFileHeaderAndSetDecompressFunc_1400A8000
中的wType
为4, 5, 6, 7),分别设置对应的压缩函数和解压缩函数(前提分别是参数2, 参数3不为NULL)。
解压函数
4种wType
下的解压函数都是一样的,为Func_OodleDecompress_14069BEF0
:
首先,这个函数的参数,和前面分析过的两个解压函数(Func_DecompressType1or2_1400A7270
和Func_DecompressType3_1400A7410
)的参数完全一致,保证了解压接口的一致性:
其次,这里的OodleLZ_Decompress
是游戏根目录下bin64/oo2core_6_win64.dll
中的导出函数。可通过UnrealEngine源码查阅详细的参数说明,点此跳转Github仓库中的具体位置:
// Decompress returns raw (decompressed) len received
// Decompress returns 0 (OODLELZ_FAILED) if it detects corruption
OOFUNC1 OO_SINTa OOFUNC2 OodleLZ_Decompress(const void * compBuf,OO_SINTa compBufSize,void * rawBuf,OO_SINTa rawLen,
OodleLZ_FuzzSafe fuzzSafe OODEFAULT(OodleLZ_FuzzSafe_Yes),
OodleLZ_CheckCRC checkCRC OODEFAULT(OodleLZ_CheckCRC_No),
OodleLZ_Verbosity verbosity OODEFAULT(OodleLZ_Verbosity_None),
void * decBufBase OODEFAULT(NULL),
OO_SINTa decBufSize OODEFAULT(0),
OodleDecompressCallback * fpCallback OODEFAULT(NULL),
void * callbackUserData OODEFAULT(NULL),
void * decoderMemory OODEFAULT(NULL),
OO_SINTa decoderMemorySize OODEFAULT(0),
OodleLZ_Decode_ThreadPhase threadPhase OODEFAULT(OodleLZ_Decode_Unthreaded)
);
具体的参数释义太长了,这里只介绍最重要的:
序号 | 参数 | 含义 |
---|---|---|
args1 | const void * compBuf | 压缩数据的指针 |
args2 | OO_SINTa compBufSize | 压缩数据的可用字节数,必须大于或等于(解压缩时)消耗的字节数 |
args3 | void * rawBuf | 解压缩后的数据的指针 |
args4 | OO_SINTa rawLen | 解压缩后的数据的字节数 |
返回值 | 解压缩后输出的字节数,如果解压缩失败,则返回0 |
最后,我们就可以对比着分析出,Func_OodleDecompress_14069BEF0
中,pSrcCtx->pbData
前四个字节存储的是明文的长度,从第四个字节开始的数据为密文。伪代码中的注释已经非常详细了,就过多赘述这一点了。
另外,我要必须记录一点:在一开始刚接触当前函数的时候,如果此前没能发现Func_DecompressType1or2_1400A7270
并分析其参数的数据类型,自然就不会从接口一致性的角度出发并分析出当前函数的参数和上述函数的参数是一致的。那么此时面对当前函数,其实是有一些头大的,因为初始伪代码是这样的:
我们可以先选中a2
,右键并选择Reset pointer type
:
从而将a2
从QWORD*
转换为QWORD
,这样会清楚一点:
转换前*((_QWORD *)a2 + 1)
,转换后*(_QWORD *)(a2 + 8)
。前者是+1
,是因为它是QWORD指针
,每次+1
对应实际地址+8
。这两种写法只是计算地址时的线性因子不同而已,汇编中是一样的:
回到正题,前3个参数其实都好分析,就是args4
容易混淆。
按照常理(对照着OodleLZ_Decompress
的参数表),这里的args4
应该像args2
一样,类似*(unsigned int *)a1
才顺眼。明明密文、密文长度、明文、明文长度这四个参数两两对应,为啥实际却使用了三次a2
、一次a1
呢,令人迷惑。
但实际上是因为a2+8
指向的QWORD
指向的缓冲区,存储着一个chunk(跟密文数据有关),该chunk的第一个DWORD
就是密文对应的明文长度,从第四个字节开始才是chunk的密文。这也是前两个参数中出现了+4
和-4
的原因。
压缩函数
同样从UE的源码中找到OodleLZ_Compress
的声明:
OOFUNC1 OO_SINTa OOFUNC2 OodleLZ_Compress(OodleLZ_Compressor compressor,
const void * rawBuf,OO_SINTa rawLen,void * compBuf,
OodleLZ_CompressionLevel level,
const OodleLZ_CompressOptions * pOptions OODEFAULT(NULL),
const void * dictionaryBase OODEFAULT(NULL),
const void * lrm OODEFAULT(NULL),
void * scratchMem OODEFAULT(NULL),
OO_SINTa scratchSize OODEFAULT(0) );
这里同样只介绍伪代码中用到的参数:
序号 | 参数 | 含义 |
---|---|---|
1 | OodleLZ_Compressor compressor | 表示具体不同的(OodleLZ变体)压缩算法 |
2 | const void * rawBuf | 要压缩的原始数据 |
3 | OO_SINTa rawLen | rawBuf中要压缩的字节数 |
4 | void * compBuf | 压缩后的数据的指针(该缓冲区长度必须不小于OodleLZ_GetCompressedBufferSizeNeeded的返回值) |
5 | OodleLZ_CompressionLevel level | 压缩等级 |
不同的wType
对应着不同的压缩函数,从0到3依次对应:
其实只是OodleLZ_Compress
的一些参数不同而已,这里整理下不同的地方。
wType | 压缩函数 | args1 compressor | OodleLZ变体算法名称 |
---|---|---|---|
0 | sub_14069C480 | 8 | Kraken |
1 | sub_14069C520 | 9 | Mermaid |
2 | sub_14069C5C0 | 11 | Selkie |
3 | sub_140228100 | 13 | Leviathan |
具体的Oodle系列的压缩算法,我们不需要去逆向分析。需要的时候,直接带着相应的参数,去调用dll中的接口就行。
3-1-5 Func_SetPackFileInfoWithNoDec_1400BEF30
总结
将要目标文件的一些信息整合到Stru_PackFileInfoWithNoDec
中并返回该结构体。
伪代码
Stru_PackFileInfoWithNoDec *__fastcall Func_SetPackFileInfoWithNoDec_1400BEF30(
Stru_FileHandle *pSFileHandle,
__int64 qwTime,
__int64 qwFilePointer,
__int64 qwPlainLen,
int flag)
{
Stru_FileFuncs1 *pFuncs; // rax
Stru_FileFuncs1 *v10; // rdi
__int64 v11; // rbp
__int64 v12; // rsi
__int64 v13; // rcx
__int64 v14; // rax
union _LARGE_INTEGER liFilePointer; // rax
Stru_PackFileInfoWithNoDec *pSInfo; // rdi
Stru_FileHandle *v18; // rax
if ( !pSFileHandle ) // 为NULL时最终返回NULL
{
liFilePointer.QuadPart = -1i64;
LABEL_20: // 后面代码有机会跳转过来
if ( liFilePointer.QuadPart == qwFilePointer )
{
pSInfo = (Stru_PackFileInfoWithNoDec *)g_pSMemoryFuncs->Func_Wrapper_malloc_1400A6410(
g_pSMemoryFuncs->qwZero,
56i64);
if ( pSInfo )
{
v18 = 0i64;
pSInfo->pFuncs = g_pFuncs_PackFileWithNoDec_143E09620;
if ( flag )
v18 = pSFileHandle;
pSInfo->pSFileHandle = pSFileHandle;
pSInfo->qwTime = qwTime;
pSInfo->pSFileHandleOrNull = v18;
pSInfo->qwFilePointer = qwFilePointer;
pSInfo->qwFilePointerAddPlainLen = qwFilePointer + qwPlainLen;
pSInfo->qwFilePointer2 = qwFilePointer;
return pSInfo;
}
}
else
{
pSInfo = 0i64;
}
if ( flag && pSFileHandle )
pSFileHandle->pFuncs->Func_Wrapper_CloseHandle_1400A3450(pSFileHandle);
return pSInfo;
}
if ( (__int64 (__fastcall **)())pSFileHandle->pFuncs != g_pFuncs_PackFileWithNoDec_143E09620
|| !flag
|| !pSFileHandle->hFile )
{
liFilePointer = pSFileHandle->pFuncs->Func_Wrapper_SetFilePointerEx_1400A3510(pSFileHandle, qwFilePointer, 0);
goto LABEL_20; // 跳到开头
}
pFuncs = pSFileHandle[2].pFuncs;
v10 = (Stru_FileFuncs1 *)((char *)pFuncs + qwFilePointer);
v11 = (__int64)pFuncs + qwFilePointer + qwPlainLen;
if ( (__int64)pFuncs + qwFilePointer > v11 || (__int64)v10 < (__int64)pFuncs || v11 > pSFileHandle[2].hFile )
goto LABEL_17;
v12 = (__int64)pSFileHandle[3].pFuncs;
v13 = (__int64)pSFileHandle[1].pFuncs;
if ( v12 < (__int64)v10 )
v12 = (__int64)pFuncs + qwFilePointer;
if ( v12 > v11 )
v12 = (__int64)pFuncs + qwFilePointer + qwPlainLen;
v14 = v13 ? (*(__int64 (__fastcall **)(__int64, __int64, _QWORD))(*(_QWORD *)v13 + 32i64))(v13, v12, 0i64) : -1i64;
if ( v14 == v12 )
{
pSFileHandle[2].pFuncs = v10;
pSFileHandle[2].hFile = v11;
pSFileHandle[3].pFuncs = (Stru_FileFuncs1 *)v12;
return (Stru_PackFileInfoWithNoDec *)pSFileHandle;
}
else
{
LABEL_17:
pSFileHandle->pFuncs->Func_Wrapper_CloseHandle_1400A3450(pSFileHandle);
return 0i64;
}
}
分析
这个函数的最后三分之一我没分析完,所以看起来很不对劲,请大家自行略过。特别是上面伪代码中诸如pSFileHandle[3].pFuncs
这种,肯定是不对的,应该是因为实际的结构体大小大于pSFileHandle
对应的结构体的大小,但是真正的结构体我又没分析出来。不过好在不影响这里我们分析主要逻辑。
首先,创建了56字节的空间:
然后根据这里的赋值操作:
可以定义Stru_PackFileInfoWithNoDec
结构体:
需要高度注意的是,此结构体中+0
处的pFuncs
是g_pFuncs_PackFileWithNoDec_143E09620
:
3-1-6 Func_SetPackFileInfoWithDec_1400A7CD0
总结
将要目标文件的一些信息整合到Stru_PackFileInfoWithDec
中并返回该结构体,该结构体的成员变量的具体数据的来源主要包括:
- 当前函数传入的参数,如
pSFileHandle
,qwTime
,qwPlainLen
,pfnDecompressFunc
,dwChunkPlainLen
- 从文件中读取的数据,如
ArrayOfChunksCipherLen
(存储着每个chunk的密文长度的数组) - 当前函数中设置(或计算出)的,如
pFuncs
=g_pFuncs_PackFileWithDec_143E09558
(该结构体对应的特定函数数组),dwChunkNum
- 等等
伪代码
Stru_PackFileInfoWithDec *__fastcall Func_SetPackFileInfoWithDec_1400A7CD0(
Stru_FileHandle *pSFileHandle,
__int64 qwCipherLen,
__int64 qwTime,
__int64 qwPlainLen,
unsigned int (__fastcall *pfnDecompressFunc)(Stru_DecompressContext *pSDstCtx, Stru_DecompressContext *pSSrcCtx),
unsigned int dwChunkPlainLen)
{
unsigned __int64 dwChunkNum; // rsi
__int64 qwBodyLen; // rbp
unsigned __int64 qwChunkPlainLenAddChunkNumMul4; // r12
Stru_PackFileInfoWithDec *pSInfo_1; // rax
Stru_PackFileInfoWithDec *pSInfo; // rbx
__int64 v13; // rdi
__int64 v14; // r8
unsigned int v15; // r10d
__int64 pos; // rdx
unsigned int v17; // eax
__int64 i; // r9
__int64 paChunksCipherLen; // rcx
if ( dwChunkPlainLen - 128 > 0x1FFF80 )
return 0i64;
dwChunkNum = qwPlainLen / dwChunkPlainLen;
if ( dwChunkNum > 0xFFFFFFFE )
return 0i64;
if ( qwPlainLen % dwChunkPlainLen )
LODWORD(dwChunkNum) = dwChunkNum + 1;
qwBodyLen = qwCipherLen - (unsigned int)(4 * dwChunkNum + 32);// qwCipherLen是header+body总长度
if ( qwBodyLen < 0 )
return 0i64;
qwChunkPlainLenAddChunkNumMul4 = (unsigned int)(4 * dwChunkNum) + (unsigned __int64)dwChunkPlainLen;
pSInfo_1 = (Stru_PackFileInfoWithDec *)g_pSMemoryFuncs->Func_Wrapper_malloc_1400A6410(
g_pSMemoryFuncs->qwZero,
qwChunkPlainLenAddChunkNumMul4 + 96);// 该buf长度为 一个chunk的长度 + 4 * chunk的数量 + 96 Byte
pSInfo = pSInfo_1;
if ( !pSInfo_1 )
return 0i64;
memset(pSInfo_1, 0, qwChunkPlainLenAddChunkNumMul4 + 96);
pSInfo->dwChunkPlainLen = dwChunkPlainLen;
pSInfo->pSFileHandle = pSFileHandle;
pSInfo->dwChunkNum = dwChunkNum;
pSInfo->pFuncs = g_pFuncs_PackFileWithDec_143E09558;
pSInfo->dwPlainLen = qwPlainLen;
pSInfo->pfnDecompressFunc = pfnDecompressFunc;
pSInfo->dwLastChunkIndex = -1;
pSInfo->qwTime = qwTime;
pSInfo->paChunksCipherLen = &pSInfo->ArrayOfChunksCipherLen;
v13 = 0i64;
pSInfo->pbChunkPlainBuf = (char *)&pSInfo->ArrayOfChunksCipherLen + 4 * (unsigned int)dwChunkNum;
pSInfo->qword28 = 0i64;
if ( pSFileHandle->pFuncs->Func_Wrapper_ReadFile_1400A34B0(
pSFileHandle,
&pSInfo->ArrayOfChunksCipherLen, // ChunksCipherLen数组
4 * dwChunkNum) != 4 * (_DWORD)dwChunkNum )// 读取dwChunkNum个DWORD,表示dwChunkNum个chunk的dwChunkCipherLen
goto LABEL_14;
v14 = 0i64;
v15 = 0;
if ( (unsigned int)dwChunkNum >= 2 ) // 如果不止一个chunk
{
pos = 0i64;
v17 = ((unsigned int)(dwChunkNum - 2) >> 1) + 1;// dwChunkNum / 2 向下取整
i = v17;
v15 = 2 * v17;
do
{
paChunksCipherLen = pSInfo->paChunksCipherLen;
pos += 8i64;
v13 -= *(unsigned int *)(paChunksCipherLen + pos - 8);// 减去奇数chunk的cipher长度
v14 -= *(unsigned int *)(paChunksCipherLen + pos - 4);// 减去偶数chunk的cipher长度
--i;
}
while ( i );
}
if ( v15 < (unsigned int)dwChunkNum ) // 如果dwChunkNum是奇数,那这里还要减去最后一个chunk的cipher长度
qwBodyLen -= *(unsigned int *)(pSInfo->paChunksCipherLen + 4i64 * v15);
if ( qwBodyLen + v14 + v13 < 0 ) // 后两者为负数
// 这么麻烦,其实意思是判断qwBodyLen >= ChunksCipherLen数组总和
{
LABEL_14:
g_pSMemoryFuncs->Func_Wrapper_free_1400A6420(g_pSMemoryFuncs->qwZero, pSInfo);
return 0i64;
}
pSInfo->qwFilePointer = pSFileHandle->pFuncs->Func_Wrapper_SetFilePointerEx_1400A3510(pSFileHandle, 0i64, 1u).QuadPart;// 返回文件指针的当前值
return pSInfo;
}
Part1
dwChunkPlainLen
需要不大于0x200000
- 通过明文长度和每个chunk块的最大长度求出一共有多少个chunk块
qwCipherLen
是文件总长度,4 * dwChunkNum + 32
是Header长度。
Part2
创建了一个长度为一个chunk的长度 + 4 * chunk的数量 + 96
的空间,并作为Stru_PackFileInfoWithDec
结构体:
伪代码的第53行开始,从文件中读取了dwChunkNum
个DWORD
,并存入结构体中的ArrayOfChunksCipherLen
数组当中(由于ida
无法表示动态长度的数组,所以定义结构体的时候我只定义了该数组的第一个DWORD
),代表每个chunk的密文长度。
需要高度注意的是,此结构体中+0
处的pFuncs
是g_pFuncs_PackFileWithDec_143E09558
:
Part3
这段代码挺啰嗦的,总结起来就每次循环都处理两个chunk,分别在v13
和v14
中减去各自chunk的cipher长度(v13
和v14
都是__int64
且初始值为0,所以会变成负数),最后可能会剩下一个chunk,那就单独减去这个chunk的长度。然后第78行的qwBodyLen + v14 + v13 < 0
本质上就是判断qwBodyLen
是否小于所有chunk的长度总和:若是返回0;若否则获取文件指针的当前值并赋值给pSInfo->qwFilePointer
,然后返回Stru_PackFileInfoWithDec
指针。
3-2 Func_GetFilePlainLenAndTimeWithNoDec_1400BEF00
总结
从Stru_PackFileInfoWithNoDec *
(args1
) 的成员变量中,获取(或计算出)文件的大小和时间,分别存入pqwSize
(args2
) 和 pqwTime
(args3
)。
伪代码
3-3 Func_GetFilePlainLenAndTimeWithDec_1400A7CB0
总结
从Stru_PackFileInfoWithDec *
(args1
) 的成员变量中,获取(或计算出)文件的大小和时间,分别存入pqwSize
(args2
) 和 pqwTime
(args3
)。
伪代码
3-4 Func_ReadFileDataWithNoDec_1400BEE30
总结
从Stru_PackFileInfoWithNoDec *
(args1
) 的成员变量中获取包含文件句柄的Stru_FileHandle
的指针,然后从该文件中读取长度为dwPlainLen
(args3
) 的数据,保存到pbFilePlain
(args2
) 中。
由于这个函数对应的是没有设置解压算法的情况,所以读取出来的数据就是文件的明文数据。
伪代码
3-5 Func_ReadFileDataWithDec_1400A7B00
总结
- 从
Stru_PackFileInfoWithDec *
(args1
) 的成员变量中获取包含文件句柄的Stru_FileHandle
的指针。 - 调用n次
Func_ReadFileChunkDataWithDec_1400A7930
,每次调用都会从该文件中读取并解压缩出一个chunk的的明文数据。 - 最终该文件的明文数据会保存到
pbFilePlain
(args2
) 中。
伪代码
size_t __fastcall Func_ReadFileDataWithDec_1400A7B00(
Stru_PackFileInfoWithDec *pSInfo,
char *pbFilePlain,
size_t dwPlainLen)
{
char *pbChunkPlainBuf; // r13
unsigned __int64 dwChunkPlainLen; // r15
__int64 field_28; // rcx
size_t v7; // rbp
__int64 dwPlainLen_2; // rax
__int64 dwPlainLen_1; // r8
unsigned __int64 dwChunkNum; // r12
unsigned __int64 v13; // rax
unsigned __int64 v14; // rcx
int v15; // ebx
size_t v16; // rsi
size_t dwLeftPlainLen; // r14
char *pbFilePlainPos; // rsi
unsigned int dwChunkIndex; // ebx
Stru_DecompressContext pSDstCtx; // [rsp+20h] [rbp-48h] BYREF
unsigned __int64 v21; // [rsp+70h] [rbp+8h]
pbChunkPlainBuf = (char *)pSInfo->pbChunkPlainBuf;// temp buf,用于暂存一个chunk的明文数据
dwChunkPlainLen = (unsigned int)pSInfo->dwChunkPlainLen;
field_28 = pSInfo->field_28; // 应该是0?
v7 = dwPlainLen;
pSDstCtx.dwLen = dwChunkPlainLen;
pSDstCtx.pbData = (__int64)pbChunkPlainBuf;
dwPlainLen_2 = field_28 + dwPlainLen;
if ( field_28 < 0 )
return 0i64;
dwPlainLen_1 = pSInfo->dwPlainLen;
if ( field_28 > dwPlainLen_1 )
return 0i64;
if ( dwPlainLen_2 > dwPlainLen_1 || dwPlainLen_2 < field_28 )
{
dwPlainLen_2 = pSInfo->dwPlainLen;
v7 = dwPlainLen_1 - field_28;
}
if ( !v7 )
return 0i64;
dwChunkNum = dwPlainLen_2 / dwChunkPlainLen; // 不含最后一个不能整除的块
v13 = field_28 / dwChunkPlainLen;
v14 = field_28 % dwChunkPlainLen;
v21 = v14;
v15 = v13;
if ( (_DWORD)v13 != pSInfo->dwLastChunkIndex )
{
if ( !(unsigned int)Func_ReadFileChunkDataWithDec_1400A7930(&pSDstCtx, pSInfo, v13) )
return 0i64;
v14 = v21;
}
v16 = dwChunkPlainLen - v14;
if ( dwChunkPlainLen - v14 >= v7 )
v16 = v7;
memcpy(pbFilePlain, &pbChunkPlainBuf[v14], v16);// 拷贝第一个chunk的明文数据
dwLeftPlainLen = v7 - v16;
if ( v7 != v16 )
{
pbFilePlainPos = &pbFilePlain[v16];
dwChunkIndex = v15 + 1;
pSDstCtx.pbData = (__int64)pbFilePlainPos;
if ( dwChunkIndex < (unsigned int)dwChunkNum )
{
while ( (unsigned int)Func_ReadFileChunkDataWithDec_1400A7930(&pSDstCtx, pSInfo, dwChunkIndex) )
{
pbFilePlainPos += dwChunkPlainLen;
++dwChunkIndex;
dwLeftPlainLen -= dwChunkPlainLen;
pSDstCtx.pbData = (__int64)pbFilePlainPos;// 用于存储下一个chunk的明文
if ( dwChunkIndex >= (unsigned int)dwChunkNum )// 相等时跳走
goto LABEL_18;
}
return 0i64;
}
LABEL_18:
if ( dwLeftPlainLen ) // 解压并拷贝最后一个chunk
{
pSDstCtx.pbData = (__int64)pbChunkPlainBuf;
if ( !(unsigned int)Func_ReadFileChunkDataWithDec_1400A7930(&pSDstCtx, pSInfo, dwChunkIndex) )
return 0i64;
memcpy(pbFilePlainPos, pbChunkPlainBuf, dwLeftPlainLen);
}
}
pSInfo->field_28 += v7;
return v7;
}
Part1
一些初始化相关的操作。
pbChunkPlainBuf
指向了Stru_PackFileInfoWithDec
结构体中的某个地址(还记得这个结构体的实际大小是一个chunk的长度 + 4 * chunk的数量 + 96
吗?是的,其中的一个chunk的长度
就是用在这里)。
field_28
实在没分析出来,不过好在没有影响完整的逻辑分析。
pSDstCtx
是后面马上就要分析的Func_ReadFileChunkDataWithDec_1400A7930
的参数,用来传递明文的大小和将要存储的位置。默认情况下,解压后的明文大小为dwChunkPlainLen
,解压后的数据要存储到pbChunkPlainBuf
中。
Part2
这里由于不知道field_28
到底是啥,所以分析一度陷入僵局。不过综合后续的代码,field_28
的值目前应该是0。因此v13
和v14
也是0。
故而在第49行,调用Func_ReadFileChunkDataWithDec_1400A7930
来读取并解密dwChunkIndex=0
的chunk(姑且称作第一个chunk吧)。
然后在第56行,通过memcpy
,将解密后的数据拷贝到pbFilePlain
中。
Part3
再然后就是在循环中,调用Func_ReadFileChunkDataWithDec_1400A7930
来读取并解密其他chunk(不包含第一个和最后一个chunk)。
这里每次循环都将pSDstCtx
的pbData
指向了pbFilePlainPos
,因此不需要额外通过memcpy
来拷贝解密后的明文。
Part4
解压并拷贝最后一个chunk。
单独处理它,是因为最后一个chunk的长度不是dwChunkPlainLen
,而是最后剩下的字节数dwLeftPlainLen = dwPlainLen - (n-1) * dwChunkPlainLen
,即文件的明文长度减去除最后一个chunk外的其他所有chunk的明文长度总和。
当然这里dwLeftPlainLen
的计算其实也涉及了那个没能分析出来的field_28
,不过好像总体上的逻辑大概就是这样,所以不管啦。
3-5-1 Func_ReadFileChunkDataWithDec_1400A7930
总结
读取并解压所选文件的第dwChunkIndex
(args3
) 个chunk,结果保存在pSDstCtx
(args1
) 中。
伪代码
__int64 __fastcall Func_ReadFileChunkDataWithDec_1400A7930(
Stru_DecompressContext *pSDstCtx,
Stru_PackFileInfoWithDec *pSInfo,
unsigned int dwChunkIndex)
{
__int64 paChunksCipherLen; // r11
unsigned int dwLen; // r9d
int dwLastChunkIndex; // eax
char *pbData; // rdx
__int64 v10; // rdx
__int64 qwFilePointer; // rsi
__int64 v12; // r9
unsigned int v13; // r10d
__int64 pos; // rcx
unsigned int v15; // eax
__int64 i; // r8
__int64 v17; // rax
Stru_DecompressContext SSrcCtx; // [rsp+20h] [rbp-4038h] BYREF
char pbTempBuf[16384]; // [rsp+30h] [rbp-4028h] BYREF
paChunksCipherLen = pSInfo->paChunksCipherLen;
dwLen = *(_DWORD *)(paChunksCipherLen + 4i64 * dwChunkIndex);
dwLastChunkIndex = pSInfo->dwLastChunkIndex;
pbData = pbTempBuf;
SSrcCtx.dwLen = dwLen;
SSrcCtx.pbData = (__int64)pbTempBuf;
if ( dwLastChunkIndex + 1 != dwChunkIndex )
{
v10 = 0i64;
qwFilePointer = pSInfo->qwFilePointer;
v12 = 0i64;
v13 = 0;
if ( dwChunkIndex >= 2 )
{
pos = paChunksCipherLen;
v15 = ((dwChunkIndex - 2) >> 1) + 1; // dwChunkIndex / 2 向下取整
i = v15;
v13 = 2 * v15;
do
{
v17 = *(unsigned int *)pos;
pos += 8i64;
v10 += v17; // 加上奇数chunk的cipher长度
v12 += *(unsigned int *)(pos - 4); // 加上偶数chunk的cipher长度
--i;
}
while ( i );
}
if ( v13 < dwChunkIndex ) // 如果dwChunkIndex是奇数,那这里还要加上下标为(dwChunkIndex-1)的chunk的cipher长度
qwFilePointer += *(unsigned int *)(paChunksCipherLen + 4i64 * v13);
if ( pSInfo->pSFileHandle->pFuncs->Func_Wrapper_SetFilePointerEx_1400A3510(
pSInfo->pSFileHandle,
v12 + v10 + qwFilePointer, // 上面搞来搞去,实际上就是求出File Body基址 + 前面(dwChunkIndex-1)个chunk的cipher长度总和,即当前的chunk第一个字节对应的文件指针
0) != v12 + v10 + qwFilePointer )
return 0i64;
pbData = (char *)SSrcCtx.pbData;
dwLen = SSrcCtx.dwLen;
}
if ( dwLen > 0x4000 ) // 内置的pbTempBuf不够长就动态申请,这样性能好一些
{
pbData = (char *)g_pSMemoryFuncs->Func_Wrapper_malloc_1400A6410(g_pSMemoryFuncs->qwZero, dwLen);
SSrcCtx.pbData = (__int64)pbData;
if ( !pbData )
return 0i64;
dwLen = SSrcCtx.dwLen;
}
if ( pSInfo->pSFileHandle->pFuncs->Func_Wrapper_ReadFile_1400A34B0(pSInfo->pSFileHandle, pbData, dwLen) != SSrcCtx.dwLen// 读取
|| !pSInfo->pfnDecompressFunc(pSDstCtx, &SSrcCtx) )// 解压
{
if ( (char *)SSrcCtx.pbData != pbTempBuf ) // 如果是在本函数内部动态申请的buf,那么还要free。下同。
g_pSMemoryFuncs->Func_Wrapper_free_1400A6420(g_pSMemoryFuncs->qwZero, (void *)SSrcCtx.pbData);
return 0i64;
}
if ( (char *)SSrcCtx.pbData != pbTempBuf )
g_pSMemoryFuncs->Func_Wrapper_free_1400A6420(g_pSMemoryFuncs->qwZero, (void *)SSrcCtx.pbData);
pSInfo->dwLastChunkIndex = dwChunkIndex;
return 1i64;
}
Part1
一些初始操作。其中:
dwLen
实际上是paChunksCipherLen[dwChunkIndex]
,伪代码中显示的不简洁。
pbData
暂时默认指向了当前函数栈内的一块0x4000
大小的缓冲区。
Part2
这里的操作(每次循环处理两个chunk)和Func_SetPackFileInfoWithDec_1400A7CD0
中的有些类似。
具体来说,这里每次循环都处理两个chunk,分别在v10
和v12
中累加各自chunk的cipher长度,最后可能会剩下一个chunk,那就单独加上这个chunk的长度。共循环$\lfloor\frac{dwChunkIndex}{2}\rfloor $次,因此实际上就是处理了dwChunkIndex
个chunk。
综上分析,第53行中的v12 + v10 + qwFilePointer
表示的其实就是前dwChunkIndex
个chunk的密文长度之和,再加上当前文件的指针位置,因而实际含义就是当前chunk在文件中的起始位置。
这个值有什么用呢?在51行的Func_Wrapper_SetFilePointerEx_1400A3510
中,用来将文件指针设置到这个chunk的起始位置,以便后续读取。
Part3
如果chunk的明文长度大于0x4000
,那么当前函数内部的那个缓冲区pbTempBuf[0x4000]
就不够用了,所以申请一块新的内存,并更新SSrcCtx.pbData
指向新创建的空间。
然后从文件中读取这个chunk的密文数据,接着使用此前设置好的解压函数来恢复出该chunk的明文数据。
格式总结
前面我们按照动调时的先后发现顺序依次分析了三个函数,又根据这三个函数内部调用其他函数的先后顺序,依次分析了涉及到的其他函数。下面我们整理一下通过分析上述函数总结出的索引文件的文件格式。
前32字节
Func_ParsePackFileHeaderAndSetDecompressFunc_1400A8000
中,读取了文件的前32字节,然后调用Func_Crc16AndParseFileStorage_1400A7E90
进行解析和crc16校验。
由此可以分析出这里的结构:
struct FileHeader {
QWORD qwPlainLen; // 明文长度
QWORD qwTime; // 时间戳
QWORD qwCipherLen; // 整个PackFile的长度(包括Header和以密文形式存在的Body)
DWORD dwChunkPlainLen; // 每个chunk的明文的最大长度
WORD wStorageType; // 存储类型,对应不同的解压函数
WORD wCrc16; // crc16校验和
}
(无解压函数时) 明文数据
如果没有设置解压函数,则此后的数据都是文件的明文数据。读取操作是在Func_ReadFileDataWithNoDec_1400BEE30
在进行的。
(有解压函数时) 所有的chunk的密文长度
Func_SetPackFileInfoWithDec_1400A7CD0
中,接着当前文件指针的位置,继续读取了N
个DWORD
,每个DWORD
都代表一个chunk的密文长度。
(有解压函数时) 所有的chunk的密文数据
Func_ReadFileDataWithDec_1400A7B00
中,接着当前文件指针的位置,调用了N
次Func_ReadFileChunkDataWithDec_1400A7930
,每次调用都会从该文件中读取并解压缩出一个chunk的的明文数据。
具体的解压算法可见解压函数列表。
其中需要注意,wType
为4~7时的OodleLZ
变体算法的解压函数Func_OodleDecompress_14069BEF0
中,会将chunk的前4个字节作为明文长度,chunk从第4个字节开始才是密文数据;而wType
为1时的解压函数Func_DecompressType1or2_1400A7270
中,chunk就是密文。
解包代码
本部分代码属于GujianOLPack
。
utils.py
import hashlib, struct, os, stat, re
def b2q(bytes):
return struct.unpack('<Q', bytes)[0]
def b2d(bytes):
return struct.unpack('<I', bytes)[0]
def b2w(bytes):
return struct.unpack('<H', bytes)[0]
def q2b(qword):
return struct.pack('<Q', qword)
def d2b(dword):
return struct.pack('<I', dword)
def w2b(word):
return struct.pack('<H', word)
def md5(bytes):
m = hashlib.md5()
m.update(bytes)
return m.hexdigest()
def sha1(bytes):
m = hashlib.sha1()
m.update(bytes)
return m.hexdigest()
def sha1_digest(bytes):
m = hashlib.sha1()
m.update(bytes)
return m.digest()
def byte2hexbytes(bytes):
h = b''
for i in bytes:
t = hex(i)[2:]
if len(t) == 1:
t = '0' + t
h += t.encode()
return h
# xxxx.yyy -> xxxx (1).yyy -> xxxx (2).yyy -> ...
def file_path_auto_numbering(file_path) -> str:
if not os.path.exists(file_path):
return file_path
else:
dir_path, full_file_name = os.path.split(file_path)
file_name, file_ext = os.path.splitext(full_file_name)
num_list = []
for enum_file in os.listdir(dir_path):
enum_file_name, enum_file_ext = os.path.splitext(enum_file)
if enum_file_name.startswith(file_name) and enum_file_ext == file_ext:
res = re.match(r'^ \((\d+?)\)$', enum_file_name[len(file_name):])
if res:
num_list.append(int(res.group(1)))
if len(num_list) == 0:
next_num = 2
else:
next_num = max(num_list) + 1
new_file_name = '{} ({}){}'.format(file_name, next_num, file_ext)
return os.path.join(dir_path, new_file_name)
# windows下设为只读
def set_file_read(file_path):
os.chmod(file_path, stat.S_IREAD)
# windows下取消只读
def unset_file_read(file_path):
os.chmod(file_path, stat.S_IWRITE)
def set_all_index_file_read(index_path):
# 官服
if os.path.isdir(index_path):
for file in os.listdir(index_path):
res = re.match(r'^\d+?\.idx$', file)
if res:
file_path = os.path.join(index_path, file)
set_file_read(file_path)
# Steam服
else:
set_file_read(index_path)
def unset_all_index_file_read(index_path):
# 官服
if os.path.isdir(index_path):
for file in os.listdir(index_path):
res = re.match(r'^\d+?\.idx$', file)
if res:
file_path = os.path.join(index_path, file)
unset_file_read(file_path)
# Steam服
else:
unset_file_read(index_path)
封装了一些小功能。
oo2core.py
import os, ctypes
script_path = os.path.split(os.path.realpath(__file__))[0]
dll_path = os.path.join(script_path, 'oo2core_6_win64.dll')
def Decompress(Cipher, PlainLen):
Lib = ctypes.CDLL(dll_path)
BufferSize = Lib.OodleLZ_GetDecodeBufferSize(PlainLen, 1)
ByteArray = bytearray(BufferSize)
CBuffer = ctypes.c_ubyte * len(ByteArray)
RealPlainLen = Lib.OodleLZ_Decompress(Cipher, len(Cipher), CBuffer.from_buffer(ByteArray), PlainLen, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
if RealPlainLen == 0:
raise Exception("Decompress Failed!")
return bytes(ByteArray[:RealPlainLen])
def Compress(Plain, Format = 8, Level = 7):
Lib = ctypes.CDLL(dll_path)
BufferSize = Lib.OodleLZ_GetCompressedBufferSizeNeeded(len(Plain))
ByteArray = bytearray(BufferSize)
CBuffer = ctypes.c_ubyte * len(ByteArray)
OutputCompressDataLen = Lib.OodleLZ_Compress(Format, Plain, len(Plain), CBuffer.from_buffer(ByteArray), Level, 0, 0, 0, 0, 0)
if OutputCompressDataLen == 0:
raise Exception("Compress Failed!")
return bytes(ByteArray[:OutputCompressDataLen])
if __name__ == '__main__':
Plain = b'1234567890' * 100
Cipher = Compress(Plain)
print('Cipher:\t', Cipher)
NewPlain = Decompress(Cipher, len(Plain))
print('Plain:\t', NewPlain)
从python中调用oo2core_6_win64.dll
中的压缩、解压缩相关函数。
在调用OodleLZ_Decompress
和OodleLZ_Compress
之前,需分别调用OodleLZ_GetDecodeBufferSize
和OodleLZ_GetCompressedBufferSizeNeeded
来获取所需缓冲区大小。
file.py
import time
from utils import *
from oo2core import *
import crcmod
class FileUnpack:
def __init__(self, f, pos):
self.f = f
self.pos = pos
self.f.seek(self.pos)
self.qwPlainLen = b2q(self.f.read(8))
self.qwTime = b2q(self.f.read(8))
self.qwCipherLen = b2q(self.f.read(8)) # 实际上是header+data区的总长度
self.dwChunkPlainLen = b2d(self.f.read(4))
self.wStorageType = b2w(self.f.read(2))
self.wCrc16 = b2w(self.f.read(2))
self.lChunksCipherLen = []
self.dwChunkNum = self.qwPlainLen // self.dwChunkPlainLen
if self.qwPlainLen % self.dwChunkPlainLen != 0:
self.dwChunkNum += 1
for i in range(self.dwChunkNum):
dwChunkCipherLen = b2d(self.f.read(4))
self.lChunksCipherLen.append(dwChunkCipherLen)
def get(self):
self.f.seek(self.pos + 8 + 8 + 8 + 4 + 2 + 2 + self.dwChunkNum * 4)
file_data = bytearray()
if (self.wStorageType == 0):
file_data += self.f.read(self.qwPlainLen)
elif (self.wStorageType == 4):
for dwChunkCipherLen in self.lChunksCipherLen:
dwChunkPlainLen = b2d(self.f.read(4))
data = self.f.read(dwChunkCipherLen - 4)
plain = Decompress(data, dwChunkPlainLen)
file_data += plain
return bytes(file_data)
def show_header(self):
print('qwPlainLen :', self.qwPlainLen)
print('qwTime: :', time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(self.qwTime)))
print('qwCipherLen :', self.qwCipherLen)
print('dwChunkPlainLen :', self.dwChunkPlainLen)
print('wStorageType :', self.wStorageType)
print('wCrc16 :', self.wCrc16)
print('lChunksCipherLen :', self.lChunksCipherLen)
class FilePack:
def __init__(self, file_data, qwTime, wStorageType):
self.file_data = file_data
self.qwPlainLen = len(file_data)
self.qwTime = qwTime # os.path.getmtime(file_path)
self.dwChunkPlainLen = 524288 if self.qwPlainLen >= 524288 else self.qwPlainLen
self.wStorageType = wStorageType
if wStorageType == 0:
self.body_data = self.file_data
self.qwCipherLen = 8 + 8 + 8 + 4 + 2 + 2 + len(self.body_data)
self.wCrc16 = self._crc16()
elif wStorageType == 4:
self.dwChunkNum = self.qwPlainLen // self.dwChunkPlainLen
if self.qwPlainLen % self.dwChunkPlainLen != 0:
self.dwChunkNum += 1
self.body_data = self._get_body_data()
self.qwCipherLen = 8 + 8 + 8 + 4 + 2 + 2 + self.dwChunkNum * 4 + len(self.body_data)
self.wCrc16 = self._crc16()
else:
raise Exception('Error Storage Type: %d', wStorageType)
def get(self):
data = bytearray()
data += q2b(self.qwPlainLen)
data += q2b(self.qwTime)
data += q2b(self.qwCipherLen)
data += d2b(self.dwChunkPlainLen)
data += w2b(self.wStorageType)
data += w2b(self.wCrc16)
if self.wStorageType == 4:
for i in range(len(self.lChunksCipherLen)):
data += d2b(self.lChunksCipherLen[i])
data += self.body_data
return bytes(data)
def show_header(self):
print('qwPlainLen :', self.qwPlainLen)
print('qwTime: :', time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(self.qwTime)))
print('qwCipherLen :', self.qwCipherLen)
print('dwChunkPlainLen :', self.dwChunkPlainLen)
print('wStorageType :', self.wStorageType)
print('wCrc16 :', self.wCrc16)
print('lChunksCipherLen :', self.lChunksCipherLen)
def _get_body_data(self):
body_data = bytearray()
self.lChunksCipherLen = []
for i in range(self.dwChunkNum):
if i + 1 == self.dwChunkNum:
dwChunkPlainLen = self.qwPlainLen - self.dwChunkPlainLen * i
else:
dwChunkPlainLen = self.dwChunkPlainLen
data = self.file_data[self.dwChunkPlainLen * i : self.dwChunkPlainLen * i + dwChunkPlainLen]
cipher = Compress(data)
self.lChunksCipherLen.append(len(cipher) + 4)
body_data += d2b(dwChunkPlainLen)
body_data += cipher
self.file_data = b''
return bytes(body_data)
def _crc16(self):
crc16 = crcmod.predefined.Crc('crc-16')
crc16.update(q2b(self.qwPlainLen))
crc16.update(q2b(self.qwTime))
crc16.update(q2b(self.qwCipherLen))
crc16.update(d2b(self.dwChunkPlainLen))
crc16.update(w2b(self.wStorageType))
return crc16.crcValue
if __name__ == '__main__':
# Unpack
index_path = r'D:\Game\Steam\steamapps\common\古剑奇谭网络版\data\_index'
f = open(index_path, 'rb')
unpacker = FileUnpack(f, 0)
unpacker.show_header()
unpack_file_data = unpacker.get()
print('Unpack File Len :', len(unpack_file_data))
f.close()
print()
unpack_output_path = r'temp/index_unpack.txt'
with open(unpack_output_path, 'wb')as f:
f.write(unpack_file_data)
# Pack
packer = FilePack(unpack_file_data, unpacker.qwTime, 4)
packer.show_header()
pack_file_data = packer.get()
print('Pack File Len :', len(pack_file_data))
print()
pack_output_path = r'temp/index_pack.bin'
with open(pack_output_path, 'wb')as f:
f.write(pack_file_data)
# Compare MD5 (如果要判断MD5是否相同,记得手动固定住pack文件的时间戳)
with open(index_path, 'rb')as f:
m1 = md5(f.read())
print('MD5:', m1)
with open(pack_output_path, 'rb')as f:
m2 = md5(f.read())
print('MD5:', m2)
FileUnpack
:解析打包文件格式并获取明文数据。
FilePack
:将明文的文件数据转换成打包格式。
index
from file import FileUnpack, FilePack
from utils import *
import time, os, re
def merge_hash_dict(dict1, dict2):
result = {}
for key in dict1.keys():
if key in dict2.keys():
result[key] = tuple(set(dict1[key] + dict2[key]))
# if (len(dict1[key]) > 1 or len(dict2[key]) > 1) and dict1[key] != dict2[key]:
# print(dict1[key], dict2[key], result[key])
else:
result[key] = dict1[key]
for key in dict2.keys():
if key not in dict1.keys():
result[key] = dict2[key]
return result
class Index:
def __init__(self, index_path):
self.index_path = index_path
with open(index_path, 'rb')as f:
unpacker = FileUnpack(f, 0)
index_data = unpacker.get()
with open(index_path + '.txt', 'wb')as f:
f.write(index_data)
self.index_list = []
for line in index_data.split(b'\r\n'):
if len(line) > 0:
# official: temp: hash name len type
# steam: temp: hash name
temp = line.split(b'\t')
self.index_list.append(temp)
self.dict = {}
for line_list in self.index_list:
hash = line_list[0]
name = line_list[1]
if hash not in self.dict.keys():
self.dict[hash] = (name, )
else:
old = self.dict[hash]
if name not in old:
self.dict[hash] = (*old, name)
print('Index File Path : %s' % index_path)
print('Index File: : %d Items' % len(self.index_list))
def get_names_by_hash(self, hash : bytes) -> tuple:
if hash in self.dict.keys():
return self.dict[hash]
else:
return (b'unknown/' + hash, )
def get_line_by_name(self, name : bytes):
line = -1
for i in range(len(self.index_list)):
if name == self.index_list[i][1]:
return i
return line
def get_line_data_by_name(self, name : bytes):
line = self.get_line_by_name(name)
if line == -1:
return None
else:
return self.index_list[line]
def update(self, name : bytes, file_path):
with open(file_path, 'rb')as f:
file_data = f.read()
return self._update(name, file_data)
def _update(self, name : bytes, file_data):
file_hash = sha1(file_data)
file_len = len(file_data)
self.update_by_info(name, file_hash, file_len)
return (file_hash, file_len)
def update_by_info(self, name : bytes, file_hash : str, file_len : int):
line = self.get_line_by_name(name)
if line == -1:
print('index文件中没有找到 %s 的记录' % name)
return
else:
self.index_list[line][0] = file_hash.encode()
if len(self.index_list[line]) == 4:
self.index_list[line][2] = str(file_len).encode()
def save(self):
# data = b''
# 我去,速度提升太明显了吧
data = bytearray()
for line_list in self.index_list:
data += b'\t'.join(line_list) + b'\r\n'
data = bytes(data)
packer = FilePack(data, int(time.time()), 4)
data = packer.get()
unset_file_read(self.index_path)
with open(self.index_path, 'wb')as f:
f.write(data)
set_file_read(self.index_path)
print('已将更改写入 %s' % self.index_path)
class OfficialIndex:
def __init__(self, index_dir):
# 官服的_index文件夹下不止一个index文件
self.list_indexs = []
self.dict = {}
for file in os.listdir(index_dir):
res = re.match(r'^\d+?\.idx$', file)
if res:
index_path = os.path.join(index_dir, file)
index = Index(index_path)
self.list_indexs.append(index)
#self.dict.update(index.dict)
self.dict = merge_hash_dict(self.dict, index.dict)
def get_names_by_hash(self, hash : bytes) -> tuple:
if hash in self.dict.keys():
return self.dict[hash]
else:
return (hash, )
# 对于OfficialIndex,此函数仅用作判断name对应的item是否在索引文件中存在
def get_line_by_name(self, name : bytes):
line = -1
for index in self.list_indexs:
line = index.get_line_by_name(name)
if line != -1:
return line
return line
def get_line_data_by_name(self, name : bytes):
line = -1
for index in self.list_indexs:
line = index.get_line_by_name(name)
if line != -1:
return index.index_list[line]
return None
def update(self, name : bytes, file_path):
with open(file_path, 'rb')as f:
file_data = f.read()
for index in self.list_indexs:
file_hash = index._update(name, file_data)
return file_hash
def update_by_info(self, name : bytes, file_hash : str, file_len : int):
for index in self.list_indexs:
index.update_by_info(name, file_hash, file_len)
def save(self):
for index in self.list_indexs:
index.save()
if __name__ == '__main__':
index_dir = r'D:\Game\GujianOL\data\_index'#r'temp\_index_of'
index = OfficialIndex(index_dir)
解包结果
磁盘文件
逻辑定位
跟索引文件一节一样,这里我们同样在CreateFileW
下断点,暂停条件改为rcx
指向d:\Game\GujianOL/data/data000
。
d:\Game\GujianOL/data/data000
0x0 0x47005c003a0064 d:\G
0x8 0x5c0065006d0061 ame\
0x10 0x69006a00750047 Guji
0x18 0x4c004f006e0061 anOL
0x20 0x7400610064002f /dat
0x28 0x610064002f0061 a/da
0x30 0x30003000610074 ta00
0x38 0x30 0
第一次断在CreateFileW
断在此处:
和上一节同样的方法,跟踪到调用这个函数的位置:
简单一对比,该地址所处的函数,刚好是前面我们分析过的Func_CreateFileHandle_1400A3D00
。
继续探索哪里调用了这个函数,可以跟踪到此处:
对比确定当前所在函数为Func_GetDiskFileLinkedList_1400ACE20
,ida
中部分伪代码为:
继续跟踪哪里调用了上面那个函数,来到此处:
对应函数为Func_LoadDiskFilesInfo_1400ACFE0
,ida
中部分伪代码为:
第二次断在CreateFileW
再次ctrl+f9
想要跟踪哪里调用了上面那个函数时,居然又断在了CreateFileW
:
同理又依次跟踪到了前面都分析过的Func_CreateFileHandle_1400A3D00
,Func_LoadFileHandle_1400A3BE0
。
然后我们跟踪到了,调用了Func_LoadFileHandle_1400A3BE0
的、前面第一次断在CreateFileW
时最后一步跟踪到的Func_LoadDiskFilesInfo_1400ACFE0
:
此处对应ida
中的部分伪代码:
第三次断在CreateFileW
这次我们继续跟踪哪里调用了上一个函数,结果还没跟到ret
,第三次断在了CreateFileW
:
同样又依次跟踪到了前面都分析过的Func_CreateFileHandle_1400A3D00
,Func_LoadFileHandle_1400A3BE0
再接下来,然后我们跟踪到了,调用了Func_LoadFileHandle_1400A3BE0
的Func_LoadDiskFileHandleAndCheckZFS_1400ABB50
:
此处对应ida
中的部分伪代码:
继续跟踪到调用了上个函数的、我们前面都已经跟踪到两次的Func_LoadDiskFilesInfo_1400ACFE0
:
此处对应ida
中的部分伪代码:
最后跟踪到调用了Func_LoadDiskFilesInfo_1400ACFE0
的Func_InitDiskFilesInfoCase2_1400A9D40
:
此处的伪代码为:
小总结
三次断在CreateFileW
,我们三次都跟踪到了Func_LoadDiskFilesInfo_1400ACFE0
,最后跟踪到了调用了该函数的Func_InitDiskFilesInfoCase2_1400A9D40
。
这里Func_InitDiskFilesInfoCase2_1400A9D40
函数名称中有Case2
的字眼,这是因为还有Func_InitDiskFilesInfoCase1_1400AD3E0
。Case1
这个函数我之前自己分析的时候动调跟踪到过,但是写博客的时候不知道为啥只会跟踪到Case2
。不过这两个函数的总体逻辑是相似的,都调用了Func_LoadDiskFilesInfo_1400ACFE0
和Func_LoadZfsChunksInfoOfAllDiskFiles_1400AD320
(Case1
的伪代码见下图),后续我们只以Func_InitDiskFilesInfoCase2_1400A9D40
为例进行分析。
接下来,我们从Func_InitDiskFilesInfoCase2_1400A9D40
这个函数开始,按照其内部调用函数的先后顺序,分析相关函数。
逆向分析
0 Func_InitDiskFilesInfoCase2_1400A9D40
总结
伪代码
Part1
Part2
Part3
Part4
1 Func_LoadDiskFilesInfo_1400ACFE0
总结
- 依次判断游戏根目录下的
data
文件夹中data000
~data255
这些文件是否存在,并将存在的文件添加到链表中。 - 创建
Stru_DiskFilesContext
结构体,内含所有dataxxx
的相关信息,包括文件句柄等,返回此结构体的指针。 - 其内部调用的
Func_LoadDiskFileHandleAndCheckZFS_1400ABB50
会检查文件开头4字节是否为"ZFS\x00"
。
伪代码
Stru_DiskFilesContext *__fastcall Func_LoadDiskFilesInfo_1400ACFE0(
const char *szPathPrefix,
int a2,
unsigned int a3,
__int64 a4)
{
size_t dwPathPrefixLen; // rbx
char v6; // sp
__int64 v7; // rcx
__int64 pMemoryFuncs_143E094A0; // r13
Stru_MemoryFuncs *pMemoryFuncs_143E094A0_1; // rax
Stru_DiskFileNode *pSDiskFileNodes; // rax
__int64 qwDiskFilesNum; // rsi
Stru_DiskFileNode *pSDiskFileNode; // r14
unsigned __int16 i; // bp
Stru_DiskFilesContext *pSDiskFilesCtx_1; // rax
Stru_DiskFilesContext *pSDiskFilesCtx; // rdi
void *szPathPrefixCopy; // rax
_BYTE *v18; // rcx
char v19; // al
__int64 pSMemoryFuncs; // rax
char *pszDiskFilePath; // rbx
__int64 dwPos; // rsi
__int64 dwDiskFilePathLen; // rdx
__int64 pszDiskFilePathCopy; // rax
char c; // cl
Stru_FileHandle *pSFileHandle; // rax
BOOL dwHasDiskFileExists; // ebx
__int64 ctx[34]; // [rsp+30h] [rbp-148h] BYREF
dwPathPrefixLen = -1i64;
do
++dwPathPrefixLen;
while ( szPathPrefix[dwPathPrefixLen] );
v7 = (__int64)ctx;
if ( ((v6 + 48) & 7) != 0 )
v7 = (__int64)&ctx[1] - ((v6 + 48) & 7);
pMemoryFuncs_143E094A0 = (__int64)&Stru_MemoryFuncs_143E094A0;
ctx[5] = 0x10000i64;
pMemoryFuncs_143E094A0_1 = &Stru_MemoryFuncs_143E094A0;
ctx[6] = 256i64;
if ( g_pSMemoryFuncs2 )
pMemoryFuncs_143E094A0_1 = g_pSMemoryFuncs2;// 和前面的赋值没区别,都指向同一地址
ctx[0] = (__int64)pMemoryFuncs_143E094A0_1;
ctx[1] = 0i64;
ctx[2] = v7 + 72;
ctx[7] = 0i64;
ctx[8] = 0i64;
ctx[3] = v7 + 256;
ctx[4] = v7 + 256;
pSDiskFileNodes = Func_GetDiskFileLinkedList_1400ACE20(szPathPrefix, (__int64)ctx, a2);// 3-动调CreateFileW 第一轮data000
qwDiskFilesNum = 0i64;
pSDiskFileNode = pSDiskFileNodes;
for ( i = 0; pSDiskFileNodes; ++qwDiskFilesNum )// 迭代链表,统计出节点个数
pSDiskFileNodes = pSDiskFileNodes->pSNextDiskFileNode;
pSDiskFilesCtx_1 = (Stru_DiskFilesContext *)g_pSMemoryFuncs->Func_Wrapper_malloc_1400A6410(
g_pSMemoryFuncs->qwZero,
56 * qwDiskFilesNum + 184);
pSDiskFilesCtx = pSDiskFilesCtx_1;
if ( pSDiskFilesCtx_1 )
{
memset(pSDiskFilesCtx_1, 0, 56 * qwDiskFilesNum + 184);
pSDiskFilesCtx->field_8 = a4;
pSDiskFilesCtx->pfn_sub_1400AB2F0 = sub_1400AB2F0;
pSDiskFilesCtx->qwDiskFilesNum = qwDiskFilesNum;
pSDiskFilesCtx->pfn_sub_1400AB830 = sub_1400AB830;
pSDiskFilesCtx->pfn_sub_1400AB5C0 = sub_1400AB5C0;
pSDiskFilesCtx->pfn_sub_1400AB9D0 = sub_1400AB9D0;
pSDiskFilesCtx->dwFlag = a2 != 0;
pSDiskFilesCtx->field_5C = 1;
InitializeCriticalSection(&pSDiskFilesCtx->rtl_critical_section);
pSDiskFilesCtx->field_60 = a3;
if ( a3 < 0xA00000 )
pSDiskFilesCtx->field_60 = 0xA00000;
szPathPrefixCopy = (void *)g_pSMemoryFuncs->Func_Wrapper_malloc_1400A6410(
g_pSMemoryFuncs->qwZero,
dwPathPrefixLen + 2);
pSDiskFilesCtx->szPathPrefix = szPathPrefixCopy;
if ( dwPathPrefixLen )
{
memcpy(szPathPrefixCopy, szPathPrefix, dwPathPrefixLen);
v18 = (_BYTE *)(pSDiskFilesCtx->szPathPrefix + dwPathPrefixLen);
v19 = *(v18 - 1);
if ( v19 != '/' && v19 != '\\' ) // 如果字符串最后一个字节不是'\\'或'/',则在其后追加一个'/'
{
*v18 = '/';
++dwPathPrefixLen;
}
}
*(_BYTE *)(dwPathPrefixLen + pSDiskFilesCtx->szPathPrefix) = 0;// 字符串末尾追加'\x00'
pSMemoryFuncs = (__int64)g_pSMemoryFuncs;
pSDiskFilesCtx->field_88 = 96i64;
pSDiskFilesCtx->field_90 = 682i64;
if ( pSMemoryFuncs )
pMemoryFuncs_143E094A0 = pSMemoryFuncs;
pSDiskFilesCtx->field_98 = 0i64;
pSDiskFilesCtx->field_A0 = 0i64;
pSDiskFilesCtx->field_A8 = 0i64;
for ( pSDiskFilesCtx->pSMemoryFuncs = (Stru_MemoryFuncs *)pMemoryFuncs_143E094A0; pSDiskFileNode; ++i )// 逐一处理每一个DiskFile节点
{
pszDiskFilePath = &pSDiskFileNode->szDiskFilePath;
dwPos = 56i64 * i;
dwDiskFilePathLen = -1i64;
do
++dwDiskFilePathLen;
while ( pszDiskFilePath[dwDiskFilePathLen] );
pszDiskFilePathCopy = g_pSMemoryFuncs->Func_Wrapper_malloc_1400A6410(
g_pSMemoryFuncs->qwZero,
dwDiskFilePathLen + 1);
*(__int64 *)((char *)&pSDiskFilesCtx->field_C0 + dwPos) = pszDiskFilePathCopy;//
// base + 0xB8之后的每56个字节都是一个Stru_DiskFileContext结构,一共i个
// base + 0xB8 + 56 * i pSFileHandle
// base + 0xB8 + 56 * i + 8 pszDiskFilePath
do
{
c = *pszDiskFilePath;
++pszDiskFilePathCopy;
++pszDiskFilePath;
*(_BYTE *)(pszDiskFilePathCopy - 1) = c;// 拷贝字符串
}
while ( c );
if ( pSDiskFilesCtx->dwFlag )
sub_1400ABD70((__int64)&pSDiskFilesCtx->field_B8 + dwPos, 4u, pSDiskFilesCtx->field_60);
pSFileHandle = Func_LoadFileHandle_1400A3BE0(*(CHAR **)((char *)&pSDiskFilesCtx->field_C0 + dwPos), 0);// 3-动调CreateFileW 第二轮data000
if ( pSFileHandle )
{
dwHasDiskFileExists = pSFileHandle->hFile != -1i64;
pSFileHandle->pFuncs->Func_Wrapper_CloseHandle_1400A3450(pSFileHandle);
if ( dwHasDiskFileExists )
Func_LoadDiskFileHandleAndCheckZFS_1400ABB50(pSDiskFilesCtx, i, a2);//
// 4-动调CreateFileW 第二轮data000的第二次
// 加载磁盘文件的句柄,并比对开头4字节是否是"ZFS\x00"
}
pSDiskFileNode = pSDiskFileNode->pSNextDiskFileNode;// 下一个节点
}
sub_1400A6570(ctx);
return pSDiskFilesCtx;
}
else
{
sub_1400A6570(ctx);
return 0i64;
}
}
Part1
伪代码中的ctx
实在没分析出来,只知道应该是一个上下文结构体,后续会传递给Func_GetDiskFileLinkedList_1400ACE20
中的Func_CreateNewDiskFileNode_1400A6460
,用于创建链表中的节点。好在没有影响分析主要逻辑。
第51行调用了Func_GetDiskFileLinkedList_1400ACE20
,依次判断游戏根目录下的data
文件夹中data000
~data255
这些文件是否存在,并将存在的文件添加到链表中并返回链表中的第一个节点的地址,数据结构为Stru_DiskFileNode
。
第54行,通过迭代链表中的节点,统计出了节点个数。
Part2
创建了一个新的结构体,长度为56 * qwDiskFilesNum + 184
,所以很明显其中存储了链表中每个节点对应的dataxxx
文件相关的信息。
我们进而定义Stru_DiskFilesContext
(这个结构体相当复杂,超级多的字段我们暂时无法确定其具体含义,哪怕结合了其他函数中涉及到的信息,也只能分析成下面这样了):
上述结构体从+0xB8
开始,每56个字节都是一个Stru_DiskFileContext
子结构体。同样由于不好定义动态长度的结构体的原因,这里我设计了一些占位的字段,以免伪代码中由于我们定义的结构体大小小于理论设计上的结构体的大小,导致出现类似于pDiskFilesCtx[1].xxx
这种错误显示。
Part3
前面的Part2
只是初始化了结构体中第一部分(从+0
到+0xB0
)的相关字段。
这里的Part3
,在for循环中,依次处理链表中的每个节点的相关信息,并将结果存储到结构体中。
正如第110行注释中所说,结构体基址 + 0xB8
开始的每56个字节都是一个Stru_DiskFileContext
结构(跟上面的Stru_DiskFilesContext
名称中差一个s
,来自后期的我承认这样命名确认容易混淆),我们这样定义Stru_DiskFileContext
:
因此在Stru_DiskFilesContext
中,结构体基址 + 0xB8 + 56 * i
是第i个节点的pSFileHandle
,结构体基址 + 0xB8 + 56 * i + 8
是第i个节点的pszDiskFilePath
。
然后在第124行,将结构体基址 + 0xC0 + dwPos
即结构体基址 + 0xB8 + 56 * i + 8
传给Func_LoadFileHandle_1400A3BE0
,第二次打开dataxxx
文件的句柄。
打开成功之后,又关闭了文件句柄。我估计又是通过是否打开文件句柄成功来判断文件是否存在(虽然前面判断过一次了,但是谁让开发者在代码中就是这样写的呢)。
如果文件存在,则调用Func_LoadDiskFileHandleAndCheckZFS_1400ABB50
(这个函数内部调用了此前日志中显示的第二轮的第二次的CreateFileW
),校验磁盘文件的开头4个字节是否为"ZFS\x00"
。
处理完当前节点对应的dataxxx
后,继续处理下一个节点的。
1-1 Func_GetDiskFileLinkedList_1400ACE20
总结
依次判断游戏根目录下的data
文件夹中data000
~data255
这些文件是否存在,并将存在的文件添加到链表中并返回Stru_DiskFileNode *
。
伪代码
Stru_DiskFileNode *__fastcall Func_GetDiskFileLinkedList_1400ACE20(const char *szPathPrefix, __int64 ctx, int a3)
{
__int64 ctx_1; // r8
Stru_DiskFileNode *pSCurrentNode; // r12
__int64 dwPathPrefixLen; // rax
unsigned int dwDiskFileIndex; // esi
unsigned __int64 qwNodeSize; // r13
Stru_DiskFileNode *pSDiskFileNode; // r15
Stru_FileHandle *pSFileHandle; // rbx
WCHAR *pwszDiskFilePath; // rax
BOOL bHasDiskFileExists; // edi
Stru_DiskFileNode *pSFirstDiskFileNode; // [rsp+20h] [rbp-1078h] BYREF
int v18; // [rsp+28h] [rbp-1070h]
__int64 ctx_2; // [rsp+30h] [rbp-1068h]
Stru_WrapperUtf16Str pSWrapperUtf16Str; // [rsp+40h] [rbp-1058h] BYREF
ctx_2 = ctx;
ctx_1 = ctx;
pSFirstDiskFileNode = 0i64;
pSCurrentNode = (Stru_DiskFileNode *)&pSFirstDiskFileNode;
v18 = a3;
dwPathPrefixLen = -1i64;
while ( szPathPrefix[++dwPathPrefixLen] != 0 )
;
dwDiskFileIndex = 0;
qwNodeSize = (dwPathPrefixLen + 31) & 0xFFFFFFFFFFFFFFF8ui64;
while ( 1 )
{
pSDiskFileNode = (Stru_DiskFileNode *)Func_CreateNewDiskFileNode_1400A6460(ctx_1, qwNodeSize);// 链表的节点
memset(pSDiskFileNode, 0, qwNodeSize);
sprintf(&pSDiskFileNode->szDiskFilePath, "%s/data%03u", szPathPrefix, dwDiskFileIndex);
if ( !a3 )
{
while ( 1 )
{
if ( pSDiskFileNode == (Stru_DiskFileNode *)-8i64 )
{
pSFileHandle = Func_CreateFileHandle_1400A3D00(0i64, 0);
}
else
{
memset(&pSWrapperUtf16Str, 0, sizeof(pSWrapperUtf16Str));
pwszDiskFilePath = Func_Ansi2Utf16_1400A3010(&pSWrapperUtf16Str, &pSDiskFileNode->szDiskFilePath);
pSFileHandle = pwszDiskFilePath ? Func_CreateFileHandle_1400A3D00(pwszDiskFilePath, 0) : 0i64;
if ( pSWrapperUtf16Str.pwszData ) // 2-动调CreateFileW 第一轮data000
{
g_pSMemoryFuncs->Func_Wrapper_free_1400A6420(g_pSMemoryFuncs->qwZero, (void *)pSWrapperUtf16Str.pwszData);
pSWrapperUtf16Str.pwszData = 0i64;
}
}
if ( pSFileHandle )
{
bHasDiskFileExists = pSFileHandle->hFile != -1i64;
pSFileHandle->pFuncs->Func_Wrapper_CloseHandle_1400A3450(pSFileHandle);// 句柄刚打开又关闭了
if ( bHasDiskFileExists ) // 所以这里的操作,应该只是用来判断磁盘文件是否存在
break; // 文件存在的话,退出内层while循环;文件不存在的话,继续向下枚举,直到找个一个存在的文件
}
if ( ++dwDiskFileIndex >= 0x100 ) // 磁盘文件数量最大255,达到上限直接返回链表
return pSFirstDiskFileNode;
sprintf(&pSDiskFileNode->szDiskFilePath, "%s/data%03u", szPathPrefix, dwDiskFileIndex);
}
a3 = v18;
}
++dwDiskFileIndex;
pSCurrentNode->pSNextDiskFileNode = pSDiskFileNode;// 节点插入链表尾部
pSCurrentNode = pSDiskFileNode; // 更新当前节点
if ( dwDiskFileIndex >= 0x100 )
break;
ctx_1 = ctx_2;
}
return pSFirstDiskFileNode;
}
Part1
一些初始化操作。
第23行获取了szPathPrefix
字符串的长度。
第26行的& 0xFFFFFFFFFFFFFFF8
实际上是向下对齐到8的倍数。
Part2
首先通过Func_CreateNewDiskFileNode_1400A6460
创建一个链表中的节点(该数据结构中具体的成员变量稍后讨论)。
但是这个函数我并没有跟进去分析,因为它的args1
是一个上下文结构体,由当前函数的调用者Func_LoadDiskFilesInfo_1400ACFE0
构造此结构体并传入。由于我此前并没有分析出来这个ctx
的数据结构,所以很难继续分析Func_CreateNewDiskFileNode_1400A6460
。我只是通过当前函数的前后逻辑以及动调来推测出该函数的大概功能。
然后通过 "%s/data%03u"
来拼接出dataxxx
的绝对路径。
Part3
第43行把ANSI
编码的路径转换成宽字符编码,然后调用我们前面分析过的Func_CreateFileHandle_1400A3D00
来打开文件句柄。但是打开文件句柄后,啥事都没干,就在第54行把句柄给关闭了。
所以我们可以推测,这里实际上是通过打开文件句柄是否成功来判断dataxxx
文件是否存在(xxx
是从000
遍历到255
),如果不存在,则继续判断序号递增的下一个dataxxx
文件是否存在,直到找到一个存在的dataxxx
文件时再跳出内层循环,交给外层循环处理相关后续(即Part4
中的在链表中添加节点)。
这里的逻辑,刚好印证了我们在本章一开始的时候,在x64dbg
的日志中观察到的有关CreateFileW
的前半部分操作,即先从data000
到data255
处理一遍。
Part4
将本轮外层循环中通过处理得到的节点,插入到链表的尾部,并将链表中的当前节点设置为该节点。
如果已经遍历完data000
~ data255
,则跳出外层循环,函数返回链表中的第一个节点。
关于链表节点的数据结构
当前函数中涉及到了用作链表中的节点的数据结构,本来应该去分析Func_CreateNewDiskFileNode_1400A6460
这个函数,但是我偷懒,直接上动调:
从而轻松观察到其实就两个成员变量,因此我们可以定义如下结构体Stru_DiskFileNode
:、
第一个QWORD
是下一个节点的地址;从第9个字节开始的数据是dataxxx
文件的绝对路径(同样由于ida
没法设置动态长度的字符串/数组,所以这里我只使用了一个BYTE
来表示占位)。
1-2 Func_LoadDiskFileHandleAndCheckZFS_1400ABB50
总结
打开文件句柄,读取文件开头4字节并判断是否为"ZFS\x00"
伪代码
Stru_FileHandle *__fastcall Func_LoadDiskFileHandleAndCheckZFS_1400ABB50(
Stru_DiskFilesContext *pSDiskFilesCtx,
unsigned __int16 i,
int iOpenFileType)
{
Stru_DiskFileContext *pSDiskFileCtx; // rbx
Stru_FileHandle *result; // rax
Stru_FileHandle *pSFileHandle; // rax
Stru_FileHandle *pSFileHandle_1; // rcx
int dwFirstDword; // [rsp+38h] [rbp+10h] BYREF
pSDiskFileCtx = (Stru_DiskFileContext *)(&pSDiskFilesCtx->field_B8 + 7 * i);
result = pSDiskFileCtx->pSFileHandle;
if ( !pSDiskFileCtx->pSFileHandle ) // 如果为空,则创建SFileHandle
{
pSFileHandle = Func_LoadFileHandle_1400A3BE0(pSDiskFileCtx->pszDiskFilePath, iOpenFileType);// 3-动调CreateFileW 第二轮data000的第二次
pSDiskFileCtx->pSFileHandle = pSFileHandle;
if ( !pSFileHandle ) // 如果以iOpenFileType创建失败,则以iOpenFileType=4再次创建
{
if ( iOpenFileType == 1 && !(unsigned int)sub_1400A5270(pSDiskFileCtx->pszDiskFilePath, 0i64, 0i64) )
pSDiskFileCtx->pSFileHandle = Func_LoadFileHandle_1400A3BE0(pSDiskFileCtx->pszDiskFilePath, 4);
if ( !pSDiskFileCtx->pSFileHandle )
return pSDiskFileCtx->pSFileHandle;
}
if ( pSDiskFileCtx->pSFileHandle
&& pSDiskFileCtx->pSFileHandle->pFuncs->Func_Wrapper_ReadFile_1400A34B0(// 读取开头4字节
pSDiskFileCtx->pSFileHandle,
&dwFirstDword,
4i64) == 4
&& dwFirstDword == aZFS ) // 判断开头4字节是否是"ZFS\x00"
{
return pSDiskFileCtx->pSFileHandle;
}
pSFileHandle_1 = pSDiskFileCtx->pSFileHandle;
if ( iOpenFileType )
{
if ( pSFileHandle_1
&& !((__int64 (__fastcall *)(Stru_FileHandle *, _QWORD, _QWORD))pSFileHandle_1->pFuncs->Func_Wrapper_SetFilePointerEx_1400A3510)(
pSFileHandle_1,
0i64,
0i64) // 指针设置到开头
&& pSDiskFileCtx->pSFileHandle
&& pSDiskFileCtx->pSFileHandle->pFuncs->Func_Wrapper_WriteFile_1400A34E0(// 写入"ZFS\x00"
pSDiskFileCtx->pSFileHandle,
&aZFS,
4i64) == 4
&& pSDiskFileCtx->pSFileHandle
&& pSDiskFileCtx->pSFileHandle->pFuncs->Func_Wrapper_FlushFileBuffers_1400A3490(pSDiskFileCtx->pSFileHandle) )
{
return pSDiskFileCtx->pSFileHandle;
}
pSFileHandle_1 = pSDiskFileCtx->pSFileHandle;
if ( pSDiskFileCtx->pSFileHandle )
goto LABEL_20;
}
else if ( pSFileHandle_1 )
{
LABEL_20:
pSFileHandle_1->pFuncs->Func_Wrapper_CloseHandle_1400A3450(pSFileHandle_1);
}
pSDiskFileCtx->pSFileHandle = 0i64;
return pSDiskFileCtx->pSFileHandle;
}
return result;
}
Part1
前半部分就是单纯的打开文件句柄,读取开头4个字节并判断是否为"ZFS\x00"
。
当然,由于ZFS
这个字符串太短了,导致ida
并不能将其识别为字符串,而是一个DWORD
,所以这里得能用肉眼看出来。
另外,伪代码第12行的&pSDiskFilesCtx->field_B8 + 7 * i
,其实是(QWORD)(&pSDiskFilesCtx->field_B8) + 56 * i
。7 * i
是因为&pSDiskFilesCtx->field_B8
是QWORD *
类型,+1
就是QWORD
类型下的+8
。
Part2
后半部分没太分析出来,只知道是要在某种情况下写入"ZFS\x00"
。
2 Func_LoadZfsChunksInfoOfAllDiskFiles_1400AD320
总结
循环中调用N
次Func_LoadZfsChunksInfo_1400AC2E0
来处理N
个磁盘文件(处理是解析磁盘文件的基本格式,将),最终应该会形成一个类似于索引一样的东西,以便后续可以动态加载需要的文件。
伪代码
分析
第9行申请了大小为0x20008
的空间。对照后续的分析,这个长度刚好是4 + 4 + 0x1000 * 0x20
。
然后在循环中,调用N
次Func_LoadZfsChunksInfo_1400AC2E0
来处理N
个磁盘文件,最终应该会形成一个类似于索引一样的东西,以便后续可以动态加载需要的文件。
2-1 Func_LoadZfsChunksInfo_1400AC2E0
总结
伪代码
__int64 __fastcall Func_LoadZfsChunksInfo_1400AC2E0(
Stru_DiskFilesContext *pSDiskFilesCtx,
unsigned __int16 i,
unsigned __int8 *pbZfsChunkHeader)
{
unsigned __int8 *pbZfsChunkHeader_1; // rsi
int dwChunkPos; // ebx
__int64 dwChunkPos_2; // rax
Stru_DiskFileContext *pSDiskFileCtx; // r12
union _LARGE_INTEGER v8; // rbp
union _LARGE_INTEGER qwFilePointer; // rcx
Stru_FileItemInfo *pSFileItemInfo_1; // r13
bool dwFlag; // zf
Stru_ZfsChunkNode *pSZfsChunkNode; // rax
__int64 *pSTempZfsChunkNode; // rcx
int dwFileItemIndex; // eax
unsigned __int16 wCalcCrc16; // r8
char *pbHashAdd1; // r9
__int64 dwRound; // r10
unsigned int dwFileItemInfoPos; // ebp
unsigned __int8 v19; // cl
unsigned __int16 v20; // dx
__int64 v21; // rax
unsigned __int16 v22; // dx
char bStorageType; // cl
int dwFilePos; // r14d
int dwCipherLen; // r15d
Stru_FileItemInfo *pSFileItemInfo; // rsi
unsigned int a; // r9d
int b; // edx
unsigned int c; // r8d
unsigned int d; // r9d
int e; // edx
unsigned int f; // r8d
unsigned int g; // r9d
int h; // edx
unsigned int ii; // r8d
unsigned int j; // r9d
int k; // r8d
int l; // edx
unsigned int m; // r8d
unsigned int n; // r9d
int o; // edx
unsigned int p; // r8d
unsigned int q; // r9d
int r; // edx
unsigned int dwHashValue1; // r9d
Stru_FileItemInfoNode *pSFileItemInfoNode; // r8
Stru_FileItemInfoNode *SFileItemInfoNode; // r8
__int64 v48; // rdx
Stru_FileItemInfoNode *pSFileItemInfoNode_3; // rdx
__int64 v50; // rcx
__int64 v51; // rdx
Stru_FileItemInfoNode *pSFileItemInfoNode_1; // rax
Stru_FileItemInfoNode *pSFileItemInfoNode_2; // rbx
unsigned int A; // esi
int B; // ecx
unsigned int C; // r8d
unsigned int D; // esi
int E; // ecx
unsigned int F; // r8d
unsigned int G; // esi
int H; // ecx
unsigned int I; // r8d
unsigned int J; // esi
int K; // r8d
int v65; // ecx
int v66; // eax
int L; // ecx
unsigned int M; // r8d
unsigned int N; // esi
int O; // ecx
unsigned int P; // r8d
unsigned int Q; // esi
int R; // ecx
int dwHashValue2; // esi
Stru_FileItemInfoNode *pSFirstFileItemInfoNode; // rax
Stru_FileItemInfoNode *pSNewNode; // rcx
Stru_FileItemInfoNode *v77; // rcx
Stru_FileItemInfoNode *pSNextNode; // rax
Stru_FileItemInfoNode *v79; // rcx
__int64 v80; // rdx
__int64 v81; // rcx
__int64 v82; // rcx
size_t v83; // rbp
Stru_FileItemInfoNode *v84; // rax
Stru_FileItemInfoNode *v85; // rsi
unsigned int v86; // r11d
__int64 v87; // r9
__int64 v88; // r10
__int64 v89; // rcx
Stru_FileItemInfoNode *v90; // rcx
int v91; // eax
Stru_FileItemInfoNode *v92; // rax
Stru_FileHandle *pSFileHandle; // rbx
int v94; // r14d
FILE *v95; // rax
FILE *v96; // rax
FILE *v97; // rax
Stru_FileHandle *pSFileHandle_1; // rcx
unsigned int dwNextChunkPosAdd4; // ebx
unsigned int dwNextChunkPos_1; // [rsp+20h] [rbp-58h] BYREF
int dwFileItemIndex_1; // [rsp+24h] [rbp-54h]
char v103; // [rsp+80h] [rbp+8h] BYREF
unsigned __int16 i_1; // [rsp+88h] [rbp+10h]
unsigned __int8 *pbZfsChunkHeader_2; // [rsp+90h] [rbp+18h]
int dwChunkPos_1; // [rsp+98h] [rbp+20h]
pbZfsChunkHeader_2 = pbZfsChunkHeader;
i_1 = i;
pbZfsChunkHeader_1 = pbZfsChunkHeader;
dwChunkPos = 0;
dwChunkPos_2 = 4i64;
pSDiskFileCtx = (Stru_DiskFileContext *)(&pSDiskFilesCtx->field_B8 + 7 * i);//
// base + 0xB8 + 56 * i pSFileHandle
// base + 0xB8 + 56 * i + 8 pszDiskFilePath
v8.QuadPart = -1i64;
dwNextChunkPos_1 = 4;
while ( 1 ) // 依次处理每一个IX开头的chunk
{
if ( pSDiskFileCtx->pSFileHandle )
{
qwFilePointer = pSDiskFileCtx->pSFileHandle->pFuncs->Func_Wrapper_SetFilePointerEx_1400A3510(
pSDiskFileCtx->pSFileHandle,
(unsigned int)dwChunkPos_2,// 文件指针设置为文件开头+4
0);
dwChunkPos_2 = dwNextChunkPos_1;
}
else
{
qwFilePointer.QuadPart = -1i64;
}
if ( qwFilePointer.QuadPart != dwChunkPos_2
|| !pSDiskFileCtx->pSFileHandle
|| pSDiskFileCtx->pSFileHandle->pFuncs->Func_Wrapper_ReadFile_1400A34B0(// 读取随后的0x20008字节,存储在pbZfsChunkHeader
pSDiskFileCtx->pSFileHandle,
pbZfsChunkHeader_1,
0x20008i64) != 0x20008
|| *(_DWORD *)pbZfsChunkHeader_1 != *(_DWORD *)"[IX]" )// 判断chunk的开头4字节为[IX]
{
break;
}
dwChunkPos = dwNextChunkPos_1; // 此时dwNextChunkPos_1还是当前chunk的pos
pSFileItemInfo_1 = (Stru_FileItemInfo *)(pbZfsChunkHeader_1 + 8);// chunk基址+8开始的数据就是该chunk的第一个FileItemInfo
dwFlag = pSDiskFilesCtx->dwFlag == 0;
dwNextChunkPos_1 = *((_DWORD *)pbZfsChunkHeader_1 + 1);// [IX]后面的4个字节是下一个Zfs Chunk的Pos
dwChunkPos_1 = dwChunkPos;
if ( dwFlag )
{
pSDiskFileCtx->dwChunkPos = dwChunkPos;
}
else
{
pSZfsChunkNode = (Stru_ZfsChunkNode *)g_pSMemoryFuncs->Func_Wrapper_malloc_1400A6410(
g_pSMemoryFuncs->qwZero,
16i64);
if ( pSZfsChunkNode )
{
pSZfsChunkNode->pSNextNode = 0i64;
pSZfsChunkNode->dwChunkPos = dwChunkPos;
pSTempZfsChunkNode = &pSDiskFileCtx->pSZfsChunkLinkedList;
for ( pSZfsChunkNode->dwNextChunkPos = pSDiskFileCtx->dwChunkPos;
*pSTempZfsChunkNode; // 直到找到最后一个Node时停止循环
pSTempZfsChunkNode = (__int64 *)*pSTempZfsChunkNode )
{
;
}
*pSTempZfsChunkNode = (__int64)pSZfsChunkNode;// 链表末尾追加这个新创建的节点
}
pSDiskFileCtx->dwChunkPos = dwChunkPos;
sub_1400ABE90(pSDiskFileCtx, dwChunkPos, dwChunkPos + 0x20008);
}
dwFileItemIndex = 0;
dwFileItemIndex_1 = 0;
while ( 1 ) // 依次处理此Zfs Chunk中的每一个文件记录
{
wCalcCrc16 = 0;
pbHashAdd1 = &pSFileItemInfo_1->pbHash[1];
dwRound = 7i64;
dwFileItemInfoPos = 32 * dwFileItemIndex + dwChunkPos + 8;
do
{
v19 = wCalcCrc16 ^ *(pbHashAdd1 - 1);
pbHashAdd1 += 4; // 计算crc16,每次迭代4字节,共7轮,总计28字节
v20 = HIBYTE(wCalcCrc16) ^ pwCrc16Table_143B0BD30[v19];
v21 = (unsigned __int8)(HIBYTE(wCalcCrc16) ^ LOBYTE(pwCrc16Table_143B0BD30[v19]) ^ *(pbHashAdd1 - 4));
v22 = ((unsigned __int16)(HIBYTE(v20) ^ pwCrc16Table_143B0BD30[v21]) >> 8) ^ pwCrc16Table_143B0BD30[(unsigned __int8)(HIBYTE(v20) ^ LOBYTE(pwCrc16Table_143B0BD30[v21]) ^ *(pbHashAdd1 - 3))];
wCalcCrc16 = HIBYTE(v22) ^ pwCrc16Table_143B0BD30[(unsigned __int8)(v22 ^ *(pbHashAdd1 - 2))];
--dwRound;
}
while ( dwRound );
bStorageType = pSFileItemInfo_1->bStorageType;
dwFilePos = pSFileItemInfo_1->dwFilePos;
dwCipherLen = pSFileItemInfo_1->dwCipherLen;
pSFileItemInfo = 0i64;
if ( wCalcCrc16 == pSFileItemInfo_1->wCrc16 )// 比对计算出的crc16和FileItemInfo中存储的crc16
pSFileItemInfo = pSFileItemInfo_1;
++pSFileItemInfo_1; // 该chunk中的下一个FileItemInfo
if ( !bStorageType ) // 用bStorageType==0来判断此chunk中的FileItemInfo都已经处理完了(原理是最后一个有效FileItemInfo后面的那个FileItemInfo的所有变量均为0)
break;
if ( bStorageType != 1 ) // 不是1的好像就不是合法数据,可能是用于标注哪些数据可以在整理碎片的时候可以删除?
{
if ( pSDiskFilesCtx->dwFlag
&& !(unsigned int)sub_1400ABC70(pSDiskFileCtx, dwFileItemInfoPos, 1i64, dwFileItemInfoPos + 32) )
{
v96 = __iob_func();
fprintf(v96 + 2, "load del index range error!\n");
}
goto LABEL_69;
}
if ( pSFileItemInfo )
{
a = (((unsigned __int8)pSFileItemInfo->pbHash[9]
+ (((unsigned __int8)pSFileItemInfo->pbHash[10] + ((unsigned __int8)pSFileItemInfo->pbHash[11] << 8)) << 8)) << 8)
- 0x1124111
+ (unsigned __int8)pSFileItemInfo->pbHash[8];// a = dw8 - 0x1124111
b = (a >> 13) ^ ((((unsigned __int8)pSFileItemInfo->pbHash[1]
+ (((unsigned __int8)pSFileItemInfo->pbHash[2]
+ ((unsigned __int8)pSFileItemInfo->pbHash[3] << 8)) << 8)) << 8)
- ((((unsigned __int8)pSFileItemInfo->pbHash[5]
+ (((unsigned __int8)pSFileItemInfo->pbHash[6]
+ ((unsigned __int8)pSFileItemInfo->pbHash[7] << 8)) << 8)) << 8)
+ (unsigned __int8)pSFileItemInfo->pbHash[4])
- a
+ (unsigned __int8)pSFileItemInfo->pbHash[0]);// b = (a >> 13) ^ (dw0 - dw4)
c = (b << 8) ^ ((((unsigned __int8)pSFileItemInfo->pbHash[5]
+ (((unsigned __int8)pSFileItemInfo->pbHash[6]
+ ((unsigned __int8)pSFileItemInfo->pbHash[7] << 8)) << 8)) << 8)
- 0x61C88647 // 魔数0x61c88647与碰撞解决
+ (unsigned __int8)pSFileItemInfo->pbHash[4]
- b
- a); // c = (b << 8) ^ (dw4 - 0x61C88647 - b - a)
d = (c >> 13) ^ (a - c - b);
e = (d >> 12) ^ (b - c - d);
f = (e << 16) ^ (c - e - d);
g = (f >> 5) ^ (d - f - e);
h = (g >> 3) ^ (e - f - g);
ii = (h << 10) ^ (f - h - g);
j = ((ii >> 15) ^ (g - ii - h)) + 20;
k = (unsigned __int8)pSFileItemInfo->pbHash[16]
+ ((unsigned __int8)pSFileItemInfo->pbHash[17] << 8)
+ ((unsigned __int8)pSFileItemInfo->pbHash[18] << 16)
+ ((unsigned __int8)pSFileItemInfo->pbHash[19] << 24)
+ ii; // k = dw16 + i
l = (j >> 13) ^ ((unsigned __int8)pSFileItemInfo->pbHash[12]
+ ((unsigned __int8)pSFileItemInfo->pbHash[13] << 8)
+ ((unsigned __int8)pSFileItemInfo->pbHash[14] << 16)
+ ((unsigned __int8)pSFileItemInfo->pbHash[15] << 24)
+ h
- k
- j); // l = (j >> 13) ^ (dw12 + h - k - j)
m = (l << 8) ^ (k - l - j);
n = (m >> 13) ^ (j - m - l);
o = (n >> 12) ^ (l - m - n);
p = (o << 16) ^ (m - o - n);
q = (p >> 5) ^ (n - p - o);
r = (q >> 3) ^ (o - p - q);
dwHashValue1 = (((r << 10) ^ (p - r - q)) >> 15) ^ (q - ((r << 10) ^ (p - r - q)) - r);// 从a到这的一大串代码,虽然逆不动,但还是可以分析出应该是一种散列算法,把20B的数据散列到4B
pSFileItemInfoNode = pSDiskFilesCtx->pSFileItemInfoLinkedList;
if ( !pSFileItemInfoNode
|| (SFileItemInfoNode = pSFileItemInfoNode->pSNextNode,
(v48 = *((_QWORD *)&SFileItemInfoNode->pSNextNode->pSNextNode
+ 2 * (dwHashValue1 & (SFileItemInfoNode->field_8 - 1)))) == 0)// 判断HashMap中并没有dwHashValue1这个key?
|| (pSFileItemInfoNode_3 = (Stru_FileItemInfoNode *)(v48 - SFileItemInfoNode->field_20)) == 0i64 )
{
LABEL_34:
if ( pSDiskFilesCtx->dwFlag )
sub_1400ABE90(pSDiskFileCtx, dwFilePos, dwCipherLen + dwFilePos);
pSFileItemInfoNode_1 = (Stru_FileItemInfoNode *)sub_1400A65D0((__int64)&pSDiskFilesCtx->pSMemoryFuncs);
pSFileItemInfoNode_2 = pSFileItemInfoNode_1;
if ( pSFileItemInfoNode_1 )
{
pSFileItemInfoNode_1->dwFileItemInfoPos = dwFileItemInfoPos;// 存储文件信息(即header)的pos
pSFileItemInfoNode_1->dwFilePos = dwFilePos;// 存储文件数据的pos
pSFileItemInfoNode_1->dwCipherLen = dwCipherLen;
pSFileItemInfoNode_1->wDiskFileIndex = i_1;
*(_OWORD *)pSFileItemInfoNode_1->pbHash = *(_OWORD *)pSFileItemInfo->pbHash;
*(_DWORD *)&pSFileItemInfoNode_1->pbHash[16] = *(_DWORD *)&pSFileItemInfo->pbHash[16];
A = (((unsigned __int8)pSFileItemInfoNode_1->pbHash[9]
+ (((unsigned __int8)pSFileItemInfoNode_1->pbHash[10]
+ ((unsigned __int8)pSFileItemInfoNode_1->pbHash[11] << 8)) << 8)) << 8)
- 0x1124111
+ (unsigned __int8)pSFileItemInfoNode_1->pbHash[8];
B = (A >> 13) ^ ((unsigned __int8)pSFileItemInfoNode_1->pbHash[0]
+ (((unsigned __int8)pSFileItemInfoNode_1->pbHash[1]
+ (((unsigned __int8)pSFileItemInfoNode_1->pbHash[2]
+ ((unsigned __int8)pSFileItemInfoNode_1->pbHash[3] << 8)) << 8)) << 8)
- ((((unsigned __int8)pSFileItemInfoNode_1->pbHash[5]
+ (((unsigned __int8)pSFileItemInfoNode_1->pbHash[6]
+ ((unsigned __int8)pSFileItemInfoNode_1->pbHash[7] << 8)) << 8)) << 8)
+ (unsigned __int8)pSFileItemInfoNode_1->pbHash[4])
- A);
C = (B << 8) ^ ((((unsigned __int8)pSFileItemInfoNode_1->pbHash[5]
+ (((unsigned __int8)pSFileItemInfoNode_1->pbHash[6]
+ ((unsigned __int8)pSFileItemInfoNode_1->pbHash[7] << 8)) << 8)) << 8)
- 0x61C88647
+ (unsigned __int8)pSFileItemInfoNode_1->pbHash[4]
- B
- A);
D = (C >> 13) ^ (A - C - B);
E = (D >> 12) ^ (B - C - D);
F = (E << 16) ^ (C - E - D);
G = (F >> 5) ^ (D - F - E);
H = (G >> 3) ^ (E - F - G);
I = (H << 10) ^ (F - H - G);
J = ((I >> 15) ^ (G - I - H)) + 20;
K = (unsigned __int8)pSFileItemInfoNode_1->pbHash[16]
+ ((unsigned __int8)pSFileItemInfoNode_1->pbHash[17] << 8)
+ ((unsigned __int8)pSFileItemInfoNode_1->pbHash[18] << 16)
+ ((unsigned __int8)pSFileItemInfoNode_1->pbHash[19] << 24)
+ I;
v65 = ((unsigned __int8)pSFileItemInfoNode_1->pbHash[13] << 8)
+ ((unsigned __int8)pSFileItemInfoNode_1->pbHash[14] << 16)
+ ((unsigned __int8)pSFileItemInfoNode_1->pbHash[15] << 24)
+ H;
v66 = (unsigned __int8)pSFileItemInfoNode_1->pbHash[12];
*(_QWORD *)&pSFileItemInfoNode_2->field_28 = pSFileItemInfoNode_2->pbHash;
pSFileItemInfoNode_2->field_30 = 20;
L = (J >> 13) ^ (v66 + v65 - K - J);
M = (L << 8) ^ (K - L - J);
N = (M >> 13) ^ (J - M - L);
O = (N >> 12) ^ (L - M - N);
P = (O << 16) ^ (M - O - N);
Q = (P >> 5) ^ (N - P - O);
R = (Q >> 3) ^ (O - P - Q);
dwHashValue2 = (((R << 10) ^ (P - R - Q)) >> 15) ^ (Q - ((R << 10) ^ (P - R - Q)) - R);
pSFileItemInfoNode_2->dwHashValue = dwHashValue2;
pSFirstFileItemInfoNode = pSDiskFilesCtx->pSFileItemInfoLinkedList;
if ( pSFirstFileItemInfoNode ) // 不为空
{
pSNextNode = pSFirstFileItemInfoNode->pSNextNode;
*(_QWORD *)&pSFileItemInfoNode_2->field_10 = 0i64;
pSFileItemInfoNode_2->pSNextNode = pSNextNode;// 插入到链表首部
*(_QWORD *)&pSFileItemInfoNode_2->field_8 = pSDiskFilesCtx->pSFileItemInfoLinkedList->pSNextNode->field_18
- pSDiskFilesCtx->pSFileItemInfoLinkedList->pSNextNode->field_20;
*(_QWORD *)(pSDiskFilesCtx->pSFileItemInfoLinkedList->pSNextNode->field_18 + 16) = pSFileItemInfoNode_2;
pSDiskFilesCtx->pSFileItemInfoLinkedList->pSNextNode->field_18 = (__int64)pSFileItemInfoNode_2;
}
else // 为空
{
*(_QWORD *)&pSFileItemInfoNode_2->field_10 = 0i64;
*(_QWORD *)&pSFileItemInfoNode_2->field_8 = 0i64;
pSDiskFilesCtx->pSFileItemInfoLinkedList = pSFileItemInfoNode_2;// 作为第一个节点
pSFileItemInfoNode_2->pSNextNode = (Stru_FileItemInfoNode *)malloc(64ui64);
pSNewNode = pSDiskFilesCtx->pSFileItemInfoLinkedList->pSNextNode;
if ( !pSNewNode )
exit(-1);
pSNewNode->pSNextNode = 0i64; // 清0
*(_QWORD *)&pSNewNode->field_8 = 0i64;
*(_QWORD *)&pSNewNode->field_10 = 0i64;
pSNewNode->field_18 = 0i64;
pSNewNode->field_20 = 0i64;
*(_QWORD *)&pSNewNode->field_28 = 0i64;
*(_QWORD *)&pSNewNode->field_30 = 0i64;
*(_QWORD *)&pSNewNode->dwFileItemInfoPos = 0i64;
pSDiskFilesCtx->pSFileItemInfoLinkedList->pSNextNode->field_18 = (__int64)pSDiskFilesCtx->pSFileItemInfoLinkedList;
pSDiskFilesCtx->pSFileItemInfoLinkedList->pSNextNode->field_8 = 32;
pSDiskFilesCtx->pSFileItemInfoLinkedList->pSNextNode->field_C = 5;
pSDiskFilesCtx->pSFileItemInfoLinkedList->pSNextNode->field_20 = 0i64;
pSDiskFilesCtx->pSFileItemInfoLinkedList->pSNextNode->pSNextNode = (Stru_FileItemInfoNode *)malloc(0x200ui64);
v77 = pSDiskFilesCtx->pSFileItemInfoLinkedList->pSNextNode->pSNextNode;
if ( !v77 )
exit(-1);
memset(v77, 0, 0x200ui64);
pSDiskFilesCtx->pSFileItemInfoLinkedList->pSNextNode->dwFileItemInfoPos = 0xA0111FE1;
}
++pSDiskFilesCtx->pSFileItemInfoLinkedList->pSNextNode->field_10;
v79 = pSDiskFilesCtx->pSFileItemInfoLinkedList->pSNextNode;
v80 = 2i64 * (dwHashValue2 & (unsigned int)(v79->field_8 - 1));
++*(&v79->pSNextNode->field_8 + 2 * v80);
v81 = *((_QWORD *)&pSDiskFilesCtx->pSFileItemInfoLinkedList->pSNextNode->pSNextNode->pSNextNode + v80);
pSFileItemInfoNode_2->field_18 = 0i64;
pSFileItemInfoNode_2->field_20 = v81;
v82 = *((_QWORD *)&pSDiskFilesCtx->pSFileItemInfoLinkedList->pSNextNode->pSNextNode->pSNextNode + v80);
if ( v82 )
*(_QWORD *)(v82 + 24) = pSFileItemInfoNode_2;
*((_QWORD *)&pSDiskFilesCtx->pSFileItemInfoLinkedList->pSNextNode->pSNextNode->pSNextNode + v80) = pSFileItemInfoNode_2;
if ( *(&pSDiskFilesCtx->pSFileItemInfoLinkedList->pSNextNode->pSNextNode->field_8 + 2 * v80) < (unsigned int)(10 * (*(&pSDiskFilesCtx->pSFileItemInfoLinkedList->pSNextNode->pSNextNode->field_C + 2 * v80) + 1))
|| pSFileItemInfoNode_2->pSNextNode->dwHashValue == 1 )
{
dwChunkPos = dwChunkPos_1;
}
else
{
v83 = 16i64 * (unsigned int)(2 * pSFileItemInfoNode_2->pSNextNode->field_8);
v84 = (Stru_FileItemInfoNode *)malloc(v83);
v85 = v84;
if ( !v84 )
exit(-1);
memset(v84, 0, v83);
v86 = 0;
for ( *(_QWORD *)&pSFileItemInfoNode_2->pSNextNode->field_28 = (((2
* pSFileItemInfoNode_2->pSNextNode->field_8
- 1) & pSFileItemInfoNode_2->pSNextNode->field_10) != 0)
+ ((unsigned int)pSFileItemInfoNode_2->pSNextNode->field_10 >> (LOBYTE(pSFileItemInfoNode_2->pSNextNode->field_C) + 1));
v86 < pSFileItemInfoNode_2->pSNextNode->field_8;
++v86 )
{
v87 = *((_QWORD *)&pSFileItemInfoNode_2->pSNextNode->pSNextNode->pSNextNode + 2 * v86);
if ( v87 )
{
do
{
v88 = *(_QWORD *)(v87 + 32);
v89 = (__int64)v85
+ 16
* (*(_DWORD *)(v87 + 52) & (unsigned int)(2 * pSFileItemInfoNode_2->pSNextNode->field_8 - 1));
if ( ++*(_DWORD *)(v89 + 8) > pSFileItemInfoNode_2->pSNextNode->field_28 )
{
++pSFileItemInfoNode_2->pSNextNode->field_2C;
*(_DWORD *)(v89 + 12) = *(_DWORD *)(v89 + 8) / pSFileItemInfoNode_2->pSNextNode->field_28;
}
*(_QWORD *)(v87 + 24) = 0i64;
*(_QWORD *)(v87 + 32) = *(_QWORD *)v89;
if ( *(_QWORD *)v89 )
*(_QWORD *)(*(_QWORD *)v89 + 24i64) = v87;
*(_QWORD *)v89 = v87;
v87 = v88;
}
while ( v88 );
}
}
free(pSFileItemInfoNode_2->pSNextNode->pSNextNode);
pSFileItemInfoNode_2->pSNextNode->field_8 *= 2;
++pSFileItemInfoNode_2->pSNextNode->field_C;
pSFileItemInfoNode_2->pSNextNode->pSNextNode = v85;
v90 = pSFileItemInfoNode_2->pSNextNode;
if ( pSFileItemInfoNode_2->pSNextNode->field_2C <= (unsigned int)pSFileItemInfoNode_2->pSNextNode->field_10 >> 1 )
v91 = 0;
else
v91 = v90->field_30 + 1;
v90->field_30 = v91;
v92 = pSFileItemInfoNode_2->pSNextNode;
dwChunkPos = dwChunkPos_1;
if ( v92->field_30 > 1u )
v92->dwHashValue = 1;
}
goto LABEL_69;
}
goto LABEL_68;
}
while ( 1 )
{
if ( pSFileItemInfoNode_3->dwHashValue == dwHashValue1 && pSFileItemInfoNode_3->field_30 == 20 )
{
v50 = *(_QWORD *)&pSFileItemInfoNode_3->field_28;
if ( *(_QWORD *)v50 == *(_QWORD *)pSFileItemInfo->pbHash
&& *(_QWORD *)(v50 + 8) == *(_QWORD *)&pSFileItemInfo->pbHash[8]
&& *(_DWORD *)(v50 + 16) == *(_DWORD *)&pSFileItemInfo->pbHash[16] )// 比对hash
{
break; // hash匹配成功则退出循环
}
}
v51 = pSFileItemInfoNode_3->field_20;
if ( v51 )
{
pSFileItemInfoNode_3 = (Stru_FileItemInfoNode *)(v51
- pSDiskFilesCtx->pSFileItemInfoLinkedList->pSNextNode->field_20);// next
if ( pSFileItemInfoNode_3 ) // 为空则跳出这个循环,往上跳转
continue;
}
goto LABEL_34;
}
}
if ( pSDiskFilesCtx->dwFlag )
{
pSFileHandle = pSDiskFileCtx->pSFileHandle;
v94 = pSDiskFilesCtx->field_5C;
v103 = -1;
if ( pSFileHandle
&& ((__int64 (__fastcall *)(Stru_FileHandle *, _QWORD, _QWORD))pSFileHandle->pFuncs->Func_Wrapper_SetFilePointerEx_1400A3510)(
pSFileHandle,
dwFileItemInfoPos + 31,
0i64) == dwFileItemInfoPos + 31
&& pSFileHandle->pFuncs->Func_Wrapper_WriteFile_1400A34E0(pSFileHandle, &v103, 1i64) == 1
&& (!v94 || pSFileHandle->pFuncs->Func_Wrapper_FlushFileBuffers_1400A3490(pSFileHandle))
&& !(unsigned int)sub_1400ABC70(pSDiskFileCtx, dwFileItemInfoPos, 1i64, dwFileItemInfoPos + 32) )
{
v95 = __iob_func();
fprintf(v95 + 2, "load dup index range error!\n");
}
LABEL_68:
dwChunkPos = dwChunkPos_1;
}
LABEL_69:
dwFileItemIndex = dwFileItemIndex_1 + 1;
dwFileItemIndex_1 = dwFileItemIndex;
if ( dwFileItemIndex >= 0x1000 ) // 每个Zfs Chunk最多0x1000个File Item
goto LABEL_79;
} // while end for per File Item
if ( pSDiskFilesCtx->dwFlag // 这段代码只在Zfs Chunk没有0x1000个File Item时执行(即break跳出while)
&& dwFileItemIndex_1 != 0x1000
&& !(unsigned int)sub_1400ABC70(
pSDiskFileCtx,
dwFileItemInfoPos,
(unsigned int)(0x1000 - dwFileItemIndex_1),
dwFileItemInfoPos + 32 * (0x1000 - dwFileItemIndex_1)) )
{
v97 = __iob_func();
fprintf(v97 + 2, "load free index range error!\n");
}
LABEL_79:
dwChunkPos_2 = dwNextChunkPos_1; // 为处理下一个Zfs Chunk做准备
if ( !dwNextChunkPos_1 )
return 1i64;
pbZfsChunkHeader_1 = pbZfsChunkHeader_2;
v8.QuadPart = -1i64;
} // while end for per Zfs Chunk
if ( pSDiskFilesCtx->dwFlag && dwChunkPos )
{
pSFileHandle_1 = pSDiskFileCtx->pSFileHandle;
dwNextChunkPosAdd4 = dwChunkPos + 4;
dwNextChunkPos_1 = 0;
if ( pSFileHandle_1 )
v8 = pSFileHandle_1->pFuncs->Func_Wrapper_SetFilePointerEx_1400A3510(pSFileHandle_1, dwNextChunkPosAdd4, 0);// 文件指针设置为最后一个Zfs Chunk基址+4
if ( v8.QuadPart == dwNextChunkPosAdd4
&& pSDiskFileCtx->pSFileHandle
&& pSDiskFileCtx->pSFileHandle->pFuncs->Func_Wrapper_WriteFile_1400A34E0(// 写入dword(0)
pSDiskFileCtx->pSFileHandle,
&dwNextChunkPos_1,
4i64) == 4
&& pSDiskFileCtx->pSFileHandle )
{
pSDiskFileCtx->pSFileHandle->pFuncs->Func_Wrapper_FlushFileBuffers_1400A3490(pSDiskFileCtx->pSFileHandle);// 刷新文件写入缓冲区
}
}
return 0i64;
}
Part1
在外层循环中,依次处理当前磁盘文件中的每一个ZFS Chunk。
第123行,将文件指针设置到当前ZFS Chunk的其实位置。
第135行,读取了文件指针处开始的0x20008个字节,这个刚好是一个ZFS Chunk Header的长度。
第139行,判断读取数据的前4个字节是否为"[IX]"
,这个判断和"ZFS\x00"
的判断一样,都是比较DWORD
而非字符串。
第144行,将当前处理的ZFS Chunk中的第一个文件记录的位置为当前ZFS Chunk的基址+8
。此处的pSFileItemInfo
是我们自定义的结构体Stru_FileItemInfo
,用于标识一个文件记录的相关信息,大小为0x20
:
每个ZFS Chunk中有0x1000
个这样的文件记录,这就解释了0x20008 = 4("[IX]") + 4(next zfs chunk pos) + 0x1000 * 0x20
第146行,从"[IX]"
后面紧跟的4个字节中,读取了下一个ZFS Chunk的位置。
Part2
在内层循环中,依次处理当前ZFS Chunk中的每一个文件记录。
其中do-while
循环中,和Func_Crc16AndParseFileStorage_1400A7E90
中的Part2一样,计算了4 * 7 = 28
字节的crc16
,并与当前文件记录中的第29~30字节处存储的crc16校验和进行比较。
第198行,地址后移0x20
,即下一个文件记录的地址。
紧接着的第199行,通过当前文件记录的bStorageType
字段是否为0,来判断当前ZFS Chunk的文件记录是否都已处理完(原理是此时文件记录对应的Stru_FileItemInfo
结构体的所有字段全都为0)。
Part3
虽然这里没有能够分析出具体的算法,但是可以看出,应该是一种将20字节的数据散列到4字节中的算法。
Part4
这里我并没有完全分析出来,只能继续猜测是通过