どうもみむらです。
先日、IPA が主催するセキュリティキャンプ 2014 の応募が締め切られまして、
噂では結構な倍率だったそうです。
http://www.ipa.go.jp/jinzai/camp/2014/zenkoku2014_sheet2.html
応募用紙は各クラス毎に大変難しい問題が用意されていて、
その中でも、特に今回は「ネットワーク・セキュリティ・クラス」の問題が難しいということで
少しやってみることにしました。
まずはマシンを用意。
Windows 7 Service Pack1 の環境を用意します。
問題ファイルを見てみる
ファイルをダウンロードして・・
テキストファイル!
ということで開いてみますと・・
・・・全然テキストファイルじゃないですね。
これが闇か。
くよくよしていられませんので早速がんばってみます。
読めそうな部分に、 “Server : Apache/2.2.16(debian)” というようなものがあります。
“Apache” という名前で調べてみると、どうやら Web サーバのようで、
もしかするとこれは HTTP の通信では無いかというところが分かります。
http://httpd.apache.org/
また、先頭が “ヤテイ” になっているところから、pcap ファイルということで間違いなさそうです。
切り出してみる
切り出してみれば何か分かるかもしれない!
HTTP なら “GET” があるはずということでそこ以降を切り出してみます。
・・・何か読めそうな文字が続きますが、それ以降が読めません。
帰ってくる内容だけ取り出してみることにします。
返答部分の最後が “Content-Type: text/plain” なのでそれを使います。
メモ帳で文字数をカウント。どうやら24文字のようです。
早速切り出してみます。でもやっぱりよく分からないデータはよく分からないデータ。
なにか余計なデータがあって、それによって化けてしまっているのかも!
・・てことで数字でデータを眺めてみます。
13,10,13,10.. 改行ですね。必要ないので削りましょう。
うん。良い感じ。これなら読める・・?
やっぱりダメですか・・?
エンコーディングが間違っているのかもしれません。
変換する処理を書くのは面倒なので、ファイルに書き出してメモ帳に与えてみます。
・・・やっぱり出ません。ちょっと何か見落としているんでしょうか。
HTTP ヘッダのところに “Content-Encoding: gzip” とあります。
でもメモ帳ではそういうエンコーディングは対応していないようです。
http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
仕様を見てみると、どうやらこれは gzip 圧縮を意味しているようです。
では残りの部分を gzip で展開させればデータが出ますね。
・・・いや、出ません。。
どうも調べてみると、通信時にパケットで分割されているらしく
そのまま後半の部分を与えても読めないようです。
確かに Content-Length をみるとなかなかに大きい値になっています。
TCP パケットの情報が分かれば・・というところですが、
「オプションフィールド」ということで可変長のデータが最後にくっついているため
後ろから見つけ出すのは問題がありそうです。
では pcap を頭から読むかとしても、
さすがに pcap を読むライブラリは入っていません。どうしたら良いでしょうか。
pcap を読む
困ったときは仕様を見ます。
まず pcap から
http://wiki.wireshark.org/Development/LibpcapFileFormat
最初に pcap ファイルに関する情報が入っているようです。 (pcap_hdr_s)
ですが余りここには必要な情報はなさそうですので
4+2+2+4+4+4+4 = 0x18 分読み飛ばします。
typedef struct pcap_hdr_s {
guint32 magic_number; /* magic number */
guint16 version_major; /* major version number */
guint16 version_minor; /* minor version number */
gint32 thiszone; /* GMT to local correction */
guint32 sigfigs; /* accuracy of timestamps */
guint32 snaplen; /* max length of captured packets, in octets */
guint32 network; /* data link type */
} pcap_hdr_t;
次に各パケット毎の情報が入っているようです。 (pcaprec_hdr_s)
取得時間とパケットのファイル上のサイズと実サイズが記述されています。
読み飛ばす際やパケットを処理する際に必要になるので、
8 – 12 にある incl_len を切り出しておきます。
typedef struct pcaprec_hdr_s {
guint32 ts_sec; /* timestamp seconds */
guint32 ts_usec; /* timestamp microseconds */
guint32 incl_len; /* number of octets of packet saved in file */
guint32 orig_len; /* actual length of packet */
} pcaprec_hdr_t;
Little Endian なので
[System.Bitconverter]::ToInt32($data | Select-Object -Skip (0x18 + 8) -First 4),0)
とかすると数字に変換できます。
incl_len で指定された範囲にあるデータが1つあたりのパケットになり、
incl_len 分読み飛ばした場所にはまた パケット毎のヘッダ情報とデータがあります。
パケットを読む
あとは仕様書に従って読んでいきます。
まずは イーサネットヘッダから
http://www.atmarkit.co.jp/ait/articles/0107/05/news001_3.html
最初のプリアンブル部分はどうも取得されていないようなので、宛先アドレス, 送信先アドレス, タイプ ってことで 6+6+2 = 14 バイト分読み飛ばします。
次に IP ヘッダ
http://www.atmarkit.co.jp/ait/articles/0304/04/news001_2.html
まずここの部分のサイズを取得する為にヘッダ長を取得します。
4bit ということで、8bit 単位でデータを取得した後で 0x0F を AND して取り出します。
出てきた数値は 32bit 単位ということですのでその値に ×4
コードとしては、
($ipheader[0] -band 0xF) * 4
とかすると、実際のサイズが出てきます。
サイズを求めたら、そのサイズ分読み飛ばします。
次に TCP ヘッダ
http://www.atmarkit.co.jp/ait/articles/0401/29/news080_2.html
まず、データ・オフセットを読みます。
これはアプリケーションが決めた送受信データ・・がどこから始まるかというのが書いてあり、
今回欲しいデータの始まる位置が分かります。
今回の場合は 上位 4bit ということで、 0xF で AND したあとで ÷16 を行い、
IP ヘッダと同じく 32bit 単位なので ×4 します。
コードとしては、
(($tcpheader[12] -band 0xf0) / 16) *4
こんな感じ。
そして、フラグも同様に切り出します。
[PSH], [ACK] を監視して PSH – ACK or [PSH,ACK] の区間を切り出してデータとして眺めるようにしてみます。
データとしては 先ほど取り出したデータオフセット(4bit)の直後から 12bit となります。
(要は 4bit + 8it)
コードとしてはこんな感じ:
($tcpheader[12] -band 0xF) * 256 + $tcpheader[13]
後は 先ほど取得したデータ・オフセット分だけデータを読み飛ばします。
ここまで来ればここから先は HTTP の通信データになります。(やったね!
HTTP を読む
何でしょうかこの、戻ってきた感じと達成感。
マリオRPG でもありましたよね。
クッパ城に最初に行って、なんか剣が突き刺さって・・
マップをぐるーっと回ってクッパ城に戻ってくる感じの。アレに似てますよね。
・・・・ちょっと時代が古いですかね。ごめんなさい。
さて。
まず HTTP 通信が行われているかどうかをさくっと判定します。
flag を見ても良いんですが 現在の位置が最初に取得したパケットのサイズを
パケットの頭の位置に足し合わせた値が一緒ならデータが無いので次のパケットを読み込みます。
ある場合にはデータを見ていきます。
・・とはいってもデータが取れているか気になるモンです。
なにか通信しているデータがある時にその中身を適当に表示させてみると。
・・・良い感じですね。良い感じでデータが出てきました。
あとは、Flag が PSH だけの間はそのデータを結合していき、
ACK or PSH,ACK が来たときにデータが完全に送られたと判断して、処理を行います。
データが取れたら、後は一番最初でやったように HTTP ヘッダを読み飛ばして下の部分だけ切り出し、
Gzip を展開します。
C# の GzipStream が使えますので、それを呼び出します。
http://msdn.microsoft.com/ja-jp/library/system.io.compression.gzipstream(v=vs.110).aspx
あとはそれをファイルに書き出すと・・。
テキストファイルが出てきました! これなら読めそうです。
・・・でも、ここからまた問題を解く必要がありそうです。
最後に今回作ったコードを貼り付けておきます:
$file = Get-Content "C:\Users\yahihashoo\Desktop\000038855.txt" -Encoding Byte
$text = New-Object System.String($file,0,$file.Length)
$p = 0x18 # PCAP の Header 分
$fin = 0
$stream = New-Object System.IO.MemoryStream
$output = New-Object System.IO.FileStream "C:\Users\yahihashoo\Desktop\question.txt",([IO.FileMode]::Create),([IO.FileAccess]::Write),([IO.FileShare]::ReadWrite)
do{
$size = [System.BitConverter]::ToInt32(($file | Select-Object -Skip ($p+4+4) -First 4),0) #packet size.
$p += 4 * 4 # PCAP の各パケット分
$packet = ($file | Select-Object -Skip $p -First $size)
$packetpos = 6 + 6 + 2 # Ethernet Frame.
$srcip = $packet[$packetpos + 12] * 0x1000000 + $packet[$packetpos + 13] * 0x10000 + $packet[$packetpos + 14] * 0x100 + $packet[$packetpos + 15]
$dstip = $packet[$packetpos + 16] * 0x1000000 + $packet[$packetpos + 17] * 0x10000 + $packet[$packetpos + 18] * 0x100 + $packet[$packetpos + 19]
$packetpos += ($packet[$packetpos] -band 0xF) * 4 # IP Frame.
$srcport = $packet[$packetpos] * 256 + $packet[$packetpos+1]
$dstport = $packet[$packetpos+2] * 256 + $packet[$packetpos+3]
$seq = $packet[$packetpos + 4] * 0x1000000 + $packet[$packetpos + 5] * 0x10000 + $packet[$packetpos + 6] * 0x100 + $packet[$packetpos + 7]
$flag = (($packet[$packetpos + 12] -band 0x0f) * 256) + $packet[$packetpos + 13]
$packetpos += (($packet[$packetpos + 12] -band 0xf0) / 16) * 4
if ($packetpos -ne $packet.Length)
{
$data = $packet | Select-Object -Skip $packetpos
$html = [System.Text.Encoding]::ASCII.GetString($data)
$htmlpos = $html.IndexOf("`r`n`r`n")
if($htmlpos -lt 0)
{
$htmlpos = 0
$htmlbody = $data
} else {
Write-Host ([System.Text.Encoding]::ASCII.GetString(($data | Select-Object -First $htmlpos)))
Write-Host "---------------------"
$htmlpos += 4
$htmlbody = $data | Select-Object -Skip $htmlpos
}
if($null -ne $htmlbody -and $htmlbody.Length -ne -1)
{
$stream.Write($htmlbody,0,$htmlbody.Length)
}
if(($flag -band 0x8) -eq 0x8) #ACK
{
if($stream.Length -gt 0)
{
$stream.Position = 0
$gzipstream = New-Object System.IO.Compression.GzipStream $stream,([IO.Compression.CompressionMode]::Decompress)
$buf = New-Object byte[](1024)
do
{
$read = $gzipstream.Read($buf,0,1024)
$output.Write($buf,0,$read)
}while($read -gt 0)
$output.Flush()
$gzipstream.Close()
}
$stream.Close()
$stream = New-Object System.IO.MemoryStream
}
}
$p += $size
}while($p -lt $file.Length)
$output.Flush()
$output.Close()
説明文中で書いた読み飛ばしよりも、若干丁寧に読んでます。
実際は シーケンス番号がちゃんと続いてるかとか、IP, PORT あたりの情報も使って
切り出そうと思ったのですが今回はご愛敬ということで。
(ただし、出せるのでやろうと思えば。。でもパケットの順番を見ていないので逆になったりとかするとダメですし、再送とかが発生しているやつを食べさせても処理できません。)
それと注意点として。
ネットワーク上の IP アドレスの値やポート番号、シーケンス番号などは
ビッグエンディアンで取り扱われています。
そのため、一つずつ取り出して掛け合わせるというような演算で求める必要があります。
( Linq とか使って4バイト出して反転してリトルエンディアンとして処理しても求まります。)
この辺を最初忘れていて、値を取り出すのに時間がかかったのは秘密。
もっとずっと簡単に:
解けるらしいですね。
http://yagihashoo.com/archives/683
なんでも、wireshark というソフトウェアを使えば
すぐに内容が抽出できるということで。
いやー・・。なんというかなんというか。。
ツールを知っていることは重要ですね。
最後に:
もちろん、今回の記事はジョークです。
こういうことになってしまった手前、やらねば、と。
もちろん Wireshark は普段使いますし、普段使うマシンにはすべて入れてあります。
こういう縛りなので、Windows 7 のマシンを一つ新規で作成して、
Windows Update を OFF にしてやってました。
PowerShell は最初から入っていることを知っていましたし、
C:\Windows\Microsoft.NET 以下に csc.exe という形で C# のコンパイラが最初から
入っていることも知ってました。
・・ま、PowerShell の方が、より面白そうということで。
・・・なんでこんな無駄な事をやったかってのがあるかと思いますが、
「こんなことも出来るんだよ!」と、
MSP (Microsoft Student Partners ) の端くれとしての意地、とでも言いましょうか。
大道芸ですごい無駄な事を真面目にやるような、
そんな、なんか気持ちを味わってもらえたらなと思ってやりました。
・・・もし感じて頂けたら本望です。
そして・・。
もしこの記事を今年のセキュリティ・キャンプの応募者の方が見ていて、
全力で端折った部分の内容がきちんと分かるということであれば、
応募用紙はさくさくっと書けたのでは無いかなと思います。
是非とも、セキュリティキャンプを通して今後の人生が変わるような、
そんなステキな体験が出来るように祈っています。