被動擷取 vs. 主動擷取#

用 Wireshark 這類工具做被動擷取(passive capture)有幾個明顯優勢:不影響待分析應用程式的網路運作,也不需要修改應用程式。缺點則是無法輕易與即時流量互動——你不能當場改動流量,看看應用程式會怎麼反應。

主動擷取(active capture)正好相反:能操弄即時流量,但需要更多前置設定,可能得修改應用程式,至少也得把應用程式流量導向一個代理。

這不是二選一的題目。實務上你會混用兩者:先被動擷取建立協定的整體認識,再用代理去驗證假設、關掉礙事的功能。選擇哪種取決於擷取難度與協定複雜度。

本節示範如何替 SuperFunkyChat 協定實作代理,重點放在如何善用主動網路擷取。

架設代理#

我們從第 2 章的擷取範例(Listing 2-4)改造起。為了簡化 SuperFunkyChat 的開發與設定流程,這裡使用連接埠轉發式(port-forwarding)代理,而不是 SOCKS 之類的方案。

chapter5_proxy.csx:主動分析代理
using static System.Console;
using static CANAPE.Cli.ConsoleUtils;

var template = new FixedProxyTemplate();
// Local port of 4444, destination 127.0.0.1:12345
template.LocalPort = 4444;
template.Host = "127.0.0.1";
template.Port = 12345;

var service = template.Create();
// Add an event handler to log a packet. Just print to console.
service.LogPacketEvent += (s,e) => WritePacket(e.Packet);

// Print to console when a connection is created or closed.
service.NewConnectionEvent += (s,e) =>
        WriteLine("New Connection: {0}", e.Description);
service.CloseConnectionEvent += (s,e) =>
        WriteLine("Closed Connection: {0}", e.Description);

service.Start();
WriteLine("Created {0}", service);
WriteLine("Press Enter to exit...");
ReadLine();
service.Stop();

把檔名傳給 CANAPE.Cli 執行檔,即可用 Canape Core 執行這支腳本。

腳本的三個關鍵點:

  • 代理本機監聽 4444 埠,並轉接到 127.0.0.1 的 12345 埠。測試聊天程式這樣就夠;若要拿去分析其他應用協定,改掉連接埠與 IP 位址即可。
  • 加上封包記錄的事件處理器(相對於第 2 章版本的主要改動),讓封包一抵達就印出來。
  • 加上連線建立與關閉的事件處理器,把兩個事件都印到主控台。

接著把 ChatClient 重新設定成連到本機 4444 埠而非原本的 12345 埠。ChatClient 只要加上 --port NUM 參數即可:

ChatClient.exe --port 4444 user1 127.0.0.1

真實世界的應用程式,改變目的地未必這麼容易。第 2 章與第 4 章討論過如何把任意應用程式的流量重導進你的代理,遇到不肯乖乖聽話的目標時請回頭參考。

用戶端應該會順利透過代理連上伺服器,代理的主控台則開始顯示封包。

代理在用戶端連線時的輸出範例
CANAPE.Cli (c) 2017 James Forshaw, 2014 Context Information Security.
Created Listener (TCP 127.0.0.1:4444), Server (Fixed Proxy Server)
Press Enter to exit...
New Connection: 127.0.0.1:50844 <=> 127.0.0.1:12345
Tag 'Out' - Network '127.0.0.1:50844 <=> 127.0.0.1:12345'
        : 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F - 0123456789ABCDEF
--------:-------------------------------------------------------------------
00000000: 42 49 4E 58 00 00 00 0E 00 00 04 16 00 05 75 73 - BINX..........us
00000010: 65 72 31 05 62 6F 72 61 78 00                   - er1.borax.

Tag 'In' - Network '127.0.0.1:50844 <=> 127.0.0.1:12345'
        : 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F - 0123456789ABCDEF
--------:-------------------------------------------------------------------
00000000: 00 00 00 02 00 00 00 01 01 00                   - ..........

PM - Tag 'Out' - Network '127.0.0.1:50844 <=> 127.0.0.1:12345'
        : 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F - 0123456789ABCDEF
--------:-------------------------------------------------------------------
00000000: 00 00 00 0D                                    - ....

Tag 'Out' - Network '127.0.0.1:50844 <=> 127.0.0.1:12345'
        : 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F - 0123456789ABCDEF
--------:-------------------------------------------------------------------
00000000: 00 00 04 11 03 05 75 73 65 72 31 05 68 65 6C 6C - ......user1.hell
00000010: 6F                                              - o
--snip--
Closed Connection: 127.0.0.1:50844 <=> 127.0.0.1:12345

