Skip to content

Instantly share code, notes, and snippets.

@bburky
Last active October 24, 2023 12:36
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bburky/cfa97a45e16fa0528f67dc9f31adc51e to your computer and use it in GitHub Desktop.
Save bburky/cfa97a45e16fa0528f67dc9f31adc51e to your computer and use it in GitHub Desktop.
SECCOMP_RET_USER_NOTIF based Frida syscall tracer

Proof of concept SECCOMP_RET_USER_NOTIF based Frida syscall tracer

A hacked up version of https://man7.org/tlpi/code/online/dist/seccomp/seccomp_user_notification.c.html running inside Frida.

installFilter() should be called on the main thread of the application. It's not possible to install the seccomp filter from rpc.exports.init() because it runs on a Frida thread.

installFilter() sets NO_NEW_PRIVS (required if non-root), installs the seccomp filter to trigger notifications, then creates a pthread to watch for notifications. Upon notifications a callback into Frida is invoked.

When the callback fires, it won't be on the thread that invoked the syscall. I'm not actually sure how to use Frida interact with the suspended thread. Don't know how to get a backtrace on it or execute code on it. Might be possible to set a temporary interceptor on it's EIP.

TODO

  • Test on Android?
    • External toolchain isn't going to work. Would need to move code into Frida itself or rewrite to use less external functions. (Or maybe just run an external preprocessor on the CModule first?)
    • BPF code is also different for each architecture
    • I'm not accually sure if the seccomp syscall is allowed on Android normally. I think it has a default seccomp profile for apps.
  • Figure out the (spurious?) Tracer: ioctlSECCOMP_IOCTL_NOTIF_RECV: Invalid argument messsage after completion
  • For better or worse, I think this also traces child processes. Maybe detect children and (optionally) ignore them?

Test on x86_64 Linux with:

