KSCrash Source Code Analysis

Keywords: iOS Unix Linker Mac

0x01 installation process

1.1 Bricks and Jades

KSCrashInstallationStandard* installation = [KSCrashInstallationStandard sharedInstance];
installation.url = [NSURL URLWithString:@"http://put.your.url.here"];
[installation install];

The above code is the installation code of KSCrash, [KSCrash Installation Standard init] the underlying layer assigns its own properties, then goes into [KSCrash Installation Standard install], where the [KSCrash init] method is invoked to set up the Crash monitor to be initialized, and then the [KSCrash install], which monitors will be started according to the previous settings. Controller.

- (void) install
{
    KSCrash* handler = [KSCrash sharedInstance];
    @synchronized(handler)
    {
        g_crashHandlerData = self.crashHandlerData;
        handler.onCrash = crashCallback;
        [handler install];
    }
}    

In front of the installation monitor, the onCrash is also set up. Finally, the void kscm_setActive Monitors (KSCrashMonitorType monitorTypes) are called to start the monitor. The monitorTypes are the g_monitoring that we set up at the beginning. In KSCrashMonitor, the monitor array g_monitors are set up, and the monitorTypes are set with us through the monitorTypes in the array. G_monitors perform operations, the results of which determine whether or not we install the monitor corresponding to the monitor type. Next, the installation process of each monitor is analyzed.

1.2 Mach kernel exceptions

Mach kernel exception, represented by enumeration KSCrashMonitorTypeMachException, is implemented by KSCrashMonitor_MachException. If it is added to the global enumeration g_monitoring, it needs to be turned on. Finally, ExceptionHandler is invoked through general Apistatic void setEnabled (bool is Enabled).

static bool installExceptionHandler()
{
    ...

//    Backup exception port
    KSLOG_DEBUG("Backing up original exception ports.");
    kr = task_get_exception_ports(thisTask,
                                  mask,
                                  g_previousExceptionPorts.masks,
                                  &g_previousExceptionPorts.count,
                                  g_previousExceptionPorts.ports,
                                  g_previousExceptionPorts.behaviors,
                                  g_previousExceptionPorts.flavors);
    
    if(kr != KERN_SUCCESS)
    {
        KSLOG_ERROR("task_get_exception_ports: %s", mach_error_string(kr));
        goto failed;
    }

    if(g_exceptionPort == MACH_PORT_NULL)
    {
//          Allocate new ports and grant access
        KSLOG_DEBUG("Allocating new port with receive rights.");
        kr = mach_port_allocate(thisTask,
                                MACH_PORT_RIGHT_RECEIVE,
                                &g_exceptionPort);
        if(kr != KERN_SUCCESS)
        {
            KSLOG_ERROR("mach_port_allocate: %s", mach_error_string(kr));
            goto failed;
        }
        
//          Add send permissions to ports
        KSLOG_DEBUG("Adding send rights to port.");
        kr = mach_port_insert_right(thisTask,
                                    g_exceptionPort,
                                    g_exceptionPort,
                                    MACH_MSG_TYPE_MAKE_SEND);
        if(kr != KERN_SUCCESS)
        {
            KSLOG_ERROR("mach_port_insert_right: %s", mach_error_string(kr));
            goto failed;
        }
    }
// Set the port to accept exceptions
    KSLOG_DEBUG("Installing port as exception handler.");
    kr = task_set_exception_ports(thisTask,
                                  mask,
                                  g_exceptionPort,
                                  EXCEPTION_DEFAULT,
                                  THREAD_STATE_NONE);
    if(kr != KERN_SUCCESS)
    {
        KSLOG_ERROR("task_set_exception_ports: %s", mach_error_string(kr));
        goto failed;
    }

//     Create auxiliary exception threads
    KSLOG_DEBUG("Creating secondary exception thread (suspended).");
//    Create a detached thread
    pthread_attr_init(&attr);
    attributes_created = true;
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    error = pthread_create(&g_secondaryPThread,
                           &attr,
                           &handleExceptions,
                           kThreadSecondary);
    if(error != 0)
    {
        KSLOG_ERROR("pthread_create_suspended_np: %s", strerror(error));
        goto failed;
    }
//     Get the thread id of the detached thread
    g_secondaryMachThread = pthread_mach_thread_np(g_secondaryPThread);
    ksmc_addReservedThread(g_secondaryMachThread);

//    Create the main exception thread.
    KSLOG_DEBUG("Creating primary exception thread.");
    error = pthread_create(&g_primaryPThread,
                           &attr,
                           &handleExceptions,
                           kThreadPrimary);
    if(error != 0)
    {
        KSLOG_ERROR("pthread_create: %s", strerror(error));
        goto failed;
    }
    pthread_attr_destroy(&attr);
    g_primaryMachThread = pthread_mach_thread_np(g_primaryPThread);
    ksmc_addReservedThread(g_primaryMachThread);

    KSLOG_DEBUG("Mach exception handler installed.");
    return true;

···
}        

