情報セキュリティスペシャリスト試験を受けた話
情報セキュリティスペシャリスト試験(以下「SC」)が平成28年秋期で最後とのことだったので、
重い腰を上げて受験しました。
これまでのSC申し込みの部失敗の歴史
あ、情報処理の申し込み期限過ぎちゃったw
— しゃろ (@Charo_IT) February 27, 2015
IPA申込者試験3回連続通過できず(*´ω`*)
— しゃろ (@Charo_IT) August 21, 2015
未だに情報処理技術者試験 申し込みの部を突破できない
— しゃろ (@Charo_IT) April 15, 2016
午前は無事通過し、午後1・午後2は何とかなるでしょw(適当)
という感じです。
午後1が始まる前に、冗談半分で
というツイートをしたのですが、午後1 pwn出題してどうぞ
— しゃろ (@Charo_IT) October 16, 2016
本当に出題されるとは思わなかった……。heap bof問が出て優勝した https://t.co/SNiXLEjrA8
— しゃろ (@Charo_IT) October 16, 2016
しかし、この問題で少し気になる点があったので、今回はその点について考えてみたいと思います。
なお、今回の試験の問題冊子と公式の解答は既にIPAのサイトで公開されています。
問題文
さて、まず問題文を見てみましょう。
午後1の問2から引用します。
問2 ソフトウェア開発における脆弱性対策に関する次の記述を読んで、設問1~4に答えよ。
(長いので中略)
図2のプログラムYは、起動時に、第1引数は利用者IDを、第2引数はパスワードを受け取る。このプログラム用にあらかじめ登録された”利用者IDとパスワード”の組と引数で与えられた組を比較し、利用者認証する。”利用者IDとパスワード”は、いずれも半角英数字、最小6文字最大8文字の文字列と仕様で定められている。
#include <iostream> #include <cstring> (省略) #define UID_SIZE 8 #define PASS_SIZE 8 (省略) using namespace std; void getPass(char *pass, char *uid) { (省略、uidで指定された利用者IDを基に登録済パスワードを取得しpassに格納、利用者IDが存在しない場合は長さ0の文字列をpassに格納) } (省略) int main(int argc, char **argv) { static char *uid; static char *pass; (省略、引数の個数をチェック) uid = new char[UID_SIZE+1]; pass = new char[PASS_SIZE+1]; getPass(pass, argv[1]); strcpy(uid, argv[1]); if(strlen(pass) == 0 || strcmp(argv[2], pass) != 0){ cout << "認証失敗" << endl; (省略、uidを出力、認証失敗時の処理) }else{ cout << "認証成功" << endl; (省略、uidを出力) } }
プログラムYにはヒープベースBOF脆弱性があり、②引数によっては、利用者認証を回避される可能性があると、L氏はK氏に指摘された。ただし、K氏によると、③このプログラムYは引数が同じでも、実行環境によっては利用者認証を回避されないとのことだった。L氏は、V社の開発した他のプログラムについても確認することとした。
(ここも中略)
設問3 図2のプログラムについて、(1)~(3)に答えよ。
(1) 本文中の下線②はどのような引数で利用者認証を回避されるか。引数の組を解答群の中から選び、記号で答えよ。
解答群
| 記号 | 第1引数 | 第2引数 |
|---|---|---|
| ア | 001(繰返し)0111111111 | 11111110 |
| イ | 001(繰返し)1111111111 | 11111110 |
| ウ | 011(繰返し)1111111101 | 11111101 |
| エ | 111(繰返し)1111111111 | 67891231 |
| オ | 123(繰返し)1231231231 | 67891231 |
(2) 利用者認証を回避される原因となるヒープベースBOF脆弱性の存在箇所を、実際にバッファがオーバフローするコードの行番号で答えよ。
(3) (2)で示した行番号の行を差し替えて行う改修案として適切なものを解答群の中から全て選び、記号で答えよ。
解答群
ア memcpy(uid, argv[1], strlen(argv[1])+1);
イ memcpy(uid, argv[1], UID_SIZE+1);
ウ pass = new char[PASS_SIZE+8];
エ strncpy(uid, argv[1], strlen(argv[1])+1);
オ strncpy(uid, argv[1], UID_SIZE+1);
問題文は以上のようになっています。
考察
設問3 (1), (2)
やるだけ問ですが、せっかくなので実際に動かしてみましょう。
問題文では実行環境について明示されていませんでしたが、今回はx86_64なUbuntu 14.04(libcはUbuntu EGLIBC 2.19-0ubuntu6.9)でやっていきます。
$ uname -a Linux test 3.16.0-77-generic #99~14.04.1-Ubuntu SMP Tue Jun 28 19:17:10 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux
まず、ソースコード中で省略されている部分をそれっぽく直します。
// g++ -o sc sc.cpp #include <iostream> #include <cstring> #define UID_SIZE 8 #define PASS_SIZE 8 using namespace std; void getPass(char *pass, char *uid){ strcpy(pass, "deadbeef"); // 面倒なのでパスワードは"deadbeef"で固定(当然攻撃者はパスワードを知らないものとする) } int main(int argc, char **argv){ static char *uid; static char *pass; if(argc != 3){ return 1; } uid = new char[UID_SIZE + 1]; pass = new char[PASS_SIZE + 1]; getPass(pass, argv[1]); strcpy(uid, argv[1]); if(strlen(pass) == 0 || strcmp(argv[2], pass) != 0){ cout << "認証失敗" << endl; cout << "uid=" << uid << endl; // uidを出力するとのことなので、その通りにする return 1; }else{ cout << "認証成功" << endl; cout << "uid=" << uid << endl; return 0; } }
引数に"AAAA"と"BBBB"を与えた状態でstrcpyの直後まで実行してみて、ヒープの中を見てみます。
gdb-peda$ tb *0x4009cc # strcpyの直後
Temporary breakpoint 1 at 0x4009cc
gdb-peda$ r AAAA BBBB
Starting program: /tmp/sc/sc AAAA BBBB
[----------------------------------registers-----------------------------------]
RAX: 0x602010 --> 0x41414141 ('AAAA')
RBX: 0x0
RCX: 0x41414141 ('AAAA')
RDX: 0x4
RSI: 0x7fffffffe388 --> 0x4242420041414141 ('AAAA')
RDI: 0x602010 --> 0x41414141 ('AAAA')
RBP: 0x7fffffffdec0 --> 0x0
RSP: 0x7fffffffdea0 --> 0x7fffffffdfa8 --> 0x7fffffffe37d ("/tmp/sc/sc")
RIP: 0x4009cc (<main+124>: mov rax,QWORD PTR [rip+0x2007d5] # 0x6011a8 <main::pass>)
R8 : 0x0
R9 : 0x2
R10: 0x7fffffffdc60 --> 0x0
R11: 0x7ffff7889360 --> 0xfff24a90fff24a80
R12: 0x400840 (<_start>: xor ebp,ebp)
R13: 0x7fffffffdfa0 --> 0x3
R14: 0x0
R15: 0x0
EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x4009c1 <main+113>: mov rsi,rdx
0x4009c4 <main+116>: mov rdi,rax
0x4009c7 <main+119>: call 0x400810 <strcpy@plt>
=> 0x4009cc <main+124>: mov rax,QWORD PTR [rip+0x2007d5] # 0x6011a8 <main::pass>
0x4009d3 <main+131>: movzx eax,BYTE PTR [rax]
0x4009d6 <main+134>: test al,al
0x4009d8 <main+136>: je 0x4009fb <main+171>
0x4009da <main+138>: mov rdx,QWORD PTR [rip+0x2007c7] # 0x6011a8 <main::pass>
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffdea0 --> 0x7fffffffdfa8 --> 0x7fffffffe37d ("/tmp/sc/sc")
0008| 0x7fffffffdea8 --> 0x300400840
0016| 0x7fffffffdeb0 --> 0x7fffffffdfa0 --> 0x3
0024| 0x7fffffffdeb8 --> 0x0
0032| 0x7fffffffdec0 --> 0x0
0040| 0x7fffffffdec8 --> 0x7ffff7732f45 (<__libc_start_main+245>: mov edi,eax)
0048| 0x7fffffffded0 --> 0x0
0056| 0x7fffffffded8 --> 0x7fffffffdfa8 --> 0x7fffffffe37d ("/tmp/sc/sc")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Temporary breakpoint 1, 0x00000000004009cc in main ()
gdb-peda$ vmmap # メモリマップの表示
Start End Perm Name
0x00400000 0x00401000 r-xp /tmp/sc/sc
0x00600000 0x00601000 r--p /tmp/sc/sc
0x00601000 0x00602000 rw-p /tmp/sc/sc
0x00602000 0x00623000 rw-p [heap]
0x00007ffff71f5000 0x00007ffff720b000 r-xp /lib/x86_64-linux-gnu/libgcc_s.so.1
0x00007ffff720b000 0x00007ffff740a000 ---p /lib/x86_64-linux-gnu/libgcc_s.so.1
0x00007ffff740a000 0x00007ffff740b000 rw-p /lib/x86_64-linux-gnu/libgcc_s.so.1
0x00007ffff740b000 0x00007ffff7510000 r-xp /lib/x86_64-linux-gnu/libm-2.19.so
0x00007ffff7510000 0x00007ffff770f000 ---p /lib/x86_64-linux-gnu/libm-2.19.so
0x00007ffff770f000 0x00007ffff7710000 r--p /lib/x86_64-linux-gnu/libm-2.19.so
0x00007ffff7710000 0x00007ffff7711000 rw-p /lib/x86_64-linux-gnu/libm-2.19.so
0x00007ffff7711000 0x00007ffff78cb000 r-xp /lib/x86_64-linux-gnu/libc-2.19.so
0x00007ffff78cb000 0x00007ffff7acb000 ---p /lib/x86_64-linux-gnu/libc-2.19.so
0x00007ffff7acb000 0x00007ffff7acf000 r--p /lib/x86_64-linux-gnu/libc-2.19.so
0x00007ffff7acf000 0x00007ffff7ad1000 rw-p /lib/x86_64-linux-gnu/libc-2.19.so
0x00007ffff7ad1000 0x00007ffff7ad6000 rw-p mapped
0x00007ffff7ad6000 0x00007ffff7bbc000 r-xp /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.19
0x00007ffff7bbc000 0x00007ffff7dbb000 ---p /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.19
0x00007ffff7dbb000 0x00007ffff7dc3000 r--p /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.19
0x00007ffff7dc3000 0x00007ffff7dc5000 rw-p /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.19
0x00007ffff7dc5000 0x00007ffff7dda000 rw-p mapped
0x00007ffff7dda000 0x00007ffff7dfd000 r-xp /lib/x86_64-linux-gnu/ld-2.19.so
0x00007ffff7fde000 0x00007ffff7fe3000 rw-p mapped
0x00007ffff7ff6000 0x00007ffff7ff8000 rw-p mapped
0x00007ffff7ff8000 0x00007ffff7ffa000 r-xp [vdso]
0x00007ffff7ffa000 0x00007ffff7ffc000 r--p [vvar]
0x00007ffff7ffc000 0x00007ffff7ffd000 r--p /lib/x86_64-linux-gnu/ld-2.19.so
0x00007ffff7ffd000 0x00007ffff7ffe000 rw-p /lib/x86_64-linux-gnu/ld-2.19.so
0x00007ffff7ffe000 0x00007ffff7fff000 rw-p mapped
0x00007ffffffde000 0x00007ffffffff000 rw-p [stack]
0xffffffffff600000 0xffffffffff601000 r-xp [vsyscall]
gdb-peda$ x/10gx 0x602000 # ヒープ領域のダンプ
0x602000: 0x0000000000000000 0x0000000000000021
0x602010: 0x0000000041414141 0x0000000000000000 # uid: 0x602010~
0x602020: 0x0000000000000000 0x0000000000000021
0x602030: 0x6665656264616564 0x0000000000000000 # pass: 0x602030~
0x602040: 0x0000000000000000 0x0000000000020fc1
ヒープ上にuidの"AAAA"とpassの"deadbeef"という文字列がありますね。
ここにあるpassとargv[2]が文字列として等しければ認証成功、異なれば認証失敗という扱いになります。
では、uidとして長い文字列を指定することでheap bofを起こしてみます。
gdb-peda$ tb *0x4009cc
Temporary breakpoint 2 at 0x4009cc
gdb-peda$ r AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA BBBB
Starting program: /tmp/sc/sc AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA BBBB
[----------------------------------registers-----------------------------------]
RAX: 0x602010 ('A' <repeats 40 times>)
RBX: 0x0
RCX: 0x4141414141414141 ('AAAAAAAA')
RDX: 0x41414141414141 ('AAAAAAA')
RSI: 0x7fffffffe380 ('A' <repeats 12 times>)
RDI: 0x60202c ('A' <repeats 12 times>)
RBP: 0x7fffffffdea0 --> 0x0
RSP: 0x7fffffffde80 --> 0x7fffffffdf88 --> 0x7fffffffe359 ("/tmp/sc/sc")
RIP: 0x4009cc (<main+124>: mov rax,QWORD PTR [rip+0x2007d5] # 0x6011a8 <main::pass>)
R8 : 0x0
R9 : 0x2
R10: 0x7fffffffdc40 --> 0x0
R11: 0x7ffff7889360 --> 0xfff24a90fff24a80
R12: 0x400840 (<_start>: xor ebp,ebp)
R13: 0x7fffffffdf80 --> 0x3
R14: 0x0
R15: 0x0
EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x4009c1 <main+113>: mov rsi,rdx
0x4009c4 <main+116>: mov rdi,rax
0x4009c7 <main+119>: call 0x400810 <strcpy@plt>
=> 0x4009cc <main+124>: mov rax,QWORD PTR [rip+0x2007d5] # 0x6011a8 <main::pass>
0x4009d3 <main+131>: movzx eax,BYTE PTR [rax]
0x4009d6 <main+134>: test al,al
0x4009d8 <main+136>: je 0x4009fb <main+171>
0x4009da <main+138>: mov rdx,QWORD PTR [rip+0x2007c7] # 0x6011a8 <main::pass>
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffde80 --> 0x7fffffffdf88 --> 0x7fffffffe359 ("/tmp/sc/sc")
0008| 0x7fffffffde88 --> 0x300400840
0016| 0x7fffffffde90 --> 0x7fffffffdf80 --> 0x3
0024| 0x7fffffffde98 --> 0x0
0032| 0x7fffffffdea0 --> 0x0
0040| 0x7fffffffdea8 --> 0x7ffff7732f45 (<__libc_start_main+245>: mov edi,eax)
0048| 0x7fffffffdeb0 --> 0x0
0056| 0x7fffffffdeb8 --> 0x7fffffffdf88 --> 0x7fffffffe359 ("/tmp/sc/sc")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Temporary breakpoint 2, 0x00000000004009cc in main ()
gdb-peda$ x/10gx 0x602000
0x602000: 0x0000000000000000 0x0000000000000021
0x602010: 0x4141414141414141 0x4141414141414141
0x602020: 0x4141414141414141 0x4141414141414141
0x602030: 0x4141414141414141 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000020fc1
ヒープを見てみると、uidが長すぎるせいで、もともと"deadbeef"という文字列があった場所(0x602030)が"AAAAAAAA"に書き換わってしまっていることがわかります。
passの検証時にはstrcpyで書き換わった後のデータとargv[2]が比較されるため、今回の環境では
という条件が成り立てば、認証を回避できることになります。
$ ./sc $(ruby -e 'print "A"*32+"B"*8') BBBBBBBB 認証成功 uid=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBB
ということで、(1)は第1引数の後ろの方が第2引数と一致しているウ(011(繰返し)1111111101 : 11111101)が正解となります。
((2)はstrcpyの行が正解)
$ ./sc 0111111111111111111111111111111111111101 11111101 認証成功 uid=0111111111111111111111111111111111111101
疑惑の設問3 (3)
ここまで長くなってしまいましたが、ここが本記事のメイン部分です。
問題文を再掲します。
(3) (2)で示した行番号の行(筆者注:
strcpyの行)を差し替えて行う改修案として適切なものを解答群の中から全て選び、記号で答えよ。解答群
ア
memcpy(uid, argv[1], strlen(argv[1])+1);
イmemcpy(uid, argv[1], UID_SIZE+1);
ウpass = new char[PASS_SIZE+8];
エstrncpy(uid, argv[1], strlen(argv[1])+1);
オstrncpy(uid, argv[1], UID_SIZE+1);
プログラムYにあるheap bofを直そう、という問題です。
選択肢を一通り眺めてみると、ウは論外、アとエは明らかにheap bofが直っていないため不適切ということで、ひとまずイとオが残ります。
イとオの各改修案に対して認証回避を試みたときのヒープの状態を見てみましょう。
- イ(
memcpy(uid, argv[1], UID_SIZE+1);)の場合
gdb-peda$ tb *0x4009f1 # memcpyの直後
Temporary breakpoint 1 at 0x4009f1
gdb-peda$ r AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA BBBB
Starting program: /tmp/sc/sc AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA BBBB
[----------------------------------registers-----------------------------------]
RAX: 0x602010 ("AAAAAAAAA")
RBX: 0x0
RCX: 0x12
RDX: 0x9 ('\t')
RSI: 0x7fffffffe364 ('A' <repeats 40 times>)
RDI: 0x602010 ("AAAAAAAAA")
RBP: 0x7fffffffdea0 --> 0x0
RSP: 0x7fffffffde80 --> 0x7fffffffdf88 --> 0x7fffffffe359 ("/tmp/sc/sc")
RIP: 0x4009f1 (<main+129>: mov rax,QWORD PTR [rip+0x2007b0] # 0x6011a8 <main::pass>)
R8 : 0x0
R9 : 0x2
R10: 0x7fffffffdc40 --> 0x0
R11: 0x7ffff77a8ec0 (<__memcpy_sse2_unaligned>: mov rax,rsi)
R12: 0x400860 (<_start>: xor ebp,ebp)
R13: 0x7fffffffdf80 --> 0x3
R14: 0x0
R15: 0x0
EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x4009e6 <main+118>: mov rsi,rcx
0x4009e9 <main+121>: mov rdi,rax
0x4009ec <main+124>: call 0x400850 <memcpy@plt>
=> 0x4009f1 <main+129>: mov rax,QWORD PTR [rip+0x2007b0] # 0x6011a8 <main::pass>
0x4009f8 <main+136>: movzx eax,BYTE PTR [rax]
0x4009fb <main+139>: test al,al
0x4009fd <main+141>: je 0x400a20 <main+176>
0x4009ff <main+143>: mov rdx,QWORD PTR [rip+0x2007a2] # 0x6011a8 <main::pass>
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffde80 --> 0x7fffffffdf88 --> 0x7fffffffe359 ("/tmp/sc/sc")
0008| 0x7fffffffde88 --> 0x300400860
0016| 0x7fffffffde90 --> 0x7fffffffdf80 --> 0x3
0024| 0x7fffffffde98 --> 0x0
0032| 0x7fffffffdea0 --> 0x0
0040| 0x7fffffffdea8 --> 0x7ffff7732f45 (<__libc_start_main+245>: mov edi,eax)
0048| 0x7fffffffdeb0 --> 0x0
0056| 0x7fffffffdeb8 --> 0x7fffffffdf88 --> 0x7fffffffe359 ("/tmp/sc/sc")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Temporary breakpoint 1, 0x00000000004009f1 in main ()
gdb-peda$ x/10gx 0x602000
0x602000: 0x0000000000000000 0x0000000000000021
0x602010: 0x4141414141414141 0x0000000000000041
0x602020: 0x0000000000000000 0x0000000000000021
0x602030: 0x6665656264616564 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000020fc1
- オ(
strncpy(uid, argv[1], UID_SIZE+1);)の場合
gdb-peda$ tb *0x4009d1 # strncpyの直後
Temporary breakpoint 1 at 0x4009d1
gdb-peda$ r AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA BBBB
Starting program: /tmp/sc/sc AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA BBBB
[----------------------------------registers-----------------------------------]
RAX: 0x602010 ("AAAAAAAAA")
RBX: 0x0
RCX: 0x4141414141414141 ('AAAAAAAA')
RDX: 0x41 ('A')
RSI: 0x7fffffffe364 ('A' <repeats 40 times>)
RDI: 0x602010 ("AAAAAAAAA")
RBP: 0x7fffffffdea0 --> 0x0
RSP: 0x7fffffffde80 --> 0x7fffffffdf88 --> 0x7fffffffe359 ("/tmp/sc/sc")
RIP: 0x4009d1 (<main+129>: mov rax,QWORD PTR [rip+0x2007d0] # 0x6011a8 <main::pass>)
R8 : 0x9 ('\t')
R9 : 0x2
R10: 0xd ('\r')
R11: 0x7ffff7889460 --> 0xfff25650fff25640
R12: 0x400840 (<_start>: xor ebp,ebp)
R13: 0x7fffffffdf80 --> 0x3
R14: 0x0
R15: 0x0
EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x4009c6 <main+118>: mov rsi,rcx
0x4009c9 <main+121>: mov rdi,rax
0x4009cc <main+124>: call 0x400820 <strncpy@plt>
=> 0x4009d1 <main+129>: mov rax,QWORD PTR [rip+0x2007d0] # 0x6011a8 <main::pass>
0x4009d8 <main+136>: movzx eax,BYTE PTR [rax]
0x4009db <main+139>: test al,al
0x4009dd <main+141>: je 0x400a00 <main+176>
0x4009df <main+143>: mov rdx,QWORD PTR [rip+0x2007c2] # 0x6011a8 <main::pass>
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffde80 --> 0x7fffffffdf88 --> 0x7fffffffe359 ("/tmp/sc/sc")
0008| 0x7fffffffde88 --> 0x300400840
0016| 0x7fffffffde90 --> 0x7fffffffdf80 --> 0x3
0024| 0x7fffffffde98 --> 0x0
0032| 0x7fffffffdea0 --> 0x0
0040| 0x7fffffffdea8 --> 0x7ffff7732f45 (<__libc_start_main+245>: mov edi,eax)
0048| 0x7fffffffdeb0 --> 0x0
0056| 0x7fffffffdeb8 --> 0x7fffffffdf88 --> 0x7fffffffe359 ("/tmp/sc/sc")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Temporary breakpoint 1, 0x00000000004009d1 in main ()
gdb-peda$ x/10gx 0x602000
0x602000: 0x0000000000000000 0x0000000000000021
0x602010: 0x4141414141414141 0x0000000000000041
0x602020: 0x0000000000000000 0x0000000000000021
0x602030: 0x6665656264616564 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000020fc1
イの場合もオの場合もpassが上書きされていないため、改修案として適切に見えます。
しかし、どちらの改修案もargv[1]がUID_SIZE+1バイト以上だった場合にuidがNULL終端されないという欠点があります。(strncpyの仕様についてはこちらをご覧ください。)
uidのNULL終端がされない場合、どんな不都合があるのでしょうか?
ここで1つの実験をしてみましょう。
問題文では実行環境が明示されていないため、malloc(9)が(x86_64なglibcの実装と同様に)32バイトという余裕のあるサイズのチャンクを準備してくれるとは限りません。
ということで、「要求サイズぴったりの領域を確保して返す。領域と領域との間は詰める」というケチなmallocを使う実行環境を擬似的に作ってみます。
// g++ -o sc sc.cpp #include <iostream> #include <cstring> #include <sys/mman.h> #include <stdlib.h> #define UID_SIZE 8 #define PASS_SIZE 8 #define HEAP_SIZE 0x1000 using namespace std; extern void *(*__malloc_hook)(size_t); void *top = NULL; void *my_malloc(size_t size){ void *mem; // ヒープ用の領域を0x13370000に準備する if(top == NULL){ top = mmap((void*)0x13370000, HEAP_SIZE, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE | MAP_FIXED, -1, 0); if(top == (void*)-1){ exit(1); } } mem = top; top = (char*)top + size; // 使った分だけtopを後ろにずらす if(top > (void*)(0x13370000 + HEAP_SIZE)){ exit(1); } return mem; } void getPass(char *pass, char *uid){ strcpy(pass, "deadbeef"); } int main(int argc, char **argv){ static char *uid; static char *pass; __malloc_hook = my_malloc; // mallocが呼ばれると内部でmy_mallocが呼ばれるようにする if(argc != 3){ return 1; } uid = new char[UID_SIZE + 1]; // newによる領域確保も内部でmallocが呼ばれる pass = new char[PASS_SIZE + 1]; getPass(pass, argv[1]); strcpy(uid, argv[1]); if(strlen(pass) == 0 || strcmp(argv[2], pass) != 0){ cout << "認証失敗" << endl; cout << "uid=" << uid << endl; return 1; }else{ cout << "認証成功" << endl; cout << "uid=" << uid << endl; return 0; } }
(私がC++をまじめに書いたことがない人間なため、C++のお作法から外れている部分があるかもしれませんがご容赦ください。)
この環境において、引数に"AAAA"と"BBBB"を与えたときのヒープの様子を見てみます。
gdb-peda$ tb *0x400b41 # strcpyの直後
Temporary breakpoint 1 at 0x400b41
gdb-peda$ r AAAA BBBB
Starting program: /tmp/sc/sc AAAA BBBB
[----------------------------------registers-----------------------------------]
RAX: 0x13370000 --> 0x41414141 ('AAAA')
RBX: 0x0
RCX: 0x41414141 ('AAAA')
RDX: 0x4
RSI: 0x7fffffffe388 --> 0x4242420041414141 ('AAAA')
RDI: 0x13370000 --> 0x41414141 ('AAAA')
RBP: 0x7fffffffdec0 --> 0x0
RSP: 0x7fffffffdea0 --> 0x7fffffffdfa8 --> 0x7fffffffe37d ("/tmp/sc/sc")
RIP: 0x400b41 (<main+135>: mov rax,QWORD PTR [rip+0x2016a8] # 0x6021f0 <main::pass>)
R8 : 0xffffffff
R9 : 0x0
R10: 0x7fffffffdc60 --> 0x0
R11: 0x7ffff7889360 --> 0xfff24a90fff24a80
R12: 0x400910 (<_start>: xor ebp,ebp)
R13: 0x7fffffffdfa0 --> 0x3
R14: 0x0
R15: 0x0
EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x400b36 <main+124>: mov rsi,rdx
0x400b39 <main+127>: mov rdi,rax
0x400b3c <main+130>: call 0x4008d0 <strcpy@plt>
=> 0x400b41 <main+135>: mov rax,QWORD PTR [rip+0x2016a8] # 0x6021f0 <main::pass>
0x400b48 <main+142>: movzx eax,BYTE PTR [rax]
0x400b4b <main+145>: test al,al
0x400b4d <main+147>: je 0x400b70 <main+182>
0x400b4f <main+149>: mov rdx,QWORD PTR [rip+0x20169a] # 0x6021f0 <main::pass>
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffdea0 --> 0x7fffffffdfa8 --> 0x7fffffffe37d ("/tmp/sc/sc")
0008| 0x7fffffffdea8 --> 0x300400910
0016| 0x7fffffffdeb0 --> 0x7fffffffdfa0 --> 0x3
0024| 0x7fffffffdeb8 --> 0x0
0032| 0x7fffffffdec0 --> 0x0
0040| 0x7fffffffdec8 --> 0x7ffff7732f45 (<__libc_start_main+245>: mov edi,eax)
0048| 0x7fffffffded0 --> 0x0
0056| 0x7fffffffded8 --> 0x7fffffffdfa8 --> 0x7fffffffe37d ("/tmp/sc/sc")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Temporary breakpoint 1, 0x0000000000400b41 in main ()
gdb-peda$ x/32bx 0x13370000
0x13370000: 0x41 0x41 0x41 0x41 0x00 0x00 0x00 0x00
0x13370008: 0x00 0x64 0x65 0x61 0x64 0x62 0x65 0x65
0x13370010: 0x66 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x13370018: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
uid, passそれぞれに9バイトぴったりの領域が与えられていることが確認できました。
では、各改修案を適用した上で、攻撃を試みます。
- イ(
memcpy(uid, argv[1], UID_SIZE+1);)の場合
gdb-peda$ tb *0x400b66 # memcpyの直後
Temporary breakpoint 1 at 0x400b66
gdb-peda$ r AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA BBBB
Starting program: /tmp/sc/sc AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA BBBB
[----------------------------------registers-----------------------------------]
RAX: 0x13370000 ("AAAAAAAAAdeadbeef")
RBX: 0x0
RCX: 0x12
RDX: 0x9 ('\t')
RSI: 0x7fffffffe364 ('A' <repeats 40 times>)
RDI: 0x13370000 ("AAAAAAAAAdeadbeef")
RBP: 0x7fffffffdea0 --> 0x0
RSP: 0x7fffffffde80 --> 0x7fffffffdf88 --> 0x7fffffffe359 ("/tmp/sc/sc")
RIP: 0x400b66 (<main+140>: mov rax,QWORD PTR [rip+0x201683] # 0x6021f0 <main::pass>)
R8 : 0xffffffff
R9 : 0x0
R10: 0x7fffffffdc40 --> 0x0
R11: 0x7ffff77a8ec0 (<__memcpy_sse2_unaligned>: mov rax,rsi)
R12: 0x400930 (<_start>: xor ebp,ebp)
R13: 0x7fffffffdf80 --> 0x3
R14: 0x0
R15: 0x0
EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x400b5b <main+129>: mov rsi,rcx
0x400b5e <main+132>: mov rdi,rax
0x400b61 <main+135>: call 0x400910 <memcpy@plt>
=> 0x400b66 <main+140>: mov rax,QWORD PTR [rip+0x201683] # 0x6021f0 <main::pass>
0x400b6d <main+147>: movzx eax,BYTE PTR [rax]
0x400b70 <main+150>: test al,al
0x400b72 <main+152>: je 0x400b95 <main+187>
0x400b74 <main+154>: mov rdx,QWORD PTR [rip+0x201675] # 0x6021f0 <main::pass>
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffde80 --> 0x7fffffffdf88 --> 0x7fffffffe359 ("/tmp/sc/sc")
0008| 0x7fffffffde88 --> 0x300400930
0016| 0x7fffffffde90 --> 0x7fffffffdf80 --> 0x3
0024| 0x7fffffffde98 --> 0x0
0032| 0x7fffffffdea0 --> 0x0
0040| 0x7fffffffdea8 --> 0x7ffff7732f45 (<__libc_start_main+245>: mov edi,eax)
0048| 0x7fffffffdeb0 --> 0x0
0056| 0x7fffffffdeb8 --> 0x7fffffffdf88 --> 0x7fffffffe359 ("/tmp/sc/sc")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Temporary breakpoint 1, 0x0000000000400b66 in main ()
gdb-peda$ x/32bx 0x13370000
0x13370000: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0x13370008: 0x41 0x64 0x65 0x61 0x64 0x62 0x65 0x65
0x13370010: 0x66 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x13370018: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
- オ(
strncpy(uid, argv[1], UID_SIZE+1);)の場合
gdb-peda$ tb *0x400b46 # strncpyの直後
Temporary breakpoint 1 at 0x400b46
gdb-peda$ r AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA BBBB
Starting program: /tmp/sc/sc AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA BBBB
[----------------------------------registers-----------------------------------]
RAX: 0x13370000 ("AAAAAAAAAdeadbeef")
RBX: 0x0
RCX: 0x4141414141414141 ('AAAAAAAA')
RDX: 0x41 ('A')
RSI: 0x7fffffffe364 ('A' <repeats 40 times>)
RDI: 0x13370000 ("AAAAAAAAAdeadbeef")
RBP: 0x7fffffffdea0 --> 0x0
RSP: 0x7fffffffde80 --> 0x7fffffffdf88 --> 0x7fffffffe359 ("/tmp/sc/sc")
RIP: 0x400b46 (<main+140>: mov rax,QWORD PTR [rip+0x2016a3] # 0x6021f0 <main::pass>)
R8 : 0x9 ('\t')
R9 : 0x0
R10: 0xd ('\r')
R11: 0x7ffff7889460 --> 0xfff25650fff25640
R12: 0x400910 (<_start>: xor ebp,ebp)
R13: 0x7fffffffdf80 --> 0x3
R14: 0x0
R15: 0x0
EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x400b3b <main+129>: mov rsi,rcx
0x400b3e <main+132>: mov rdi,rax
0x400b41 <main+135>: call 0x4008e0 <strncpy@plt>
=> 0x400b46 <main+140>: mov rax,QWORD PTR [rip+0x2016a3] # 0x6021f0 <main::pass>
0x400b4d <main+147>: movzx eax,BYTE PTR [rax]
0x400b50 <main+150>: test al,al
0x400b52 <main+152>: je 0x400b75 <main+187>
0x400b54 <main+154>: mov rdx,QWORD PTR [rip+0x201695] # 0x6021f0 <main::pass>
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffde80 --> 0x7fffffffdf88 --> 0x7fffffffe359 ("/tmp/sc/sc")
0008| 0x7fffffffde88 --> 0x300400910
0016| 0x7fffffffde90 --> 0x7fffffffdf80 --> 0x3
0024| 0x7fffffffde98 --> 0x0
0032| 0x7fffffffdea0 --> 0x0
0040| 0x7fffffffdea8 --> 0x7ffff7732f45 (<__libc_start_main+245>: mov edi,eax)
0048| 0x7fffffffdeb0 --> 0x0
0056| 0x7fffffffdeb8 --> 0x7fffffffdf88 --> 0x7fffffffe359 ("/tmp/sc/sc")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Temporary breakpoint 1, 0x0000000000400b46 in main ()
gdb-peda$ x/32bx 0x13370000
0x13370000: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0x13370008: 0x41 0x64 0x65 0x61 0x64 0x62 0x65 0x65
0x13370010: 0x66 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x13370018: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
どちらもheap bofはしませんが、uidとpassが文字列として繋がっているため、uid出力時にパスワードが漏れてしまうという結果になります。
そもそもパスワードを平文または可逆変換して保管している時点でどうかしてる。
$ ./sc AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA BBBB # イ(memcpy)バージョン 認証失敗 uid=AAAAAAAAAdeadbeef $ ./sc AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA BBBB # オ(strncpy)バージョン 認証失敗 uid=AAAAAAAAAdeadbeef
従って、個人的にはこの問題の正答は「該当なし」だと思うのですが、「きっと出題者はstrncpyの仕様を間違えていたに違いない」という余計な推察をしてstrncpyのみ選びました。(今思えば「該当なし」と書けばよかった……)
ちなみに、今日公表された公式解答によると、出題者の考える正答は「イ・オ」だそうです。
ここまで考えてきたようにmallocの実装によってはパスワードが漏れますが大丈夫なんでしょうか。
正しい改修案
さて、この脆弱性を本当に修正するにはどうすればいいのでしょうか?
事前にmemset(uid, 0, UID_SIZE + 1);してからmemcpy(uid, argv[1], UID_SIZE);またはstrncpy(uid, argv[1], UID_SIZE);とするのもありですが、
1回の関数呼び出しで済ますという条件であればsnprintf(uid, UID_SIZE + 1, "%s", argv[1]);がよいかと考えました。
gdb-peda$ tb *0x400b50 # snprintfの直後
Temporary breakpoint 1 at 0x400b50
gdb-peda$ r AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA BBBB
Starting program: /tmp/sc/sc AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA BBBB
[----------------------------------registers-----------------------------------]
RAX: 0x28 ('(')
RBX: 0x0
RCX: 0x28 ('(')
RDX: 0x400d06 --> 0xe5bca8e88daae800
RSI: 0x7fffffd7
RDI: 0x7fffffffdc40 --> 0xfbad8001
RBP: 0x7fffffffdea0 --> 0x0
RSP: 0x7fffffffde80 --> 0x7fffffffdf88 --> 0x7fffffffe359 ("/tmp/sc/sc")
RIP: 0x400b50 (<main+150>: mov rax,QWORD PTR [rip+0x201699] # 0x6021f0 <main::pass>)
R8 : 0x0
R9 : 0x0
R10: 0x7ffff7acdfe0 --> 0x0
R11: 0xffffffd8
R12: 0x400910 (<_start>: xor ebp,ebp)
R13: 0x7fffffffdf80 --> 0x3
R14: 0x0
R15: 0x0
EFLAGS: 0x206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x400b43 <main+137>: mov rdi,rax
0x400b46 <main+140>: mov eax,0x0
0x400b4b <main+145>: call 0x400840 <snprintf@plt>
=> 0x400b50 <main+150>: mov rax,QWORD PTR [rip+0x201699] # 0x6021f0 <main::pass>
0x400b57 <main+157>: movzx eax,BYTE PTR [rax]
0x400b5a <main+160>: test al,al
0x400b5c <main+162>: je 0x400b7f <main+197>
0x400b5e <main+164>: mov rdx,QWORD PTR [rip+0x20168b] # 0x6021f0 <main::pass>
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffde80 --> 0x7fffffffdf88 --> 0x7fffffffe359 ("/tmp/sc/sc")
0008| 0x7fffffffde88 --> 0x300400910
0016| 0x7fffffffde90 --> 0x7fffffffdf80 --> 0x3
0024| 0x7fffffffde98 --> 0x0
0032| 0x7fffffffdea0 --> 0x0
0040| 0x7fffffffdea8 --> 0x7ffff7732f45 (<__libc_start_main+245>: mov edi,eax)
0048| 0x7fffffffdeb0 --> 0x0
0056| 0x7fffffffdeb8 --> 0x7fffffffdf88 --> 0x7fffffffe359 ("/tmp/sc/sc")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Temporary breakpoint 1, 0x0000000000400b50 in main ()
gdb-peda$ x/32bx 0x13370000
0x13370000: 0x41 0x41 0x41 0x41 0x41 0x41 0x41 0x41
0x13370008: 0x00 0x64 0x65 0x61 0x64 0x62 0x65 0x65
0x13370010: 0x66 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x13370018: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
uidがしっかりNULL終端されています。snprintfえらい。
パスワードが漏れることもありません。
$ ./sc AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA BBBB 認証失敗 uid=AAAAAAAA
おわりに
pwnが出たことはとても嬉しかったのですが、午後問は(この問題に限らず)解いてみてもやもやする問題でした。
来年度から「情報処理安全確保支援士」という制度が新しく始まります。
公式解答に掲載されている午後1問2の出題意図には
今日の情報セキュリティスペシャリストならば、単なるキーワードの理解だけではなく、そのメカニズムと具体的対策も知っておくべきものである。
と書いてありましたが、ここに挙げられているようなエンジニアが増えるような制度になるといいなと思います。