Thanks to the following contributors: - @snowykami - @frg2089 - Nya_Twisuki
220
docs/notes/RA2/ExtremeStarryInLinux.md
Normal file
@ -0,0 +1,220 @@
|
||||
---
|
||||
category:
|
||||
- Linux
|
||||
- RA2
|
||||
tag:
|
||||
- Wine
|
||||
- Bottles
|
||||
- 虚拟环境
|
||||
---
|
||||
|
||||
# 在 Linux 中游玩《星辰之光》
|
||||
因为一些机缘巧合,现在我改用 Linux 作为主力系统了。然鹅地图我还是得做的,我又没有两台电脑搞远程,那如何在 Linux 里游玩红红、制作地图首先就成了问题。
|
||||
|
||||
当然有人马上会想到虚拟机,比如熟悉的 VirtualBox、VMWare,比如`winapps`。但虚拟机也好,双系统也罢,都太重量级了,我 128GB 的二手盘不堪重负。
|
||||
|
||||
那都玩 Linux 了,Wine 兼容层怎么样呢?不也有过 Wine 跑《原神》的成功案例了嘛。但遗憾的是,自 9.17-1 版本开始,**原生 Wine** 的 DDraw 兼容都做得十分甚至九分的抽象,无论是红警 2 本体还是 FA2 地图编辑器都无法正常显示,原生 Wine 也就不再适用了。
|
||||
|
||||
> 现阶段也不再建议用 Wine 玩原神,容易被封号。可以试试云原神,但首先需要解决“鼠标无法转动视角”的问题。
|
||||
|
||||
所以,兜兜转转,还是回到了 Bottles。
|
||||
|
||||
::: important
|
||||
本篇笔记仅以《星辰之光》这个红警 2 模组作为范例,因为它是我这里最早成功跑起来的红警 2 mod。
|
||||
对于其他 mod,乃至其他游戏和 Windows 程序,本篇笔记的方案可能有一定参考价值,**但不保证能够成功运行**。
|
||||
:::
|
||||
|
||||
::: note 图片大小
|
||||
本篇笔记的插图原图对于电脑端来说会偏大一些,因此我基本上都做了缩小处理——你可以点击图片查看原图。
|
||||
如果您在用移动设备阅读,则这种“缩小”效果可能更明显些。还请见谅。
|
||||
:::
|
||||
|
||||
那么正式开始之前,我有必要先说一下我的 Linux 环境。由于 Linux 发行版众多,我**无法保证别的包源、别的发行版能否这么操作**。
|
||||
|
||||
- 操作系统:Arch Linux
|
||||
- 桌面环境:KDE 6
|
||||
|
||||
## 一、Bottles
|
||||
|
||||
Bottles 是由 [bottlesdevs](https://github.com/bottlesdevs) 开发的可视化 Wine 配置工具,旨在“让用户便利地在喜欢的发行版里运行 Windows 程序”。
|
||||
|
||||
> 参考链接:[GitHub](https://github.com/bottlesdevs/Bottles) [官网](https://usebottles.com/)
|
||||
|
||||
官方推荐通过 Flatpak 安装,在沙箱里运行。但由于「懒加载」问题,游戏本体和地编都无法正常启动。因此还是改用`pacman`吧。
|
||||
|
||||
::: info 懒加载
|
||||
经实测发现,单文件 exe 才可以在这种情况下直接在 Bottles 里启动。
|
||||
但凡需要读同级文件、子文件夹的,都需要在 Bottle 里添加快捷方式,并在快捷方式的设置里手动指明工作目录。
|
||||
:::
|
||||
|
||||
首先需要引入`archlinuxcn`源。具体步骤参见[《Arch 安装流程》](../OS/ArchInstall.md#_4-1-cn-源和-aur-助手),这里不再重复。
|
||||
接着`sudo pacman -Sy bottles`安装。等待进度跑完,就可以从“应用程序菜单栏”运行了。
|
||||
|
||||
初次运行 Bottles 会弹出一个向导跟你 blabla,无脑下一步即可。
|
||||
到最后一步时 Bottles 会下载一些组件包。由于众所周知的原因,可能会花费比较长的时间。
|
||||
|
||||

|
||||
|
||||
## 二、部署 Bottle
|
||||
|
||||
### 2.1 新建
|
||||
进入 Bottles 的主界面,点击“新建 Bottle……”(或者窗口左上角的加号),填些基本信息:
|
||||
|
||||
> [!note]
|
||||
> 我的系统语言是英文,中文翻译仅供参考。
|
||||
|
||||
- 名称`Name`:自拟(为便于说明,后面用`$venv`表示);
|
||||
- 环境`Environment`:建议选自定义`Custom`。
|
||||
|
||||
> 应用程序`Application`和游戏`Gaming`这两个预设,初次新建 Bottle 时会下载巨量的依赖。
|
||||
> 如果你网不是特别好,也没走代理,直接“自定义”就可以了。
|
||||
|
||||
- 打开共享用户目录`Share User Directory`选项
|
||||
- 兼容层,或者说运行器`Runner`选择 soda-9.0-1(以最新版为准)
|
||||
|
||||
> 如果你选了预设,这里是改不了兼容层的,得等创建好 Bottle 之后进设置再改。
|
||||
|
||||
> 此外,cn 源的 Bottles 使用系统中装的原生 Wine。在文章开头我就强调过原生 Wine 已不适用红红。所以务必换用别的。
|
||||
|
||||
- Bottle 目录`Bottle Directory`可改可不改(为便于说明,后面用`$bottles`表示)。
|
||||
|
||||
> 默认你的环境位于`~/.local/share/bottles/bottles`目录下。
|
||||
|
||||
> [!warning]
|
||||
> 如果你在全局设置里改过默认目录,千万不要在新建这里又改到同一个位置,否则会报**符号占用,创建失败**。
|
||||
|
||||

|
||||
|
||||
然后在右上角点击“创建”即可。
|
||||
|
||||
> [!note]
|
||||
> 在 Linux 中,`~`和`$HOME`通常指代`/home/<user_id>` [^home_dir],比如`/home/nyacl`。
|
||||
> 类比下 Win7 的`%UserProfile%`和`C:\Users\nyacl`就知道了。
|
||||
>
|
||||
> [^home_dir]: Linux 的路径是**区分大小写**的,终端里的环境变量(通常全大写)也是。
|
||||
> 即`YURI.exe` ≠ `yuri.EXE`;`$HOME` ≠ `$home`。
|
||||
|
||||
### 2.2 Bottle 选项
|
||||
|
||||
点击刚建好的 Bottle 进入详情页,点开设置`Settings`:
|
||||
|
||||
1. 需要开启 DirectX 翻译——将组件`Components`部分的 DXVK 和 VKD3D 打开;
|
||||
2. 可以考虑在显示`Display`部分启用独立显卡`Discrete Graphics`(我的笔记本没有捏);
|
||||
3. 性能`Performance`部分的同步`Synchronization`可以考虑 Fsync,除此之外的选项建议不动;
|
||||
|
||||
做完设置,退回上一页把依赖`Dependencies`装上:
|
||||
|
||||
::: tip 红警 2 推荐依赖
|
||||
|
||||
- 客户端需要:`mono` (Wine mono .NET 依赖) (耗时较长,建议最后安装)
|
||||
- 中日韩字体:`cjkfonts`(避免“口口文学”)
|
||||
|
||||
> 你也可以手动下载(或复制 C:\Windows\Fonts 里的)msyh.ttc 和 simsun.ttc,
|
||||
> 并复制到`$bottles/$venv/drive_c/windows/Fonts`里。
|
||||
|
||||
- 游戏本体需要:`cnc-ddraw`
|
||||
- Reshade 特效层需要:`d3dcompiler_*.dll` `d3dx*`
|
||||
|
||||
> 这里的 * 代表全都要,比如 d3dx11 和 d3dx9。
|
||||
<!-- - gdiplus -->
|
||||
:::
|
||||
|
||||
## 三、部署游戏环境
|
||||
|
||||
下载好《星辰之光》大版本包体(如有必要,额外再下载小更新包),用`unzip`解压:
|
||||
```bash
|
||||
sudo pacman -S unzip
|
||||
# 请根据实际情况替换压缩包路径
|
||||
unzip -O GBK -o '~/Documents/Extreme Starry v0.6.zip'
|
||||
# 如果网络不好,不方便更新,并且群里恰有离线更新包,也可以直接下载、覆盖更新
|
||||
unzip -O GBK -o '~/Documents/0.6.2 离线更新包.zip' -d '~/Documents/Extreme Starry'
|
||||
```
|
||||
|
||||
::: details unzip 命令行解释
|
||||
`unzip [opt] </path/to/zip> [-d extract_dir]`
|
||||
|
||||
- `-O encoding`:指定在 Windows 里打包的 ZIP 采用什么编码打开。
|
||||
- `-o`(注意大小写不一样):有相同文件名的,一律覆盖。
|
||||
- `/path/to/zip`:zip 路径。
|
||||
> 遇到空格需要加反斜杠转义,或者像我那样直接打引号。
|
||||
- `-d extract_dir`:解压到单独的文件夹。
|
||||
> 像上面离线包直接解压出来是散装跟`Extreme Starry`并列放的。而`~/Documents`可能不止放《星辰之光》。
|
||||
|
||||
更多细节还请自行`unzip -h`。虽然解说都是英文。
|
||||
:::
|
||||
|
||||
然后点开你的 Bottle 进入详情页,为客户端`Extreme Starry.exe`添加快捷方式,这样就不需要每次都点“运行可执行程序”找半天了。
|
||||
|
||||

|
||||
|
||||
> [!tip]
|
||||
> 在“选择可执行文件”对话框中,若找不到 exe,请在“过滤”那里改为`Supported Executables`。
|
||||
|
||||
## 四、渲染补丁
|
||||
我们知道,红警 2 是个 Windows 游戏,但众所周知,由于系统调用的不同,Windows 程序无法直接在 Linux 上跑,这点对于“渲染补丁”也是一样。
|
||||
所以客户端设置也好,玩家自备`ddraw.dll`也罢,**均无法在 Wine 里使用**。
|
||||
|
||||
### 4.1 游戏本体
|
||||
可能你会有疑问:前面不是让装`cnc-ddraw`了吗?怎么又有问题捏?因为文中的 Bottles 以及用于原生 Wine 的 Winetricks 均只提供这个。换言之,你基本上**只有`cnc-ddraw`类补丁可以选**。
|
||||
|
||||
除此之外,Bottle 容器与 Windows 类似,**默认从游戏目录(即“内建`Builtin`”)加载 DLL**。所以,还需要调整`ddraw.dll`加载次序。
|
||||
- 找到 Bottle 详情的“工具”一栏;
|
||||
- 点开`Legacy Wine Tools`找到`Configuration`,打开`winecfg`。
|
||||
- 选中函数库`Libraries`页面,在列表中选中`ddraw`,点击编辑`Edit`;
|
||||
若找不到,**先**在上面的输入框里手打`ddraw.dll`,点击添加`Add`。
|
||||
- 在弹出的 5 个选项中,选择**原装`Native` (Windows)**。
|
||||
|
||||
而对于 Reshade,国内有一些 Reshade 会伪装成`d3d*.dll`。由于上面提到的默认规则,这种 Reshade 实际仍能配合`ddraw.dll`运作,在游戏中显示出 Reshade 版本提示。当然具体特效显示成什么样就未经细致测试了。
|
||||
|
||||
::: details Wine 的 DLL 查找
|
||||
经查证,前面说的`soda`、`proton`均为 Wine 的变种。所以只需讨论 Wine 的做法即可。
|
||||
总的来说,Wine 的查找与 Windows 的 KnownDlls 机制类似,但做了简化[^wine_forum_dll]:
|
||||
|
||||
- 内建(Builtin):(默认优先)在程序的**当前目录**(或者叫**工作目录**,在本文中又称**游戏目录**)下查找、加载。
|
||||
- 原装(Native):(默认备选)在 Wine 容器(即`$venv`虚拟 C 盘的`System32`,可能还有`SysWOW64`)中查找。
|
||||
|
||||
[^wine_forum_dll]: 参见帖子 _[Wine can't find/load DLLs in the same dir](https://forum.winehq.org/viewtopic.php?t=36023)_。
|
||||
:::
|
||||
|
||||
### 4.2 FA2 及其扩展(FA2sp 等)
|
||||
开篇提到,我还有做地图的需求。
|
||||
|
||||
目前圈子里所谓“FA2 防卡补丁”实际是 DxWnd,它仍会加载系统目录的`ddraw.dll`。那么对本随记而言,便只需讨论“原装”的 DDraw。经过测试,刚建好的 Wine 环境其`ddraw.dll`恰可以为 FA2 所用。
|
||||
|
||||
> 原生 9.16-1 那版对我来说刚好,但是无视缩放比;
|
||||
> 9.17-1 及往后的新版本则会因屏幕缩放有一些拉扯感,不知高分屏用户觉得如何。
|
||||
> Proton 等 Wine 改版的表现与 9.16-1 一致。推测是并未跟进最新版本。
|
||||
|
||||
那需要做的就很简单了:**另起一个 Bottle 跑地编**。或者,在`cnc-ddraw`安装之前先提取出`$venv/drive_c/windows/System32`(也可能是`SysWOW64`,如果有的话)里面的`ddraw.dll`,**覆盖 DxWnd**。
|
||||
|
||||
## 五、开玩
|
||||
|
||||
在做完全部配置之后,点击你建过的快捷方式右边的`▶`图标,开耍。……虽然,读条可能会比较慢。
|
||||
|
||||

|
||||
|
||||
::: info 再次启动客户端没有反应
|
||||
可能是因为进程还驻留在 Wine 环境当中,需要“强制停止所有进程”手动干掉:
|
||||
|
||||

|
||||
:::
|
||||
|
||||
## 附录:关于 Syringe 命令行
|
||||
> [!tip]
|
||||
> 像《星辰之光》这种有独立客户端的 mod 无需查阅此附录,客户端本身就负责了命令行解析。
|
||||
|
||||
Linux 的文件名允许英文引号(如`"game"md.exe`),在终端里,这会给 Syringe 带来歧义:
|
||||
```log
|
||||
Syringe.exe "\"gamemd.exe\"" -SPAWN ...
|
||||
```
|
||||
解法也很简单,把它扔进批处理即可:
|
||||
```cmd
|
||||
PUSHD %~dp0
|
||||
Syringe.exe "gamemd.exe" -SPAWN -log -cd -speedcontrol
|
||||
```
|
||||
然后把批处理扔进游戏目录(或者说和`gamemd.exe`放在一起),让 Wine 去启动批处理即可:
|
||||
```bash
|
||||
# wine 运行时会把 Linux 根目录挂载到 Z 盘。
|
||||
wine cmd /c "Z:/home/agxcoy/Documents/ES-FA2/launch.cmd"
|
||||
```
|
||||
|
301
docs/notes/RA2/ReverseEngineering.md
Executable file
@ -0,0 +1,301 @@
|
||||
---
|
||||
category:
|
||||
- 逆向工程
|
||||
- RA2
|
||||
tag:
|
||||
- Syringe
|
||||
- 进程注入
|
||||
---
|
||||
|
||||
# 基于 FA2sp 的逆向小记
|
||||
|
||||
参考资料:
|
||||
- Zero-Fanker:[Ares Wiki](https://gitee.com/Zero_Fanker/Ares/wikis)
|
||||
- 王道论坛:[2023 考研 408 学习资料](https://github.com/ddy-ddy/cs-408)
|
||||
|
||||
## 背景
|
||||
|
||||
FA2sp 是为了改善红警 2 地图编辑器 FinalAlert2(下面简称 FA2)的使用体验而开发的扩展库。它通过 Syringe 注入 Hook 的方式,无需修改 FA2 本体便能享受到扩展的功能和修复。
|
||||
|
||||
2024年3月8日,EA 在发布 Steam 版红警 2 时,终于把 FA2 的源码放了出来,自此 FA2sp 完成了历史使命。
|
||||
然鹅,FA2sp 的主要开发者 [@secsome](https://github.com/secsome)(书伸)虽然承诺把扩展功能和 bug 修复移植到官方源码中,但由于三次元原因该计划迟迟未能推进。
|
||||
考虑到《星辰之光》这边仍有对 FA2 的扩展需求,我便当起了接盘侠,继续 [FA2sp](https://github.com/ClLab-YR/FA2sp) 的维护。
|
||||
|
||||
那既然接了盘,总该干点事不是。于是才疏学浅的我结合自己备考 408 的粗略理解,尝试研究书伸留下来的逆向成果——`finalalert2yr.exe.idb`。
|
||||
|
||||
## 复习一下寄组
|
||||
|
||||
> [!note]
|
||||
> 我事非科班生,对汇编的认识仅限考研 408 计算机组成原理对“指令系统”的考察。
|
||||
> > 其实我参加的是 24 考研(2023.12.23-24),但回去翻考研群已经只剩 23 考研的资料了。
|
||||
>
|
||||
> FA2 显然是 Intel x86 架构的程序,恰好 24 考研主要考 x86 汇编。
|
||||
|
||||
### 寄存器
|
||||
|
||||
除了考研常考的通用寄存器`e[abcd]x`、帧指针`ebp`栈指针`esp`外,
|
||||
在遇到 Fatal Error 时,我们还重点关注`except.txt`里的`eip`寄存器:
|
||||
```
|
||||
EIP: 00534096 ESP: 013A89D4 EBP: 013A89FC
|
||||
EAX: 00000000 EBX: 00886240 ECX: 00886240
|
||||
EDX: 003F5000 ESI: 00886230 EDI: 2A3C0000
|
||||
```
|
||||
在 FA2sp 里,这些寄存器可以通过 [Syringe](https://github.com/Ares-Developers/YRpp/blob/master/Syringe.h) Hook 定义里的`REGISTERS *R`指针参数存取。
|
||||
|
||||
### 跳转汇编指令
|
||||
|
||||
> [!important]
|
||||
> 考虑到王道书里介绍的多数基本运算指令在分析 FA2 中意义并不大,这里就直接跳过了。
|
||||
> 完整版的 x86 汇编指令介绍还请移步《汇编语言程序设计》或者《汇编原理》之类的课程,恕不浪费太多时间咯。
|
||||
|
||||
#### 常规:Jump 系列
|
||||
|
||||
分为 jmp 无条件跳转,和 j*condition* 有条件跳转两种。其中条件跳转可以**部分**参考 pwsh 的比较:
|
||||
|条件跳转指令字|PowerShell 比较
|
||||
|-|-|
|
||||
|`je` (Equal `==`)|`-eq`|
|
||||
|`jne` (Not Equal `!=`)|`-ne`|
|
||||
|`jz` (Zero `== 0`)|`-eq 0`|
|
||||
|`jg` (Greater than `>`)|`-gt`|
|
||||
|`jge` (Greater than or Equal to `>=`)|`-ge`|
|
||||
|...|
|
||||
|
||||
在 IDA 中,`jmp` `j...`通常跟的是标签(如`LABEL_20`),标签用于指代某一个虚拟地址(32 位程序基址`0x400000`)。
|
||||
跳转指令认出标签指代的地址后,将 EIP 寄存器设为该地址,CPU 从那里继续取指、间址(可能跳过)、执行、中断(可能跳过)四部曲。
|
||||
|
||||
#### 特殊:函数调用
|
||||
|
||||
主要是`call`和`ret`这一对。
|
||||
|
||||
`call lbl`是父级函数去“调用”。它会把函数参数、下一指令地址压入栈,然后无条件跳转到`lbl`标签指代的地址(同时改变 EBP 的值,以便建立新的栈帧);
|
||||
|
||||
相对的,`ret`是子函数要“返回”。在回收子函数栈帧、还原 EBP 之后,`ret`指令会无条件跳转回先前执行到的位置。
|
||||
|
||||
> [!info]
|
||||
> 回收栈帧、还原回父级函数的 EBP 这两步由`leave`指令完成,
|
||||
> 相当于`mov esp, ebp`再`pop ebp`,详见「栈帧」。
|
||||
|
||||
::: details 栈帧
|
||||
|
||||
函数的执行是由进程的栈空间管理的(相应的,`malloc` `new`之类则从堆空间申请内存),正所谓“函数调用栈”。
|
||||
栈帧通常会记录局部变量等临时用到的数据,同时也是实现函数调用的重要跳板。
|
||||
|
||||
设有这么两段代码:
|
||||
```c
|
||||
int eg_sub(int x, int y) { return x * y; }
|
||||
int example() {
|
||||
int a = 10;
|
||||
int b = eg_sub(a, 1024);
|
||||
return b - a;
|
||||
}
|
||||
```
|
||||
又假设`example()`被 main 函数调用,那么栈帧可能会是这种分布:
|
||||
|地址|...|备注|
|
||||
|-|:--:|-|
|
||||
|0x520|(`main()`的 EBP)|`example()`栈帧从这里开始|
|
||||
|0x51C|int a = 10|
|
||||
|0x518|int b|
|
||||
|0x514|(空余 8B)|`gcc`编译器要求栈帧大小为 16B 的整数倍|
|
||||
|0x50C|1024|参数 y|
|
||||
|0x508|10|参数 x,即复制 a 的值|
|
||||
|0x504|调用`eg_sub()`时 EIP 指向的下一指令地址|亦即被`call`压栈、`ret`返回的地址<br>`example()`栈帧到此结束|
|
||||
|0x500|(`example()`函数的 EBP)|这里是`eg_sub()`的栈帧了|
|
||||
|...|...||
|
||||
|
||||
调用`eg_sub()`前,首先把参数`y` `x`压栈(`cdecl`约定采取反向入栈)。
|
||||
对于`int b`那一行语句,我们不妨拆成这样的汇编指令:
|
||||
```
|
||||
push ecx # 设 a=10 位于 ecx
|
||||
call eg_sub # 函数调用,返回值在 eax
|
||||
mov ebx, eax # 假设 b 在 ebx,把返回值赋给 b
|
||||
```
|
||||
那么执行到`call`指令时,EIP 指向下一条`mov`指令,于是`call`指令保存(入栈)EIP 的值,放心地跳转到`eg_sub`的指令地址去了。
|
||||
|
||||
在进入`eg_sub`那里之后,首先建立它自己的栈帧:
|
||||
```
|
||||
push ebp
|
||||
...
|
||||
mov ecx, [ebp + 12] # 假设 ecx 存 y
|
||||
mov edx, [ebp + 8] # 假设 edx 存 x
|
||||
...
|
||||
```
|
||||
执行完之后保存返回值`mov eax, ...`,回收栈帧、还原现场`leave`,然后`ret`指令跳转回`example()`。
|
||||
`ret`指令把执行`call`指令时的“下一指令地址”弹回 EIP 寄存器,然后 CPU 就若无其事地继续跑 example 函数了。
|
||||
:::
|
||||
|
||||
> [!note]
|
||||
> 王道计组书和视频课「过程调用的机器级表示」那一节对于`call` `ret`指令以及栈帧的介绍可能更清楚一点。
|
||||
> 24 考研距今也有半年余了,恕我没有办法准确地复述出来。
|
||||
|
||||
### 寻址方式
|
||||
上面讲栈帧出现了个`[ebp + 12]`,涉及到两种寻址:寄存器间接寻址和 EBP“相对寻址”。
|
||||
|
||||
> [!important]
|
||||
> 注意我这“相对寻址”是打了引号的,因为并不是以 PC(或者说 IP、EIP 寄存器)为基准的相对,而是 EBP。
|
||||
|
||||
首先是 EBP 寻址。进程由操作系统管理,其堆栈空间在内存中开辟。既然如此,EBP 和 ESP 的值实际上就是指向内存中栈空间的地址。
|
||||
比如在上面「栈帧」里举的例子,执行`example()`函数主体时,\[EBP\]=0x520,\[ESP\]=0x504;
|
||||
进入`eg_sub()`函数调用后,\[EBP\] 则变为 0x500。
|
||||
|
||||
于是,我们可以对栈指针 ESP 和帧指针 EBP 做加减运算,找出函数参数、局部变量等信息。
|
||||
例如上面建立`eg_sub`的栈帧时把函数参数从栈里读出来(不是`pop`出栈),就用`eg_sub`的 EBP 往上加。
|
||||
由于两个栈帧之间总隔着一个“返回地址”,所以第一个参数并不是`+4`,而是`+8`。
|
||||
而相对的,访问局部变量可以用 EBP 往下减,`EBP - 4`,`EBP - 8`,之类的。
|
||||
|
||||
> 通常来说,ESP 容易受`pop` `push`指令的影响,比较“多动”;而 EBP 相比起来更“安稳”一些。
|
||||
> 当然 ESP 寻址肯定是有的,Syringe 里的`XXX_STACK`就是 ESP 寻址。只是 EBP 寻址我讨论起来方便。
|
||||
|
||||
其次是寄存器间接寻址。对 EBP 指针做加减运算,找到参数、局部的地址之后,还需要做一次间接寻址,去内存里把真正的数据抓出来。
|
||||
间接寻址不需要你操心,我只是让你注意寄存器旁边的中括号而已:
|
||||
```
|
||||
mov eax, ebx # 把 EBX 寄存器里的值直接传给 EAX
|
||||
mov eax, [ebx] # 把 EBX 里的内存地址取出来,再读那个内存地址,把数据传进 EAX。
|
||||
```
|
||||
|
||||
## 初探 IDA
|
||||
|
||||
### 案例
|
||||
|
||||
FA2 的“国家”和“所属”是靠后缀区分的,国家直接取自 Rules*.ini,所属则是在国家基础上添加了` House`后缀,比如国家`YuriCountry`和所属`YuriCountry House`。
|
||||
默认在触发编辑器属性页里,触发所属方会截断空格,只许你选“国家”。现要求把这个碍事的截断给干掉,方便我们实现多人合作地图的“所属”关联。
|
||||
|
||||
### 逆向分析
|
||||
|
||||
::: details 有源码做题就是快
|
||||
注意到`TriggerOptionsDlg.cpp`里关于“触发所属方”的事件定义:
|
||||
```cpp {14,}
|
||||
void CTriggerOptionsDlg::OnEditchangeHouse()
|
||||
{
|
||||
// ... 前面忘了
|
||||
|
||||
CString newHouse;
|
||||
m_House.GetWindowText(newHouse); // 实际是 GetWindowTextA
|
||||
|
||||
// FA2 读完所属会用 CSF 本地化这些窗口控件的所属名字(但是非常鸡肋)
|
||||
// 这一步又把本地化的所属翻译回 INI 的所属 ID
|
||||
newHouse=TranslateHouse(newHouse);
|
||||
|
||||
newHouse.TrimLeft();
|
||||
// 如果你英语好一点,空格 => space,你便已经找到要淦的位置了:
|
||||
TruncSpace(newHouse);
|
||||
|
||||
// ... 后面忘了
|
||||
}
|
||||
```
|
||||
右键对`TruncSpace`转到定义,可以在`functions.cpp`发现:
|
||||
```cpp
|
||||
void TruncSpace(CString& str)
|
||||
{
|
||||
str.TrimLeft();
|
||||
str.TrimRight();
|
||||
if(str.Find(" ")>=0) str.Delete(str.Find(" "), str.GetLength()-str.Find(" "));
|
||||
}
|
||||
```
|
||||
于是确定我们要干掉的就是这个`TruncSpace`。
|
||||
|
||||
当然了前面说过,书伸还没搬运完 FA2sp 的功能修复,改源码暂时没什么意义。
|
||||
:::
|
||||
|
||||
> 开始之前赞美一下书伸,书门!(
|
||||
> 没有书伸的成果,我不可能很快找出待修改函数的虚拟地址。
|
||||
|
||||
在 32 位 IDA 里新建一个反编译项目,打开 FA2 的主程序。我们案例要淦的函(方)数(法)位于`0x501D90`,在菜单栏`Jump`里找到`Jump to address`,把这个地址复制进去确认。
|
||||
默认它会切换为 Graph View,你需要右键改为 Text View:
|
||||
|
||||

|
||||
|
||||
往下翻到`.text:00501E58`,注意到`GetWindowTextA`这个 WinAPI。如果你翻看了上面的源码,就会发现我们离目标不远了。
|
||||
|
||||
> [!tip]
|
||||
> 引用的 API,比如说 WinAPI 或者 CString 类的 API,地址通常都比较靠后。
|
||||
> 在 Text View 里双击那个`GetWindowTextA`,可以发现地址跑到`0x553134`去力(瞄完可以用工具栏上的左箭头返回我们正文看的位置)。
|
||||
> 所以接下来不要认错函数调用咯。
|
||||
|
||||
借着上面的提示,同屏`GetWindowTextA`后面只剩两个怀疑对象:`sub_43C3C0`和`sub_43EA90`。
|
||||
|
||||

|
||||
|
||||
接下来看看这两个嫌疑函数的特征。直接菜单栏`View`,`Open subviews`,`Generate pseudocode (F5)`生成反汇编代码,于是我们得到案例方法的 C 式伪代码:
|
||||
|
||||

|
||||
|
||||
由上面的源码可得,截断空格的函数`TruncSpace`只有一个参数,至此我们确定是`sub_43EA90`背锅。
|
||||
|
||||
## 编写 Hook
|
||||
|
||||
目前我已知两种 Hook 用法,我们这里写的 Hook 是第二种用途:
|
||||
|
||||
- 在原函数里新增内容实现扩展(`return 0`)
|
||||
- 绕过(或覆盖)原函数的执行流程(`return`到目标地址)
|
||||
|
||||
### 背景芝士:Syringe
|
||||
|
||||
`Syringe.h`提供了定义 Hook 的宏:
|
||||
```cpp
|
||||
#define EXPORT_FUNC(name) extern "C" __declspec(dllexport) DWORD __cdecl name (REGISTERS *R)
|
||||
|
||||
#define DEFINE_HOOK(hook, funcname, size) \
|
||||
declhook(hook, funcname, size) \
|
||||
EXPORT_FUNC(funcname)
|
||||
```
|
||||
|
||||
更详细的介绍可以翻 Zero Fanker 的 Ares Wiki。这里只需要知道,写 Hook 靠`DEFINE_HOOK`准没错。
|
||||
然后解释一下`DEFINE_HOOK`这个宏要补的三个参数:
|
||||
|
||||
- `hook`:即你要灌注(覆盖)的地址。
|
||||
|
||||
> 毕竟你外部定义的 Hook 不可能凭空插入原程序里,肯定需要遮掉原有的一部分指令机器码,才有机会跳转到你的 Hook。
|
||||
|
||||
- `funcname`:即你的 Hook 名字。
|
||||
|
||||
> [!warning]
|
||||
> 虽然 Hook 名字实际上就是 DLL 导出的函数名字,但并不推荐随性的命名。最好还是讲清楚你淦的原函数叫什么,或者你写这个 Hook 要做什么。
|
||||
|
||||
- `size`:即 Hook 覆盖多少字节的原函数指令码(bixv >= 5B)
|
||||
|
||||
::: info 简单提一嘴 Syringe 如何“灌注”Hook:
|
||||
完整版可以参考 Thomas 写的[高阶知识:Syringe 的工作原理](https://gitee.com/Zero_Fanker/Ares/wikis/%E9%AB%98%E9%98%B6%E7%9F%A5%E8%AF%86/Syringe%E7%9A%84%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86)。
|
||||
|
||||
浓缩版就是,向`hook`的地址那里写入`jmp`无条件跳转指令。由于`jmp`指令码本身占 1B,后面跟的虚拟地址总是占 4B,故`size`至少得是 5。
|
||||
那倘若要覆盖超过 5B 的机器代码呢?答案是多余部分用`nop`(空指令,什么也不做)填充。
|
||||
|
||||

|
||||
:::
|
||||
|
||||
### 注意事项
|
||||
|
||||
我们这里针对的是函数调用,需要注意 C++ 的函数执行完成后**会触发栈区局部变量的析构函数**(通常是空间回收),因此并不建议把传参的汇编指令也给覆盖掉。
|
||||
|
||||
就这个例子而言,只需覆盖`call`指令:
|
||||
|
||||
> [!tip]
|
||||
> 在 IDA 选项(`Options` > `General`)里,右上角勾选`Stack Pointer`,把`Number of opcode bytes`改为 8,确认即可看到机器码视图。
|
||||
> 然后你就会发现`call`指令刚好 5 个字节。
|
||||
>
|
||||
> 
|
||||
|
||||
### 实战
|
||||
|
||||
> 都有现成项目 FA2sp 了,你不会想着要白手起家吧?
|
||||
|
||||
在 FA2sp 项目里依次打开`FA2sp\Ext\CTriggerOption`,在`Hooks.cpp`里添一个 Hook:
|
||||
```cpp
|
||||
DEFINE_HOOK(501EAD, CTriggerOption_OnCBHouseChanged, 5)
|
||||
{
|
||||
// 这里什么都不用做,我们只是跳过 FA2 截断空格那一步而已。
|
||||
return 0x501EB2;
|
||||
}
|
||||
```
|
||||
由于`declhook`宏设置 Hook 位置时已经标了`0x`(可以在 Visual Studio 里把鼠标移到宏上面预览展开的代码),
|
||||
这里`DEFINE_HOOK`后面设置的地址就不需要再补`0x`了。
|
||||
|
||||
## 补充
|
||||
|
||||
### REGISTERS 寄存器类
|
||||
在上面的「背景芝士」中,注意到导出的 Hook 函数只有一个`REGISTERS`类的指针参数 R。
|
||||
有时我们会需要获取原函数的实参、局部变量等信息,并加以修改,这时就要靠 R 指针获取了:
|
||||
|
||||
[进阶知识:Hook 函数的用法](https://gitee.com/Zero_Fanker/Ares/wikis/%E8%BF%9B%E9%98%B6%E7%9F%A5%E8%AF%86/HOOK%E5%87%BD%E6%95%B0%E7%9A%84%E7%94%A8%E6%B3%95)
|
||||
|
||||
具体的例子还要结合已有的`idb`逆向成果自行意会。虽然函数调用的基本原理在计组那一块已有涉及,
|
||||
但一个函数叫什么名字、里面什么寄存器对应什么变量,这些都是前辈们自行逆向出来的结论。对此,咱还是保留点最起码的尊重罢。
|
BIN
docs/notes/RA2/bottle_kill_proc.webp
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
docs/notes/RA2/bottle_preferences.webp
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
docs/notes/RA2/bottles_main.webp
Normal file
After Width: | Height: | Size: 4.9 KiB |
BIN
docs/notes/RA2/bottles_new_venv.webp
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
docs/notes/RA2/ida_call_opcode.webp
Executable file
After Width: | Height: | Size: 26 KiB |
BIN
docs/notes/RA2/ida_find_calls.webp
Executable file
After Width: | Height: | Size: 80 KiB |
BIN
docs/notes/RA2/ida_graph_view.webp
Executable file
After Width: | Height: | Size: 112 KiB |
BIN
docs/notes/RA2/ida_recog_func_calls.webp
Executable file
After Width: | Height: | Size: 42 KiB |
BIN
docs/notes/RA2/linux_bottles_ES.webp
Normal file
After Width: | Height: | Size: 101 KiB |