callander logo

Today I’m excited to open source Callander, a new tool for sandboxing x86-64 and ARM64 Linux programs.

Callander takes the guesswork out of system call sandboxing to make applying a sandbox as easy as prefixing your command with callander, as one might do with sudo to run a program as root. Other tools in this space require specifying the list of allowed system calls (“syscalls”), which is cumbersome and error-prone. Instead, Callander uses the power of binary analysis to automatically generate the list of allowed operations.

To be honest, no reasonable person should have attempted this. But I was frustrated by crafting sandboxing policies enough to descend into this rabbit hole until I willed a more civilized solution into existence.

This post describes how Callander works at a high level and walks through a demo to show how it can help us sandbox programs in practice (and also because I wouldn’t believe my claims either).

How Callander works

flowchart LR AnalyzeProgram --> CoalesceSyscalls Launch --> ResolveProgram(Resolve\nProgram\nBinary) --> LoadProgram(Map Binary\ninto Memory) --> LoadLibraries subgraph ChildProcess [Coordinate Child Process] ForkChild(Fork Child\nProcess) --> Ptrace(Ptrace\nChild Process) --> ExecAndPause(Exec Target\nProgram\nPaused) --> SetBreakpoint(Set Breakpoint\non Main Function) --> ResumeChild(Resume and\nWait for Break) --> ClearBreak(Clear\nBreakpoint) --> InjectSeccompProgram end subgraph PrepareSeccomp [Prepare Seccomp Sandbox] CoalesceSyscalls(Coalesce\nSyscall List) --> GenerateSeccompProgram(Generate\nSeccomp\nProgram) --> OptimizeSeccompProgram(Optimize\n+ Split\nSeccomp) --> InjectSeccompProgram(Inject\nSeccomp\nSandbox) end subgraph AnalyzeProgram [Analyze Target Program] DisassembleInstructions -.-> |Discover\nFunction\nPointer or Call| AnalyzeFunction LoadLibraries(Load\nShared\nObjects) --> AnalyzeDataSections LoadLibraries -.->|Parse DT_NEEDED| LoadLibraries AnalyzeDataSections(Scan\nData\nSections) -->|Discover\nFunction\nPointer| AnalyzeFunction LoadLibraries -->|Analyze\nInitializers| AnalyzeFunction AnalyzeFunction(Analyze Function) --> DisassembleInstructions DisassembleInstructions(Disassemble +\nSimulate\nInstructions) -->|Discover\nsyscall| ExtractArgs(Extract\nSyscall\nArgs) --> RecordSyscall(Record\nSyscall) end LoadProgram --> ForkChild LoadProgram -->|Locate\nentrypoint| AnalyzeFunction InjectSeccompProgram --> ResumeProgram(Resume\nSandboxed\nChild\nProcess)

Callander analyzes the target program by tracing through every possible execution path to build a complete list of system calls the program could ever issue. This means there’s no opportunity to make a mistake when specifying a policy, relieving most of our burden in achieving effective system call sandboxing. We no longer need to worry about our program crashing due to it missing some necessary syscall we forgot to specify in the policy. It offers better protection than custom fiddly system call profiles, and much better protection than the syscall sandboxing profiles provided in container runtimes.

We can use Callander to sandbox things like:

It makes many types of exploitation difficult or impossible, so you can be confident in exposing your program or service to untrusted input.

Precision

In addition to making system call sandboxing less error-prone, Callander’s analysis allows it to be more precise than any manually specified policy could ever be. It knows the call sites and range of potential arguments for each system call and encodes this information into the sandbox policy it asks the Linux kernel to apply.

Sysfilter pioneered the use of static analysis to derive system call lists, with much less precision. Using Callander’s precise analysis-derived policy, the kernel will enforce not only what operations the process can perform, but what operations with what parameters and from what specific call addresses in the program. If a program only reads from files, then any attacker successfully exploiting it won’t be able to make file writes. Alternative tools offer limited ability to specify arguments, and no ability to restrict call sites.

Callander even discovers what parts of the program only run at startup to initialize the program and will wait until initialization completes before applying the policy. This means it can block all of the system calls that are used only during program initialization and never again for the rest of a program’s lifetime. Programs and especially the operating system’s dynamic loader perform all sorts of sensitive operations to initialize, so eliding these operations from the policy is critical.

Caveats

Callander is most effective on programs that compile to native code such as C, C++, Rust, and Go. It also works with interpreted languages like Python and JavaScript, but it is less effective at blocking unused system calls than with native code. These runtimes generally expose a large set of facilities to the programs running inside them, and Callander can’t see into the inner interpreted language to understand which facilities are actually used.

Although Callander does not work on programs that execute other programs, such as build systems and shells, it will alert on and reject requests to sandbox one, with an option to override.

Demo

At All Day DevOps 2023, I demonstrated Callander protecting a standard, out-of-date version of Nginx that was vulnerable to CVE-2013-2028, a long-public stack buffer overflow bug. A recording of the talk is available to watch. The demonstration exploit is fairly standard for a stack overflow bug: it uses return-oriented programming to write some shellcode into the process, makes it executable, jumps to the shellcode, and then the shellcode performs some syscalls to execute a shell.

Running the exploit against an unsandboxed Nginx produces a shell for the attacker as expected. If we instead run the exploit against a Callander-sandboxed Nginx, the kernel will terminate the exploited Nginx process as soon as it attempts to make the shellcode executable. Callander understands that Nginx never maps executable pages after glibc finishes initializing Nginx’s shared objects, and asks the kernel to reject any mmap and mprotect syscalls with the PROT_EXEC option set.

callander demo animated gif

Without any prior knowledge of Nginx or the exploit, Callander effectively blocks the attack. We frustrate the attacker and force them to reformulate their plan – or abandon us for a different target. And it only requires typing callander nginx to gain this protection.

A note for exploit-minded people

The attacker types reading this might object that an attacker could implement their entire attack in return-oriented programming to avoid shellcode entirely. This is true; attackers don’t strictly need shellcode, but even assembly shellcode is much less cumbersome to write than return-oriented programming. We’re still raising the cost of attack by preventing attackers from using this well-loved mechanism, and ideally confusing and frustrating them, too.

Even assuming an attacker can build a crafty exploit that does everything via ROP, they are still limited to the syscall patterns performed by the target program. If any deviation from the program’s intended behavior arises, Callander’s seccomp sandbox will reject them.

As an industry, we should prefer techniques that make work much more difficult for attackers, with only minimal cost to defenders and system operators. With Callander you can add security to existing systems, without imposing a ton of effort and overhead on engineers.

Going forward

Building Callander and making it fast required discovering new program analysis techniques, and careful curation and implementation of existing ones. I will be writing about them as I can carve time away from the new rabbit holes I’m exploring.

In the meantime, I’m excited for you to try Callander out. Let me know what you’re sandboxing with it and what enhancements might make it more useful to you.

Special Thanks

Kelly Shortridge, a frequent collaborator, graciously provided the logo, detailed feedback on this post, and general guidance on Callander itself.