最近在做梆梆的脚本,这个游戏用 ProtoBuf 搭载通信数据。最开始的思路是先抓包知道 ProtoBuf 的 field_id,然后从逆向数据里得出 field_name,最后构建出 ProtoBuf。干了几天之后发现好累,而且有的时候数据包是不全的。

然后发现这个游戏的 ProtoBuf 是用 ProtoBuf-Net 这个库实现的,特点是每个字段都用 Attribute 标注字段序号,而不是官方的 protoc 根据 .proto 文件生成代码的形式。那么问题就简单了,只要能获取到 Attribute 就可以分析出数据。

手动逆向

Il2CppDumper 支持输出 Attribute,但是需要手动修改 config.json 打开。打开以后输出的数据能看到 Attribute,但是没有传给 constructor 的数值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Namespace: CE
[ProtoContractAttribute] // 0x1893B32
public class UserExchangesList // TypeDefIndex: 1663
{
// Fields
[DebuggerBrowsableAttribute] // 0x1893B66
[CompilerGeneratedAttribute] // 0x1893B66
private UserExchanges[] <entries>k__BackingField; // 0x8
// Properties
[ProtoMemberAttribute] // 0x1893C22
public UserExchanges[] entries { get; set; }
// Methods
public void .ctor(); // 0x14DA502
[CompilerGeneratedAttribute] // 0x1893BBA
public UserExchanges[] get_entries(); // 0x14DA531
[CompilerGeneratedAttribute] // 0x1893BEE
public void set_entries(UserExchanges[] value); // 0x14DA541
}

传给 constructor 的数值,实际上就位于类似 0x1893C22 的地方。用 IDA 打开 libil2cpp.so,定位到位置,发现有这样的汇编:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
.text:01893C22
.text:01893C22 ; =============== S U B R O U T I N E =======================================
.text:01893C22
.text:01893C22 ; Attributes: bp-based frame
.text:01893C22
.text:01893C22 sub_1893C22 proc near
.text:01893C22 ; __unwind {
.text:01893C22 push ebp
.text:01893C23 mov ebp, esp
.text:01893C25 push ebx
.text:01893C26 and esp, 0FFFFFFF0h
.text:01893C29 sub esp, 20h
.text:01893C2C call $+5
.text:01893C31 pop ebx
.text:01893C32 add ebx, 0A28877h
.text:01893C38 mov eax, [ebp+8]
.text:01893C3B mov eax, [eax+4]
.text:01893C3E mov eax, [eax]
.text:01893C40 mov [esp], eax
.text:01893C43 mov dword ptr [esp+8], 0
.text:01893C4B mov dword ptr [esp+4], 1
.text:01893C53 call ProtoMemberAttribute$$_ctor
.text:01893C58 lea esp, [ebp-4]
.text:01893C5B pop ebx
.text:01893C5C pop ebp
.text:01893C5D retn
.text:01893C5D ; } // starts at 1893C22
.text:01893C5D sub_1893C22 endp

这个 call ProtoMemberAttribute$$_ctor 就是 ProtoMemberAttribute 的构造函数调用。上一行的 1,是传递过去的第二个参数,经分析发现和 field_id 一致,那么它就是 field_id

mov dword ptr [esp+4], 1 对应的 HEX 是 C7 44 24 04 01 00 00 00,第五位对应数字 1。那么写一个状态机就可以把它提取出来。

编写逆向工具

从函数起始的 0x1893C22 位开始搜索,状态由 OTHERS -> C7 -> 44 -> 24 -> 04 流动,当状态进入 04 时说明下一位是 field_id,如果不符合这样的模式,说明不是我们要找的指令,设置状态回 OTHERS

知道了 field_id 的计算方法,一个个手动填入也挺麻烦的,不如直接生成进 C# 文件中。

最简单的方法是直接修改 Il2CppDumper 的代码,但我实在是读不懂他的代码,只好退而求其次,自己写一个修改 dump.cs 的工具。

幸好 dump.cs 够简单,所以就不用生成 AST 了(其实我也不会)。还是用状态机。

遇到 [ProtoContractAttribute] 进入 CLASS 状态,再遇到 } 起始的行退出 CLASS 进入 NOT_CLASS

CLASS 状态,遇到 public class 就输出当前行;遇到 // Property 进入 PROPERTY

PROPERTY 状态,遇到 [ProtoMemberAttribute] 就提取后面的 HEX,保存起来;遇到 public 起始的行,输出对应的 Property 声明和提取的 Attribute 描述(从之前保存的 HEX 计算);遇到 // MethodsPROPERTY 转移到 CLASS

状态机比写一堆正则好用。

这里留下了一个 bug,输出 public class 行会带上接口声明,将来再慢慢优化。