1. 程式人生 > >NetAnalyzer筆記 之 十一 打造自己的協議分析語言(1)初衷與語法構想

NetAnalyzer筆記 之 十一 打造自己的協議分析語言(1)初衷與語法構想

回頭看看NetAnalyzer開發系文件上次一篇竟然是2016年,老臉一紅。不過這幾年墨雲成功過的討到一個溫柔賢淑的老婆,有了一個幸福的家庭,去年9月又有了一個大胖兒子,想想也就釋然了^_^

其實這幾年NetAnalyzer的開發一直也沒有中斷過,上一篇的NetAnalyzer還是3.x系列的版本,現在最新的版本已經是 5.6.0.38 版本了,去年8月份更新的

NetAnalyzer官網地址: http://twzy.sinaapp.com/

廢話不多說了,回到今天的主題--打造自己的協議分析語言。


1. 初衷

《道德經》中有“道生一,一生二,二生三,三生萬物”的說法,描述了萬物從少到多,從簡單到複雜的一個過程。在計算機中我們所面對的各種各樣的檔案,如:圖片,文字,音樂甚至最基本的程式檔案其實都是通過二進位制資料也就是大量的0或1的方式儲存在硬碟或記憶體中的。但是如何從0和1轉換為我們熟知的各種媒體資料呢,這就需要根據0和1不同的排列順來完成,這就是編碼方案,而這種編碼方案更通俗的來說就是一種協議,這種協議來約束不同的裝置,不同的系統當遇到對應的資料是應該將其解析為什麼檔案。

當今網路作為與我們生活朝夕相關的事物,給我們帶來了便利的生活體驗,有些應用甚至可以做到計算機與智慧手機之間的無縫切換,這就得益於網路中各個層次的協議完美對接。目前的網際網路模型大部分都是基於經典的TCP/IP協議,雖然其安全性、傳輸效率等問題在這些年逐步暴露出來,但是其擁有的完整協議體系卻是其他協議體系不具備的。從物理層使用的CSMA/CD(載波監聽多路訪問衝突檢測)協議實現端到端的資料傳輸,再到網路層中IP通訊協議,RIP、OSPF網路路徑發現協議,實現從主機與主機實現跨網資料傳輸的功能,在而到保證讓主機介面可以獲取到無差別資料的TCP協議、實現最終資料呈現應用層協議,如http協議。這些協議都是公共開放的協議型別,而有部分軟體就是基於這些公共協議進行工作的,如基於http的各種瀏覽器、基於FTP的各種檔案傳輸 軟體,雖然基於公共協議的軟體很多,但是我們大部分情況下使用的更多的是專屬軟體,這部分軟體具有自己獨立的協議,而且很大的一部分是在TCP協議之上建立起來的私有獨立應用協議。

2.MangoScript

這裡打造自己的協議分析語言的初衷就是為了解析這部分協議,而我給它起了一個名字MangoScript,私有協議是一個公司或一個組織定義的一套專屬於內部的資料交流方案、這些協議可能因為涉密或是團體影響力過小並不能被外部人員獲取到。而想要分析這些資料,箱藉助協議分析工具進行分析是不可能的,而手動從各種二進位制資料中獲取資訊,效率又極其低下。MangoScript的思想就是通過將資料方案轉換為對應的指令碼程式碼,將程式碼繫結到NetAnalyzer,通過NetAnalyzer實現與解析公共協議無差別的資料分析。

MangoScript作為NetAnalyzer擴充套件協議分析的專職語言,區別於現有流行的C\C++ java C# 之類的語言,設計的更像一種配置檔案,可以通過不同的配置方式,實現對資料流的解析。指令碼使用協議分析樹的邏輯方法,指令碼編輯方式就是協議樹的呈現方式,即是沒有接觸過程式設計的人也可以輕鬆進行程式碼編寫。

當然,因為MangoScript正處於測試開發階段,所提供的功能也不近完善,這需要讀者的體諒,也很希望讀者可以提供一些好的建議與意見。目前指令碼採取寬泛執行的方式,即對於一些語法錯誤會自動忽略,以保證儘可能的完成資料分析。

 3.MangoScript簡單語法規則

