From 039ee72a6894be4a00508613779ed7773a4e1538 Mon Sep 17 00:00:00 2001 From: Alvin Wong Date: Thu, 29 Sep 2022 18:19:42 +0800 Subject: [PATCH] Add CFGuard test --- run-tests.sh | 46 ++++++++++++++++- test/cfguard-test.c | 118 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 test/cfguard-test.c diff --git a/run-tests.sh b/run-tests.sh index 33ac91e..757291a 100755 --- a/run-tests.sh +++ b/run-tests.sh @@ -42,6 +42,20 @@ if ! $ANY_ARCH-w64-mingw32-gcc -E is-ucrt.c > /dev/null 2>&1; then fi rm -f is-ucrt.c +if (echo "int main(){}" | $ANY_ARCH-w64-mingw32-clang -x c++ - -o has-cfguard-test.exe -mguard=cf); then + if llvm-readobj --coff-load-config has-cfguard-test.exe | grep -q 'CF_INSTRUMENTED (0x100)'; then + HAS_CFGUARD=1 + elif [ -n "$HAS_CFGUARD" ]; then + echo "error: Toolchain doesn't seem to include Control Flow Guard support." 1>&2 + rm -f has-cfguard-test.exe + exit 1 + fi + rm -f has-cfguard-test.exe +elif [ -n "$HAS_CFGUARD" ]; then + echo "error: Toolchain doesn't seem to include Control Flow Guard support." 1>&2 + exit 1 +fi + : ${TARGET_OSES:=${TOOLCHAIN_TARGET_OSES-$DEFAULT_OSES}} if [ -z "$RUN_X86" ]; then @@ -82,6 +96,9 @@ TESTS_OMP="hello-omp" TESTS_UWP="uwp-error" TESTS_IDL="idltest" TESTS_OTHER_TARGETS="hello" +if [ -n "$HAS_CFGUARD" ]; then + TESTS_CFGUARD="cfguard-test" +fi for arch in $ARCHS; do case $arch in i686|x86_64) @@ -165,6 +182,9 @@ for arch in $ARCHS; do $arch-w64-mingw32-clang $test.c -o $TEST_DIR/$test.exe -fstack-protector-strong FAILURE_TESTS="$FAILURE_TESTS $test" done + for test in $TESTS_CFGUARD; do + $arch-w64-mingw32-clang $test.c -o $TEST_DIR/$test.exe -mguard=cf + done for test in $TESTS_FORTIFY; do $arch-w64-mingw32-clang $test.c -o $TEST_DIR/$test-fortify.exe -O2 -D_FORTIFY_SOURCE=2 -lssp TESTS_EXTRA="$TESTS_EXTRA $test-fortify" @@ -217,6 +237,14 @@ for arch in $ARCHS; do TESTS_EXTRA="$TESTS_EXTRA $test-asan" FAILURE_TESTS="$FAILURE_TESTS $test-asan" fi + if [ -n "$HAS_CFGUARD" ]; then + # Smoke test ASAN with CFGuard to make sure it doesn't trip. + $arch-w64-mingw32-clang $test.c -o $TEST_DIR/$test-asan-cfguard.exe -fsanitize=address -g -gcodeview -Wl,-pdb,$TEST_DIR/$test-asan.pdb -mguard=cf + if [ -n "$NATIVE" ]; then + TESTS_EXTRA="$TESTS_EXTRA $test-asan-cfguard" + FAILURE_TESTS="$FAILURE_TESTS $test-asan-cfguard" + fi + fi done for test in $TESTS_UBSAN; do case $arch in @@ -287,7 +315,7 @@ for arch in $ARCHS; do echo $file trigger failed expectedly, returned $ret case $test in - stacksmash-asan) + stacksmash-asan|stacksmash-asan-cfguard) grep -q stack-buffer-overflow $OUT grep -q "func.*stacksmash.c" $OUT ;; @@ -331,6 +359,22 @@ for arch in $ARCHS; do fi i=$(($i+1)) done + for test in $TESTS_CFGUARD; do + file=$test.exe + OUT=cmdoutput + rm -f $OUT + if $RUN $test.exe check_enabled; then + $RUN $test.exe normal_icall + $RUN $test.exe invalid_icall_nocf || [ $? = 2 ] + # We want to check the exit code to be 0xc0000409 + # (STATUS_STACK_BUFFER_OVERRUN aka fail fast exception). + # MSYS2 bash does not give us the full 32-bit exit code, so + # we have to rely on cmd.exe to perform the check. + # (This probably doesn't work on Wine, but Wine doesn't + # support CFG anyway, at least not for now...) + $RUN cmd //v:on //c "$test.exe invalid_icall & if !errorlevel! equ -1073740791 (exit 0) else (exit 1)" + fi + done fi fi cd .. diff --git a/test/cfguard-test.c b/test/cfguard-test.c new file mode 100644 index 0000000..e66e08a --- /dev/null +++ b/test/cfguard-test.c @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2022 Alvin Wong + * + * This file is part of llvm-mingw. + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#define WINVER 0x0603 +#define _WIN32_WINNT 0x0603 +#include + +#include +#include + +__attribute__ (( noinline )) +void nop_sled_target(void) { + __asm__("nop"); __asm__("nop"); __asm__("nop"); __asm__("nop"); + __asm__("nop"); __asm__("nop"); __asm__("nop"); __asm__("nop"); + __asm__("nop"); __asm__("nop"); __asm__("nop"); __asm__("nop"); + __asm__("nop"); __asm__("nop"); __asm__("nop"); __asm__("nop"); + + __asm__("nop"); __asm__("nop"); __asm__("nop"); __asm__("nop"); + __asm__("nop"); __asm__("nop"); __asm__("nop"); __asm__("nop"); + __asm__("nop"); __asm__("nop"); __asm__("nop"); __asm__("nop"); + __asm__("nop"); __asm__("nop"); __asm__("nop"); __asm__("nop"); + +#if defined(__x86_64__) + // On x86_64 the stack frame has to be aligned to 16 bytes. Since we + // skipped the prologue we need to manually realign it to prevent + // alignment-related crashes when calling puts() or exit(). + __asm__("and $~0xF, %rsp"); +#endif + + puts("Pwned!!!"); + + // We skipped the function prologue with the indirect call. If we let + // the function return normally it will just crash with a segfault, so + // do an exit instead. + exit(2); +} + +__attribute__ (( noinline )) +void normal_function(void) { + puts("Normal function called."); +} + +__attribute__ (( noinline )) +void make_indirect_call(void (*fn_ptr)(void)) { + fn_ptr(); +} + +__attribute__ (( noinline, guard(nocf) )) +void make_indirect_call_nocf(void (*fn_ptr)(void)) { + fn_ptr(); +} + +int check_cfguard_status(void) { + PROCESS_MITIGATION_CONTROL_FLOW_GUARD_POLICY policy; + BOOL result = GetProcessMitigationPolicy(GetCurrentProcess(), + ProcessControlFlowGuardPolicy, + &policy, + sizeof(policy)); + if (!result) + return 0; + return policy.EnableControlFlowGuard; +} + +int main(int argc, char *argv[]) { + if (argc == 2) { + if (strcmp(argv[1], "check_enabled") == 0) { + if (check_cfguard_status()) { + puts("Control Flow Guard is enabled!"); + return 0; + } else { + puts("Control Flow Guard is _not_ enabled!"); + return 1; + } + } + if (strcmp(argv[1], "normal_icall") == 0) { + puts("Performing normal indirect call."); + make_indirect_call(normal_function); + return 0; + } + if (strcmp(argv[1], "invalid_icall") == 0) { + void *target = nop_sled_target; + target += 16; + puts("Performing invalid indirect call. If CFG is enabled this " + "should crash with exit code 0xc0000409 (-1073740791)..."); + fflush(stdout); + make_indirect_call(target); + puts("Unexpectedly returned from indirect call!"); + return 1; + } + if (strcmp(argv[1], "invalid_icall_nocf") == 0) { + void *target = nop_sled_target; + target += 16; + puts("Performing invalid indirect call without CFG. You should " + "get an exit code 2..."); + fflush(stdout); + make_indirect_call_nocf(target); + puts("Unexpectedly returned from indirect call!"); + return 1; + } + } + printf("%s ( check_enabled | normal_icall | invalid_icall | invalid_icall_nocf )\n", argv[0]); + return 32; +}