Two factor authentication (2FA) is an amazing invention. For one thing, it can significantly increase the security of your online accounts without significantly increasing the hassle of logging in. Additionally, the most popular 2FA algorithms are available in both free software and proprietary software implementations. This weekend, I reverse engineered Symantec's proprietary 2FA token solution with the goal of creating a free software alternative.

Motivation

Why did I do this? Well, like many people in the world, I use PayPal to send and receive money. To protect the security of my account, I use 2FA. Normally, when you use 2FA, the service provider presents a barcode to you that you can scan with any one of a number of applications (Authy, Duo Mobile, FreeOTP, Google Authenticator, etc.). However, PayPal uses the Symantec Validation and ID Protection Service (formerly Verisign Identity Protection) for their second factor. PayPal probably didn't want the overhead of managing a database of user tokens, so they went with Symantec's managed solution. My problem with this is that, while I can use Authy for all of my other accounts, I need to use the VIP Access app for PayPal only. So, I guess my reasoning here was:

Having multiple apps that do essentially the same thing seemed inefficient The VIP Access app for iOS is pretty ugly (in my opinion) I would prefer to have all of my tokens generated with one application/hardware device

Since it appeared as though no one else had done so, I decided to reverse engineer Symantec's VIP client myself.

Prior Work

I originally started working on this project around this time last year. I worked on it on and off for a few months, but I never made much progress. This was partially due to the fact I was attempting to de-obfuscate a heavily obfuscated Android application. I eventually got tired of that project and set it aside for a rainy day.

That "rainy day" came earlier this year when I saw this post, in which someone reversed their bank's obfuscated Android 2FA application in order to create a hardware token for it. Interestingly enough, the obfuscation used in that application was strikingly similar to the kind I found the VIP Access Android app using. Despite this newfound knowledge, I was still unable to deobfuscate many of the important portions of the application. At this point, I still thought that VIP Access used a proprietary algorithm to generate one time passwords.

Earlier this month, I found this script, in which I learned that VIP Access didn't use a proprietary algorithm to generate the tokens. I also learned that Symantec had released VIP Access applications for OS X and Windows. While this token extractor would have almost fit my needs, I really didn't want to have to rely on Symantec's proprietary client in order to generate these token keys. Plus, this script only works on OS X, so Linux and Windows users would be unable to extract their keys. So, eager to try out my recently-purchased disassembler (Hopper), I downloaded the VIP Access application and got to work.

The Process

Analysis of the Client-Server Communications

I started by opening the VIP Access application. The first window that appears indicates that the program is "Activating VIP Access". This would make sense if the program was calling out to some server to activate, so I fired up mitmproxy to watch the communications.

Here's an example of a provisioning request made by the application that would be POSTed to https://services.vip.symantec.com/prov :

<?xml version="1.0" encoding="UTF-8" ?> <GetSharedSecret Id= "1412030064" Version= "2.0" xmlns= "http://www.verisign.com/2006/08/vipservice" xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance" ><TokenModel> VSST </TokenModel><ActivationCode></ActivationCode><OtpAlgorithm type= "HMAC-SHA1-TRUNC-6DIGITS" /><SharedSecretDeliveryMethod> HTTPS </SharedSecretDeliveryMethod><DeviceId><Manufacturer> Apple Inc. </Manufacturer><SerialNo> 7QJR44Y54LK3 </SerialNo><Model> MacBookPro10,1 </Model></DeviceId><Extension extVersion= "auth" xsi:type= "vip:ProvisionInfoType" xmlns:vip= "http://www.verisign.com/2006/08/vipservice" ><AppHandle> iMac010200 </AppHandle><ClientIDType> BOARDID </ClientIDType><ClientID> Mac-3E36319D3EA483BD </ClientID><DistChannel> Symantec </DistChannel><ClientInfo><os> MacBookPro10,1 </os><platform> iMac </platform></ClientInfo><ClientTimestamp> 1412030064 </ClientTimestamp><Data> mxk5NtUnCwd36GEpQq6+Zmnh+rPKDePuS/XYci6/WD0= </Data></Extension></GetSharedSecret>

Because that request is really hard to read, I've run it through an XML beautifier:

