当前位置: 首页 > news >正文

1万美元iO bounty破解之旅

1. 引言

在 2024 年 11 月 14 日的 Devcon 大会上,以太坊基金会、Phantom.zone 和 0xPARC 联合发起了一个 1 万美元悬赏,奖励给成功破解他们的 不可区分混淆(Indistinguishability Obfuscation,简称 iO) 实现的人。详情见 obfustopia.io 。

本文简要介绍了什么是不可区分混淆(iO)、该悬赏的背景,以及Killari是如何成功破解它的过程。

2. 何为不可区分混淆?

不可区分混淆(Indistinguishability Obfuscation,iO)是一种密码学方案,旨在将一个程序转化为**“黑盒”**。这意味着:

  • 混淆后的程序可以被自由分享和执行,但其内部结构和逻辑对外界完全隐藏。
  • 用户可以无限次运行该程序,但无法推断出它的内部逻辑或实现细节。

乍一看,这个概念似乎不太现实。毕竟,如果可以随意运行程序,难道不能通过穷举输入并分析输出来反推出它的功能吗?但实际上,大多数程序的输入空间巨大,穷举测试几乎是不可能完成的任务。

iO 最显而易见的应用场景是数字版权管理(digital rights management,DRM)。如,一个游戏可以经过混淆处理后发布,用户可以自由游玩,但无法逆向工程其代码。

一个不那么显然、但更令人兴奋的用例是:

  • 安全地将秘密嵌入到混淆程序中

举个例子,一个私钥可以隐藏在一个经过 iO 混淆的程序中。这个程序可以作为智能合约运行,只有满足特定条件的消息才会被签名。这样既能保证密钥的安全,又能严格限制其使用条件。进一步来说,这甚至使智能合约能够在区块链环境之外运行,从而大幅拓展应用边界。

理论上,iO 有潜力成为 几乎所有密码学协议的基础(见2013年论文How to Use Indistinguishability Obfuscation: Deniable Encryption, and More),因此它也被称作 “万能协议(God Protocol)”(见下图)。它的通用性和强大能力让它成为密码学领域的革命性概念。但也正因为如此,它的“过于美好”引发了不少怀疑。
在这里插入图片描述

上图源自0xPARC团队2024年7月30日博客 Programmable Cryptography (Part 1)。

2. iO 真能实现吗?

当Killari第一次接触不可区分混淆(iO)这个主题时——其对iO完全一无所知——对iO的可实现性深感怀疑。从纯粹的理论角度看,它几乎不可能实现。当年第一次接触 零知识证明技术 时也有过类似怀疑。然而,在深入研究之后,Killari相信零知识证明不仅在理论上是可靠的,在很多实际场景中也已得到应用验证。

零知识技术已经证明是可行且具有巨大影响力的,但Killari仍然不确定 iO 能否真正实现。不过,仍然抱有希望,因为如果 iO 真能实现,将会开启密码学领域的巨大突破。

尽管 iO 潜力巨大,但到目前为止,尚未出现实用的不可区分混淆实现。学术界已经提出过多种方案,但最终都被证明存在缺陷或不安全。

也就是说,目前已经有理论证明(2020年论文Indistinguishability Obfuscation from Well-Founded Assumptions)表明:在经过良好验证的密码学假设下,iO 是可能存在的。这个证明确认了 iO 在原理上是成立的,但它并不保证在实践中可以高效实现

需要注意的是,完全黑盒混淆器(即一个可以混淆任意程序且除输入输出行为之外不泄露任何信息的工具)已被证明不可能存在(见2013年论文How to Use Indistinguishability Obfuscation: Deniable Encryption, and More)。不过,iO 是一个更狭义、更可实现的目标

  • iO 的要求并不是隐藏程序的所有内部细节,而是确保两个功能相同的程序在混淆后无法区分

2. Obfustopia 的 iO 实现(悬赏部分)

为了这次悬赏,项目方创建了一个包含 1024 个操作的随机程序,然后对其进行了混淆处理,使其膨胀到超过 20 万个操作
这个悬赏的目标是:

  • 将这个混淆程序优化还原为功能相同但包含 1024 个操作或更少的版本

需要特别说明的是:

  • 悬赏的目标并不是找出程序中隐藏的秘密,也不是弄清楚它的功能或行为纯粹是为了将程序还原为更高效的表示形式

