Nmap is loud by default. Every technique below exists to reduce the signal you generate - in volume, in pattern, and in attribution - while still getting the recon you need. This post walks through the opsec stack from “don’t scan at all if you don’t have to” down to packet-level fingerprint evasion.
1. Minimize Active Scanning
The best scan is the one you don’t run.
Passive recon first - pull as much as possible before touching the target:
Shodan, Censys - pre-indexed port/service data
crt.sh - certificate transparency logs (subdomains, infra hints)
DNSDumpster - historical DNS records
BGP.he.net - ASN/netblock mapping
Resolve scope without noise
nslookup domain.com - only scan the resolved target IP, not the whole domain surface
Use a non-default, non-org DNS resolver for lookups (e.g. nslookup @1.1.1.1) so you’re not leaking recon intent to the target’s own authoritative DNS logs
2. Timing & Rate Control
Fast scans trip thresholds. Slow, jittered scans look like background noise.
Flag
Purpose
-T1 / -T2 / -T3
Slow-and-steady timing templates - avoid -T4/-T5 for anything stealth-relevant
--scan-delay 10s
Fixed delay between probes
--max-rate / --min-rate
Cap or randomize packet rate
Jittered delay
Don’t use a single static --scan-delay - vary it between a min/max range. A perfectly even delay is itself a fingerprint
Probe one port at a time where possible - sequential single-port sweeps look more like normal traffic than a broad parallel sweep.
The challenge was simple on paper: disarm a Linux kernel module rootkit and recover hidden data. In practice, this was my first Hack The Box challenge, and until now I had mostly played with crackmes. Getting an IP address and port as part of a reversing challenge already threw me off for a moment.
The provided file was diamorphine.ko. A .ko file is a Linux kernel object: it can be loaded with insmod, removed with rmmod, and listed with lsmod if the module is not hiding itself.
file identified it as:
1
ELF 64-bit LSB relocatable, x86-64, with debug_info, not stripped
That last part mattered. Because the module was not stripped, Ghidra still showed useful function names instead of leaving me with anonymous blobs. strings produced too much noise to be helpful, so I moved into Ghidra and started from the symbol tree.
The interesting functions were:
diamorphine_init
diamorphine_cleanup
find_task
get_syscall_table_bf
give_root
hacked_getdents
hacked_getdents64
hacked_kill
is_invisible
module_hide
module_show
Every kernel module has an initialization and cleanup path, which in this case were diamorphine_init and diamorphine_cleanup. The rest of the names were already a pretty good map of the rootkit:
module_hide and module_show remove or restore the module from the kernel’s module list, which is why lsmod may not show it.
give_root does exactly what the name suggests.
get_syscall_table_bf locates the syscall table by brute force.
is_invisible checks whether a process has been marked as hidden.
From there, the two most important hooks were hacked_kill and hacked_getdents64.
DNS is easy to overlook during a compromise. It is usually allowed out of a network, it is noisy by nature, and many environments focus harder on HTTP, SSH, or obvious file transfer tools. That makes it a useful channel for moving small amounts of data when a host can resolve external names.
The important trick is that DNS names can carry attacker-controlled text. Instead of sending the file directly over HTTP or copying it with scp, the file is encoded and placed into DNS query labels.
This part reads the file and converts it to DNS-safe text:
After inspecting the site, one small detail in the Survey stood out: Our team reads every word! Be creative and specific. when reading this i instantly thought about XSS.
from http.server import HTTPServer, SimpleHTTPRequestHandler import json
class PostHandler(SimpleHTTPRequestHandler): def do_POST(self): content_length = int(self.headers['Content-Length']) post_data = self.rfile.read(content_length) # Log the POST data print(f"POST request received:") print(f"Path: {self.path}") print(f"Headers: {self.headers}") print(f"Body: {post_data.decode('utf-8')}") # Send response self.send_response(200) self.send_header('Content-type', 'application/json') self.end_headers() response = {'status': 'success', 'received': post_data.decode('utf-8')} self.wfile.write(json.dumps(response).encode())
if __name__ == '__main__': server = HTTPServer(('localhost', 8000), PostHandler) print('Server running on http://localhost:8000') server.serve_forever()
to get the flag all i had to do is start the webserver by running: python3 server.py and sent the payload via the survey.