2025腾讯游戏安全技术竞赛 安卓初赛

查看 69|回复 10
作者:lrhtony   
第一次参加,写得不怎么好,还请多多指教‍♂️
题目


屏幕截图 2025-04-14 234642.png (417.64 KB, 下载次数: 0)
下载附件
2025-4-14 23:47 上传

启动游戏后可以明显发现游戏存在加速、自瞄、透视等问题。
首先判断虚幻引擎版本,我们可以直接在AndroidManifest.xml中找到UE4.27

对所有so文件进行分析。首先利用偷懒的方法,将so文件通过Virustotal计算一下hash查看首次上传时间可以得知,除libUE4.so和libGame.so都曾经上传过,是标准库,可以不用看
参考文章https://www.cnblogs.com/revercc/p/17641855.html ,分别找到libUE4.so中三个核心参数的偏移
GWorld 0xAFAC398
GName 0xADF07C0
GUObject 0xAE34A98
然后使用UE4Dumper对SDK进行提取,再进一步分析
异常点1-3:无后座、加速以及加速度
对libGame.so进行分析,可以看到该文件对函数使用控制流平坦化混淆,利用IDA插件D-810默认配置即可有效去除进行分析。我们可以留意到有几个异或函数对字符串进行了加密,由于字符串不多这里手动恢复标注即可。
这个so通过.init_array调用函数,通过pthread_create创建新线程,在0x1B9C通过读取/proc/self/maps等方法获取libUE4.so的基址,然后下面通过基址+偏移计算得到UE4中关键参数的地址

在下面通过遍历Actors中的元素,找到所要的Actor后,通过偏移计算找到对应要修改的参数

异常点1
根据偏移查找SDK可以得知是开枪时的后坐力,后续通过Frida修改为其他值也可以进一步验证

异常点2、3
同理,根据偏移去查找对应的参数,可知是人物的速度和加速度

修复
这里选择将这三个地方的STR赋值汇编NOP掉,阻止其修改,应用patch,使用MT管理器替换so将apk重新打包签名,即可修复


异常点4:自瞄
在游戏内开枪,可以发现在开枪时视角/枪口被强制面向其中一个cube。对SDK进行分析,通过Frida Hook进一步确认,发现Controller.Actor.Object内的ControlRotation决定视角/枪口,可以修改这个值来实现自瞄
class Rotator {
    constructor(Pitch, Yaw, Roll) {
        this.Pitch = Pitch;
        this.Yaw = Yaw;
        this.Roll = Roll;
    }
    toString() {
        return `(${this.Pitch}, ${this.Yaw}, ${this.Roll})`;
    }
}
function dumpRotator(rotatorAddr){
    const values = Memory.readByteArray(rotatorAddr, 3 * 4);
    const rot = new Rotator(
        new Float32Array(values, 0, 1)[0],
        new Float32Array(values, 4, 1)[0],
        new Float32Array(values, 8, 1)[0]
    );
    console.log("dump rot", rot);
    return rot;
}
function getControlRotation(actorAddr){
    var data_addr = ptr(actorAddr).add(0x288);
    var rot = dumpRotator(data_addr);
    return rot;
}
function writeControlRotation(actorAddr, a, b, c){
    ptr(actorAddr).add(0x288).writeFloat(a);
    ptr(actorAddr).add(0x288+4).writeFloat(b);
    ptr(actorAddr).add(0x288+8).writeFloat(c);
}

因此可以对Actor列表里的PlayerController+0x288的位置下一个写硬件断点,在其被修改时栈回溯找到修改函数。

使用stackplz断点,可以发现正常移动视角时,堆栈情况如hit_count:4,而开枪时则hit_count:3的情况,对比两种情况,因为两种情况都要进入#00所在的函数,因此不好patch。通过frida replace置空#01所在函数,可以发现无法正常开枪,因此该函数与开枪有关系,在这之前也不能动。因此只能对#01所在的函数跳转BLRpatch成NOP,阻止其修改ControlRotation,即可修复。

异常点5:子弹乱飞
在修复自瞄后开枪会发现,子弹并不朝着准星瞄准的方向发射,在查阅相关资料后,得知子弹发射时会通过GunOffset、Location和Rotation等参数计算出发射位置及方向。使用Frida对相关参数进行获取可发现GunOffset这一参数被设置为(100, 0, 10),且通过硬件断点确定该参数在开枪时会被读取。将其修改为(0, 0, 20)后,子弹乱飞情况有所缓解,但未能彻底解决,测试在Yaw为90,270时(即人物侧对地面文字)影响较大,0,180,360时(即人物正对地面文字)影响较小。
function getGunOffset(actorAddr){
    var data_addr = ptr(actorAddr).add(0x500);
    dumpVector(data_addr);
}
function writeGunOffset(actorAddr, x, y, z){
    ptr(actorAddr).add(0x500).writeFloat(x);
    ptr(actorAddr).add(0x500+4).writeFloat(y);
    ptr(actorAddr).add(0x500+8).writeFloat(z);
}
writeGunOffset(actorAddrs["FirstPersonCharacter_C"], 0, 0, 20);

