diff --git a/infra/experimental/sanitizers/ExecSan/Makefile b/infra/experimental/sanitizers/ExecSan/Makefile index 568f69fe3..079a76147 100644 --- a/infra/experimental/sanitizers/ExecSan/Makefile +++ b/infra/experimental/sanitizers/ExecSan/Makefile @@ -2,7 +2,7 @@ CXX = clang++ CFLAGS = -std=c++17 -Wall -Wextra -O3 -g3 -all: clean execSan tripwire target +all: clean execSan target execSan: execSan.cpp $(CXX) $(CFLAGS) -lpthread -o $@ $^ @@ -14,4 +14,4 @@ test: all vuln.dict ./execSan ./target -dict=vuln.dict clean: - rm -f execSan /tmp/tripwire target + rm -f execSan /tmp/tripwire target diff --git a/infra/experimental/sanitizers/ExecSan/README.md b/infra/experimental/sanitizers/ExecSan/README.md index ef910b9e4..5a96cc1ea 100644 --- a/infra/experimental/sanitizers/ExecSan/README.md +++ b/infra/experimental/sanitizers/ExecSan/README.md @@ -22,14 +22,18 @@ Note this will overwrite /tmp/tripwire if it exists. make test ``` -Look for the following line: +Look for one of the following lines: > ===BUG DETECTED: Shell injection=== -which indicates the detection of shell injection. +which indicates the detection of executing the planted `/tmp/tripwire`. + + +> ===BUG DETECTED: Shell corruption=== + +which indicates the detection of executing a syntactic erroneous command. ## TODOs -1. Flag syntax errors of shell commands by hooking write() calls from shell. -2. Find real examples of past shell injection vulnerabilities using this. +1. Find real examples of past shell injection vulnerabilities using this. diff --git a/infra/experimental/sanitizers/ExecSan/execSan.cpp b/infra/experimental/sanitizers/ExecSan/execSan.cpp index 6c89541d4..68184d363 100644 --- a/infra/experimental/sanitizers/ExecSan/execSan.cpp +++ b/infra/experimental/sanitizers/ExecSan/execSan.cpp @@ -17,49 +17,108 @@ /* C standard library */ #include +#include #include #include -#include /* POSIX */ -#include #include #include #include +#include /* Linux */ -#include #include +#include #include -#include +#include #include +#include +#include +#include #include #define DEBUG_LOGS 0 #if DEBUG_LOGS -#define debug_log(...) \ - do { \ - fprintf(stderr, __VA_ARGS__); fflush(stdout); \ - fputc('\n', stderr); \ +#define debug_log(...) \ + do { \ + fprintf(stderr, __VA_ARGS__); \ + fflush(stdout); \ + fputc('\n', stderr); \ } while (0) #else #define debug_log(...) #endif -#define fatal_log(...) \ - do { \ +#define fatal_log(...) \ + do { \ fprintf(stderr, __VA_ARGS__); \ - fputc('\n', stderr); \ - exit(EXIT_FAILURE); \ + fputc('\n', stderr); \ + exit(EXIT_FAILURE); \ } while (0) // The magic string that we'll use to detect full control over the command // executed. const std::string kTripWire = "/tmp/tripwire"; +// Shell injection bug confirmed with /tmp/tripwire. +const std::string kInjectionError = "Shell injection"; +// Shell corruption bug detected based on syntax error. +const std::string kCorruptionError = "Shell corruption"; + // The PID of the root process we're fuzzing. pid_t g_root_pid; +// Assuming the longest pathname is "/bin/bash". +constexpr int kShellPathnameLength = 20; + +// Syntax error messages of each shell. +const std::map> kShellSyntaxErrors = { + {"bash", + { + ": command not found", // General + ": syntax error", // Unfinished " or ' or ` or if, leading | or ; + ": missing `]'", // Unfinished [ + ": event not found", // ! leads large numbers + ": No such file or directory", // Leading < or / + }}, + {"csh", + { + ": Command not found.", // General + ": Missing }.", // Unfinished { + "Too many ('s.", // Unfinished ( + "Invalid null command.", // Leading | or < or > + "Missing name for redirect.", // Single < or > + ": No match.", // Leading ? or [ or * + "Modifier failed.", // Leading ^ + "No previous left hand side.", // A ^ + ": No such job.", // Leading % + ": No current job.", // A % + ": Undefined variable.", // Containing $ + ": Event not found.", // ! leads large numbers + // TODO: Make this more specific. + "Unmatched", // Unfinished " or ' or `, leading ; + }}, + {"dash", + { + ": not found", // General + ": Syntax error", // Unfinished " or ' or ` or if, leading | or ; or & + ": missing ]", // Unfinished [ + ": No such file", // Leading < + }}, + {"zsh", + { + ": command not found", // General + ": syntax error", // Unfinished " or ' or ` + ": ']' expected", // Unfinished [ + ": no such file or directory", // Leading < or / + ": parse error near", // Leading |, or & + ": no such user or named directory", // Leading ~ + }}, +}; + +// Shells used by Processes. +std::map g_shell_pids; struct Tracee { pid_t pid; @@ -82,7 +141,8 @@ pid_t run_child(char **argv) { return pid; } -std::vector read_memory(pid_t pid, unsigned long long address, size_t size) { +std::vector read_memory(pid_t pid, unsigned long long address, + size_t size) { std::vector memory; for (size_t i = 0; i < size; i += sizeof(long)) { @@ -91,33 +151,122 @@ std::vector read_memory(pid_t pid, unsigned long long address, size_t return memory; } - std::byte *word_bytes = reinterpret_cast(&word); - memory.insert(memory.end(), word_bytes, word_bytes+sizeof(long)); + std::byte *word_bytes = reinterpret_cast(&word); + memory.insert(memory.end(), word_bytes, word_bytes + sizeof(long)); } return memory; } -void inspect(pid_t pid, const user_regs_struct ®s) { - auto memory = read_memory(pid, regs.rdi, kTripWire.length()); - if (memory.size() == 0) { - return; +// Construct a string with the memory specified in a register. +std::string read_string(pid_t pid, unsigned long reg, unsigned long length) { + auto memory = read_memory(pid, reg, length); + if (!memory.size()) { + return ""; } - std::string path(reinterpret_cast( - memory.data()), std::min(memory.size(), kTripWire.length())); + std::string content(reinterpret_cast(memory.data()), + std::min(memory.size(), length)); + return content; +} + +void report_bug(std::string bug_type) { + // Report the bug found based on the bug code. + std::cerr << "===BUG DETECTED: " << bug_type.c_str() << "===\n"; + // Rely on sanitizers/libFuzzer to produce a stacktrace by sending SIGABRT + // to the root process. + // Note: this may not be reliable or consistent if shell injection happens + // in an async way. + kill(g_root_pid, SIGABRT); + _exit(0); +} + +void inspect_for_injection(pid_t pid, const user_regs_struct ®s) { + // Inspect a PID's registers for the sign of shell injection. + std::string path = read_string(pid, regs.rdi, kTripWire.length()); + if (!path.length()) { + return; + } debug_log("inspecting"); if (path == kTripWire) { - fprintf(stderr, "===BUG DETECTED: Shell injection===\n"); - // Rely on sanitizers/libFuzzer to produce a stacktrace by sending SIGABRT - // to the root process. - // Note: this may not be reliable or consistent if shell injection happens - // in an async way. - kill(g_root_pid, SIGABRT); - _exit(0); + report_bug(kInjectionError); } } +std::string get_pathname(pid_t pid, const user_regs_struct ®s) { + // Parse the pathname from the memory specified in the RDI register. + std::string pathname = read_string(pid, regs.rdi, kShellPathnameLength); + debug_log("Pathname is %s (len %lu)\n", pathname.c_str(), pathname.length()); + return pathname; +} + +std::string match_shell(std::string binary_pathname); + +// Identify the exact shell behind sh +std::string identify_sh(std::string binary_name) { + char shell_pathname[kShellPathnameLength]; + if (readlink(binary_name.c_str(), shell_pathname, kShellPathnameLength) == + -1) { + std::cerr << "Cannot query which shell is behind sh: readlink failed\n"; + std::cerr << "Assuming the shell is dash\n"; + return "dash"; + } + debug_log("sh links to %s\n", shell_pathname); + std::string shell_pathname_str(shell_pathname); + + return match_shell(shell_pathname_str); +} + +std::string match_shell(std::string binary_pathname) { + // Identify the name of the shell used in the pathname. + if (!binary_pathname.length()) { + return ""; + } + for (const auto &item : kShellSyntaxErrors) { + std::string known_shell = item.first; + std::string binary_name = binary_pathname.substr( + binary_pathname.find_last_of("/") + 1, known_shell.length()); + debug_log("Binary is %s (%lu)\n", binary_name.c_str(), + binary_name.length()); + if (!binary_name.compare(0, 2, "sh")) { + debug_log("Matched sh: Needs to identify which specific shell it is.\n"); + return identify_sh(binary_pathname); + } + if (binary_name == known_shell) { + debug_log("Matched %s\n", binary_name.c_str()); + return known_shell; + } + } + return ""; +} + +std::string get_shell(pid_t pid, const user_regs_struct ®s) { + // Get shell name used in a process. + std::string binary_pathname = get_pathname(pid, regs); + return match_shell(binary_pathname); +} + +void match_error_pattern(std::string buffer, std::string shell) { + auto error_patterns = kShellSyntaxErrors.at(shell); + for (const auto &pattern : error_patterns) { + debug_log("Pattern : %s\n", pattern.c_str()); + debug_log("Found at: %lu\n", buffer.find(pattern)); + if (buffer.find(pattern) != std::string::npos) { + std::cerr << "--- Found a sign of shell corruption ---\n" + << buffer.c_str() + << "\n----------------------------------------\n"; + report_bug(kCorruptionError); + } + } +} + +void inspect_for_corruption(pid_t pid, const user_regs_struct ®s) { + // Inspect a PID's registers for shell corruption. + std::string buffer = read_string(pid, regs.rsi, regs.rdx); + debug_log("Write buffer: %s\n", buffer.c_str()); + match_error_pattern(buffer, g_shell_pids[pid]); +} + void trace(std::map pids) { while (!pids.empty()) { std::vector new_pids; @@ -144,13 +293,16 @@ void trace(std::map pids) { debug_log("finished waiting %d", pid); if (WIFEXITED(status) || WIFSIGNALED(status)) { - debug_log("%d exited", pid); + debug_log("%d exited", pid); it = pids.erase(it); + // Remove pid from the watchlist when it exits + g_shell_pids.erase(pid); continue; } // ptrace sets 0x80 for syscalls (with PTRACE_O_TRACESYSGOOD set). - bool is_syscall = WIFSTOPPED(status) && WSTOPSIG(status) == (SIGTRAP | 0x80); + bool is_syscall = + WIFSTOPPED(status) && WSTOPSIG(status) == (SIGTRAP | 0x80); int sig = 0; if (!is_syscall) { // Handle generic signal. @@ -163,10 +315,10 @@ void trace(std::map pids) { debug_log("forwarding signal %d to %d", sig, pid); } - if (WIFSTOPPED(status) && - (status>>8 == (SIGTRAP | (PTRACE_EVENT_CLONE<<8)) || - status>>8 == (SIGTRAP | (PTRACE_EVENT_FORK<<8)) || - status>>8 == (SIGTRAP | (PTRACE_EVENT_VFORK<<8)))) { + if (WIFSTOPPED(status) && + (status >> 8 == (SIGTRAP | (PTRACE_EVENT_CLONE << 8)) || + status >> 8 == (SIGTRAP | (PTRACE_EVENT_FORK << 8)) || + status >> 8 == (SIGTRAP | (PTRACE_EVENT_VFORK << 8)))) { long new_pid; if (ptrace(PTRACE_GETEVENTMSG, pid, 0, &new_pid) == -1) { debug_log("ptrace(PTRACE_GETEVENTMSG, %d): %s", pid, strerror(errno)); @@ -185,7 +337,19 @@ void trace(std::map pids) { if (tracee.syscall_enter) { if (regs.orig_rax == __NR_execve) { - inspect(pid, regs); + inspect_for_injection(pid, regs); + std::string shell = get_shell(pid, regs); + if (shell != "") { + debug_log("Shell parsed: %s", shell.c_str()); + g_shell_pids.insert(std::make_pair(pid, shell)); + } + } + + if (regs.orig_rax == __NR_write && + g_shell_pids.find(pid) != g_shell_pids.end()) { + debug_log("Inspecting the `write` buffer of shell process %d.", + pid); + inspect_for_corruption(pid, regs); } } @@ -225,11 +389,8 @@ int main(int argc, char **argv) { pid_t pid = run_child(argv + 1); - long options = - PTRACE_O_TRACESYSGOOD - | PTRACE_O_TRACEFORK - | PTRACE_O_TRACEVFORK - | PTRACE_O_TRACECLONE; + long options = PTRACE_O_TRACESYSGOOD | PTRACE_O_TRACEFORK | + PTRACE_O_TRACEVFORK | PTRACE_O_TRACECLONE; if (ptrace(PTRACE_SEIZE, pid, nullptr, options) == -1) { fatal_log("ptrace(PTRACE_SEIZE): %s", strerror(errno));