1

Consider following C code (x86_64)

#include <unistd.h>
int main()
{
    execve("/bin/ls", 0, 0);
}

I compiled as gcc a.c and executed; I got a SIGABRT with error

A NULL argv[0] was passed through an exec system call.  
Aborted

Next running on gdb, at first I also got a SIGABRT, however I did second run and it worked!

Starting program: /bin/ls 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Why?


I tested for /bin/sh and found it always worked with *argv[] = NULL ...
Again I wrote some executable file (without any parameter needed) to test and found all them work.

So I guess only /bin/sh or other shells would work with *argv[] set to NULL, other files (like /bin/ls) fail or behave unexpectedly.

Toby Speight
  • 27,591
  • 48
  • 66
  • 103
poming
  • 378
  • 1
  • 2
  • 12
  • read about [undefined behavior](https://en.wikipedia.org/wiki/Undefined_behavior). – Sourav Ghosh Aug 01 '17 at 06:08
  • Read https://linux.die.net/man/2/execve and see what parameters it expects. – Tony Tannous Aug 01 '17 at 06:16
  • Yeah I know it is UB and also what parametes execve() expects, but is there any way to figure out why it work, or just luck. I'm solving some security challenge and if I can find out why it works that could be a great improvement for my shellcode. – poming Aug 01 '17 at 06:20
  • @poming What OS are you running? – Erki Aring Aug 01 '17 at 08:31

2 Answers2

3

The man page for the execve() system call says

The argv and envp arrays must each include a null pointer at the end of the array.

If your program doesn't conform to those requirements, there's no certainty of how well things will go from that point on. If it "works" for some programs, that's just bad luck.


The man page also says

By convention, the first of these strings (i.e., argv[0]) should contain the filename associated with the file being executed.

That convention is rather strong (mandated by POSIX), so programs that fail to do so can be considered buggy. It's probably a good idea for your main() to test it's been called correctly if you're going to rely on argv[0] so you can fail with a nice error message rather than a fault, but not all programs do.

Toby Speight
  • 27,591
  • 48
  • 66
  • 103
  • 1
    The Linux man page documents the fact that *Linux* treats `argv=NULL` as a valid empty list, instead of returning `EFAULT`. https://man7.org/linux/man-pages/man2/execve.2.html#NOTES. This is common in shellcode because it's already non-portable and it saves bytes. And actual shells like /bin/sh do work if invoked with an empty argv (and thus argc=0). [Why would this shellcode work if passes 0 as the environment pointer for execve?](https://stackoverflow.com/q/70133251) The Linux man page recommends against it only for portability reasons; it is guaranteed to work on Linux. – Peter Cordes Nov 27 '21 at 09:06
2

Linux specifically treats argv=NULL or envp=NULL as a valid empty list, instead of returning -EFAULT1. This is documented in the man page:
https://man7.org/linux/man-pages/man2/execve.2.html#NOTES.

This is not guaranteed by POSIX; some other OSes work this way but some don't.

Note 1: (Or if going through the libc wrapper, returning -1 and setting errno = EFAULT).

This is common in shellcode because it's already non-portable, and it saves machine-code bytes in the payload. (Or in what you have to construct as part of a ROP attack). But otherwise it's a very bad idea. The Linux man page strongly recommends against it only for portability reasons; it is actually guaranteed to work on Linux. (The execve itself; the program being invoked may be unhappy to find argc=0 and argv[0] == NULL, as well as an empty environment.)


Actual shells like /bin/sh do work if invoked this way, which is the other part of why it's viable for shellcode not to construct a {"/bin/sh", NULL} array of char* and pass a pointer to that.

As Toby explained, it's not totally surprising for /bin/ls to bail out in that case. Even if you had used execve("/bin/sh", (char*[]){NULL}, (char*[]){NULL}); to use execve portably, you'd still be starting the program with argc=0 exactly the way you are under Linux.

TL:DR: don't do it unless it's part of some code-size or other hacky reason for not doing it the normal way.

Toby Speight
  • 27,591
  • 48
  • 66
  • 103
Peter Cordes
  • 328,167
  • 45
  • 605
  • 847