Linux forensics, част 1: Когато машината ти лъже в очите
Как се разглобява жива система, на чиито инструменти вече не вярваш
Тази тема идва от заявка на @linux в темата за поръчки. Дословно: сървърът се държи странно, а на ps, netstat, lsof и ls вече не се вярва. Иска подход отдолу нагоре, през /proc, /sys и сурови данни, без да се обляга на готови инструменти.
Заявката е добра, защото описва точния момент, в който повечето ръководства спират да работят. Те приемат, че системата казва истината. Ние тук приемаме обратното.
Част 1 е за живата машина - тази, която още работи и която не искаш да гасиш. Част 2 ще е за диска и паметта, тоест за случая, в който вече не вярваш на нищо, което е вътре.
Правилото, което държи цялата статия
Едно-единствено правило, и всичко останало е приложение на него:
Нито едно твърдение не се приема от един източник.
Всеки факт се потвърждава от два независими пътя. Процес - от ps и от суровото съдържание на /proc. Сокет - от ss и от /proc/net/tcp, а най-добре и от скан отвън. Файл - от ls и от stat, а накрая и от машина, която монтира диска отстрани.
Когато двата източника съвпадат, имаш слаба увереност. Когато се разминават, имаш находка. Цялата работа на форензика е да произвежда разминавания.
Заваръчният еквивалент е прост. Визуалният оглед ти казва, че шевът изглежда добре. Това не е контрол. Контрол е, когато вторият метод не потвърди онова, което окото е видяло.
Кой слой те лъже - и докъде му стига ръката
Няма смисъл да търсиш, преди да си наясно какво може да те лъже. Слоевете са четири и всеки следващ вижда повече от предишния.
1. Подменен бинарник
Най-примитивното. Заменен /bin/ps или /bin/ls, който филтрира резултата. Понякога дори не се подменя нищо - просто се пипа PATH или се слага фалшив ps в директория, която идва по-рано в търсенето.
Не може да скрие: нищо, което четеш директно от /proc или /sys.
Разпознава се по: проверка на пакетите, type и command -v, сравнение на контролни суми с чист източник.
2. LD_PRELOAD и ld.so.preload
Библиотека, заредена преди системната, която подменя функции като readdir. Оригиналният ls си стои непокътнат, но не вижда файловете, чиито имена библиотеката е решила да изяде.
Не може да скрие: нищо от статично свързан бинарник, който не ползва динамичния зареждач.
Разпознава се по: съдържанието на /etc/ld.so.preload, променливи в средата на процесите, сравнение на изхода от динамичен и статичен инструмент.
3. Модул в ядрото
Тук вече лъжата е под всичко в потребителското пространство. Хванати системни извиквания, филтриран /proc, скрити процеси, скрити сокети. И ps, и cat /proc/... получават една и съща обработена истина, защото и двете минават през ядрото.
Не може да скрие: това, което се вижда отвън - трафик по кабела, състоянието на диска, гледан от друга система.
Разпознава се по: вътрешни несъответствия, tainted състояние, разлика между /proc/modules и /sys/module, и най-вече чрез външни наблюдения.
4. еБПФ
Модерният вариант на същото. Не се зарежда модул, не се цапа ядрото по класическия начин, а се закача програма, която може да чете и да променя поведение. Оставя по-малко следи по стария контролен списък и точно затова старите ръководства не го хващат.
Разпознава се по: типа на заредените програми и точките, в които са закачени - не по самото им наличие. Вижте отделния раздел по-долу, защото тук е най-лесно да се изплашиш от нищо.
Колкото по-дълбоко е скрит лъжецът, толкова по-малко смисъл има да го питаш. На някакво ниво единственият честен източник е този, който е извън машината.
Преди да пипнеш каквото и да е
Всяка твоя команда е събитие, което може да бъде забелязано. Ако на машината има активен нападател, ти му съобщаваш, че си вътре и че гледаш. Затова редът е важен.
- Не рестартирай и не гаси. Изгасената машина губи паметта, а с нея и половината доказателства. Дори да е заразена, тя е по-полезна включена, отколкото чиста и празна.
- Не инсталирай нищо на нея. Всяко apt install пипа системата, която разследваш, и минава през мрежа, на която не можеш да вярваш.
- Носи си инструментите. Статично свързан busybox от чиста машина, копиран на носител само за четене. Той не ползва библиотеките на заподозряната система, значи слой 2 не го засяга.
- Записвай всичко извън машината. Изходът върви към твоя лаптоп, не към диска на сървъра. Логовете на заразена машина са данни, а не доказателства.
- Отбележи часа. Часът на машината и реалният час. Разминаването само по себе си е находка.
Всичко по-долу е писано и проверявано на Debian 13 (trixie) с ядро от серия 6.12, на реална работеща машина - не в стерилна виртуалка. Всяко число в статията идва от нейния изход. Пълното ѝ описание е в раздела с базовите стойности, защото без него числата не значат нищо.
За bookworm с 6.1 пътищата и командите са същите - разликите са в имената на някои пакети и във версиите на инструментите, не в подхода.
Процеси: гледай директориите, не списъка
ps е просто програма, която обхожда /proc и подрежда резултата. Ако ще ѝ вярваш, поне я карай да се разминава сама със себе си.
Първата проверка е сравнение между това, което ядрото изброява, и това, което ps признава:
Code:
# всички числови директории в /proc
ls -1 /proc | grep -E '^[0-9]+$' | sort -n > /tmp/proc.list
# същото според ps
ps -eo pid --no-headers | tr -d ' ' | sort -n > /tmp/ps.list
разлика /tmp/proc.list /tmp/ps.list
Разлика тук значи, че нещо между двата източника филтрира. Липсата на разлика не значи чисто - значи само, че лъжата, ако има такава, е под двата.
Следващата стъпка е по-груба. Директория в /proc може да бъде скрита от изброяване, но пак да е достъпна по име. Затова се пита за всеки възможен номер поотделно:
Code:
# зареждаме известните PID-ове в паметта на обвивката, без форкове в цикъла
декларирам -Виждан
докато чете -rp; направете seen[$p]=1; готово < /tmp/ps.list
макс=$(cat /proc/sys/kernel/pid_max)
за ((pid=1; pid<=max; pid++)); направете
[ -d /proc/$pid ] || продължи
[ -n "${seen[$pid]}" ] && продължи
# ВАЖНО: нишките също имат директория в /proc, но не се изброяват.
# Ако Tgid се различава от Pid, това е нишка на друг процес, не скрит процес.
tgid=$(awk '/^Tgid:/{print $2}' /proc/$pid/status 2>/dev/null)
[ -z "$tgid" ] && продължи
[ "$tgid" != "$pid" ] && продължете
echo "СКРИТ PID: $pid $(tr '\0' ' ' < /proc/$pid/cmdline 2>/dev/null)"
готово
Редът с Tgid е най-важният в целия скрипт и почти никое ръководство не го споменава.
Директория в /proc има не само за процес, а и за всяка нишка. Разликата е, че нишките не се изброяват при четене на /proc, но са напълно достъпни по име. Обхождането ги намира, ps не ги показва като отделни записи, и без филтъра всяка нишка на базата данни или на контейнер се обявява за скрит процес.
Ето какво дава точно този скрипт на здрава машина, при пълен обход на диапазона:
Code:
TOTAL_DIRS = 889 # всички директории в /proc (процеси + нишки)
RAW_CANDIDATES = 478 # не са в изхода на ps - "скрити" преди филтъра
TRUE_CANDIDATES = 0 # остават след проверката по Tgid
478 срещу 0. Без този един ред получаваш 478 доклада за скрити процеси на напълно здрава система и започваш да търсиш нападател, който не съществува. С него - нула.
Това е разликата между метод и паника.
Скорост и нормален шум
На 64-битово Debian pid_max е 4194304. Ако цикълът вика външна програма при всяка итерация, говорим за четири милиона процеса-наследници - часове работа и натоварване, което ще бъде забелязано. Вариантът горе вика нещо само при попадение, тоест 478 пъти, а не четири милиона.
Измерено: 28,7 секунди за целия диапазон с пълната логика. Без проверката по Tgid са 26,4 секунди, тоест филтърът струва около две секунди и спестява 478 фалшиви тревоги. За сравнение, 300 000 номера минават за 1,6 секунди.
Бързо е, защото /proc не е на диск. Той не е и файлова система в паметта - той е procfs, виртуална файлова система, която ядрото сглобява в момента на четенето. Няма какво да се чака.
Затова обхождай целия диапазон. Таван има смисъл само на много натоварена машина, където и половин минута системни извиквания е проблем.
И най-важното за четенето на резултата. Сравнението между /proc и ps на здрава машина не дава нула. При моя тест излязоха по три записа от всяка страна - процеси, родени и умрели между двете последователни команди. Дори общият брой директории се разминава с единица между два последователни обхода, защото машината живее, докато я броиш. Това е нормален шум, не находка. Затова: пускай сканирането два пъти и гледай само номерата, които оцеляват и в двата пъти.
За всеки подозрителен номер се вадят суровите данни:
[КОД]pid=1234
readlink /proc/$pid/exe # истинският изпълним файл
readlink /proc/$pid/cwd # работна директория
tr '\0' ' ' < /proc/$pid/cmdline # аргументи
tr '\0' '\n' < /proc/$pid/environ # среда, търси LD_PRELOAD
cat /proc/$pid/status # PPid, UID, нишки, възможности
ls -l /proc/$pid/fd/ # отворени файлове и сокети
cat /proc/$pid/maps # заредени библиотеки[/CODE]
Три неща, които се четат най-бързо и лъжат най-трудно:
Изтрит бинарник, който още работи. Връзката exe завършва с (deleted). Класика - програмата се пуска и веднага се трие от диска, за да не остане следа. Следата остава в паметта.
[КОД]ls -l /proc/*/exe 2>/dev/null | grep -i изтрит[/КОД]
И веднага уговорката, която прави разликата между полезна и вредна проверка. На здрав Debian 13 сървър тази команда връща цял списък. Ето какво излезе при мен и защо всяко от тях е нормално:
- plymouthd - програмата за начален екран, трие се след зареждане и продължава да работи
- php-fpm с /memfd
pcache_lock - кешът на PHP държи анонимна памет с име. Показва се като изтрит винаги. Това е най-често срещаният фалшив аларм на уеб сървър.
- mariadbd с /tmp/#23, #24 и подобни - временни таблици. Базата ги създава и веднага им маха името, за да се чистят сами.
- mdmon с /dev/.tmp.md... - наблюдателят на RAID масива
- pipewire с /memfd
ipewire-memfd - аудио буфери в анонимна памет, идват от графичната среда
Забележете, че списъкът е огледало на ролите на машината. База данни, уеб стек, RAID, десктоп. На вашата машина ще са други имена по същата логика - и точно затова не може да се публикува универсален списък "тези са наред".
Общото между всички: memfd и /tmp/# са легитимни техники за анонимна памет и временни файлове. Находка е нещо друго - изтрит бинарник в /usr/bin, /usr/sbin или потребителска директория, чието име би трябвало да сочи към истински инсталиран файл. Тоест гледаш пътя, не думата deleted.
Фалшив системен процес. Истинските нишки на ядрото нямат exe връзка и се водят под kthreadd. Нещо, което се нарича [kworker/0:2], но има изпълним файл и родител в потребителското пространство, лъже за това какво е.
Несъответствие в средата. LD_PRELOAD в environ на процес, който няма причина да го има.
И накрая - независим брояч. Планировчикът на ядрото води собствена сметка за задачите и не минава през същия път като /proc:
[КОД]mount | grep debugfs || mount -t debugfs няма /sys/kernel/debug
grep -c . /sys/kernel/debug/sched/debug[/CODE]
Важно за точността: този файл не е на /proc/sched_debug. Преместен е в debugfs от ядро 5.13 нататък, тоест на всяко актуално Debian. Старите статии по темата още дават стария път и затова не работят.
Мрежа: сокетът има номер, номерът има собственик
ss пита ядрото по един път, /proc/net/tcp е друг път до същата информация. Сравнението хваща подменен инструмент, но не хваща модул, който филтрира и двете. Затова накрая идва външният скан.
[КОД]ss -tulpanH | wc -l
cat /proc/net/tcp /proc/net/tcp6 | wc -l[/CODE]
Адресите в /proc/net/tcp са шестнайсетични и с обърнати байтове, което плаши хората, а не бива. Портът е втората част след двоеточието: 0x0016 е 22, 0x01BB е 443. Предпоследно е състоянието, а колоната с inode е тази, която ти трябва.
Оттам номерът на сокета се връзва обратно към процеса:
[КОД]inode=123456
ls -l /proc//fd/ 2>/dev/null | grep "сокет:\[$inode\]"[/CODE]
Ако сокетът съществува, а собственик няма, значи процесът е скрит от изброяването. Това е същинска находка.
Още две места, които хората пропускат:
- /proc/net/packet и /proc/net/raw - тук се виждат снифери. Само че не е вярно, че на нормална машина списъкът е празен: при мен има точно един запис и той се проследи до NetworkManager, който държи такъв сокет за ARP. Правилото е същото както навсякъде - вземи номера на сокета от последната колона и го върни до процеса. Ако собственикът е обясним, записът е обясним.
- Режим promiscuous по интерфейса: ip -d link show и стойността promiscuity. На чиста машина е нула по всички интерфейси, включително loopback, docker0 и виртуалните двойки. Плюс търсене в dmesg за влизане в този режим.
Числата от здрава машина за мащаб: ss показа 39 записа, суровите файлове в /proc/net - 38. Разликата от един е връзка, отворена или затворена между двете команди. Пак шум, пак не е находка.
И задължителното, което не може да бъде излъгано отвътре: скан отвън, от друга машина, по целия диапазон портове. Ако отвън се отговаря на порт, който отвътре не съществува, разговорът приключва. Имаш модул в ядрото.
Същото важи и за трафика. Ако имаш рутер или втора машина на пътя, огледът на трафика оттам е независим свидетел. Заразената машина може да лъже за сокетите си, но не може да скрие пакетите от кабела.
Файлове: времето и правата издават повече от името
Имената се сменят за секунда. Метаданните - по-трудно.
ctime срещу mtime. Времето на промяна се фалшифицира тривиално с touch. Времето на промяна на inode не се пипа така - то се обновява от ядрото и за да се пренапише убедително, трябва да се мърда системният часовник. Файл със стар mtime и пресен ctime е точно това, което търсиш.
[КОД]find / -xdev -newerct '2026-09-01' -printf '%T+ %C+ %p\n' 2>/dev/null | сортиране[/КОД]
Права, които не се виждат в списъка. Съвременният начин за скрита привилегия не е SUID бит, а capabilities. Пакетът е libcap2-bin:
[КОД]getcap -r / 2>/dev/null
find / -xdev -perm -4000 -type f 2>/dev/null # старата школа, все още жива[/CODE]
Файлове, които не се трият. Флагът за неизменност се вижда само с lsattr, не с ls:
[КОД]lsattr -R /etc /usr/bin /usr/sbin 2>/dev/null | grep -- '-i-'[/КОД]
Отворено, но изтрито. Файл без име, който още се държи от процес:
[КОД]lsof +L1 2>/dev/null[/КОД]
Скрито чрез монтиране. Едно вързано монтиране върху директория я скрива напълно, без да пипа нито един файл. Затова се сравнява какво казва mount и какво пише ядрото:
[КОД]cat /proc/self/mountinfo
findmnt -A[/CODE]
И накрая местата, където изпълним файл няма работа: /tmp, /var/tmp, /dev/shm, /dev.
[КОД]намери /tmp /var/tmp /dev/shm /dev -xdev -type f -executable 2>/dev/null[/КОД]
Ядрото: къде се къса историята
Модул, който се е отписал от списъка, често забравя следите си другаде. Сравнението е безплатно:
[КОД]awk '{print $1}' /proc/modules | сортиране > /tmp/mod.proc
ls -1 /sys/module | сортиране > /tmp/mod.sys
разлика /tmp/mod.proc /tmp/mod.sys[/CODE]
Разликата не е автоматично доказателство - вградените в ядрото неща се появяват в /sys/module, без да са в /proc/modules. За мащаб: на здрав Debian 13 сървър тази разлика е 135 реда и всичко е наред. Затова се гледат непознатите имена, не бройката.
Следващото е състоянието на замърсяване. Числото е битово поле и се чете така:
[КОД]cat /proc/sys/kernel/tainted
за i в $(seq 20); направете echo $(($i-1)) $(($(cat /proc/sys/kernel/tainted)>>($i-1)&1)); готово[/CODE]
Битовете, които значат нещо в този разговор:
- бит 0 - зареден е собственически модул
- бит 1 - модул е зареден насила
- бит 3 - модул е изваден насила
- бит 12 - зареден е модул, който не е от дървото на ядрото
- бит 13 - зареден е неподписан модул
На чисто Debian с подписани модули битове 12 и 13 са неутрални само ако знаеш откъде идват - драйвер за видео, ZFS, VirtualBox. Ако не знаеш откъде идват, имаш въпрос за задаване.
Останалото по темата:
[КОД]dmesg | grep -iE 'taint|проверката на модула е неуспешна|извън дървото'
grep -c . /proc/kallsyms # при kptr_restrict адресите са нули[/CODE]
eBPF: как изглежда чистата машина
Този раздел го пиша, след като пуснах командите на собствен сървър с Debian 13, systemd и Docker. Резултатът е двайсет и пет заредени програми. Нито една от тях не е зловредна.
Наличието на eBPF програми не значи нищо. Ако някой ти каже да се разтревожиш, защото списъкът не е празен, той не е гледал истинска машина.
Code:
apt install bpftool # отделен пакет в Debian 13, от източника на ядрото
bpftool prog list # какви програми са заредени
bpftool map list # какви карти ползват
bpftool net list # закачени по мрежата - xdp, tc
bpftool cgroup tree # закачени по контролни групи
bpftool perf show # закачени kprobe, uprobe, tracepoint
ls -l /sys/fs/bpf # пинати обекти, преживяват процеса си
Ето какво е нормално да видиш на обикновен Debian сървър:
- cgroup_device с име sd_devices - systemd, по една на услуга, контролира достъпа до устройства
- cgroup_skb с имена sd_fw_ingress и sd_fw_egress - вградената филтрация на systemd по услуги
- cgroup_device без име, закачена на docker-....scope - контейнерите на Docker. Безименна програма изглежда подозрително, а е най-обикновен контейнер.
- lsm с име restrict_filesystems - модулът за сигурност на systemd, ограничава кои файлови системи вижда услугата
Празен изход от bpftool net list е добър знак - значи нищо не е закачено по мрежовия път. Прескачащи номера в списъка също са нормални, програмите се раждат и умират.
Тогава какво наистина се гледа:
- Типове, които нямат работа на сървър за услуги: kprobe, uprobe, tracepoint, raw_tracepoint, socket_filter, xdp, sched_cls. Ако не си пускал bcc, bpftrace, Cilium или подобен инструмент, там не бива да има нищо.
- Закачане по мрежата в bpftool net list при машина, на която не върти такъв софтуер.
- Време на зареждане, което не съвпада с рестарт или със стартиране на услуга. Колоната loaded_at е полезна точно за това.
- Пинати обекти в /sys/fs/bpf - те преживяват процеса, който ги е създал, и са класически начин за оцеляване.
- Закачени probe-ове в bpftool perf show - тук се вижда кой процес какво следи.
И най-практичният съвет от целия раздел: снеми този отпечатък сега, докато машината е здрава. Запази изхода извън нея, с дата. Следващия път сравнението отнема секунда вместо цял следобед догадки.
Уговорка, която трябва да се каже: всичко в този раздел се чете през ядрото. Ако ядрото е компрометирано, то може да ти върне каквото си поиска. Затова разделът служи за хващане на немарливи нападатели, не за доказване на чистота.
Устойчивост: как се връща след рестарт
Нападателят, който не е измислил как да оцелее след рестарт, не е завършил работата си. Местата на Debian със systemd са предвидими:
- Услуги и таймери: systemctl list-unit-files --state=enabled, systemctl list-timers --all, после съдържанието на /etc/systemd/system, /run/systemd/system, /usr/lib/systemd/system и потребителските в ~/.config/systemd/user
- Планирани задачи: /etc/cron.*, /etc/crontab, /var/spool/cron/crontabs, отложени задачи през atq
- Ключове за достъп: authorized_keys на всеки потребител с домашна директория, включително тези със системен номер
- Сметки с номер нула: awk -F: '$3==0' /etc/passwd - редът трябва да е точно един
- Права през sudo: /etc/sudoers и целият /etc/sudoers.d/
- Зареждане на библиотеки: /etc/ld.so.preload, /etc/ld.so.conf.d/, променливи в /etc/environment и /etc/profile.d/
- Удостоверяване: /etc/pam.d/ и модулите в /lib/x86_64-linux-gnu/security/ - подменен модул тук дава тих вход завинаги
- Съобщение при вход: скриптовете в /etc/update-motd.d/ се изпълняват при всяко влизане
- Най-ниското ниво: initramfs и конфигурацията на зареждача
Пакетите на Debian: полезно, но не доказателство
Тук Debian дава предимство пред много други системи, стига да си наясно какво точно проверяваш.
[КОД]dpkg --verify
подходящо инсталиране на debsums
sudo debsums -c # само променените - задължително с права
sudo debsums -ac # включително конфигурационните
sudo debsums -l # пакети без записани суми[/CODE]
Изходът на dpkg --verify е низ от въпросителни, в който петата позиция е контролната сума. Ред, който започва с ??5, значи променено съдържание.
Пусни го с права. Без тях получаваш редове can't open за защитените setuid бинарници - при мен четири на брой, от cups-browsed, cups-filters, mariadb-server и smbclient. Това не са находки, а липса на достъп за четене. Точно те обаче са и файловете, които най-много искаш да провериш, тоест проверката без права проверява всичко освен същественото.
Резултатите от здрава машина, за сравнение: debsums -c с права - нула променени бинарника. debsums -l - нула пакета без суми, тоест пълно покритие. debsums -ac - три файла, и трите мои собствени съзнателни промени по конфигурации. Тоест на добре поддържана система този инструмент е тих и всяко изключение има име.
Сега ограниченията, защото без тях разделът подвежда:
- Сумите се четат от /var/lib/dpkg/info/*.md5sums - база, която седи на същата машина и се пренаписва със същите права, с които е бил подменен бинарникът.
- Конфигурационните файлове по подразбиране не влизат в тези списъци. Затова е нужен -a.
- Част от пакетите изобщо нямат записани суми. debsums -l ти казва кои са.
- Алгоритъмът е MD5. За тази задача върши работа срещу немарливост, но не е това, което искаш да представиш като доказателство.
Честната версия на проверката минава отвън: сваляш същия пакет на чиста машина, разопаковаш го и сравняваш контролните суми срещу копие на подозрителния файл.
[КОД]# на машина CLEAN
подходящо изтегляне на coreutils
dpkg-deb -x coreutils_*.deb /tmp/clean
sha256sum /tmp/clean/bin/ls
# срещу sha256sum на копието от заподозряната машина[/CODE]
Пакетът се копира от машината, не се проверява на нея. Сравнението се прави там, където инструментът за сравнение е чист.
Готовите инструменти: къде им е мястото
В хранилищата на Debian има rkhunter, chkrootkit, unhide и lynis. Заявката беше за подход без тях и това е правилната нагласа, но не защото са лоши.
unhide прави механично точно това, което описах по-горе - обхожда номерата на процесите и сравнява източниците. Може да се ползва, за да ти спести писането. Проблемът е друг: той работи на заразената машина и с нейните библиотеки. Тоест е още един свидетел от същото село.
rkhunter и chkrootkit търсят познати отпечатъци. Хващат стари и мързеливи неща. Нищо от това, което си струва да се крие днес, не е в списъците им.
Използвай ги като първи бърз преглед. Никога като присъда - нито обвинителна, нито оправдателна.
Инструмент, който сам оценява системата, на която се изпълнява, не дава заключение. Дава мнение.
Полигонът: как се проверява всичко това, без да чакаш беда
Няма смисъл да четеш командите. Трябва да си видял как изглежда изходът, когато нещо наистина липсва.
Направи си виртуална машина с Debian, без реални данни, със снимка на състоянието преди всеки опит. После си симулирай симптомите по честен път:
- Пусни програма и изтрий бинарника ѝ, докато работи. Виж (deleted) в /proc.
- Сложи флаг за неизменност на файл и се опитай да го изтриеш като root. После го намери с lsattr.
- Дай мрежова привилегия на копие на /bin/cat и го намери с getcap -r /.
- Вържи с монтиране празна директория върху пълна. Виж как изчезва от ls и как си остава в mountinfo.
- Отвори слушащ порт с nc, намери номера на сокета в /proc/net/tcp и се върни оттам до процеса.
Пет упражнения, около час работа. След тях четенето на сурови данни спира да е теория.
Базова линия: как изглежда здравата машина
Този раздел го няма в нито едно ръководство и точно затова хората се плашат от нищо. Числата долу са от една конкретна машина - при мен тя се води Ветеранът - и имат стойност само заедно с това какво върти тя.
- Хардуер: Dell Precision T5810, Xeon E5-1650 v3 (6 ядра, 12 слота), 64 GB ECC, Radeon RX 580
- Система: Debian 13 (trixie), ядро 6.12 в rt вариант. Уточнявам изрично: RT ядрото е просто това, което беше налично при инсталацията, а не избор заради ниска латентност. Няма аудио или реалновремеви сценарий зад него и не бива да си го обяснявате така.
- Не е headless: пълен GNOME с GDM и Wayland сесия, машината се ползва и като работна станция
- Услуги: MariaDB, PHP 8.4-FPM с няколко приложения, Samba, CUPS
- Docker: три контейнера - локален AI интерфейс, сървър за автоматизация на браузър и самостоятелна търсачка
- RAID: Intel Matrix с метаданни IMSM, огледало от два диска, състояние [UU]. Не чист Linux софтуерен RAID, а Intel RST, обслужван през mdadm.
Това е чувствително по-натоварена комбинация от гола сървърна инсталация. Графичната среда сама по себе си вдига и SUID файловете, и услугите, и звуковите процеси. Затова сравнявай със своята машина по роля, а не с тези числа буквално. Ако твоят сървър е headless и има 54 включени услуги, въпросът е защо, а не "и при Тони са толкова".
- Директории в /proc при пълен обход: 889 (процеси плюс нишки)
- Кандидати за скрит процес: 478 преди филтъра по Tgid, 0 след него
- Разлика /proc срещу ps: по три записа от всяка страна - чист шум от времето между командите
- Изтрити бинарници, които още работят: десетина, всичките от plymouth, php-fpm, mariadb, mdmon и pipewire
- Сокети: 39 според ss, 38 според суровите файлове
- /proc/net/packet: един запис, собственик NetworkManager
- promiscuity: нула по всички интерфейси
- опетнени: 0
- Разлика /proc/modules срещу /sys/module: 135 реда
- eBPF програми: 24, всичките от systemd и Docker; нищо закачено по мрежовия път
- getcap по цялата система: празно
- SUID файлове: 20
- immutable флагове: празно
- Изпълними файлове в /tmp, /var/tmp, /dev/shm, /dev: нула
- Включени услуги на systemd: 54
- Акаунти с номер нула: точно един
- debsums с права: нула променени бинарника, нула пакета без суми, три променени конфигурации със знайна причина
Прочети списъка още веднъж и забележи какво прави. Половината проверки на здрава машина връщат не празен резултат. Нула би била лъжа. Умението не е да намериш нещо - умението е да знаеш кое от намереното има собственик и причина.
И оттук идва единственият съвет, който ще ти свърши работа преди беда: снеми тези числа на своята машина сега, докато е здрава. Запази ги извън нея, с дата. Това е разликата между разследване и гадаене.
Честната част: какво тази статия НЕ може
Всичко дотук се изпълнява върху машината, която разследваш. Това е фундаменталното ограничение и никаква команда не го заобикаля.
Ако лъжата е в ядрото, всеки отговор, който получаваш, минава през лъжеца.
Затова живият оглед има три честни употреби. Първо - хваща по-плитките слоеве, а те са мнозинството от реалните случаи. Второ - събира волатилните данни, които изчезват при изгасване. Трето - произвежда разминавания, които оправдават следващата стъпка.
Това, което не можеш да докажеш от вътре:
- че системата е чиста - недоказуемо по принцип от самата нея
- какво има в паметта, което не се показва през /proc
- какво е било изтрито от диска
- дали логовете, които четеш, са тези, които са били записани
И четирите отиват в част 2 - там се работи с образ на диска и дъмп на паметта, от чиста система, с хешове и без доверие към нищо, което машината твърди за себе си.
От гаража
Кола влиза с лампа и код за бедна смес. Кодът сочи към сондата, сменяш сондата. След двеста километра лампата се връща. Слагаш оригинална част, връща се пак.
Кодът не е излъгал нито веднъж. Той отговаря на въпроса "какво отчитам", а аз го четях като "какво е счупено". Сондата вярно докладваше бедна смес. Причината беше пукнат маркуч след дебитомера, който се отваря чак когато гумата се стопли и се измести на топло. На студено двигателят е примерен. На стенда с изгасен двигател дефектът просто не съществува.
Оттогава не сменям част, преди да съм видял дефекта по втори начин. Кодът е първият източник. Пушекът, сапунената вода или манометърът са вторият. Ако вторият не потвърди, това не е диагноза, а предположение с фактура.
ps е точно такъв инструмент. Не лъже. Отговаря на "какво ми е показано", а ти го четеш като "какво работи". И както онзи маркуч се отваря само на топло, скритият процес съществува само докато машината е включена.
Затова не се гаси.
Чеклист за жива система
- Часът на машината сверен с реалния, отбелязано разминаване
- Изходът се записва извън машината
- Статичен busybox от чист носител е под ръка
- Сравнени /proc и ps, обходени всички номера за скрити процеси
- Проверени exe връзките за изтрити бинарници
- Проверена средата на процесите за LD_PRELOAD
- Сравнени ss и /proc/net/tcp, сокетите върнати до процеси по inode
- Скан отвън, от друга машина, по целия диапазон портове
- Проверени /proc/net/packet и /proc/net/raw
- Търсене по ctime, не само по mtime
- getcap по цялата система, SUID файлове, immutable флагове
- Сравнени mount и /proc/self/mountinfo
- Сравнени /proc/modules и /sys/module, прочетено tainted
- Проверени eBPF програми по тип и точка на закачане, не по брой
- Снет отпечатък на чистото състояние, съхранен извън машината с дата
- Обходени всички места за устойчивост, включително PAM и update-motd.d
- debsums пуснат, ограниченията му записани до резултата
- Ключови бинарници сверени срещу пакет, свален на чиста машина
- Всяка находка записана с час, източник и дали е видяна или предположена
Част 2 идва с диска и паметта: кога се гаси машината и кога не, ред на волатилност, дъмп на паметта, образ с контролни суми, времева линия от файловата система, кои логове се подправят за две секунди и кои не, и защо заразена машина не се чисти, а се преинсталира.
@linux - заявката е приета изцяло. Ако нещо от част 1 е останало на твърде високо ниво, кажи кое и го разписвам по-надолу в част 2.
Ключови думи: linux forensics, rootkit, скрити процеси, /proc, debian, incident response, ebpf, kernel module, debsums, dpkg verify, ld.so.preload, capabilities, persistence, компрометиран сървър
Източници: документация на ядрото за tainted състояние (docs.kernel.org/admin-guide/tainted-kernels.html), документация за scheduler debugfs (docs.kernel.org/scheduler/sched-debug.html), ръководство на debsums (manpages.debian.org), ръководства на dpkg, proc(5), lsof, getcap. Написано и проверявано на Debian 13 (trixie), ядро 6.12. За Debian 12 важат същите пътища.
toni@dtgarage:~$ уау
Тони Ангелчовски | Ексклузивно за DTGaraGe
toni@dtgarage:~$ ЛИЦЕНЗ ЗА КОТКА
Копирането и препубликуването без разрешение не е позволено.
toni@dtgarage:~$ ./support.sh
Подкрепи DTGaraGe и независимото техническо съдържание
toni@dtgarage:~$ █
Тони Ангелчовски | Ексклузивно за DTGaraGe
toni@dtgarage:~$ ЛИЦЕНЗ ЗА КОТКА
toni@dtgarage:~$ ./support.sh
toni@dtgarage:~$ █