The goal of this challenge is escaping docker with seccomp-ed environment by using off-by-one in kmalloc-4k.
The seccomp prohibits us to use struct msg_msg and struct msg_msgseg.
So, we need to find new structure which makes us exploit off-by-one.
And after making RIP control (ROP, or something…), we have to LPE and Escaping docker.
Module
The module is simple: just prints the count of each syscall called and makes us to filter which syscall’s call count will be counted.
It utilizes Syscall statistics patch based on https://lwn.net/Articles/896474/ (check out build/build_kernel.sh).
The source code of module is not provided, but the challenge gives us the demo program using it.
/*
* This is a demo program to show how to interact with SPARK.
* Don't waste time on finding bugs here ;)
*
* Copyright (c) 2020 david942j
*/
#include <assert.h>
#include <fcntl.h>
#include <linux/spark.h>
#include <stdio.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <unistd.h>
#define DEV_PATH "/dev/node"
#define N 6
static int fd[N];
const char l[] = "ABCDEF";
/*
B -- 10 -- D -- 11 -- F
/ \ |
4 | |
/ | |
A 5 4
\ | |
2 | |
\ | |
C -- 3 - E
*/
static void link(int a, int b, unsigned int weight) {
printf("Creating link between '%c' and '%c' with weight %u\n", l[a], l[b],
weight);
assert(ioctl(fd[a], SPARK_LINK, fd[b] | ((unsigned long long)weight << 32)) ==
0);
}
static void query(int a, int b) {
struct spark_ioctl_query qry = {
.fd1 = fd[a],
.fd2 = fd[b],
};
assert(ioctl(fd[0], SPARK_QUERY, &qry) == 0);
printf("The length of shortest path between '%c' and '%c' is %lld\n", l[a],
l[b], qry.distance);
}
int main(int argc, char *argv[]) {
for (int i = 0; i < N; i++) {
fd[i] = open(DEV_PATH, O_RDONLY);
assert(fd[i] >= 0);
}
link(0, 1, 4);
link(0, 2, 2);
link(1, 2, 5);
link(1, 3, 10);
link(2, 4, 3);
link(3, 4, 4);
link(3, 5, 11);
assert(ioctl(fd[0], SPARK_FINALIZE) == 0);
query(0, 5);
query(3, 2);
query(2, 5);
for (int i = 0; i < N; i++) close(fd[i]);
return 0;
}
As we can see in demo.c, this module is for a shortest path search.
And each node can be allocated by open and two nodes can be linked by link. The searching path is done by query.
typedef struct request_t {
uint32_t cmd;
uint32_t arg;
} request_t;
int64_t gnote_write(struct file *a1, const request_t *req, size_t a3,
loff_t *a4) {
uint64_t len;
note_data_t *req;
void *new_note_data;
mutex_lock(&lock);
switch (req->cmd) {
case 1:
if ((uint64_t)cnt <= 7) {
len = (uint32_t)req->arg;
cur_note = ¬es[cnt];
cur_note->len = len;
if (len <= 0x10000) {
new_note_data = kmalloc(len, 0x6000C0LL);
++cnt;
cur_note->data = new_note_data;
}
}
break;
case 2:
printk("Edit Not implemented\n");
break;
case 3:
printk("Delete Not implemented\n");
break;
case 4:
printk("Copy Not implemented\n");
break;
case 5:
if ((uint32_t)req->arg < (uint64_t)cnt) selected = (uint32_t)req->arg;
break;
default:
break;
}
mutex_unlock(&lock);
return a3;
}
uint64_t gnote_read(struct file *a1, char *a2, size_t len, loff_t *a4) {
note_data_t *cur_note;
mutex_lock(&lock);
if (selected == -1) {
mutex_unlock(&lock);
return 0LL;
} else {
cur_note = ¬es[selected];
if (cur_note->len <= len) len = cur_note->len;
copy_to_user(a2, cur_note->data, len);
selected = -1LL;
mutex_unlock(&lock);
return len;
}
}
The gnote module has just 3 features: add new note, select note and get note’s content. But with these features, we cannot write content to note (Edit is not implemented…).