Entry

プログラミングメモ - 危険なパスワードチェックとか

2008年06月05日

知り合いと話していて,こんなソースは危ねいよ,と言われたからメモ。

int
check_passwd(char *passwd)
{
    char buf[16];
    int ret;

    get_passwd(buf, sizeof(buf));
    ret = (!strcmp(buf, passwd)) ? TRUE : FALSE;

    /* !! buf clear */
    memset(buf, 0, sizeof(buf));

    return ret;
}

check_passwd() はパスワードをチェックする関数です。引数でもらった比較元のパスワードと,ユーザが入力したパスワードを比較して,結果を返す関数。ユーザの入力内容をメモリに残さないように,最後は memset() でゼロクリアしています。完璧じゃん!

けどこれ,最適化してコンパイルすると,ちょっとまずい。スタック上の自動変数は,どっかが勝手によろしくやってくれるもんだと思って,memset() しない人もいるから,これはこれでまだマシな方なんでしょうけどね。ともあれ,Cygwin の gcc 3.4.4 で,こんな風にコンパイルしてみました。

aian ~ $ cc -S -O2 -o foo.a foo.c

途中経過(アセンブリ)を見ると,こんな風になってるのが分かります。

_check_passwd:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $40, %esp
    movl    %edi, -4(%ebp)
    movl    8(%ebp), %eax
    leal    -24(%ebp), %edi
    movl    %ebx, -8(%ebp)
    xorl    %ebx, %ebx
    movl    %edi, (%esp)
    movl    %eax, 4(%esp)
    call    _strcmp
    cld
    testl   %eax, %eax
    sete    %bl
    xorl    %edx, %edx
    movl    $4, %ecx
    movl    %edx, %eax
    rep
    stosl
    movl    -4(%ebp), %edi
    movl    %ebx, %eax
    movl    -8(%ebp), %ebx
    movl    %ebp, %esp
    popl    %ebp
    ret

memset() の呼び出しがなくなってる!これじゃメモリはクリアされません。つまり,スタックにはユーザが入力したパスワードが残っているということです。メモリはクリアされていました。訂正の詳細は,「qune: プログラミングメモ - 「危険なパスワードチェックとか」誤記の訂正とお詫び」をご参照ください。参考までに,最適化オプション(-O2)を取り除いてコンパイルすると,memset() が呼ばれているのが分かります。

_check_passwd:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $56, %esp
    movl    $16, 4(%esp)
    leal    -24(%ebp), %eax
    movl    %eax, (%esp)
    call    _get_passwd
    leal    -24(%ebp), %edx
    movl    8(%ebp), %eax
    movl    %eax, 4(%esp)
    movl    %edx, (%esp)
    call    _strcmp
    testl   %eax, %eax
    sete    %al
    movzbl  %al, %eax
    movl    %eax, -28(%ebp)
    movl    $16, 8(%esp)
    movl    $0, 4(%esp)
    leal    -24(%ebp), %eax
    movl    %eax, (%esp)
    call    _memset
    movl    -28(%ebp), %eax
    leave
    ret

じゃあ,最適化オプションを付けないでコンパイルすればいいか,というと,そういうわけじゃもちろんありませんよね。こんなことで最適化オプション云々が左右される筋合いはないし,第一,処理系によって何が最適化されるかは分かりません。最適化オプションを付けた Cygwin gcc 3.4.4 では,「memset() してるのに buf を参照してないじゃん!」と判断して,memset() をコールしなかったわけですけれど,他の処理系でも同じ最適化ポリシーがあるとは限りません。

というわけで,こういうときは,最適化されてもゼロクリアされるように,自前でコーディングするのが,お作法なんだそうな。こんな感じで。

int
check_passwd(char *passwd)
{
    volatile char buf[16];
    int ret;
    int i;

    get_passwd(buf, sizeof(buf));

    /* compare */
    ret = TRUE;
    for (i = 0; *(buf + i) != '\0'; i++) {
        if (*(buf + i) != *(passwd + i)) {
            ret = FALSE;
            break;
        }
    }

    /* buf clear */
    for (i = 0; i < sizeof(buf); i++) {
        *(buf + i) = 0;
    }

    return ret;
}

strcmp() と memset() を簡単に自作(※あまりいい実装じゃないけど)。自作ルーチンに相当する箇所は,gcc ではアセンブリで書かれていると思うので,速度は落ちると思います。それと,buf に volatile 修飾子をつけました。組み込み系の人にはおなじみかもしれませんけど,volatile 修飾子ってのは,コンパイラに勝手な解釈をさせないように指示する修飾子のことです。組み込みでは,チップの脚(I/O)の数を変数の幅と一致させているときがあるわけで,変数の幅は割と重要だったりします。けどこれ,何も指定していないと,4バイト境界なんかとの関係で,勝手に short を long にしたりする処理系もあったりする。volatile 修飾子は,そゆのするな!と指示するわけです。

と,ここでは偉そうに書いたものの,あたしゃさして気にもせず,memset() を呼んでいました。なんだか奥がふかいですね。ちなみに,memset() を bzero() で置き換えても,結果は同じでした。Windows の ZeroMemory() マクロも多分同じだと思う……。

こゆところのバグは,かなり気をつけないと分からないよなあ……。これは C だったから,メモリの中身を比較的簡単に操作できたけれども,Java や LL には,ここら辺の手当てがあるんだろうか……。

Trackback
Trackback URL:
Ads
About
Search This Site
Ads
Categories
Recent Entries
Log Archive
Syndicate This Site
Info.
クリエイティブ・コモンズ・ライセンス
Movable Type 3.36
Valid XHTML 1.1!
Valid CSS!
ブログタイムズ

© 2003-2012 AIAN