
このハックパックでは、ボットネットの基本的な特徴を実装するための基礎的なフレームワークを順を追って説明します。C言語を扱った経験があると役立つかもしれません。このチュートリアルが気に入ったら、このリポジトリにスターを付けてください!
注意: ここで学んだことを悪意のある目的に使わないでください。このハックパックは教育目的のためのボットネットのケーススタディに過ぎません。このハックパックで学ぶ概念は、幅広いユースケース(基本的にはネットワーキングに関わるすべてのこと)に応用できます。最も重要なこととして、このハックパックはローカルでテストし、展開することを意図しています(このハックパックに関連して作成したものを他のハッカーと共有しないでください)。プライバシーは重要なので、それを尊重してください。
ボットネットを構築する前に、ボットネットとは何かを理解することが重要です。ボットネットとは、リモートでコマンドを受信し、それをローカルで展開できるコンピュータのネットワークです。必要に応じて、ネットワーク内の他のノードに情報を中継することもできます。ボットネットは、分散型サービス拒否(DDoS)攻撃からスパイウェアの広範囲な展開まで、あらゆることに使用されてきました。これまでに多くのボットネットを聞いたことがあるかもしれません。最も有名なのは、それぞれ380万台と360万台のIoTデバイスを制御したMiraiとGameover Zeusでしょう。ボットネットが特定のタスクを実装する方法には多くの違いがあります。しかし、ボットネットを成功裏に構築するためには、動作するネットワーク内で以下の機能を保証する必要があります。
私たちのボットネットは、次のことを行うべきです:
この構造は、Command & Control(C&C)ボットネットとして知られるものの特徴です。これらのボットネットには、1つのマスターサーバーと多数のスレーブサーバーがあります。しかし、このスタイルのボットネットは時代遅れで、マスタードメインへのアクセスを遮断することで簡単に破壊できます。より最近の洗練されたボットネットはピアツーピア(P2P)アーキテクチャに従っており、管理者権限はネットワーク内のすべてのノードまたは一部のノードに分散されています。これらのボットネットは、中央の制御点がなく、数百万のノードに成長する可能性があるため、セキュリティの専門家にとって大きな頭痛の種です。そのようなボットネットを壊すこと自体が興味深い読み物です。しかし、このハックパックの目的のためには、物事を単純に保ちましょう。C&Cボットネット用のシンプルなスレーブノードを実装します。