当然,一旦找到了更小、更优的版本,理解程序隐藏部分的内容也会变得容易得多。

该程序是直线代码程序(straight-line code),即没有循环和分支。执行从第一行开始,顺序进行直到最后一行。程序的输入是一个 64 位布尔数组(如:[true, false, true, false, ...]),对其进行一系列变换后,输出一个 64 位结果
这样的设计避免了停机问题(halting problem),程序也不是图灵完备的

Obfustopia 的 iO 实现——https://github.com/phantomzone-org/obfustopia(Rust+Python)采用了一种称为 局部混合(local mixing) 的方法进行程序混淆,其原理在2024年论文 Towards general-purpose program obfuscation via local mixing 中进行了描述。
虽然该论文内容较为晦涩,但这篇由 Janmajayamall 撰写的摘要Program obfuscation via local mixing提供了更容易理解的解释。

以下为对该协议的简化版总结:

  • 1)生成随机程序(电路)
  • 2)混淆该随机程序

2.1 生成随机程序(电路)

与其说是对一个已有程序进行混淆,不如说这个悬赏任务本质上是生成一个随机程序,然后再对它进行混淆处理。
该程序是使用reversible boolean gates可逆布尔门构建的,这意味着这些操作可以通过再次执行相同操作来恢复原状
举个例子:

v0 = v0 XOR v1

这里有两个布尔变量 v0v1。对 v0v1 执行 异或 (XOR) 操作,并将结果存回 v0。其对应的真值表如下:

v0v1v0 XOR v1
000
011
101
110

如果连续执行两次该操作:

v0 = v0 XOR v1
v0 = v0 XOR v1

最终结果仍然是原始的 v0。这可以通过以下方式直观理解:

v0 = (v0 XOR v1) XOR v1= v0 XOR v1 XOR v1= v0 XOR (v1 XOR v1) // XOR operations can be computed in any order= v0 XOR (FALSE) // XOR with itself is always false (check above truth table) = v0

然而,如果使用不可逆门——如true gate——直接赋值 true:

v0 = true

则再次执行该操作,v0 永远都是 true,无法恢复原本的 v0。这种操作属于不可逆操作。
虽然可逆门在破解电路时并不重要,但它们对于混淆程序的生成过程来说是至关重要的。

2.2 混淆该随机程序

接下来,使用 局部混合(Local Mixing) 方法对这个随机生成的程序进行混淆。这种方法的核心思想是:

  • 选取程序中的一小部分,将其改写成不同的样子,同时保持功能不变

一个简单的做法是插入一些对最终结果没有影响的额外操作,比如插入两个冗余的门操作:

v0 = v0 XOR v1
v0 = v0 XOR v1

由于两次异或操作互相抵消,最终不影响程序功能。但这种方法并不理想,因为攻击者很容易识别并移除这些冗余操作。

为了实现真正的 iO(不可区分混淆),需要采用更强健的混淆方式

局部混淆包含两个阶段:

  • 1)膨胀阶段(Inflationary stage)
    选择程序中的一个小部分,并通过插入冗余逻辑使其 “膨胀”,看起来更复杂。
  • 2)揉捏阶段(Kneading stage)
    将这些局部修改打散、分散到整个程序之中,就像揉面团一样,使得局部结构与整体深度混合,进一步增加混淆难度。

3. 破解Obfustopia 的 iO 实现

当意识到这个悬赏本质上是一个优化问题时,立刻想到了一些可能破解混淆的方法。

3.1 攻击思路一 —— 猜测程序功能

Killari对攻击 iO 协议的第一个直觉是这样的:
如果给我一个混淆后的程序,我可以无限次运行它并观察它的行为,那么混淆真的安全吗?

举个简单例子:
假设这个混淆程序实际上只做了如下简单操作:

v0 = v1 AND v2

那么完全可以运行这个程序上百万次,使用各种随机输入,最终猜测出它执行的是 AND 操作

当然,这种攻击方式有明显的反驳理由

  • 这是一个概率性的攻击。或许可以“相当有信心”地猜测这个程序是 AND 操作,但无法百分之百确认,除非穷举所有可能输入,并验证所有输出行为。
  • 更重要的是,即便这个程序看起来像一个简单的 AND 操作,也可能存在特定输入时它表现得像 OR 操作。要发现这种情况,要么在输入选择上非常幸运,要么能够遍历所有可能的输入组合
  • 为了防止这种概率性攻击,密码学中提出了穿孔程序(Punctured Program)的概念。相关原理可参考2013年论文 How to Use Indistinguishability Obfuscation: Deniable Encryption, and More。穿孔程序技术就是用来防御攻击者通过概率性猜测来推断程序行为的。

