LoginSignup
4
1

More than 3 years have passed since last update.

CTFerへの道(1)「スタックバッファオーバフロー攻撃」

Last updated at Posted at 2019-12-13

はじめに

こんにちは!@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に入っていて,今は閲覧権限がないものとします.

コンパイルして試しに実行してみましょう.

bash
root@kali:~/Desktop/Hacking/mycode# gcc -g -o my_auth_overflow my_auth_overflow.c
root@kali:~/Desktop/Hacking/mycode# ./my_auth_overflow test

アクセスを拒否しました。

拒否されてしまいました.まあそうですよね.

もういっちょパスワードを入力してみましょう.

bash
root@kali:~/Desktop/Hacking/mycode# ./my_auth_overflow this_is_password

アクセスを拒否しました。

うむ,しっかり拒否されました.

じゃあこれならどうだ?

bash
root@kali:~/Desktop/Hacking/mycode# ./my_auth_overflow AAAAAAAAAAAAAAAAAAAAAAAAAAAAA

-=-=-=-=-=-=-=-=-=-=-=-=-=-
   アクセスを許可します。
-=-=-=-=-=-=-=-=-=-=-=-=-=-

??????なぜかアクセスが許可されました(やった!).

ちなみに,password.txtの中身は「AAAAAAAAAAAAAAAAAAAAAAAAAAAAA」ではないのです.
パスワードは合っていないのにアクセスが許可されてしまうのです!!

不思議ですよね?

この記事では,この謎を理解していただければ嬉しいなと思っております.

プログラムの動作を詳しくみてみる.

先ほどなぜアクセスが許可されてしまったのか,gdbデバッグしながら解明していきましょう.
gdbでlistと入力すると,行番号とともにソースコードを閲覧することができます(そのためには,コンパイル時に -g オプションの指定が必要です).

bash
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行目にブレークポイントを設定し,プログラムを実行させてみましょう.

bash
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行目手前まで到達しました.
続いて,各変数の中身をみてみます.

bash
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に初期化されたままです.

プログラムを進めて,もう一度変数の中身をみてみます.

bash
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行目に設定して,プログラムを走らせてみましょう.

bash
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 コマンドを使用することで,スタックの様子を見ることができます.

bash
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

図で表すとこんな感じですかね.

IMG_1014A0044DAA-1.jpeg

ここでポイントなのは,変数の宣言順に,スタックの高位アドレスから低位アドレスにデータがpushされているという部分です.
図で確認すると,ちゃんと宣言順でスタックに変数が積まれているのがわかりますね.

ではプログラムの続きを実行させて,スタック状態を調べます.

bash
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が設定されてしまったというわけです!!!!

以上のように,大きなデータを入力することで,本来格納されるべきでないスタック部分にデータを格納させる攻撃のことを「スタックバッファオーバフロー攻撃」と言います.

最後に

いかがでしたか?

何か分からないこと&ご指摘等があればコメントをお願いいたします!!

参考文献

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1