FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

LLVM Armバックエンド脆弱性 stack protectorとは何なのか

LLVM脆弱性の話が出てきたので、掘り下げてみることにした。まだ理解できていないことが多いが、このような機能があるのだと思って勉強になった。

事の発端は、LLVMのArmバックエンドにおいて、スタックスロットの割り当てについてバグがあったという話。 発端の記事はこちら。

reviews.llvm.org

LocalStackSlotPassはスタックプロテクタを事前に割り当て、それがスタック上のローカル変数の前に来るようにする。 PEIの間に、スタックプロテクタを割り当て直さないようにすべきである。もしそうでないと、新しいスタック保護スロットがが、それを保護すべきであるローカル変数の後に配置されてしまう。

PEIというのはPrologue Epilogue Inserterのことで、関数のプロローグ、エピローグを生成するためのルーチンである。

そもそも関数が呼ばれると、まずはスタックを掘り下げてローカル変数のためのスタック領域を作成する。しかしプログラムのバグなどによってローカル変数のスタックを飛び越え、スタックを破壊して戻り値のスロットなどを破壊してしまった場合、関数の戻り先などを誤ることになり、脆弱性となる。 このような脆弱性を用いた攻撃手法なども存在するらしい(これがReturn Oriented Programmingというやつか?)。

そこで、スタックのローカル変数のスロットの前に、スタックプロテクタという領域を用意し、スタック領域がスタックプロテクタを超えてアクセス(というか、スタックプロテクタを踏んでしまった場合)を検知し、もしスタックプロテクタに格納されている値が関数の呼び出し時と関数から戻る時に異なっていた場合は、スタック領域を破壊したということで問題が発生したということにする。 これがスタックプロテクタの役割である。

では、実際にスタックプロテクタを生成してみる。clangのオプションで、-fstack-protectorオプションをつけることで生成できるらしい。やってみる。 ソースコードは上記掲示板で出ているソースコードを使ってみた。

./bin/clang -c -emit-llvm stack_pointer.cpp -o stack_pointer.bc
./bin/clang -c -emit-llvm -fstack-protector stack_pointer.cpp -o stack_pointer.protect.bc
./bin/llvm-dis -d stack_pointer.bc -o -
./bin/llvm-dis stack_pointer.protect.bc -o stack_pointer.protect.ll
  • スタックプロテクタをつけた場合のIR
; ModuleID = 'stack_pointer.protect.bc'
source_filename = "stack_pointer.cpp"
target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-unknown-linux-gnu"

; Function Attrs: noinline optnone ssp uwtable
define dso_local i32 @_Z2fnPKc(i8* %str) #0 {
entry:
  %str.addr = alloca i8*, align 8
  %buffer = alloca [65536 x i8], align 16
  store i8* %str, i8** %str.addr, align 8
  %arraydecay = getelementptr inbounds [65536 x i8], [65536 x i8]* %buffer, i32 0, i32 0
  %0 = load i8*, i8** %str.addr, align 8
  %call = call i8* @strcpy(i8* %arraydecay, i8* %0) #3
  %arraydecay1 = getelementptr inbounds [65536 x i8], [65536 x i8]* %buffer, i32 0, i32 0
  %call2 = call i32 @puts(i8* %arraydecay1)
  %arrayidx = getelementptr inbounds [65536 x i8], [65536 x i8]* %buffer, i64 0, i64 65535
  %1 = load i8, i8* %arrayidx, align 1
  %conv = sext i8 %1 to i32
  ret i32 %conv
}

; Function Attrs: nounwind
declare dso_local i8* @strcpy(i8*, i8*) #1

declare dso_local i32 @puts(i8*) #2

attributes #0 = { noinline optnone ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "min-legal-vector-width"="0" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+fxsr,+mmx,+sse,+sse2,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #1 = { nounwind "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+fxsr,+mmx,+sse,+sse2,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #2 = { "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+fxsr,+mmx,+sse,+sse2,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #3 = { nounwind }

!llvm.module.flags = !{!0}
!llvm.ident = !{!1}

!0 = !{i32 1, !"wchar_size", i32 4}
!1 = !{!"clang version 8.0.1 (https://github.com/llvm-mirror/clang.git a03da8be08a208122e292016cb6cea1f30229677) (https://github.com/msyksphinz/llvm.git a0792a15b93d4b73f2546ae87912c21598098889)"}
  • スタックプロテクタをつけない場合のIR
...
attributes #0 = { noinline optnone uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "min-legal-vector-width"="0" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+fxsr,+mmx,+sse,+sse2,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }

違いは、attributesの中にsspが入っているかの違い。

では実際にコード生成して、差分を確認してみる。左側がスタックプロテクタをつけたもの、右側がスタックプロテクタをつけないもの。

