2025騰訊遊戲安全大賽(安卓初賽)

admin 2025年4月23日01:18:32评论0 views字数 31153阅读103分50秒阅读模式
作者坛账号:ngiokweng

前言

第一次聽說這比賽是上年偶然和舍友聊天時他告訴我的,沒想到還有以遊戲安全為主的比賽,當時看到有安卓的賽道就報名了,然後比賽時就被那門卡了2天,然後就沒有然後了。

今年沒意外的話是我大學生涯的最後一年,也許也是最後一年打這個比賽了吧,下年也不知道有沒有空看看題。

以下是我的解題記錄,一部份是比賽時寫的,一部份是賽後補充的,有寫錯的還請指正。

前置準備

版本:4.27

2025騰訊遊戲安全大賽(安卓初賽)

GName:0xADF07C0

2025騰訊遊戲安全大賽(安卓初賽)

GObject:0xAE34A98

2025騰訊遊戲安全大賽(安卓初賽)

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逆向,無任何反調試。

2025騰訊遊戲安全大賽(安卓初賽)

速度異常

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

2025騰訊遊戲安全大賽(安卓初賽)

進入後會看到明顯的控制流平坦化,先不管。

打斷點進入case 12623,分析後發現就是通過/proc/self/maps獲取libUE4.so的基址。

2025騰訊遊戲安全大賽(安卓初賽)

之後本想手動還原下控制流,但突然想起IDA有個D-810插件貌似能解控制流混淆,嘗試下,發現效果很好。

獲取了libUE4_base後會賦給infos[19]

2025騰訊遊戲安全大賽(安卓初賽)

然後*(_QWORD *)(libUE4_base_1 + 0xAFAC398)獲取了libUE4.so的一個全局變量,猜測是GWorld

2025騰訊遊戲安全大賽(安卓初賽)

用ue4dumper來驗證,發現能順利dump出SDK,由此可知0xAFAC398的確是GWorld

記dump出來的文件為SDKW.txt

 复制代码 隐藏代码./ue4dumper64 --sdkw --newue+ --gname 0xADF07C0 --gworld 0xAFAC398 --package com.ACE2025.Game

獲取GWorld後,就能通過遍歷其中的屬性定位到FirstPersonCharacter_C,具體原理如下,這是用frida實現的。

其中用了vtabsUObject的第0個成員屬性,虛表 )的函數偏移是否等於0xA63BE28來確定是否FirstPersonCharacter_C對象,0xA63BE28大概是FirstPersonCharacter_C的一個特徵?

最終通過修改CharacterMovementComponentMaxAccelerationMaxWalkSpeed來改變人物速度。

 复制代码 隐藏代码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

2025騰訊遊戲安全大賽(安卓初賽)

IDA跳到0x8B387C0,如下:

2025騰訊遊戲安全大賽(安卓初賽)

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是射擊的回調函數,自瞄邏輯應該就在裡面。

0x670F110process_before_shoot

 复制代码 隐藏代码Interceptor.replace(base.add(0x670F110), newNativeCallback(() => {return1;}, "int", []))

0x670F110中從調用mb_aimbot處向上分析,發現是否調用mb_aimbot邏輯是由sub_680B790(v32, "E")決定的。

2025騰訊遊戲安全大賽(安卓初賽)

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);        }    })}

輸出如下,可以看出是字符串對比函數,resa0a1第1個不相等字符的差值,若相等則為0( 不區分大小寫 )。記sub_680B790utf16_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

嘗試在a1EditorCube8時將返回值固定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開始看,一開始先遍歷自瞄目標。

2025騰訊遊戲安全大賽(安卓初賽)

然後調用calcTargetOffset計算自瞄值,然後根據這個值來設置CameraRotation( 人物相機的轉向,使它朝向目標以實現自瞄的效果 )。

2025騰訊遊戲安全大賽(安卓初賽)

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]}

子彈發射位置異常

可以明顯看出子彈發射的起始位置是隨機的。

猜測可能與MyProjectCharacterGunOffset有關。

 复制代码 隐藏代码Class: MyProjectCharacter.Character.Pawn.Actor.Object// ...        Vector GunOffset;//[Offset: 0x500, Size: 0xc]

GunOffset下硬斷( 讀 )。

2025騰訊遊戲安全大賽(安卓初賽)

命中如下兩處地址:

 复制代码 隐藏代码// libUE4 base: 6f6c74e0001. PC: 0x6F7307EA6C (0x6930A6C)   LR: 0x6F7307EA682. PC: 0x6F7307EA7C (0x6930A7C)   LR: 0x6F7307EA68

0x6930A6C所在函數是sub_6930A3C

2025騰訊遊戲安全大賽(安卓初賽)

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()生成隨機值,猜測這與槍口的隨機有關。

2025騰訊遊戲安全大賽(安卓初賽)

嘗試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位置。

2025騰訊遊戲安全大賽(安卓初賽)

 复制代码 隐藏代码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] & 10,會調用about_bullt_loc1生成一些隨機值,調用about_bullt_loc2生成一個基於GunOffset等參數而來的值,最終傳入mb_process_bullet_shoot_loc做最後的處理。

2025騰訊遊戲安全大賽(安卓初賽)

而當a1[0x14A] & 11時,則會從VR_MuzzleLocation裡獲取一些參數,最終同樣傳入mb_process_bullet_shoot_loc

注:通過hook所在函數,將*((QWORD*)a1 + 155)當成UObject來打印它的名字,從而確定它是VR_MuzzleLocation對象。

2025騰訊遊戲安全大賽(安卓初賽)

2025騰訊遊戲安全大賽(安卓初賽)

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_SetRelativeLocationK2_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"], 09000ptr(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_CThirdPersonCharacter_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"]);

嘗試二:設置bDisableDepthTest0。( 沒效果 )

 复制代码 隐藏代码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"]);

以下部份是賽後的分析。

找資料時發現類似這遊戲的人物透視效果基本上有以下兩種實現思路:

  1. 通過Disable Depth Test ( 參考 )。
  2. 通過Custom Depth ( 參考 )。

但嘗試後發現遊戲似乎不是用上述方法實現透視效果的( 不太確定,也有可能是我修改的地方不對 )?

找了很久都沒有什麼思路,最終只好退而求其次,用一種「掩耳盜鈴」的方式來修復,具體思路如下:

  • 調用KismetSystemLibrary的靜態函數LineTraceSingle來獲取FirstPersonCharacter_CThirdPersonCharacter_C之間的HitResult
  • 分析HitResult,會發現ThirdPersonCharacter_C沒有被遮擋時HitResult.Distance0,否則為二者之間的距離。
  • 因此可以根據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], 00, buf1, 1, HitResult, 025500002550010000);    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 MyProjectProjectileOnHit,打印調用棧:

 复制代码 隐藏代码[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邏輯。

2025騰訊遊戲安全大賽(安卓初賽)

嘗試直接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], 00, buf1, 1, HitResult, 025500002550010000);        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"], 09000ptr(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騰訊遊戲安全大賽(安卓初賽)

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年4月23日01:18:32
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   2025騰訊遊戲安全大賽(安卓初賽)https://cn-sec.com/archives/3969637.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息