吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1720|回复: 18
上一主题 下一主题
收起左侧

[Android CTF] 2025腾讯游戏安全技术竞赛 安卓初赛

[复制链接]
跳转到指定楼层
楼主
lrhtony 发表于 2025-4-14 23:43 回帖奖励
本帖最后由 lrhtony 于 2025-4-14 23:47 编辑

第一次参加,写得不怎么好,还请多多指教🙇‍♂️

题目

启动游戏后可以明显发现游戏存在加速、自瞄、透视等问题。

首先判断虚幻引擎版本,我们可以直接在AndroidManifest.xml中找到UE4.27

UE_ver

对所有so文件进行分析。首先利用偷懒的方法,将so文件通过Virustotal计算一下hash查看首次上传时间可以得知,除libUE4.solibGame.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中关键参数的地址

image-20250329185455154

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

image-20250329190346023

异常点1

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

image-20250329190400922

异常点2、3

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

image-20250329190618465

修复

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

image-20250329191133745

image-20250329191152965

异常点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);
}

image-20250329191835258

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

屏幕截图 2025-03-29 104825

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

image-20250329193238453

异常点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);

image-20250329193930047

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

image-20250330005044597

但仍未弄清楚此处计算结果与ControlRotation还会如何运算。在0x8D2ED800x8D2E214的函数里面,可见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

image-20250330112923633

对应一下

image-20250330120524114

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

image-20250330120647854

上面是传入该函数的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 < 250) {
        var str = str_addr.readUtf8String(str_length);
        return str;
    } else {
        return "None";
    }
}

function set(modulename) {
    moduleBase = Module.findBaseAddress(modulename);
    GWorld = moduleBase.add(GWorld_Ptr_Offset).readPointer();
    GName = moduleBase.add(GName_Offset);
    GObjects = moduleBase.add(GObjects_Offset);
}

function getActorsAddr(){
    var Level_Offset = 0x30
    var Actors_Offset = 0x98

    var Level = GWorld.add(Level_Offset).readPointer()
    var Actors = Level.add(Actors_Offset).readPointer()
    var Actors_Num = Level.add(Actors_Offset).add(8).readU32()
    var actorsAddr = {};
    for(var index = 0; index < Actors_Num; index++){
        var actor_addr = Actors.add(index * 8).readPointer()
        var actorName = GUObject.getName(actor_addr)
        actorsAddr[actorName] = actor_addr;    
    }
    return actorsAddr;
}

class Vector {
    constructor(x, y, z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }
    toString() {
        return `(${this.x}, ${this.y}, ${this.z})`;
    }
}

class Rotator {
    constructor(Pitch, Yaw, Roll) {
        this.Pitch = Pitch;
        this.Yaw = Yaw;
        this.Roll = Roll;
    }
    toString() {
        return `(${this.Pitch}, ${this.Yaw}, ${this.Roll})`;
    }
}

function dumpVector(vectorAddr){
    const values = Memory.readByteArray(vectorAddr, 3 * 4);
    const vec = new Vector(
        new Float32Array(values, 0, 1)[0], 
        new Float32Array(values, 4, 1)[0], 
        new Float32Array(values, 8, 1)[0] 
    );
    console.log("dump vec", vec);
    return vec;
}

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 getActorLocation(actorAddr){
    var functionAddr = moduleBase.add(0x92e16b4);
    var getActorLocationFunc = new NativeFunction(functionAddr, 'void', ['pointer', 'pointer', 'pointer']);
    var location = Memory.alloc(0x100);
    try{
        getActorLocationFunc(ptr(actorAddr), location, location);
        dumpVector(location);
        return location;
    }
    catch(e){
    }
}

function getActorRotation(actorAddr){
    var functionAddr = moduleBase.add(0x937BB14);
    var getActorRotationFunc = new NativeFunction(functionAddr, 'void', ['pointer', 'pointer', 'pointer']);
    var rotation = Memory.alloc(0x100);
    try{
        getActorRotationFunc(ptr(actorAddr), rotation, rotation);
        dumpRotator(rotation);
        return rotation;
    }
    catch(e){
    }
}

// function getVector(actorAddr){
//     var data_addr = ptr(actorAddr).add(0x500);
//     dumpVector(data_addr);
// }

