前言
第一次聽說這比賽是上年偶然和舍友聊天時他告訴我的,沒想到還有以遊戲安全為主的比賽,當時看到有安卓的賽道就報名了,然後比賽時就被那門卡了2天,然後就沒有然後了。
今年沒意外的話是我大學生涯的最後一年,也許也是最後一年打這個比賽了吧,下年也不知道有沒有空看看題。
以下是我的解題記錄,一部份是比賽時寫的,一部份是賽後補充的,有寫錯的還請指正。
前置準備
版本:4.27
GName:0xADF07C0
GObject:0xAE34A98
dump sdk by GObject,記為SDKO.txt
复制代码 隐藏代码./ue4dumper64 --sdku --newue+ --gname 0xADF07C0 --guobj 0xAE34A98 --package com.ACE2025.Game
dump all objects,記為Objects.txt
复制代码 隐藏代码./ue4dumper64 --objs --newue+ --gname 0xADF07C0 --guobj 0xAE34A98 --package com.ACE2025.Game
異常點分析與修復
題目說明如下,純粹的UE4逆向,無任何反調試。
速度異常
hook pthread_create
,patch掉libGame.so
創建唯一一個線程後,速度不再異常。
复制代码 隐藏代码function hook_pthread(){ var pthread_create_addr = Module.findExportByName(null, 'pthread_create'); console.log("pthread_create_addr,", pthread_create_addr); var pthread_create = newNativeFunction(pthread_create_addr, "int", ["pointer", "pointer", "pointer", "pointer"]); Interceptor.replace(pthread_create_addr, newNativeCallback(function (parg0, parg1, parg2, parg3) { var so_name = Process.findModuleByAddress(parg2).name; var so_path = Process.findModuleByAddress(parg2).path; var so_base = Module.getBaseAddress(so_name); var offset = parg2 - so_base;// console.log("so_name", so_name, "offset", offset, "path", so_path, "parg2", parg2); var PC = 0;if ((so_name.indexOf("libGame.so") > -1)) { console.log("find thread func offset", so_name, offset);if ((7068 === offset)) { console.log("anti bypass"); } else { PC = pthread_create(parg0, parg1, parg2, parg3); console.log("ordinary sequence", PC) } } else { PC = pthread_create(parg0, parg1, parg2, parg3);// console.log("ordinary sequence", PC) }return PC; }, "int", ["pointer", "pointer", "pointer", "pointer"]))}
由此可知相關邏輯就在libGame.so
創建的線程中。接下來分析它的實現原理。
用IDA動調線程回調函數sub_1B9C
。
進入後會看到明顯的控制流平坦化,先不管。
打斷點進入case 12623
,分析後發現就是通過/proc/self/maps
獲取libUE4.so
的基址。
之後本想手動還原下控制流,但突然想起IDA有個D-810插件貌似能解控制流混淆,嘗試下,發現效果很好。
獲取了libUE4_base
後會賦給infos[19]
。
然後*(_QWORD *)(libUE4_base_1 + 0xAFAC398)
獲取了libUE4.so
的一個全局變量,猜測是GWorld
。
用ue4dumper來驗證,發現能順利dump出SDK,由此可知0xAFAC398
的確是GWorld
。
記dump出來的文件為SDKW.txt
。
复制代码 隐藏代码./ue4dumper64 --sdkw --newue+ --gname 0xADF07C0 --gworld 0xAFAC398 --package com.ACE2025.Game
獲取GWorld
後,就能通過遍歷其中的屬性定位到FirstPersonCharacter_C
,具體原理如下,這是用frida實現的。
其中用了vtabs
( UObject
的第0
個成員屬性,虛表 )的函數偏移是否等於0xA63BE28
來確定是否FirstPersonCharacter_C
對象,0xA63BE28
大概是FirstPersonCharacter_C
的一個特徵?
最終通過修改CharacterMovementComponent
的MaxAcceleration
和MaxWalkSpeed
來改變人物速度。
复制代码 隐藏代码let GWorld = base.add(0xAFAC398).readPointer(); // FirstPersonExampleMap (GWorld)let PersistentLevel = GWorld.add(0x30).readPointer() // PersistentLevellet StreamingLevels = PersistentLevel.add(0x98).readPointer(); // StreamingLevelsToConsider.StreamingLevelslet StreamingLevelsNum = PersistentLevel.add(0xA0).readU32();let FirstPersonCharacter_C = null;for(let i = 0; i < StreamingLevelsNum; i++) { let StreamingLevel = StreamingLevels.add(i * 8).readPointer(); let vtabs = StreamingLevel.readPointer();if (vtabs.sub(base) == 0xA63BE28) {// console.log(i, vtabs.readPointer()); FirstPersonCharacter_C = StreamingLevel; }}let CharacterMovementComponent = FirstPersonCharacter_C.add(0x288).readPointer()CharacterMovementComponent.add(0x1a0).writeFloat(1000000000)CharacterMovementComponent.add(0x18c).writeFloat(1000000000)
自瞄異常
在SDKO.txt
裡可以看到我的角色類裡有個ProjectileClass
成員,而它有個OnHit
成員函數
复制代码 隐藏代码Class: MyProjectCharacter.Character.Pawn.Actor.Object SkeletalMeshComponent* Mesh1P;//[Offset: 0x4b8, Size: 0x8] SkeletalMeshComponent* FP_Gun;//[Offset: 0x4c0, Size: 0x8] SceneComponent* FP_MuzzleLocation;//[Offset: 0x4c8, Size: 0x8] // 槍口位置 SkeletalMeshComponent* VR_Gun;//[Offset: 0x4d0, Size: 0x8] SceneComponent* VR_MuzzleLocation;//[Offset: 0x4d8, Size: 0x8] CameraComponent* FirstPersonCameraComponent;//[Offset: 0x4e0, Size: 0x8] MotionControllerComponent* R_MotionController;//[Offset: 0x4e8, Size: 0x8] // for VR MotionControllerComponent* L_MotionController;//[Offset: 0x4f0, Size: 0x8] // for VRfloat BaseTurnRate;//[Offset: 0x4f8, Size: 0x4] // 左右轉向速率float BaseLookUpRate;//[Offset: 0x4fc, Size: 0x4] // 上下轉向速率 Vector GunOffset;//[Offset: 0x500, Size: 0xc]classMyProjectProjectile* ProjectileClass;//[Offset: 0x510, Size: 0x8] SoundBase* FireSound;//[Offset: 0x518, Size: 0x8] AnimMontage* FireAnimation;//[Offset: 0x520, Size: 0x8]bool bUsingMotionControllers;//(ByteOffset: 0, ByteMask: 1, FieldMask: 1)[Offset: 0x528, Size: 0x1]float RecoilPitch;//[Offset: 0x52c, Size: 0x4] // 後座力float RecoilYaw;//[Offset: 0x530, Size: 0x4] // 後座偏航float RecoilRecoverySpeed;//[Offset: 0x534, Size: 0x4] // 後座力恢復速度float RecoilAccumulationRate;//[Offset: 0x538, Size: 0x4] // 後座力累積率Class: MyProjectProjectile.Actor.Object SphereComponent* CollisionComp;//[Offset: 0x220, Size: 0x8] ProjectileMovementComponent* ProjectileMovement;//[Offset: 0x228, Size: 0x8]voidOnHit(PrimitiveComponent* HitComp, Actor* OtherActor, PrimitiveComponent* OtherComp, Vector NormalImpulse, out const HitResult Hit);// 0x67138e8
嘗試hook OnHit
,分別在enter和leave時打印CameraRotation
,發現兩者相等,即在enter前就已經完成自瞄,代表相關的自瞄邏輯不在這裡。
复制代码 隐藏代码function hook_onHit(){// void OnHit(PrimitiveComponent* HitComp, Actor* OtherActor, PrimitiveComponent* OtherComp, Vector NormalImpulse, out const HitResult Hit);// 0x67138e8 Interceptor.attach(base.add(0x6711D34), { onEnter: function(args) { console.log("[onHit] enter: ", JSON.stringify(getCameraRotation())) }, onLeave: function() {// setCameraRotation([100, 200, 0]) console.log("[onHit] leave: ", JSON.stringify(getCameraRotation())) } })}
對CameraRotation
下硬斷( 寫 ),命中信息如下:
命中PC:0x799F6637C0
libUE4 base:0x7996b2b000
計算得Offset為0x8B387C0
IDA跳到0x8B387C0
,如下:
記0x8B387C0
所在函數為mb_aimbot
,嘗試直接patch掉mb_aimbot
,發現patch後人物無法轉動視角。
复制代码 隐藏代码function patch_mb_aimbot(){ Interceptor.replace(base.add(0x8B3861C), newNativeCallback(() => { console.log("patch mb_aimbot") }, "void", []))}
hook mb_aimbot
,打印調用棧,未點擊時,調用棧如下
复制代码 隐藏代码[hook_aimbot] 799fa8e604 is in libUE4.so offset: 0x8f9b604799fa92444 is in libUE4.so offset: 0x8f9f444799fa9a358 is in libUE4.so offset: 0x8fa7358799fcf0b8c is in libUE4.so offset: 0x91fdb8c799d2c1bb0 is in libUE4.so offset: 0x67cebb0799d2c1730 is in libUE4.so offset: 0x67ce730799d2c0e24 is in libUE4.so offset: 0x67cde24799fcecc04 is in libUE4.so offset: 0x91f9c04799fcea3bc is in libUE4.so offset: 0x91f73bc799f82e760 is in libUE4.so offset: 0x8d3b760799f6f98f0 is in libUE4.so offset: 0x8c068f0799d93f614 is in libUE4.so offset: 0x6e4c614799c5ee728 is in libUE4.so offset: 0x5afb728799c5e83bc is in libUE4.so offset: 0x5af53bc799c5e6514 is in libUE4.so offset: 0x5af3514[hook_aimbot] 79a03df660 is in libUE4.so offset: 0x98ec660799fcf0b8c is in libUE4.so offset: 0x91fdb8c799d2c1bb0 is in libUE4.so offset: 0x67cebb0799d2c1730 is in libUE4.so offset: 0x67ce730799d2c0e24 is in libUE4.so offset: 0x67cde24799fcecc04 is in libUE4.so offset: 0x91f9c04799fcea3bc is in libUE4.so offset: 0x91f73bc799f82e760 is in libUE4.so offset: 0x8d3b760799f6f98f0 is in libUE4.so offset: 0x8c068f0799d93f614 is in libUE4.so offset: 0x6e4c614799c5ee728 is in libUE4.so offset: 0x5afb728799c5e83bc is in libUE4.so offset: 0x5af53bc799c5e6514 is in libUE4.so offset: 0x5af3514
點擊後,多了一個不同的調用棧0x670f3fc
。
复制代码 隐藏代码[hook_aimbot]799d2f83fc is in libUE4.so offset: 0x670f3fc7a136f0c94 is in libart.so offset: 0x2e6c94799d2f8eb0 is in libUE4.so offset: 0x670feb0799fe51e38 is in libUE4.so offset: 0x9268e38799fe4fe04 is in libUE4.so offset: 0x9266e04799fb8958c is in libUE4.so offset: 0x8fa058c799fb886f4 is in libUE4.so offset: 0x8f9f6f4799d3b9420 is in libUE4.so offset: 0x67d0420799fb88374 is in libUE4.so offset: 0x8f9f374799f49b7bc is in libUE4.so offset: 0x88b27bc799fb90358 is in libUE4.so offset: 0x8fa7358799fde6b8c is in libUE4.so offset: 0x91fdb8c799d3b7bb0 is in libUE4.so offset: 0x67cebb0799d3b7730 is in libUE4.so offset: 0x67ce730799d3b6e24 is in libUE4.so offset: 0x67cde24799fde2c04 is in libUE4.so offset: 0x91f9c04
嘗試patch掉0x670f3fc
所在函數0x670F110
,雖然點擊後不會再自動瞄到某處,但子彈射不出。
由此猜測0x670F110
是射擊的回調函數,自瞄邏輯應該就在裡面。
記0x670F110
為process_before_shoot
。
复制代码 隐藏代码Interceptor.replace(base.add(0x670F110), newNativeCallback(() => {return1;}, "int", []))
在0x670F110
中從調用mb_aimbot
處向上分析,發現是否調用mb_aimbot
邏輯是由sub_680B790(v32, "E")
決定的。
hook sub_680B790
,打印參數和返回值。
注:hexdump
後可知是unicode編碼的字符串,因此要用readUtf16String
。
复制代码 隐藏代码function hook_680B790(){ Interceptor.attach(base.add(0x680B790), { onEnter: function(args) {this.a1 = args[1]; console.log("a0: ", args[0].readUtf16String()); console.log("a1: ", args[1].readUtf16String()); }, onLeave: function(retval) { console.log("res: ", retval); } })}
輸出如下,可以看出是字符串對比函數,res
是a0
、a1
第1個不相等字符的差值,若相等則為0
( 不區分大小寫 )。記sub_680B790
為utf16_cmp
。
可以看到前面一直在和EditorCube8
對比,明顯它就是自瞄的目標,
复制代码 隐藏代码a0: BigWalla1: EditorCube8 res: 0xfffffffda0: BigWall2 a1: EditorCube8 res: 0xfffffffda0: EditorCube10a1: EditorCube8 res: 0xfffffff9a0: EditorCube11a1: EditorCube8 res: 0xfffffff9a0: EditorCube12a1: EditorCube8res: 0xfffffff9a0: EditorCube13a1: EditorCube8res: 0xfffffff9a0: EditorCube14a1: EditorCube8res: 0xfffffff9a0: EditorCube15a1: EditorCube8res: 0xfffffff9a0: EditorCube16a1: EditorCube8res: 0xfffffff9a0: EditorCube17a1: EditorCube8res: 0xfffffff9a0: EditorCube18a1: EditorCube8res: 0xfffffff9a0: EditorCube19a1: EditorCube8res: 0xfffffff9a0: EditorCube20a1: EditorCube8res: 0xfffffffaa0: EditorCube21a1: EditorCube8res: 0xfffffffaa0: EditorCube8a1: EditorCube8res: 0x0a0: EditorCube9a1: EditorCube8res: 0x1a0: Floor_12a1: EditorCube8res: 0x1a0: Wall1a1: EditorCube8res: 0x12a0: Wall2_11a1: EditorCube8res: 0x12a0: Wall3a1: EditorCube8res: 0x12a0: Wall4a1: EditorCube8res: 0x12a0: ../../../MyProject/Saved/Config/Android/Engine.inia1: ../../../MyProject/Saved/Config/Android/Engine.inires: 0x0a0: truea1: Trueres: 0x0a0: Androida1: Androidres: 0x0a0: Androida1: Androidres: 0x0a0: Androida1: Androidres: 0x0
嘗試在a1
為EditorCube8
時將返回值固定replace為一個大於0
的值。
复制代码 隐藏代码Interceptor.attach(base.add(0x680B790), { onEnter: function(args) {this.a1 = args[1]; }, onLeave: function(retval) {if (this.a1.readUtf16String() == "EditorCube8") { retval.replace(5); } }})
結果是射擊時不再自動瞄到指定目標,但手槍在射完後會向上抬一下,類似後座力?不知是否屬於異常點。
下面簡單看看它的自瞄實現原理:
從process_before_shoot
開始看,一開始先遍歷自瞄目標。
然後調用calcTargetOffset
計算自瞄值,然後根據這個值來設置CameraRotation
( 人物相機的轉向,使它朝向目標以實現自瞄的效果 )。
calcTargetOffset
實現大概像這樣:利用目標location與人物的location向量來計算。
复制代码 隐藏代码function calcTargetOffset(targetLoc, cameraLoc){ let x = targetLoc.x - cameraLoc.x; let y = targetLoc.y - cameraLoc.y; let z = targetLoc.z - cameraLoc.z; let angleX = 0; let angleY = 0;if (x > 0 && y == 0) angleX = 0;if (x > 0 && y > 0) angleX = Math.abs(Math.atan(y / x)) / Math.PI * 180;if (x == 0 && y > 0) angleX = 90;if (x < 0 && y > 0) angleX = 90 + Math.abs(Math.atan(x / y)) / Math.PI * 180;if (x < 0 && y == 0) angleX = 180;if (x < 0 && y < 0) angleX = 180 + Math.abs(Math.atan(y / x)) / Math.PI * 180;if (x == 0 && y < 0) angleX = 270;if (x > 0 && y < 0) angleX = 270 + Math.abs(Math.atan(x / y)) / Math.PI * 180;if (angleX < 0) { angleX += 360; }if (angleX > 360) { angleX -= 360; } angleY = Math.atan(z / Math.sqrt(x * x + y * y)) / Math.PI * 180;if (angleY < 0) { angleY += 360; }return [angleY, angleX, 0]}
子彈發射位置異常
可以明顯看出子彈發射的起始位置是隨機的。
猜測可能與MyProjectCharacter
的GunOffset
有關。
复制代码 隐藏代码Class: MyProjectCharacter.Character.Pawn.Actor.Object// ... Vector GunOffset;//[Offset: 0x500, Size: 0xc]
對GunOffset
下硬斷( 讀 )。
命中如下兩處地址:
复制代码 隐藏代码// libUE4 base: 6f6c74e0001. PC: 0x6F7307EA6C (0x6930A6C) LR: 0x6F7307EA682. PC: 0x6F7307EA7C (0x6930A7C) LR: 0x6F7307EA68
0x6930A6C
所在函數是sub_6930A3C
。
hook sub_6930A3C
打印調用棧。
其中0x670f658
位於0x670F110
函數( 即process_before_shoot
)。
复制代码 隐藏代码6f72e75e24 is in libUE4.so offset: 0x670fe246f72e75658 is in libUE4.so offset: 0x670f658 (位於0x670F110)6f72e75eb0 is in libUE4.so offset: 0x670feb06f72e75eb0 is in libUE4.so offset: 0x670feb06f759cee38 is in libUE4.so offset: 0x9268e386f759cce04 is in libUE4.so offset: 0x9266e046fe951f09c is in libart.so offset: 0x59b09c
process_before_shoot
中有調用rand()
生成隨機值,猜測這與槍口的隨機有關。
嘗試hook rand
固定其返回值。
复制代码 隐藏代码function hook_rand(){ Interceptor.attach(Module.findExportByName(null, "rand"), { onLeave: function(retval) { retval.replace(100); console.log("[rand] res: ", retval); } })}
結果是子彈發射位置固定了,但是固定在了人物的頭上偏左的位置。
繼續嘗試其他修復思路。
讓process_before_shoot
中的a1[0x14A] & 1
不為0
,目的是讓執行流無法走到上述的0x6930A6C
位置。
复制代码 隐藏代码function hook_process_before_shoot(){ Interceptor.attach(base.add(0x670F110), { onEnter: function(args) { let val = args[0].add(0x528).readU8(); args[0].add(0x528).writeU8(val | 1); console.log("[process_before_shoot] a1[0x14A] & 1: ", args[0].add(0x528).readU8() & 1) }, onLeave: function(retval) { } })}
結果同樣可以固定子彈發射的位置,但這次是在人物的下方固定向正前方發射,上下抬頭時不會改變發射方向。
以下部份是賽後的分析。
若a1[0x14A] & 1
為0
,會調用about_bullt_loc1
生成一些隨機值,調用about_bullt_loc2
生成一個基於GunOffset
等參數而來的值,最終傳入mb_process_bullet_shoot_loc
做最後的處理。
而當a1[0x14A] & 1
為1
時,則會從VR_MuzzleLocation
裡獲取一些參數,最終同樣傳入mb_process_bullet_shoot_loc
。
注:通過hook所在函數,將*((QWORD*)a1 + 155)
當成UObject
來打印它的名字,從而確定它是VR_MuzzleLocation
對象。
但VR_MuzzleLocation
看名字來說是給VR設備使用的,安卓設備感覺是使用FP_MuzzleLocation
才對。
因此合理懷疑這也是一個異常點。
复制代码 隐藏代码Class: MyProjectCharacter.Character.Pawn.Actor.Object SkeletalMeshComponent* Mesh1P;//[Offset: 0x4b8, Size: 0x8] SkeletalMeshComponent* FP_Gun;//[Offset: 0x4c0, Size: 0x8] SceneComponent* FP_MuzzleLocation;//[Offset: 0x4c8, Size: 0x8] // 槍口位置 SkeletalMeshComponent* VR_Gun;//[Offset: 0x4d0, Size: 0x8] SceneComponent* VR_MuzzleLocation;//[Offset: 0x4d8, Size: 0x8]
hook process_before_shoot
,從IDA裡可知args[0].add(0x4D8)
保存著VR_MuzzleLocation
對象的地址,嘗試將它改為FP_MuzzleLocation
的地址。
复制代码 隐藏代码function hook_test(){ Interceptor.attach(base.add(0x670F110), { onEnter: function(args) { console.log("[process_before_shoot]"); let VR_MuzzleLocation = args[0].add(0x4D8) VR_MuzzleLocation.writePointer(All_Objects["MuzzleLocation"])printName(VR_MuzzleLocation.readPointer()); }, onLeave: function(retval) { } })}
結果是子彈終於會隨著槍口的變化而變化,但卻是從槍口往左發射的,正常應該是往前才對。
接下來嘗試修改FP_MuzzleLocation
裡的一些參數,看看能否改變發射方向。
FP_MuzzleLocation
屬於USceneComponent
類,其中有以下這兩個屬性:
复制代码 隐藏代码Class: SceneComponent.ActorComponent.Object//... Vector RelativeLocation;//[Offset: 0x11c, Size: 0xc] Rotator RelativeRotation;//[Offset: 0x128, Size: 0xc]
利用K2_SetRelativeLocation
和K2_SetRelativeRotation
函數來修改。通過不斷嘗試,發現只需要將Rotation設置為[0, 90, 0]
即可,相當於旋轉了90度。完整修複代碼如下:
注:不能直接通過內存來修改,要用API來修改。
复制代码 隐藏代码function fix_MuzzleLocation(){ Interceptor.attach(base.add(0x670F110), { onEnter: function(args) { console.log("[process_before_shoot]");/* API */ let K2_SetRelativeLocation = newNativeFunction(base.add(0x8AE6D70), "void", ["pointer", "float", "float", "float", "bool", "pointer", "bool"]); let K2_SetRelativeRotation = newNativeFunction(base.add(0x8AE6F00), "void", ["pointer", "float", "float", "float", "bool", "pointer", "bool"]);// K2_SetRelativeLocation(All_Objects["MuzzleLocation"], 0, 0, 0, 0, ptr(0), 0);K2_SetRelativeRotation(All_Objects["MuzzleLocation"], 0, 90, 0, 0, ptr(0), 0);/* Prop */ let RelativeLocation = All_Objects["MuzzleLocation"].add(0x11c); let RelativeRotation = All_Objects["MuzzleLocation"].add(0x128); console.log("location: ", JSON.stringify(readVector(RelativeLocation))) console.log("rotation: ", JSON.stringify(readVector(RelativeRotation)))// Replace let MuzzleLocation = args[0].add(0x4D8) MuzzleLocation.writePointer(All_Objects["MuzzleLocation"]) }, onLeave: function(retval) { } })}
透視分析
在Objects.txt
裡可以看到FirstPersonCharacter_C
和ThirdPersonCharacter_C
,它們分別是我控制的人物和假人。
复制代码 隐藏代码[0x3c05]:Name: FirstPersonCharacter_CClass: FirstPersonCharacter_CObjectPtr: 0x71694840c0ClassPtr: 0x7169582700[0x3c53]:Name: ThirdPersonCharacterClass: ThirdPersonCharacter_CObjectPtr: 0x7168662630ClassPtr: 0x716958c100
嘗試一:替換Material。( 沒效果 )
复制代码 隐藏代码let ThirdPersonCharacter_Mesh = ThirdPersonCharacter_obj.add(Offset.ACharacterToUSkeletalMeshComponent).readPointer();let SetMaterial = newNativeFunction(ThirdPersonCharacter_Mesh.readPointer().add(0x598).readPointer(), "void", ["pointer", "int", "pointer"]);SetMaterial(ThirdPersonCharacter_Mesh, 0, All_Objects["BaseMaterial"]);// SetMaterial(ThirdPersonCharacter_Mesh, 0, All_Objects["DefaultTextMaterialOpaque"]);// SetMaterial(ThirdPersonCharacter_Mesh, 0, All_Objects["m_SimpleVolumetricCloud"]);// SetMaterial(ThirdPersonCharacter_Mesh, 0, All_Objects["DefaultSpriteMaterial"]);// SetMaterial(ThirdPersonCharacter_Mesh, 0, All_Objects["PokeAHoleMaterial"]);// SetMaterial(ThirdPersonCharacter_Mesh, 0, All_Objects["OculusMR_ChromaKey"]);// SetMaterial(ThirdPersonCharacter_Mesh, 0, All_Objects["DebugMeshMaterial"]);// SetMaterial(ThirdPersonCharacter_Mesh, 0, All_Objects["EmissiveMeshMaterial"]);
嘗試二:設置bDisableDepthTest
為0
。( 沒效果 )
复制代码 隐藏代码let ThirdPersonCharacter_Mesh = ThirdPersonCharacter_obj.add(Offset.ACharacterToUSkeletalMeshComponent).readPointer();let GetNumMaterials = newNativeFunction(ThirdPersonCharacter_Mesh.readPointer().add(0x6e8).readPointer(), "int", ["pointer"]);let GetMaterial = newNativeFunction(ThirdPersonCharacter_Mesh.readPointer().add(0x590).readPointer(), "pointer", ["pointer", "int"]);let NumMaterials = GetNumMaterials(ThirdPersonCharacter_Mesh)console.log("NumMaterials: ", NumMaterials);for(let i = 0; i < NumMaterials; i++) { let material = GetMaterial(ThirdPersonCharacter_Mesh, i) console.log(`material[${i}]: `, material, getName64(material.add(Offset.UObjectToFNameIndex).readU32())) let bDisableDepthTest = material.add(0x1f8); bDisableDepthTest.writeU8(bDisableDepthTest.readU8() & (~1)); console.log("bDisableDepthTest: ", bDisableDepthTest.readU8() & 1)}
嘗試三:利用SetTexture
隨便設置一個Texture。( 沒效果 )
复制代码 隐藏代码let ThirdPersonCharacter_Mesh = ThirdPersonCharacter_obj.add(Offset.ACharacterToUSkeletalMeshComponent).readPointer();/* API */// void SetTexture(Texture* InTexture);// 0x95efb5clet SetTexture = newNativeFunction(base.add(0x8B2C4CC), "void", ["pointer", "pointer"]);// Texture* GetTexture();// 0x95efa98let GetTexture = newNativeFunction(ThirdPersonCharacter_Mesh.readPointer().add(0x1F8).readPointer(), "pointer", ["pointer"]);/* Prop */let ThirdPersonCharacter_Mesh = ThirdPersonCharacter_Mesh.add(Offset.USkeletalMeshComponentToSkeletalMesh).readPointer();SetTexture(ThirdPersonCharacter_Mesh, All_Objects["T_ML_Rubber_Blue_01_N"]);
以下部份是賽後的分析。
找資料時發現類似這遊戲的人物透視效果基本上有以下兩種實現思路:
-
通過Disable Depth Test ( 參考 )。 -
通過Custom Depth ( 參考 )。
但嘗試後發現遊戲似乎不是用上述方法實現透視效果的( 不太確定,也有可能是我修改的地方不對 )?
找了很久都沒有什麼思路,最終只好退而求其次,用一種「掩耳盜鈴」的方式來修復,具體思路如下:
-
調用 KismetSystemLibrary
的靜態函數LineTraceSingle
來獲取FirstPersonCharacter_C
和ThirdPersonCharacter_C
之間的HitResult
。 -
分析 HitResult
,會發現ThirdPersonCharacter_C
沒有被遮擋時HitResult.Distance
為0
,否則為二者之間的距離。 -
因此可以根據 HitResult.Distance
是否為0
來設置ThirdPersonCharacter_C
是否渲染到MainPass。
具體調用LineTraceSingle
、獲取HitResult.Distance
的代碼如下:
复制代码 隐藏代码function getFirstPersonThirdPersonDistance(){// static bool LineTraceSingle(const Object* WorldContextObject, const Vector Start, const Vector End, byte TraceChannel, bool bTraceComplex, out const Actor*[] ActorsToIgnore, byte DrawDebugType, out HitResult OutHit, bool bIgnoreSelf, LinearColor TraceColor, LinearColor TraceHitColor, float DrawTime);// 0x9471770 let LineTraceSingle = newNativeFunction(base.add(0x8D1AA78), "bool", ["pointer", "float", "float", "float", "float", "float", "float", "uint8", "bool", "pointer", "uint8", "pointer", "bool", "float", "float", "float", "float", "float", "float", "float", "float", "float"]); let buf1 = Memory.alloc(0x1000); let HitResult = Memory.alloc(0x1000); let start_loc = getActorLocation(FirstPersonCharacter_C_obj); let end_loc = getActorLocation(ThirdPersonCharacter_obj); let r = LineTraceSingle(FirstPersonCharacter_C_obj, start_loc[0], start_loc[1], start_loc[2], end_loc[0], end_loc[1], end_loc[2], 0, 0, buf1, 1, HitResult, 0, 255, 0, 0, 0, 0, 255, 0, 0, 10000); let HitResult_Distance = HitResult.add(0x8).readFloat();return HitResult_Distance;}
時機選擇在Actor
類的ReceiveTick
函數,在其中判斷是否渲染:
复制代码 隐藏代码function bypassWallhack(){ let SetRenderInMainPass = newNativeFunction(base.add(0x8AB9E58), "void", ["pointer", "bool"]); let ThirdPersonCharacter_Mesh = ThirdPersonCharacter_obj.add(Offset.ACharacterToUSkeletalMeshComponent).readPointer();// void ReceiveTick(float DeltaSeconds);// 0x6c50500 Interceptor.attach(base.add(0x6c50500), { onEnter: function(args) {if (getFirstPersonThirdPersonDistance() == 0) { console.log("can see ThirdPerson")SetRenderInMainPass(ThirdPersonCharacter_Mesh, 1); } else { console.log("can not see ThirdPerson")SetRenderInMainPass(ThirdPersonCharacter_Mesh, 0); } } })}
當然這肯定不是正解,而且效果也非常一般,之後看看有沒有其他大佬分析下正解吧。
白色的Cube碰撞異常
子彈射在白色的Cube上不會反彈。
白色的Cube應是就是EditorCubeN
。
复制代码 隐藏代码[0x3c3e]:Name: EditorCube8Class: StaticMeshActorObjectPtr: 0x7170c09f40ClassPtr: 0x717e7c0100[0x3c26]:Name: EditorCube10Class: StaticMeshActorObjectPtr: 0x7170c0ba40ClassPtr: 0x717e7c0100
子彈射在EditorCubeN
上會瞬間消失,但EditorCubeN
是有被擊退的效果,而子彈射在黑色的Cube上能正常被反彈。
嘗試一:看看是否因為物理模擬未啟用。( 沒效果 )
复制代码 隐藏代码let StaticMeshComponent = All_Objects["EditorCube8"].add(0x220).readPointer();let SetSimulatePhysics = newNativeFunction(StaticMeshComponent.readPointer().add(0x5D0).readPointer(), "void", ["pointer", "bool"]);let SetNotifyRigidBodyCollision = newNativeFunction(StaticMeshComponent.readPointer().add(0x658).readPointer(), "void", ["pointer", "bool"]);SetSimulatePhysics(StaticMeshComponent, 1);SetNotifyRigidBodyCollision(StaticMeshComponent, 1)
嘗試二:設置物理材質的反彈系數為1。( 沒效果 )
复制代码 隐藏代码let StaticMeshComponent = All_Objects["EditorCube8"].add(0x220).readPointer();/* API */let GetPhysicalMaterial = newNativeFunction(StaticMeshComponent.readPointer().add(0x2D0).readPointer(), "pointer", ["pointer"]);let GetNumMaterials = newNativeFunction(StaticMeshComponent.readPointer().add(0x6e8).readPointer(), "int", ["pointer"]);let GetMaterial = newNativeFunction(StaticMeshComponent.readPointer().add(0x590).readPointer(), "pointer", ["pointer", "int"]);/* Prop */let NumMaterials = GetNumMaterials(StaticMeshComponent)console.log("NumMaterials: ", NumMaterials);for(let i = 0; i < NumMaterials; i++) { let material = GetMaterial(StaticMeshComponent, i) let PhysicalMaterial = GetPhysicalMaterial(material); let Restitution = PhysicalMaterial.add(0x34); Restitution.writeFloat(1)}
嘗試三:設置與所有物體的碰撞響應都為Block( 具體值是2 )。( 沒效果 )
复制代码 隐藏代码let StaticMeshComponent = All_Objects["EditorCube8"].add(0x220).readPointer();let SetCollisionResponseToAllChannels = newNativeFunction(StaticMeshComponent.readPointer().add(0x850).readPointer(), "void", ["pointer", "uint8"]);SetCollisionResponseToAllChannels(StaticMeshComponent, 2); // 設置為0,1,3後, Cube會掉到地底
經過上述嘗試,可知子彈消失大概率與Collision無關。
猜測子彈是在擊中EditorCubeN
時執行了一段Destroy邏輯。
hook MyProjectProjectile
的OnHit
,打印調用棧:
复制代码 隐藏代码[onHit] enter:6f70d20aac is in libUE4.so offset: 0x6713aac6f71110b7c is in libUE4.so offset: 0x6b03b7c6f7125e9ac is in libUE4.so offset: 0x6c519ac6f7125e7d0 is in libUE4.so offset: 0x6c517d06f70139f24 is in libUE4.so offset: 0x5b2cf24// 16f72eb6a8c is in libUE4.so offset: 0x88a9a8c// 16f7356ff04 is in libUE4.so offset: 0x8f62f04// 06f72eb6bc4 is in libUE4.so offset: 0x88a9bc46f730c087c is in libUE4.so offset: 0x8ab387c6f738ee130 is in libUE4.so offset: 0x92e11306f71110b7c is in libUE4.so offset: 0x6b03b7c6f73907350 is in libUE4.so offset: 0x92fa3506f71110b7c is in libUE4.so offset: 0x6b03b7c6f71262774 is in libUE4.so offset: 0x6c557746f712629f0 is in libUE4.so offset: 0x6c559f06f7125d7ec is in libUE4.so offset: 0x6c507ec[onHit] leave: this.HitComp 0x6f74e76428
Destroy邏輯可能就在其中,但沒時間看了。。
以下部份是賽後的分析。
OnHit
最後調會return sub_88A8D2C((__int64)v4, 0, 1)
,而sub_88A8D2C
函數如下。
about_ActorDestroying
中有"ActorDestroying"
字符串,猜測會不會就是那段Destroy邏輯。
嘗試直接patch掉該函數,使其固定返回1
。
复制代码 隐藏代码Interceptor.replace(base.add(0x88A8D2C), newNativeCallback(() => { console.log("call 88A8D2C");return1;}, "int", []));
結果是射到EditorCubeN
時也會正常反彈,成功修復該異常點。
其他異常
在測試過程中還發現以下一些不確定算不算異常點的:
-
子彈要射到角色的腳底才會與角色發生碰撞,射在其他位置會直接穿過。 -
有時候射著射著人物就飛高高了。
完整代碼
frida -U -f com.ACE2025.Game -l final.js
复制代码 隐藏代码let GWorld = null;let GName = null;let GObject = null;let Offset = {//Class: UWorld UWorldToPersistentLevel: 0x58,// Class: ULevel ULevelToActors: 0xa0,// Class: FNamePool GNamesToFNamePool: 0x38, FNamePoolToCurrentBlock: 0x0,//Class: UObject UObjectToClassPrivate: 0x10, UObjectToFNameIndex: 0x18, UObjectToOuterPrivate: 0x20,//Class: FUObjectArray FUObjectArrayToTUObjectArray: 0x10,//Class: TUObjectArray TUObjectArrayToNumElements: 0x14,// Global FUObjectItemPadd: 0x0, FUObjectItemSize: 0x18,// Class: AActor AActorToRootComponent: 0x130,// Class: USceneComponent USceneComponentToRelativeLocation: 0x11c,// Class: ACharacter ACharacterToUSkeletalMeshComponent: 0x280,// Class: USkeletalMeshComponent USkeletalMeshComponentToSkeletalMesh: 0x478, USkeletalMeshComponentToUMaterialInterface: 0x448,// class: USkeletalMesh USkeletalMeshToFSkeletalMaterial: 0xd8}function startUE4(base) { console.log("UE4.base: ", base);/* Utils area */// 設置三件套function setupUE4(){ GWorld = base.add(0xAFAC398).readPointer(); GName = base.add(0xADF07C0); GObject = base.add(0xAE34A98); }function getName64(idx){ var ComparisonIndex = idx; var FNameEntryAllocator = GName.add(0x38); // 64位的FNameEntryAllocator偏移為0x38 var FNameBlockOffsetBits = 16 var FNameBlockOffsets = 65536 var Block = ComparisonIndex >> FNameBlockOffsetBits var Offset = ComparisonIndex & (FNameBlockOffsets - 1) var Blocks_Offset = 0x8 var Blocks = FNameEntryAllocator.add(Blocks_Offset) var FNameEntry = Blocks.add(Block * Process.pointerSize).readPointer().add(Offset * 2) var FNameEntryHeader = FNameEntry.readU16()// console.log("FNameEntry: ", hexdump(FNameEntry)); var isWide = FNameEntryHeader & 1 var Len = FNameEntryHeader >> 6// if (0 == isWide) {// console.log(`x1b[32m[+] ${FNameEntry.add(2).readCString(Len)}x1b[0m`)// }return FNameEntry.add(2).readCString(Len); } let ThirdPersonCharacter_obj = null; let FirstPersonCharacter_C_obj = null; let All_Objects = {}// 遍歷UObjectArray function travUObjectArray() { let TUObjectArray = GObject.add(Offset.FUObjectArrayToTUObjectArray); let Objects = TUObjectArray.readPointer().readPointer();for(let i = 0;; i++) {try { let objectItem = Objects.add(i * Offset.FUObjectItemSize).add(Offset.FUObjectItemPadd); let obj = objectItem.readPointer(); let objNameIdx = obj.add(Offset.UObjectToFNameIndex).readU32(); let objName = getName64(objNameIdx); All_Objects[objName] = obj;if (objName == "ThirdPersonCharacter") { ThirdPersonCharacter_obj = obj;// console.log("ThirdPersonCharacter_obj: ", ptr(obj)); }if (objName == "FirstPersonCharacter_C") { FirstPersonCharacter_C_obj = obj;// console.log("FirstPersonCharacter_C_obj: ", ptr(obj)); } } catch (error) { console.log(error);break } } }function getActorLocation(actor){ let RootComponent = ptr(actor).add(Offset.AActorToRootComponent).readPointer(); let RelativeLocation = RootComponent.add(Offset.USceneComponentToRelativeLocation); let x = RelativeLocation.add(0 * 4).readFloat() let y = RelativeLocation.add(1 * 4).readFloat() let z = RelativeLocation.add(2 * 4).readFloat()return [x, y, z] } function getFirstPersonThirdPersonDistance() {// static bool LineTraceSingle(const Object* WorldContextObject, const Vector Start, const Vector End, byte TraceChannel, bool bTraceComplex, out const Actor*[] ActorsToIgnore, byte DrawDebugType, out HitResult OutHit, bool bIgnoreSelf, LinearColor TraceColor, LinearColor TraceHitColor, float DrawTime);// 0x9471770 let LineTraceSingle = newNativeFunction(base.add(0x8D1AA78), "bool", ["pointer", "float", "float", "float", "float", "float", "float", "uint8", "bool", "pointer", "uint8", "pointer", "bool", "float", "float", "float", "float", "float", "float", "float", "float", "float"]); let buf1 = Memory.alloc(0x1000); let HitResult = Memory.alloc(0x1000); let start_loc = getActorLocation(FirstPersonCharacter_C_obj); let end_loc = getActorLocation(ThirdPersonCharacter_obj); let r = LineTraceSingle(FirstPersonCharacter_C_obj, start_loc[0], start_loc[1], start_loc[2], end_loc[0], end_loc[1], end_loc[2], 0, 0, buf1, 1, HitResult, 0, 255, 0, 0, 0, 0, 255, 0, 0, 10000); let HitResult_Distance = HitResult.add(0x8).readFloat();return HitResult_Distance; }function hook_utf16_cmp(){ Interceptor.attach(base.add(0x680B790), { onEnter: function(args) {this.a1 = args[1];// console.log("a0: ", args[0].readUtf16String());// console.log("a1: ", args[1].readUtf16String()); }, onLeave: function(retval) {if (this.a1.readUtf16String() == "EditorCube8") { retval.replace(5); }// console.log("res: ", retval); } }) }function hook_process_before_shoot(){ Interceptor.attach(base.add(0x670F110), { onEnter: function(args) { let val = args[0].add(0x528).readU8(); args[0].add(0x528).writeU8(val | 1); console.log("[process_before_shoot] a1[0x14A] & 1: ", args[0].add(0x528).readU8() & 1) }, onLeave: function(retval) { } }) }function fix_MuzzleLocation(){ Interceptor.attach(base.add(0x670F110), { onEnter: function(args) { console.log("[process_before_shoot]");/* API */// void K2_SetRelativeRotation(Rotator NewRotation, bool bSweep, out HitResult SweepHitResult, bool bTeleport);// 0x9597380 let K2_SetRelativeRotation = newNativeFunction(base.add(0x8AE6F00), "void", ["pointer", "float", "float", "float", "bool", "pointer", "bool"]);K2_SetRelativeRotation(All_Objects["MuzzleLocation"], 0, 90, 0, 0, ptr(0), 0);/* Prop */ let RelativeLocation = All_Objects["MuzzleLocation"].add(0x11c); let RelativeRotation = All_Objects["MuzzleLocation"].add(0x128);// console.log("location: ", JSON.stringify(readVector(RelativeLocation)))// console.log("rotation: ", JSON.stringify(readVector(RelativeRotation)))// Replace let MuzzleLocation = args[0].add(0x4D8) MuzzleLocation.writePointer(All_Objects["MuzzleLocation"]) }, onLeave: function(retval) { } }) }function fix_EditorCubeN_bullet_problem(){ Interceptor.replace(base.add(0x88A8D2C), newNativeCallback(() => {return1; }, "int", [])); }function bypassWallhack(){ let SetRenderInMainPass = newNativeFunction(base.add(0x8AB9E58), "void", ["pointer", "bool"]); let ThirdPersonCharacter_Mesh = ThirdPersonCharacter_obj.add(Offset.ACharacterToUSkeletalMeshComponent).readPointer(); Interceptor.attach(base.add(0x6c50500), { onEnter: function(args) {if (getFirstPersonThirdPersonDistance() == 0) { console.log("can see ThirdPerson")SetRenderInMainPass(ThirdPersonCharacter_Mesh, 1); } else { console.log("can not see ThirdPerson")SetRenderInMainPass(ThirdPersonCharacter_Mesh, 0); } } }) }/* call area */setupUE4();travUObjectArray();hook_utf16_cmp(); // bypass: aimbothook_process_before_shoot(); // bypass: rand bullet shoot location (fixed )fix_EditorCubeN_bullet_problem(); // bypass: EditorCubeN problemfix_MuzzleLocation(); // bypass: fix and replace the right MuzzleLocationbypassWallhack(); // bypass: fix wallhack in a different way}function hook_pthread(){ var pthread_create_addr = Module.findExportByName(null, 'pthread_create'); console.log("pthread_create_addr,", pthread_create_addr); var pthread_create = newNativeFunction(pthread_create_addr, "int", ["pointer", "pointer", "pointer", "pointer"]); Interceptor.replace(pthread_create_addr, newNativeCallback(function (parg0, parg1, parg2, parg3) { var so_name = Process.findModuleByAddress(parg2).name; var so_path = Process.findModuleByAddress(parg2).path; var so_base = Module.getBaseAddress(so_name); var offset = parg2 - so_base;// console.log("so_name", so_name, "offset", offset, "path", so_path, "parg2", parg2); var PC = 0;if ((so_name.indexOf("libGame.so") > -1)) { console.log("find thread func offset", so_name, offset);if ((7068 === offset)) { console.log("anti bypass"); } else { PC = pthread_create(parg0, parg1, parg2, parg3); console.log("ordinary sequence", PC) } } else { PC = pthread_create(parg0, parg1, parg2, parg3);// console.log("ordinary sequence", PC) }return PC; }, "int", ["pointer", "pointer", "pointer", "pointer"]))}function main(){hook_pthread();setTimeout(() => {startUE4(Module.findBaseAddress("libUE4.so")); }, 3500);}setImmediate(main)
結語
今年跟上年一樣是UE4的題型,猜到了會出UE4,賽前本想找些遊戲來練練手,但一直沒找到合適的,只能說可惜了。這也導致了比賽前2天基本都在熟悉UE4,直到最後也沒有完整地修復幾個異常點。
本以為決賽無望的,沒想到邭馔镁尤贿M了,算是圓了上一年的遺憾吧。
原文始发于微信公众号(吾爱破解论坛):2025騰訊遊戲安全大賽(安卓初賽)
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论