Callander is a simple system sandboxing tool for Linux. It uses program analysis
to generate and apply right-sized seccomp sandboxes to programs. It can help you
isolate the effects of software that might have security issues, such as memory
corruption vulnerabilities. For many programs, using it is as simple as
prefixing your command with callander
.
Callander is different from other system call sandboxing tools in that it doesn't require any policy to be specified. Instead it analyzes any program it is asked to run to discover the exact calls, arguments, and call sites that the program could make. Callander uses this information to construct a precise policy limiting the program to only the operations the program could normally perform. These policies can be both more precise and more accurate than profiles assembled by hand or by observing program behavior. Even the most obscure error paths are analyzed and accounted for, unlike with observation-derived system call lists. Additionally, Callander waits for the process to initialize before applying a sandboxing policy, and thus can use a more restrictive policy that blocks calls that only occur during startup. This means many programs can run without the ability to forge executable memory mappings.
Information on how callander works was presented at All Day DevOps 2023. Slides and a recording of this talk are available.
- Download a .tgz from https://github.com/rpetrich/callander/releases/latest
- Extract with
tar xfz callander-*.tgz
or a graphical decompression utility - Run a command prefixed with the path to
callander
Prefix any command you wish to run sandboxed with callander
. Callander will
analyze, apply the sandbox, and run the program.
Add the --show-permitted
option to see which system calls callander has
discovered your command uses:
$ callander --show-permitted echo hi
callander: permitted syscalls: read(any u32, any, any) @ libc.so.6+0x1149d0(__read+16)
read(any u32, any, any) @ libc.so.6+0x114a0a(__read+74)
read(any u32, any, any) @ libc.so.6+0x119d16(__read_nocancel+6)
read(any, any, 1) @ libc.so.6+0x119d16(__read_nocancel+6)
write(any u32, any, any) @ libc.so.6+0x114a75(__write+21)
write(any u32, any, any) @ libc.so.6+0x114aad(__write+77)
write(any u32, any, any) @ libc.so.6+0x119d89(__write_nocancel+9)
close(any u32) @ libc.so.6+0x115155(__close+21)
close(any u32) @ libc.so.6+0x115179(__close+57)
close(any u32) @ libc.so.6+0x119a59(__close_nocancel+9)
lseek(any u32, any, any) @ libc.so.6+0x114b09(__lseek+9)
mmap(0, any, PROT_READ|PROT_WRITE, any u32, -1 as u32, 0) @ libc.so.6+0x11ec15(mmap+21)
mmap(NULL-0xfffffffffffffffe, any, PROT_NONE-PROT_READ, any u32, any u32, 0) @ libc.so.6+0x11ec15(mmap+21)
mmap(any, any, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_NORESERVE, -1 as u32, 0) @ libc.so.6+0x11ec15(mmap+21)
mmap(non-NULL, any, PROT_NONE, any u32, -1 as u32, 0) @ libc.so.6+0x11ec15(mmap+21)
mprotect(any, any, PROT_READ|PROT_WRITE) @ libc.so.6+0x11ec99(__mprotect+9)
munmap(any, any) @ libc.so.6+0x11ec69(__munmap+9)
brk(any) @ libc.so.6+0x11aa19(brk+9)
rt_sigaction(SIGABRT, any, 0, 8) @ libc.so.6+0x425f1(__libc_sigaction+193)
rt_sigprocmask(SIG_BLOCK, libc.so.6+0x1d2a60(sigall_set*), any, 8) @ libc.so.6+0x96933(pthread_kill+99)
rt_sigprocmask(SIG_SETMASK, any, 0, 8) @ libc.so.6+0x96980(pthread_kill+176)
rt_sigprocmask(SIG_UNBLOCK-SIG_SETMASK, any, 0, 8) @ libc.so.6+0x9bc19(pthread_sigmask+73)
rt_sigreturn(any) @ libc.so.6+0x42527
ioctl(any u32, TCGETS, any) @ libc.so.6+0x11a138(tcgetattr+40)
pread64(STDIN_FILENO-0xfffffffe, any, 511, any) @ libc.so.6+0x119d4c(__pread64_nocancel+12)
writev(any u32, any, any) @ ld-linux-x86-64.so.2+0xf5c2(_dl_debug_vdprintf*+258)
writev(STDERR_FILENO, any, 1) @ libc.so.6+0x89368(__libc_message.constprop.0*+232)
writev(STDERR_FILENO, any, any) @ libc.so.6+0x895db(__libc_message*+507)
sched_yield() @ libc.so.6+0x108c99(sched_yield+9)
mremap(any, any, any, MREMAP_MAYMOVE, any) @ libc.so.6+0x126b5d(mremap+45)
madvise(NULL-0xfffffffffffffffe, any, MADV_DONTNEED) @ libc.so.6+0x11ed69(madvise+9)
madvise(any, any, MADV_HUGEPAGE) @ libc.so.6+0x11ed69(madvise+9)
getpid() @ ld-linux-x86-64.so.2+0x26ee9(__GI___getpid*+9)
getpid() @ libc.so.6+0xec049(__getpid+9)
exit(0) @ libc.so.6+0x29db4(__libc_start_call_main*+164)
fcntl(any u32, F_SETFD, 1) @ libc.so.6+0x119ac3(__fcntl64_nocancel*+67)
fcntl(any u32, F_GETFL, any) @ libc.so.6+0x119ac3(__fcntl64_nocancel*+67)
getcwd(non-NULL, any) @ libc.so.6+0x115453(getcwd+147)
gettimeofday(any, any) @ [vdso]+0x7ff(gettimeofday+271)
sysinfo(any) @ libc.so.6+0x126fd9(sysinfo+9)
sched_getparam(any u32, any) @ libc.so.6+0x108c09(sched_getparam+9)
sched_setscheduler(any u32, any u32, any) @ libc.so.6+0x108c39(__sched_setscheduler+9)
sched_getscheduler(any u32) @ libc.so.6+0x108c69(sched_getscheduler+9)
sched_get_priority_max(1) @ libc.so.6+0x108cc9(sched_get_priority_max+9)
sched_get_priority_min(1) @ libc.so.6+0x108cf9(__sched_get_priority_min+9)
gettid() @ libc.so.6+0x969e5(pthread_kill+277)
futex(any, any u8, 1, 0, any u32, any)
futex(any, any u8, 1, 0, any, any u32)
futex(any, any u8, INT_MAX, 0, any, any)
futex(any, any u32, any u32, 0, 0, -1 as u32)
futex(any, FUTEX_WAIT|FUTEX_PRIVATE_FLAG, 2, 0, any, any)
futex(any, FUTEX_WAKE|FUTEX_PRIVATE_FLAG, 1, 0, any, any)
futex(libc.so.6+0x21aa6c(once*), FUTEX_WAIT|FUTEX_PRIVATE_FLAG, any u32, 0, 202, 2177)
futex(libc.so.6+0x227a80(once.0*), FUTEX_WAIT|FUTEX_PRIVATE_FLAG, any u32, 0, 202, 2177)
futex(ld-linux-x86-64.so.2+0x3aa48(_rtld_global+2568), FUTEX_WAIT, NULL-0xfff80002, 0, NULL-0xfff80000, NULL-0xfff80001)
futex(ld-linux-x86-64.so.2+0x3aa48(_rtld_global+2568), FUTEX_WAIT, 0x1-0xffffffff, 0, 0, ld-linux-x86-64.so.2+0x3aa68(_rtld_global+2600))
futex(ld-linux-x86-64.so.2+0x3aa48(_rtld_global+2568), FUTEX_WAIT, 0x1-0xffffffff, 0, 0x40000000, ld-linux-x86-64.so.2+0x3aa68(_rtld_global+2600))
futex(ld-linux-x86-64.so.2+0x3aa48(_rtld_global+2568), FUTEX_WAIT, 2, 0, 128, any)
futex(ld-linux-x86-64.so.2+0x3aa48(_rtld_global+2568), FUTEX_WAKE, 1, 0, any, any)
futex(ld-linux-x86-64.so.2+0x3aa48(_rtld_global+2568), FUTEX_LOCK_PI, 0, 0, 0, any)
futex(ld-linux-x86-64.so.2+0x3aa48(_rtld_global+2568), FUTEX_UNLOCK_PI, 0, 0, any, any)
futex(ld-linux-x86-64.so.2+0x3aa48(_rtld_global+2568), FUTEX_WAIT|FUTEX_PRIVATE_FLAG, NULL-0xfff80002, 0, NULL-0xfff80000, NULL-0xfff80001)
futex(ld-linux-x86-64.so.2+0x3aa48(_rtld_global+2568), FUTEX_LOCK_PI|FUTEX_PRIVATE_FLAG, 0, 0, 0, any)
futex(ld-linux-x86-64.so.2+0x3aa48(_rtld_global+2568), FUTEX_UNLOCK_PI|FUTEX_PRIVATE_FLAG, 0, 0, ld-linux-x86-64.so.2+0x3aa48(_rtld_global+2568), any)
sched_getaffinity(0, 0x1000, any) @ libc.so.6+0x121c56(__get_nprocs_sched*+54)
getdents64(any u32, any, any) @ libc.so.6+0xe6645(getdents64+21)
restart_syscall()
clock_gettime(CLOCK_REALTIME-CLOCK_MONOTONIC, any) @ libc.so.6+0xe5685(clock_gettime+53)
clock_gettime(any u32, any) @ [vdso]+0x99a(__vdso_clock_gettime+202)
clock_getres(any u32, any) @ [vdso]+0xb9e(__vdso_clock_getres+94)
clock_nanosleep(CLOCK_REALTIME, 0, any, any) @ libc.so.6+0xe5788(clock_nanosleep+88)
clock_nanosleep(CLOCK_REALTIME, 0, any, any) @ libc.so.6+0xe57f6(clock_nanosleep+198)
clock_nanosleep(CLOCK_MONOTONIC, 0, any, any) @ libc.so.6+0xe584d(clock_nanosleep+285)
clock_nanosleep(CLOCK_MONOTONIC, 0, any, any) @ libc.so.6+0xe58e1(clock_nanosleep+433)
exit_group(NULL-0x1fe) @ ld-linux-x86-64.so.2+0x26eaf(_exit*+31)
exit_group(2048) @ ld-linux-x86-64.so.2+0x26eaf(_exit*+31)
exit_group(any u32) @ libc.so.6+0xeac2f(_exit+47)
tgkill(any u32, any u32, SIGABRT) @ libc.so.6+0x969c9(pthread_kill+249)
tgkill(any u32, any u32, SIGFPE) @ libc.so.6+0x969c9(pthread_kill+249)
tgkill(any u32, any u32, SIGABRT) @ libc.so.6+0x969fa(pthread_kill+298)
tgkill(any u32, any u32, SIGFPE) @ libc.so.6+0x969fa(pthread_kill+298)
openat(AT_FDCWD, any, O_RDONLY|O_CLOEXEC, any) @ libc.so.6+0x114729(__open+89)
openat(AT_FDCWD, any, O_RDONLY|O_CLOEXEC, any) @ libc.so.6+0x1147a2(__open+210)
openat(any u32, libc.so.6+0x1da071, O_RDONLY, any) @ libc.so.6+0x1148a4(openat+68)
openat(any u32, libc.so.6+0x1da071, O_RDONLY, any) @ libc.so.6+0x114931(openat+209)
openat(AT_FDCWD, any, O_RDONLY|O_CLOEXEC, any) @ libc.so.6+0x119c0a(__open_nocancel+74)
openat(AT_FDCWD, non-NULL, O_RDONLY, any) @ libc.so.6+0x119c0a(__open_nocancel+74)
openat(AT_FDCWD, non-NULL, O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC, any) @ libc.so.6+0x119c0a(__open_nocancel+74)
openat(AT_FDCWD, libc.so.6+0x1db609, O_RDONLY|O_DIRECTORY, any) @ libc.so.6+0x119c0a(__open_nocancel+74)
newfstatat(any u32, any, any, AT_SYMLINK_NOFOLLOW) @ libc.so.6+0x113f2c(fstatat64+12)
newfstatat(any u32, libc.so.6+0x1d846f, any, AT_EMPTY_PATH) @ libc.so.6+0x113f2c(fstatat64+12)
newfstatat(AT_FDCWD, any, any, 0) @ libc.so.6+0x113f2c(fstatat64+12)
prlimit64(0, RLIMIT_NOFILE, 0, any) @ libc.so.6+0x11a4d2(getrlimit+18)
getrandom(libc.so.6+0x2204d8(tcache_key*), 8, GRND_NONBLOCK) @ libc.so.6+0x46c85(getrandom+21)
getrandom(libc.so.6+0x2204d8(tcache_key*), 8, GRND_NONBLOCK) @ libc.so.6+0x46cbd(getrandom+77)
hi
Programs that launch other programs are not supported by callander, due to the
limitations of seccomp. If asked to run a program that does exec
other
programs, callander will emit an error that can be overridden with the
--block-exec
option.
x86_64 and aarch64 linux binaries are available. No support for other architectures is planned.
Additionally, callander is beta software and requires more real-world testing. If you have a program that it doesn't analyze properly or rejects, please open an issue.
Callander's high-level architecture is a hybrid of patterns found in binary analysis tools, debuggers, and simple compilers.
flowchart
AnalyzeProgram --> CoalesceSyscalls
Launch(Launch\nCallander) --> ResolveProgram(Resolve Program Path) --> LoadProgram(Map Program into Memory) --> LoadLibraries
subgraph AnalyzeProgram [Analyze Program]
DisassembleInstructions -.-> |Discover Function\nPointer or Call| AnalyzeFunction
LoadLibraries(Load Dependent Libraries) --> AnalyzeDataSections
LoadLibraries -.->|Parse DT_NEEDED| LoadLibraries
AnalyzeDataSections(Scan Data Sections) -->|Discover\nFunction Pointer| AnalyzeFunction
LoadLibraries -->|Analyze Initializers| AnalyzeFunction
AnalyzeFunction(Analyze Function) --> DisassembleInstructions
DisassembleInstructions(Disassemble & Simulate\nInstructions) -->|Discover\nsyscall instruction| ExtractArgs(Extract\nSyscall Arguments) --> RecordSyscall(Record Syscall)
end
%%subgraph PrepareSeccomp [Prepare Seccomp]
CoalesceSyscalls(Coalesce\nSyscall List) --> GenerateSeccompProgram(Generate\nSeccomp Program) --> OptimizeSeccompProgram(Peephole Optimize\nand Split\nSeccomp Program) --> InjectSeccompProgram(Inject\nSeccomp Programs)
%%end
LoadProgram --> ForkChild
subgraph ChildProcess [Child Process]
ForkChild(Fork Child\nProcess) --> Ptrace(Ptrace\nChild Process) --> ExecAndPause(Exec Target Program\nand Pause in\nChild Process) --> SetBreakpoint(Set Breakpoint\non Main Function) --> ResumeChild(Resume and\nWait for Break) --> InjectSeccompProgram
end
LoadProgram -->|Locate entrypoint| AnalyzeFunction
InjectSeccompProgram --> ResumeProgram(Resume the now\nSandboxed\nChild Process)
Callander can be built by running make:
git clone git@github.com:rpetrich/callander.git
cd callander/src
make -j4
For best results, use a modern version of gcc to produce completely static binaries that run on any recent Linux kernel.
The logo was generously provided by my frequent collaborator, Kelly Shortridge. Without this and other contributions, Callander would not exist.
aarch64 disassembly uses Vector35's arch-arm64 disassembler.
x86 disassembly uses length disassembler by Stefan Johnson.