通過MangoScript可以快速的對資料的結構進行描述與呈現。並且語法非常簡單,適合快速入手使用。 在MangoScript中大小寫不敏感(部分函式提供的引數除外),支援定義中文欄位。

程式碼整體可以認為有兩部分:

  •  對程式碼整體結構的約束定義(block程式碼)
  •  對具體資料呈現方式的定義(node程式碼)

從某種意義上來說MangoScript更像是一種配置檔案,因為該語言目前還不支援判斷、迴圈等邏輯,只支援一種簡單的分支,此外還不能自定義資料處理函式。 這也是墨雲一直以來稱其為語言有遲疑的地方。 然而從解析資料來看卻要比真正所謂的指令碼語言要快捷的多。

這是一段最簡單的MangoScript程式碼:

 1 /*
 2 定義block(結構塊)
 3 */
 4 
 5 block main
 6 {
 7      //定義標題
 8      title "我是標題";
 9      //定義一個node(節點)
10      node node1= select(0,1/*註釋 選則長度*/);
11 }
12 
13  

block

block 我稱之為結構塊。用於定義一組資料呈現的結構體。block中包含一整塊資料的規劃與處理,程式碼定義方式為

1  block <name>
2  {
3      …
4  }

其中程式碼中name為必填項。

如在的上面的程式碼中就定義了一個名字叫main的block。

在結果呈現上,大部分情況下表現為父級節點。 在一段程式碼中可以定義多個block,顯式定義中,不允許存在巢狀(在switch函式下可以定義匿名block,這種定義方式為隱式定義。具體請看該函式的功能說明)。並且在該段程式碼中必須存在一個名稱為main的block作為程式碼起點,結構塊的先後順序不受影響資料解析方式。

node

node是MangoScript用於描述資料呈現方式的最基本部分。node通常用來描述一個子節點由資料轉為自己制定型別過程和資料的呈現方式,具體的呈現內容則是通過一系列函式鏈進行不斷演進的結果。 函式部分通過內部定義(MangoScript不支援自定義函式)MangoScipt通過對各種函式進行對應的選取排列來獲取需要的值,而具體的函式方式可以通過函式API文件得到相關的幫助資訊,如下程式碼為定義一個節點:

1 node data = select(2, 8).text("ascii");

首先使用node:作為字首,然後定義節點名稱對應的變數沒成data ,定義完名稱之後通過=開始函式部分的編寫。

首先我們需要選取資料區域,在這裡我們通過選取函式select獲取從資料塊中第2個開始8個位元組的資料塊。

當完成資料選取以後,再執行text將選取到的8個位元組轉為文字編碼為“ASCII”的字串。

最後將結果轉換後的結果賦予變數data ,node定義以分號“;”結束。部分node還有方法體,使用大括號包裹起來,還是以分號“;”結尾。

函式

在node中用於描述資料轉換的方式,就是這裡要說的函式。函式通常使用 “.”符號開始,如上面的程式碼,其中的select函式和text的函式都是通過 “.” 符號開始的,接下來就是函式名稱,並且在括號中輸入相關的控制引數。因為不同的函式輸入的引數型別和內容不一樣,並且隨後還會不斷的擴充套件函式庫, 通過多個函式一起連結,node就形成列函式鏈,函式鏈從左往右,前一個的函式輸出資料是後一個函式的輸入資料,對於第一個函式,預設為全部的待處理資料,這就是為大節點都是以select函式作為開始的,對於最後一個函式則統一處理為文字進行輸出。

一些特殊函式的說明

在MangoScript中有一些特殊的函式需要單獨的說明一下。這些函式為指令碼提供了最基本的資料訪問和結構控制的功能,整個MangoScript都是建立在這些功能上面的。

select(offset,length) select函式,資料選擇函式,是MangoScript中核心函式之一,主要功能是從待分析資料塊中根據offset引數和length引數獲取到需要處理的資料,如上面的程式碼中從第3個位元組開始(索引都是從0開始的,所以offset=2)找到8個位元組(length=8)作為待處理的子位元組陣列。對於select函式,除了可以進行正常的選擇轉換之外,還可以進行細節擴充套件,對該欄位進行進一步的描述,通過以子節點的方式呈現出來。程式碼如下:

 

