argv silliness

Most C programmers should be aware that the argv argument to main() is a NULL terminated list of strings, where the first element is the name of the program. On Linux there is an odd “feature” which allows the list to be empty. From the Linux execve(2) manpage:

On Linux, argv can be specified as NULL, which has the same effect as specifying this argument as a pointer to a list containing a single NULL pointer. Do not take advantage of this misfeature! It is non standard and non portable: on most other UNIX systems doing this will result in an error (EFAULT).

This allows us to execute an application with argv[0] == NULL. Many applications, including several setuid applications, make the assumption that argv[0] is always a valid pointer. While I haven’t found any potential exploits using this, it does allow for some amusing behaviour from setuid binaries.

First we want to set up an easy to use environment for calling execve and trapping the executed process in a debugger. Python’s os library will let us call execve() with an empty argv list. First we want to start a Python interpreter and get its pid so that we can attach a debugger to it:

$ python
>>> import os
>>> os.getpid()
18452

On Linux gdb is able to catch fork and exec events and attach the the debugger to the forked or execed process. There is a full description of these features here. For this example, we just need to catch the exec event and attach to the new process. The default settings for gdb will stop execution at the beginning of the exec’ed process (note you will need to run gdb as root if you want to catch an exec of a setuid binary):

$ gdb
(gdb) catch exec
(gdb) attach 18542
(gdb) c

The debugger is now attached to the Python shell. We now need to pick a target binary to execute. Unfortunately many setuid binaries do the following somewhere early on in main():

progname = basename(argv[0]);

With the POSIX version of basename() this would be okay, since the standard says that passing a NULL pointer will return the string “.”. The basename() function has some historical clunkiness. Because it returns a string, rather than using a user supplied buffer and length, implementations may modify the path name argument in place. The GNU C library attempted to fix this by providing their own version, also called basename() to avoid confusion. One difference, which the basename(3) manpage fails to mention, is that GNU basename() will segfault if passed a NULL pointer.

After searching around on a stock Ubuntu system for setuid binaries that looked promising for passing argv[0] == NULL to I found pkexec. pkexec is part of the Polkit package, and allows a binary to be executed as another user (similar to sudo). We call execve() passing a empty argv list and a single dummy environment varaible:

>>> os.execve("/usr/bin/pkexec", [], {"FOO":"aaaaaaaaa"})

The exec event is caught, and gdb will stop inside the pkexec binary in the function _start():

process 18452 is executing new program: /usr/bin/pkexec

Catchpoint 1 (exec'd /usr/bin/pkexec), 0x00007f634eafc6b0 in _start ()
   from /lib64/ld-linux-x86-64.so.2
(gdb) where
#0  0x00007f634eafc6b0 in _start () from /lib64/ld-linux-x86-64.so.2
#1  0x0000000000000000 in ?? ()

There is a bunch of stuff that happens before main() is called. We want to break on the function __libc_start_main(), which is what calls main():

(gdb) b __libc_start_main
(gdb) c
Continuing.
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Breakpoint 3, __libc_start_main (main=0x402010, argc=0, 
    ubp_av=0x7fff44607b98, init=0x403660, fini=0x4036f0, 
    rtld_fini=0x7f7a3ad43740 <_dl_fini>, stack_end=0x7fff44607b88)
    at libc-start.c:96

When a process is executed, the arguments and the environment are passed as a continuous chunk of memory separated by a NULL pointer in the ubp_av argument. The __libc_start_main() function separates them out as follows (simplifed):

#define argv ubp_av
char **evp = &ubp_av[argc + 1];

In gdb we can see that the environment is immediately after argv, which contains only a single NULL pointer:

(gdb) p *ubp_av
$1 = 0x0
(gdb) p *(ubp_av + 1)
$2 = 0x7fff44607fda "FOO=aaaaaaaaa"

Looking at the main() function for pkexec we see that it parses its command line arguments as follows (simplified):

for (n = 1; n < (guint) argc; n++)
  {
    ...
  }

  ...
 
path = g_strdup (argv[n]);
if (path == NULL)
  {
    usage (argc, argv);
    goto out;
  }

We never enter the body of the for loop because argc == 0. At the end of the loop n == 1, which means that g_strdup() is going to copy from the environment rather than argv, resulting in this:

Cannot run program FOO=aaaaaaaaa: No such file or directory

By creating an executable in the local directory called “FOO=aaaaaaaaa” we can get pkexec all the way to actually trying to run it. However, because we can’t pass any option arguments, the executing user will default to root, which presumably we don’t have the password for. Although getting a setuid binary to use envp in place of argv is amusing, a quick skim of the pkexec source doesn’t show anything that is likely to be vulnerable to having argv[0] be NULL. For a binary to be vulnerable would probably require some reasonably unorthodox code. That said, a number of binaries, notably the sudo suite, do protect against the case where argc == 0.

14 comments

  1. Pingback: A bug lurking for 12 years gives attackers root on most major Linux distros - alltimenews.org
  2. Pingback: A bug lurking for 12 years gives attackers root on every major Linux distro – SHAQ HAX
  3. Pingback: A bug lurking for 12 years gives attackers root on most major Linux distros - Amogh-it-news
  4. Pingback: A bug lurking for 12 years gives attackers root on most major Linux distros – Lacorte News
  5. Pingback: A Bug Lurking For 12 Years Gives Attackers Root On Every Major Linux Distro - ThreatsHub Cybersecurity News
  6. Pingback: Das uralte Loch ermöglicht es Ihnen, Root-Rechte zu erhalten - Gamingdeputy Germany
  7. Pingback: A bug lurking for 12 years gives attackers root on most major Linux distros – xxp5 grooves
  8. Pingback: Vulnerabilidade de 12 anos permite escalonar privilégios de root
  9. Pingback: PwnKit exploits land: Beware this critical bug in all major Linux distros
  10. Pingback: A bug lurking for 12 years gives attackers root on most major Linux distros - AI Caosuo
  11. Pingback: Age Hole ermöglicht es Ihnen, Root-Rechte zu erhalten - Gamingdeputy Germany
  12. Robert

    I’ve been programming in C for 30 years and I’ve been always taking it for granted that there was always at least one string present in argv. I never knew it was possible for main() to be called without at least the executable name in argv[0]!

Leave a reply to StacyJ Cancel reply