Then, using detached threads, perform the following methods

static void* handleExceptions(void* const userData)
{
    ...
    for(;;)
    {
        KSLOG_DEBUG("Waiting for mach exception");

        // Waiting for an exception to trigger
        kern_return_t kr = mach_msg(&exceptionMessage.header,
                                    MACH_RCV_MSG,
                                    0,
                                    sizeof(exceptionMessage),
                                    g_exceptionPort,
                                    MACH_MSG_TIMEOUT_NONE,
                                    MACH_PORT_NULL);
        if(kr == KERN_SUCCESS)
        {
            break;
        }

        // Loop and try again on failure.
        KSLOG_ERROR("mach_msg: %s", mach_error_string(kr));
    }

   ...
}

1.3 Fatal signals

Fatal signals in the code, KSCrashMonitor Type Signal to represent whether to open, in KSCrashMonitor_Signal to achieve the relevant implementation, through the general APIstatic void setEnabled (bool is Enabled), the last call is installSignalHandler.

1.4 C++ exceptions

In the code, the C++ exception is represented by enumerating KSCrashMonitorTypeCPPException, and the related code is implemented in the class KSCrashMonitor_CPPException. In the general API Static void setEnabled (bool is Enabled), the callback function is set by std::set_terminate(terminate_handler).

1.5 Objective-C exceptions

In the code, the enumeration value is represented by KSCrashMonitor TypeNSException, the concrete code is implemented in KSCrashMonitor_NSException, and the callback is set by void NSSet Uncaught Exception Handler (NSUncaught Exception Handler* _Nullable) in general APIstatic void setEnabled(bool isEnabled).

1.6 Main thread deadlock

Main thread deadlock is to monitor the deadlock exception of the main thread. It is represented by enumerating KSCrashMonitorTypeMainThreadDeadlock in the code, implementing the relevant code in the class KSCrashMonitor_Deadlock, and installing the deadlock monitoring by calling the initialization of KSCrashDeadlock Monitor in the static void setEnabled (bool is Enabled).

1.7 Custom crashes

Custom Crash, expressed in the code as KSCrashMonitorTypeUser Reported, can be implemented in KSCrashMonitor_User class. This can be customized crash. Unlike the above, custom crash has no standard crash timing and needs to be defined by itself. That is, when encountering some special cases, you can collect information and then insert it into KSCrashMonitor_r. After doing this, you can insert it into KSCrashMonitor_User. Continued processing.

0x02 Running Process

2.1 capture

2.1.1 Mach kernel exceptions

In Mach, exceptions are handled through the messaging mechanism, the infrastructure of the kernel. Exceptions are thrown by an error thread or task (via msg_send()) and then captured by a handler (msg_recv()). The handler can handle exceptions, clear exceptions (that is, mark exceptions as complete and continue), and decide to terminate threads.

Mach exception handling model is different from other exception handling models. Exception handlers in other models run in the context of the thread in error, while Mach exception handlers run exception handlers in different contexts. The thread in error sends messages to the pre-defined exception port, and then waits for a response. Each task can register an exception port, which will work on all threads in the same task. In addition, a single thread can register its own exception port through thread_set_exception_prots.

So Mach kernel exceptions use mach_task_self to get the current task process. Because Mach exceptions are actually message forwarding exceptions, they need message receiving permission. Mach_port_allocate (thisTask, MACH_PORT_RIGHT_RECEIVE, & g_exceptionPort) is given to initialize the exceptional ports. However, MACH_MSG_TYPE_MAKE_SEND is also given to port-changing ports later. It is the task_set_exception_ports that require this permission, and then task_set_exception_ports sets this port as the exception port for the target task.

At this point, exceptions have been caught, but nothing has been done. To do this, an active listener is created on the exception port using mach_msg. Exception handling can be done by another thread of the same program. There can also be another program for exception handling. This is what the exception port of the launched registered process does. So later, two separate threads g_secondary MachThread and g_primaryPThread are set up to wait for the exception to trigger.

