Process Hardening // Countermeasures to Process Snooping

Context

On Linux, /proc exposes every user’s processes by default. Any local user can read arguments, environment variables, and memory maps of any process. Tools like pspy, ps -ef, cat /proc/PID/cmdline exploit this visibility to intercept credentials passed as process arguments.

Real-world example: Codify_ENmysqldump -p"password" executed as root exposes the password to any local user.


1 // The Problem: Process Snooping

Why it works

When a program is executed, the kernel writes its arguments (argv) to /proc/PID/cmdline. This file is readable by all users by default.

# Any user can do this:
ps -ef | grep mysql
# root  12345  mysqldump -u root -pSuperSecret123 --all-databases
 
cat /proc/12345/cmdline | tr '\0' ' '
# mysqldump -u root -pSuperSecret123 --all-databases

How pspy exploits this

pspy uses inotify on /proc to receive notifications when a new process is created. As soon as the kernel writes the PID, pspy reads /proc/PID/cmdline and captures the arguments before the process terminates or masks them.

# Typical pspy output:
CMD: UID=0 /usr/bin/mysqldump -u root -pSuperSecret123 --all-databases

2 // Countermeasure: hidepid

What it does

The hidepid option on the /proc filesystem controls process visibility between users.

ValueEffect
hidepid=0Default — everyone sees all processes
hidepid=1Users see other PIDs but not their details (cmdline, environ, maps)
hidepid=2Users see only their own processes — other users’ /proc/PID/ is inaccessible

Apply temporarily (immediate, does not persist across reboot)

sudo mount -o remount,hidepid=2 /proc

Apply permanently (persists across reboot)

# /etc/fstab — add or modify the /proc line:
proc    /proc    proc    defaults,hidepid=2    0    0

Verification

# As a non-root user:
ps aux
# Shows only own processes
 
ls /proc/ | grep -E '^[0-9]+$' | wc -l
# Only own PIDs
 
cat /proc/1/cmdline
# cat: /proc/1/cmdline: Permission denied

Impact on pspy

With hidepid=2, pspy can no longer read /proc/PID/cmdline of root processes. The process snooping attack is completely neutralized.

Limitation

hidepid does not protect processes from the same user. If an attacker obtains a shell as the same UID running the command, they will still see the arguments. Defense must be layered.


3 // Countermeasure: argv masking

The concept

Some programs overwrite argv in memory after startup to hide sensitive arguments. The argv memory area is modifiable by the process itself — by overwriting the bytes, the content visible in /proc/PID/cmdline changes.

Example: MySQL behavior

# Original command:
mysql -u root -pSuperSecret123
 
# What ps shows after argv masking:
mysql -u root -pxxxxxxxxxxxxxxx

MySQL reads the password from argv[2], stores it internally, then overwrites the bytes with x. This happens after the process has started — there is a time window where the password is visible in cleartext.

C demo

// argv_mask_demo.c
#include <stdio.h>
#include <string.h>
#include <unistd.h>
 
int main(int argc, char *argv[]) {
    printf("PID: %d\n", getpid());
    printf("Original arguments:\n");
    for (int i = 0; i < argc; i++)
        printf("  argv[%d] = %s\n", i, argv[i]);
 
    // Masking: overwrite every argument with 'X'
    for (int i = 1; i < argc; i++)
        memset(argv[i], 'X', strlen(argv[i]));
 
    printf("Arguments masked. Verify with:\n");
    printf("  cat /proc/%d/cmdline | tr '\\0' ' '\n", getpid());
 
    // Keep process alive for inspection
    pause();
    return 0;
}
gcc -o argv_mask_demo argv_mask_demo.c
./argv_mask_demo password123 secret &
# Before masking (time window):
cat /proc/$(pgrep argv_mask)/cmdline | tr '\0' ' '
# ./argv_mask_demo password123 secret
 
# After masking:
cat /proc/$(pgrep argv_mask)/cmdline | tr '\0' ' '
# ./argv_mask_demo XXXXXXXXXXX XXXXXX

Race condition

Why argv masking is not enough

Masking happens after execve(). Between the moment the kernel creates the process and the moment userspace code overwrites argv, there is a time window. pspy with inotify reads /proc/PID/cmdline within this window, capturing the original arguments.

Timeline:

execve()            → kernel writes argv to /proc/PID/cmdline
  ↓ (window)         → pspy reads cmdline → password in cleartext
main() → memset()   → argv overwritten with 'X'
  ↓                  → ps/cat now show 'XXXXXXX'

Argv masking reduces the exposure window but does not eliminate it.


4 // Best Practice: Never Pass Credentials as Arguments

The correct defense eliminates the problem at the root: credentials must never appear in argv.

MySQL/MariaDB — configuration file

# ~/.my.cnf (permissions: 600)
[client]
user=root
password=SuperSecret123
chmod 600 ~/.my.cnf
mysqldump --all-databases    # reads credentials from file

No -p argument in the process — nothing to read from /proc/PID/cmdline.

Environment variables (use with caution)

export MYSQL_PWD='SuperSecret123'
mysqldump -u root --all-databases

/proc/PID/environ is readable with the same permissions as cmdline. With hidepid=0, environment variables are exposed the same way. Prefer configuration files.

Bash scripts // read from file, not from argument

# Wrong:
DB_PASS=$(/usr/bin/cat /root/.creds)
mysqldump -u root -p"$DB_PASS" --all-databases
 
# Correct:
mysqldump --defaults-file=/root/.my.cnf --all-databases

5 // Hardening Checklist

  • Enable hidepid=2 on /proc in /etc/fstab
  • Verify no scripts pass credentials as process arguments
  • Replace -p"password" with --defaults-file or ~/.my.cnf for MySQL
  • Set 600 permissions on all files containing credentials
  • Quote variables in bash comparisons: [[ "$a" == "$b" ]]
  • Verify with ps auxwwe and pspy that no process leaks secrets