./bin/llc -march=aarch64 ../build-myriscvx/stack_pointer.protect.bc -filetype=asm -o stack_pointer.protect.aarch.s
./bin/llc -march=aarch64 ../build-myriscvx/stack_pointer.bc -filetype=asm -o stack_pointer.aarch.s
diff -y -W150 stack_pointer.protect.aarch.s stack_pointer.aarch.s | less
        .text                                                                           .text
        .file   "stack_pointer.cpp"                                                     .file   "stack_pointer.cpp"
        .globl  _Z2fnPKc                // -- Begin function _Z2fnPKc                   .globl  _Z2fnPKc                // -- Begin function _Z2fnPKc
        .p2align        2                                                               .p2align        2
        .type   _Z2fnPKc,@function                                                      .type   _Z2fnPKc,@function
_Z2fnPKc:                               // @_Z2fnPKc                            _Z2fnPKc:                               // @_Z2fnPKc
        .cfi_startproc                                                                  .cfi_startproc
// %bb.0:                               // %entry                               // %bb.0:                               // %entry
        stp     x28, x19, [sp, #-32]!   // 16-byte Folded Spill           |             str     x28, [sp, #-32]!        // 8-byte Folded Spill
        stp     x29, x30, [sp, #16]     // 16-byte Folded Spill                         stp     x29, x30, [sp, #16]     // 16-byte Folded Spill
        add     x29, sp, #16            // =16                                          add     x29, sp, #16            // =16
        sub     sp, sp, #16, lsl #12    // =65536                                       sub     sp, sp, #16, lsl #12    // =65536
        sub     sp, sp, #32             // =32                            |             sub     sp, sp, #16             // =16
        .cfi_def_cfa w29, 16                                                            .cfi_def_cfa w29, 16
        .cfi_offset w30, -8                                                             .cfi_offset w30, -8
        .cfi_offset w29, -16                                                            .cfi_offset w29, -16
        .cfi_offset w19, -24                                              <
        .cfi_offset w28, -32                                                            .cfi_offset w28, -32
        mov     x19, sp                                                   |             sub     x8, x29, #24            // =24
        adrp    x8, __stack_chk_guard                                     |             str     x0, [x8]
        ldr     x8, [x8, :lo12:__stack_chk_guard]                         |             ldr     x1, [x8]
        str     x8, [x19]                                                 |             mov     x0, sp
        str     x0, [sp, #8]                                              <
        ldr     x1, [sp, #8]                                              <
        add     x0, sp, #16             // =16                            <
        bl      strcpy                                                                  bl      strcpy
        add     x0, sp, #16             // =16                            |             mov     x0, sp
        bl      puts                                                                    bl      puts
        add     x8, sp, #16             // =16                            |             mov     x8, sp
        orr     x9, xzr, #0xffff                                                        orr     x9, xzr, #0xffff
        add     x8, x8, x9                                                              add     x8, x8, x9
        ldrsb   w0, [x8]                                                                ldrsb   w0, [x8]
        ldr     x8, [x19]                                                 <
        adrp    x9, __stack_chk_guard                                     <
        ldr     x9, [x9, :lo12:__stack_chk_guard]                         <
        subs    x8, x9, x8                                                <
        cbnz    x8, .LBB0_1                                               <
        b       .LBB0_2                                                   <
.LBB0_1:                                // %entry                         <
        bl      __stack_chk_fail                                          <
.LBB0_2:                                // %entry                         <
        add     sp, sp, #16, lsl #12    // =65536                                       add     sp, sp, #16, lsl #12    // =65536
        add     sp, sp, #32             // =32                            |             add     sp, sp, #16             // =16
        ldp     x29, x30, [sp, #16]     // 16-byte Folded Reload                        ldp     x29, x30, [sp, #16]     // 16-byte Folded Reload
        ldp     x28, x19, [sp], #32     // 16-byte Folded Reload          |             ldr     x28, [sp], #32          // 8-byte Folded Reload
        ret                                                                             ret
.Lfunc_end0:                                                                    .Lfunc_end0:
        .size   _Z2fnPKc, .Lfunc_end0-_Z2fnPKc                                          .size   _Z2fnPKc, .Lfunc_end0-_Z2fnPKc
        .cfi_endproc                                                                    .cfi_endproc
                                        // -- End function                                                              // -- End function

        .ident  "clang version 8.0.1 (https://github.com/llvm-mirror/c                  .ident  "clang version 8.0.1 (https://github.com/llvm-mirror/c
        .section        ".note.GNU-stack","",@progbits                                  .section        ".note.GNU-stack","",@progbits

スタックプロテクタを付けると、__stack_chk_guardという領域からランダム値を読み取り、それをスタックのスタックプロテクタの領域に配置する。 関数から抜けるときに、スタックプロテクタの値を再度ロードし、__stack_chk_guardからもう一度値を読み直して比較し、一致していればOK、そうでなければスタック破壊が起きていると判断する。

まだ理解が足りないので、スタックプロテクタがどのようにバグを発生せるのか、理解できていない。最新版のLLVMとバグを発生させるバージョンのLLVMでコード生成を比較したが、違いがほとんどなく分からなかった。 もう少し掘り下げていく必要がありそうだ。