for(;;)
    {
        KSLOG_DEBUG("Waiting for mach exception");
    // The message loop is blocked until a message is received and must be an exception message.
    // Other messages do not reach the exception port.
        kern_return_t kr = mach_msg(&exceptionMessage.header,
                                    MACH_RCV_MSG,
                                    0,
                                    sizeof(exceptionMessage),
                                    g_exceptionPort,
                                    MACH_MSG_TIMEOUT_NONE,
                                    MACH_PORT_NULL);
        if(kr == KERN_SUCCESS)
        {
            break;
        }

        // Loop and try again on failure.
        KSLOG_ERROR("mach_msg: %s", mach_error_string(kr));
    }

Mach waits for exceptions as follows:

2.1.2 Fatal signals

Mach has provided the underlying trap handling through the exception mechanism, while BSD has built a signal processing mechanism based on the exception mechanism. The signal generated by the hardware is captured by Mach and converted to the corresponding UNIX signal. To maintain a unified mechanism, the signals that the operating system and users attempt are first converted to Mach exceptions and then to Signals, as shown in the following figure:

As you can see, what's different from our custom Mach exception capture is the handling of Mach exceptions.

When the BSD process (user-mode process) is started by the bsdinit_task() function, a Mach kernel thread named ux_handle is set. Ux_handle, on the other hand, is similar to Mach exception capture above, except that it deals with Mach exception conversion to signal.

Hardware-generated signals start with processor traps. Processor traps are platform-related. Ux_exception is responsible for converting traps into signals. To deal with machine-related situations, ux_exception calls machine_exception to first attempt to handle machine traps. If this function fails to convert signals, ux_exception handles the general case.

If the signal is not generated by hardware, then the signal comes from two API calls: kill or pthread_kill. These two functions send signals to the process, respectively.

In summary, signals can be seen as encapsulation of hardware and software anomalies.

Errors in hardware and software correspond to the corresponding signal. In KSCrash, the first signal is registered and callback.

static const int g_fatalSignals[] =
{
    SIGABRT,
    SIGBUS,
    SIGFPE,
    SIGILL,
    SIGPIPE,
    SIGSEGV,
    SIGSYS,
    SIGTRAP,
};

2.1.3 C++ exceptions

C++ exceptions use the system-encapsulated function std:: set_terminate (CPPException Terminate) to set callbacks.

2.1.4 Objective-C exceptions

Objective-C exceptions are invoked using the NSSet Uncaught Exception Handler system callback.

It should be noted that there may be a problem of overwriting registration.

2.1.5 Main thread deadlock

First, the runMonitor method is executed in the sub-thread, then the watchdogPulse method is executed. In watchdogPulse, the flag bit is reset by dispatch_async to the main thread to identify whether a deadlock has occurred.

2.2 record

2.2.1 process

Acquisition: Data acquisition, starting from Crash, is mainly to collect system information, as well as crash information, crash information mainly includes stack address, reasons and so on.

Flow: Record from report, binary_images, process, system and crash respectively. Next, we will discuss symbol restoration in detail.

Symbol acquisition

In NSException, get the call StackReturn Addresses address stack directly.

In Mach exception, the stack originates from the address of the register. First, the value address of the current pc register is obtained, then the symbol restores, then the lr pointer is obtained, and then the symbol restores, then the current fp pointer is obtained, the symbol restores, and then the recursive fp pointer is repeatedly restored. The operation of restoring the symbol is known to recurse to the current address 0 or the preframe is emptive.

In Signal exceptions, stack fetches are the same as Mach.

Symbol reduction

The object of symbol restoring is address. The core code of symbol restoring is as follows: Firstly, the index of Image where the current incoming address is located is obtained by imageIndexContaining Address method. How to get the index of Image?

