![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 網絡再次安全。