1   node 時間戳= select([幀長度]-12,14).text("ascii")
2   {
3          node 年=select(0,4).text("ascii");
4          node 月=select(4,2).text("ascii");
5          node 日=select(6,2).text("ascii");
6          node 時=select(8,2).text("ascii");
7          node 分=select(10,2).text("ascii");
8          node 秒=select(12,2).text("ascii");
9    };

 

如上面的時間戳節點,呈現的只是簡單的將選中的資料轉為以ASCII編碼的文字,但是如果我們想要知道其內部的具體細節,則需要藉助子節點功能。在主節點最後一個函式後面新增大括號,再在大括號中定義子節點,這裡的子節點選擇的資料為主節點選中的資料,所以需要將索引值置為0重新開始。如:

   node 年= select(0,4).text("ascii");

最後需要注意的是,在大括號後面加需要有一個分號,結束對該節點的定義。

while(offset,length) while函式,結構迴圈函式。在資料分析過程中,需要特定的結構,對資料塊依次進行相同的分析,很多情況下,我們並不知道該迴圈需要執行的次數,這就需要通過指令碼來自行判斷,這時候就用到了while函式,該函式是有方法和select帶子節點結構一致,但是解析方式是不一樣的,該函式只需要定義開始分析位置,以及所要分析的長度,之後再在函式後面新增需要迴圈的分析塊,該函式會自動根據填寫的內容自動判斷需要迴圈的次數。示例程式碼如下:

1 node 序號= while(4,16)
2 {
3     node sub=select(0,2); 
4     node sub2=select(2,6); 
5 };

 

該程式碼會根據偏移量最大的一個節點(該節點的偏移量加選擇長度)作為一次迴圈的結束的標誌,進行自我判斷,對資料進行迴圈解析,直到所選資料結束為止。如上面程式碼,會被解析兩次,因為sub2節點結束時候資料偏移到8(2+6),當前選擇的資料為16,所以可以再次進行一次迴圈。

switch(offset,length) switch函式,轉換函式。在一些業務分析過程中,通過會有根據不同欄位,後續所要分析協議格式不同的問題。這種情況下就會用到switch函式,在switch中有兩個引數,和select一樣用來選擇資料,但是與select不同的是,當switch選擇完資料後,會直接轉為數字型別(也就是說length最大為4), 並在switch對應的函式體內進行判斷,在改函式體內,通過case關鍵字列出不同的值,並且指向不同的block,如果switch選擇的資料與其中一個case的值相對應,則會指向對應的block程式碼, 在case中指向的block有兩種方式:1.通過>方式的指向,該種指向為外部定義的block,後面只需要輸入對應的block名稱即可;2.通過:方式的指向,這種指向使用匿名方式建立一個block,不需要是使用block關鍵字,不需要定義名稱,只要在後面輸入大括號,就可以進行程式碼輸入了,和平時定義block一樣。

如下程式碼:

node date = switch(0,2)  //switch 自動將其轉為無符號整數
{
      case 0x0101>Test,//指向一個block
      case 0x0102:  //該種結構又叫做匿名block
      {
             node test=select(3,34);
      }
 };
    ……

//定義的另外一個節點
block Test
{
      node name = select(0,1).num(4,"hex");   
}

 

 

對於輸入資料[0x01 0x01 0x03 ……],我們通過switch(0,2) 獲取到數字0x0101(十六進位制方式),則會跳轉到block名稱為Test的結構塊進行分析,注意我們這裡使用 case 0x0101>Test 這是使用外部block呼叫的方式。

如果輸入資料為[0x01 0x02 0x03 ……] 得到的數字為0x0102,在這裡使用的方式是內建匿名block:

1 case 0x0102:
2 {
3       node test= select(3,34);
4 }

 

這兩種方式都可以按照普通的block一樣使用。 當分析完資料後,並不會顯示switch所在的節點內容,而是使用對應case所指向的block中的所有節點來代替。