bool ksdl_dladdr(const uintptr_t address, Dl_info* const info)
{
    info->dli_fname = NULL;
    info->dli_fbase = NULL;
    info->dli_sname = NULL;
    info->dli_saddr = NULL;

    const uint32_t idx = imageIndexContainingAddress(address);
    if(idx == UINT_MAX)
    {
        return false;
    }
    const struct mach_header* header = _dyld_get_image_header(idx);
    const uintptr_t imageVMAddrSlide = (uintptr_t)_dyld_get_image_vmaddr_slide(idx);
    const uintptr_t addressWithSlide = address - imageVMAddrSlide;
    const uintptr_t segmentBase = segmentBaseOfImageIndex(idx) + imageVMAddrSlide;
    if(segmentBase == 0)
    {
        return false;
    }

    info->dli_fname = _dyld_get_image_name(idx);
    info->dli_fbase = (void*)header;

    // Find symbol tables and get whichever symbol is closest to the address.
    const STRUCT_NLIST* bestMatch = NULL;
    uintptr_t bestDistance = ULONG_MAX;
    uintptr_t cmdPtr = firstCmdAfterHeader(header);
    if(cmdPtr == 0)
    {
        return false;
    }
    for(uint32_t iCmd = 0; iCmd < header->ncmds; iCmd++)
    {
        const struct load_command* loadCmd = (struct load_command*)cmdPtr;
        if(loadCmd->cmd == LC_SYMTAB)
        {
            const struct symtab_command* symtabCmd = (struct symtab_command*)cmdPtr;
            const STRUCT_NLIST* symbolTable = (STRUCT_NLIST*)(segmentBase + symtabCmd->symoff);
            const uintptr_t stringTable = segmentBase + symtabCmd->stroff;

            for(uint32_t iSym = 0; iSym < symtabCmd->nsyms; iSym++)
            {
                // If n_value is 0, the symbol refers to an external object.
                if(symbolTable[iSym].n_value != 0)
                {
                    uintptr_t symbolBase = symbolTable[iSym].n_value;
                    uintptr_t currentDistance = addressWithSlide - symbolBase;
                    if((addressWithSlide >= symbolBase) &&
                       (currentDistance <= bestDistance))
                    {
                        bestMatch = symbolTable + iSym;
                        bestDistance = currentDistance;
                    }
                }
            }
            if(bestMatch != NULL)
            {
                info->dli_saddr = (void*)(bestMatch->n_value + imageVMAddrSlide);
                if(bestMatch->n_desc == 16)
                {
                    // This image has been stripped. The name is meaningless, and
                    // almost certainly resolves to "_mh_execute_header"
                    info->dli_sname = NULL;
                }
                else
                {
                    info->dli_sname = (char*)((intptr_t)stringTable + (intptr_t)bestMatch->n_un.n_strx);
                    if(*info->dli_sname == '_')
                    {
                        info->dli_sname++;
                    }
                }
                break;
            }
        }
        cmdPtr += loadCmd->cmdsize;
    }
    
    return true;
}

First, by subtracting the address offset from the incoming address, we get the location of the address in the Image, which is temporarily called the processed address. Then we traverse all the currently loaded images. Then, within the Image, we traverse all LCs and compare whether the address is within the address range of the current LC. Specifically, we traverse each Image, then get each LC, and then compare the current processed places. Whether the address is larger than VMAdress address and smaller than VM Adress plus VM Size, if it meets the criteria, is the Image we are looking for.

Back to ksdl_dladdr, get the Image where the address is located, and then get the segment base address of the specified image. Similarly, you need to traverse the LC of the current Image, and then get the LC_SEGMENT (_LINKEDIT) of the current Image, parse the information needed by the linker of _LINKEDIT, _LINKEDIT dynamic library, including relocation information (such as symbol table, string table), binding information, and laziness. Loading information, etc. When LC_SEGMENT (_LINKEDIT) is obtained, the value of VM_Address is returned minus the value of File Offset. Then use the return value plus the actual memory offset. In fact, here should be the base address of the current symbol table and string table, the formula: the actual location = the base address of the current _LINKEDIT in MachO - the current _LINKEDIT file offset + the actual offset (file offset has been included here).

Then, the current symbol table information is obtained by traversing LC, and then the symbol table is traversed. Then, the best matching address is obtained by gradually approaching the target address.


Get the most additive symbol address matching, and then find the corresponding string, the structure of a single symbol, so we can use n_strx that is String Table Index to get the string, so far, a single address restore completed, and so on.

struct nlist_64 {
    union {
        uint32_t  n_strx; /* index into the string table */
    } n_un;
    uint8_t n_type;        /* type flag, see below */
    uint8_t n_sect;        /* section number or NO_SECT */
    uint16_t n_desc;       /* see <mach-o/stab.h> */
    uint64_t n_value;      /* value of this symbol (or stab offset) */
};

The whole capture process is shown in the following figure

0x03 Reference

Deep Analysis of Mac OS X & iOS Operating System
iOS Reverse Application and Security

Posted by fatal on Wed, 20 Mar 2019 20:39:27 -0700