<?xml version="1.0" encoding="UTF-8"?> <GetSharedSecret Id= "1412030064" Version= "2.0" xmlns= "http://www.verisign.com/2006/08/vipservice" xmlns:xsi= "http://www.w3.org/2001/XMLSchema-instance" > <TokenModel> VSST </TokenModel> <ActivationCode></ActivationCode> <OtpAlgorithm type= "HMAC-SHA1-TRUNC-6DIGITS" /> <SharedSecretDeliveryMethod> HTTPS </SharedSecretDeliveryMethod> <DeviceId> <Manufacturer> Apple Inc. </Manufacturer> <SerialNo> 7QJR44Y54LK3 </SerialNo> <Model> MacBookPro10,1 </Model> </DeviceId> <Extension extVersion= "auth" xsi:type= "vip:ProvisionInfoType" xmlns:vip= "http://www.verisign.com/2006/08/vipservice" > <AppHandle> iMac010200 </AppHandle> <ClientIDType> BOARDID </ClientIDType> <ClientID> Mac-3E36319D3EA483BD </ClientID> <DistChannel> Symantec </DistChannel> <ClientInfo> <os> MacBookPro10,1 </os> <platform> iMac </platform> </ClientInfo> <ClientTimestamp> 1412030064 </ClientTimestamp> <Data> mxk5NtUnCwd36GEpQq6+Zmnh+rPKDePuS/XYci6/WD0= </Data> </Extension> </GetSharedSecret>

Notice how the values for Manufacturer , SerialNo , Model , ClientID , and os all have newline characters in the strings? This will be important later. For now, let's look at the response we get back.

<?xml version="1.0" encoding="UTF-8"?> <GetSharedSecretResponse RequestId= "1412030064" Version= "2.0" xmlns= "http://www.verisign.com/2006/08/vipservice" > <Status> <ReasonCode> 0000 </ReasonCode> <StatusMessage> Success </StatusMessage> </Status> <SharedSecretDeliveryMethod> HTTPS </SharedSecretDeliveryMethod> <SecretContainer Version= "1.0" > <EncryptionMethod> <PBESalt> u5lgf1Ek8WA0iiIwVkjy26j6pfk= </PBESalt> <PBEIterationCount> 50 </PBEIterationCount> <IV> Fsg1KafmAX80gUEDADijHw== </IV> </EncryptionMethod> <Device> <Secret type= "HOTP" Id= "VSST26070843" > <Issuer> OU = ID Protection Center, O = VeriSign, Inc. </Issuer> <Usage otp= "true" > <AI type= "HMAC-SHA1-TRUNC-6DIGITS" /> <TimeStep> 30 </TimeStep> <Time> 0 </Time> <ClockDrift> 4 </ClockDrift> </Usage> <FriendlyName> OU = ID Protection Center, O = VeriSign, Inc. </FriendlyName> <Data> <Cipher> ILBweOCEOoMBLJARzoeUIlu0+5m6b3khZljd5dozARk= </Cipher> <Digest algorithm= "HMAC-SHA1" > MoaidW7XDzeTZJqhfRQCZEieARM= </Digest> </Data> <Expiry> 2017-09-25T23:36:22.056Z </Expiry> </Secret> </Device> </SecretContainer> <UTCTimestamp> 1412030065 </UTCTimestamp> </GetSharedSecretResponse>

As you can see, these requests use XML and most of the fields are pretty self explanatory

To start reversing this protocol, I used HTTP Client to send modified POST requests and note the responses. Interestingly enough, I could change most of the values and still get "valid" responses. I put "valid" in quotes because, as I would later learn, the value of Data would determine whether the credential would be activated or not. I didn't get very far by poking the provisioning server, so I moved on to a static analysis of the program.

Searching the Binary for Clues

Static Analysis

To start, I decided to look for some functions involved in parsing the response from the server. The decryptCipher method in ProvisioningController looked promising, so I took a look at the disassembled code. Lo and behold, one of the lines read:

eax = CCCrypt ( 0x1 , 0x0 , 0x0 , STK29 , 0x10 , eax , edi , STK25 , ecx , esi , edx );

From the CCCryptor man page:

CCCrypt ( CCOperation op , CCAlgorithm alg , CCOptions options , const void * key , size_t keyLength , const void * iv , const void * dataIn , size_t dataInLength , void * dataOut , size_t dataOutAvailable , size_t * dataOutMoved );

From this, I could tell that CCCrypt is performing a decrypt ( op = 1 = kCCDecrypt ) operation using AES-128 ( alg = 0 = kCCAlgorithmAES128 ) with no padding and no ECB ( options = 0 ) and a 16-byte keylength ( keyLength = 0x10 , to be expected with AES-128-CBC). I didn't try to find out the key and IV just yet, so I moved on to the next interesting method in ProvisioningController , prepareHmacInRequest :

... CCHmac ( 0x2 , esi , edi , STK29 , eax , & var_44 ) ...

And from the CCHmac man page:

CCHmac ( CCHmacAlgorithm algorithm , const void * key , size_t keyLength , const void * data , size_t dataLength , void * macOut );

