VB.NETでパケットキャプチャ2~HTTPリクエストボディ・レスポンスボディの取得
VB.NETでパケットキャプチャ作成 - Future Convergence PRJ.の続き。
VB.NETのパケットキャプチャでHTTPリクエストボディ・レスポンスボディを取得するのが目標。utf8でエンコードしてファイルに書き込んだ場合、HTTPを取り扱ったパケットだと以下のように表示される。
(IPヘッダ・TCPヘッダ部分の文字化け) HTTP/1.1 200 OK Date: Sun, 04 Jan 2015 08:14:04 GMT Server: Apache Vary: Client-Version,Accept-Encoding X-Powered-By: XXX Platform server_version: 20120101 user_id: 99999 version_up: 0 status_code: 200 authorize: timeStamp=1420359244&version=1.0&token=xxxxxxxxxx&requestTimeStamp=1420359244 X-Message-Code: aaaaaaaaaaaaaaaaaaaaaaaaaaaaa Content-Encoding: gzip Content-Length: 885 Content-Type: application/json; charset=utf-8 Connection: close (gzipによる文字化け)
(gzipによる文字化け)と書いた部分がレスポンスボディにあたる部分で、ヘッダに「Content-Encoding: gzip」と記載がある通り、gzip圧縮されているために文字化けしており、解凍しないと内容を見ることができない。そこで、ボディ部分のみ取得してgzip解凍の処理をする必要がある。httpの決まりでヘッダとボディの間には改行コード(CRLF)が2回入る。これはバイナリ表記だと「0A 0D 0A 0D」なので、これを判別して以降をボディとして取得するようにする。
あとは、パケットの終わりまで取得すれば終わりといいたいところだけど残念ながらそうはいかず…1パケットにボディ部が全て入っていれば問題ないんだけど、たいてい複数パケットに分かれて送られてくるんで、パケットの最後までの取得が終わったら、取得したバイト数とヘッダの「Content-Length: 」に記載されているサイズを比較して、すべて取得できているか判別する必要がある。もしまだ「Content-Length: 」のサイズに達していないのなら次のパケットからボディ部の続きを取得するようにする。
この、次にくるパケット以降にはhttpヘッダがないので、「0A 0D 0A 0D」による判別は使えない。tcpヘッダの終わった直後からをボディ部として取得するようにする。これはパケットの先頭からIPヘッダサイズとtcpヘッダサイズの和の分を進んだ先からになる。パケットの最後まで取得が終わったらまた「Content-Length: 」のサイズと比較、「Content-Length: 」のサイズに達したらボディの取得は終了、まだなら次のパケットをまた待つ。
こうしてボディ部の取得が完了したら一度gzipファイルとして書き出し、出来たファイルを読み込み・gzip解凍すれば目的は達成。厳密にやるならTcpの順序制御とかを見てデータの結合しないとダメなんだろうけど、通信対象と1か所にしとけば今のところ問題なく動いたのでその処理は割愛。ソースは以下の通り。
Imports System.Net Imports System.Net.Sockets Imports System.Text Imports System.IO.Compression Imports System.IO Module PakCap 'IPヘッダ長 Private Const IP_HEADER_LENGTH As Integer = 20 Sub Main() '文字コードはUTF8にする Dim enc As Encoding = Encoding.UTF8 '自分のIPアドレスを設定する Dim ip As String = "192.168.3.100" Dim socket As New Socket(AddressFamily.InterNetwork, SocketType.Raw, ProtocolType.IP) socket.Bind(New IPEndPoint(IPAddress.Parse(ip), 0)) socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AcceptConnection, True) socket.IOControl(IOControlCode.ReceiveAll, New Byte() {1, 0, 0, 0}, New Byte() {0, 0, 0, 0}) Dim packetCounter As Integer = 0 Dim fileCounter As Integer = 0 Dim isBuffered As Boolean = False Dim contentSize As Integer = 0 Dim contentCounter As Integer = 0 Dim contentBuff As Byte() = Nothing Dim isGzip As Boolean = False Using ws As New System.IO.StreamWriter("C:\test\packet.txt", False, enc) Do Dim buff As Byte() = New Byte(4095) {} socket.Receive(buff) 'IPパケットヘッダ情報の取得 Dim packetSize As Integer = buff(2) * 256 + buff(3) Dim packetId As Integer = buff(4) * 256 + buff(5) Dim packetFlg As Integer = buff(6) * 256 + buff(7) Dim protocolNum As Integer = buff(9) '送信元IP Dim srcIP As String = String.Format("{0}.{1}.{2}.{3}", buff(12), buff(13), buff(14), buff(15)) '送信先IP Dim dstIP As String = String.Format("{0}.{1}.{2}.{3}", buff(16), buff(17), buff(18), buff(19)) '送信元Port Dim srcPort As Integer = buff(20) * 256 + buff(21) '送信先Port Dim dstPort As Integer = buff(22) * 256 + buff(23) 'utf8の文字列 Dim utf8Str As String = enc.GetString(buff, 0, packetSize) Console.WriteLine("packetSize={0}", packetSize) Console.WriteLine("送信元{0}/{1} - 送信先{2}/{3}", srcIP, srcPort, dstIP, dstPort) Console.WriteLine() '特定サーバとの通信に絞って処理をする '他のサーバとのパケットが混ざるとボディの取得が失敗する可能性が高いと思われるため Dim targetIP as String = "210.xxx.xxx.xxx" If srcIP = targetIP Or dstIP = targetIP Then If isBuffered Then 'バッファリング待機中の場合 If protocolNum = 6 Then 'TCPの場合ヘッダ長を取得 Dim tcpHeaderLength As Integer = Convert.ToInt32(Convert.ToString(buff(32), 2).PadLeft(8, "0").Substring(0, 4), 2) * 4 For i As Integer = IP_HEADER_LENGTH + tcpHeaderLength To packetSize - 1 contentBuff(contentCounter) = buff(i) contentCounter = contentCounter + 1 'ContentSizeを超えるデータ量だった場合は中断(パケット取り違えの場合) If contentCounter >= contentBuff.Length Then Exit For End If Next End If 'もしパケットにContent-Lengthが含まれていたらバッファリングは中断する If utf8Str.Contains("Content-Length:") Then isBuffered = False End If End If If Not isBuffered Then 'バッファリング待機なしの場合 'コンテンツバッファカウンターのクリア contentCounter = 0 'Content-Lengthのクリア contentSize = 0 'httpヘッダのContent-Lengthを取得 If utf8Str.Contains("Content-Length:") Then Dim r As New System.Text.RegularExpressions.Regex("Content-Length: ([0-9]+)", System.Text.RegularExpressions.RegexOptions.IgnoreCase Or System.Text.RegularExpressions.RegexOptions.Singleline) Dim mc As System.Text.RegularExpressions.MatchCollection = r.Matches(enc.GetString(buff)) For Each m As System.Text.RegularExpressions.Match In mc '正規表現に一致したグループの文字列を表示 contentSize = m.Groups(1).Value Exit For Next End If If contentSize > 0 Then 'gzipかどうかの判別 isGzip = utf8Str.Contains("Content-Encoding: gzip") 'Contentがスタートする場所を取得 Dim contentStart As Integer = packetSize For i As Integer = 4 To packetSize - 1 If buff(i - 4) = 13 AndAlso buff(i - 3) = 10 AndAlso buff(i - 2) = 13 AndAlso buff(i - 1) = 10 Then contentStart = i Exit For End If Next 'Contentの取得 contentBuff = New Byte(contentSize - 1) {} For i As Integer = contentStart To packetSize - 1 contentBuff(contentCounter) = buff(i) contentCounter = contentCounter + 1 Next End If End If If contentSize > 0 AndAlso contentBuff IsNot Nothing Then If contentCounter >= contentSize - 1 Then 'コンテンツが全部取得できた isBuffered = False 'ファイルに書き出す Dim filePath As String = "C:\test\Contents[" & packetCounter.ToString.PadLeft(6, "0") & "]" Dim thawFilePath As String = filePath If isGzip Then filePath = filePath & ".gz" End If Using fs As New System.IO.FileStream(filePath, System.IO.FileMode.Create, System.IO.FileAccess.Write) fs.Write(contentBuff, 0, contentBuff.Length) End Using If isGzip Then Dim gzipFileStrm As New System.IO.FileStream(filePath, System.IO.FileMode.Open, System.IO.FileAccess.Read) Using gzipStrm As New System.IO.Compression.GZipStream(gzipFileStrm, System.IO.Compression.CompressionMode.Decompress) Using outFileStrm As New System.IO.FileStream(thawFilePath, System.IO.FileMode.Create, System.IO.FileAccess.Write) Dim gzipBuff(1024) As Byte While True '書庫から展開されたデータを読み込む Dim readSize As Integer = gzipStrm.Read(gzipBuff, 0, gzipBuff.Length) '最後まで読み込んだ時は、ループを抜ける If readSize = 0 Then Exit While End If '展開先のファイルに書き込む outFileStrm.Write(gzipBuff, 0, readSize) End While End Using End Using '圧縮ファイルを削除する File.Delete(filePath) End If Else 'コンテンツが全部取得できていないので次のパケットを待つ isBuffered = True End If End If ws.WriteLine("[PacketCount={0}]", packetCounter.ToString.PadLeft(6, "0")) ws.WriteLine("送信元{0}/{1} - 送信先{2}/{3}", srcIP, srcPort, dstIP, dstPort) ws.WriteLine("packetSize={0}, protocolNum={1}, contentSize={2}", packetSize, protocolNum, contentSize) ws.WriteLine(utf8Str) ws.WriteLine() ws.Write("----------------------- BINARY -----------------------") 'パケットの内容を16進数で表記 For i As Integer = 0 To packetSize - 1 If (i Mod 16) = 0 Then ws.WriteLine() ws.Write("{0}:", (i / 16).ToString().PadLeft(4, "0")) End If ws.Write(" {0}", Convert.ToString(buff(i), 16).PadLeft(2, "0")) Next ws.WriteLine() ws.WriteLine("------------------------------------------------------") ws.WriteLine() ws.WriteLine() packetCounter = packetCounter + 1 End If Loop End Using End Sub End Module