diff --git a/cpp/src/security/AsyncUnsafeSignalHandler/AsyncUnsafeSignalHandler.qhelp b/cpp/src/security/AsyncUnsafeSignalHandler/AsyncUnsafeSignalHandler.qhelp index cc1130f..3394cf5 100644 --- a/cpp/src/security/AsyncUnsafeSignalHandler/AsyncUnsafeSignalHandler.qhelp +++ b/cpp/src/security/AsyncUnsafeSignalHandler/AsyncUnsafeSignalHandler.qhelp @@ -7,9 +7,23 @@ This is a CodeQL query constructed to find signal handlers that are performing async unsafe operations.

+

+ Because a signal may be delivered at any moment, e.g., in the middle of a malloc, the handler shouldn't touch + the program's internal state. +

+

The kernel defines a list of async-safe signal functions in its man page. - Any signal handler that performs operations that are not safe asynchronously may be vulnerable. + Any signal handler that performs operations that are not safe for asynchronous execution may be vulnerable. +

+ +

+ Moreover, signal handlers may be re-entered. Handlers' logic should take that possibility into account. +

+ +

+ If the issue is exploitable depends on attacker's ability to deliver the signal. + Remote attacks may be limitted to some signals (like SIGALRM and SIGURG), while local attacks could use all signals.

@@ -17,17 +31,24 @@

Attempt to keep signal handlers as simple as possible. Only call async-safe functions from signal handlers.

+

+Block delivery of new signals inside signal handlers to prevent handler re-entrancy issues. +

- In this example, while both syntatically valid, a correct handler is defined in the correct_handler function and sets a flag. The function calls log_message, a async unsafe function, within the main loop. + In this example, while both syntatically valid, a correct handler is defined in the correct_handler function and sets a flag. + The function calls log_message, a async unsafe function, within the main loop.

