Ikoct的饮冰室

你愿意和我学一辈子二进制吗?

0%

记一次Unity3D IL2CPP逆向

第一次做这个类型的题, 记录一下逆向流程和工具使用. 案例来源是NSSCTF2025的CodeRunner, U3D制作, IL2CPP打包的PE游戏.

游戏本身没有任何目标, 地面是一个QRcode, 但是跳跃高度有限无法观察到整个QRcode:

image-20251106153111701

由于边缘有空气墙同时无法暂停游戏, 无法通过CE修改坐标的方法来获取全貌, 接下来开始逆向分析.

global-metadata.dat前4个字节为AF 1B B1 FA, 和标准的metadata文件头相同, global-metadata.dat大概率没有进行加密, 接下来尝试直接提取资源文件看看能不能提取出地面材质.

提取资源

工具选择使用AssetRipper, 直接选择加载整个CodeRunner_Data文件夹, 在sharedassets1.assets中找到了黑白块材质:

image-20251106153900257

那么QRcode大概率不是直接储存在资源文件中而是在游戏逻辑中动态生成的, 下面就开始提取原项目符号和偏移

提取符号和偏移

用DIE打开GameAssembly.dll会发现加了UPX壳, 这种情况下工具是不能正常提取的, 题目附件中的壳修改了标志位, 手动改回UPX即可正常脱壳, 脱壳后使用Il2CppInspectorRedux提取符号和偏移, 打开项目的global-metadata.datGameAssembly.dll后先导出项目中所有符号, 每个类提取到一个文件中:

image-20251106154528718

然后在提取出的文件中可以发现有一个QRCodeBuilder类, 确定游戏确实是在运行过程中动态生成的QRcode:

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
29
30
31
/*
* Generated code file by Il2CppInspector - http://www.djkaty.com - https://github.com/djkaty
*/

using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using UnityEngine;

// Image 8: Assembly-CSharp.dll - Assembly: Assembly-CSharp, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null - Types 3762-3837

public class QRCodeBuilder : MonoBehaviour // TypeDefIndex: 3765
{
// Fields
public Material blackMaterial; // 0x20
public Material whiteMaterial; // 0x28
public float moduleSize; // 0x30
public Vector3 origin; // 0x34
public float thickness; // 0x40
private int[,] qrMatrix; // 0x48

// Constructors
public QRCodeBuilder(); // 0x00000001808118B0-0x0000000180811A60

// Methods
private void Awake(); // 0x0000000180810F60-0x0000000180810F90
private void Update(); // 0x0000000180811860-0x00000001808118B0
private void ConstructQRCode(); // 0x0000000180810F90-0x0000000180811500
private void CreateModule(bool isBlack, Vector3 localPosition, Transform parent); // 0x0000000180811500-0x0000000180811860
}

同时还能发现一个WinAPI.cs:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/*
* Generated code file by Il2CppInspector - http://www.djkaty.com - https://github.com/djkaty
*/

using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;

// Image 8: Assembly-CSharp.dll - Assembly: Assembly-CSharp, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null - Types 3762-3837

public static class WinAPI // TypeDefIndex: 3767
{
// Fields
private static string message; // 0x00
private const uint TH32CS_SNAPPROCESS = 2; // Metadata: 0x001C5AD0
private static readonly IntPtr INVALID_HANDLE_VALUE; // 0x08

// Nested types
private struct PROCESSENTRY32 // TypeDefIndex: 3766
{
// Fields
public uint dwSize; // 0x00
public uint cntUsage; // 0x04
public uint th32ProcessID; // 0x08
public IntPtr th32DefaultHeapID; // 0x10
public uint th32ModuleID; // 0x18
public uint cntThreads; // 0x1C
public uint th32ParentProcessID; // 0x20
public int pcPriClassBase; // 0x24
public uint dwFlags; // 0x28
public string szExeFile; // 0x30
}

// Constructors
static WinAPI(); // 0x0000000180839280-0x0000000180839390

// Methods
public static extern bool IsDebuggerPresent(); // 0x0000000180838E00-0x0000000180838EC0
public static extern bool CheckRemoteDebuggerPresent(IntPtr hProcess, out bool isDebuggerPresent); // 0x0000000180838190-0x0000000180838310
public static extern IntPtr GetCurrentProcess(); // 0x00000001808385B0-0x0000000180838650
private static extern IntPtr CreateToolhelp32Snapshot(uint dwFlags, uint th32ProcessID); // 0x00000001808383E0-0x00000001808384A0
private static extern bool Process32First(IntPtr hSnapshot, ref PROCESSENTRY32 lppe); // 0x0000000180838EC0-0x00000001808390A0
private static extern bool Process32Next(IntPtr hSnapshot, ref PROCESSENTRY32 lppe); // 0x00000001808390A0-0x0000000180839280
private static extern bool CloseHandle(IntPtr hObject); // 0x0000000180838310-0x00000001808383E0
private static string GetParentProcessName(); // 0x0000000180838650-0x0000000180838E00
public static void DetectDebugger(); // 0x00000001808384A0-0x00000001808385B0
public static void CheckParent(); // 0x0000000180837F60-0x0000000180838190
}

导入了很多用来检测调试器的WinAPI, 那么下面进行调试的时候就需要定位到反调试的逻辑并绕过.

接下来的任务就是分析GameAssembly.dll找到生成QRcode的逻辑并模拟产生一个QRcode.

GameAssembly.dll 分析

使用Il2CppInspectorRedux导出给IDA用的恢复符号的IDAPython脚本:

image-20251106154923253

IDA运行脚本后就能恢复符号了, 找到QRCodeBuilder::ConstructQRCode:

image-20251106155341416

其中的核心代码如上图, 使用嵌套循环来为坐标(j, k)构造QRcode, 关键是fields.qrmMatrix这个成员, v34储存了从这个矩阵中取出的值, 后续根据取出的是0还是1决定当前坐标的块是黑块还是白块, 这里选择直接调试获取到这个矩阵, 但是刚刚发现了游戏有反调试, 先找到WinAPI::DetectDebugger, 在函数头下断点, 进入函数时直接修改RIP跳到返回处, 然后在获取矩阵值处下条件断点输出矩阵值:

image-20251106160046598

用pillow生成QRcode:

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
29
from PIL import Image
import numpy as np

matrix_str = ''.join(map(str, [1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0,]))

length = len(matrix_str)
size = int(length ** 0.5)

matrix = [[int(matrix_str[y * size + x]) for x in range(size)] for y in range(size)]

module_size = 12
quiet_zone = 4
total_size = size + 2 * quiet_zone
img_size = total_size * module_size

img_array = np.ones((img_size, img_size, 3), dtype=np.uint8) * 255 # 白色背景

for y in range(size):
for x in range(size):
if matrix[y][x] == 1:
start_y = (quiet_zone + y) * module_size
end_y = start_y + module_size
start_x = (quiet_zone + x) * module_size
end_x = start_x + module_size
img_array[start_y:end_y, start_x:end_x] = [0, 0, 0]

img = Image.fromarray(img_array, 'RGB')
img.save('qrcode.png', 'PNG')
print(f"Saved at 'qrcode.png' (size: {img_size}x{img_size} px)")

image-20251106160303816