// function writeVector(actorAddr, x, y, z){
//     ptr(actorAddr).add(0x500).writeFloat(x);
//     ptr(actorAddr).add(0x500+4).writeFloat(y);
//     ptr(actorAddr).add(0x500+8).writeFloat(z);
// }

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

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

// function getFloat(actorAddr){
//     var data_addr = ptr(actorAddr).add(0x1a0);
//     const values = Memory.readByteArray(data_addr, 4);
//     console.log(new Float32Array(values, 0, 1)[0]);
// }

// function writeFloat(actorAddr, value){
//     ptr(actorAddr).add(0x52c).writeFloat(value);
// }

function getProjectil(){
    // 判断actorAddrs是否有FirstPersonProjectile_C
    actorAddrs = getActorsAddr();
    if (actorAddrs.hasOwnProperty("FirstPersonProjectile_C")) {
        var actorAddr = actorAddrs["FirstPersonProjectile_C"];
        var projectileMovement_addr = ptr(actorAddr).add(0x228).readPointer();
        console.log(projectileMovement_addr.add(0xec).readFloat());
    }

}

function setRenderCustomDepth(actorAddr, bEnabled){
    var functionAddr = moduleBase.add(0x8AB9DE8);
    var setRenderCustomDepthFunc = new NativeFunction(functionAddr, 'void', ['pointer', 'char']);
    setRenderCustomDepthFunc(ptr(actorAddr), bEnabled);
}

function applyCustomDepthToAllActors(bEnabled) {
    const actors = getActorsAddr(); // 获取所有Actor地址
    for (const actorName in actors) {
        if (actors.hasOwnProperty(actorName)) {
            const actorAddr = actors[actorName];
            try {
                setRenderCustomDepth(actorAddr, bEnabled); // 调用函数设置CustomDepth
                console.log(`Applied CustomDepth to ${actorName} at ${actorAddr}`);
            } catch (e) {
                console.error(`Failed to apply CustomDepth to ${actorName} at ${actorAddr}: ${e}`);
            }
        }
    }
}

function setCustomDepthStencilValue(actorAddr, value){
    var functionAddr = moduleBase.add(0x8AB9E0C);
    var setCustomDepthStencilValueFunc = new NativeFunction(functionAddr, 'void', ['pointer', 'int']);
    setCustomDepthStencilValueFunc(ptr(actorAddr), value);
}

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

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

免费评分

参与人数 2吾爱币 +3 热心值 +2 收起 理由
noname96 + 1 + 1 用心讨论,共获提升!
ngiokweng + 2 + 1 谢谢@Thanks!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

推荐
 楼主| lrhtony 发表于 2025-4-19 13:54 |楼主
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
推荐
落尘大大和你呢 发表于 2025-4-16 16:10
师傅,运行
./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 发表于 2025-4-15 16:17
太深了,无底洞,看不懂。。。。

免费评分

参与人数 2吾爱币 +1 热心值 +1 收起 理由
ngiokweng + 2 + 1 谢谢@Thanks!
我心飞翔1995 -1 请勿灌水,提高回帖质量是每位会员应尽的义务!

查看全部评分

3#
ngiokweng 发表于 2025-4-15 19:53
tql!!!透視我研究了差不多2天也沒研究明白...
4#
well2006hzy 发表于 2025-4-16 13:53
感谢分享,
5#
NightLobster 发表于 2025-4-16 15:05
哎呦我滴天,小白要学多久才能做出来
7#
 楼主| lrhtony 发表于 2025-4-16 17:30 |楼主
落尘大大和你呢 发表于 2025-4-16 16:10
师傅,运行
./ue4dumper64 --sdku --newue --gname 0xADF07C0 --guobj 0xAE34A98 --package com.ACE2025.G ...

你看一下https://github.com/revercc/UE4Dumper
新的UE4里面的参数偏移会有不同,这个有--newue+的选项,用这个才能dump
8#
HackerWen 发表于 2025-4-16 17:40
感谢楼主,差不多看懂了,可以分享下题目吗,官网下不了了
9#
落尘大大和你呢 发表于 2025-4-16 21:23
lrhtony 发表于 2025-4-16 17:30
你看一下https://github.com/revercc/UE4Dumper
新的UE4里面的参数偏移会有不同,这个有--newue+的选项 ...

好的,可以了,感谢师傅
10#
OrionisLi 发表于 2025-4-17 01:17
学习学习,谢谢大佬
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2025-4-25 00:46

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表