![post-mortem.png](../images/post_mortem_7efc587c07.png)

## 发现
10 月 13 日，[Toshi](https://twitter.com/toshiya_ma) 在“#sdk-js”频道的 [Discord](https://discord.gg/jtZn2mQvsh) 上向我们报告了一些意外行为。 他一直在比较已弃用的 [TypeScript SDK](https://github.com/symbol/symbol-sdk-typescript-javascript) 与较新的 [JavaScript SDK](https://github.com/symbol/ symbol/tree/dev/sdk/javascript) 并注意到聚合交易哈希的计算方式不同。 那……不是预期的行为。 更令人担忧的是，他们都被网络接受（并确认）了。

## 根本原因
该团队很快发现了两个严重的错误——一个在 Catapult（Symbol 客户端）中，一个在 TypeScript 和 Java SDK 中。

### 客户
在 Catapult 的“Fushicho 2”（2019 年 11 月 8 日）版本中，一个新的验证器被添加到聚合交易插件中，用于验证已完成和绑定的聚合交易的 TransactionsHash 字段。 不幸的是，由于疏忽，它从未在聚合交易插件中注册，因此从未被调用。

```
manager.addStatelessValidatorHook([config](auto& builder) {
         // 以下行本应存在但未存在
     builder.add(验证器::CreateAggregateTransactionsHashValidator());

     builder.add(验证器::CreateBasicAggregateCosignaturesValidator(
配置.MaxTransactionsPerAggregate，
配置.MaxCosignaturesPerAggregate));
如果（config.EnableStrictCosignatureCheck）
builder.add(验证器::CreateStrictAggregateCosignaturesValidator());
});
```

由于匆忙推出，我们忘记添加一个会触发 `Failure_Aggregate_Transactions_Hash_Mismatch` 失败代码的 E2E 测试。 因此，Symbol 是在没有对聚合交易哈希进行适当验证的情况下启动的。

最重要的是，已弃用的 TypeScript SDK 之间聚合交易的哈希计算存在差异； Java 开发工具包； 以及我们的 Python 和 JavaScript SDK。

虽然 Python 和 JavaScript SDK 可以正确计算聚合交易哈希值，但我们今年早些时候弃用的 TypeScript SDK 和 Java SDK 却不能。

### 开发工具包
（已弃用的）TypeScript SDK 在其计算中有两个错误。

回想一下，聚合事务是嵌入式事务的容器。 每个嵌入式事务都保证从 8 字节边界开始。 为了实现这一点，只要有必要，就会在事务之间插入置零填充字节。 在计算嵌入式交易哈希时，只对交易有意义的数据进行哈希处理，并排除零填充字节。 不幸的是，TypeScript SDK 在哈希计算中包含了这些零填充字节。 虽然令人沮丧，但这并不会降低安全性。

真正的 bug 是在 Merkle 哈希计算中发现的。 由于 splice 的不当使用，拼接 **inserted** 而不是 **replaced** 一个元素。 第二个参数本来应该是1来代替。

```
hashes.splice(i / 2, 0, this.hash([hashes[i], hashes[i + 1]]));
```

由于测试覆盖率低以及没有使用我们的标准测试向量，这两个疏忽都没有引起注意。

（已弃用的）Java SDK 具有与 TypeScript SDK 相同的 Merkle 哈希计算错误。 令人惊讶的是，它正确地计算了嵌入的交易哈希值（没有填充）。

有点有趣的是，这两个 SDK 多年来一直在计算不同的聚合交易哈希值，直到现在才有人注意到。 至少，他们应该一直在使用通用测试向量。

### 攻击如何运作
由于缺少对聚合交易哈希的检查，共同签名者仅签署聚合交易标头。 因此，唯一的限制是嵌入式事务的总大小必须与标头中的 payload_size 匹配。

由于 TypeScript 和 Java SDK 使用的无效 Merkle Hash 算法中的错误，损坏的 Merkle Hash 仅保护一部分交易不被修改。 例如，当有三个事务时，只有前两个事务不被修改，而第三个不被修改。 只要第三笔交易的大小不变，可以换成任何东西。

### 示例攻击：聚合完成
假设有三个参与者 Alice、Bob 和 Charlie 想要交换马赛克。

爱丽丝想付给鲍勃和查理每人 10 个阿尔法马赛克。
作为交换，查理同意支付 100 XYM。

爱丽丝创建了一个包含三个部分的聚合完整交易：

1. Alice 送 10 只 Alpaca 给 Bob
2.爱丽丝送10只羊驼给查理
3.查理发送100 XYM给爱丽丝

创建交易后，爱丽丝对其进行签名。 她将其传递给共同签名的 Bob。 Bob 将其传递给想要欺骗 Alice 的 Charlie。

查理知道这一点并替换了第三笔交易，因此他向爱丽丝发送了 0 XYM。 或者，更糟糕的是，他可以改变它，让 Alice 给他发了 100 XYM！

替换第三笔交易后，Charlie 对其进行联合签名并将其发送到网络。 他和鲍勃每人从爱丽丝那里得到 10 只羊驼毛。 爱丽丝什么也没收到，甚至可能最终将 XYM 发送给查理。

### 示例攻击：聚合绑定
之前的攻击也可以针对聚合保税交易进行。

创建交易后，爱丽丝将其发送到网络，并在网络中将其添加到部分交易缓存中。 Bob 将他的共同签名提交给网络。

此时，查理下载交易。 他修改了上一节中的第三个事务。 接下来，他从网络上下载 Bob 的共同签名并将其附加。 最后，他自己共同签名，并将现已完成的聚合发送到网络。

## 评估
发现后，我们有两个优先事项：在黑帽可以利用漏洞之前修补网络，并评估当前链的损坏。

在分析时（10 月 25 日），网络上确认的聚合数为 38,8607。

**第一组**
- 73,615 (19.07%) 正在使用未填充的哈希值以及正确的 Merkle 哈希算法。
- 129,987 (33.68%) 使用填充交易哈希，以及正确的 Merkle 哈希算法。 这些很可能是通过 TypeScript SDK 启动的。 由于 Merkle 哈希算法中漏洞的特殊性，包含两个或更少聚合的聚合将计算出一个密码可验证的哈希。

**第 2 组**
- 184,471 (47.80%) 正在使用填充交易哈希以及无效的 Merkle 哈希算法。 这些很可能是通过 TypeScript SDK 启动的。 其中：
   - 102,591 是关键链接交易（因此由单一拥有账户发布）；
- 514 (0.13%) 正在使用未填充的交易哈希以及无效的 Merkle 哈希算法。 这些很可能是用 Java SDK 启动的；
- 20 (0.005%) 已损坏且未通过上述方法之一计算。 进一步分析，这些似乎都是由杂项脚本发起的。 查看这些脚本，其中一些脚本在计算聚合交易哈希后修改一个或多个嵌入式交易，但不重新计算它。

第 1 组中的所有嵌入式交易都可以按照 Symbol 设计中的预期进行密码验证。 第 2 组中的嵌入式交易均无法通过密码验证。 幸运的是，一旦它们最终确定，攻击者将无法欺骗它们，因为状态哈希必须匹配。

## 修复
### 弹射器
在 v1.0.3.4 中，`AggregateTransactionsHashValidator` 在 Aggregate Transaction 插件中正确注册。

在 1'690'500 的分叉块中，所有聚合交易哈希值都将根据嵌入的交易哈希值计算，无需填充，并使用正确的 Merkle 哈希算法。

在分叉块之前，它将允许具有满足以下任何条件的交易哈希的聚合交易：

* 使用正确的 Merkle Hash 算法未填充的交易哈希
* 使用正确的 Merkle Hash 算法填充交易哈希（TS SDK，<= 2 个嵌入式交易）
* 使用无效 Merkle Hash 算法的未填充交易哈希（Java SDK，> 2 个嵌入式交易）
* 使用无效的 Merkle 哈希算法填充交易哈希（TS SDK，> 2 个嵌入式交易）
* 哈希在新的 corrupt_aggregate_transaction_hashes 网络配置设置中有匹配条目（20 个杂项）

出于性能原因，在分叉区块和之后，只允许使用版本 2 的聚合交易。 此版本提升将允许聚合交易哈希验证器无状态并在主提交锁之外并行运行。

### SDK
Python 和 JavaScript SDK 已更新以支持 V2 聚合交易（完整的和绑定的）。

TypeScript SDK 已更新为使用正确的 Merkle 哈希算法，无需填充即可计算嵌入式交易哈希，支持创建 V2 聚合交易，并对读取和验证 V1 聚合交易提供有限支持。

Java SDK 已更新为使用正确的 Merkle 哈希算法，并且对 V2 聚合交易的支持有限。

## 向前进
成为客户端开发人员的挑战之一是如何处理网络中的漏洞。 这是负责任的披露和权力下放之间不断的权衡：公开发言，你可能会发现自己与投机取巧的黑客进入了一场猫捉老鼠的游戏； 私下说话，您会隔离很大一部分用户群。 你的攻击面随着你通知的每个用户而增加，所以你不断地权衡取舍。

没有可遵循的标准框架——每种情况都是独一无二的。 您正在即时制定战斗计划。

很明显，我们需要一种更好的方法来快速更新网络。

传统上，项目将有“作战室”，验证者和服务提供者聚集——交易所很少会与开发商和其他运营商混在一起，从而增加信息分发的负担。 我们应该建立一个作战室，以便将来快速传播信息，并且作为一个社区在君子协议下互相问责，不泄露或传播信息到渠道之外。

同样清楚的是，我们需要一种更好的发布方式。 今年早些时候，我们转向单一仓库结构以减轻负担（感谢 Jenkins），但我们从分叉到发布的周转时间几乎是八个小时。 这是不可接受的。

最后，我们需要放慢脚步。 急于发布 Symbol 以满足某个任意期限，端到端测试被跳过，关键审计也被跳过。 我们对产品的测试覆盖率很低。 Bootstrap 中的最后一个错误导致部分节点无法升级——我们目前仍在对这个错误进行分类。

我们将在分叉后开发专用的节点部署和维护工具，以帮助减轻节点运营商的负担，并确保我们在未来进行无缝升级。 我们的团队正在努力提高桌面和移动钱包的测试范围，并将 TypeScript SDK 迁移到我们新的双 NEM 和 Symbol JavaScript SDK，并且我们正在进行文档大修以确保未来出现未定义或不正确的行为 细心的社区开发人员和热情的社区成员都可以抓住。

最后，我们向 Toshi 提供了大约 370,000 日元的负责任的披露奖金。 感谢我们最喜欢的太空狮子，Symbol 网络再次安全。
