LLVMの脆弱性の話が出てきたので、掘り下げてみることにした。まだ理解できていないことが多いが、このような機能があるのだと思って勉強になった。
事の発端は、LLVMのArmバックエンドにおいて、スタックスロットの割り当てについてバグがあったという話。 発端の記事はこちら。
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でコード生成を比較したが、違いがほとんどなく分からなかった。 もう少し掘り下げていく必要がありそうだ。