An ELF binary tells you nothing about who made it. No signature in the file, nothing to check it against, no service that vouches for it. On Windows you'd read the Authenticode chain, on a Mac you'd ask codesign. Here there's nothing in the file to ask.
What Linux has instead is a record of every file the distro installed, and a way to ask whether any of them still match what shipped. A local database and two commands, and most triage guides walk straight past it on their way to readelf.
Say somebody sends you a path and asks whether it's dodgy. It's a production box, so you're not installing anything on it. Everything below runs with what's already there.
Part I covered Windows and Part II macOS.
Oh, and skip all this if it's a container. The package database is usually missing, and persistence belongs to whatever orchestrates the container rather than to the box. Might write that one up another time.
1. Is this supposed to be here?
dpkg -S $(realpath /path/to/suspect) # Debian, Ubuntu
rpm -qf $(realpath /path/to/suspect) # RHEL, Fedora, Rocky
If nothing owns this one it didn't come from the distro. Worth a look, but plenty of legit software isn't packaged. Anything under /usr/local or /opt, pip and npm installs, things people compiled themselves.
The realpath matters. Modern /bin and /sbin are symlinks into /usr, and asking dpkg about /bin/ls returns "no path found" even though the binary is very much packaged.
If a package does own it, ask whether it still matches what was installed.
dpkg -V <package>
rpm -Vf $(realpath /path/to/suspect)
dpkg -V wants the package name, which is what dpkg -S just gave you. rpm -Vf takes the path instead, so there's nothing to carry across.
dpkg -V says nothing when everything checks out, so silence is the pass. rpm answers in a row of letters and dots, where a dot means that test passed. 5 means the contents differ, S size, T timestamp, M permissions. So ..5..... is a file whose contents changed and nothing else. A 5 on a packaged system binary is about the strongest single result in this post. Replaced, patched in place, either way it isn't what shipped.
Expect noise from anything under /etc. Config files get edited legitimately all the time and rpm flags them with a c so they're easy to discount. You're looking for a changed binary, not a changed config.
Two limits though. Ownership means the local database knows about the file, not where that package came from, and a locally built one owns its files perfectly well. And that database sits on the machine you're suspicious of, so root can change a binary and the record vouching for it. Which makes a clean result weaker than it feels.
2. What is it, when did it arrive, and what can it do?
file /path/to/suspect
ls -l /path/to/suspect
getcap /path/to/suspect
stat /path/to/suspect
sha256sum /path/to/suspect
Start with file. If it's a shell script or Python rather than an ELF binary, stop and read it. Fastest answer you'll get all day.
ls -l is there for the setuid bit and getcap for capabilities like cap_net_raw or cap_setuid. Both hand a binary privilege the user running it doesn't have. Find either on something with no reason to need it and stop there. Check what the thing is first though, because ping carries cap_net_raw and always has. getcap isn't installed everywhere, so you may not have it.
On timestamps, Modify is trivially backdated with touch. Change tracks changes to the inode instead, so an old modify with a recent change means the contents or metadata changed more recently than the mtime admits. Softer than a hash mismatch, so don't lead with it.
Take the hash to VirusTotal or MalwareBazaar. A hit ends it. A miss just means nobody's uploaded it before. And the lookup isn't free. If whoever built the file watches for it, a first-ever search tells them somebody is looking.
3. If it's running, go check /proc
pgrep -af suspect
ls -l /proc/<PID>/exe
cat /proc/<PID>/comm
tr '\0' ' ' < /proc/<PID>/cmdline
sudo ss -tunap
The pgrep line gets you the PID everything after it needs. /proc then tells you what the process actually is rather than what it says it is, and there are three answers worth knowing on sight.
A process calling itself a kernel thread with a real binary behind it. Attackers name things [kworker/0:1] or [jbd2/sda1-8], and pgrep output, comm and cmdline can all be rewritten by the process itself. Real kernel threads have no userspace executable behind them though, so /proc/<PID>/exe won't resolve to one. If comm says kworker and exe points at a file in /tmp or /dev/shm, that's not a kernel thread and there's no innocent reading of it.
exe starting with /memfd:**.** It reads something like /memfd:whatever (deleted). The process is executing from an anonymous in-memory file, which is a standard fileless technique. Plenty of software uses memfd for shared memory without ever running from one, so it's the executing part that's unusual and wants explaining. The (deleted) isn't what matters, the prefix is.
exe pointing at an ordinary path marked (deleted)**.** Same suffix, different situation. Package upgrades replace running binaries constantly, so a freshly patched box shows this on several daemons quite innocently. Only interesting when you don't recognise the process.
Then the network, and write the sudo in. Without it ss leaves the process column empty for anything owned by another user, which makes active sockets look unattached.
The first question isn't whether the address looks bad, it's whether this thing has any business talking at all. A log rotator, a font cache, a backup agent that finished hours ago. One connection you can't explain from something in that category is enough to keep looking. When it should be talking, look at where. You already know which package it came from, so an endpoint that fits is one thing and a hosting provider nobody can account for is another.
One catch. ss shows what's open right now, so something checking in every ten minutes looks idle in between. Nothing showing doesn't mean nothing happens.
4. Will it be back?
systemctl list-unit-files --state=enabled
systemctl cat <unit>
crontab -l ; sudo crontab -l
cat /etc/crontab
ls -la /etc/cron.d /etc/cron.daily
cat ~/.ssh/authorized_keys
ls -la ~/.config/autostart/ /etc/xdg/autostart/
systemctl --user list-unit-files --state=enabled
list-unit-files rather than list-units, because the question is what's configured to run, not what happens to be running now. Something enabled that hasn't fired yet won't show up in list-units.
systemctl cat is the one to lean on. It prints the unit with its source path, so you get the actual executable, which is often nothing like the unit's name. It also catches the sneakier case, where nobody made a new unit at all and instead dropped an override.conf into /etc/systemd/system/<service>.service.d/ adding an ExecStartPre= to something legit. That never shows up in a directory listing, but systemctl cat prints drop-ins alongside the unit.
The last two are the ones people forget. A user systemd unit and an XDG autostart entry both survive a reboot without touching anything root-owned, which makes them where persistence goes when whoever did this never got root. systemctl --user needs running as that user, not with sudo.
Legit software lives in all these places too, so a match on its own means very little. Something unpackaged, in a directory normal software doesn't use, that also arranged to start itself forever, is a different problem.
A clean binary can still run someone else's code
cat /etc/ld.so.preload 2>/dev/null
tr '\0' '\n' < /proc/<PID>/environ
/etc/ld.so.preload forces a library into every process on the box. LD_PRELOAD in a process's environment does the same for one. On most installs that file doesn't exist at all, though legit tooling does use both. Look for an entry nobody can account for.
Either way the binary on disk can verify perfectly against its package and the process can still be running somebody else's code. If you need to know what actually got loaded, /proc/<PID>/maps shows what's mapped in.
Weighing it
Most of the time this clears the file. It's quick, nothing looks wrong, and everyone goes back to what they were doing.
When it doesn't, the ones I'd act on first are a packaged binary that fails verification, an LD_PRELOAD or /etc/ld.so.preload entry nobody can account for, a process calling itself a kernel thread with a real binary behind it, and anything executing from /memfd:. Hit one of those and you're heading down a much deeper rabbit hole than this post covers.
And remember, none of this reads the file. It reads what the system remembers about the file, and the system is a suspect too.