はじめに
こんにちは!@flat-fieldです!
今回は,CTFでよく使用される「スタックバッファオーバフロー」を用いた攻撃手法についてご説明します.
読者対象
- CTFこれからやってみたいと思っている人
- CTF始めたけどよく分からないって人
- C言語の基礎知識がわかっている人
- 面白そうだから読んでみたいという人
スタックバッファオーバフローとは
まずスタックについて説明します.スタックとはFILO(First in Last out, つまり先入れ後出し)方式のデータ構造です.
C言語の局所関数を記憶しておくためのメモリ領域と考えてもらえればOKです.
例えば以下のプログラムを見てみましょう.
# include <stdio.h>
# include <stdlib.h>
# include <string.h>
int check_authentication(char *password) {
int auth_flag = 0;
char password_buffer[16];
char correct_password[256];
FILE *fp;
fp = fopen("password.txt", "r");
fgets(correct_password, 256, fp);
correct_password[strlen(correct_password)-1] = 0;
strcpy(password_buffer, password);
if(strcmp(password_buffer, correct_password) == 0)
auth_flag = 1;
return auth_flag;
}
int main(int argc, char *argv[]) {
if(argc < 2) {
printf("使用方法: %s <パスワード>\n", argv[0]);
exit(0);
}
if(check_authentication(argv[1])) {
printf("\n-=-=-=-=-=-=-=-=-=-=-=-=-=-\n");
printf(" アクセスを許可します。\n");
printf("-=-=-=-=-=-=-=-=-=-=-=-=-=-\n");
} else {
printf("\nアクセスを拒否しました。\n");
}
}
パスワードを入力して,合っていればアクセスを許可して,そうでなければアクセスを拒否するプログラムになっています.
パスワードはpassword.txtに入っていて,今は閲覧権限がないものとします.
コンパイルして試しに実行してみましょう.
root@kali:~/Desktop/Hacking/mycode# gcc -g -o my_auth_overflow my_auth_overflow.c
root@kali:~/Desktop/Hacking/mycode# ./my_auth_overflow test
アクセスを拒否しました。
拒否されてしまいました.まあそうですよね.
もういっちょパスワードを入力してみましょう.
root@kali:~/Desktop/Hacking/mycode# ./my_auth_overflow this_is_password
アクセスを拒否しました。
うむ,しっかり拒否されました.
じゃあこれならどうだ?
root@kali:~/Desktop/Hacking/mycode# ./my_auth_overflow AAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-=-=-=-=-=-=-=-=-=-=-=-=-=-
アクセスを許可します。
-=-=-=-=-=-=-=-=-=-=-=-=-=-
??????なぜかアクセスが許可されました(やった!).
ちなみに,password.txtの中身は「AAAAAAAAAAAAAAAAAAAAAAAAAAAAA」ではないのです.
パスワードは合っていないのにアクセスが許可されてしまうのです!!
不思議ですよね?
この記事では,この謎を理解していただければ嬉しいなと思っております.
プログラムの動作を詳しくみてみる.
先ほどなぜアクセスが許可されてしまったのか,gdbデバッグしながら解明していきましょう.
gdbでlistと入力すると,行番号とともにソースコードを閲覧することができます(そのためには,コンパイル時に -g オプションの指定が必要です).
root@kali:~/Desktop/Hacking/mycode# gdb -q ./my_auth_overflow
Reading symbols from ./my_auth_overflow...done.
gdb-peda$ list 1
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <string.h>
4
5 int check_authentication(char *password) {
6 int auth_flag = 0;
7 char password_buffer[16];
8 char correct_password[256];
9 FILE *fp;
10
gdb-peda$
11 fp = fopen("password.txt", "r");
12 fgets(correct_password, 256, fp);
13 correct_password[strlen(correct_password)-1] = 0;
14
15 strcpy(password_buffer, password);
16
17 if(strcmp(password_buffer, correct_password) == 0)
18 auth_flag = 1;
19
20 return auth_flag;
gdb-peda$
21 }
22
23 int main(int argc, char *argv[]) {
24 if(argc < 2) {
25 printf("使用方法: %s <パスワード>\n", argv[0]);
26 exit(0);
27 }
28 if(check_authentication(argv[1])) {
29 printf("\n-=-=-=-=-=-=-=-=-=-=-=-=-=-\n");
30 printf(" アクセスを許可します。\n");
gdb-peda$
31 printf("-=-=-=-=-=-=-=-=-=-=-=-=-=-\n");
32 } else {
33 printf("\nアクセスを拒否しました。\n");
34 }
35 }
gdb-peda$
まず15行目, 20行目にブレークポイントを設定し,プログラムを実行させてみましょう.
gdb-peda$ break 15
Breakpoint 1 at 0x1208: file my_auth_overflow.c, line 15.
gdb-peda$ break 20
Breakpoint 2 at 0x123f: file my_auth_overflow.c, line 20.
gdb-peda$ run AAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Starting program: /root/Desktop/Hacking/mycode/my_auth_overflow AAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Breakpoint 1, check_authentication (password=0x7fffffffe4e6 'A' <repeats 15 times>...) at my_auth_overflow.c:15
15 strcpy(password_buffer, password);
15行目手前まで到達しました.
続いて,各変数の中身をみてみます.
gdb-peda$ p password
$3 = 0x7fffffffe4e6 'A' <repeats 15 times>...
gdb-peda$ p password_buffer
$4 = "\000\000\000\000\000\000\000\000\035SUUUU\000"
gdb-peda$ p auth_flag
$5 = 0x0
passwordには,入力したAの羅列が表示されており,password_bufferはまだ設定されていません.
またauth_flagは0に初期化されたままです.
プログラムを進めて,もう一度変数の中身をみてみます.
gdb-peda$ c
Continuing.
Breakpoint 2, check_authentication (password=0x7fffffffe4e6 'A' <repeats 15 times>...) at my_auth_overflow.c:20
20 return auth_flag;
gdb-peda$ p password
$6 = 0x7fffffffe4e6 'A' <repeats 15 times>...
gdb-peda$ p password_buffer
$7 = 'A' <repeats 16 times>
gdb-peda$ p auth_flag
$8 = 0x41
20行目手前まできました.
password_bufferには,Aの羅列がコピーされています.
しかし,auth_flagが0x41に設定されています.プログラムでは(パスワードに合致していないので)auth_flagに関する操作はしていないはずなのに...
これでauth_flagに値が設定されてしまったせいで,28行目の条件分岐でtrueになってしまい,アクセスが許可されてしまうのです.
なぜauth_flagに値が設定されてしまったのか...
この理由は「スタックバッファオーバフロー」が発生しているからなのです!!!!
これについて詳しく説明します.
なぜauth_flagに値が設定されたのか
gdbデバッガでスタックの様子を見ることが出来るので,見てみましょう.
もう一度ブレークポイントを15, 20行目に設定して,プログラムを走らせてみましょう.
root@kali:~/Desktop/Hacking/mycode# gdb -q ./my_auth_overflow
Reading symbols from ./my_auth_overflow...done.
gdb-peda$ break 15
Breakpoint 1 at 0x1208: file my_auth_overflow.c, line 15.
gdb-peda$ break 20
Breakpoint 2 at 0x123f: file my_auth_overflow.c, line 20.
gdb-peda$ run AAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Starting program: /root/Desktop/Hacking/mycode/my_auth_overflow AAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Breakpoint 1, check_authentication (password=0x7fffffffe4e6 'A' <repeats 15 times>...) at my_auth_overflow.c:15
15 strcpy(password_buffer, password);
gdb-peda$
15行目手前まできました.
ここで,bt full
コマンドを使用することで,スタックの様子を見ることができます.
gdb-peda$ bt full
# 0 check_authentication (password=0x7fffffffe4e6 'A' <repeats 15 times>...) at my_auth_overflow.c:15
auth_flag = 0x0
password_buffer = "\000\000\000\000\000\000\000\000\035SUUUU\000"
correct_password = "hiratatomonori\000\000\000\000\000\000\000\000\000\000"...
fp = 0x555555559260
# 1 0x0000555555555291 in main (argc=0x2, argv=0x7fffffffe1a8) at my_auth_overflow.c:28
No locals.
# 2 0x00007ffff7e12b17 in __libc_start_main (main=0x555555555244 <main>, argc=0x2, argv=0x7fffffffe1a8, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffe198) at ../csu/libc-start.c:310
self = <optimized out>
__self = <optimized out>
result = <optimized out>
unwind_buf = {
cancel_jmp_buf = {{
jmp_buf = {0x0, 0x330e4ee4207765cb, 0x5555555550c0, 0x7fffffffe1a0, 0x0, 0x0, 0x665b1bb1447765cb, 0x665b0b8cd04965cb},
mask_was_saved = 0x0
}},
priv = {
pad = {0x0, 0x0, 0x7fffffffe1c0, 0x7ffff7ffe170},
data = {
prev = 0x0,
cleanup = 0x0,
canceltype = 0xffffe1c0
}
}
}
not_first_call = <optimized out>
# 3 0x00005555555550ea in _start ()
No symbol table info available.
上記を見ると,スタックには各局所変数が積まれていることがわかります.
各変数のアドレスを確認してみましょう.
gdb-peda$ p &auth_flag
$1 = (int *) 0x7fffffffe09c
gdb-peda$ p &password_buffer
$3 = (char (*)[16]) 0x7fffffffe080
gdb-peda$ p &correct_password
$4 = (char (*)[256]) 0x7fffffffdf80
図で表すとこんな感じですかね.

