­

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擴展協議分析的專職語言,區別於現有流行的CC++ 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編譯器