深入探索Widevine L3
摘要
这篇博客文章详细介绍了使用Qiling仿真框架、差分故障分析和代码反混淆来破解Widevine L3 DRM的技术,并解释了如何从Android库中提取设备密钥。
<p><a href="https://lobste.rs/s/fuyanm/diving_into_depths_widevine_l3">评论</a></p>
查看缓存全文
缓存时间: 2026/07/03 12:23
# 深入剖析 Widevine L3 源码:https://neodyme.io/en/blog/widevine_l3/
## 简介
Widevine 是 Google 推出的一种 DRM 方案,用于安全地向终端用户交付内容——可通过安全硬件或混淆软件实现。本文将介绍如何利用 Qiling 模拟框架,结合多种技术来攻破纯软件 DRM。具体来说,我们会讲解如何将 Android 库加载到 Qiling 中,在真实目标上应用差分故障分析(DFA),以及模拟如何辅助代码反混淆。我们先快速了解一下 Widevine 的概况。
Widevine 需要三个服务器:**配置服务器(Provisioning Server)**、**许可证服务器(License Server)** 和 **内容服务器(Content Server)**。信任的根被称为 **keybox**。它是一个二进制 blob,结构如下:
| 字段 | 字节数 |
|-------------------|--------|
| KeyboxSize | 0x20 |
| Device ID | 0x10 |
| Device Key | 0x48 |
| Data (version specific) | 0x04 |
| Magic (“kbox”) | 0x04 |
| Checksum | 0x04 |
| **总计** | **0x80** |
存在三种配置模型:
1. 直接使用 keybox 中的令牌请求新许可证。
2. 使用 keybox 请求设备证书,再使用该证书请求许可证。
3. 让 OEM 处理证书(可用于将 L1 作为 OTA 更新进行配置;稍后我们会解释 L1 的含义)。
Google 运营着 **配置服务器** 和 **许可证服务器**,负责设备证书的创建和管理。第三方许可证代理连接到 **许可证服务器**,检查客户端是否有权请求特定内容的许可证。然后 **内容服务器** 提供加密后的内容。
Widevine 有三个安全级别:L1、L2 和 L3,它们有不同的要求:
- **L1**:确保 Widevine DRM 密钥和解密后的内容完全由安全硬件处理,避免暴露给主机 CPU。
- **L2**:仅确保 Widevine DRM 密钥由安全硬件存储,而解密后的内容由主机 CPU 处理。
- **L3**:所有操作都在主机 CPU 上进行;密钥必须得到合理保护。
内容提供商可以根据安全级别限制对特定内容(如高清流)的访问。
## 我为什么要破解 Widevine
我破解 Widevine 主要是出于好奇。近年来 Widevine L3 被多次破解,但我所知道的项目都使用 **Frida 钩子** 从运行中的 Widevine 会话中 dump 设备密钥。虽然这只需要 Android 设备有 root 权限,但我总觉得应该有另一种需要更少权限的方法。阅读论文《Exploring Widevine for Fun and Profit》(https://arxiv.org/pdf/2204.09298) 给了我启发,该论文对架构的工作原理提供了很好的概述。他们声称 keybox 本身并没有受到很好的保护,可以直接从内存中 dump。论文还提到实际的加密算法已被攻破两次:一次是 David Buchanan 在 2019 年使用差分故障分析(DFA)破解了用于保护 Chrome 版 Widevine keybox 的白盒 AES;一年后,Tomer Hadad 破解了使用 RSA 的更新版本,并发布了一个 Chrome 扩展,可用于解密内容。不过根据这篇帖子 (https://github.com/tomer8007/widevine-l3-decryptor/issues/14#issuecomment-718087740) 来看,谁参与了其中似乎存在争议。
## 我打算如何破解 Widevine
此时,我对我计划研究的 L3 版本的内部工作原理几乎一无所知。我只是按照一篇博客文章 (https://www.ismailzai.com/blog/picking-the-widevine-locks) 的指引,使用 Android Studio 模拟器,并通过博客中提到的 Frida 脚本 dump 设备密钥,以便更好地理解流程,并随后验证我的方法是否有效。
为了了解 OEMCrypto(Android 上用于 DRM 的组件)的工作原理,我使用 Frida 进行了一些动态分析,以探究 L3 接口中不同函数的行为以及它们所需的参数。论文《Exploring Widevine for Fun and Profit》(https://arxiv.org/pdf/2204.09298) 中包含了一个逆向工程后的符号映射。例如,`oecc01` 初始化 DRM 上下文,而 `_lccXX` 函数是对应的 L3 函数。
虽然 Frida 是一个强大的动态分析工具,但我更喜欢更隔离、可重现的环境来进行实验。在解决和编写 CTF 挑战时,我熟悉了 Qiling (https://qiling.io/),这是一个 Python 项目,底层使用 Unicorn 模拟用户空间并重新实现操作系统层。它功能强大,如果某些功能不如预期那样工作(这种情况经常发生,但没关系),也很容易修改。因此,我的第一个目标是让 Widevine 在 Qiling 内运行。这也是我在项目期间花费大部分时间的地方。
## 让 Widevine 在 Qiling 内运行以简化分析
为了能够运行 Widevine,我首先需要将其加载到模拟器中。实现所有 Widevine L3 操作的库是 `libwvhidl.so`。我只想对这个库进行检测,而不是像真实 Android 设备上使用的完整程序(`[email protected]`,即 Android DRM 服务)。因此,我试图欺骗加载器,让它认为该库是一个可执行文件,这样它就会自动加载所有依赖的库。我以前做过几次,使用 LIEF 通常很简单。他们的文档中甚至有一篇简短的文章 (https://lief.re/doc/latest/tutorials/08_elf_bin2lib.html) 介绍了如何操作,但由于某些原因,该库会进入损坏状态,导致应用重定位时出现问题。经过数小时的调试,我发现可以这样得到一个可用的 ELF:
```
lib = lief.parse(src)
lib[lief.ELF.DynamicEntry.TAG.from_value(0x6000000F)].value = (
lib[lief.ELF.DynamicEntry.TAG.from_value(0x6000000F)].value + 0x1000
)
lib.interpreter = b"/system/bin/linker"
lib.write(dst)
```
加载器愉快地加载了该库,并且奇迹般地成功了 `¯\_(ツ)_/¯`
这样,我就可以手动执行 `_lcc01` 函数来初始化 DRM,该函数会创建 `/data/vendor/mediadrm/IDM1013/L3/ay64.dat`,即加密后的 keybox。为了验证一切是否按预期工作,我将 `/data/vendor/mediadrm/` 中的所有文件复制到我的 Qiling 根文件系统中,并初始化了 DRM。然而,它没有“接受”我的 keybox,总是创建一个新 keybox,所以我不确定这是否是一个合法的 key,还是包含了一个“垃圾”key。
我还注意到,如果我多次运行它,即使我在运行之间清理了文件系统,加密 keybox 的内容也会发生变化。这时我才意识到 Qiling 不是可重现的。在将 `/dev/random` 链接到 `/dev/zero`、钩住 `getrandom` 系统调用使其总是写入零、并钩住 `clock_gettime` 和 `gettimeofday` 系统调用使其返回静态时间后,我至少能够在 Qiling 内获得可重现的结果。但在 Android 模拟器上使用 Frida 钩住相同的函数后,我仍然得到不同的加密 keybox。
```
from math import floor
from qiling import Qiling
from qiling.os.linux.syscall import __get_timespec_struct
from qiling.const import *
from qiling.os.const import *
import random
random.seed(0)
FAKE_TIME = 170000000
def get_faketime():
tv_sec = floor(FAKE_TIME)
tv_nsec = floor((FAKE_TIME - tv_sec) * 1e6)
ts_cls = __get_timespec_struct(32)
return ts_cls(tv_sec=tv_sec, tv_nsec=tv_nsec)
def hook_clock_gettime(ql: Qiling, clock_id: int, tp: int):
ql.mem.write(tp, b"\x00" * 8)
return 0
def hook_gettimeofday(ql: Qiling, tv: int, tz: int):
if tv:
ql.mem.write(tv, bytes(get_faketime()))
if tz:
ql.mem.write(tz, b"\x00" * 8)
return 0
def hook_getrandom(ql: Qiling, buf: int, buflen: int, flags: int):
ql.mem.write(buf, b"\x00" * buflen)
return buflen
def make_deterministic(ql: Qiling):
ql.os.set_syscall("getrandom", hook_getrandom, QL_INTERCEPT.CALL)
ql.os.set_syscall("clock_gettime", hook_clock_gettime, QL_INTERCEPT.CALL)
ql.os.set_syscall("gettimeofday", hook_gettimeofday, QL_INTERCEPT.CALL)
ql.add_fs_mapper("/dev/urandom", "/dev/zero")
```
此时我有两种推测:要么是某些系统调用处理仍有差异,要么是混淆部分以某种方式检测到这不是真实设备。我跟踪了所有系统调用,没有发现任何可疑之处。此时我确信他们采用了某种巧妙的检测手段,而且我希望这种检测是在 DRM 初始化之前进行的。于是,我用 Frida 钩住了 keybox 的读取,并编写了一个脚本,将整个内存和 CPU 状态 dump 出来,以便之后加载到 Qiling 中。由于某些原因,钩子无法读取进程映射的所有内存区域。因此,我创建了一个 shell 脚本,使用 `dd` 从 `procfs` 外部 dump。Frida 的 CPU 状态中缺少一些寄存器,但这并不重要。在编写了一个简单的加载器并修补了 Frida 在进程内存中为钩子所做的修改后,我成功地在模拟器中运行了它。不幸的是,它仍然生成了不同的 key。那时我很沮丧,于是开始跟踪并对比所有执行的指令。虽然我为模拟器提供了来自文件系统的设备属性,但它们没有被正确解析。我注意到,在钩住 `__system_property_get` 后,Widevine 会检查 `ro.serialno`。一旦我在基于 Qiling 的模拟器中实现了一个返回 Android 模拟器序列号的钩子,它终于接受了 keybox,并且没有创建新的。
利用 Qiling 模拟器,我验证了论文中的说法:在钩住 `munmap` 时搜索内存中的 `kbox` 即可 dump keybox。此时,剩下的工作就是破解用于保护 keybox 的加密算法。
初始化 Widevine 时,它会在 Logcat 中输出以下内容:`WVCdm : [(0):] Level3 Library 4464 Apr 20 2018 14:54:35`。我启动了模拟器,并选择了 Android 9,因为我之前按照博客文章设置 Android 模拟器时,文章指示我这样做。Widevine 版本非常旧。尽管如此,有了这个信息,我确信 keybox 是使用白盒 AES 保护的,正如 David Buchanan 在这条推文 (https://x.com/David3141593/status/1080606827384131590) 中所述。
由于我实际上不知道 DFA 在实践中如何工作,我阅读了 Quarkslab 2018 年的一篇优秀博客文章 (https://blog.quarkslab.com/differential-fault-analysis-on-white-box-aes-implementations.html)。关于其工作原理的深入解释,请查看他们的博客。就我们的目的而言,我们需要执行以下步骤:
1. 识别 AES 操作。
2. 在精确位置注入故障,破坏内部状态一点。
3. 为相同的输入生成多个故障输出。
4. 将结果输入 phoenixAES。
5. 成功?嗯,这听起来很简单(稍后你会看到确实如此);最困难的部分实际上是识别 AES 操作。
他们编写了一个名为 TraceGraph 的跟踪框架,用于可视化执行内存访问。他们的框架仅适用于 Valgrind 和 PIN,不适用于 Qiling。我编写了一个小型 Python 脚本来复现他们的想法。它从我的执行中创建了一幅图像,但输出太大而无法分析。因此,我稍微降低了分辨率,结果在初看时产生了很有希望的图片。通过记录所有内存读取和写入,我还通过观察*何时*能在内存中识别出加密 keybox 和解密 keybox,缩小了需要记录的区域。生成的图像如下所示:
在 Qiling 内执行 Widevine L3:x 轴为内存地址,y 轴为时间。绿色像素表示读取,蓝色像素表示写入,黑色像素表示执行的字节。
## 生成跟踪并用 TraceGraph 可视化
我很快意识到,虽然结果看起来很有希望,但处理图像确实很困难。大多数查看器根本无法打开它,而 GIMP 虽然可以打开,但由于图像太大,导航速度非常慢。我最终创建了一个兼容 TraceGraph (https://github.com/SideChannelMarvels/Tracer/tree/master/TraceGraph) 的数据库。TraceGraph 在处理大型数据库时速度很慢且严重滞后。不过,它仍然比我的 GIMP 方法性能好得多,因此我寻找其他替代方案。我甚至考虑过重写它,但得出结论:这需要比我愿意投入的更多精力。最后,我编写了一个简单的 Python 模块 (https://github.com/neodyme-labs/QLTrace)。它在将内存访问解析为指令时存在一些错误,但对于识别模式很有用。
## 在混淆代码中查找 AES 操作
加载 L3 初始化的跟踪后,可以通过视觉进行检查。以下是我查找 AES 的方法。
TraceGraph 中 L3 初始化的可视化:x 轴(左➛右)为内存地址,y 轴(上➛下)为时间。绿色表示读取,红色表示写入,黑色像素表示执行的字节。
尽管代码进行了混淆,但 AES 操作竟出奇地容易找到。
TraceGraph 中突出显示 AES 操作的 L3 初始化可视化:x 轴为内存地址,y 轴为时间。绿色表示读取,红色表示写入,黑色像素表示执行的字节。
TraceGraph 可视化显示四个大块和一个小块读取操作,以及重复的执行操作结构。
左侧的四个框看起来像查找表,右侧的框包含与密钥相关的内容(稍后详述)。内部状态在栈上管理。我们先关注一下代码流程。这幅图显示指令分三组执行。
放大其中一组:有两个不同的指令块。两个块看起来很相似。
高级 AES 算法如下所示:
```
KeyExpansion
AddRoundKey
for i in range(rounds-1):
Substitution
ShiftRows
MixColumns
AddRoundKey
Substitution
ShiftRows
AddRoundKey
```
我们有 9 个“相同”的轮次,因此可能是 AES-128(总共 10 轮)。我好奇为什么他们对奇偶轮次使用不同的代码。第一轮以一些额外代码开始。
第一个块的可视化,开头有一些额外代码。
最后一轮以一些附加代码结束。最后一个块从未在前面使用过,并在倒数第二个块之后继续。
我最初的假设如下:
> 第一轮应该以额外的 `KeyExpansion` 和 `AddRoundKey` 开头,并且有一些额外代码。最后一轮不应包含 `MixColumns` 操作。由于它位于其他操作之间,当内联时,将其作为“不同”代码是有意义的。
然而,后来我 CTF 团队中的一名成员指出,这看起来像是一种 t-表实现。以下是如何可能识别出这一点的依据。
相似文章
DFlash与Spec V2解码(14分钟阅读)
Z Lab、SGLang和Modal发布DFlash,这是一种针对Qwen 3.5 397B-A17B的新型投机解码模型,采用块扩散和KV注入技术,相较于基线实现超过4倍吞吐量提升,相较于原生MTP实现1.5倍提升。
使用Claude Code对Android恶意软件进行逆向工程
本文详细介绍了作者如何使用Claude Code对预装在廉价Android投影仪上的恶意软件进行逆向工程,识别并禁用了可疑的软件包。
在搭载RTX 4060(8GB)的笔记本电脑上运行Qwen3.6-35B-A3B——哪些有效、哪些无效以及一个令人意外的推测解码结果
详细记录了在8GB笔记本GPU上运行Qwen3.6-35B-A3B MoE模型的经历,涵盖有效优化(如--no-mmap和VRAM余量)、意料之外的发现(推测解码相比基准测试提升26%的速度)以及Windows和CPU瓶颈的陷阱。
z-lab/Qwen3.6-27B-DFlash
本文介绍 Qwen3.6-27B-DFlash,这是专为 DFlash 设计的草稿模型。DFlash 是一种新型推测解码方法,利用块扩散技术加速推理速度。文章提供了 vLLM 和 SGLang 的安装说明,以便与目标模型 Qwen3.6-27B 实现并行草稿生成。
@0x0SojalSec: 用 AI 逆向工程任意 Android APK——Apktool + 任意 LLM 或本地 Ollama,实现实时反编译、Smali 分析、...
一款将 AI(任意 LLM 或本地 Ollama)与 Apktool 集成在一起的工具,可通过自然语言实现 Android APK 的实时反编译、Smali 分析、清单审查、漏洞挖掘和即时修补。