在 Obfustopia.io 这个悬赏挑战中,清楚地知道:

  • 穷举完整输入空间是不可能的,因为 64 位输入对应的可能组合数量是:
    2 64 = 1.8446744 × 10 19 2^{64} = 1.8446744 \times 10^{19} 264=1.8446744×1019

不过,也可以确信:

  • 这个随机生成的程序不太可能只是一个简单程序,除了极少数特定输入外,其行为应该是一致复杂的。

考虑到原始程序包含 1024 个操作,如果试图盲目猜测正确的 1024 个操作组合来还原这个程序,这显然是不切实际的。虽然没有精确计算过针对 64 位输入的 1024 操作程序可能数量,但非常确信这个空间庞大到无法通过穷举方式完成破解

3.2 攻击思路2 —— 迭代化简

这个被混淆的程序大致看起来像下面这样(实际上长度要长得多):

1:  v20 ^= v51  
2:  v15 ^= \!v2  
3:  v1  ^= \!v42  
4:  v33 ^= (v40 & v51) | (\!v40 & \!v51)  
5:  v50 ^= \!v32  
6:  v60 ^= v29  
7:  v13 ^= v26  
8:  v46 ^= \!v43  
9:  v48 ^= (v13 & v19) | (\!v13 & \!v19)  
10: v12 ^= v6  
11: v29 ^= v22  
12: v50 ^= \!v32  

这里,^= 表示异或赋值操作,含义是:

左边变量 = 左边变量 ⊕ 右边表达式 \text{左边变量} = \text{左边变量} \oplus \text{右边表达式} 左边变量=左边变量右边表达式

同时,& 表示与(AND)操作,| 表示或(OR)操作,\! 表示取反(NOT)操作。

如果仔细观察,会发现有如下情况:

5:  v50 ^= \!v32  
12: v50 ^= \!v32  

这两个操作在程序中出现了两次,且在这两次操作之间:

  • v50 和 v32 均未被其他操作使用或修改

这意味着可以安全地移除这两条语句。原因是:
( a ⊕ b ) ⊕ b = a (a \oplus b) \oplus b = a (ab)b=a

连续两次对同一个变量进行相同的异或操作,等价于无操作(identity operation)

移除后,程序被简化为:

1:  v20 ^= v51  
2:  v15 ^= \!v2  
3:  v1  ^= \!v42  
4:  v33 ^= (v40 & v51) | (\!v40 & \!v51)  
5:  v60 ^= v29  
6:  v13 ^= v26  
7:  v46 ^= \!v43  
8:  v48 ^= (v13 & v19) | (\!v13 & \!v19)  
9:  v12 ^= v6  
10: v29 ^= v22  

现在,这个程序只有 10 条指令,相比原来的 12 条明显更简洁。

Obfustopia.io 悬赏项目中的混淆程序包含超过 20万条操作指令

是否可以采用这种“迭代简化”的方法,将整个程序逐步缩减至 1,024 条以内?

更有趣的是:

  • 可以任意选取程序中连续的一小段作为“子程序”
  • 对这个子程序进行输入输出分析,看看能否进一步简化。
  • 如果可以简化,就将其简化版本替换回原程序中
  • 每次进行这样的操作,整个程序就会进一步变小

这个方法称为 窥孔优化(Peephole Optimization),是很多编译器都会采用的标准优化技术。

这个迭代过程让Killari开始质疑局部混合(Local Mixing)方案是否足够稳健

一个合格的 iO 程序不应允许通过这种方式逐步“拆掉”混淆结构

iO 的理想状态应(与大多数密码学系统的工作原理类似):

  • 程序中的每个部分都高度依赖于整体结构
  • 移除任何局部片段都会破坏程序整体功能

换句话说,iO 混淆设计应具备“结构紧密耦合性”,避免被逐步剥离还原。

