Controleren of een draaiend proces nog het originele binaire bestand is: /proc/<pid>/exe, sha256 en dpkg -V
Een aanvaller die een backdoor in sshd of nginx plant, laat het proces gewoon draaien. Het bestand op schijf is vervangen, of het proces draait uit een verwijderd bestand dat nergens meer bestaat. Drie controles die dat zichtbaar maken zonder EDR: de hash van /proc/<pid>/exe tegen een baseline, dpkg -V tegen de package-database, en de (deleted)-markering die geen legitiem proces hoort te hebben.
Inhoud
ps toont een proces met de naam sshd. Dat zegt niets over wat er draait. De naam is het eerste argument; het bestand kan vervangen zijn; het proces kan zijn gestart uit een binary die daarna is verwijderd, zodat er op schijf niets verdachts meer te vinden is. Wie een server overneemt, vervangt zelden ls — die vervangt sshd (om wachtwoorden te loggen) of laat een bestaand proces een geïnjecteerde library laden. Dit artikel geeft je drie onafhankelijke controles die op elke Linux-host werken zonder agent, plus het cron-script dat ze combineert.
Controle 1: wat draait er écht? — /proc/<pid>/exe
Voor elk proces houdt de kernel een symlink /proc/<pid>/exe bij naar het bestand dat is uitgevoerd. Die link volgt de inode, niet de naam: als het bestand wordt vervangen (cp nieuw /usr/sbin/sshd), wijst de link van het oude proces nog naar de oude inode en krijgt hij het suffix (deleted).
# Per proces: pid, naam, echt pad
sudo ls -l /proc/[0-9]*/exe 2>/dev/null | awk '{print $NF, $(NF-2)}' | sed 's|/proc/\([0-9]*\)/exe|\1|' | sort -u | head
# Processen die uit een VERWIJDERD bestand draaien
sudo find /proc -maxdepth 2 -name exe -lname '*(deleted)*' 2>/dev/null \
| while read -r l; do pid=${l#/proc/}; pid=${pid%/exe}; printf '%s\t%s\t%s\n' "$pid" "$(cat /proc/$pid/comm)" "$(sudo readlink "$l")"; done
# 1437 agetty /usr/sbin/agetty (deleted) ← util-linux is bijgewerkt; getty draait nog de oude
Een (deleted)-proces is meestal onschuldig: een service die na een apt upgrade nog niet herstart is (zie unattended-upgrades). Maar de combinatie "verwijderd" + "geen package-update in de logs" + "pad in /tmp of /dev/shm" is precies hoe malware zich verstopt.
# Processen waarvan de binary in een verdachte map staat (ook zonder deleted)
sudo ls -l /proc/[0-9]*/exe 2>/dev/null | awk '{print $NF}' | grep -E '^/(tmp|dev/shm|var/tmp|run/user|home)/' | sort | uniq -c
Controle 2: klopt de binary nog met het package? — dpkg -V
Debian en Ubuntu bewaren voor elk geïnstalleerd bestand een MD5-som in /var/lib/dpkg/info/*.md5sums. dpkg -V vergelijkt de bestanden op schijf daarmee.
# Alles wat afwijkt van het package (leest élk bestand van élk package: reken op minuten, niet seconden)
sudo dpkg -V 2>/dev/null | grep -vE '^\?\?5\?+ +c ' | head -30
# Uitleg van de output: "??5??????" = MD5 wijkt af; " c " = config-bestand (verwacht); "missing" = weg
# Alleen binaries en libraries:
sudo dpkg -V 2>/dev/null | awk '$NF ~ /^\/(usr\/)?(s?bin|lib)/ && $2 != "c"'
Een afwijkende binary in /usr/sbin die géén config-bestand is, heeft precies twee verklaringen: iemand heeft hem vervangen, of je hebt hem zelf gepatcht en dat vergeten. RHEL-familie: rpm -Va.
De beperking: dpkg -V controleert tegen de md5sums die het package zelf heeft meegeleverd. Wie root is, kan die ook aanpassen. Daarom controle 3.
Controle 3: een baseline die niet op de host staat
Neem van elk draaiend executable een sha256 en bewaar die buiten de server. Daarna vergelijk je elke dag.
sudo tee /usr/local/sbin/exe-baseline.sh >/dev/null <<'EOF'
#!/usr/bin/env bash
# Print sha256 van het bestand achter /proc/<pid>/exe voor elk uniek executable.
# Hasht via /proc/<pid>/exe zelf, zodat ook een verwijderde binary gehasht wordt.
for l in /proc/[0-9]*/exe; do
pid=${l#/proc/}; pid=${pid%/exe}
target=$(readlink "$l" 2>/dev/null) || continue
[ -z "$target" ] && continue
h=$(sha256sum "$l" 2>/dev/null | awk '{print $1}') || continue
printf '%s\t%s\n' "$h" "$target"
done | sort -u -k2,2
EOF
sudo chmod 0755 /usr/local/sbin/exe-baseline.sh
# Baseline maken op een schone host, en WEG van de host bewaren
sudo /usr/local/sbin/exe-baseline.sh > /tmp/exe-baseline.tsv
scp /tmp/exe-baseline.tsv beheer-host:/srv/baselines/$(hostname -s).tsv
Het dagelijkse vergelijk, vanaf de beheer-host (zodat de baseline niet op de gecontroleerde machine ligt):
sudo tee /usr/local/sbin/exe-verify.sh >/dev/null <<'EOF'
#!/usr/bin/env bash
# Draait op de beheer-host; controleert elke host in hosts.txt tegen zijn baseline.
NTFY="https://ntfy.example.be/security"
B=/srv/baselines
while read -r h; do
cur=$(ssh -o BatchMode=yes -o ConnectTimeout=5 "$h" sudo /usr/local/sbin/exe-baseline.sh 2>/dev/null) || { echo "$h unreachable"; continue; }
base="$B/$h.tsv"; [ -f "$base" ] || { echo "$cur" > "$base"; echo "$h: baseline created"; continue; }
# Zelfde pad, andere hash = vervangen (of gepatcht). Nieuw pad = nieuw proces.
changed=$(join -t $'\t' -1 2 -2 2 <(sort -t $'\t' -k2,2 "$base") <(echo "$cur" | sort -t $'\t' -k2,2) | awk -F'\t' '$2!=$3 {print $1}')
new=$(comm -13 <(cut -f2 "$base" | sort) <(echo "$cur" | cut -f2 | sort))
deleted=$(echo "$cur" | grep -c '(deleted)')
msg=""
[ -n "$changed" ] && msg+="CHANGED BINARY: $(echo "$changed" | paste -sd, -)\n"
[ -n "$new" ] && msg+="new executables: $(echo "$new" | paste -sd, -)\n"
[ "$deleted" -gt 0 ] && msg+="$deleted process(es) running from deleted files\n"
[ -n "$msg" ] && printf "$msg" | curl -s -H "Title: integrity $h" -H "Priority: urgent" --data-binary @- "$NTFY" >/dev/null
done < /srv/baselines/hosts.txt
EOF
sudo chmod 0755 /usr/local/sbin/exe-verify.sh
echo '40 * * * * root /usr/local/sbin/exe-verify.sh' | sudo tee /etc/cron.d/exe-verify
Na een legitieme update (apt upgrade + herstart van de service) verandert de hash uiteraard. Het script meldt dat; jij bevestigt het tegen dpkg.log en vernieuwt de baseline:
grep "$(date +%F)" /var/log/dpkg.log | grep ' upgrade ' | awk '{print $4}' # welke packages vandaag?
ssh web-01 sudo /usr/local/sbin/exe-baseline.sh > /srv/baselines/web-01.tsv # baseline vernieuwen ná verificatie
Controle 4 (bonus): geladen libraries en LD_PRELOAD
Een proces hoeft niet vervangen te worden om gekaapt te zijn: een library-injectie via LD_PRELOAD of /etc/ld.so.preload volstaat. Twee snelle checks:
# Bestaat /etc/ld.so.preload? Op een gewone server hoort hij niet te bestaan.
ls -la /etc/ld.so.preload 2>/dev/null && cat /etc/ld.so.preload
# Processen met LD_PRELOAD in hun environment
for e in /proc/[0-9]*/environ; do pid=${e#/proc/}; pid=${pid%/environ}; sudo cat "$e" 2>/dev/null | tr '\0' '\n' | grep -q '^LD_PRELOAD=' && echo "$pid $(cat /proc/$pid/comm)"; done
# Libraries geladen vanuit verdachte paden
sudo awk '/\.so/ && $NF ~ /^\/(tmp|dev\/shm|var\/tmp|home)\// {print FILENAME, $NF}' /proc/[0-9]*/maps 2>/dev/null | sort -u | head
Valkuilen
- Herstart na update vergeten. De meeste
(deleted)-meldingen zijn services die na een library- of binary-update niet herstart zijn. Dat is geen inbraak, wel een reden omneedrestartaan te zetten — anders verdrinkt de echte melding in de ruis. - Interpreters. Voor een Python- of Node-service is
/proc/<pid>/exede interpreter (/usr/bin/python3.12), niet je applicatie. De hash van de interpreter klopt terwijl de app-code is aangepast. Hash daarvoor de app-map apart (find /srv/app -type f -exec sha256sum {} +), of gebruikdpkg -Vniet en git wel. - Baseline op de host. Een baseline in
/var/lib/…op dezelfde machine kan een aanvaller met root mee-aanpassen. Vandaar de beheer-host. - Prelink en ASLR. Op systemen met
prelink(zeldzaam tegenwoordig) veranderen binaries op schijf legitiem. Debian/Ubuntu gebruiken het niet. - Containers.
/proc/<pid>/exevan een container-proces wijst naar een pad in het container-filesystem (/api,/bin/node_exporter); de baseline bevat die dus ook, maardpkg -Vop de host weet er niets van. Per image hashen, of Trivy gebruiken.
Wat je hiermee nog niet hebt
- De eerste baseline. Als de host al gecompromitteerd was toen je de baseline maakte, is elke afwijking daarna "normaal". Maak hem direct na installatie, of vergelijk met een verse installatie van hetzelfde package.
- Correlatie. Een vervangen binary én een nieuwe SSH-key én een login uit een nieuw land binnen één uur: drie scripts, drie berichten, geen verhaal.
- Bewijs. Voor een NIS2-incidentmelding wil je de tijdlijn (wanneer veranderde de hash, welke sessie was actief) ondertekend en bewaard.
Zo doet monsys het
De monsys-agent doet dit continu als Process DNA: bij de eerste run neemt hij van elk draaiend executable de SHA256 van /proc/<pid>/exe als baseline, en daarna meldt hij elke divergentie — vervangen binary, proces uit een verwijderd bestand, executable in /tmp of /dev/shm — als process_dna_divergence. De baseline ligt in de hub, niet op de host. Na een legitieme package-update ziet de hub de bijbehorende dpkg-inventariswijziging en markeert hij de nieuwe hash als verwacht, zodat alleen de onverklaarde afwijking een alert wordt. Zie integriteitsbewaking in de docs.
FAQ
Hoe zie ik of een draaiend proces gewijzigd is?
Vergelijk de hash van /proc/<pid>/exe met een eerder opgeslagen baseline, en controleer of de link het suffix (deleted) heeft. dpkg -V <package> vergelijkt daarnaast het bestand op schijf met de checksum uit het package.
Is dpkg -V betrouwbaar als bewijs?
Als eerste indicatie ja; als bewijs nee, omdat de md5sums op dezelfde host staan als de binaries en door root aangepast kunnen worden. Een baseline buiten de host (of een agent die de hashes elders bewaart) is het antwoord.
Wat als een proces uit een verwijderd bestand draait?
Controleer eerst /var/log/dpkg.log van vandaag: is het package net bijgewerkt, dan herstart je de service. Zo niet, en zeker als het pad in /tmp, /dev/shm of een home-map ligt: behandel het als incident, isoleer de host en bewaar /proc/<pid>/exe (cp /proc/<pid>/exe /root/evidence-<pid>) vóór je het proces stopt.
Geschreven door het monsys-team — sysadmins die dit dagelijks doen.
Zelf gedaan? Laat monsys het bijhouden.
Alles uit deze how-to draait in monsys als doorlopende check, met historie, alerts en audit-bewijs. 5 servers gratis, EU-gehost in België, geïnstalleerd in 60 seconden.