Recently, I hosted an internal CTF event in my company. I wanted to include a challenge which would include some Windows Exploitation. Custom-made binaries are not fun, I wanted a challenge that would be bit realistic but not too difficult. Since we are living in times of ‘Quarantine’, it gave me an idea of having some anti-virus exploitation challenge. While searching for some recent anti-virus exploits, I found QuickHeal’s CVE-2017-5005. I downloaded the PoC, ran it on my test environment of Windows XP SP3 and it didn’t work. After some tinkering, I got it working and found it easy enough to give it as a challenge. Since the CTF has finished now, I’m sharing this technical analysis of the vulnerability.

Mach-O File Format

As mentioned in the original exploit, the vulnerability lies in the LC_UNIXTHREAD.cmdsize parameter of Mach-O files. To understand what this is, I’ll first give a brief of Mach-O file format.

An overview

A Mach-O file would contain three components:

  • Header
  • Load Commands
  • Data

Header contains some basic information about the file and is not much of our concern here. Load Commands are important because LC_UNIXTHREAD is one. We will cover them in detail in next section.

Data is basically collection of ‘Segments’. These segments are further a collection of ‘Sections’. Segments are analogous to .text, .data, etc. The code or data that an executable usually have will fall into one of the sections in these segments. Again, not going into much details here as it’s not required.

Load Commands

Load Commands are special structures that are sort of commands to the loader. They define the overall structure of the file, location of segments, entrypoint, symbol tables, etc. For all of these tasks, there are different kind of load commands, for instance, we have Segment Load Command that defines segments, Symbol Load Command for symbol table info, Thread Load Command for thread info and dozens more.

A Load Command would contain a minimum of following two integers and can contain some other content depending on its type:

struct load_command {
    uint32_t cmd;       /* type of load command */
    uint32_t cmdsize;   /* total size of command in bytes */
};

cmdsize would define the size of entire command. The cmd field is a constant for type of load command. You can look at these types in loader.h file. Some that concerns us are below:

#define LC_SEGMENT      0x1     /* segment of this file to be mapped */
#define LC_UNIXTHREAD   0x5     /* unix thread (includes a stack) */
#define LC_LOAD_DYLIB   0xc     /* load a dynamically linked shared library */
#define LC_SEGMENT_64   0x19    /* 64-bit segment of this file to be mapped */

Thread State

Since the overflow is in LC_UNIXTHREAD load command, I’ll cover only its internals here. So what exactly does this load command do? If you have some programming background in C language, you’ll know there is a main() function that defines the entrypoint of the program. In recent MACH-O files, we have a load command LC_MAIN which defines this entrypoint. In earlier versions, we had LC_UNIXTHREAD for this. LC_UNIXTHREAD does this by having a ‘thread state’ of entrypoint. Thread state actually is just a snapshot of all the registers, it stores the value of all the registers at a given state.

The LC_UNIXTHREAD or LC_THREAD structure looks like this:

struct thread_command {
    uint32_t    cmd;        /* LC_THREAD or  LC_UNIXTHREAD */
    uint32_t    cmdsize;    /* total size of this command */
    /* uint32_t flavor           flavor of thread state */
    /* uint32_t count           count of longs in thread state */
    /* struct XXX_thread_state state   thread state for this flavor */
    /* ... */
};

Like other load commands, this also contains cmd and cmdsize integers. After these two, we have three components- two other integers flavor and count, and thread_state.

There are different type or ‘flavors’ of thread states; flavor here defines the type of thread state this load command is storing. Here is a list of flavors from thread_status.h file:

#define x86_THREAD_STATE32      1
#define x86_FLOAT_STATE32       2
#define x86_EXCEPTION_STATE32   3
#define x86_THREAD_STATE64      4
#define x86_FLOAT_STATE64       5
#define x86_EXCEPTION_STATE64   6

LC_UNIXTHREAD would save x86_THREAD_STATE64 flavor of thread. Let’s quickly look at the associated thread_state structure from _structs.h to get an idea of how it looks:

_STRUCT_X86_THREAD_STATE64
{
    __uint64_t    __rax;
    __uint64_t    __rbx;
    __uint64_t    __rcx;
    __uint64_t    __rdx;
    __uint64_t    __rdi;
    __uint64_t    __rsi;
    __uint64_t    __rbp;
    __uint64_t    __rsp;
    __uint64_t    __r8;
    __uint64_t    __r9;
    __uint64_t    __r10;
    __uint64_t    __r11;
    __uint64_t    __r12;
    __uint64_t    __r13;
    __uint64_t    __r14;
    __uint64_t    __r15;
    __uint64_t    __rip;
    __uint64_t    __rflags;
    __uint64_t    __cs;
    __uint64_t    __fs;
    __uint64_t    __gs;
};

The thread state is just collection of 21 integers, one for each register.

Breaking Down the PoC

Now that we have enough theory of Mach-O internals, let’s break the PoC down and look at them in practice. I’ll mainly take help from the combination of macho_parser from penvirus and xxd to showcase the internals.

