Mono

The Forest

The Forest 是 Steam 平台的一款生存类游戏, 示例代码1 中实现了如下几个功能:

  • 将玩家初始血量调满

  • 玩家拥有无限精力,跑步挥砍不消耗精力

  • 修改玩家奔跑速度为原速的 5 倍

  • 建筑物不需要材料直接完成建造

首先是修改玩家血量的实现,在 Assembly-CSharp 的 PlayerStats 类中有一个成员变量(以下简称属性)名为 Health。 因为 Health 不是静态变量,所以需要先在内存中找到 PlayerStats 类的实例地址,这里我们通过同类下的另一个属性 Energy 的值来定位类实例。

因为每次游戏开局玩家的外圈精力条(精力上限)都是 10.0(float 类型),所以在通过 guess_instance_address 方法取得所有可能的实例地址列表后,将它们逐一设置为 Energy 属性的类实例,通过 .value 访问其值,借此筛选出满足条件的实例地址。

以上描述反映到如下代码:

 1PlayerStats = ref.find_class_in_image("Assembly-CSharp", "PlayerStats")
 2field_energy = PlayerStats.find_field("Energy")
 3
 4# 通过条件筛选出类实例
 5found = False
 6addresses = PlayerStats.guess_instance_address()
 7for address in addresses:
 8    field_energy.instance = address
 9    if field_energy.value == 10.0:
10        PlayerStats.instance = address
11        found = True
12assert found, "Can't find PlayerStats instance"

在设置好 PlayerStats 类的实例后,通过 find_field 拿到 Health 属性, 它会自动继承所属类的实例地址,无需再去设置该属性的 instance 属性了, 直接通过 x.value = yx.set_value(y) 就可以设置该属性的值。

1field_health = PlayerStats.find_field("Health")
2field_health.value = 100.0

然后是让玩家具有无限精力。基于事先的逆向分析,我们可以知道对以下三处进行 nop 可以达到精力值与精力上限不减的效果:

  • FirstPersonCharacter 类 HandleRunningStaminaAndSpeed 方法偏移 0x112 处 8 字节

  • PlayerStats 类 setStamina 方法偏移 0x2e 处 8 字节

  • PlayerStats 类 Update 方法 0x382 处 6 字节

这些都可以通过 uniref 来完成:

 1field_health = PlayerStats.find_field("Health")
 2field_health.value = 100.0
 3
 4FirstPersonCharacter = ref.find_class_in_image("Assembly-CSharp", "FirstPersonCharacter")
 5
 6HandleRunningStaminaAndSpeed = FirstPersonCharacter.find_method("HandleRunningStaminaAndSpeed")
 7patch1 = HandleRunningStaminaAndSpeed.native_nop(0x112, 8)
 8
 9PlayerStats = ref.find_class_in_image("Assembly-CSharp", "PlayerStats")
10
11setStamina = PlayerStats.find_method("setStamina")
12patch2 = setStamina.native_nop(0x2e, 8)
13
14Update = PlayerStats.find_method("Update")
15patch3 = Update.native_nop(0x382, 6)

修改奔跑速度部分的代码展示了如何在 uniref 中使用自己书写的汇编进行 patch:

1FirstPersonCharacter = ref.find_class_in_image("Assembly-CSharp", "FirstPersonCharacter")
2
3HandleRunningStaminaAndSpeed = FirstPersonCharacter.find_method("HandleRunningStaminaAndSpeed")
4new_run_speed = ref.injector.new_double(40.0)
5
6code = f"movsd xmm0, [{hex(new_run_speed.address)}]             \n" \
7       f"jmp {hex(HandleRunningStaminaAndSpeed.address + 0x34e)}  "
8patch = HandleRunningStaminaAndSpeed.native_patch(0x167, code)

最后是超级建造功能的实现,写法同样是找到方法后对其中的代码进行 patch 和 nop:

1Craft_Structure = ref.find_class_in_image("Assembly-CSharp", "TheForest.Buildings.Creation.Craft_Structure")
2
3CheckNeeded = Craft_Structure.find_method("CheckNeeded")
4patch1 = CheckNeeded.native_patch(0xF, b"\xEB\x70")
5
6Initialize = Craft_Structure.find_method("Initialize")
7patch2 = Initialize.native_nop(0x183, 3)

SCTF2019 - Who is he

一道比赛的赛题,部分 wp 参考:

示例代码2 展示了如何用 uniref 定位真实的 EncryptDataencryptKey 属性并获取它们的值。您可以通过如下代码来推测哪个实例地址才是正确的:

 1ref = WinUniRef("Who is he.exe")
 2clazz = ref.find_class_in_image("UnityEngine.UmbraModule", "UnityEngine.UmbraModule.Main")
 3
 4encrypt_data = clazz.find_field("EncryptData")
 5addresses = clazz.guess_instance_address()
 6for address in addresses:
 7    encrypt_data.set_instance(address)
 8    cipher = encrypt_data.value
 9    if isinstance(cipher, str):
10        print(hex(address), cipher)

Mirror

Mirror 是 Steam 平台的一款三消游戏,该应用进程为 32 位。 示例代码3same_name_function_sample 函数展示了如何区分同类下的同名、同参数个数方法,以及如何调用方法。

首先找到 Enemy 类实例:

1Enemy = ref.find_class_in_image("Assembly-CSharp", "Enemy")
2instances = Enemy.guess_instance_address()
3
4CurHP = Enemy.find_field("<CurHP>k__BackingField")
5for instance in instances:
6    CurHP.instance = instance
7    if CurHP.value == 4000:
8        Enemy.instance = instance
9        break

Enemy 类中的 BrokeCloth 方法有两个重载,且它们的参数个数都是 1。 我们可以列出该类中的所有方法,再通过方法签名来找到目标方法:

1methods = Enemy.list_methods()
2for method in methods:
3    if method.name == "BrokeCloth" and method.signature == "System.Void (int level)":
4        method.instance = instance

经过逆向分析,可以得知该函数需要一个 int 参数且函数调用约定类似 cdecl,故可按如下方式调用:

method(args=(1,), call_type=CALL_TYPE_CDECL)

注:只有 32 位的应用才需要指定函数调用约定

IL2CPP

Goose Goose Duck

Goose Goose Duck 是 Steam 平台的一款狼人杀游戏, 示例代码4 中的 show_my_position 函数展示了如何通过事先分析的多级指针找到类实例。

WinMonoInjector 中提供了 get_module_base 方法,用于获取目标进程中指定模块的基址,可以结合 mem_read_multilevel_pointer 和偏移数组来读取类实例,代码如下:

1game_assembly_base = ref.injector.get_module_base("GameAssembly.dll")
2
3LocalPlayer = ref.find_class_in_image("Assembly-CSharp.dll", "Handlers.GameHandlers.PlayerHandlers.LocalPlayer")
4local_player_instance = ref.injector.mem_read_multilevel_pointer(game_assembly_base, [0x3BA7B38, 0xB8, 0])
5assert local_player_instance > 0, "Error multilevel pointer offsets due to game update"
6LocalPlayer.instance = local_player_instance

MRCTF2021 - EzGame

一道比赛的赛题,部分 wp 参考:

示例代码5 展示了如何用少量 Python 代码实现官方 wp 的解法。

在获取到关键类 GetFlag 后,直接通过 x.value = y 的形式来设置该类下的 goHome, findAlien, eatCookie 三个静态变量:

1ref = WinUniRef("GameHack.exe")
2class_GetFlag = ref.find_class_in_image("Assembly-CSharp.dll", "Platformer.Flag.GetFlag")
3class_GetFlag.find_field("goHome").value = True
4class_GetFlag.find_field("findAlien").value = True
5class_GetFlag.find_field("eatCookie").value = True

再获取 EatTokenUpdateKey 静态方法并直接调用:

1method_EatTokenUpdateKey = class_GetFlag.find_method("EatTokenUpdateKey")
2for i in range(105):
3    method_EatTokenUpdateKey()

您可以通过 field.is_static()method.is_static() 来判断属性和方法是否是静态的。 对于静态的属性,无需设置类实例即可查看/修改值;对于静态方法,无需设置类实例即可调用。