本記事は、オライリージャパンから発行されている「サイバーセキュリティプログラミング ―Pythonで学ぶハッカーの思考(原題:Black Hat Python)」の学習メモとして、書籍ではPython2で書かれていますが、自分なりに解釈した上でPython3に書き直しをしています。
今回は、Pythonでプロキシツールの実装を学んでいきます。
- プロキシとは
- ライブラリのインポート
- 16進ダンプの実装
- データ受信処理の実装
- パケットの改ざん、改変の処理
- プロキシハンドラーの実装
- サーバ処理の実装
- メイン関数の実装
- プロキシサーバを起動してみる
- 最後に
- 参考書籍
プロキシとは
プロキシとは代理を意味し、主にWebブラウザとWebサーバの間に入って処理をする「プロキシサーバ」が馴染み深いと思います。
ここでは、ローカルホストがリモートサーバにアクセスする際の間に入り、リクエストとレスポンスを中継しつつ、そのパケットの内容を表示するツールをPythonで実装していきます。
ライブラリのインポート
今回は以下のライブラリを使用します。
# proxy.py import sys import socket from threading import Thread
16進ダンプの実装
ローカルとリモートのパケットを16進数とテキストでダンプしたものを表示します。
def hexdump(src, length=16): result = [] for i in range(0, len(src), length): s = src[i:i+length] hexa = ' '.join(['{:02X}'.format(x) for x in s]) text = ''.join([chr(x) if x >= 32 and x < 127 else '.' for x in s]) result.append('{:04X} {}{} {}'.format(i, hexa, ((length-len(s))*3)*' ', text)) for s in result: print(s)
データ受信処理の実装
ローカルとリモートのsocketオブジェクトからデータの受信を処理し、そのbytesオブジェクトを返り値とします。
def received_from(connection): buffer = b'' connection.settimeout(2) try: recv_len = 1 while recv_len: data = connection.recv(4096) buffer += data recv_len = len(data) if recv_len < 4096: break except: pass return buffer
パケットの改ざん、改変の処理
今回は実装していませんが、ローカルおよびリモートのパケットを改ざん、改変することも可能です。
必要に応じて以下に特定のパケットが来たら改ざんする処理を実装します。
def request_handler(buffer): return buffer def response_handler(buffer): return buffer
プロキシハンドラーの実装
今回のプロキシサーバのメイン処理を行う関数です。
def proxy_handler(client_socket, remote_host, remote_port, receive_first): remote_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) remote_socket.connect((remote_host, remote_port)) if receive_first: remote_buffer = received_from(remote_socket) hexdump(remote_buffer) remote_buffer = response_handler(remote_buffer) if len(remote_buffer): print('[<==] Sending {} bytes to localhost.'.format(len(remote_buffer))) client_socket.send(remote_buffer) while True: local_buffer = received_from(client_socket) if len(local_buffer): print('[==>] Received {} bytes from localhost.'.format(len(local_buffer))) hexdump(local_buffer) local_buffer = request_handler(local_buffer) remote_socket.send(local_buffer) print('[==>] Sent to remote.') remote_buffer = received_from(remote_socket) if len(remote_buffer): print('[<==] Received {} bytes from remote.'.format(len(remote_buffer))) hexdump(remote_buffer) remote_buffer = response_handler(remote_buffer) client_socket.send(remote_buffer) print('[<==] Sent to localhost.')
サーバ処理の実装
ローカルクライアントからの接続を待ち受ける関数です。
def server_loop(local_host, local_port, remote_host, remote_port, receive_first): server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: server.bind((local_host, local_port)) except: print('[!!] Failed to listen on {}:{}'.format(local_host, local_port)) print('Check for other listening sockets or correct permissions.') sys.exit(0) print('[*] Listening on {}:{}'.format(local_host, local_port)) server.listen(5) while True: client_socket, addr = server.accept() print('[==>] Received incoming connection from {}:{}'.format(addr[0], addr[1])) proxy_thread = Thread(target=proxy_handler, args=[client_socket,remote_host, remote_port, receive_first]) proxy_thread.start()
メイン関数の実装
コマンドライン引数の解釈については、細かいチェックは行わず、引数の数だけチェックを行います。
def main(): if len(sys.argv[1:]) != 5: print('Usage: ./proxy.py [localhost] [localport] [remotehost] [remoteport] [receive_first]') print('Example: ./proxy.py 127.0.0.1 9000 10.12.132.1 9000 True') sys.exit(1) local_host = sys.argv[1] local_port = int(sys.argv[2]) remote_host = sys.argv[3] remote_port = int(sys.argv[4]) receive_first = sys.argv[5] if 'True' in receive_first: receive_first = True else: receive_first = False server_loop(local_host, local_port, remote_host, remote_port, receive_first) if __name__ == '__main__': main()
プロキシサーバを起動してみる
ここでは、前回作成したNetcatライクなコマンドラインツールを使って、パケットの中身を見てみます。
まず、プロキシサーバを起動します。
> python proxy.py 127.0.0.1 8888 127.0.0.1 8000 False [*] Listening on 127.0.0.1:8888
こちらではローカルサーバのポート番号8888でクライアントからの接続待ちをし、そのパケットをリモート先のポート番号8000に転送します。
最後の引数の"False"はサーバから先にパケットを受信するかどうかのフラグになり、ここではクライアントが先にパケットを送信することを指定します。
FTPサーバなどでは、TCPの3 Way Handshakeでコネクションが確立されたら、サーバ側から最初にバナー情報などのパケットを送信するため、その場合は"True"を指定します。
それではコマンドラインツールを起動し、パケットの中身を表示してみます。
[==>] Received incoming connection from 127.0.0.1:15152 [<==] Received 7 bytes from remote. 0000 3C 42 48 3A 23 3E 20 <BH:#> [<==] Sent to localhost. [==>] Received 2 bytes from localhost. 0000 6C 73 ls [==>] Sent to remote. [<==] Received 21 bytes from remote. 0000 62 68 6E 65 74 2E 70 79 A 68 6F 67 65 A 3C 42 bhnet.py.hoge. [<==] Sent to localhost. [==>] Received 8 bytes from localhost. 0000 63 61 74 20 68 6F 67 65 cat hoge [==>] Sent to remote. [<==] Received 28 bytes from remote. 0000 74 65 73 74 20 66 69 6C 65 20 75 70 6C 6F 61 64 test file upload 0010 69 6E 67 2E A 3C 42 48 3A 23 3E 20 ing..<BH:#> [<==] Sent to localhost. [==>] Received 4 bytes from localhost. 0000 65 78 69 74 exit [==>] Sent to remote.
最初にlsコマンドを実行し、前回アップロードしたhogeファイルをcatコマンドで表示しているのが確認できました。
最後に
今回はプロキシサーバの構築について学びました。
Wiresharkなどのパケットキャプチャツールの場合は、パケットを解析するためのドライバ(WinPcapやNpcapなど)をインストールしなければなりませんが、このようなプロキシを通すやり方であれば、簡単にパケットの流れを読むことができます。