The Header and Load Commands

The macho_parser exposes certain APIs that we have to call from a python script. So, let’s write one to extract the header and load commands:

from macho_parser.macho_parser import MachO

with MachO('CVE-2017-5005.mach') as m:
    print 'Header:'
    print m.get_header()

    print ''
    print 'Load Commands:'
    for c in m.get_load_commands():
        print c

This script would give following output:

From this output we can see that we have four load commands with cmd of 25 or 0x19. Scroll up and check that 0x19 is cmd for LC_SEGMENT_64 or Segment Load Command. So, we can say that we have four segments in this file. After this, we see that we have a weird value of cmd but let’s ignore that. Our main concern is LC_UNIXTHREAD and it should have a cmd of 0x5.

The LC_UNIXTHREAD

We eventually find the cmd of 0x5 after few entries. It has a cmdsize of 1208 but it won’t be a valid value as PoC would be having an inflated value to cause an overflow. Let’s look at this load command closely using xxd:

Breaking the output down in following pointers:

  • Red Underline: The cmd and cmdsize of LC_UNIXTHREAD
    • Yellow Highlight: The cmd with value of 0x5. Consumes 4 bytes.
    • Pink Highlight: The cmdsize with value of 0x4b8 or 1208. Consumes 4 bytes.
  • Blue Underline: The thread state
    • Blue Highlight: The flavor. It’s value is 0x4 which corresponds to x86_THREAD_STATE64. Consumes 4 bytes.
    • Orange Highlight: The count. It’s value is 0x2a or 21. Consumes 4 bytes.
    • Rest is the _STRUCT_X86_THREAD_STATE64, the collection of 21 integers (int64 actually). Consumes 21*8 bytes.
  • Green and Yellow Underline: Other load commands

In an ideal scenario, cmdsize should have the length of (4 + 4 + 4 + 4 + 21*8) bytes which equals 184 or 0xb8 bytes. But we have 0x4b8 mentioned there which is resulting in an overflow. How does this overflow look? Let’s see.

The Overflow

Let’s attach our debugger and analyse the crash. Following is the screenshot:

Also, for context, following is the hexdump of the region where shellcode is stored in PoC:

Comparing these two, we can observe that the contents of our PoC are in the stack. ESP is currently pointing to the NOPs highlighted in green in hexdump. If we look at the stack again, we see the address 10007056 (highlighted in pink in hexdump). EIP is pointing to a nearby address, so maybe 10007056 was intended to be the address of JMP ESP and then it would have jumped to the NOPs and started executing the shellcode? Let’s confirm this assumption by placing a breakpoint on 10007056 and running the exploit again.

And the execution hits our breakpoint! We don’t have JMP ESP at 10007056, most probably because the PoC was created for a different version of the product. Let’s change the instructions at 10007056 to JMP ESP and resume the execution.

We can observe that the execution continues and we have a calculator running under the context of vulnerable service.

Exploit Development

Now that we’ve broken down the PoC to understand the point of overflow, let’s replicate the crash and create our own exploit that will run start a bind shell.

If we notice, the location of LC_UNIXTHREAD was 0xc70 in the PoC (refer to first hexdump). The location of bytes overwriting EIP is 0xdb0 (refer to the second hexdump).

0xdb0 - 0xc70 = 0x140

So we can assume that QuickHeal service is creating a buffer of maximum 0x140 or 320 bytes for thread_state of LC_UNIXTHREAD. Let’s confirm this assumption by creating a PoC with cmdsize as 320 bytes, then 324 bytes.

Here I’ve modified the cmdsize to 0x140 (320) bytes.

While scanning this file, we notice the application doesn’t crash.

Now let’s scan a file with cmdsize of 0x144 (324 bytes).

Aaaand… wait, we don’t have a crash? Our assumption of 320 bytes is wrong? Ummm… Let’s parse our PoC to see if cmdsize is indeed 324.

Okay, the parser failed. It did show cmdsize as 324 but the next load command is corrupt? If we look carefully, since we have cmdsize of LC_UNIXTHREAD as 324, the parser should be expecting a load command to start at 325th (or 0xc70 + 0x144 = 0xdb4th) byte. What do we have at 0xdb4th byte?

It is 0x74000000 or 1946157056, same as the cmd of the corrupt load command. So we know the problem- the load commands present after the LC_UNIXTHREAD load command. Let’s modify our PoC to remove all those load commands. Also, in an anticipation that bytes 320-324 will overwrite the EIP, I’ll place 0xcccccccc in these 4 bytes.

Double checking with the parser:

Let’s scan this file. And we finally notice the crash, with 0xcccccccc in EIP.

Cool! Now, all we have to do is increase the cmdsize to accommodate for our shellcode, replace 0xcccccccc with address to JMP ESP and place our bind shell afterwards.

The pink highlight is the value of cmdsize. Green highlight is address to JMP ESP. Orange highlight is bind shell to port 4444 shellcode. Let’s finally scan this exploit now.

Boom! It’s working!