この記事は信州大学kstmアドベントカレンダー2018の10日目として執筆された記事です。
CTFのパケット解析問題で手動で処理するのが大変な時に、自動化に使えるPythonライブラリのお話と実際の具体例です。
#使ったライブラリ
Scapy
Pythonで作成されたインタラクティブ型のパケット操作プログラムです。importすることでモジュールとしてPythonコード内で利用することもできるため、今回はこれを利用してパケット処理を自動化します。
re
Pythonに標準で搭載されている正規表現操作モジュールです。Scapyはセッション層以上の層のデータは文字列型として格納されているため、HTTPで転送されたデータ等の抽出には正規表現を用いる必要があります。
#解く問題
CTF過去問の本に載っている猫の写真の問題です。(パケットファイルの抽出まではカット)
方針
得られたパケットファイルをwiresharkで確認すると、HTTPで受信されているデータ内にflag.pngというファイルがありますが、ダウンロードして確認すると破損していて開けません。画像が送られてきたパケットを確認してみると、Content-range=hoge~hoge
というヘッダーがあることがわかります(下記コードの最後尾)。
このことから、HTTPのGETリクエストの送信の際、Rangeヘッダーにデータの範囲を入れて送信すると、その部分だけを取得できるrange requests
を用いて、1枚の画像を分割取得していることがわかります。なので、これらの画像を繋ぎなおせばflagが得られそうですが、受信順がランダムで大変そうなので、pythonで自動化したいとおもいます。
自動化
取得したパケットファイルにはHTTP以外にもICMPによるechoパケットやTCPの3ウェイハンドシェイク(TCPは接続相手からの応答を確認してからデータのやりとりを始めるコネクション型プロトコルなので、通信前に3ウェイハンドシェイクと呼ばれるコネクション確立を行う)に使用されたパケットも含まれているので、事前に画像ファイルが含まれているパケットのみを抜き出す必要があります。
from scapy.all import*
import re
from operator import itemgetter
ansdata = ''
packet = rdpcap('hidden.pcap').filter(lambda p:Raw in p and TCP in p and p[TCP].sport == 80)
このコードではhidden.pcapファイルの内、送信元ポートが80番、トランスポート層のプロトコルがTCP、画像を含んでいるRawレイヤーを持つパケットのみを抽出しています。
scapyでは個々のパケットの要素には下記のようにアクセスできます。
>>> packets[16]['IP'] #17番目パケットのネットワーク層以降のデータを表示
<IP version=4 ihl=5 tos=0x0 len=52 id=34872 flags=DF frag=0 ttl=64 proto=tcp chksum=0x2ec8 src=192.168.1.65 dst=192.168.1.50 options=[] |<TCP sport=http dport=35410 seq=1481748364 ack=1033181681 dataofs=8 reserved=0 flags=A window=227 chksum=0x3388 urgptr=0 options=[('NOP', None), ('NOP', None), ('Timestamp', (4294911925, 371917))] |>>
>>> packets[16]['IP'].dst #17番目のパケットの送信先IPアドレスを表示
'192.168.1.50'
なので、画像ファイルを取得しているパケットのRawレイヤーを見てみると、下記のようになっていることがわかります。
>>> packets[17]['Raw'].load
b'HTTP/1.1 206 Partial Content\r\nServer: nginx/1.10.0 (Ubuntu)\r\nDate: Thu, 22 Jun 2017 23:38:14 GMT\r\nContent-Type: image/png\r\nContent-Length: 909\r\nLast-Modified: Tue, 20 Jun 2017 06:32:38 GMT\r\nConnection: keep-alive\r\nETag: "5948c186-2e45"\r\nContent-Range: bytes 3017-3925/11845\r\n\r\n\xaf_?\x992e\x8a\x04\x04\x04\xd8\xe4\xc4Z[e6o\xde\\\x86\x0c\x19"C\x86\x0c\xb9\xe1\xff\x0b\x0b\x0b%))\xc9\xa2\xc7\x82m\xdb\xb6\xd5\x9d\xee\xe5\xe5eW\xf5611QJJJ\xc4\xd3\xd3S<==\xc5\xc3\xc3C\xb7I\x8b\x8b\x8bK\xd5c\xb3\xc2\xc2\xc2\xaa\xc7eEEE\x12\x10\x10`\x17\xdbT\xdfu\xe3vfOuE/\x00z{{\xd7\xe9~i\xf0/\x0f\xf1\x08\xb8\xe1>\x02...\xd6\xec\x0c\xdb\xc5\xc5E\xe5\xe6\xe6*\xa5\x94:z\xf4\xa8\xe6:\xdf{\xef\xbd\x16\x97[XX\xa8\x9a6m\xaa\xb9\xccf\xcd\x9aY\xdd\t\xef\xf1\xe3\xc7U\xb7n\xdd\xd4\x8c\x193TTT\x94\xfa\xd7\xbf\xfeeq\x9f\x85Z\x8f\x80\x07\x0f\x1e\\\xe3\xc7\xedZ\x8f\x1c\xe4\xdf}ieffj\xf6\x15\x19\x14\x14du\xd9\x17.\\PO>\xf9\xa4E\xa3\xdft\xef\xde]m\xd8\xb0\xc1\xea\xc7\xde\xf5Q\xa6\xb5\xe6\xcf\x9f\xaf\xbb^Z\x9d\xc3\xbe\xf7\xde{\xba\x1d\xe4^/##CEGG\xabG\x1ey\xa4jT\x1b\'\'\'\xe5\xe9\xe9\xa9\xfc\xfc\xfc\xd4\xb0a\xc3\xd4\xeb\xaf\xbf\xae\x8e\x1e=jv\xfb\xf5~\xbb,\xf9\xb4l\xd9\xd2\xf0\xf6\x04\x04\x04\xdc\xf0\xdd\xac\xac,\xb5v\xedZ\x15\x16\x16\xa6\xfc\xfd\xfdMv\xdc\xfe\xfe\xfb\xef\xdbU\xddx\xff\xfd\xf7\ro_EE\x85\x8a\x8b\x8bSK\x96,Q\x03\x07\x0eT\x9d;wVnnn\xca\xdd\xdd]u\xec\xd8Q\x8d\x1c9R\xfd\xedo\x7fS\xe9\xe9\xe95:\xf6\xae\\\xb9\xa2V\xaf^\xad\xc2\xc3\xc3U\xf7\xee\xdd\x95\x97\x97\x97rttT-Z\xb4P~~~j\xd4\xa8Q\xea\xb5\xd7^SqqqU\xf3l\xd9\xb2Es;z\xf5\xeae\x97u\xa5\xd2\x13O<\xa19oAAA\x9d\xd6\xfbW^y\xa5A?\x02&\x006\xe0\x00\xa8\xf7#2n\xdc\xb8\x1b~\x08;u\xea\xa4\xf9\xdd\xc4\xc4D\x8b\xca\xdd\xbe}\xbb\xee\x0f\xc8\x7f\xfe\xe7\x7f\xd6\xebqQ[\x01\xf0\xf5\xd7_\xd7\xdcfgggu\xe4\xc8\x11\xa5\x94R\xce\xce\xce6\r\x80\x1f\x7f\xfcq\x8dz\xde\x1f?~\xbc\xba|\xf9\xb2\xdd\x97Y\x93\x13\xb0\xaf\xaf\xaf\xe6\xbat\xee\xdcY3\x90}\xf2\xc9\'f\x03`vv\xb6z\xe9\xa5\x97,\xea\xe0<$$D\xed\xdd\xbb\xb7\xce\x03\xe0\xda\xb5k\xcd\x06\xa4\xb2\xb22\xf5\xce;\xef\xa8\xc6\x8d\x1b\x9b]~BB\x82]\xd5\x8dm\xdb\xb6\x19\n\x80\'N\x9cP\x83\x06\r2\xb4\x0f]]]\xd5_\xff\xfaWUZZj\xd1q\x97\x97\x97\xa7\xfe\xe3?\xfe\xc3\xa2\xe3"88X\xc5\xc6\xc6\xaa/\xbf\xfcR\xf3;\x03\x07\x0e\xb4\xcb\xbaR\xe9\xc1\x07\x1f49\xaf\x87\x87G\x9d\xff\xd6\x13\x00\t\x80\r6\x00\x86\x85\x85i\xaeKtt\xf4\r\xdf\xd5\x1b\x19c\xd1\xa2E\x16\x95\x1b\x11\x11\xa1\xfb#w\xf1\xe2\xc5\xdb.\x00\xc6\xc4\xc4\xe8n\xf3\xaaU\xab\xaa\xee\xca\x8aN\x0f\xf5\x96\xdeq|\xe1\x85\x17l\x12\x14\xbau\xebf\xe8\xael}\x94Y\xd3\x13\xda\x88\x11#t\xd7c\xf9\xf2\xe5\x9a\xf3o\xde\xbcYs\xbe\xb1c\xc7\xaa\xa3G\x8f\xaa\xf6\xed\xdb[\xbd\x0f^|\xf1E\x93\xa3\x12\xd4V\x00\xd4;N\x03\x02\x02Taa\xa1\x1a<x\xb0\xa1e\xfb\xf8\xf8\x18\xba\x93[\x97uC\xefiF\xbf~\xfdTEE\x85Z\xb2d\x89rpp\xb0x_>\xf0\xc0\x03\x86C`||\xbc\xea\xd0\xa1\x83\xd5\x7f\xb7{\xef\xbdWs\xda\xe8\xd1\xa3\xed\xb2\xaeT\xba\xe7\x9e{L\xce\xdb\xb5k\xd7\xaa\xef\x1c9rD'
この文字列の中から、画像データ部と画像内での部分を示すContent-Range
の数字部を抽出し、辞書型で紐づけて管理します。
contents = []
d = {}
for session in session_list:
for i,p in enumerate(session):
data = p[Raw].load
if i == 0:
m = re.search(b'(?P<bytes>(\d+)-(\d+))/(\d+)\r\n\r\n(?P<payload>(.*))',data,flags=(re.MULTILINE | re.DOTALL))
if m is not None:
d = m.groupdict()
else:
d['payload'] += data
contents.append(d)
#contents[0] = {'bytes': b'0-1184', 'payload': b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x02\x80\x00\x00\x01\x90\x08\x06\x00\x00\x00>\xf3\xd1%\x00\x00\x00\x06bKGD\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\x00\x00\tpHYs\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x07tIME\x07\xe1\x06\x14\x045*?t\xf4S\x00\x00\x00\x1diTXtComment\x00\x00\x00\x00\x00Created with GIMPd.e\x07\x00\x00\hogehogehogehoge'}
?P<name>
を持ちいることで、後にname部で指定したインデックスで一致部のデータにアクセスできるようになります。
print(new_content[0]['bytes']) #b'0-1184'
bytesインデックス内のデータでソートを行い、ファイルに書き込みを行うことでflagが書かれたファイルが得られます。
new_content = sorted(contents,key = itemgetter('bytes'))
print(new_content[0]['bytes'])
f = open('flag_ans.png','wb')
for i in new_content:
f.write(i['payload'])
f.close()
#終わりに
CTFビギナーで唯一とけたのがパケット解析問題だったのでそこからいろいろ調べてみましたが、ネットワーク関係の知識もつけつつ勉強できるのでとてもいい経験になりました(大学生並の感想)。