+
  • + Michal Zalewski, "Delivering Signals for Fun and Profit" +
  • SEI CERT C Coding Standard "SIG30-C. Call only asynchronous-safe functions within signal handlers"
  • diff --git a/cpp/src/security/AsyncUnsafeSignalHandler/AsyncUnsafeSignalHandler.ql b/cpp/src/security/AsyncUnsafeSignalHandler/AsyncUnsafeSignalHandler.ql index 9ee350f..ae5d5b1 100644 --- a/cpp/src/security/AsyncUnsafeSignalHandler/AsyncUnsafeSignalHandler.ql +++ b/cpp/src/security/AsyncUnsafeSignalHandler/AsyncUnsafeSignalHandler.ql @@ -11,6 +11,8 @@ */ import cpp +import semmle.code.cpp.dataflow.new.DataFlow + /* List from https://man7.org/linux/man-pages/man3/stdio.3.html */ class StdioFunction extends Function { @@ -46,22 +48,106 @@ predicate isAsyncUnsafe(Function signalHandler) { ) } -predicate isSignalHandlerField(FieldAccess fa) { - fa.getTarget().getName() in ["__sa_handler", "__sa_sigaction", "sa_handler", "sa_sigaction"] +/** + * Flows from any function ptr + */ +module HandlerToSignalConfiguration implements DataFlow::ConfigSig { + predicate isSource(DataFlow::Node source) { + source.asExpr() = any(Function f || f.getAnAccess()) + } + + predicate isSink(DataFlow::Node sink) { + sink = sink + } } +module HandlerToSignal = DataFlow::Global; -from FunctionCall fc, Function signalHandler, FieldAccess fa -where - ( - fc.getTarget().getName().matches("%_signal") or - fc.getTarget().getName() = "signal" - ) and - signalHandler.getName() = fc.getArgument(1).toString() and - isAsyncUnsafe(signalHandler) +/** + * signal(SIGX, signalHandler) + */ +predicate isSignal(FunctionCall signalCall, Function signalHandler) { + signalCall.getTarget().getName() = "signal" + and exists(DataFlow::Node source, DataFlow::Node sink | + HandlerToSignal::flow(source, sink) + and source.asExpr() = signalHandler.getAnAccess() + and sink.asExpr() = signalCall.getArgument(1) + ) +} + +/** + * struct sigaction sigactVar = ... + * sigaction(SIGX, &sigactVar, ...) + */ +predicate isSigaction(FunctionCall sigactionCall, Function signalHandler, Variable sigactVar) { + exists(Struct sigactStruct, Field handlerField, DataFlow::Node source, DataFlow::Node sink | + sigactionCall.getTarget().getName() = "sigaction" + and sigactionCall.getArgument(1).getAChild*() = sigactVar.getAnAccess() + + // struct sigaction with the handler func + and sigactStruct.getName() = "sigaction" + and handlerField.getName() = ["sa_handler", "sa_sigaction", "__sa_handler", "__sa_sigaction", "__sigaction_u"] + and ( + handlerField = sigactStruct.getAField() + or + exists(Union u | u.getAField() = handlerField and u = sigactStruct.getAField().getType()) + ) + + and ( + // sigactVar.sa_handler = &signalHandler + exists(Assignment a, ValueFieldAccess vfa | + vfa.getTarget() = handlerField + and vfa.getQualifier+() = sigactVar.getAnAccess() + and a.getLValue() = vfa.getAChild*() + + and source.asExpr() = signalHandler.getAnAccess() + and sink.asExpr() = a.getRValue() + and HandlerToSignal::flow(source, sink) + ) + or + // struct sigaction sigactVar = {.sa_sigaction = signalHandler}; + exists(ClassAggregateLiteral initLit | + sigactVar.getInitializer().getExpr() = initLit + and source.asExpr() = signalHandler.getAnAccess() + and sink.asExpr() = initLit.getAFieldExpr(handlerField).getAChild*() + and HandlerToSignal::flow(source, sink) + ) + ) + ) +} + +/** + * Determine if new signals are blocked + * TODO: should only find writes and only for correct (or all) signals + * TODO: should detect other block mechanisms + */ +predicate isSignalDeliveryBlocked(Variable sigactVar) { + exists(ValueFieldAccess dfa | + dfa.getQualifier+() = sigactVar.getAnAccess() and dfa.getTarget().getName() = "sa_mask" + ) or - fc.getTarget().getName() = "sigaction" and - isSignalHandlerField(fa) and - signalHandler = fa.getTarget().getAnAssignedValue().(AddressOfExpr).getAddressable() and + exists(Field mask | + mask.getName() = "sa_mask" + and exists(sigactVar.getInitializer().getExpr().(ClassAggregateLiteral).getAFieldExpr(mask)) + ) +} + +string deliveryNotBlockedMsg() { + result = "Moreover, delivery of new signals may be not blocked. " +} + +from FunctionCall fc, Function signalHandler, string msg +where isAsyncUnsafe(signalHandler) -select signalHandler, - "is a non-trivial signal handler that may be using functions that are not async-safe." + and ( + (isSignal(fc, signalHandler) and msg = deliveryNotBlockedMsg()) + or + exists(Variable sigactVar | + isSigaction(fc, signalHandler, sigactVar) + and if isSignalDeliveryBlocked(sigactVar) then + msg = "" + else + msg = deliveryNotBlockedMsg() + ) + ) +select signalHandler, "$@ is a non-trivial signal handler that uses not async-safe functions. " + msg + + "Handler is registered by $@.", signalHandler, signalHandler.toString(), fc, fc.toString() diff --git a/cpp/test/include/libc/signal.h b/cpp/test/include/libc/signal.h index 5ade866..fcc0bf8 100644 --- a/cpp/test/include/libc/signal.h +++ b/cpp/test/include/libc/signal.h @@ -3,27 +3,27 @@ #ifndef HEADER_SIGNAL_STUB_H #define HEADER_SIGNAL_STUB_H -#ifdef __cplusplus -extern "C" { -#endif - #define SIGALRM 14 #define SIGSEGV 11 +#define SIGTERM 15 #define SIG_ERR -1 #define EXIT_FAILURE 2 -#define SA_SIGINFO 0x00000004 +#define SA_SIGINFO 4 -typedef void (*sighandler_t)(int); -extern int signal(int, sighandler_t); +{} // to silent error from codeql's extractor +typedef void (*sig_t)(int); +extern int signal(int, sig_t); typedef struct { unsigned long sig[64]; } sigset_t; + typedef struct { int si_signo; /* Signal number */ int si_errno; /* An errno value */ int si_code; /* Signal code */ } siginfo_t; + struct sigaction { void (*sa_handler)(int); void (*sa_sigaction)(int, siginfo_t *, void *); @@ -35,9 +35,6 @@ struct sigaction { extern int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); extern int kill(int pid, int sig); -#ifdef __cplusplus -} -#endif #endif diff --git a/cpp/test/include/libc/stdarg.h b/cpp/test/include/libc/stdarg.h new file mode 100644 index 0000000..c9f353f --- /dev/null +++ b/cpp/test/include/libc/stdarg.h @@ -0,0 +1,25 @@ +#ifndef USE_HEADERS + +#ifndef HEADER_STDARG_STUB_H +#define HEADER_STDARG_STUB_H + +#ifdef __cplusplus +extern "C" { +#endif + +typedef void *va_list; +#define va_start(ap, parmN) +#define va_end(ap) +#define va_arg(ap, type) ((type)0) + +#ifdef __cplusplus +} +#endif + +#endif + +#else // --- else USE_HEADERS + +#include + +#endif // --- end USE_HEADERS \ No newline at end of file diff --git a/cpp/test/include/libc/stdlib.h b/cpp/test/include/libc/stdlib.h index cbacdf8..cc18de3 100644 --- a/cpp/test/include/libc/stdlib.h +++ b/cpp/test/include/libc/stdlib.h @@ -16,6 +16,7 @@ long lrand48(void) { } void _Exit(int); +void exit(int); void free(void*); void *malloc(unsigned long); diff --git a/cpp/test/include/libc/string_stubs.h b/cpp/test/include/libc/string_stubs.h index e7e83ce..9310e50 100644 --- a/cpp/test/include/libc/string_stubs.h +++ b/cpp/test/include/libc/string_stubs.h @@ -24,6 +24,10 @@ extern int wprintf(const wchar_t * format, ...); extern wchar_t* wcscpy(wchar_t * s1, const wchar_t * s2); extern void perror(const char *s); +extern void openlog(const char*, int, int); +extern void syslog(int, const char*, ...); +extern void closelog(void) + #ifdef __cplusplus } #endif @@ -37,6 +41,7 @@ extern void perror(const char *s); #include #include #include +#include #define strcpy_s strcpy #define _mbsncat strncat diff --git a/cpp/test/include/libc/unistd.h b/cpp/test/include/libc/unistd.h new file mode 100644 index 0000000..7163a23 --- /dev/null +++ b/cpp/test/include/libc/unistd.h @@ -0,0 +1,22 @@ +#ifndef USE_HEADERS + +#ifndef HEADER_UNISTD_STUB_H +#define HEADER_UNISTD_STUB_H + +#ifdef __cplusplus +extern "C" { +#endif + +void _exit(int); + +#ifdef __cplusplus +} +#endif + +#endif + +#else // --- else USE_HEADERS + +#include + +#endif // --- end USE_HEADERS \ No newline at end of file diff --git a/cpp/test/query-tests/security/AsyncUnsafeSignalHandler/AsyncUnsafeSignalHandler.c b/cpp/test/query-tests/security/AsyncUnsafeSignalHandler/AsyncUnsafeSignalHandler.c index 32b6b43..72c0b9c 100644 --- a/cpp/test/query-tests/security/AsyncUnsafeSignalHandler/AsyncUnsafeSignalHandler.c +++ b/cpp/test/query-tests/security/AsyncUnsafeSignalHandler/AsyncUnsafeSignalHandler.c @@ -1,11 +1,14 @@ #include "../../../include/libc/string_stubs.h" #include "../../../include/libc/signal.h" #include "../../../include/libc/stdlib.h" +#include "../../../include/libc/unistd.h" +#include "../../../include/libc/stdarg.h" void transitive_call() { printf("UNSAFE"); } void transitive_call3() { + // TODO: decide which functions are exploitable int *x = (int*)malloc(16); free(x); } @@ -20,6 +23,12 @@ void safe_handler(int sig) { } } +void safe_handler2(int signo, siginfo_t *info, void *context) { + if (signo == SIGALRM) { + kill(1, SIGALRM); + } +} + void unsafe_handler(int sig) { transitive_call(); } @@ -31,10 +40,57 @@ void unsafe_handler2(int signo, siginfo_t *info, void *context) { transitive_call2(); } -void unsafe_handler3(int signal) { +void unsafe_handler3(int signo, siginfo_t *info, void *context) { transitive_call3(); } +// data flow case, regresshion-like +#define sigdie(...) do_logging_and_die(__FILE__, NULL, __VA_ARGS__) + +static void do_log(int level, const char *suffix, const char *fmt, va_list args) { + int pri = 0; + openlog(suffix, 1, 2); + // the unsafe call from the handler + syslog(pri, "%.500s", fmt); + closelog(); +} + +static const int level = 0; +void logv(const char *file, const char *suffix, const char *fmt, va_list args) { + char tmp[] = "sufsuf: "; + suffix = tmp; + if(!args) { + suffix = &tmp[3]; + } + do_log(level, suffix, fmt, args); +} + +void do_logging_and_die(const char *file, const char *suffix, const char *fmt, ...) { + va_list args; + va_start(args, fmt); + logv(file, suffix, fmt, args); + va_end(args); + _exit(1); +} + +static void df_handler(int sig) { + if (2*sig % 5 == 3) { + _exit(1); + } + sigdie("some log %d", sig); +} + +sig_t register_signal(int signum, sig_t handler) { + struct sigaction sa, osa; + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = handler; + sigfillset(&sa.sa_mask); + if (sigaction(signum, &sa, &osa) == -1) { + return NULL; + } + return osa.sa_handler; +} + int main() { // Register the safe signal handler if (signal(SIGALRM, safe_handler) == SIG_ERR) { @@ -42,13 +98,23 @@ int main() { _Exit(EXIT_FAILURE); } - // Unsafe example + // Safe handler 2 + struct sigaction actG2 = {0}; + actG2.sa_flags = SA_SIGINFO; + actG2.sa_sigaction = &safe_handler2; + if (sigaction(SIGSEGV, &actG2, NULL) == -1) { + perror("sigaction"); + _Exit(EXIT_FAILURE); + } + + // Unsafe example 1 if (signal(SIGALRM, unsafe_handler) == SIG_ERR) { perror("Unable to catch SIGALRM"); _Exit(EXIT_FAILURE); } - // Another unsafe example + // Unsafe example 2 + // TODO: decide which signals are exploitable and should produce findings struct sigaction act = { 0 }; act.sa_flags = SA_SIGINFO; act.sa_sigaction = &unsafe_handler2; @@ -57,13 +123,52 @@ int main() { _Exit(EXIT_FAILURE); } - struct sigaction act2 = { 0 }; - act2.sa_flags = SA_SIGINFO; - act2.sa_handler = &unsafe_handler3; + // Unsafe example 3 + struct sigaction act2 = {.sa_flags = SA_SIGINFO, .sa_sigaction = unsafe_handler2}; if (sigaction(SIGALRM, &act2, NULL) == -1) { perror("sigaction 2"); _Exit(EXIT_FAILURE); } + // Unsafe example 4 + // TODO: not every masked signals should indicate safe signal handling + sigset_t sigset; + sigemptyset(&sigset); + sigaddset(&sigset, SIGALRM); + struct sigaction act3 = {.sa_flags = SA_SIGINFO, .sa_sigaction = unsafe_handler2, .sa_mask = sigset}; + if (sigaction(SIGALRM, &act3, NULL) == -1) { + perror("sigaction 3"); + _Exit(EXIT_FAILURE); + } + + // Unsafe example 5 + struct sigaction act4 = {.sa_flags = SA_SIGINFO, .sa_sigaction = unsafe_handler3}; + act4.sa_mask = sigset; + if (sigaction(SIGSEGV, &act4, NULL) == -1) { + perror("sigaction 4"); + _Exit(EXIT_FAILURE); + } + + // Unsafe example 6 + struct sigaction act5 = {.sa_mask = sigset}; + act5.sa_flags = SA_SIGINFO; + act5.sa_sigaction = &unsafe_handler3; + if (sigaction(SIGSEGV, &act5, NULL) == -1) { + perror("sigaction 5"); + _Exit(EXIT_FAILURE); + } + + // Unsafe example 7 + sig_t wrapper = unsafe_handler; + if (signal(SIGALRM, wrapper) == SIG_ERR) { + perror("Unable to catch SIGALRM"); + _Exit(EXIT_FAILURE); + } + + // Unsafe example 8, indirect handler registration + if(register_signal(SIGALRM, df_handler)) { + _exit(EXIT_FAILURE); + } + return 0; } \ No newline at end of file diff --git a/cpp/test/query-tests/security/AsyncUnsafeSignalHandler/AsyncUnsafeSignalHandler.expected b/cpp/test/query-tests/security/AsyncUnsafeSignalHandler/AsyncUnsafeSignalHandler.expected index e51cab2..da13543 100644 --- a/cpp/test/query-tests/security/AsyncUnsafeSignalHandler/AsyncUnsafeSignalHandler.expected +++ b/cpp/test/query-tests/security/AsyncUnsafeSignalHandler/AsyncUnsafeSignalHandler.expected @@ -1,3 +1,8 @@ -| AsyncUnsafeSignalHandler.c:23:6:23:19 | unsafe_handler | is a non-trivial signal handler that may be using functions that are not async-safe. | -| AsyncUnsafeSignalHandler.c:27:6:27:20 | unsafe_handler2 | is a non-trivial signal handler that may be using functions that are not async-safe. | -| AsyncUnsafeSignalHandler.c:34:6:34:20 | unsafe_handler3 | is a non-trivial signal handler that may be using functions that are not async-safe. | +| AsyncUnsafeSignalHandler.c:32:6:32:19 | unsafe_handler | $@ is a non-trivial signal handler that uses not async-safe functions. Moreover, delivery of new signals may be not blocked. Handler is registered by $@. | AsyncUnsafeSignalHandler.c:32:6:32:19 | unsafe_handler | unsafe_handler | AsyncUnsafeSignalHandler.c:111:9:111:14 | call to signal | call to signal | +| AsyncUnsafeSignalHandler.c:32:6:32:19 | unsafe_handler | $@ is a non-trivial signal handler that uses not async-safe functions. Moreover, delivery of new signals may be not blocked. Handler is registered by $@. | AsyncUnsafeSignalHandler.c:32:6:32:19 | unsafe_handler | unsafe_handler | AsyncUnsafeSignalHandler.c:163:9:163:14 | call to signal | call to signal | +| AsyncUnsafeSignalHandler.c:36:6:36:20 | unsafe_handler2 | $@ is a non-trivial signal handler that uses not async-safe functions. Handler is registered by $@. | AsyncUnsafeSignalHandler.c:36:6:36:20 | unsafe_handler2 | unsafe_handler2 | AsyncUnsafeSignalHandler.c:139:9:139:17 | call to sigaction | call to sigaction | +| AsyncUnsafeSignalHandler.c:36:6:36:20 | unsafe_handler2 | $@ is a non-trivial signal handler that uses not async-safe functions. Moreover, delivery of new signals may be not blocked. Handler is registered by $@. | AsyncUnsafeSignalHandler.c:36:6:36:20 | unsafe_handler2 | unsafe_handler2 | AsyncUnsafeSignalHandler.c:121:9:121:17 | call to sigaction | call to sigaction | +| AsyncUnsafeSignalHandler.c:36:6:36:20 | unsafe_handler2 | $@ is a non-trivial signal handler that uses not async-safe functions. Moreover, delivery of new signals may be not blocked. Handler is registered by $@. | AsyncUnsafeSignalHandler.c:36:6:36:20 | unsafe_handler2 | unsafe_handler2 | AsyncUnsafeSignalHandler.c:128:9:128:17 | call to sigaction | call to sigaction | +| AsyncUnsafeSignalHandler.c:43:6:43:20 | unsafe_handler3 | $@ is a non-trivial signal handler that uses not async-safe functions. Handler is registered by $@. | AsyncUnsafeSignalHandler.c:43:6:43:20 | unsafe_handler3 | unsafe_handler3 | AsyncUnsafeSignalHandler.c:147:9:147:17 | call to sigaction | call to sigaction | +| AsyncUnsafeSignalHandler.c:43:6:43:20 | unsafe_handler3 | $@ is a non-trivial signal handler that uses not async-safe functions. Handler is registered by $@. | AsyncUnsafeSignalHandler.c:43:6:43:20 | unsafe_handler3 | unsafe_handler3 | AsyncUnsafeSignalHandler.c:156:9:156:17 | call to sigaction | call to sigaction | +| AsyncUnsafeSignalHandler.c:76:13:76:22 | df_handler | $@ is a non-trivial signal handler that uses not async-safe functions. Handler is registered by $@. | AsyncUnsafeSignalHandler.c:76:13:76:22 | df_handler | df_handler | AsyncUnsafeSignalHandler.c:88:6:88:14 | call to sigaction | call to sigaction |