惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Microsoft Azure Blog
Microsoft Azure Blog
宝玉的分享
宝玉的分享
博客园 - 【当耐特】
有赞技术团队
有赞技术团队
G
Google Developers Blog
Microsoft Security Blog
Microsoft Security Blog
Apple Machine Learning Research
Apple Machine Learning Research
The Cloudflare Blog
Blog — PlanetScale
Blog — PlanetScale
博客园_首页
L
LangChain Blog
Stack Overflow Blog
Stack Overflow Blog
Last Week in AI
Last Week in AI
Y
Y Combinator Blog
罗磊的独立博客
T
Tailwind CSS Blog
博客园 - 叶小钗
T
The Blog of Author Tim Ferriss
Engineering at Meta
Engineering at Meta
博客园 - 聂微东
博客园 - Franky
B
Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
F
Fortinet All Blogs

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
How To "Gaslight" A Binary
Douxx · 2026-04-30 · via DEV Community

Here is a very simple C code:

int main() {
    uid_t uid = getuid();
    struct passwd *pw = getpwuid(uid);
    printf("uid: %d, user: %s\n", uid, pw->pw_name);
    return 0;
}

Enter fullscreen mode Exit fullscreen mode

It simply prints your identity in the current session. Let's run it:

[local@DouLen] ~ › ./whoami
uid: 0, user: root

Enter fullscreen mode Exit fullscreen mode

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:

export LD_PRELOAD=./fake_uid.so

Enter fullscreen mode Exit fullscreen mode

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:

int getuid() { return 0; }

Enter fullscreen mode Exit fullscreen mode

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.

Why This Works

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:

  • If your library defines a function (e.g., getuid)
  • That definition is used instead of the one in libc

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:

int main() {
    while (1) {
        printf("Haha I'm so evil >:)\n");
        sleep(5);
    }

    return 0;
}

Enter fullscreen mode Exit fullscreen mode

It just loops and prints forever, nothing special.

And now, let's switch to a sysadmin that wants to search for an evil process:

[admin@DouLen] ~ › ps a | grep evil
   7050 pts/2    S+     0:00 ./evil

Enter fullscreen mode Exit fullscreen mode

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.

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;
}

Enter fullscreen mode Exit fullscreen mode

(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.

[admin@DouLen] ~ › LD_PRELOAD=./ps_hide.so ps a | grep evil
   7214 pts/0    S+     0:00 grep --color=auto evil

Enter fullscreen mode Exit fullscreen mode

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:

keylog lib gif

It works the same way: hook fgets from libc, and everything typed into the program flows through your code first.

Variables Are Impractical

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.

1. Hooking New Shells

/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.

2. System-Wide Persistence

/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.

Detecting Malicious Libraries

The Obvious Check

The first thing to do is check both persistence mechanisms directly:

cat /etc/ld.so.preload
ls /etc/profile.d/

Enter fullscreen mode Exit fullscreen mode

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

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:

strace -e openat ps

Enter fullscreen mode Exit fullscreen mode

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.

Static Binaries

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:

ldd $(which ps)
# if it says "not a dynamic executable", LD_PRELOAD won't touch it

Enter fullscreen mode Exit fullscreen mode

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.

a small meme hahaha