frida  -C test.c --toolchain=external -l test.js --no-pause -f /bin/mkdir -- /tmp/test123
#pragma GCC diagnostic warning "-Wunused-function"
#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/prctl.h>
#include <fcntl.h>
#include <limits.h>
#include <signal.h>
#include <sys/wait.h>
#include <stddef.h>
#include <stdbool.h>
#include <linux/audit.h>
#include <sys/syscall.h>
#include <sys/stat.h>
#include <linux/filter.h>
#include <linux/seccomp.h>
#include <sys/ioctl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <pthread.h>
#include <gum/guminterceptor.h>
extern int notifyFd;
extern void onMessage(char*);
#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \
} while (0)
#define X32_SYSCALL_BIT 0x40000000
#define X86_64_CHECK_ARCH_AND_LOAD_SYSCALL_NR \
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, \
(offsetof(struct seccomp_data, arch))), \
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, AUDIT_ARCH_X86_64, 0, 2), \
BPF_STMT(BPF_LD | BPF_W | BPF_ABS, \
(offsetof(struct seccomp_data, nr))), \
BPF_JUMP(BPF_JMP | BPF_JGE | BPF_K, X32_SYSCALL_BIT, 0, 1), \
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL_PROCESS)
static int
seccomp(unsigned int operation, unsigned int flags, void *args)
{
return syscall(__NR_seccomp, operation, flags, args);
}
int
installNotifyFilter(void)
{
struct sock_filter filter[] = {
X86_64_CHECK_ARCH_AND_LOAD_SYSCALL_NR,
/* mkdir() triggers notification to user-space tracer */
BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_mkdir, 0, 1),
BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_USER_NOTIF),
/* Every other system call is allowed */
BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
};
struct sock_fprog prog = {
.len = (unsigned short) (sizeof(filter) / sizeof(filter[0])),
.filter = filter,
};
int notifyFd = seccomp(SECCOMP_SET_MODE_FILTER,
SECCOMP_FILTER_FLAG_NEW_LISTENER, &prog);
if (notifyFd == -1)
errExit("seccomp-install-notify-filter");
return notifyFd;
}
static void
checkNotificationIdIsValid(int notifyFd, __u64 id, char *tag)
{
if (ioctl(notifyFd, SECCOMP_IOCTL_NOTIF_ID_VALID, &id) == -1) {
fprintf(stderr, "Tracer: notification ID check (%s): "
"target has died!!!!!!!!!!!\n", tag);
}
}
void *
// watchForNotifications(int notifyFd, struct cmdLineOpts *opts)
watchForNotifications()
{
struct seccomp_notif *req;
struct seccomp_notif_resp *resp;
struct seccomp_notif_sizes sizes;
char path[PATH_MAX];
int procMem; /* FD for /proc/PID/mem of target process */
/* Discover the sizes of the structures that are used to receive
notifications and send notification responses, and allocate
buffers of those sizes. */
if (seccomp(SECCOMP_GET_NOTIF_SIZES, 0, &sizes) == -1)
errExit("Tracer: seccomp-SECCOMP_GET_NOTIF_SIZES");
req = malloc(sizes.seccomp_notif);
if (req == NULL)
errExit("Tracer: malloc");
resp = malloc(sizes.seccomp_notif_resp);
if (resp == NULL)
errExit("Tracer: malloc");
/* Loop handling notifications */
for (;;) {
/* Wait for next notification, returning info in '*req' */
if (ioctl(notifyFd, SECCOMP_IOCTL_NOTIF_RECV, req) == -1)
errExit("Tracer: ioctlSECCOMP_IOCTL_NOTIF_RECV");
printf("Tracer: got notification for PID %d; ID is %llx\n",
req->pid, req->id);
/* If a delay interval was specified on the command line, then delay
for the specified number of seconds. This can be used to demonstrate
the following:
(1) The target process is blocked until the tracer sends a response.
(2) If the blocked system call is interrupted by a signal handler,
then the SECCOMP_IOCTL_NOTIF_SEND operation fails with the error
ENOENT.
(3) If the target process terminates, then we can discover this
using the SECCOMP_IOCTL_NOTIF_ID_VALID operation (which is
employed by checkNotificationIdIsValid()). */
// if (opts->delaySecs > 0) {
// printf("Tracer: delaying for %d seconds:", opts->delaySecs);
// checkNotificationIdIsValid(notifyFd, req->id, "pre-delay");
// for (int d = opts->delaySecs; d > 0; d--) {
// printf(" %d", d);
// sleep(1);
// }
// printf("\n");
// checkNotificationIdIsValid(notifyFd, req->id, "post-delay");
// }
/* Access the memory of the target process in order to discover
the pathname that was given to mkdir() */
snprintf(path, sizeof(path), "/proc/%d/mem", req->pid);
procMem = open(path, O_RDONLY);
if (procMem == -1)
errExit("Tracer: open");
/* Check that the process whose info we are accessing is still alive */
checkNotificationIdIsValid(notifyFd, req->id, "post-open");
/* Since, the SECCOMP_IOCTL_NOTIF_ID_VALID operation (performed in
checkNotificationIdIsValid()) succeeded, we know that the
/proc/PID/mem file descriptor that we opened corresponded to the
process for which we received a notification. If that process
subsequently terminates, then read() on that file descriptor will
return 0 (EOF). This can be tested by (1) uncommenting the sleep()
call below (and rebuilding the program); (2) running the program
with flags to ensure that the tracer is not killed if the target
dies; and (3) killing the target process during the sleep(). */
// printf("About to sleep in target\n");
// sleep(15);
/* Seek to the location containing the pathname argument (i.e., the
first argument) of the mkdir(2) call and read that pathname */
if (lseek(procMem, req->data.args[0], SEEK_SET) == -1)
errExit("Tracer: lseek");
ssize_t s = read(procMem, path, sizeof(path));
if (s == -1)
errExit("read");
else if (s == 0) {
fprintf(stderr, "Tracer: read returned EOF\n");
exit(EXIT_FAILURE);
}
printf("Tracer: mkdir(\"%s\", %llo)\n", path, req->data.args[1]);
// Callback to Frida
onMessage(path);
if (close(procMem) == -1)
errExit("close-/proc/PID/mem");
/* The response to the notification includes the notification ID */
resp->id = req->id;
// resp->flags = 0; /* Must be zero as at Linux 5.0 */
// Allow the syscall to continue
resp->flags = SECCOMP_USER_NOTIF_FLAG_CONTINUE;
/* Success return value is the length of the pathname given to
mkdir() */
// resp->val = strlen(path);
/* If the directory is in /tmp, then create it on behalf of the tracer;
give an error for a directory pathname in any other location. */
// if (strncmp(path, "/tmp/", strlen("/tmp/")) == 0) {
// mkdir(path, req->data.args[1]);
// resp->error = 0;
// } else {
// resp->error = -EPERM;
// }
/* Provide a response to the target process */
if (ioctl(notifyFd, SECCOMP_IOCTL_NOTIF_SEND, resp) == -1) {
if (errno == ENOENT)
printf("Tracer: response failed with ENOENT; perhaps target "
"process's syscall was interrupted by signal?\n");
else
perror("ioctl-SECCOMP_IOCTL_NOTIF_SEND");
}
/* If the pathname is just "/bye", then the tracer terminates. This
allows us to see what happens if the target process makes further
calls to mkdir(2). */
if (strcmp(path, "/bye") == 0) {
printf("Tracer: terminating <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n");
exit(EXIT_FAILURE);
}
}
}
int
installFilter()
{
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0))
errExit("prctl");
notifyFd = installNotifyFilter();
pthread_t threadId;
// TODO catch err
pthread_create(&threadId, NULL, &watchForNotifications, NULL);
return notifyFd;
}
cs = {
prctl: Module.getExportByName(null, "prctl"),
syscall: Module.getExportByName(null, "syscall"),
perror: Module.getExportByName(null, "perror"),
malloc: Module.getExportByName(null, "malloc"),
ioctl: Module.getExportByName(null, "ioctl"),
__printf_chk: Module.getExportByName(null, "__printf_chk"),
__snprintf_chk: Module.getExportByName(null, "__snprintf_chk"),
lseek: Module.getExportByName(null, "lseek"),
exit: Module.getExportByName(null, "exit"),
read: Module.getExportByName(null, "read"),
__printf_chk: Module.getExportByName(null, "__printf_chk"),
close: Module.getExportByName(null, "close"),
__errno_location: Module.getExportByName(null, "__errno_location"),
mkdir: Module.getExportByName(null, "mkdir"),
__fprintf_chk: Module.getExportByName(null, "__fprintf_chk"),
fwrite: Module.getExportByName(null, "fwrite"),
open: Module.getExportByName(null, "open"),
pthread_create: Module.getExportByName(null, "pthread_create"),
// Some global memory for the CModule to store the fd:
notifyFd: Memory.alloc(8),
// Callback for watchForNotifications
onMessage: new NativeCallback(messagePtr => {
const message = messagePtr.readUtf8String();
console.log('Frida onMessage:', message);
}, 'void', ['pointer'])
}
rpc.exports = {
init(){
// Installing the seccomp filter in rpc.exports.init() doesn't work because this runs on a Frida thread
// and the seccomp filter won't be on the process's main thread.
// const installFilter = new NativeFunction(cm.installFilter, 'int', []);
// const result = installFilter()
// Is there a way in Frida to run some code on the process's main thread here?
// I tried to drive the loop from the Frida main thread, but I was getting a SECCOMP_IOCTL_NOTIF_RECV "invalid argument" error.
// Maybe this could work though. Maybe you could avoid the pthread created in the CModule, but the ioctl is blocking anyway.
// const watchForNotifications = new NativeFunction(cm.watchForNotifications, 'void', ["int"]);
// setInterval(() => notifyFd != undefined && watchForNotifications(notifyFd), 100);
}
}
// Instead hook something in the process (that preferably runs early in the process's initialization)
Interceptor.attach(Module.getExportByName(null, "mkdir"), {
onEnter(args) {
const installFilter = new NativeFunction(cm.installFilter, 'int', []);
const notifyFd = installFilter();
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment