最近在做梆梆的脚本,这个游戏用 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 计算);遇到 // Methods
从 PROPERTY
转移到 CLASS
。
状态机比写一堆正则好用。
这里留下了一个 bug,输出 public class
行会带上接口声明,将来再慢慢优化。