From time to time developers want to modify the command line of program that is shown in ps or top utilities.
There is no API in linux to modify the command line.
This article is linux specific but I hope that following these steps you can do it in almost every OS(BTW, FreeBSD has setproctitle routine that should do the job).
Both ps and top tools comes from procps package in all linux distribution I worked with. So they have the same base and let's stop on ps because it's a bit easier to investigate it.
To figure out how ps gets information about the processes let's simply trace it
$strace ps aux 2>&1 1>/dev/null|grep $$
stat64("/proc/4391", {st_mode=S_IFDIR|0555, st_size=0, ...}) = 0
open("/proc/4391/stat", O_RDONLY) = 6
read(6, "4391 (bash) S 4388 4391 4349 348"..., 1023) = 222
open("/proc/4391/status", O_RDONLY) = 6
open("/proc/4391/cmdline", O_RDONLY) = 6
readlink("/proc/4391/fd/2", "/dev/pts/1", 127) = 10
read(6, "4760 (strace) S 4391 4760 4349 3"..., 1023) = 208
read(6, "4761 (grep) S 4391 4760 4349 348"..., 1023) = 196
read(6, "grep\0004391\0", 2047) = 10
As expected it reads information from procfs.
$cat /proc/$$/cmdline
bash
Now prepare yourself, I'm going to dig into the kernel sourses.
Looking into fs/proc/base.c I found desired function proc_pid_cmdline that is used to show the command line in /proc/<pid>/cmdline
The part of it that we are interested:
static int proc_pid_cmdline(struct task_struct *task, char * buffer) {
...
struct mm_struct *mm = get_task_mm(task);
...
len = mm->arg_end - mm->arg_start;
if (len > PAGE_SIZE)
len = PAGE_SIZE;
res = access_process_vm(task, mm->arg_start, buffer, len, 0);
// If the nul at the end of args has been overwritten, then
// assume application is using setproctitle(3).
if (res > 0 && buffer[res-1] != '\0' && len < PAGE_SIZE) {
len = strnlen(buffer, res);
if (len < res) {
res = len;
} else {
len = mm->env_end - mm->env_start;
if (len > PAGE_SIZE - res)
len = PAGE_SIZE - res;
res += access_process_vm(task, mm->env_start, buffer+res, len, 0);
res = strnlen(buffer, res);
}
}
...
That's funny but in comments mentioned setproctitle(3). After I saw these lines I tried to find setproctitle for linux but failed. It's interesting why is mentioned here as it available in FreeBSD but not in linux.
Anyway let's move forward.
The most interesting parts here are
len = mm->arg_end - mm->arg_start;
if (len > PAGE_SIZE)
len = PAGE_SIZE;
res = access_process_vm(task, mm->arg_start, buffer, len, 0);
access_process_vm, defined in mm/memory.c, accesses another process' address space. The prototype:
int access_process_vm(struct task_struct *tsk, unsigned long addr, void *buf, int len, int write)
Fifth argument
write is a flag, if it is 0 then access_process_vm reads
len bytes of the process' memory to
buf starting from address
addr.
Going back to proc_pid_cmdline we can see that
start address of the string with the command line is mm->arg_start and its
length is mm->arg_end - mm->arg_start but not bigger than PAGE_SIZE. PAGE_SIZE is set to 4096 bytes as defined in include/asm-i386/page.h
#define PAGE_SHIFT 12
#define PAGE_SIZE (1UL << PAGE_SHIFT)
Well, now we know that we have 4KB where we can write.
Who fills the memmory between mm->arg_start and mm->arg_end and what is stored there?
I hope everybody uses ELF binary format now. So let's go to fs/binfmt_elf.c
The name of the function that creates process environment is create_elf_tables.
The part of it we are actually interested in:
static int
create_elf_tables(struct linux_binprm *bprm, struct elfhdr *exec,
unsigned long load_addr, unsigned long interp_load_addr) {
...
/* Populate argv and envp */
p = current->mm->arg_end = current->mm->arg_start;
while (argc-- > 0) {
size_t len;
if (__put_user((elf_addr_t)p, argv++))
return -EFAULT;
len = strnlen_user((void __user *)p, MAX_ARG_STRLEN);
if (!len || len > MAX_ARG_STRLEN)
return -EINVAL;
p += len;
}
if (__put_user(0, argv))
return -EFAULT;
current->mm->arg_end = current->mm->env_start = p;
while (envc-- > 0) {
size_t len;
if (__put_user((elf_addr_t)p, envp++))
return -EFAULT;
len = strnlen_user((void __user *)p, MAX_ARG_STRLEN);
if (!len || len > MAX_ARG_STRLEN)
return -EINVAL;
p += len;
}
if (__put_user(0, envp))
return -EFAULT;
current->mm->env_end = p;
...
According to create_elf_tables argv points to current->mm->arg_start and current->mm->arg_end points to the end of the environment(envp).
To modify cmdline of the process you have to overwrite memory between current->mm->arg_start and current->mm->arg_end and to keep program's integrity move its environment.
Looking into the sources of getenv/setenv we can see that they access **environ variable(environ(7)). environ is declared in the <unistd.h> but it's preffered to declare it in the user program as
extern char **environ;
The following code rewrites cmdline of the process and moves environment to it's new 'home'.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <limits.h>
#include <sys/user.h>
extern char **environ;
int main(int argc, char **argv)
{
unsigned int pid = getpid();
char proc_pid_cmdline_path[PATH_MAX];
char cmdline[PAGE_SIZE];
sprintf(proc_pid_cmdline_path, "/proc/%d/cmdline", pid);
FILE *proc_pid_cmdline = fopen(proc_pid_cmdline_path, "r");
fgets(cmdline, PAGE_SIZE, proc_pid_cmdline);
fclose(proc_pid_cmdline);
printf("%s : %s\nenvironment variable HOME = %s\n", proc_pid_cmdline_path, cmdline, getenv("HOME"));
int env_len = -1;
if (environ)
while (environ[++env_len])
;
unsigned int size;
if (env_len > 0)
size = environ[env_len-1] + strlen(environ[env_len-1]) - argv[0];
else
size = argv[argc-1] + strlen(argv[argc-1]) - argv[0];
if (environ)
{
char **new_environ = malloc(env_len*sizeof(char *));
unsigned int i = -1;
while (environ[++i])
new_environ[i] = strdup(environ[i]);
environ = new_environ;
}
char *args = argv[0];
memset(args, '\0', size);
snprintf(args, size - 1, "This is a new title and it should be definetely longer than initial one. Actually we can write %d bytes long string to the title. Well, it works!", size);
proc_pid_cmdline = fopen(proc_pid_cmdline_path, "r");
fgets(cmdline, PAGE_SIZE, proc_pid_cmdline);
fclose(proc_pid_cmdline);
printf("%s : %s\nenvironment variable HOME = %s\n", proc_pid_cmdline_path, cmdline, getenv("HOME"));
return 0;
}
The output should be
$./chcmdline
/proc/5865/cmdline : ./chcmdline
environment variable HOME = /home/niam
/proc/5865/cmdline : This is a new title and it should be definetely longer than initial one. Actually we can write 1843 bytes long string to the title. Well, it works!
environment variable HOME = /home/niam
There is an option to expand memory region that we can overwrite. Just assign environment variable for the process:
$ENV_VAR=$(perl -e 'print "0" x 4096;') ./chcmdline
/proc/5863/cmdline : ./chcmdline
environment variable HOME = /home/niam
/proc/5863/cmdline : This is a new title and it should be definetely longer than initial one. Actually we can write 5948 bytes long string to the title. Well, it works!
environment variable HOME = /home/niam
That small perl script prints "0" 4096 times to stdout, so as we can see the memory size of environment grew up to 5948 bytes.
This code might be portable to other OSes, I don't know. In general in POSIX system with gcc compiler you might be successfull with this code.