首页
社区
课程
招聘
[原创]安卓ACE反作弊拉黑设备机制技术解析
发表于: 2026-8-30 20:17 4958

[原创]安卓ACE反作弊拉黑设备机制技术解析

2026-8-30 20:17
4958

文档性质:技术详解
数据来源:真实环境 ACE 逆向分析

注:相关分析仅供参考

ACE(Anti-Cheat Expert)在安卓端判定"设备身份"的核心硬锚点是 Widevine DRM 的 deviceUniqueId。

该 ID 由硬件安全单元(TrustZone)内出厂烧录的 KeyBox 派生,不在普通文件系统中。ACE 客户端取到该 ID 后上报服务端,用于设备级拉黑。

由于该 ID 硬件派生且存储于 TEE,刷机、恢复出厂、root、修改系统分区均难以改变。

deviceUniqueId 由 Widevine KeyBox 在设备 provisioning 阶段建立。KeyBox 为出厂烧录的二进制结构:

KeyBox 整体约 0x80 字节(128 字节)。

Device ID 即为设备唯一标识的原始来源,经 Widevine 密钥派生后对外表现为 deviceUniqueId。

逆向确认的数据流如下:

关键点:链路贯穿 Java、native、vendor HAL,最终落到 TEE。ACE 不依赖 API 表层返回值,会走端到端真值。

下述内容来自对 libtersafe.so(TSS SDK 7.7.38)的 IDA 逆向实证,与上层"取 ID 的数据流"衔接,说明 deviceUniqueId 在 ACE 客户端内如何存储与上报。

实证结论:

实证上报链路:

要点:此链路说明 deviceUniqueId 随 ACE 的常规上报数据一起发出,属于"始终上报"的部分,而非仅异常时才发送。ACE 在 native 层不直接持有 socket,而是经游戏回调转交后走 ACE 的 UDP 加密路径。

下述结构体与伪代码依据逆向报告第五、四章还原,字段命名与偏移以 init_info 结构体(sub_50D9A0)及 UDP 打包函数(tss_sdk_encryptpacketsub_4B0814)为准。

游戏接入 ACE 时填写的初始化信息(sub_50D9A0 反编译):

deviceUniqueId 传入接口(TssSDKSetUserInfo 系列)的调用方式还原:

负责格式化单条上报键值,格式为:

tss_sdk_encryptpacket(0x1CC704)最终生成的 UDP 包结构(第四章 4.3):

tss_sdk_encryptpacket 的内部流程(调用链 sub_4B0814 还原):

将"取 ID → 收集 → 回调 → 打包 → 发送"串成完整调用伪代码:

本节仅概括操作与结果,只列出与 deviceUniqueId 相关的数据。

通过 MediaDrm 读取 deviceUniqueId,原始字节(Base64)及长度:

操作:清除 detect 应用数据 / 恢复出厂设置。
结果:deviceUniqueId 不变。

产生变化的数据(仅 Android ID 等弱锚点):

操作:逆向修改 persist 分区,改写其中的 MAC 字段,并经 9008 写盘。
结果:WiFi MAC 地址成功变更,deviceUniqueId 不变。

变更数据:

结论:MAC 属于可改写弱锚点,deviceUniqueId 不受影响。

操作:删除 persist/data 下 4 个 provisioning 目录,并清空 /data/vendor/mediadrm,重启系统重新 provisioning。
结果:密钥文件全部重新生成,哈希全部变化;deviceUniqueId 不变。

关键文件新旧哈希对照(仅展示差异):

结论:重新生成的是上层密钥文件,派生 deviceUniqueId 的根 KeyBox 仍在 TEE 内,未受影响。

操作:在 framework/Java 层 hook MediaDrm.getPropertyByteArray 返回值。
结果:走标准 API 的调用方拿到伪造值;native 独立路径与服务端对账时穿帮。

双路径歧义校验:

采集路径分 Java 层与 native 层。两者最终都落到同一 Widevine HAL,故返回真值应一致;若不一致,即为双路径校验的判据。

标准 API,android.media.MediaDrm,UUID 取 Widevine 的固定值 EDEF8BA9-79D6-4ACE-A3C8-27DCD51D21ED:

等价 native API,AMediaDrm_getByteArrayProperty,链接 mediandk:

不经 mediandk,直接与 Widevine DRM HAL 通信取同一属性。涉及 libwvhidl(HIDL 旧版)或 libwvaidl(AIDL 新版)的 getPropertyByteArray。此路径与 6.1 同源,主要区别在于调用方是 native 进程,绕开 Java MediaDrmService,用于双路径校验中的"native 独立取值"。

说明:以上 6.1/6.2/6.3 返回的应为同一真值。ACE 若同时用 Java 与 native 取值对比,任一被伪造都会造成不一致,这一不一致即破绽。

部分特征(如 root 模块痕迹 Zygisk/Magisk/KernelSU/.so)可通过隐藏模块规避,故不列入。以下只列市面上常见手段难以掩盖的破绽:

ACE 服务端的校验不只信任客户端上报的单个值,而是通过多种相互独立的手段交叉验证。本节约定"校验方"为服务端,客户端(或被 hook 的进程)视为不可信来源。以下是主要校验方法与参考代码讲解。

原理:同一设备真值应只有一份。用两条互不共享的取数路径(Java MediaDrm 与 native AMediaDrm/HAL 直连)各取一次,比对是否一致。若其中一个被 hook 而另一个没有,两条路径的值必然产生分歧。

讲解:核心约束是两条路径要尽量独立,避免他们都走同一个被 hook 的符号。示例思路(伪 Java)如下:

说明:该方案对"只 hook 了 Java 层"的篡改有效;若篡改者同时覆盖 native 路径,则需结合下方硬件签名类校验。

原理:Android Keystore 生成密钥时,可由 TEE/StrongBox 签发一条 X.509 认证链,证书内包含 rootOfTrust(含 verifiedBootStatedeviceLockedverifiedBootKey)与 verifiedBootHash。该链由安全硬件签名,客户端篡改不了。服务端用 Google Hardware Attestation Root 验签即可判定设备是否被解锁/被刷机。

讲解:这是区分"原厂锁定镜像"与"被 root/解锁"的关键。解锁 Bootloader 后,verifiedBootState 会变为解锁态或密钥哈希变化,服务端可直接判为不满足设备完整性。参考关键点:

讲解:服务端收到后,校验证书链的根是否为硬件认证根,并比对 rootOfTrust 中的状态。若设备已解锁/刷机,此处即被判异常,不依赖客户端自报。

原理:由 Google Play 服务负责在受信任环境里评估设备完整性,返回 MEETS_DEVICE_INTEGRITY / MEETS_STRONG_INTEGRITY 等判定。服务端用解密后的反base64 JSON 结果判断设备是否满足"锁定 + 认证镜像"。

讲解:该判定含硬件背书(较新的结果由 TEE 签署),比客户端自报可信。服务端只信任这份 verifaction:

讲解:若 deviceIntegrity 不含 MEETS 判定,说明设备未满足完整性要求(可能解锁/root),服务端可作为拉黑或拒绝的强依据。

原理:从 Widevine HAL 读取 provisioning 是否完成及 Security Level(L1/L3)。L1 要求 TEE 参与,若设备 provisioning 异常或降级到 L3,表明 DRM 链路的硬件保护可能被破坏。

讲解:参考读取方式:

讲解:注意安全级别在不同设备/ROM 灵力不同,单点判断易误伤,通常作为辅助特征,与 8.1-8.3 综合使用。

原理:受篡改的设备容易在"可改字段"与"不可改字段"之间不一致。把 deviceUniqueId、Build 指纹、SoC/GPU、传感器特征等组合比对,若 DRM ID 归属的平台与 Build/GPU 指向的平台不符,判可疑。

讲解:参考思路(服务端比对,值来自不同采集点):

讲解:这类布尔不确定性较大,单靠某一条不足以下判,故 ACE 通常做加权融合:多维度同时异常才升级为设备级拉黑。

说明:单靠任一手段都可能被局部规避,组合使用、以服务端验证为准,才能有效识别 deviceUniqueId 被篡改的设备。任何校验代码都应避免绝对化表述,实际判定应结合具体业务风险阈值。

针对"修改 deviceUniqueId 后信任链破碎"的场景,市面有「Play Integrity Fix」类模块、TEE 模块替换密钥等手段。以下说明这些手段实际起作用的环节。

信任链破碎的表现,是设备不再满足服务端能验证的"硬件背书状态",包括:

这些状态由 TEE/安全硬件签名或 Google 服务在受信任环境签发,并非普通文件系统里的可改字段。

PIF 类模块的主要手段是拦截 Play Integrity 的请求与返回,把客户端看到的判定改写为看似正常的值,并隐藏 root/模块痕迹。

它作用的环节是客户端进程可见层。Play Integrity 的令牌最终需由服务端解密验签;若服务端独立复核,客户端对返回值的改动无法改变签名结果。因此它服务于"客户端自检/人工查看"层面的观感,不改变服务端能验证到的硬件背书状态。

它作用的是设备本地的信任根与密钥。若获取的 keybox 确实来自被认可的签发链路,服务端证书体系会认可该签发者,信任链在此维度可被弥合。现有关键在于该 keybox 是否被认可、是否已列入吊销名单,而非"设备能否自证"。

说明:下发已签发合法 keybox 的思路与"本地自签"不同——它让设备拿到的是被认可的合法资质。是否奏效取决于该 keybox 是否来自被认可的签发链路、是否已列入吊销名单,以及业务方验证重点是"认可该 keybox 资质"还是"同时验证其他环节"。

不能。PIF / TEE keybox 手段解决的是设备环境完整性维度,而 deviceUniqueId 的取证是设备身份取值维度,两者正交,互不替代。