在射击函数中,也就是前面自瞄修复的下面,可以看到0x670FBAC的函数中有两个rand函数。将其patch成固定值后,此处我patch成0x7fffff(0xffffff/2)后运行发现小球能够以相对稳定的角度射出,说明此处随机数确实与前面小球左右横跳的情况有关

但仍未弄清楚此处计算结果与ControlRotation还会如何运算。在0x8D2ED80、0x8D2E214的函数里面,可见ActorSpawning的字符串以及对UObject列表等进行修改,此处应该生成了Projectile。本人猜想是需要在这附近对生成子弹的角度修改为ControlRotation,使小球恢复向玩家前方射出,参考UE官方示例代码https://dev.epicgames.com/documentation/zh-cn/unreal-engine/3---implementing-projectiles?application_version=4.27#%E5%AE%9E%E7%8E%B0%E5%8F%91%E5%B0%84%E5%87%BD%E6%95%B0

对应一下

刚好符合SpawnActor的构造,1个指针+4个参数,使用脚本hook一下第3个参数

上面是传入该函数的Rotation,下面是PlayerController的Rotation,可见刚好写反(此处调试时已把rand patch掉),把传参改回来就行
    var func_addr = moduleBase.add(0x8D2ED80)
    Interceptor.attach(func_addr, {
        onEnter: function (args) {
            dumpRotator(ptr(args[3]));
            var playerRotation = getControlRotation(actorAddrs["PlayerController"]);
            ptr(args[3]).writeFloat(playerRotation.Pitch);
            ptr(args[3]).add(4).writeFloat(playerRotation.Yaw);
        },
        onLeave: function (retval) {
        }
    });
此时子弹即可正常向前方射出,但是准心偏下,这个就需要慢慢调参解决
异常点6:透视
可以看到FirstPersonCharacter_C和ThirdPersonCharacter都被渲染成红色,可以猜测二者被应用同一修改
网上查找过相关资料,透视可通过渲染自定义深度实现,Frida测试过这里并没有开启该参数
function getRenderCustomDepth(actorAddr){
    var value = ptr(actorAddr).add(0x212).readU8();
    var bitValue = (value >> 3) & 1;
    return bitValue;
}
function getAllRenderCustomDepth(){
    const actors = getActorsAddr();
    for (const actorName in actors) {
        if (actors.hasOwnProperty(actorName)) {
            const actorAddr = actors[actorName];
            try {
                var value = getRenderCustomDepth(actorAddr);
                console.log(`RenderCustomDepth of ${actorName} at ${actorAddr}: ${value}`);
            } catch (e) {
                console.error(`Failed to get RenderCustomDepth of ${actorName} at ${actorAddr}: ${e}`);
            }
        }
    }
}
由于对UE4渲染这方面实在不熟悉,不清楚该功能如何实现。猜想可能是从源码上修改了Character.Pawn.Actor.Object的深度,使其渲染在其他Actor的顶层,同时使其渲染成红色。
相关Frida脚本
var moduleBase;
var GWorld;
var GWorld_Ptr_Offset = 0xAFAC398;
var GName;
var GName_Offset = 0xADF07C0;
var GObjects;
var GObjects_Offset = 0xAE34A98;
var actorAddrs
var offset_UObject_InternalIndex = 0xC;
var offset_UObject_ClassPrivate = 0x10;
var offset_UObject_FNameIndex = 0x18;
var offset_UObject_OuterPrivate = 0x20;
var GUObject = {
    getClass: function (obj) {
        return ptr(obj).add(offset_UObject_ClassPrivate).readPointer();
    },
    getNameId: function (obj) {
        try {
            return ptr(obj).add(offset_UObject_FNameIndex).readU32();
        }
        catch (e) {
            return 0;
        }
    },
    getName: function(obj) {
        if (this.isValid(obj)){
            return getFNameFromID(this.getNameId(obj));
        } else {
            return "None";
        }
    },
    getClassName: function(obj) {
        if (this.isValid(obj)) {
            var classPrivate = this.getClass(obj);
            return this.getName(classPrivate);
        } else {
            return "None";
        }
    },
    isValid: function(obj) {
        return (ptr(obj) > 0 && this.getNameId(obj) > 0 && this.getClass(obj) > 0);
    }
}
function getFNameFromID(index) {
    var FNameStride = 0x2
    var offset_GName_FNamePool = 0x30;
    var offset_FNamePool_Blocks = 0x10;
    var offset_FNameEntry_Info = 0;
    var FNameEntry_LenBit = 6;
    var offset_FNameEntry_String = 0x2;
    var Block = index >> 16;
    var Offset = index & 65535;
    var FNamePool = GName.add(offset_GName_FNamePool);
    var NamePoolChunk = FNamePool.add(offset_FNamePool_Blocks + Block * 8).readPointer();
    var FNameEntry = NamePoolChunk.add(FNameStride * Offset);
    try {
        if (offset_FNameEntry_Info !== 0) {
            var FNameEntryHeader = FNameEntry.add(offset_FNameEntry_Info).readU16();   
        } else {
            var FNameEntryHeader = FNameEntry.readU16();
        }
    } catch(e) {
        return "";
    }
    var str_addr = FNameEntry.add(offset_FNameEntry_String);
    var str_length = FNameEntryHeader >> FNameEntry_LenBit;
    var wide = FNameEntryHeader & 1;
    if (wide) return "widestr";
    if (str_length > 0 && str_length > 3) & 1;
    return bitValue;
}
function getAllRenderCustomDepth(){
    const actors = getActorsAddr();
    for (const actorName in actors) {
        if (actors.hasOwnProperty(actorName)) {
            const actorAddr = actors[actorName];
            try {
                var value = getRenderCustomDepth(actorAddr);
                console.log(`RenderCustomDepth of ${actorName} at ${actorAddr}: ${value}`);
            } catch (e) {
                console.error(`Failed to get RenderCustomDepth of ${actorName} at ${actorAddr}: ${e}`);
            }
        }
    }
}
function main(){
    Java.perform(function(){
        set("libUE4.so");
        actorAddrs = getActorsAddr();
        writeGunOffset(actorAddrs["FirstPersonCharacter_C"], 0, 0, 20);
    });
    var func_addr = moduleBase.add(0x8D2ED80)
    Interceptor.attach(func_addr, {
        onEnter: function (args) {
            dumpRotator(ptr(args[3]));
            var playerRotation = getControlRotation(actorAddrs["PlayerController"]);
            ptr(args[3]).writeFloat(playerRotation.Pitch);
            ptr(args[3]).add(4).writeFloat(playerRotation.Yaw);
        },
        onLeave: function (retval) {
        }
    });
}
setImmediate(main);

函数, 参数

lrhtony
OP
  


BlackSheep3 发表于 2025-4-18 22:48
可以分享一下题目吗?谢谢!

百度网盘:https://pan.baidu.com/s/1xf3wHr_cAm66lvru8qHo8Q?pwd=fxeb 提取码: fxeb
OneDrive:https://shamiko-my.sharepoint.com/:f:/g/personal/m_yuru_pro/ElgvEItBpKlBn2hEhg9MiT0BFdt4LBW32sX13D3v-gTiEw?e=E997TN
落尘大大和你呢   

师傅,运行
./ue4dumper64 --sdku --newue --gname 0xADF07C0 --guobj 0xAE34A98 --package com.ACE2025.Game
输出
Process name: com.ACE2025.Game, Pid: 2940
Base Address of libUE4.so Found At 73cdeb3000
Dumping SDK List
Objects Counts: 15612
就一直卡着不动了,没啥反应,设备问题吗还是。
SherlockProel   

太深了,无底洞,看不懂。。。。
ngiokweng   

tql!!!透視我研究了差不多2天也沒研究明白...
well2006hzy   

感谢分享,
NightLobster   

哎呦我滴天,小白要学多久才能做出来
lrhtony
OP
  


落尘大大和你呢 发表于 2025-4-16 16:10
师傅,运行
./ue4dumper64 --sdku --newue --gname 0xADF07C0 --guobj 0xAE34A98 --package com.ACE2025.G ...

你看一下https://github.com/revercc/UE4Dumper
新的UE4里面的参数偏移会有不同,这个有--newue+的选项,用这个才能dump
HackerWen   

感谢楼主,差不多看懂了,可以分享下题目吗,官网下不了了
落尘大大和你呢   


lrhtony 发表于 2025-4-16 17:30
你看一下https://github.com/revercc/UE4Dumper
新的UE4里面的参数偏移会有不同,这个有--newue+的选项 ...

好的,可以了,感谢师傅
您需要登录后才可以回帖 登录 | 立即注册

返回顶部