読者です 読者をやめる 読者になる 読者になる

しゃろの日記

CTFのwriteup置き場になる予定(`・ω・´)

情報セキュリティスペシャリスト試験を受けた話

情報セキュリティスペシャリスト試験(以下「SC」)が平成28年秋期で最後とのことだったので、
重い腰を上げて受験しました。

これまでのSC申し込みの部失敗の歴史


午前は無事通過し、午後1・午後2は何とかなるでしょw(適当)
という感じです。

午後1が始まる前に、冗談半分で

というツイートをしたのですが、 本当に出題されるとは思わなかった……。

しかし、この問題で少し気になる点があったので、今回はその点について考えてみたいと思います。

なお、今回の試験の問題冊子と公式の解答は既に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を出力)
    }
}

↑図2 ヒープベースBOF脆弱性のあるプログラムY

プログラム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"という文字列がありますね。
ここにあるpassargv[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]が比較されるため、今回の環境では

  • argv[1]がある程度長い(32バイト以上)
  • argv[1]の後ろの方(33バイト目以降)が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はしませんが、uidpassが文字列として繋がっているため、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の出題意図には

今日の情報セキュリティスペシャリストならば、単なるキーワードの理解だけではなく、そのメカニズムと具体的対策も知っておくべきものである。

と書いてありましたが、ここに挙げられているようなエンジニアが増えるような制度になるといいなと思います。