I’m still waiting for some bug fixes to release the previously planned posts, and in the meantime I continue to poke at other PrivilegedHelperTools. This post born because I actually failed to exploit an XPC service, and I learned something new in regards, of how to securely write such a service. One application that came to my sight is Viscosity. This tool was already in Tyler Bohan’s list, where his team looked on exploiting such services: GitHub - blankwall/Offensive-Con: Talk and materials for Offensive Con presentation - Privileged Helper Tools. I still thought that I will give it a try, because many times, fixes are not properly done, and you can reexploit the bugs.

I went through the typical cycles, checks I used to, in order to see if I can abuse the XPC service.

Is the client protected against injection?

% codesign -dv --entitlements :- /Applications/Viscosity.app 
Format=app bundle with Mach-O thin (x86_64)
CodeDirectory v=20500 size=45670 flags=0x12000(library-validation,runtime) hashes=1420+3 location=embedded
Signature size=9053
Timestamp=2020. Jan 13. 4:46:48
Info.plist entries=35
Runtime Version=10.15.0
Sealed Resources version=2 rules=13 files=762
Internal requirements count=1 size=220

Sure it is! Nice!

That was the easy part, let’s load the XPC server component into Hopper, and take a look at the shouldAcceptNewConnection function.

Is the XPC service verifies the client based on the auth token and not the PID? Yes! Nice, as this is not frequently seen.