3.3 编写窥孔优化(Peephole Optimization攻击程序

3.3.1 创建彩虹表(Rainbow Table)

首先着手编写一个 彩虹表(Rainbow Table),因为性能对于这个任务至关重要

这个彩虹表包含了所有可能的、使用不超过三条操作、涉及不超过四个变量的程序组合
下面是一个非常粗糙但足够用的程序示例,它可以枚举所有针对指定变量数量(在本例中是 4 个变量)的3个操作组合:

function* generateAllGates(numberOfVariables: number) {for (let a = 0; a < numberOfVariables; a++) {for (let b = 0; b < numberOfVariables; b++) {for (let target = 0; target < numberOfVariables; target++) {for (let gate_i = 0; gate_i < 16; gate_i++) {yield { a, b, target, gate_i }}}}}
}

随后,又针对 0 个操作1 个操作2 个操作的情况分别做了类似处理。

最终,这个彩虹表生成的文件大小超过了 1 GB

借助这个彩虹表,可以高效地回答以下类型的问题:

“对于一个输入和输出都最多包含四个变量的程序,如何用最少的操作次数来表示它(假设最优程序可以用不超过三条操作表示)?”

3.3.2 利用彩虹表

为了测试这种攻击方法,使用该悬赏提供的工具生成了一个简单的 4 位混淆程序
这一过程帮助极大,因为能够访问生成原始混淆程序的源码,能更容易理解问题本质,同时也帮助测试自己的程序逻辑是否正确。

接下来,开始对混淆程序进行迭代简化

  • 每次提取2-10 行代码的子程序
  • 如果这个子程序的真值表在彩虹表中存在对应项,就用最优版本替换掉原来的子程序。

结果非常成功!
Killari成功破解了所有自己生成的这些简单 4 位混淆程序

3.3.3 破解真实程序

然而,仍然面临一个重大挑战:悬赏项目的目标程序是 64 位输入的,而目前仅能处理 4 位的情况。

如何才能将这种方法推广到更大的程序上呢?

这里的关键在于:

  • 不需要一次性破解整个 64 位程序
  • 而是聚焦于包含较少变量的小型子程序

实际上,大部分混淆程序都由仅使用少数几个变量的小型子程序组成
此外,每个逻辑门最多涉及 3 个变量 —— 比如:v0 = v1 AND v2

这种形式正是变量依赖最复杂的逻辑门

于是,对程序做了优化:

  • 不再考虑所有 2-10 行的子程序组合
  • 只考虑涉及不超过 4 个变量的子程序。

调整完成后,开始将这个攻击方法应用于真实目标程序

出乎意料的是:

  • 成功将原始程序从 20 多万个逻辑门压缩到了不足 10 万个逻辑门,优化幅度超过 50%!

这个阶段的成果极大增强了对于彻底破解这个混淆方案的信心。

3.3.3.1 行顺序(Row Order)

Killari还意识到,一个由顺序执行操作组成的程序可以用多种不同的方式编写
特别是——可以交换彼此之间没有依赖关系的行顺序
如:

1: v1 ^= v3
2: v60 ^= v29
3: v13 ^= v26

1: v1 ^= v3
2: v13 ^= v26
3: v60 ^= v29

实际上是完全相同的程序,但之前的彩虹表实现却把它们视为不同的程序,因为只比较连续的代码行。

为了应对这个问题,实现了一个行交换器(row swapper),它会扫描整个程序,并随机交换不互相依赖的行
引入这一机制后,重新运行了攻击,结果发现——优化效果更进一步了!

3.3.3.2 变量缩减攻击(Variable Reduction Attack)

在应用了彩虹表攻击行交换器之后,Killari意识到还可以针对涉及超过 4 个变量的子程序进一步优化。

Killari想出了一个策略:

  • 针对包含 5-8 个变量的子程序,可以随机用另一个子集内的变量替换其中的某个变量

如:
如果有一个子程序,涉及变量:

[v0, v1, v2, v3, v4]

可以将 v0 替换成 v4,从而让程序只使用变量

[v1, v2, v3, v4]

然后可以分别计算两个版本程序的真值表,进行对比。
如果两个真值表完全相同,说明可以移除这个变量,进而简化这个子程序

⚠️ 这种方法虽然不能减少操作次数,但可以减少变量数量。

然而——当子程序只涉及 4 个变量时,之前的彩虹表攻击又可以派上用场。
这种变量缩减策略比单纯依赖彩虹表更具可扩展性,即使涉及多达 10 个变量,真值表也只有 2 10 = 1024 2^{10} = 1024 210=1024 行,计算起来完全可行。

幸运的是,并不需要再进一步增加变量数量,因为混淆程序已经被破解了
Killari成功地将这个程序缩减到了1,023 条操作比原始程序少 1 条!

Killari按照悬赏要求的格式打包了这个简化程序,并使用悬赏提供的工具将其与去混淆后的参考程序进行对比

3.3.3.3 程序一致性验证

检查两个程序是否完全一致并不是一件简单的事情。
在悬赏挑战中,这一过程通过随机采样输入,然后验证两个程序的输出是否相同来完成。

Killari多次运行这一检测,并不断增加检测次数。
Killari对破解结果非常有信心,于是将结果提交给了悬赏主办方,他们也确认混淆已经被破解。

随着后续的优化进行,Killari 最终将程序压缩到了不到 800 个逻辑门

  • 这意味着找到了一个比作者原始伪随机程序还要更优的版本。

4. 后续优化(Further optimizations)

Killari对这个解混淆器最大的抱怨是:

  • 它不是确定性的。
    • 会不断随机打乱程序行顺序,然后测试是否能找到更优解,接着反复尝试
    • 这种做法非常低效。

事实上,Killari还有很多攻击思路根本没来得及实现,因为目前这套攻击方案已经足够破解悬赏了

Killari一直在慢慢优化和改进这个解混淆器,可在 https://github.com/KillariDev/cryptopia-deobfuscator(TypeScript) 找到最新版代码。
⚠️ 需要注意的是:这个版本的代码不是破解悬赏时用的原始版本,而是一个更高效、更智能的后续版本。

5. 展望(The Road Ahead)

在与悬赏项目的作者交流后,他们承认原始的混淆器存在一个漏洞,这使得整个挑战更容易被破解。
他们正在开发一个新的版本,声称将能够防御本文所采用的这种攻击方法

有趣的是:

  • 在 Devcon 大会期间,Barry Whitehat 曾经提出是否存在类似的攻击方法。
    • 当时,主办方的回答是——不可能
    • 结果 Barry 是对的。

非常期待测试新的混淆系统,并且打算持续不断地尝试攻破它,直到要么被彻底反驳,要么终于拥有一个真正安全的“不可区分混淆”(Indistinguishability Obfuscation,iO)密码系统。

不过需要强调的是:

“我无法破解一个密码系统 ≠ 它就是安全的。”

比如,Local Mixing 方法本身并没有被证明是安全的。
即使在正确实现的情况下,它也可能存在未被发现的漏洞

这正是密码学探索道路上最有趣的一部分。

参考资料

[1] Killari 2025年2月5日博客 Breaking the $10,000 iO Bounty: My Journey to Crack an Indistinguishability Obfuscation Implementation

相关文章:

  • 《Deep Residual Learning for Image Recognition》(深度残差学习在图像识别中的应用)
  • Active Directory Certificate Services(AD CS)攻击基础
  • 泰国跨境电商系统开发:多语言多币种 + 国际物流对接,中泰贸易桥梁
  • 影刀学院课程地图导航汇总
  • 数据库中的索引作用:索引分类、索引失效、聚集索引和非聚集索引
  • 构建高效CMDB管理体系:企业数字化转型的基石
  • tp3.1临时连接指定数据库,切片分类in查询,带过滤需要的数据
  • 工业自动化发展:工业相机在机器视觉领域的应用趋势
  • OpenLayers 图层控制
  • 14.安卓逆向2-frida hook技术-HookJava主动调用
  • 离线部署minio以及主从复制
  • ITIL 4考试介绍
  • atcoder [ABC271D] Flip and Adjust
  • js 查看字符串字节数
  • FreeSurfer的脑区表面重建与注意力nii图结合
  • Java面试题021:一文深入了解微服务之网关Zuul
  • 缓存击穿,缓存穿透,缓存雪崩的原因和解决方案
  • 传智健康---十天项目总结
  • 挑战杯三个项目
  • 09 - TripletAttention模块
  • 域名网站备案/郑州seo公司
  • 建筑网站设计/高端营销型网站建设
  • 跨境电商热销产品排行/网站关键词优化怎么做的
  • 国外做滑板网站/免费的seo优化工具
  • 一个人做网站 优帮云/营销型公司网站建设
  • 青岛网站备案/发稿网