PC 版 ACE 的身份锚点在 SMBIOS/DMI/磁盘序列号/MAC 等软件与固件可改写项,存在改硬件 ID 的成熟工具。安卓版 deviceUniqueId 落在 TEE/RPMB 专有安全区,通常缺少系统级改写通道。

ACE 借助单个 deviceUniqueId 实现刷机、恢复出厂、换号、重装均难以绕过的设备级拉黑。原因:

结论:这不是缺少工具,而是安卓硬件信任链设计使然。在能开机、不破坏信任链、不留破绽的前提下,彻底更换安卓设备身份在技术上通常较为困难。

Play Integrity 完整性判定
7b6K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6V1k6i4k6W2L8r3!0H3k6i4u0Q4x3X3g2S2L8X3c8J5L8$3W2V1i4K6u0W2j5$3!0E0i4K6u0r3k6$3!0G2k6$3I4W2i4K6u0r3M7r3I4S2P5g2)9J5c8X3W2F1N6r3g2Y4M7X3W2@1P5g2)9J5c8Y4k6W2M7X3c8A6j5%4c8Q4x3@1k6Z5L8q4)9K6c8s2A6Z5i4K6u0V1j5$3^5`.

声明:文档由AI辅助生成,大部分来源数据在真实环境下客观事实分析得出结论

调用方(游戏/ROM)
  -> MediaDrm.getPropertyByteArray("deviceUniqueId")     [Java 层]
  -> MediaDrmService(system_server)                       [Java 框架]
  -> native / libwvhidl / libwvaidl                        [native 层]
  -> Widevine DRM HAL(vendor 进程)                        [vender 层]
  -> TEE / RPMB / KeyBox                                    [硬件信任根]
客户端取到 ID
  -> 上报 ACE 服务端(与 Build、传感器等特征一起)
  -> 服务端与历史作弊记录绑定 = 设备级黑名单
Java: MediaDrm.getPropertyByteArray("deviceUniqueId")
  -> 32 字节 deviceUniqueId
    -> TssSDKSetUserInfoWithLicense() / TssSDKSetUserInfo()      [写入 native 状态]
      -> TssSDKGetReportData() / TssSDKGetReportData4()           [收集进上报数据]
        -> 游戏回调 tss_sdk_send_data_to_svr                     [转发到游戏层]
          -> tss_sdk_encryptpacket 加密打包                       [UDP 数据包]
            -> sendto 发送                                        [UDP 上报]
struct init_info {
    uint32_t size_;                     // 固定为 16,用于结构体版本校验
    uint32_t game_id_;                  // 游戏 ID(如 8888/8890/...)
    void*    tss_sdk_send_data_to_svr;  // 游戏提供的发送回调函数指针
};
// TssSDKSetUserInfoWithLicense():携带 license 的完整信息写入
int TssSDKSetUserInfoWithLicense(
    const char* device_unique_id,   // 32 字节 deviceUniqueId(由 Java 层传入)
    const char* license,            // 附带 license
    uint32_t    game_id
);

// 或 TssSDKSetUserInfo():基础信息写入
int TssSDKSetUserInfo(
    const char* device_unique_id,
    const char* serial_no,          // 序列号(回退/附加字段)
    const char* android_id
);

冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。

最后于 2026-8-30 20:20 被Lun_OS编辑 ,原因:
收藏
免费 27
打赏
分享
最新回复 (12)
雪    币: 4676
活跃值: (7992)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
2
感谢分享
2026-8-31 08:10
0
雪    币: 112
活跃值: (9290)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
kkd
2026-8-31 09:11
0
雪    币: 240
活跃值: (9528)
能力值: ( LV7,RANK:102 )
在线值:
发帖
回帖
粉丝
4
怎么我在MVM虚拟机里只看到了JAVA的调用,咋还有C++的啊
2026-8-31 10:38
0
雪    币: 4
活跃值: (837)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
随机化严重
2026-8-31 13:58
0
雪    币: 451
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
fjqisba 怎么我在MVM虚拟机里只看到了JAVA的调用,咋还有C++的啊[em_004]

ACE中只用java层的获取,c语言的是另一种示例,java层的获取比较主流

最后于 2026-8-31 17:36 被Lun_OS编辑 ,原因:
2026-8-31 17:22
0
雪    币: 662
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
7
话说ace对这种获取信息的不加vmp的吗?
2026-8-31 17:31
0
雪    币: 132
活跃值: (1842)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
这是下发的文件里面的吧,mrpcs_a_v_f.data 里的 0x6C92
6天前
0
雪    币: 3085
活跃值: (2680)
能力值: ( LV6,RANK:80 )
在线值:
发帖
回帖
粉丝
9
胡闹
6天前
0
雪    币: 1364
活跃值: (1267)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10
emmmm
6天前
0
雪    币: 149
活跃值: (506)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
11
感谢分享
6天前
0
雪    币: 594
活跃值: (3509)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
12

666

最后于 6天前 被Istaroth编辑 ,原因:
6天前
0
雪    币: 451
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
13
XCicada 胡闹
大佬,哪里不对,求指教
4天前
0
游客
登录 | 注册 方可回帖
返回