This told me that the HMAC algorithm being used was SHA-256 ( algorithm = 2 = kCCHmacAlgSHA256 ).

Unfortunately, static analysis could only go so far and while I did investigate other objects and their methods, most of them were dead ends. From there, I moved on to the dynamic analysis.

Dynamic Analysis

Debugging a program in Hopper is incredibly easy since you can set all the breakpoints in the GUI and read out the memory at arbitrary addresses with simple commands. Thankfully, VIP Access doesn't have any debugging protections, nor does it have any memory obfuscation, so finding the inputs to each of the important functions was trivial.

I started with CCCrypt , the function that decrypts the OTP token secret in the provisioning response. I set breakpoints where I saw the pointers to the key , iv , and dataIn variables were being set. From there, I followed those pointers to the areas in memory where those values were stored and dumped them.

First I dumped the key:

After dumping it, I decided to try it a few more times to make sure it wouldn't change—thankfully, it didn't, so I knew it was a static key. But if the key didn't change, what did? None other than the IV, of course! I set up a mitmproxy capture while performing my dynamic analysis to confirm that, yes, the IV used to decrypt dataIn was the same as the one in the provisioning response (base64-decoded). Similarly, dataIn ended up being the base64-decoded value of Cipher . From there, I wrote a small Python function to do the decryption myself. After removing the padding, I ran oathtool --totp with the hex-encoded OTP token secret to confirm that it was actually the correct secret. And it was! This was my first major victory, but there was a setback—when I tried playing back a valid captured request, I would get a proper response, but when I went to check the token online, Symantec said the credential ID was invalid. Even after updating the time in the request, the ID was still reported as invalid, so I knew that the value of Data in the request was significant in some way.

Looking back on the static analysis, the method I hadn't dynamically analyzed yet was prepareHmacInRequest in ProvisioningController . Again, by strategically setting breakpoints, I was able to determine both the HMAC key and the data used to generate it.

The 32-byte dump is the static HMAC key and the 56-byte dump is the data that is HMAC'd (the blacked out portion is the unique part of my board ID). Oh, and remember earlier when I noted that ClientID had a newline character in it? Well, you can see it in the data to be HMAC'd (0x0a), right at the end of the blacked out bytes. Apparently, when the program uses ioreg to retrieve the computer information ( -[ProvisioningController getDeviceInfo:] ), it doesn't strip the newline characters (which a simple tr -d '

' would have fixed, but I digress). It doesn't matter, of course, since the VIP provisioning system will accept any values for those keys.

Writing a Free Client

With both the HMAC key and the token encryption key, I was able to write a Python program to automatically provision a VIP Access credential and generate both an otpauth URI and a QR code of that URI. You can find the code I wrote in its repository on GitHub.

All in all, it was pretty straightforward to write a client for this service since, at it's core, it's a simple HTTP POST with some hashing and decryption. The only obstacle I encountered was that the lxml.ElementMaker API wasn't properly generating the namespace attributes of the Extension tree, but I got around that by creating a "template" to plug the dictionary of values into. While it isn't the "correct" way to generate XML with Python, in this instance it was the only way. Oh, and I haven't yet figured out how the response digest HMAC is generated, but the program works without it.

In the future, I might add the capability to emulate the mobile version of VIP Access and I'll definitely add more error-checking to the code. Right now, all it does it check whether the OTP token actually functions by sending a separate request to Symantec to confirm that it does work. I might even turn it into a full Python module, complete with class definitions and all that fancy stuff.

Extras

There was some code in there that had to do with updating a token's details from the server ( -[ProvisioningController updateNonACRequest:] ), but I didn't really pay attention to it. I assume it's there to handle updating the token when it expires, but because the strings were xor-obfuscated and since reversing it wasn't necessary for my purposes, I left it alone. Why was that portion of the code obfuscated? Only the developers know. Speaking of which...

If you're curious, you can learn the names and usernames of some of the developers of this application by looking at the strings in the VIP Access binary and the .svn directory in the root of the VIP Access.app folder.

Conclusion

Hopefully, the work I've done will benefit the Internet community by facilitating the interoperability of computer programs and, more specifically, by allowing anyone on any operating system to use Symantec's VIP service.

Aside from the ugly iOS app, this experience has actually given me a lot of respect for the VIP service. It seems to to be a very robust system, capable of using any implemented OTP algorithm by simply changing some of the attributes in the protocol. Its inclusion of expiration dates for the tokens is also a good idea, and I'm sure token revocation is implemented on the backend. In other words, the people who built this system really knew what they were doing.

Lessons learned: