# HKCERT CTF 2023: 解決手把手題目之秘笈 (I)
[TOC]
## Re:Zero / 從零開始的新世界 (Web)
1. 仔細閱讀挑戰內容,確認需要什麼樣的成就。
2. 似乎成就有所矛盾。你是不是要在完成遊戲後獲得旗幟?如果不是,你應該在哪裡檢查以修改保存?(關鍵字:本地存儲)
3. 你已經找到了以下結構。需要進行什麼樣的修改?

4. 保存並刷新瀏覽器,旗幟位於控制台中。
## 收到收到 / yes-I-know-I-know (Forensic)
1. 你需要 [電綫鯊](https://www.wireshark.org/download.html) 來分析 pcapng 檔案。
2. 你可以從許多優秀的線上資源中學習如何使用這個工具。然而,對於這個挑戰,你應該能夠從這個按鈕獲得大部分你所需的資訊

3. 到處探索一下,看看是否有什麼有趣的東西。透過觀察流量來找出當時在進行什麼活動。
4. 辨識用於執行該動作的方法/工具,並在網路上尋找相關資訊。
5. 想出一個方法來從相關的捕獲DNS封包中提取資訊。
6. 獲利
## 求旗簽名 (I) / Sign me a Flag (I) (Crypto)
### 挑戰描述
在這個挑戰中,我們可以連接伺服器並被允許執行以下操作:
`sign [key_client] [message]`:對一個不包含子串 flag 的任意訊息進行簽名。
`verify [signature]`:驗證訊息 `gib flag pls` 的簽名。設 $k_\mathcal{C}$、$k_\mathcal{S}$ 和 $m$ 分別為客戶端密鑰、服務器密鑰和訊息。訊息 $m$ 的簽名由以下公式給出:
$$
\text{Sign}(k_\mathcal{C}, m) := \text{HMAC-SHA256}(k_\mathcal{C} \oplus k_\mathcal{S}, m).
$$
請注意,$k_\mathcal{S}$ 將不會提供給玩家。目標是在將 $k_\mathcal{C}$ 固定為 16 個空字節的情況下,恢復訊息 `gib flag pls` 的簽名。
### 部分指南
#### 異或(XOR)運算符
在這個挑戰中,兩個操作數 $a$ 和 $b$ 的異或運算(即 $a \oplus b$)如下:
```python
def xor(a, b):
return bytes(u^v for u, v in zip(a, b))
```
具體而言,當兩個字符串的長度不同時,輸出字符串的長度等於較短的輸入字符串的長度。例如,
$$
\begin{aligned}
\texttt{000102}_{16} \oplus \texttt{12345678}_{16} &= \texttt{123554}_{16} \\
\texttt{00010203}_{16} \oplus \texttt{123456}_{16} &= \texttt{123554}_{16}
\end{aligned}
$$
現在假設 $k$ 是一個 16 字節的秘密值,並假設
$$k \oplus \texttt{00}_{16} = \texttt{40}_{16},
$$
那麼我們知道 $k$ 的第一個字節將是 $\texttt{40}_{16}$。
#### 逐字節洩漏
:::warning
🤔 **以下情節純粹虛構。** 在這一部分,我們將假設我們可以對服務器進行無限次調用。不幸的是,實際上我們只有 10 次調用。
:::
我們可以通過上述行為逐字節地恢復秘密字節。以下是我們如何使用 256 次調用洩漏第一個字節的方法:
1. 本地計算 $\hat{s_1} := \text{HMAC-SHA256}(\texttt{00}, \text{hello world})$。
2. 調用 $\text{Sign}(\texttt{00}, \text{hello world})$、$\text{Sign}(\texttt{01}, \text{hello world})$、$\text{Sign}(\texttt{02}, \text{hello world})$....。
3. 最終,將會有一個 $u_1$,使得 $\text{Sign}(u_1, \text{hello world}) = \hat{s_1}$。在這種情況下,$k_\mathcal{S}$ 的第一個字節將是 $u_1$。
隨後,如果我們想要 $k_\mathcal{S}$ 的第二個字節,我們可以
1. 本地計算 $\hat{s_2} := \text{HMAC-SHA256}(\texttt{0000}, \text{hello world})$。
2. 調用 $\text{Sign}(u_1 \| \texttt{00}, \text{hello world})$、$\text{Sign}(u_1 \| \texttt{01}, \text{hello world})$、$\text{Sign}(u_1 \| \texttt{02}, \text{hello world})$、...。
3. 最終,將會有一個 $u_2$,使得 $\text{Sign}(u_1 \| u_2, \text{hello world}) = \hat{s_2}$。在這種情況下,$k_\mathcal{S}$ 的第二個字節將是 $u_2$。
我們可以重複上述過程,直到恢復 $k_\mathcal{S}$。我們最多需要 16 次 256 次調用。
您可以閱讀實現該算法的 `solve.py`。
#### 減少調用次數
實際上,我們可以使用一次遠程調用檢索一個字節。這有效地將調用次數從 4096 減少到 16。以下是我們如何洩漏 $k_\mathcal{S}$ 的第一個字節:
1. 調用 $\text{Sign}(\texttt{00}, \text{hello world})$ 並將其標記為 $\hat{s_1}$。
2. 計算 $\text{HMAC-SHA256}(\texttt{00}, \text{hello world})$、$\text{HMAC-SHA256}(\texttt{01}, \text{hello world})$、$\text{HMAC-SHA256}(\texttt{02}, \text{hello world})$、....。
3. 最終,將會有一個 $u_1$,使得 $\text{HMAC-SHA256}(u_1, \text{hello world}) = \hat{s_1}$。在這種情況下,$k_\mathcal{S}$ 的第一個字節將是 $u_1$。
我們可以重複該過程 16 次來完全恢復 $k_\mathcal{S}$。嘗試使用此想法將調用次數減少到 10 次以下!
## ISA: 始料未及 / ISA Jump Scare (Pwn)
### 挑戰描述
在這個挑戰中,我們被要求編寫一個無註釋的程序來讀取 `flag.txt`。然而,有一個問題:有一個檢查器,用於驗證輸入程序的每一行是否以 `JMP` 開頭。
### 部分指南
首先讓我們看一下以下 ISA 代碼:
```
0x400000: JMP 0x40000d; JMP 0x400000
⬆️
0x40001b: NOP
```
當我們運行程序時,程序控制 (`PC`) 會是 `0x400000`(即代碼的開頭)。在這種情況下,要執行的指令將是 `JMP 0x40000d`。
雖然 `0x40000d` 不是指令開頭的地址,但是與大量[現實中的解釋器](https://devblogs.microsoft.com/oldnewthing/20220111-00/?p=106144)一樣,解釋器單純地從 PC 開始解析指令。在這種情況下,0x40000d 指向 `JMP 0x400000` 的開頭(這是一個註釋)。解釋器認為這將是下一條指令,決定在此後運行 `JMP 0x400000`。
```
0x400000: JMP 0x40000d; JMP 0x400000
⬆️
0x40001b: NOP
```
---
我們將希望在挑戰中執行一些不同於 `JMP` 的命令。我們該如何實現呢?我們可以利用 `JMP` 指令並跳轉到所需的地址... 也許跳轉到指令的中間?
然而,檢查器不允許我們編寫註釋。幸運的是,解釋器在開始時不檢查指令是否有效。只要我們不是無效指令,這對我們來說就足夠了。這在我們的情況下非常方便。
## ISA: 世界與我有關? / ISA Intrusion (Reverse)
### 挑戰描述
在這項挑戰中,我們被要求逆向工程一個使用 Bauhinia ISA 編寫的程式,並取得旗幟。如果你直接加載挑戰並運行,你會看到程式終止並顯示退出代碼 65(步驟數超出)。你需要了解為什麼會發生這樣的情形,並查看旗幟的位置。
### 部分指南
你可以複製代碼並將其加載到「偵錯遊樂場」。在那裡,你可以增加斷點(通過點擊你想停下的行數),並在斷點後逐行步進。
為了看出發生了什麼事,我們可以在第一行添加斷點,然後運行程式,並多次點擊步進來查看程式的運行流程。
我們注意到程式從未在第 5 行之後運行:它在第 3-5 行之間來回跳轉。
移除斷點並點擊繼續,我們可以看到寄存器的最終狀態:PC 最終停於 0x40003b (第 6 行的開始)。注意這並不意味著它完成了第 6 行的運行,而是意味著它完成了第 5 行運行(因為 PC 會在執行完該行後設置到下一行的開始)。
讓我們首先看一下它循環而不退出的代碼段:
```
ADD R1, 1;
LT R1, 100000;
JNZ -35;
```
這是一個簡單的循環結構,每次循環向 R1 加 1,如果 R1 少於 100000,就進行循環。由於執行器只能運行 131072 條指令,這使得步驟數超出。
接下來,你必須找到一種方法來最佳化這個問題:使編輯後的代碼執行相同的操作,但不需要運行那麼多指令。這種編輯組合語言代碼的行為被稱為「修補」,這是逆向工程動態分析中非常有用的技術。我們可以使用修補的技巧來嘗試運行不同的邏輯,以幫助我們更深入了解程式、繞過某些限制等等。
在你修補代碼來繞過「步驟計數超出」錯誤之後,你應該能夠讓程式無錯誤地運行。(即退出代碼為 0)。然而,終端機上仍然沒有輸出。
現在你有多個選擇:
1. 靜態逆向組合語言以理解其完整邏輯(這樣你就可以找出它如何隱藏資訊)。
2. 嘗試在程式的不同點斷點,並檢查當刻的記憶體(通過Memory View)。如果程式隱藏了旗幟,很可能在執行期間的某個時刻記憶體中會包含這個資訊。