Detect shell injection based on syntax errors (#7795)

* Remove redundant tripwire from Makefile

* Detect shell corruption based on syntax errors

* Type, name, format, typo, etc.

* Error pattern matching logic

* clang-format

* Code structure fix

* Extend the pathname length of shell to be safe

* Remove redundant operations on memory read from regs

* More specific patterns

* Identify sh

* Remove redudant substr

* Document shell corruption in README.md

* Clang-format

* Organise printf/debug_log/cerr

* Remove a completed TODO

* Use readlink instead of `file`

* Clang-format
This commit is contained in:
Dongge Liu 2022-06-06 14:14:01 +10:00 committed by GitHub
parent ae83c88a54
commit 62f034e81d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 211 additions and 46 deletions

View File

@ -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

View File

@ -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.

View File

@ -17,49 +17,108 @@
/* C standard library */
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <signal.h>
/* POSIX */
#include <unistd.h>
#include <sys/stat.h>
#include <sys/user.h>
#include <sys/wait.h>
#include <unistd.h>
/* Linux */
#include <syscall.h>
#include <sys/ptrace.h>
#include <syscall.h>
#include <fstream>
#include <string>
#include <iostream>
#include <map>
#include <set>
#include <sstream>
#include <string>
#include <vector>
#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<std::string, std::set<std::string>> 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<pid_t, std::string> g_shell_pids;
struct Tracee {
pid_t pid;
@ -82,7 +141,8 @@ pid_t run_child(char **argv) {
return pid;
}
std::vector<std::byte> read_memory(pid_t pid, unsigned long long address, size_t size) {
std::vector<std::byte> read_memory(pid_t pid, unsigned long long address,
size_t size) {
std::vector<std::byte> memory;
for (size_t i = 0; i < size; i += sizeof(long)) {
@ -91,33 +151,122 @@ std::vector<std::byte> read_memory(pid_t pid, unsigned long long address, size_t
return memory;
}
std::byte *word_bytes = reinterpret_cast<std::byte*>(&word);
memory.insert(memory.end(), word_bytes, word_bytes+sizeof(long));
std::byte *word_bytes = reinterpret_cast<std::byte *>(&word);
memory.insert(memory.end(), word_bytes, word_bytes + sizeof(long));
}
return memory;
}
void inspect(pid_t pid, const user_regs_struct &regs) {
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<char*>(
memory.data()), std::min(memory.size(), kTripWire.length()));
std::string content(reinterpret_cast<char *>(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 &regs) {
// 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 &regs) {
// 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 &regs) {
// 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 &regs) {
// 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<pid_t, Tracee> pids) {
while (!pids.empty()) {
std::vector<pid_t> new_pids;
@ -144,13 +293,16 @@ void trace(std::map<pid_t, Tracee> 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<pid_t, Tracee> 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<pid_t, Tracee> 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));