このハックパックは主にクライアントマルウェアの実装を扱います。マスターサーバーには、オープンソースのTCPサーバーであるNetcatを使用できます。Netcatはボットネットとは何の関係もありません。これは、クライアントとの間でテキストパケットを送受信するために再利用できる、便利で確立されたツールにすぎません(マスターとは実際にはそのようなものです)。Netcatサーバーを少し調整して、"master"というバイナリにコンパイルしました。ここでこれ以上の作業は必要ありません!マスターはすぐに使用できます。
より興味深い部分に移りましょう:リモートコマンドの受信と実行です(マルウェアの偽装については後で心配します)。ここでの目標は、スレーブノードを可能な限りシンプルにし、上記の要件に準拠することです。多くの定数がlib/macros.hで定義されているので、自由に使用してください。実装されたすべての関数シグネチャは、lib/connect.hまたはlib/utils.hにあります。
bot.cファイルを開いてください。サーバーで新しいノードを開始するとき、マスターがどのクライアントにコマンドを展開するかを知ることができるように、ノードに名前を付けるべきでしょう。使用できる命名規則は多数あります。IPアドレスを使用することはおそらく最良です。なぜなら、それは各クライアントの一意の識別子だからです。しかし、一般の人間にとって読みやすくするために、コンピュータのユーザー名を使用しましょう。C関数のgetenv()を引数"USER"とともに使用すると、コンピュータがUSER環境変数に格納しているものが返されます。これはユーザーのユーザー名が格納されている場所の1つなので、それを使用しましょう。また、スレーブが実行されているので、マスターを見つけましょう。これを行うには、マスターのIPアドレスを知っている必要があります。すべてのネットワークデバイスにはIPアドレスがあります。それは、他のノードを識別し、位置アドレス指定を行う役割を担っています。さらに、マスターは異なるポートで多くのサーバーを実行できます。したがって、マスターに接続するだけでなく、正しいポートも指定する必要があります。このポートはマスターによって選択されますが、変更できます。このハックパックでは、ローカルでテストすることを望んでいます。そこで、コンピュータをネットワークとして使用します。すべてのコンピュータのローカルIPアドレス("localhost"も解決される)は"127.0.0.1"です。マスターでは、ポート9999で実行するように指定しました。これらの3つの要素(マスターIPアドレス、マスターポート、スレーブ名)により、サーバーとクライアント間の通信パイプライン(ソケットと呼ばれる)を開始できます。これら3つの引数を関数init_socket()に渡してソケットを作成します。init_function()はCの組み込みコマンドではありません。むしろ、それを実装する必要があります。次に、受信メッセージを保持するためにスタック上にいくらかのスペースを割り当てる必要があります。ここではかなり余裕を持たせて、約10KBのスタックスペースを使用しましょう。このスタックポインタをmsgと呼びます。最後に、すべてがうまくいっていることを示すprintfステートメントがあります。
char* name = //クライアントのユーザー名を取得してnameに格納します
int channel = //SERVER、PORT、nameを指定してチャネルを初期化します
//CMD_LENGTHサイズのスタックスペースを割り当てて、char型のデータを保持します。スタックポインタをmsgと呼びます
printf("%s joining the botnet\n", name);
次にlib/connect.cに切り替えます。init_channel()を実装しましょう。まず、長さCMD_LENGTHのスタックcharバッファmsgと、マスターへの接続に関する情報を保持する特別なCネットワーク構造体であるserverを定義しました。まず、渡されたIPアドレスを、人間が読める形式(数字とドットで構成)からネットワークバイトオーダーのバイナリ形式に変換します。これは、ソケットライブラリの特別なC関数inet_addr()を使用して行われます。これは単にIPアドレスを受け取り、ネットワークで使用可能なバイナリを出力します。Cでは、sockaddr_inという構造体のフィールドを埋めることでネットワークを簡単に指定できます。この構造体のインスタンスはserverと呼ばれます。この構造体の3つのフィールドを埋める必要があります:server.sin_addr.s_addr(マスターIPアドレス)、server.sin_family(通信ドメインを指定する1バイト値)、server.sin_port(マスター上で接続するポート)。sin_familyには、ソケットライブラリによって提供されるCマクロを指定できます。通常、この場合のように、このフィールドをAF_INETに設定します。これは、接続がネットワークノードをIPアドレスで識別することを意味し、それが私たちの望みです。ただし、PF_INETを使用することも可能で、これはAF_INETと似ていますが、ネットワークがプロトコル内の任意のものを使用して特定のノードを識別できることを指定します。また、両方が存在する歴史的な理由も数多くありますが、それは私がよく知らないし、あまり気にしないことです。ただAF_INETを使用してください。最後に、サーバーポートを設定するときは、portを特別な関数htons()(host to network short)に通す必要があります。これはデータをホストバイトオーダーからネットワークバイトオーダーに変換します。このバイトオーダーの問題は、エンディアンと呼ばれるものに関係しています。詳細はこちらで読むことができます。
最後に、マスターとスレーブの間の実際の接続を定義する必要があります! これを行うには、データを送信できるネットワークソケットを定義します。マスターが多くの「電気ソケット」を持っていると考えてください。次に、スレーブ側にマスターの「壁コンセント」に合う「プラグ」を構築する必要があります。これはソケットライブラリのsocket()関数を使用して行えます。なんと便利でしょう! socket()は3つの引数を取ります:通信ドメイン、ソケットタイプ、プロトコルです。通信ドメインには、おそらくもうお分かりでしょう:AF_INETです。ソケットタイプには、データを双方向にストリーミングするだけのソケットが必要です。したがって、与えられたマクロSOCK_STREAMを使用します。ソケットプロトコルについては心配しないでください。これはかなり基本的なネットワークなので、デフォルトのプロトコルを示す値0を使用しましょう。この関数はソケットを表すintを返します。この値をchannelに格納します。次に、ソケットを起動します(スレーブをマスターの壁コンセントに差し込みます)。C関数connect()を呼び出します。これは3つの引数を取ります:チャネル、sockaddr構造体、構造体のバイト単位のサイズです。connect()が正の整数を返す場合、マスターとの接続は成功です! 新しく確立された接続をテストするために、マスターに挨拶メッセージを送信しましょう! メッセージバッファを埋めて、respond()(まだ実装されていません)を使用してmsgをchannelを通じてマスターに送信します。最後に、init_channel()関数がこの成功した接続を返すようにします。
int init_channel (char *ip, int port, char *name) {
char msg[CMD_LENGTH];
struct sockaddr_in server;
server.sin_addr.s_addr = //ipをネットワークバイトオーダーに変換します
server.sin_family = //サーバーの通信ドメインを設定します
server.sin_port = //portをネットワークバイトオーダーに変換します
int channel = //SOCK_STREAMソケットを定義します
if(channel < 0) {
perror ("socket:");
exit(1);
}
int connection_status = //定義したチャネルを使用してスレーブをマスターサーバーに接続します
if (connection_status < 0) {
perror ("connect:");
exit(1);
}
//文字列をmsgにロードして挨拶メッセージをマスターに送信します(ヒント:snprintfが役立ちます)
respond (channel, msg);
return channel;
}
スレーブがマスターに接続されると、常にメッセージを待機し、コマンドを受信したらすぐに行動する必要があります。そこで、無限whileループを使用して、これらのメッセージを受信および解析しましょう。bot.cのprintfステートメントの下に、recieve()とparse()の2つの関数をその順序で呼び出す無限whileループを追加します。両方の関数は、channelとmsgスタックバッファを引数として取ります。関数シグネチャはlib/utils.hにあります。これは次のようになります:
無限ループ {
recieve(...);
parse(...);
}
utils.cに移動して、recieve()とrespond()を実装します。recieve()はチャネルからメッセージを取得し、respond()はチャネルを通じてメッセージを送り返します。respond()のパラメータは、ソケットアドレスsとスタックバッファmsg_bufです。C関数write()を使用して、スタックバッファの内容をチャネルに書き込み、そのステータスを返します。write()には3つの引数が必要です:ソケットアドレス、メッセージバッファ、メッセージの長さです。
int respond(int s, char *msg_buf) {
//msg_bufの内容をソケットsに書き込み、ステータスを返します
}
recieve()も同様に単純なヘルパーです。msgバッファをリセットします(ヒント:memset()を使用)。次に、ソケットライブラリ関数read()を呼び出してmsgを読み取ります。read()は3つの引数を取ります:ソケットアドレス、メッセージバッファ、メッセージの最大期待長です。
int recieve(int s, char *msg) {
//msgバッファをリセットします
int read_status = //ソケットsの内容をmsgに読み込みます
if (read_status) {
perror("log:");
exit(1);
}
return 0;
}
もうすぐ完了です! 今のところ、ボットネットはかなり退屈です。ソケットを介してメッセージを受信および送信することしかできません。受信したものをターミナルで実際に実行させましょう。まず、関数parse()を実装します。これは名前が示すとおり、コマンドを解析します。メッセージが不正な形式かどうかを確認する簡単なエラーチェックを行うことができます。また、受信したが自分宛てではないメッセージは黙って無視したいです。メッセージはマスターから(ボットネットの名前):(実行するコマンド)の形式で送信されます。前者はすでにやっておきました。2つのチェックを通過したら、コマンドをexecute()関数に渡しましょう。
int parse (int s, char *msg, char* name) {
char *target = msg;
//メッセージがこのクライアント宛てかどうかを確認します。違う場合は、0を返してパケットを黙ってドロップします
char *cmd = strchr(msg, ':');
if (cmd == NULL) {
printf("Incorrect formatting. Reference: TARGET: command");
return -1;
}
//cmdポインタを実際のコマンドの先頭に調整します
//終端文字をコマンドの末尾に調整します
//受信したコマンドの内容をローカルに表示するステートメントを出力します
execute (s, cmd);
return 0;
}
次はクールな部分です。execute()は、受信したコマンドをターミナルにパイプし、出力をソケットを通じてマスターに書き込む必要があります。各行の入力を格納するスタックバッファを作成します。次に、C関数popen()を使用して入力を実行し、出力をファイルfに格納します(この時点では、多くの方法があります。マスター入力を使用してボットネットを本当にクールなことを行うようにカスタマイズしたり、ボットネット内の他のノードと自律的な連携/更新を実行したりできます。自由に創造性を発揮してください。今のところは、標準的な目標に固執します)。fを行ごとに解析し、すべてをソケットを通じて送信します。fを閉じて完了です!
int execute (int s, char *cmd) {
FILE *f = //popenを使用してコマンドをローカルで実行します
if (!f) return -1;
while (!feof (f)) {
//fを行ごとに解析し、出力をマスターに送り返します
}
fclose(f);
return 0;
}
新しいボットネットを次のターミナルコマンドでコンパイルします:
gcc -lcurl lib/connect.c lib/utils.c bot.c -o bin/slave
1つのターミナルウィンドウでbin/masterを実行し、他のウィンドウでbin/slaveを実行します。コマンドを(スレーブのユーザー名):(リモートターミナルコマンド)として入力します。おめでとうございます! ボットネットを構築しました!