if ([r12 codeSignatureIsValidForAuditToken:@selector(auditToken)] != 0x0) { 

Is the code signature properly verified?

rax = SecRequirementCreateWithString(@"anchor apple generic and certificate 1[field.1.2.840.113635.] /* exists */ and certificate leaf[field.1.2.840.113635.] /* exists */ and certificate leaf[subject.OU] = \"34XR7GXFPX\"", 0x0, &var_58);

Yes and no - everything looks good, except that the client version is not verified, which means that if I would find an old client, to which I can inject I can talk to the service. I was wrong.

So I downloaded an older version of Viscosity, specifically 1.6.8, as we can see it doesn’t have hardened runtime, and nothing else, which means that we can inject our own dylib.

% codesign -dv Viscosity.app 
Format=app bundle with Mach-O thin (x86_64)
CodeDirectory v=20200 size=5114 flags=0x0(none) hashes=154+3 location=embedded
Signature size=4651
Signed Time=2017. Jan 16. 21:39:25
Info.plist entries=35
Sealed Resources version=2 rules=13 files=477
Internal requirements count=1 size=220

I wrote a small POC:

#import <Foundation/Foundation.h>
static NSString* XPCHelperMachServiceName = @"com.sparklabs.ViscosityHelper";

@protocol SLViscosityIPCXPCProtocol
- (void)message:(NSDictionary *)arg1 withReply:(void (^)(NSDictionary *))arg2;

static void customConstructor(int argc, const char **argv) {
        NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys: @"resetNetwork",@"command", nil];
        NSString*  _serviceName = XPCHelperMachServiceName;
        NSXPCConnection* _agentConnection = [[NSXPCConnection alloc] initWithMachServiceName:_serviceName options:4096];
        [_agentConnection setRemoteObjectInterface:[NSXPCInterface interfaceWithProtocol:@protocol(SLViscosityIPCXPCProtocol)]];
        [_agentConnection resume];

        id obj = [_agentConnection remoteObjectProxyWithErrorHandler:^(NSError* error)
            NSLog(@"Connection Failure");
        NSLog(@"obj: %@", obj);
        NSLog(@"conn: %@", _agentConnection);
        [obj message:dict withReply:^(NSDictionary * dic){
                        NSLog(@"Reply, %@", dic);

and I tried to connect:

% DYLD_INSERT_LIBRARIES=main.dylib ./Viscosity.app/Contents/MacOS/Viscosity 
2020-03-22 14:03:27.214 Viscosity[5026:207265] obj: <__NSXPCInterfaceProxy_SLViscosityIPCXPCProtocol: 0x7f926c500d60>
2020-03-22 14:03:27.214 Viscosity[5026:207265] conn: <NSXPCConnection: 0x7f926c40b1f0> connection to service on pid 0 named com.sparklabs.ViscosityHelper
2020-03-22 14:03:27.383 Viscosity[5026:207275] Connection Failure

and as you can see I got a failure, which was weird, as I really expected the connection to work.

Looking at the logs I could see that there was an error with the code signing check:

default 14:03:27.383645+0100 com.sparklabs.ViscosityHelper SecCodeCopySigningInformation() csFlags error

The CSFLAG related error is already alarming, and looking up the error in Hopper I arrived to the following location:

    var_50 = 0x0;
    rax = SecCodeCopySigningInformation(0x0, 0x8, &var_50);

    rdx = **_kSecCodeInfoStatus;
    rax = [var_50 objectForKeyedSubscript:rdx];
    rax = [rax retain];
    [rax intValue];
    [rax release];
    if (!COND) {
            rbx = 0x0;
            NSLog(@"SecCodeCopySigningInformation() csFlags error");

The service retrieves the CS flags of the connecting process, which will be in kSecCodeInfoStatus. Hopper didn’t reveal for me what is COND, so I took a look at the assembly:

mov        rdi, qword [rbp+var_50] ; argument "instance" for method _objc_msgSend, CODE XREF=-[SLViscosityIPCServer codeSignatureIsValidForAuditToken:]+240
mov        rax, qword [_kSecCodeInfoStatus_10008d0f8] ; _kSecCodeInfoStatus_10008d0f8
mov        rdx, qword [rax]
mov        rsi, qword [0x1000a1578] ; argument "selector" for method _objc_msgSend, @selector(objectForKeyedSubscript:)
call       r12          ; Jumps to 0x1000e6780 (_objc_msgSend), _objc_msgSend
mov        rdi, rax     ; argument "instance" for method imp___stubs__objc_retainAutoreleasedReturnValue
call       imp___stubs__objc_retainAutoreleasedReturnValue ; objc_retainAutoreleasedReturnValue
mov        r15, rax
mov        rsi, qword [0x1000a1368] ; argument "selector" for method _objc_msgSend, @selector(intValue)
mov        rdi, rax     ; argument "instance" for method _objc_msgSend
call       r12          ; Jumps to 0x1000e6780 (_objc_msgSend), _objc_msgSend
mov        r12d, eax
mov        rdi, r15     ; argument "instance" for method _objc_release
call       qword [_objc_release_10008d118] ; _objc_release, _objc_release_10008d118,_objc_release
mov        rdi, qword [rbp+var_50] ; argument "cf" for method imp___stubs__CFRelease
call       imp___stubs__CFRelease ; CFRelease
bt         r12d, 0xd
jb         loc_10000f2d7

The test is done here:

bt         r12d, 0xd
jb         loc_10000f2d7

This will do a Bit Test operation, checking the 13th bit we get earlier for the CS flags. It’s done by moving the specified bit to the CF (carry) flag. The 13th bit means 0x2000 in hex, which is equal for library-validation. This means that if it’s set (jb assembly will verify the CF flag, and jump if set), it will proceed with the regular code signing check:

mov        rdi, qword [rbp+var_48] ; argument "code" for method imp___stubs__SecCodeCheckValidityWithErrors, CODE XREF=-[SLViscosityIPCServer codeSignatureIsValidForAuditToken:]+391
mov        rdx, qword [rbp+var_58] ; argument "requirement" for method imp___stubs__SecCodeCheckValidityWithErrors
xor        esi, esi     ; argument "flags" for method imp___stubs__SecCodeCheckValidityWithErrors
xor        ecx, ecx     ; argument "errors" for method imp___stubs__SecCodeCheckValidityWithErrors
call       imp___stubs__SecCodeCheckValidityWithErrors ; SecCodeCheckValidityWithErrors
mov        r15d, eax
mov        rdi, qword [rbp+var_48]
test       rdi, rdi
je         loc_10000f2f9

It means that our client has to have library-validation set, which will involve that we can’t do DYLIB injection into the client, as I discussed this here before: DYLD_INSERT_LIBRARIES DYLIB injection in macOS / OSX · theevilbit blog.

I didn’t know that it’s possible, and it’s a very good way to actually verify the client. Normally I said that we need to ensure that the client is a new version, where we know that it’s already hardened, but this is also a very good way.

I found after this, that this idea is explained in LittleSnitch’s blog as well: The Story Behind CVE-2019-13013