libseccomp icon indicating copy to clipboard operation
libseccomp copied to clipboard

Q: use a whitelist, and notify when the process tries to use a syscall that is not on the whitelist

Open godalming123 opened this issue 2 years ago • 8 comments

Hi, I was trying to develop a sandbox application where processes start with the bare minimum allowed syscalls to operate (read, write, exit, sigreturn) and then when they try to access more system calls, the user is notified and can either allow access or deny access and kill the process.

Currently I have the follwoing zig code:

const std = @import("std");
const c = @cImport({
    // See https://github.com/ziglang/zig/issues/515
    @cDefine("_NO_CRT_STDIO_INLINE", "1");
    @cInclude("seccomp.h");
});

/// Applies a seccomp filter where every syscall is disallowed, except the `allowed_syscalls`, and if the process violates this then it will be killed.
fn apply_seccomp_filter(comptime allowed_syscalls: []const c_int) !void {
    var ctx = c.seccomp_init(c.SCMP_ACT_NOTIFY);
    if (ctx == null) {
        return error.FailedToInitialiseSeccomp;
    }
    defer c.seccomp_release(ctx);

    for (allowed_syscalls) |allowed_syscall| {
        if (c.seccomp_rule_add_exact(ctx, c.SCMP_ACT_ALLOW, allowed_syscall, 0) != 0) {
            return error.FailedToAddSeccompRules;
        }
    }

    if (c.seccomp_load(ctx) != 0) {
        return error.FailedToLoadSeccompRules;
    }
}

pub fn main() !void {
    try apply_seccomp_filter(&[_]c_int{
        c.__NR_read,
    });

    // Test if the seccomp filter has denied every syscall except read.
    std.debug.print("Testing123", .{});
}

godalming123 avatar Nov 05 '23 14:11 godalming123

Hi @godalming123,

As a FYI, we don't provide Zig language bindings so the amount of help we can provide may be limited, but we'll try. Beyond that, I'm not clear what you are asking about in this issue, can you rephrase your question?

pcmoore avatar Nov 05 '23 22:11 pcmoore

@pcmoore Firstly, zig has built in c/c++ interop, and so far, I've managed to get a program that restricts the syscalls and then dies because it uses a restricted syscall in zig. And if you feel more comfortable giving me C examples, then go ahead.

In terms of what I want, I want to create a whitelist syscall filter that a sandboxed program can use, and then when the sandboxed program tries to use a syscall that isn't in the whitelist, trigger a callback so that the main program can ask the user if they want to allow the sandboxed program access to the syscall.

Something like:

Sandbox /bin/echo Hello world
/bin/echo would like access to the write syscall (allow/deny): allow
Hello world

godalming123 avatar Nov 06 '23 08:11 godalming123

@pcmoore Is this possible, and if so how can it be done? I don't mind if this isn't possible, I just want a response please.

godalming123 avatar Nov 18 '23 07:11 godalming123

You have to use ACT_NOTIFY, as you already did in your OP, to get user space callbacks.

Also you must make sure that the program can not spoof user input and self-approve those requests.

rusty-snake avatar Nov 18 '23 16:11 rusty-snake

Hi @godalming123, are you satisfied with @rusty-snake's answer above?

pcmoore avatar Dec 08 '23 23:12 pcmoore

@pcmoore rusty-snakes answer helped, but I still can't get it too work, so far I have the following:

const std = @import("std");
const c = @cImport({
    @cInclude("seccomp.h");
});

pub fn main() !void {
    // Initialise libseccomp
    const ctx = c.seccomp_init(c.SCMP_ACT_NOTIFY);
    if (ctx == null) {
        return error.FailedToInitialiseSeccomp;
    }
    defer c.seccomp_release(ctx);

    // Apply seccomp filters
    // TODO: add any other filters that are needed
    if (c.seccomp_rule_add_exact(ctx, c.SCMP_ACT_ALLOW, c.__NR_read, 0) != 0) {
        return error.FailedToAllowReadSyscall;
    }
    if (c.seccomp_rule_add_exact(ctx, c.SCMP_ACT_ALLOW, c.__NR_write, 0) != 0) {
        return error.FailedToAllowWriteSyscall;
    }

    // Load seccomp filters
    if (c.seccomp_load(ctx) < 0) {
        return error.FailedToLoadSeccompRules;
    }

    // Setup listener for a notify event
    const fd = c.seccomp_notify_fd(ctx);
    if (fd < 0) {
        return error.FailedToFindSeccompNotifyFileDiscriptor;
    }
    std.os.close(fd);

    // Fork
    const pid = try std.os.fork();
    if (pid == 0) { // Child process for running the specified command
        return std.os.execvpeZ(std.os.argv[1], @ptrCast(&std.os.argv[1..]), &[_:null]?[*:0]u8{null});
    } else { // Parent process for listening if the child process uses a syscall other then `read` or `write`
        // Allocate structs for request and response
        var req: ?*c.seccomp_notif = null;
        var resp: ?*c.seccomp_notif_resp = null;
        if (c.seccomp_notify_alloc(&req, &resp) != 0) {
            return error.FailedToAllocateNotify;
        }

        // Loop for listening to notify events
        while (true) {
            if (c.seccomp_notify_receive(fd, req) != 0) {
                return error.FailedToRecieveNotify;
            }

            std.debug.print("Seccomp process tried to use syscall number: {}.", .{req.?.data.nr});

            if (c.seccomp_notify_id_valid(fd, req.?.id) != 0) {
                return error.SeccompNotifyIdIsNotValid;
            }

            if (c.seccomp_notify_respond(fd, null) != 0) {
                return error.FailedToRespondToSeccompNotifySyscall;
            }
        }
    }
}

If I run the compiled binary like so: sandpack sh Then I just get a blank console, and do not see any text on the screen saying that the command tried to acess any syscalls. Do you have any ideas as to why this might not be working, or any code fixes because I am not confident in this programs memory safety? Much appreciated.

godalming123 avatar Dec 09 '23 08:12 godalming123

You create a seccomp filter that only allows read(2) and write(2). Every other syscall will trigger a userspace callback and block the calling thread indefinitely (ignoring signals and the like). Then you load this seccomp filter. In other words you activate it. After that, you try to get the unotify fd (ioctl(2), fork(2), execve(2), mmap(2)?, .... All those syscalls will not get executed.

rusty-snake avatar Dec 09 '23 11:12 rusty-snake

@rusty-snake thank you for the explanation as to why my code does not work. Do you have a way to implement this functionality that does work? If you move the part of the code that gets the file descriptor before the code that loads the filter, then libseccomp returns a negative file descriptor which (correct me if I'm wrong) signals an error.

godalming123 avatar Dec 11 '23 20:12 godalming123