ifblock(flag) ifblock函式,結構判定函式,在處理一些協議總會看到Magic欄位,如某款IM軟體協議中第一個位元組就是0x02,這些協議通常是和其他服務共用了某些特徵,如某款IM軟體使用8000埠號,但是有好多應用都會使用這個埠,為了正確的識別這些協議,於是有了ifblock方法。 該方法的flag為數字型別,所以前面select所輸入的長度不能超過4。比如我們在判定是否為某款IM軟體協議的時候,輸入程式碼:

  

1 node flag=select(0,4).ifblock(0x02);

如果待測資料第一個位元組為0x02 則繼續進行下面的分析,如果不是則直接跳出,返回空的block。

display(flag) display,顯示函式,在一些協議中,有些欄位本身其代表的是一種型別,如:icmp中的型別欄位,對應每個欄位都有不同的意義。但是在做協議分析的時候,我們拿到的僅僅是程式碼,如果能將程式碼對應的欄位使用文字方式呈現出來,則更加具有可讀性。 display函式正是基於這種思想實現的。在使用display之前,我們首先需要定義對應關係。display的對應關係我們這裡叫做enum,該結構被定義在block外部,主要是為了共享enum,以下以某款IM軟體協議(精簡過)為例,定義方式如下:

 1 block main
 2 {
 3      …… //nodes
 4 }
 5 
 6 enum imcomond
 7 {
 8        case 0x0001 > "登出登入",
 9        case 0x0002 > "心跳資訊",
10        case 0x0004 > "更新使用者資訊",
11        case 0x0005 > "搜尋使用者"
12     ………
13  }

在block中定義display Node

1 node 命令= select(3,2).display(imcomond);

當待測資料為[0x01 0x01 0x01 0x00 0x02 0x00],輸出為:

命令:心跳資訊

其他功能點

MangoScript還包含了enum 等功能項以及函式資訊,詳細內容可以參考官網的NetAnalyzer開發文件。

這裡有個針對某協議的示例:

 1 block main
 2 { 
 3     title "協議測試";
 4     node 字首= select(0,2);
 5     node 序號= select(2,2).num();
 6     node 版本= select(4,3).eachbyte(".","num");//num text  
 7     node 資料型別= select(7,1).display(FrameType);
 8     node 幀長度= select(8,2).num();
 9     node 資料型別=select(10,1).display(mtype);
10     node 程式碼= select(11,3).reverse().num();
11     node 資料塊= select(14,[幀長度]-26);
12     node 時間戳= select([幀長度]-12,14).text("ascii")
13                  {
14                       node 年= select(0,4).text("ascii");
15                       node 月= select(6,2).text("ascii");
16                       node 日= select(6,2).text("ascii");
17                       node 時= select(8,2).text("ascii");
18                       node 分= select(10,2).text("ascii");
19                       node 秒= select(12,2).text("ascii");
20                  };
21     node 校驗和=select([幀長度]+2,2).num();
22     node 字尾=select([幀長度]+4,2);
23    
24 }
25   enum FrameType
26   {
27        case 0x00 > "幀型別1",
28        case 0x01 > "幀型別2"
29   }
30    
31   enum mtype
32   {
33        case 0x00 > "測試型別1",
34        case 0x01 > "測試型別2",
35        case 0x02 > "測試型別3"
36   }

在該程式碼中,可以看到只定義了一個數據塊main和enum塊,main數塊的title 為“協議測試”然後定義了11個Node,都是常規定義定義方法,這裡需要注意一點,從資料塊開始在select函式的引數中有一個用中括號括起來的幀長度欄位。這是一種引用欄位資料的方式,但是使用引用欄位的欄位必須定義在被引用欄位之後。除了引用欄位還有找一種叫做公共引數的變數、雖然目前在MangoScript中只定義了一個也就是END 表示一直到資料末尾,所以我能可以這樣使用它:

1 node 資料塊= select(0,END);

通過改程式碼可以獲取到整個資料塊。

最後我們來看看程式碼的執行情況吧:

在下面的一篇中我們將會詳細說明MangoScript編譯器

&n