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

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

手动逆向

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

// 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,定位到位置,发现有这样的汇编:

.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 行会带上接口声明,将来再慢慢优化。