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

查看 81|回复 9
作者:ngiokweng   
前言
第一次聽說這比賽是上年偶然和舍友聊天時他告訴我的,沒想到還有以遊戲安全為主的比賽,當時看到有安卓的賽道就報名了,然後比賽時就被那門卡了2天,然後就沒有然後了。
今年沒意外的話是我大學生涯的最後一年,也許也是最後一年打這個比賽了吧,下年也不知道有沒有空看看題。
以下是我的解題記錄,一部份是比賽時寫的,一部份是賽後補充的,有寫錯的還請指正。
前置準備
版本:4.27


image.png (21.99 KB, 下载次数: 0)
下载附件
2025-4-14 09:41 上传

GName:0xADF07C0


image1.png (20.43 KB, 下载次数: 0)
下载附件
2025-4-14 09:41 上传

GObject:0xAE34A98


image2.png (20.25 KB, 下载次数: 1)
下载附件
2025-4-14 09:41 上传

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


image3.png (211.81 KB, 下载次数: 0)
下载附件
2025-4-14 09:41 上传

速度異常
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 = new NativeFunction(pthread_create_addr, "int", ["pointer", "pointer", "pointer", "pointer"]);
    Interceptor.replace(pthread_create_addr, new NativeCallback(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。


image4.png (16.93 KB, 下载次数: 0)
下载附件
2025-4-14 09:41 上传

進入後會看到明顯的控制流平坦化,先不管。
打斷點進入case 12623,分析後發現就是通過/proc/self/maps獲取libUE4.so的基址。


image5.png (35.57 KB, 下载次数: 0)
下载附件
2025-4-14 09:41 上传

之後本想手動還原下控制流,但突然想起IDA有個D-810插件貌似能解控制流混淆,嘗試下,發現效果很好。
獲取了libUE4_base後會賦給infos[19]。


image6.png (59.93 KB, 下载次数: 0)
下载附件
2025-4-14 09:41 上传

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


image7.png (22.61 KB, 下载次数: 0)
下载附件
2025-4-14 09:41 上传

用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()       // PersistentLevel
let StreamingLevels = PersistentLevel.add(0x98).readPointer();    // StreamingLevelsToConsider.StreamingLevels
let StreamingLevelsNum = PersistentLevel.add(0xA0).readU32();
let FirstPersonCharacter_C = null;
for(let i = 0; i
自瞄異常
在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 VR
    float BaseTurnRate;//[Offset: 0x4f8, Size: 0x4]         // 左右轉向速率
    float BaseLookUpRate;//[Offset: 0x4fc, Size: 0x4]       // 上下轉向速率
    Vector GunOffset;//[Offset: 0x500, Size: 0xc]
    class MyProjectProjectile* 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]
    void OnHit(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


image8.png (34.92 KB, 下载次数: 0)
下载附件
2025-4-14 09:49 上传

IDA跳到0x8B387C0,如下:


image9.png (46.95 KB, 下载次数: 0)
下载附件
2025-4-14 09:49 上传

記0x8B387C0所在函數為mb_aimbot,嘗試直接patch掉mb_aimbot,發現patch後人物無法轉動視角。
function patch_mb_aimbot() {
    Interceptor.replace(base.add(0x8B3861C), new NativeCallback(() => {
        console.log("patch mb_aimbot")
    }, "void", []))
}
hook mb_aimbot,打印調用棧,未點擊時,調用棧如下
[hook_aimbot]
799fa8e604 is in libUE4.so offset: 0x8f9b604
799fa92444 is in libUE4.so offset: 0x8f9f444
799fa9a358 is in libUE4.so offset: 0x8fa7358
799fcf0b8c is in libUE4.so offset: 0x91fdb8c
799d2c1bb0 is in libUE4.so offset: 0x67cebb0
799d2c1730 is in libUE4.so offset: 0x67ce730
799d2c0e24 is in libUE4.so offset: 0x67cde24
799fcecc04 is in libUE4.so offset: 0x91f9c04
799fcea3bc is in libUE4.so offset: 0x91f73bc
799f82e760 is in libUE4.so offset: 0x8d3b760
799f6f98f0 is in libUE4.so offset: 0x8c068f0
799d93f614 is in libUE4.so offset: 0x6e4c614
799c5ee728 is in libUE4.so offset: 0x5afb728
799c5e83bc is in libUE4.so offset: 0x5af53bc
799c5e6514 is in libUE4.so offset: 0x5af3514
[hook_aimbot]
79a03df660 is in libUE4.so offset: 0x98ec660
799fcf0b8c is in libUE4.so offset: 0x91fdb8c
799d2c1bb0 is in libUE4.so offset: 0x67cebb0
799d2c1730 is in libUE4.so offset: 0x67ce730
799d2c0e24 is in libUE4.so offset: 0x67cde24
799fcecc04 is in libUE4.so offset: 0x91f9c04
799fcea3bc is in libUE4.so offset: 0x91f73bc
799f82e760 is in libUE4.so offset: 0x8d3b760
799f6f98f0 is in libUE4.so offset: 0x8c068f0
799d93f614 is in libUE4.so offset: 0x6e4c614
799c5ee728 is in libUE4.so offset: 0x5afb728
799c5e83bc is in libUE4.so offset: 0x5af53bc
799c5e6514 is in libUE4.so offset: 0x5af3514
點擊後,多了一個不同的調用棧0x670f3fc。
[hook_aimbot]
799d2f83fc is in libUE4.so offset: 0x670f3fc
7a136f0c94 is in libart.so offset: 0x2e6c94
799d2f8eb0 is in libUE4.so offset: 0x670feb0
799fe51e38 is in libUE4.so offset: 0x9268e38
799fe4fe04 is in libUE4.so offset: 0x9266e04
799fb8958c is in libUE4.so offset: 0x8fa058c
799fb886f4 is in libUE4.so offset: 0x8f9f6f4
799d3b9420 is in libUE4.so offset: 0x67d0420
799fb88374 is in libUE4.so offset: 0x8f9f374
799f49b7bc is in libUE4.so offset: 0x88b27bc
799fb90358 is in libUE4.so offset: 0x8fa7358
799fde6b8c is in libUE4.so offset: 0x91fdb8c
799d3b7bb0 is in libUE4.so offset: 0x67cebb0
799d3b7730 is in libUE4.so offset: 0x67ce730
799d3b6e24 is in libUE4.so offset: 0x67cde24
799fde2c04 is in libUE4.so offset: 0x91f9c04
嘗試patch掉0x670f3fc所在函數0x670F110,雖然點擊後不會再自動瞄到某處,但子彈射不出。
由此猜測0x670F110是射擊的回調函數,自瞄邏輯應該就在裡面。
記0x670F110為process_before_shoot。
Interceptor.replace(base.add(0x670F110), new NativeCallback(() => {
    return 1;
}, "int", []))
在0x670F110中從調用mb_aimbot處向上分析,發現是否調用mb_aimbot邏輯是由sub_680B790(v32, "E")決定的。


image10.png (34.4 KB, 下载次数: 0)
下载附件
2025-4-14 09:49 上传

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:  BigWall
a1:  EditorCube8
res:  0xfffffffd
a0:  BigWall2   
a1:  EditorCube8
res:  0xfffffffd
a0:  EditorCube10
a1:  EditorCube8
res:  0xfffffff9
a0:  EditorCube11
a1:  EditorCube8
res:  0xfffffff9
a0:  EditorCube12
a1:  EditorCube8
res:  0xfffffff9
a0:  EditorCube13
a1:  EditorCube8
res:  0xfffffff9
a0:  EditorCube14
a1:  EditorCube8
res:  0xfffffff9
a0:  EditorCube15
a1:  EditorCube8
res:  0xfffffff9
a0:  EditorCube16
a1:  EditorCube8
res:  0xfffffff9
a0:  EditorCube17
a1:  EditorCube8
res:  0xfffffff9
a0:  EditorCube18
a1:  EditorCube8
res:  0xfffffff9
a0:  EditorCube19
a1:  EditorCube8
res:  0xfffffff9
a0:  EditorCube20
a1:  EditorCube8
res:  0xfffffffa
a0:  EditorCube21
a1:  EditorCube8
res:  0xfffffffa
a0:  EditorCube8
a1:  EditorCube8
res:  0x0
a0:  EditorCube9
a1:  EditorCube8
res:  0x1
a0:  Floor_12
a1:  EditorCube8
res:  0x1
a0:  Wall1
a1:  EditorCube8
res:  0x12
a0:  Wall2_11
a1:  EditorCube8
res:  0x12
a0:  Wall3
a1:  EditorCube8
res:  0x12
a0:  Wall4
a1:  EditorCube8
res:  0x12
a0:  ../../../MyProject/Saved/Config/Android/Engine.ini
a1:  ../../../MyProject/Saved/Config/Android/Engine.ini
res:  0x0
a0:  true
a1:  True
res:  0x0
a0:  Android
a1:  Android
res:  0x0
a0:  Android
a1:  Android
res:  0x0
a0:  Android
a1:  Android
res:  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開始看,一開始先遍歷自瞄目標。


image11.png (44.83 KB, 下载次数: 0)
下载附件
2025-4-14 09:49 上传

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


image12.png (31.35 KB, 下载次数: 0)
下载附件
2025-4-14 09:49 上传

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) angleX = 90 + Math.abs(Math.atan(x / y)) / Math.PI * 180;
    if (x  0 && y  360) {
        angleX -= 360;
    }
    angleY = Math.atan(z / Math.sqrt(x * x + y * y)) / Math.PI * 180;
    if (angleY
子彈發射位置異常
可以明顯看出子彈發射的起始位置是隨機的。
猜測可能與MyProjectCharacter的GunOffset有關。
Class: MyProjectCharacter.Character.Pawn.Actor.Object
        // ...
        Vector GunOffset;//[Offset: 0x500, Size: 0xc]
對GunOffset下硬斷( 讀 )。


image13.png (13.86 KB, 下载次数: 0)
下载附件
2025-4-14 09:49 上传

命中如下兩處地址:
// libUE4 base: 6f6c74e000
1. PC: 0x6F7307EA6C (0x6930A6C)   LR: 0x6F7307EA68
2. PC: 0x6F7307EA7C (0x6930A7C)   LR: 0x6F7307EA68
0x6930A6C所在函數是sub_6930A3C。


image14.png (29.07 KB, 下载次数: 0)
下载附件
2025-4-14 09:49 上传

hook sub_6930A3C打印調用棧。
其中0x670f658位於0x670F110函數( 即process_before_shoot )。
6f72e75e24 is in libUE4.so offset: 0x670fe24
6f72e75658 is in libUE4.so offset: 0x670f658 (位於0x670F110)
6f72e75eb0 is in libUE4.so offset: 0x670feb0
6f72e75eb0 is in libUE4.so offset: 0x670feb0
6f759cee38 is in libUE4.so offset: 0x9268e38
6f759cce04 is in libUE4.so offset: 0x9266e04
6fe951f09c is in libart.so offset: 0x59b09c
process_before_shoot中有調用rand()生成隨機值,猜測這與槍口的隨機有關。


image15.png (25.53 KB, 下载次数: 0)
下载附件
2025-4-14 09:49 上传

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


image16.png (31.37 KB, 下载次数: 0)
下载附件
2025-4-14 09:49 上传

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做最後的處理。


image17.png (66.69 KB, 下载次数: 0)
下载附件
2025-4-14 09:49 上传

而當a1[0x14A] & 1為1時,則會從VR_MuzzleLocation裡獲取一些參數,最終同樣傳入mb_process_bullet_shoot_loc。
注:通過hook所在函數,將*((QWORD*)a1 + 155)當成UObject來打印它的名字,從而確定它是VR_MuzzleLocation對象。


image18.png (77.06 KB, 下载次数: 0)
下载附件
2025-4-14 09:49 上传



image19.png (57.48 KB, 下载次数: 0)
下载附件
2025-4-14 09:49 上传

但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 = new NativeFunction(base.add(0x8AE6D70), "void", ["pointer", "float", "float", "float", "bool", "pointer", "bool"]);
            let K2_SetRelativeRotation = new NativeFunction(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_C
Class: FirstPersonCharacter_C
ObjectPtr: 0x71694840c0
ClassPtr: 0x7169582700
[0x3c53]:
Name: ThirdPersonCharacter
Class: ThirdPersonCharacter_C
ObjectPtr: 0x7168662630
ClassPtr: 0x716958c100
嘗試一:替換Material。( 沒效果 )
let ThirdPersonCharacter_Mesh = ThirdPersonCharacter_obj.add(Offset.ACharacterToUSkeletalMeshComponent).readPointer();
let SetMaterial = new NativeFunction(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 = new NativeFunction(ThirdPersonCharacter_Mesh.readPointer().add(0x6e8).readPointer(), "int", ["pointer"]);
let GetMaterial = new NativeFunction(ThirdPersonCharacter_Mesh.readPointer().add(0x590).readPointer(), "pointer", ["pointer", "int"]);
let NumMaterials = GetNumMaterials(ThirdPersonCharacter_Mesh)
console.log("NumMaterials: ", NumMaterials);
for(let i = 0; i
嘗試三:利用SetTexture隨便設置一個Texture。( 沒效果 )
let ThirdPersonCharacter_Mesh = ThirdPersonCharacter_obj.add(Offset.ACharacterToUSkeletalMeshComponent).readPointer();
/* API */
// void SetTexture(Texture* InTexture);// 0x95efb5c
let SetTexture = new NativeFunction(base.add(0x8B2C4CC), "void", ["pointer", "pointer"]);
// Texture* GetTexture();// 0x95efa98
let GetTexture = new NativeFunction(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"]);
以下部份是賽後的分析。
找資料時發現類似這遊戲的人物透視效果基本上有以下兩種實現思路:
[ol]
  • 通過Disable Depth Test ( 參考 )。
  • 通過Custom Depth ( 參考 )。
    [/ol]
    但嘗試後發現遊戲似乎不是用上述方法實現透視效果的( 不太確定,也有可能是我修改的地方不對 )?
    找了很久都沒有什麼思路,最終只好退而求其次,用一種「掩耳盜鈴」的方式來修復,具體思路如下:
  • 調用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 = new NativeFunction(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 = new NativeFunction(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: EditorCube8
    Class: StaticMeshActor
    ObjectPtr: 0x7170c09f40
    ClassPtr: 0x717e7c0100
    [0x3c26]:
    Name: EditorCube10
    Class: StaticMeshActor
    ObjectPtr: 0x7170c0ba40
    ClassPtr: 0x717e7c0100
    子彈射在EditorCubeN上會瞬間消失,但EditorCubeN是有被擊退的效果,而子彈射在黑色的Cube上能正常被反彈。
    嘗試一:看看是否因為物理模擬未啟用。( 沒效果 )
    let StaticMeshComponent = All_Objects["EditorCube8"].add(0x220).readPointer();
    let SetSimulatePhysics = new NativeFunction(StaticMeshComponent.readPointer().add(0x5D0).readPointer(), "void", ["pointer", "bool"]);
    let SetNotifyRigidBodyCollision = new NativeFunction(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 = new NativeFunction(StaticMeshComponent.readPointer().add(0x2D0).readPointer(), "pointer", ["pointer"]);
    let GetNumMaterials = new NativeFunction(StaticMeshComponent.readPointer().add(0x6e8).readPointer(), "int", ["pointer"]);
    let GetMaterial = new NativeFunction(StaticMeshComponent.readPointer().add(0x590).readPointer(), "pointer", ["pointer", "int"]);
    /* Prop */
    let NumMaterials = GetNumMaterials(StaticMeshComponent)
    console.log("NumMaterials: ", NumMaterials);
    for(let i = 0; i
    嘗試三:設置與所有物體的碰撞響應都為Block( 具體值是2 )。( 沒效果 )
    let StaticMeshComponent = All_Objects["EditorCube8"].add(0x220).readPointer();
    let SetCollisionResponseToAllChannels = new NativeFunction(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: 0x6713aac
    6f71110b7c is in libUE4.so offset: 0x6b03b7c
    6f7125e9ac is in libUE4.so offset: 0x6c519ac
    6f7125e7d0 is in libUE4.so offset: 0x6c517d0
    6f70139f24 is in libUE4.so offset: 0x5b2cf24  // 1
    6f72eb6a8c is in libUE4.so offset: 0x88a9a8c  // 1
    6f7356ff04 is in libUE4.so offset: 0x8f62f04  // 0
    6f72eb6bc4 is in libUE4.so offset: 0x88a9bc4
    6f730c087c is in libUE4.so offset: 0x8ab387c
    6f738ee130 is in libUE4.so offset: 0x92e1130
    6f71110b7c is in libUE4.so offset: 0x6b03b7c
    6f73907350 is in libUE4.so offset: 0x92fa350
    6f71110b7c is in libUE4.so offset: 0x6b03b7c
    6f71262774 is in libUE4.so offset: 0x6c55774
    6f712629f0 is in libUE4.so offset: 0x6c559f0
    6f7125d7ec 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邏輯。


    image20.png (40.07 KB, 下载次数: 0)
    下载附件
    2025-4-14 09:49 上传

    嘗試直接patch掉該函數,使其固定返回1。
    Interceptor.replace(base.add(0x88A8D2C), new NativeCallback(() => {
        console.log("call 88A8D2C");
        return 1;
    }, "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 = new NativeFunction(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 = new NativeFunction(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), new NativeCallback(() => {
                return 1;
            }, "int", []));
        }
        function bypassWallhack() {
            let SetRenderInMainPass = new NativeFunction(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: aimbot
        hook_process_before_shoot();        // bypass: rand bullet shoot location (fixed )
        fix_EditorCubeN_bullet_problem();   // bypass: EditorCubeN problem
        fix_MuzzleLocation();               // bypass: fix and replace the right MuzzleLocation
        bypassWallhack();                   // 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 = new NativeFunction(pthread_create_addr, "int", ["pointer", "pointer", "pointer", "pointer"]);
        Interceptor.replace(pthread_create_addr, new NativeCallback(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了,算是圓了上一年的遺憾吧。

    下载次数, 下载附件

  • murasame520   

    太强了,跟着大佬复现!
    ngiokweng
    OP
      


    murasame520 发表于 2025-4-14 10:01
    太强了,跟着大佬复现!

    逆向太難了, 共勉
    tk0056   

    膜拜大佬
    colaraa   

    厉害,学习了
    lakal   

    逆向太難了
    Love0912   

    感谢大佬的精彩分享,大佬是台湾同胞么。一手的繁体字。。
    maoliangs   

    膜拜大佬
    guhuishou   

    最后一句没想到什么了?我这个门外汉 全文就看得懂中文。没想到最后没看懂了。
    guhuishou   

    学习了,app逆向入门确实有难度
    您需要登录后才可以回帖 登录 | 立即注册

    返回顶部