0x0 逆向前的准备 (本文中引用的代码基于UE4.27)
笔者是第一次接触虚幻引擎相关的逆向,所以这里先速览一下基础知识
先快速看一眼UE5的文档,核心的开发模型应该变化不会太大。
UE5文档速览 直接看虚幻引擎术语 。这里摘要自认为重要的地方
虚幻引擎术语
对象 Object 是虚幻引擎中最基本的类。虚幻引擎中的几乎所有功能都继承自object(或使用其中的部分功能)。
在C++中,UObject
是所有object的基类,可以实施多种功能,例如垃圾回收、用于将变量提供给虚幻编辑器的元数据(UProperty
)支持以及用于加载和保存的序列化。
Actor Actor 是可以放到关卡中的任何object,例如摄像机、静态网格体或玩家出生点位置。
在C++中,AActor
是所有Actor的基类。
Pawn Pawn 是Actor的子类,作为游戏内的形象或人像(例如游戏中的角色)。玩家或游戏的AI可以控制Pawn,将其作为非玩家角色(NPC)。
角色 角色(Character) 是计划用作玩家角色的Pawn Actor的子类。角色子类包括碰撞设置、双足运动的输入绑定以及用于玩家控制动作的其他代码。
玩家状态 玩家状态(Player State) 是游戏参与者在游戏中的状态,例如人类玩家或模拟玩家的机器人。非玩家AI作为游戏世界的一部分而存在,没有玩家状态。
玩家状态可能包含的玩家信息示例包括:
关联的C++类是 PlayerState
。
体积 体积(Volumes) 是带有边界的3D空间,根据连接到体积的效果,具有不同的使用方法。例如:
阻挡体积(Blocking Volumes) 是可见的,用于阻止Actor通过它们。
施加伤害体积(Pain Causing Volume) 对与其重叠的任何Actor造成持续伤害。
触发器体积(Trigger Volumes) 的编程方式为,在Actor进入或退出体积时触发事件
Actor 的文档
Actor
玩家出生点 玩家出生点(Player Start) 是放置在关卡中的一种Actor,用于指定在玩家开始关卡时,玩家角色在何处生成。
阻挡体积 顾名思义,阻挡体积(Blocking Volumes) 用于防止玩家穿过。例如,你可以使用阻挡体积不让玩家从游戏世界的边缘掉落。
以及命名约定
命名约定
类型名称以额外的大写字母为前缀,以区别于变量名称。例如,FSkin
是类型名称,Skin
是 FSkin
类型的实例。
模板类以 T 为前缀。
从 UObject 继承的类以 U 为前缀。
从 AActor 继承的类以 A 为前缀。
从 SWidget 继承的类以 S 为前缀。
作为抽象接口的类以 I 为前缀。
枚举以 E 为前缀。
布尔变量必须以 b 为前缀。
大多数其他类都以 F 为前缀
也就是说游戏里所有的实体都是一个actor
。想实现修改首先需要找到对应的actor。那么该如何找到呢?
UE4内部实现
(以下基于UE4.27)
先编译一下UE4的源码,方便后续分析:https://dev.epicgames.com/documentation/zh-cn/unreal-engine/building-unreal-engine-from-source?application_version=4.27
官方文档中给出了将源码导入到visual studio的方法。导入后可以更直观的分析类之间的关系和计算类成员变量的偏移 。注意要把平台设置到android
这里需要先了解一点虚幻引擎的内部实现。UE4的大体框架如下:
(图片来源:https://renyili.org/post/game_cheat2/)
其中:
UGameInstance
类,对应UE中最顶层的Game概念,在游戏创建时产生,直到游戏实例关闭时才被销毁,它通过WorldContext (图中未表现)管理着游戏中的多个世界(World) 。一般来说,全局唯一。
UWorld
类,对应引擎中的世界(World)概念,它包含了游戏中的所有关卡(Level) 。它可以处理关卡流送,还能生成(创建)动态Actor。
ULevel
类,对应引擎中的**关卡(Level)**概念,它是用户定义的游戏区域。关卡包含了玩家能看到的所有内容,例如几何体、Pawn和Actor。
AActor
类,对应Actor 概念,所有可以放入关卡 的对象都是 Actor ,无论是实体还是非实体,比如摄像机、静态网格体。
USceneComponent
类,AActor
本身不包含位置信息,UE将其封装到了UScaneComponent
中,作为AActor
的RootComponent
成员。
APawn
类,对应Pawn 概念,它是Actor的子类,可以与玩家发生交互的称之为Pawn,它可以由玩家操控,也可以由游戏AI控制并以非玩家角色(NPC)的形式存在于游戏中。
AShooterCharacter
类,该类是ShooterGame游戏中的类,非UE引擎中定义的类。该类对应**角色(Character)**概念,它是ACharacter
的子类(图中未展示),而ACharacter
又是APawn
的子类。ACharacter
旨在表示所有的人形的带骨骼的Pawn。
APlayerController
类,对应**玩家控制器(Player Controller)**概念,它会获取游戏中玩家的输入信息,然后转换为交互效果,每个游戏中至少有一个玩家控制器。玩家控制器通常会控制一个Pawn或Character,将其作为玩家在游戏中的化身。
APlayerState
类,存储Player的一些状态信息。
想要做修改,需要先找到对应的Actor。可以通过UWorld→ULevel PersistentLevel→TArray<AActor*> Actors
来遍历所有Actor。UE4中实际是用一个全局变量UWorldProxy GWorld;
来指向当前World
。其内部结构:
1 2 3 4 5 6 class UWorldProxy { ... private : UWorld* World; }
也就是说,逆向过程中只要确定了GWorld
的地址就可以找到所有Actor
。只需要再计算一下偏移:
首先直接*GWorld
,获取到UWorld
对象
UWorld简化结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 class UWorld { ... private : Level* PersistentLevel; NetDriver* NetDriver; LineBatchComponent* LineBatcher; LineBatchComponent* PersistentLineBatcher; LineBatchComponent* ForegroundLineBatcher; GameNetworkManager* NetworkManager; PhysicsCollisionHandler* PhysicsCollisionHandler; ... }
那么*(UWorld+0x30)
即可获取到ULevel* PersistentLevel
ULevel
实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class ULevel : public UObject, public IInterface_AssetUserData, public ITextureStreamingContainer, public IEditorPathObjectInterface{ GENERATED_BODY () public : FURL URL; TArray<TObjectPtr<AActor>> Actors; TArray<TObjectPtr<AActor>> ActorsForGC; ENGINE_API bool TryAddActorToList (AActor* InActor, bool bAddUnique) ; ... } template <typename InElementType, typename InAllocatorType>class TArray { ... protected : ElementAllocatorType AllocatorInstance; SizeType ArrayNum; SizeType ArrayMax; ... }
那么到这里我们可以通过ULevel+0x98
来获取Actors
。即**Actors = *(*GWorld+0x30)+0x98
**
获取到所有Actor
后如何判断哪个是我们想要的呢?可以通过FName
来判断。
FName
是UObject
的成员变量,用来保存tag的逻辑名称。可以通过观察actor
的逻辑名称来判断是不是我们想控制的actor
。
UObject
的成员变量如下:
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 50 51 52 53 54 class UObject : public UObjectBaseUtility{ enum class ENetFields_Private { NETFIELD_REP_START = 0 , NETFIELD_REP_END = -1 }; struct FAssetRegistryTag { enum ETagType { TT_Hidden, TT_Alphabetical, TT_Numerical, TT_Dimensional, TT_Chronological, }; enum ETagDisplay { TD_None = 0 , TD_Date = 1 <<0 , TD_Time = 1 <<1 , TD_InvariantTz = 1 <<2 , TD_Memory = 1 <<3 , }; FName Name; FString Value; ETagType Type = TT_Alphabetical; uint32 DisplayFlags = TD_None; }; }
UObject是几乎所有其他类的基类。所以在其他类中,FName的偏移统一是0x30
实际上FName
并不保存字符串,只是保存了字符串在内存池中的位置:
1 2 3 4 5 6 7 8 9 10 11 12 class FName { ... private : FNameEntryId ComparisonIndex; uint32 Number; FNameEntryId DisplayIndex; }
实际的字符串内容由FNameEntry
来保存
1 2 3 4 5 6 7 8 9 10 11 12 struct FNameEntry { private : FNameEntryHeader Header; union { ANSICHAR AnsiName[NAME_SIZE]; WIDECHAR WideName[NAME_SIZE]; FNumberedData NumberedName; }; ... }
所有的FNameEntry
的指针全都保存在FNameEntryAllocator
管理的Blocks
中:
1 2 3 4 5 6 7 8 9 10 11 12 class FNameEntryAllocator { public : enum { Stride = alignof (FNameEntry) }; enum { BlockSizeBytes = Stride * FNameBlockOffsets }; private : mutable FRWLock Lock; uint32 CurrentBlock = 0 ; uint32 CurrentByteCursor = 0 ; uint8* Blocks[FNameMaxBlocks] = {}; }
这里FRWLock
在android
下实际是pthread_rwlock_t
,32位下是0x28,64位下是0x38
而FNameEntryAllocator
封装在FNamePool
中。实际上是通过一个全局变量FNamePool GName
来管理。GName
保存在alignas(FNamePool) static uint8 NamePoolData[sizeof(FNamePool)];
。也就是说,找到GWorld
后可以找到所有Actor
以及Actor
的FName
,找到GName
后就能找到所有的FNameEntry
。
1 2 3 4 5 6 7 8 9 10 11 class FNamePool { private : enum { MaxENames = 512 }; FNameEntryAllocator Entries; alignas (PLATFORM_CACHE_LINE_SIZE) FNameEntryId ENameToEntry[(uint32)EName::MaxHardcodedNameIndex] = {}; uint32 LargestEnameUnstableId; TMap<FNameEntryId, EName, TInlineSetAllocator<MaxENames>> EntryToEName; };
那么如何将FName
和FNameEntry
一一对应呢?观察FName
中的方法GetComparisonNameEntry
:
1 2 3 4 5 const FNameEntry* FName::GetComparisonNameEntry () const { return ResolveEntryRecursive (GetComparisonIndexInternal ()); }
GetComparisonNameEntry
调用的GetComparisonIndexInternal
直接返回FName
的成员变量FNameEntryId ComparisonIndex
。其中FNameEntryId
的成员变量为uint32 Value;
1 2 3 4 5 6 7 8 9 10 11 12 FORCEINLINE FNameEntryId GetComparisonIndexInternal () const { return ComparisonIndex; } struct FNameEntryId { ... private : uint32 Value; ... };
继续跟进ResolveEntryRecursive
1 2 3 4 5 const FNameEntry* FName::ResolveEntryRecursive (FNameEntryId LookupId) { const FNameEntry* Entry = ResolveEntry (LookupId); return Entry; }
继续跟进ResolveEntry
。这里它调用Resolve
的过程做了一次类型转换,将FNameEntryId
转换成了FNameEntryHandle
。将uint32 Value
的高16位和低16位分别赋给了Block
和Offset
,然后根据这两个值从Blocks
中获取到对应的FNameEntry
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const FNameEntry* FName::ResolveEntry (FNameEntryId LookupId) { return &GetNamePool ().Resolve (LookupId); } FNameEntryHandle (FNameEntryId Id) : Block (Id.ToUnstableInt () >> FNameBlockOffsetBits) , Offset (Id.ToUnstableInt () & (FNameBlockOffsets - 1 )) { } FNameEntry& Resolve (FNameEntryHandle Handle) const { return *reinterpret_cast <FNameEntry*>(Blocks[Handle.Block] + Stride * Handle.Offset); }
也就是说,当我们获取到了Actor
的FName
后,就可以根据它的ComparisonIndex
来找到相应的FNameEntry
来找到对应的字符串。
0x1 开始逆向 确定UE4版本,定位GUObjectArray GWorld GName
ida打开libUE4,搜索可以看到版本是4.27
GUObjectArray 接下来定位GUObjectArray
。在源码中可以发现以下模式:
跟踪调用发现:
所以在ida中搜索到Max UObject count is invalid. It must be a number that is greater than 0.
所在的函数的第一个参数就是GUObjectArray
。最终偏移是0xB1B5F98
GName ida搜索ByteProperty
,查找引用可以找到GName的初始化函数
查找引用。调用这个函数传的参数就是GName。偏移是0xB171CC0
GWorld 观察world.cpp
源码,可以发现以下模式
ida搜索SeamlessTravel FlushLevelStreaming
,找到
往上翻
再往上翻
所以GWorld的地址是0xB32D8A8
有了这三个的地址就可以确定到需要的actor了。接下来用ue4dumper
来dump出sdk方便后续操作。
下载ue4dumper上传到手机上:https://github.com/kp7742/UE4Dumper/tree/master
1 2 3 4 5 6 7 8 ./ue4dumper64 --package com.tencent.ace.match2024 --newue+ --sdkw --gworld 0xB32D8A8 --gname 0xB171CC0 --output /data/local/tmp/dumpSDK ./ue4dumper64 --package com.tencent.ace.match2024 --newue+ --objs --gname 0xB171CC0 --guobj 0xB1B5F98 --output /data/local/tmp/dumpSDK ./ue4dumper64 --package com.tencent.ace.match2024 --newue+ --actors --gworld 0xB32D8A8 --gname 0xB171CC0 > /data/local/tmp/dumpSDK/actors.txt
0x2 解题 section0 先把偏移和找actor相关的方法写了
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 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 class UE4Reverse { constructor (GNameOffset, GWorldOffset ) { this .GNameOffset = GNameOffset ; this .GWorldOffset = GWorldOffset ; this .moduleName = "libUE4.so" ; this .moduleBase = Module .findBaseAddress (this .moduleName ); if (!this .moduleBase ) { console .error (`[!] 模块 ${this .moduleName} 未加载` ); return ; } this .GWorld = this .moduleBase .add (GWorldOffset ).readPointer (); console .log ("moduleBase: " + this .moduleBase .toString (16 )); console .log ("GWorld Offset: " + GWorldOffset .toString (16 )); console .log ("GWorld: " + this .GWorld .toString (16 )); this .GName = this .moduleBase .add (GNameOffset ); this .FNameEntryAllocator = this .GName .add (0x30 ); this .Blocks = this .FNameEntryAllocator .add (0x10 ); this .GLevel = this .GWorld .add (0x30 ).readPointer (); this .Actors = this .GLevel .add (0x98 ).readPointer (); this .ActorNum = this .GLevel .add (0x98 ).add (0x8 ).readU32 (); } getNameById (ComparisonIndex ) { var BlockId = ComparisonIndex >> 16 ; var Offset = ComparisonIndex & 65535 ; var currentBlock = this .Blocks .add (8 * BlockId ).readPointer (); var FNameEntry = currentBlock.add (2 * Offset ); var FNameEntryHeader = FNameEntry .readU16 (); var str_length = FNameEntryHeader >> 6 ; var wide = FNameEntryHeader & 1 ; var str_addr = FNameEntry .add (0x2 ); if (wide) { return "wide_string" ; } if (str_length > 0 && str_length < 250 ) { var str = str_addr.readUtf8String (str_length); return str; 0xadf07c0 ; } else { return "None" ; } } getActorByName (actorName ) { for (var i = 0 ; i < this .ActorNum ; i++) { var Actor = this .Actors .add (i * 8 ).readPointer (); var ComparisonIndex = Actor .add (0x18 ).readU32 (); var Name = this .getNameById (ComparisonIndex ); if (Name == actorName) { console .log ("return Actor " + actorName); return Actor ; } } console .log ("can't find Actor:" + actorName); return "None" ; } getNameByActor (actor ) { var ComparisonIndex = actor.add (0x18 ).readU32 (); var Name = this .getNameById (ComparisonIndex ); return Name ; } printAllActorName ( ) { for (var i = 0 ; i < this .ActorNum ; i++) { var Actor = this .Actors .add (i * 8 ).readPointer (); var ComparisonIndex = Actor .add (0x18 ).readU32 (); var Name = this .getNameById (ComparisonIndex ); console .log (`[!] actor ${i} Name: ${Name} ` ); } } forAllActor (func ) { for (var i = 0 ; i < this .ActorNum ; i++) { var Actor = this .Actors .add (i * 8 ).readPointer (); func (Actor ); } } }
首先是出门。碰到墙就会重置到原点。推测可能是撞墙后扣血了?
这里想到的方法:
这里主要尝试了改扣血函数和改血量。
改血量: 观察dump出的sdk中,FirstPersonCharacter_C
存在变量float 生命值;//[Offset: 0x510, Size: 0x4]
。用frida直接将其改成一个比较大的数字即可。
扣血函数: 观察dump出的sdk可以看到FirstPersonCharacter_C
有一个方法void ReceiveHit(...);// 0x5e93094
跟进可以看到它调用了虚表中偏移为0x8b0的函数
用frida来dump出虚表的地址是0xa86e1f0
函数的地址是0x5e843d4
1 2 3 4 5 const ue4Reverse = new UE4Reverse (0xb171cc0 , 0xb32d8a8 );const user = ue4Reverse.getActorByName ("FirstPersonCharacter_C" );const table = user.readPointer ()console .log ("table addr:" + (table - ue4Reverse.moduleBase ).toString (16 ));console .log ("receive hit addr:" + (table.add (0x8b0 ).readPointer () - ue4Reverse.moduleBase ).toString (16 ));
跟进可以看到
把扣血改掉即可。
section1 关于瞬移(主要用于飞到天上看flag):
尝试调用AddInputVector
,失败
尝试调用CharacterMovementComponent.addForce
失败
尝试调用Actor.K2_SetActorLocation
,成功
测试过程中推测K2_SetActorLocation
本质上是将角色平移到对应的坐标。如果两个坐标的连线上有障碍物的话会被拦截。
隐藏的flag:
对所有actor
的rootComponent
调用SetVisibility(1,1)
,成功
疑问:再次调用SetVisibility(0,0)
,只是变成了黑色,物块依旧可见
得到第一段flag:8939
section2 让物块变得不可穿透:
主要试了以下几种方案:
对所有actor
调用actor -> setActorEnableCollision
没用
对所有actor
的rootComponent
调用PrimitiveComponent -> setSimulatePhysics
。开启后人动不了了。关闭后会出现很奇怪的问题。
对所有actor
的rootComponent
调用PrimitiveComponent -> SetCollisionEnabled
开启后人动不了了。关闭后出现了奇怪的问题。
对所有名字里带cube
的actor
调用actor
-> setActorEnableCollision
和 PrimitiveComponent -> setSimulatePhysics
也不行
对所有名字里带cube
的actor
调用PrimitiveComponent -> SetCollisionEnabled
,成功。撞击三个箱子后天上出现flag
得到第二段flag:008
section3 提示是藏在小球所在的类里面。
那么第一个问题是如何找到小球所在的类。我这里用的方法是站在小球前面,然后计算了所有actor和我的距离。离我最近的actor就是小球。
最终定位小球的名字actor,对应的类是MyActor
sdk里找到:
1 2 3 Class: MyActor.Actor.Object bool getlastflag () ;
跟进看到这里调用了libplay.so
中的get_last_flag
方法。
继续跟进,可以看到这里用了br混淆,导致ida没法正确识别控制流。
其实这段代码不长,直接把汇编扔给deepseek就能还原,但本着学习的态度尝试一下去混淆。
去混淆思路: 先unidbg trace一下,观察一下模式:
1 2 3 4 5 6 7 8 [libplay.so 0x013b8] [5f0113eb] 0x120013b8: "cmp x10, x19" x19=0x12 => nzcv: N=1, Z=0, C=0, V=0 x10=0x5 [libplay.so 0x013bc] [8f020c8b] 0x120013bc: "add x15, x20, x12" x20=0x12358000 x12=0x3 => x15=0x12358003 [libplay.so 0x013c0] [eb279f1a] 0x120013c0: "cset w11, lo" nzcv: N=1, Z=0, C=0, V=0 => w11=0x1 [libplay.so 0x013c4] [10023691] 0x120013c4: "add x16, x16, #0xd80" x16=0x12003000 => x16=0x12003d80 [libplay.so 0x013c8] [ef054039] 0x120013c8: "ldrb w15, [x15, #1]" x15=0x12358003 => w15=0x38 [libplay.so 0x013cc] [105a6bf8] 0x120013cc: "ldr x16, [x16, w11, uxtw #3]" x16=0x12003d80 w11=0x1 => x16=0x12001354 [libplay.so 0x013d0] [ce210f2a] 0x120013d0: "orr w14, w14, w15, lsl #8" w14=0x740000 w15=0x38 => w14=0x743800 [libplay.so 0x013d4] [00021fd6] 0x120013d4: "br x16" x16=0x12001354
1 2 3 [libplay.so 0x01438] [ea279f1a] 0x12001438: "cset w10, lo" nzcv: N=1, Z=0, C=0, V=0 => w10=0x1 [libplay.so 0x0143c] [2a596af8] 0x1200143c: "ldr x10, [x9, w10, uxtw #3]" x9=0x12003d60 w10=0x1 => x10=0x12001314 [libplay.so 0x01440] [40011fd6] 0x12001440: "br x10" x10=0x12001314
主要就是这一种模式:
1 2 3 4 5 cset reg0, flag ... ldr reg1, xxx ... br reg1
这里的思路是将ldr向下挪动,移动到br上方,变成
1 2 3 4 5 6 cset reg0, flag ins1 ... ... ldr reg1, xxx br reg1
再把ldr替换成b.flag addr
,将br
替换成br addr
,即变成
1 2 3 4 5 6 cset reg0, flag ins1 ... ... b.flag addr1 br addr2
这里使用unidbg来去混淆。具体思路是,通过一个长度为10的队列来保存最近执行的十条指令和上下文。
考虑当遇到br时:
向上搜索最近的cset并记录条件
向上搜索距离最近的ldr
读取ldr对应地址处偏移为0和1的两个地址。1为条件跳转目的地,0为无条件跳转目的地。并记录
将ldr操作和br之间的操作向上移动
将ldr根据cset的条件修改成条件跳转
将br跳转地址固定
例如,如下指令
1 2 3 4 5 6 7 8 [libplay.so 0x013b8] [5f0113eb] 0x120013b8: "cmp x10, x19" x19=0x12 => nzcv: N=1, Z=0, C=0, V=0 x10=0x5 [libplay.so 0x013bc] [8f020c8b] 0x120013bc: "add x15, x20, x12" x20=0x12358000 x12=0x3 => x15=0x12358003 [libplay.so 0x013c0] [eb279f1a] 0x120013c0: "cset w11, lo" nzcv: N=1, Z=0, C=0, V=0 => w11=0x1 [libplay.so 0x013c4] [10023691] 0x120013c4: "add x16, x16, #0xd80" x16=0x12003000 => x16=0x12003d80 [libplay.so 0x013c8] [ef054039] 0x120013c8: "ldrb w15, [x15, #1]" x15=0x12358003 => w15=0x38 [libplay.so 0x013cc] [105a6bf8] 0x120013cc: "ldr x16, [x16, w11, uxtw #3]" x16=0x12003d80 w11=0x1 => x16=0x12001354 [libplay.so 0x013d0] [ce210f2a] 0x120013d0: "orr w14, w14, w15, lsl #8" w14=0x740000 w15=0x38 => w14=0x743800 [libplay.so 0x013d4] [00021fd6] 0x120013d4: "br x16" x16=0x12001354
最后被修改为
1 2 3 4 5 6 7 8 [libplay.so 0x013b8] [5f0113eb] 0x120013b8: "cmp x10, x19" x19=0x12 => nzcv: N=1, Z=0, C=0, V=0 x10=0x5 [libplay.so 0x013bc] [8f020c8b] 0x120013bc: "add x15, x20, x12" x20=0x12358000 x12=0x3 => x15=0x12358003 [libplay.so 0x013c0] [eb279f1a] 0x120013c0: "cset w11, lo" nzcv: N=1, Z=0, C=0, V=0 => w11=0x1 [libplay.so 0x013c4] [10023691] 0x120013c4: "add x16, x16, #0xd80" x16=0x12003000 => x16=0x12003d80 [libplay.so 0x013c8] [ef054039] 0x120013c8: "ldrb w15, [x15, #1]" x15=0x12358003 => w15=0x38 [libplay.so 0x013d0] [ce210f2a] 0x120013d0: "orr w14, w14, w15, lsl #8" w14=0x740000 w15=0x38 => w14=0x743800 [libplay.so 0x013cc] [105a6bf8] 0x120013cc: "b.lo addr" [libplay.so 0x013d4] [00021fd6] 0x120013d4: "br addr" x16=0x12001354
去混淆后再看,逻辑就清楚多了。这其中还有一些间接跳转可以优化。这里主要就是一个异或加密和一个换表的base64。
xor的密码:0A 0C 0E 00 51 16 27 38 49 1A 3B 5C 2D 4E 6F FA FC FE
base64表:ACE0BDFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz123456789+/
target:UT1fc0gIYDArdz80Z0Xem46J
解密脚本 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 import base64 # 自定义base64解码 custom_table = 'ACE0BDFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz123456789+/' custom_dict = {c: i for i, c in enumerate(custom_table)} def custom_b64decode(s): # 处理填充字符 s += '=' * ((4 - len(s) % 4 ) % 4 ) binary_str = [] for c in s: if c == '=' : binary_str.append('000000' ) continue binary_str.append(f"{custom_dict[c]:06b}" ) # 拼接二进制并转换为字节 bit_stream = ''.join(binary_str) bytes_list = [int(bit_stream[i:i+8], 2) for i in range(0, len(bit_stream), 8) if i+8 <= len(bit_stream)] return bytes(bytes_list) # 异或解密 def xor_decrypt(data, key): key_len = len(key) return bytes([data[i] ^ key[i % key_len] for i in range(len(data))]) if __name__ == "__main__": encoded_str = "UT1fc0gIYDArdz80Z0Xem46J" result1 = custom_b64decode(encoded_str) key = bytes.fromhex("0A0C0E0051162738491A3B5C2D4E6FFAFCFE") result2 = xor_decrypt(result1, key) print("最终结果:", result2.decode("utf-8", errors="replace"))
得到第三段flag:_Anti_Cheat_Expert
至此得到最终flag:8939008_Anti_Cheat_Expert
脚本 frida脚本: 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 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 class UE4Reverse { constructor (GNameOffset, GWorldOffset ) { this .GNameOffset = GNameOffset ; this .GWorldOffset = GWorldOffset ; this .moduleName = "libUE4.so" ; this .moduleBase = Module .findBaseAddress (this .moduleName ); if (!this .moduleBase ) { console .error (`[!] 模块 ${this .moduleName} 未加载` ); return ; } this .GWorld = this .moduleBase .add (GWorldOffset ).readPointer (); console .log ("moduleBase: " + this .moduleBase .toString (16 )); console .log ("GWorld Offset: " + GWorldOffset .toString (16 )); console .log ("GWorld: " + this .GWorld .toString (16 )); this .GName = this .moduleBase .add (GNameOffset ); this .FNameEntryAllocator = this .GName .add (0x30 ); this .Blocks = this .FNameEntryAllocator .add (0x10 ); this .GLevel = this .GWorld .add (0x30 ).readPointer (); this .Actors = this .GLevel .add (0x98 ).readPointer (); this .ActorNum = this .GLevel .add (0x98 ).add (0x8 ).readU32 (); } getNameById (ComparisonIndex ) { var BlockId = ComparisonIndex >> 16 ; var Offset = ComparisonIndex & 65535 ; var currentBlock = this .Blocks .add (8 * BlockId ).readPointer (); var FNameEntry = currentBlock.add (2 * Offset ); var FNameEntryHeader = FNameEntry .readU16 (); var str_length = FNameEntryHeader >> 6 ; var wide = FNameEntryHeader & 1 ; var str_addr = FNameEntry .add (0x2 ); if (wide) { return "wide_string" ; } if (str_length > 0 && str_length < 250 ) { var str = str_addr.readUtf8String (str_length); return str; 0xadf07c0 ; } else { return "None" ; } } getActorByName (actorName ) { for (var i = 0 ; i < this .ActorNum ; i++) { var Actor = this .Actors .add (i * 8 ).readPointer (); var ComparisonIndex = Actor .add (0x18 ).readU32 (); var Name = this .getNameById (ComparisonIndex ); if (Name == actorName) { console .log ("return Actor " + actorName); return Actor ; } } console .log ("can't find Actor:" + actorName); return "None" ; } getNameByActor (actor ) { var ComparisonIndex = actor.add (0x18 ).readU32 (); var Name = this .getNameById (ComparisonIndex ); return Name ; } printAllActorName ( ) { for (var i = 0 ; i < this .ActorNum ; i++) { var Actor = this .Actors .add (i * 8 ).readPointer (); var ComparisonIndex = Actor .add (0x18 ).readU32 (); var Name = this .getNameById (ComparisonIndex ); console .log (`[!] actor ${i} Name: ${Name} ` ); } } forAllActor (func ) { for (var i = 0 ; i < this .ActorNum ; i++) { var Actor = this .Actors .add (i * 8 ).readPointer (); func (Actor ); } } } function setHP (user, HP ) { user.add (0x510 ).writeFloat (HP ); } function getCharacterMovementComponent (user ) { var characterMovementComponent = user.add (0x288 ).readPointer (); return characterMovementComponent; } function setActorLocation (user, x, y, z ) { const K2_SetActorLocationAddr = ue4Reverse.moduleBase .add (0x8c3181c ); const K2_SetActorLocation = new NativeFunction ( K2_SetActorLocationAddr, "void" , ["pointer" , "bool" , "pointer" , "bool" , "float" , "float" , "float" ] ); const buf = Memory .alloc (0x100 ); K2_SetActorLocation (user, 1 , buf, 1 , x, y, z); } function getActorLoaction (user, log=true ) { const getActorLoactionAddr = ue4Reverse.moduleBase .add (0x965ddf8 ); const getActorLoaction = new NativeFunction ( getActorLoactionAddr, "pointer" , ["pointer" , "pointer" , "pointer" ] ); const buf = Memory .alloc (0x100 ); getActorLoaction (user, buf, buf); var x = buf.readFloat (); var y = buf.add (4 ).readFloat (); var z = buf.add (8 ).readFloat (); if (log){ console .log ("x: " + x + " y: " + y + " z: " + z); } return { x : x, y : y, z : z }; } function SetVisibility (actor ) { const setVisibilityAddr = ue4Reverse.moduleBase .add (0x8e619bc ); const setVisibility = new NativeFunction (setVisibilityAddr, "void" , [ "pointer" , "bool" , "bool" , ]); const rootComponent = actor.add (0x130 ).readPointer (); console .log ("rootComponent: " + rootComponent.toString (16 )); if (rootComponent != 0 ) { setVisibility (rootComponent, 1 , 1 ); } } function setActorEnableCollision (actor ) { const name = ue4Reverse.getNameByActor (actor); if (!name.includes ("Cube" )){ return } const setActorEnableCollisionAddr = ue4Reverse.moduleBase .add (0x8c21320 ); const setActorEnableCollision = new NativeFunction (setActorEnableCollisionAddr, "void" , ["pointer" , "bool" ]); setActorEnableCollision (actor, 1 ); const rootComponent = actor.add (0x130 ).readPointer (); if (rootComponent != 0 ) { if (rootComponent.readPointer ().add (0x5d0 ) != 0 ) { var setSimulatePhysicsAddr = rootComponent.readPointer ().add (0x5d0 ).readPointer (); var setSimulatePhysics = new NativeFunction (setSimulatePhysicsAddr, "void" , ["pointer" , "bool" ]); try { console .log ("setSimulatePhysics" ); setSimulatePhysics (rootComponent, 0 ); } catch (e) { console .error ("error: " + e); } } } } function setCollisionEnabled (actor ) { const name = ue4Reverse.getNameByActor (actor); if (!name.includes ("Cube" )){ return } console .log ("name: " + name); const rootComponent = actor.add (0x130 ).readPointer (); console .log ("rootComponent: " + rootComponent.toString (16 )); if (rootComponent != 0 ) { const setCollisionEnabledAddr = rootComponent .readPointer () .add (0x660 ) .readPointer (); const setCollisionEnabled = new NativeFunction ( setCollisionEnabledAddr, "void" , ["pointer" , "bool" ] ); try { setCollisionEnabled (rootComponent, 3 ); } catch (e) { console .error ("error: " + e); } } } function printDistanceWithActor (actor ) { var location1 = getActorLoaction (user, false ) var location2 = getActorLoaction (actor,false ) const actorName = ue4Reverse.getNameByActor (actor) var distance = Math .sqrt (Math .pow (location1.x - location2.x , 2 ) + Math .pow (location1.y - location2.y , 2 )) console .log (`actorName: ${actorName} distance: ${distance} ` ) } const ue4Reverse = new UE4Reverse (0xb171cc0 , 0xb32d8a8 );const user = ue4Reverse.getActorByName ("FirstPersonCharacter_C" );setHP (user, 1000000 );ue4Reverse.forAllActor (SetVisibility ) ue4Reverse.forAllActor (setCollisionEnabled); const actor = ue4Reverse.getActorByName ("Actor" )
unidbg脚本 ACE2024.java:
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 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 package com.ACE2024;import capstone.Capstone;import capstone.api.Instruction;import com.github.unidbg.AndroidEmulator;import com.github.unidbg.Module;import com.github.unidbg.arm.backend.Backend;import com.github.unidbg.arm.backend.CodeHook;import com.github.unidbg.arm.backend.UnHook;import com.github.unidbg.linux.android.AndroidEmulatorBuilder;import com.github.unidbg.linux.android.AndroidResolver;import com.github.unidbg.linux.android.dvm.DalvikModule;import com.github.unidbg.linux.android.dvm.VM;import com.github.unidbg.memory.Memory;import com.github.unidbg.virtualmodule.android.AndroidModule;import keystone.Keystone;import keystone.KeystoneArchitecture;import keystone.KeystoneEncoded;import keystone.KeystoneMode;import unicorn.Arm64Const;import java.io.File;import java.io.FileInputStream;import java.io.FileOutputStream;import java.util.ArrayList;import java.util.List;public class ACE2024 { public final AndroidEmulator emulator; public final VM vm; public final Memory memory; public final Module module ; private final AsmContextQueue asmQueue; private final PatchInfoQueue patchQueue; private final Capstone capstone; private final Keystone keystone; private static final String inName = "C:\\Users\\Lenovo\\Desktop\\ACE2024\\lib2024\\arm64-v8a\\libplay.so" ; private static final String outName = "C:\\Users\\Lenovo\\Desktop\\ACE2024\\lib2024\\arm64-v8a\\libplayPatch.so" ; public ACE2024 () { emulator = AndroidEmulatorBuilder.for64Bit().build(); memory = emulator.getMemory(); memory.setLibraryResolver(new AndroidResolver (23 )); emulator.getSyscallHandler().setEnableThreadDispatcher(true ); vm = emulator.createDalvikVM(); new AndroidModule (emulator,vm).register(memory); DalvikModule dalvikModule = vm.loadLibrary(new File ("C:\\Users\\Lenovo\\Desktop\\ACE2024\\lib2024\\arm64-v8a\\libplay.so" ), true ); module = dalvikModule.getModule(); asmQueue = new AsmContextQueue (); patchQueue = new PatchInfoQueue (); capstone = new Capstone (Capstone.CS_ARCH_ARM64,Capstone.CS_MODE_ARM); keystone = new Keystone (KeystoneArchitecture.Arm64, KeystoneMode.LittleEndian); } public static void main (String[] args) { ACE2024 mainActivity = new ACE2024 (); mainActivity.debugger(); } public void debugger () { deBR(); module .callFunction(emulator,0x0000000000001458 ,"_Anti_Cheat_Expert" ); module .callFunction(emulator,0x000000000000EEC ,"aaaa" ,1 ); doPatch(); } public void deBR () { emulator.getBackend().hook_add_new(new CodeHook () { @Override public void hook (Backend backend, long address, int size, Object user) { Capstone capstone = new Capstone (Capstone.CS_ARCH_ARM64,Capstone.CS_MODE_ARM); byte [] bytes = backend.mem_read(address, 4 ); Instruction disasm = capstone.disasm(bytes, address)[0 ]; AsmContext asmContext = new AsmContext (); asmContext.setAddress(address); asmContext.setIns(disasm); asmContext.setRegs(saveRegs(backend)); asmQueue.add(asmContext); if (disasm.getMnemonic().equals("br" )){ doProcessBR(backend, disasm); } } @Override public void onAttach (UnHook unHook) { } @Override public void detach () { } },module .base, module .base + module .size, null ); } public void doProcessBR (Backend backend, Instruction disasm) { long brAddr = disasm.getAddress() - module .base; long condBrAddr = brAddr -4 ; long ldrAddr = 0 ; long brTargetAddr = 0 ; long condBrTargetAddr = 0 ; String jcc = new String (); AsmContext csetContext = null ; AsmContext ldrContext = null ; for (int i = 0 ; i < asmQueue.getSize() - 1 ; i++){ AsmContext asmContext = asmQueue.get(i); Instruction ins = asmContext.getIns(); String insMnemonic = ins.getMnemonic(); if (insMnemonic.equals("cset" )){ csetContext = asmContext; } else if (insMnemonic.equals("ldr" )){ ldrContext = asmContext; } } if (ldrContext == null ){ System.err.println("unrecognize br" ); } if (csetContext != null ){ Instruction csetIns = csetContext.getIns(); String opstr = csetIns.getOpStr(); String flag = opstr.split(", " )[1 ]; jcc = "b" + flag; } Instruction ldrIns = ldrContext.getIns(); ldrAddr = ldrIns.getAddress(); String[] opList = ldrIns.getOpStr().split(", " ); if (!jcc.isEmpty()){ for (long addr = ldrAddr; addr < brAddr - 4 ; addr += 4 ){ byte [] bytes = backend.mem_read(addr + 4 , 4 ); Instruction patchAsm = capstone.disasm(bytes, 0 )[0 ]; patchQueue.add(addr, patchAsm.getBytes()); } String ldrBaseReg = opList[1 ].replace("[" , "" ); String ldrOffsetReg = opList[2 ]; Number tabelBaseAddr = ldrContext.getReg(ldrBaseReg); Number tabelOffsetAddr = ldrContext.getReg(ldrOffsetReg); condBrTargetAddr = readInt64(backend, tabelBaseAddr.longValue() + 8 ) - module .base; brTargetAddr = readInt64(backend, tabelBaseAddr.longValue()) - module .base; KeystoneEncoded condBr = keystone.assemble(jcc+" 0x" + Integer.toHexString((int )condBrTargetAddr).toString(), (int )condBrAddr ); KeystoneEncoded br = keystone.assemble("b 0x" + Integer.toHexString((int )brTargetAddr), (int ) brAddr); patchQueue.add(condBrAddr, condBr.getMachineCode()); patchQueue.add(brAddr, br.getMachineCode()); } else { if (opList.length < 4 ) { return ; } String ldrBaseReg = opList[1 ].replace("[" , "" ); String ldrOffsetReg = opList[2 ]; Number tabelBaseAddr = ldrContext.getReg(ldrBaseReg); Number tabelOffsetAddr = ldrContext.getReg(ldrOffsetReg); brTargetAddr = readInt64(backend, tabelBaseAddr.longValue() + tabelOffsetAddr.longValue() * 8 ) - module .base; KeystoneEncoded br = keystone.assemble("b 0x" + Integer.toHexString((int )brTargetAddr), (int ) brAddr); patchQueue.add(brAddr, br.getMachineCode()); } } public void doPatch () { try { File f = new File (inName); FileInputStream fis = new FileInputStream (f); byte [] data = new byte [(int ) f.length()]; fis.read(data); fis.close(); for (int i = 0 ; i < patchQueue.size(); i++){ PatchInfo pi = patchQueue.get(i); for (int j=0 ; j < 4 ; j++) { data[(int ) pi.address+j] = pi.ins[j]; } } File fo = new File (outName); FileOutputStream fos = new FileOutputStream (fo); fos.write(data); fos.flush(); fos.close(); System.out.println("finish" ); } catch (Exception e) { e.printStackTrace(); } } public List<Number> saveRegs (Backend bk) { List<Number> nb = new ArrayList <>(); for (int i=0 ;i<29 ;i++) { nb.add(bk.reg_read(i+ Arm64Const.UC_ARM64_REG_X0)); } nb.add(bk.reg_read(Arm64Const.UC_ARM64_REG_FP)); nb.add(bk.reg_read(Arm64Const.UC_ARM64_REG_LR)); return nb; } public long readInt64 (Backend bk,long addr) { byte [] bytes = bk.mem_read(addr, 4 ); long res = 0 ; for (int i=0 ;i<bytes.length;i++) { res =((bytes[i]&0xffL ) << (8 *i)) + res; } return res; } }
AsmContext.java:
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 package com.ACE2024;import capstone.api.Instruction;import java.util.List;public class AsmContext { private List<Number> regs; private Instruction ins; private long address; public void setRegs (List<Number> regs) { this .regs = regs; } public void setIns (Instruction ins) { this .ins = ins; } public void setAddress (long address) { this .address = address; } public List<Number> getRegs () { return regs; } public Instruction getIns () { return ins; } public long getAddress () { return address; } public Number getReg (String reg) { String offset = (reg.replaceAll("[^0-9]" , "" )); return regs.get(Integer.parseInt(offset)); } }
AsmContextQueue.java
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 package com.ACE2024;import java.util.LinkedList;public class AsmContextQueue { private static final int MAX_SIZE = 10 ; private final LinkedList<AsmContext> queue = new LinkedList <>(); public synchronized void add (AsmContext asmContext) { if (queue.size() >= MAX_SIZE) { queue.poll(); } queue.add(asmContext); } public synchronized AsmContext get (int index) { if (index < 0 || index >= queue.size() || index >= MAX_SIZE) { return null ; } return queue.get(index); } public synchronized LinkedList<AsmContext> getInstructions () { return new LinkedList <>(queue); } public synchronized void clear () { queue.clear(); } public synchronized int getSize () { return queue.size(); } }
PatchInfo.java
1 2 3 4 5 6 7 8 9 10 11 package com.ACE2024;public class PatchInfo { long address; byte [] ins; PatchInfo(long address, byte [] ins) { this .address = address; this .ins = ins; } }
PatchQueue.java
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 package com.ACE2024;import java.util.ArrayList;import java.util.List;public class PatchInfoQueue { private final List<PatchInfo> queue; public PatchInfoQueue () { this .queue = new ArrayList <>(); } public void add (long address, byte [] ins) { for (PatchInfo entry : queue) { if (entry.address == address) { entry.ins = ins; return ; } } queue.add(new PatchInfo (address, ins)); } public PatchInfo get (int idx) { if (idx >= queue.size()) return null ; return queue.get(idx); } public boolean remove (long address) { return queue.removeIf(entry -> entry.address == address); } public int size () { return queue.size(); } public void printAll () { for (PatchInfo patchInfo : queue) { System.out.println("address=" + patchInfo.address + ", ins=" + patchInfo.ins); } } }
解密脚本 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 import base64 # 自定义base64解码 custom_table = 'ACE0BDFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz123456789+/' custom_dict = {c: i for i, c in enumerate(custom_table)} def custom_b64decode(s): # 处理填充字符 s += '=' * ((4 - len(s) % 4 ) % 4 ) binary_str = [] for c in s: if c == '=' : binary_str.append('000000' ) continue binary_str.append(f"{custom_dict[c]:06b}" ) # 拼接二进制并转换为字节 bit_stream = ''.join(binary_str) bytes_list = [int(bit_stream[i:i+8], 2) for i in range(0, len(bit_stream), 8) if i+8 <= len(bit_stream)] return bytes(bytes_list) # 异或解密 def xor_decrypt(data, key): key_len = len(key) return bytes([data[i] ^ key[i % key_len] for i in range(len(data))]) if __name__ == "__main__": encoded_str = "UT1fc0gIYDArdz80Z0Xem46J" result1 = custom_b64decode(encoded_str) key = bytes.fromhex("0A0C0E0051162738491A3B5C2D4E6FFAFCFE") result2 = xor_decrypt(result1, key) print("最终结果:", result2.decode("utf-8", errors="replace"))
参考 https://renyili.org/post/game_cheat2/
https://www.52pojie.cn/thread-1838396-1-1.html
https://www.cnblogs.com/revercc/p/17641855.html
https://oacia.dev/unidbg-anti-br/