ここでポイントなのは,変数の宣言順に,スタックの高位アドレスから低位アドレスにデータがpushされているという部分です.
図で確認すると,ちゃんと宣言順でスタックに変数が積まれているのがわかりますね.
ではプログラムの続きを実行させて,スタック状態を調べます.
gdb-peda$ c
Continuing.
Display various information of current execution context
Usage:
context [reg,code,stack,all] [code/stack length]
Breakpoint 2, check_authentication (password=0x7fffffffe4e6 'A' <repeats 15 times>...) at my_auth_overflow.c:20
20 return auth_flag;
gdb-peda$ bt full
# 0 check_authentication (password=0x7fffffffe4e6 'A' <repeats 15 times>...) at my_auth_overflow.c:20
auth_flag = 0x41
password_buffer = 'A' <repeats 16 times>
correct_password = "hiratatomonori\000\000\000\000\000\000\000\000\000\000"...
fp = 0x4141414141414141
# 1 0x0000555555555291 in main (argc=0x2, argv=0x7fffffffe1a8) at my_auth_overflow.c:28
No locals.
# 2 0x00007ffff7e12b17 in __libc_start_main (main=0x555555555244 <main>, argc=0x2, argv=0x7fffffffe1a8, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffe198) at ../csu/libc-start.c:310
self = <optimized out>
__self = <optimized out>
result = <optimized out>
unwind_buf = {
cancel_jmp_buf = {{
jmp_buf = {0x0, 0x3ad790484fda182e, 0x5555555550c0, 0x7fffffffe1a0, 0x0, 0x0, 0x6f82c51d2bda182e, 0x6f82d520bfe4182e},
mask_was_saved = 0x0
}},
priv = {
pad = {0x0, 0x0, 0x7fffffffe1c0, 0x7ffff7ffe170},
data = {
prev = 0x0,
cleanup = 0x0,
canceltype = 0xffffe1c0
}
}
}
not_first_call = <optimized out>
# 3 0x00005555555550ea in _start ()
No symbol table info available.
スタックの状態を見ると,auth_flagが0x41に設定されていますね.
これは,プログラムの15行目strcpy(password_buffer, password);
が理由で起きているバグなんです.
先ほど確認した通り,password_bufferとauth_flagのアドレス差は,「0x7fffffffe09c - 0x7fffffffe080 = 0x1c」です.つまり,10進数で考えると「28バイト」分の差があるということです.
したがって,「28バイトより大きなバイトのデータを書き込むことによって,auth_flagのメモリ部分を書き換えてしまう」ので,auth_flagに値が設定されてしまうのです!!!
ちなみに今回は,「AAAAAAAAAAAAAAAAAAAAAAAAAAAAA」,つまりAを29個分入力しています.char型1文字は1バイトなので,29バイトのデータをpassword_bufferに書き込んでしまっています.したがって,最後のiつのAがauth_flagに書き込まれてしまっているわけです!!!!!!
そして,Aはasciiの16進数で「0x41」と表されるので,今回はauth_flagに0x41が設定されてしまったというわけです!!!!
以上のように,大きなデータを入力することで,本来格納されるべきでないスタック部分にデータを格納させる攻撃のことを「スタックバッファオーバフロー攻撃」と言います.
最後に
いかがでしたか?
何か分からないこと&ご指摘等があればコメントをお願いいたします!!
参考文献