輸出的讀法:

  • 每個封包都帶一個標頭,用 OutIn 標籤標示方向(出站/入站),並顯示它來自哪一條代理連線——同時代理多個複雜應用程式時,多條連線可能並存。
  • 若終端機支援 24 位元色彩(多數 Linux、macOS 甚至 Windows 10 終端機都支援),啟動代理腳本時加上 --color 參數即可啟用色彩,配色與 Wireshark 類似:出站粉紅、入站藍色
  • 每個封包都以十六進位與 ASCII 傾印。和 Wireshark 一樣,流量仍可能被切成多個封包送達。

代理相對於 Wireshark 有個實質好處:不必處理重傳、分片這類網路效應。作業系統已經替我們處理完畢,我們拿到的是原始的 TCP 串流資料。

用代理做協定分析#

代理架好後就能開始基本的協定分析。目前顯示的還只是原始資料,理想上應該像先前的 Python 腳本一樣寫程式碼把流量剖析出來。為此我們寫一個 Data Parser 類別,負責從網路讀寫資料。

parser.csx:代理的基本解析器
using CANAPE.Net.Layers;
using System.IO;

class Parser : DataParserNetworkLayer
{
    protected override bool NegotiateProtocol(
        Stream serverStream, Stream clientStream)
    {
        var client = new DataReader(clientStream);
        var server = new DataWriter(serverStream);

        // Read magic from client and write it to server.
        uint magic = client.ReadUInt32();
        Console.WriteLine("Magic: {0:X}", magic);
        server.WriteUInt32(magic);

        // Return true to signal negotiation was successful.
        return true;
    }
}

NegotiateProtocol() 協商方法會在任何其他通訊發生之前被呼叫,並取得兩個 C# stream 物件:一個連到 Chat Server,一個連到 Chat Client。這裡用它來處理協定的魔術值,但它也能處理更複雜的任務——例如在協定支援時啟用加密。

方法的第一項工作是從用戶端讀取魔術值並轉交給伺服器。要簡單地讀寫這 4 位元組,先把 stream 包進 DataReaderDataWriter 類別,再從用戶端讀出魔術值、印到主控台、寫給伺服器。

接上解析層要做兩件事:

  • chapter5_proxy.csx 最上方加上 #load "parser.csx"。主腳本被剖析時就會自動一併載入並剖析 parser.csx。這個載入機制讓你能把解析器的各個元件分寫在不同檔案,複雜代理才寫得下去。
  • template.Port = 12345; 之後加上 template.AddLayer<Parser>();,把解析層加進每一條新連線。每條連線都會實例化一個新的 Parser,所以你需要的任何狀態都可以存成類別成員。

重新啟動代理並透過它連上用戶端,就只會記錄真正重要的協定資料,魔術值不再出現(除了主控台輸出以外)。

加上基本的協定剖析#

接下來要重新切分(reframe) 網路協定,讓每個封包只包含單一封包的資料。做法是加上函式從網路讀出長度與校驗和欄位,只留下資料;同時在把資料送給原收件者時重新寫入長度與校驗和,讓連線能繼續運作。

加進 Parser 類別的協定剖析程式碼
int CalcChecksum(byte[] data) {
    int chksum = 0;
    foreach(byte b in data) {
        chksum += b;
    }
    return chksum;
}

DataFrame ReadData(DataReader reader) {
    int length = reader.ReadInt32();
    int chksum = reader.ReadInt32();
    return reader.ReadBytes(length).ToDataFrame();
}

void WriteData(DataFrame frame, DataWriter writer) {
    byte[] data = frame.ToArray();
    writer.WriteInt32(data.Length);
    writer.WriteInt32(CalcChecksum(data));
    writer.WriteBytes(data);
}

protected override DataFrame ReadInbound(DataReader reader) {
    return ReadData(reader);
}

protected override void WriteOutbound(DataFrame frame, DataWriter writer) {
    WriteData(frame, writer);
}

protected override DataFrame ReadOutbound(DataReader reader) {
    return ReadData(reader);
}

protected override void WriteInbound(DataFrame frame, DataWriter writer) {
    WriteData(frame, writer);
}

程式碼有點囉嗦(要怪就怪 C#),但相當好懂:

  • CalcChecksum() 實作校驗和計算。我們可以拿它去驗證讀入封包的校驗和,但這裡只用它在轉送封包時重算校驗和。
  • ReadData() 從網路連線讀出一個封包:先讀大端序(big endian)32 位元整數當長度,再讀 32 位元校驗和,最後讀出資料位元組,然後轉成 DataFrame。(DataFrame 是承載網路封包的物件,可從位元組陣列或字串轉換而來。)
  • WriteData() 做的是 ReadData() 的反向操作:用 ToArray()DataFrame 轉回位元組,重算校驗和與長度,再全部寫回 DataWriter
  • 最後實作出入站串流讀寫的各個方法。

完成之後有個很划算的附帶效果:長度、校驗和這類非必要資訊都從資料中消失了,而且只要你在代理內修改資料,送出的封包就會自動帶上正確的校驗和與長度來匹配你的修改。這正是主動分析真正的槓桿所在——省掉了每次改東西都要手算校驗和的苦工。

改變協定行為#

協定常帶有可選元件,例如加密或壓縮。麻煩在於,不做大量逆向工程很難搞清楚那些加密或壓縮是怎麼實作的。做基本分析時,如果能直接把這些元件拿掉會方便許多。而且既然是可選的,協定幾乎一定會在初始連線協商時表明支援與否——只要能改流量,我們就有機會改掉那個支援設定,把功能停掉。

以聊天程式為例,它的可選功能之一是 XOR 加密(第 7 章會說明為什麼這其實稱不上加密),啟用方式是對用戶端傳入 --xor 參數。比較有無 XOR 參數時連線最初的幾個封包:

OUTBOUND XOR :       00 05 75 73 65 72 32 04 4F 4E 59 58 01         - ..user2.ONYX.
OUTBOUND NO XOR:     00 05 75 73 65 72 32 04 4F 4E 59 58 00         - ..user2.ONYX.
INBOUND XOR :        01 E7                                          - ..
INBOUND NO XOR:      01 00                                          - ..

兩處差異透露了設計:

  • 出站封包(依第一個位元組判斷是指令 0)的最後一個位元組,啟用 XOR 時是 0x01,未啟用時是 0x00。推測這個旗標表示「用戶端支援 XOR 加密」。
  • 入站封包(指令 1)的最後一個位元組,啟用時是 0xE7,未啟用時是 0x00。推測這是 XOR 加密的金鑰

實際上,啟用 XOR 加密時去看用戶端主控台,會出現 ReKeying connection to key 0xE7 這一行,證實它確實是金鑰。

這時協商流量本身是有效的,但你若透過代理送出訊息,連線就會失效甚至斷開。原因是代理仍試著從連線剖析長度之類的欄位,卻讀到無效的值:例如長度應為 0x10,代理讀到的卻是 0x10 XOR 0xE7,也就是 0xF7;因為網路連線上沒有那麼多位元組,代理就會卡住。

要繼續分析就得處理掉 XOR。當然,寫程式在讀取時解 XOR、寫出時再 XOR 回去並不特別困難,但如果這個功能換成某種專有壓縮方案,就沒這麼好辦了。所以我們選擇更省事的路:不管用戶端怎麼設定,一律在代理中停用 XOR

做法是讀取連線的第一個封包,把最後一個位元組強制設為 0。這樣轉送出去後,伺服器就不會啟用 XOR,並回傳金鑰 0;而 0 在 XOR 中是 NO-OP(A XOR 0 = A),等於徹底關掉 XOR。

protected override DataFrame ReadOutbound(DataReader reader) {
  DataFrame frame = ReadData(reader);
  // Convert frame back to bytes.
  byte[] data = frame.ToArray();
  if (data[0] == 0) {
    Console.WriteLine("Disabling XOR Encryption");
    data[data.Length - 1] = 0;
    frame = data.ToDataFrame();
  }
  return frame;
}

改好之後再透過代理建立連線,無論 XOR 設定是否啟用,用戶端都無法啟用 XOR。這個例子雖然簡單,卻正好展示了代理相對於 Wireshark 純被動分析的威力:我們可以修改連線,讓分析變得更容易

結語#

本章示範了如何用被動與主動擷取技術,對一個未知協定進行基本的協定分析:

  • 先用 Wireshark 擷取範例流量做基本分析,再透過人工檢視與一支簡單的 Python 腳本,理解了這個範例聊天協定的部分結構——魔術值、長度、校驗和與 Tag。
  • 接著以初步分析的成果實作了一支 Lua 解析器,把協定資訊抽出並直接顯示在 Wireshark GUI 中。用 Lua 替 Wireshark 的協定分析工具做原型是理想選擇。
  • 最後實作了中間人代理(man-in-the-middle proxy)。代理流量帶來幾種新的分析手法,例如修改協定流量、關掉會妨礙純被動分析的協定功能(如加密)。

該選哪種技術,取決於很多因素:流量擷取的難度、協定的複雜度等等。要完整分析一個未知協定,你需要的是最適合當下情境的技術組合,而不是單一銀彈。