前言 第一次聽說這比賽是上年偶然和舍友聊天時他告訴我的,沒想到還有以遊戲安全為主的比賽,當時看到有安卓的賽道就報名了,然後比賽時就被那門卡了2天,然後就沒有然後了。
今年沒意外的話是我大學生涯的最後一年,也許也是最後一年打這個比賽了吧,下年也不知道有沒有空看看題。
以下是我的解題記錄,一部份是比賽時寫的,一部份是賽後補充的,有寫錯的還請指正。
前置準備 版本:4.27
GName:0xADF07C0
GObject:0xAE34A98
dump sdk by GObject,記為SDKO.txt
1 ./ue4dumper64 --sdku --newue+ --gname 0xADF07C0 --guobj 0xAE34A98 --package com.ACE2025.Game
dump all objects,記為Objects.txt
1 2 ./ue4dumper64 --objs --newue+ --gname 0xADF07C0 --guobj 0xAE34A98 --package com.ACE2025.Game
異常點分析與修復 題目說明如下,純粹的UE4逆向,無任何反調試。
速度異常 hook pthread_create
,patch掉libGame.so
創建唯一一個線程後,速度不再異常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 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; 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); } 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
。
1 ./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
來改變人物速度。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 let GWorld = base.add (0xAFAC398 ).readPointer (); let PersistentLevel = GWorld .add (0x30 ).readPointer () let StreamingLevels = PersistentLevel .add (0x98 ).readPointer (); let 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 ) { FirstPersonCharacter _C = StreamingLevel ; } } let CharacterMovementComponent = FirstPersonCharacter _C.add (0x288 ).readPointer ()CharacterMovementComponent .add (0x1a0 ).writeFloat (1000000000 )CharacterMovementComponent .add (0x18c ).writeFloat (1000000000 )
自瞄異常 在SDKO.txt
裡可以看到我的角色類裡有個ProjectileClass
成員,而它有個OnHit
成員函數
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 Class: MyProjectCharacter.Character.Pawn.Actor.Object SkeletalMeshComponent* Mesh1P; SkeletalMeshComponent* FP_Gun; SceneComponent* FP_MuzzleLocation; SkeletalMeshComponent* VR_Gun; SceneComponent* VR_MuzzleLocation; CameraComponent* FirstPersonCameraComponent; MotionControllerComponent* R_MotionController; MotionControllerComponent* L_MotionController; float BaseTurnRate; float BaseLookUpRate; Vector GunOffset; class MyProjectProjectile * ProjectileClass; SoundBase* FireSound; AnimMontage* FireAnimation; bool bUsingMotionControllers; float RecoilPitch; float RecoilYaw; float RecoilRecoverySpeed; float RecoilAccumulationRate; Class: MyProjectProjectile.Actor.Object SphereComponent* CollisionComp; ProjectileMovementComponent* ProjectileMovement; void OnHit (PrimitiveComponent* HitComp, Actor* OtherActor, PrimitiveComponent* OtherComp, Vector NormalImpulse, out const HitResult Hit) ;
嘗試hook OnHit
,分別在enter和leave時打印CameraRotation
,發現兩者相等,即在enter前就已經完成自瞄,代表相關的自瞄邏輯不在這裡。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function hook_onHit ( ) { Interceptor .attach (base.add (0x6711D34 ), { onEnter : function (args ) { console .log ("[onHit] enter: " , JSON .stringify (getCameraRotation ())) }, onLeave : function ( ) { console .log ("[onHit] leave: " , JSON .stringify (getCameraRotation ())) } }) }
對CameraRotation
下硬斷( 寫 ),命中信息如下:
命中PC:0x799F6637C0
libUE4 base:0x7996b2b000
計算得Offset為0x8B387C0
IDA跳到0x8B387C0
,如下:
記0x8B387C0
所在函數為mb_aimbot
,嘗試直接patch掉mb_aimbot
,發現patch後人物無法轉動視角。
1 2 3 4 5 function patch_mb_aimbot ( ) { Interceptor .replace (base.add (0x8B3861C ), new NativeCallback (() => { console .log ("patch mb_aimbot" ) }, "void" , [])) }
hook mb_aimbot
,打印調用棧,未點擊時,調用棧如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 [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
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 [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
。
1 2 3 Interceptor .replace (base.add (0x670F110 ), new NativeCallback (() => { return 1 ; }, "int" , []))
在0x670F110
中從調用mb_aimbot
處向上分析,發現是否調用mb_aimbot
邏輯是由sub_680B790(v32, "E")
決定的。
hook sub_680B790
,打印參數和返回值。
注:hexdump
後可知是unicode編碼的字符串,因此要用readUtf16String
。
1 2 3 4 5 6 7 8 9 10 11 12 13 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
對比,明顯它就是自瞄的目標,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 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 _12a1 : EditorCube8 res : 0x1 a0 : Wall1 a1 : EditorCube8 res : 0x12 a0 : Wall2 _11a1 : 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
的值。
1 2 3 4 5 6 7 8 9 10 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向量來計算。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 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
有關。
1 2 3 Class: MyProjectCharacter.Character.Pawn.Actor.Object Vector GunOffset;
對GunOffset
下硬斷( 讀 )。
命中如下兩處地址:
1 2 3 1. PC : 0x6F7307EA6C (0x6930A6C ) LR : 0x6F7307EA68 2. PC : 0x6F7307EA7C (0x6930A7C ) LR : 0x6F7307EA68
0x6930A6C
所在函數是sub_6930A3C
。
hook sub_6930A3C
打印調用棧。
其中0x670f658
位於0x670F110
函數( 即process_before_shoot
)。
1 2 3 4 5 6 7 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()
生成隨機值,猜測這與槍口的隨機有關。
嘗試hook rand
固定其返回值。
1 2 3 4 5 6 7 8 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
位置。
1 2 3 4 5 6 7 8 9 10 11 12 13 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
才對。
因此合理懷疑這也是一個異常點。
1 2 3 4 5 6 Class: MyProjectCharacter.Character.Pawn.Actor.Object SkeletalMeshComponent* Mesh1P; SkeletalMeshComponent* FP_Gun; SceneComponent* FP_MuzzleLocation; SkeletalMeshComponent* VR_Gun; SceneComponent* VR_MuzzleLocation;
hook process_before_shoot
,從IDA裡可知args[0].add(0x4D8)
保存著VR_MuzzleLocation
對象的地址,嘗試將它改為FP_MuzzleLocation
的地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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
類,其中有以下這兩個屬性:
1 2 3 4 Class : SceneComponent .ActorComponent .Object Vector RelativeLocation ; Rotator RelativeRotation ;
利用K2_SetRelativeLocation
和K2_SetRelativeRotation
函數來修改。通過不斷嘗試,發現只需要將Rotation設置為[0, 90, 0]
即可,相當於旋轉了90度。完整修複代碼如下:
注:不能直接通過內存來修改,要用API來修改。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 function fix_MuzzleLocation ( ) { Interceptor .attach (base.add (0x670F110 ), { onEnter : function (args ) { console .log ("[process_before_shoot]" ); 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_SetRelativeRotation (All _Objects["MuzzleLocation" ], 0 , 90 , 0 , 0 , ptr (0 ), 0 ); 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 ))) let MuzzleLocation = args[0 ].add (0x4D8 ) MuzzleLocation .writePointer (All _Objects["MuzzleLocation" ]) }, onLeave : function (retval ) { } }) }
透視分析 在Objects.txt
裡可以看到FirstPersonCharacter_C
和ThirdPersonCharacter_C
,它們分別是我控制的人物和假人。
1 2 3 4 5 6 7 8 9 10 11 [0x3c05 ]: Name: FirstPersonCharacter_C Class: FirstPersonCharacter_C ObjectPtr: 0x71694840c0 ClassPtr: 0x7169582700 [0x3c53 ]: Name: ThirdPersonCharacter Class: ThirdPersonCharacter_C ObjectPtr: 0x7168662630 ClassPtr: 0x716958c100
嘗試一:替換Material。( 沒效果 )
1 2 3 4 5 6 7 8 9 10 11 12 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" ]);
嘗試二:設置bDisableDepthTest
為0
。( 沒效果 )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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 < 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。( 沒效果 )
1 2 3 4 5 6 7 8 9 10 11 12 13 let ThirdPersonCharacter _Mesh = ThirdPersonCharacter _obj.add (Offset .ACharacterToUSkeletalMeshComponent ).readPointer ();let SetTexture = new NativeFunction (base.add (0x8B2C4CC ), "void" , ["pointer" , "pointer" ]);let GetTexture = new NativeFunction (ThirdPersonCharacter _Mesh.readPointer ().add (0x1F8 ).readPointer (), "pointer" , ["pointer" ]);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
的代碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function getFirstPersonThirdPersonDistance ( ) { 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
函數,在其中判斷是否渲染:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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 ); } } }) }
當然這肯定不是正解,而且效果也非常一般,之後看看有沒有其他大佬分析下正解吧。
白色的Cube碰撞異常 子彈射在白色的Cube上不會反彈。
白色的Cube應是就是EditorCubeN
。
1 2 3 4 5 6 7 8 9 10 11 12 [0x3c3e ]: Name: EditorCube8 Class: StaticMeshActor ObjectPtr: 0x7170c09f40 ClassPtr: 0x717e7c0100 [0x3c26 ]: Name: EditorCube10 Class: StaticMeshActor ObjectPtr: 0x7170c0ba40 ClassPtr: 0x717e7c0100
子彈射在EditorCubeN
上會瞬間消失,但EditorCubeN
是有被擊退的效果,而子彈射在黑色的Cube上能正常被反彈。
嘗試一:看看是否因為物理模擬未啟用。( 沒效果 )
1 2 3 4 5 6 7 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。( 沒效果 )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 let StaticMeshComponent = All _Objects["EditorCube8" ].add (0x220 ).readPointer ();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" ]);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 )。( 沒效果 )
1 2 3 4 5 let StaticMeshComponent = All _Objects["EditorCube8" ].add (0x220 ).readPointer ();let SetCollisionResponseToAllChannels = new NativeFunction (StaticMeshComponent .readPointer ().add (0x850 ).readPointer (), "void" , ["pointer" , "uint8" ]);SetCollisionResponseToAllChannels (StaticMeshComponent , 2 );
經過上述嘗試,可知子彈消失大概率與Collision無關。
猜測子彈是在擊中EditorCubeN
時執行了一段Destroy邏輯。
hook MyProjectProjectile
的OnHit
,打印調用棧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 [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 6f72eb6a8c is in libUE4.so offset : 0x88a9a8c 6f7356ff04 is in libUE4.so offset : 0x8f62f04 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邏輯。
嘗試直接patch掉該函數,使其固定返回1
。
1 2 3 4 Interceptor .replace (base.add (0x88A8D2C ), new NativeCallback (() => { console .log ("call 88A8D2C" ); return 1 ; }, "int" , []));
結果是射到EditorCubeN
時也會正常反彈,成功修復該異常點。
其他異常 在測試過程中還發現以下一些不確定算不算異常點的:
子彈要射到角色的腳底才會與角色發生碰撞,射在其他位置會直接穿過。
有時候射著射著人物就飛高高了。
完整代碼 frida -U -f com.ACE2025.Game -l final.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 let GWorld = null ;let GName = null ;let GObject = null ;let Offset = { UWorldToPersistentLevel : 0x58 , ULevelToActors : 0xa0 , GNamesToFNamePool : 0x38 , FNamePoolToCurrentBlock : 0x0 , UObjectToClassPrivate : 0x10 , UObjectToFNameIndex : 0x18 , UObjectToOuterPrivate : 0x20 , FUObjectArrayToTUObjectArray : 0x10 , TUObjectArrayToNumElements : 0x14 , FUObjectItemPadd : 0x0 , FUObjectItemSize : 0x18 , AActorToRootComponent : 0x130 , USceneComponentToRelativeLocation : 0x11c , ACharacterToUSkeletalMeshComponent : 0x280 , USkeletalMeshComponentToSkeletalMesh : 0x478 , USkeletalMeshComponentToUMaterialInterface : 0x448 , USkeletalMeshToFSkeletalMaterial : 0xd8 } function startUE4 (base ) { console .log ("UE4.base: " , base); 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 ); 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 () var isWide = FNameEntryHeader & 1 var Len = FNameEntryHeader >> 6 return FNameEntry .add (2 ).readCString (Len ); } let ThirdPersonCharacter _obj = null ; let FirstPersonCharacter _C_obj = null ; let All _Objects = {} 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; } if (objName == "FirstPersonCharacter_C" ) { FirstPersonCharacter _C_obj = 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 ( ) { 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 ]; }, onLeave : function (retval ) { if (this .a1 .readUtf16String () == "EditorCube8" ) { retval.replace (5 ); } } }) } 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]" ); 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 ); let RelativeLocation = All _Objects["MuzzleLocation" ].add (0x11c ); let RelativeRotation = All _Objects["MuzzleLocation" ].add (0x128 ); 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 ); } } }) } setupUE4 (); travUObjectArray (); hook_utf16_cmp (); hook_process_before_shoot (); fix_EditorCubeN_bullet_problem (); fix_MuzzleLocation (); bypassWallhack (); } 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; 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); } return PC ; }, "int" , ["pointer" , "pointer" , "pointer" , "pointer" ])) } function main ( ) { hook_pthread (); setTimeout (() => { startUE4 (Module .findBaseAddress ("libUE4.so" )); }, 3500 ); } setImmediate (main)
結語 今年跟上年一樣是UE4的題型,猜到了會出UE4,賽前本想找些遊戲來練練手,但一直沒找到合適的,只能說可惜了。這也導致了比賽前2天基本都在熟悉UE4,直到最後也沒有完整地修復幾個異常點。
本以為決賽無望的,沒想到運氣挺好居然進了,算是圓了上一年的遺憾吧。