This post is about some thoughts I had about obfuscation. Since I am using Receigen to do the heavy lifting I have not tried any of this.
The purpose of obfuscation is to make it as difficult as possible for a hacker to break you code. How can we achieve that?
You may think that, once compiled, it becomes very difficult to "read" the code. But in modern languages (like Swift) there is a great deal of information left in the compiled code that can be used to make it easier for a hacker to find the places where receipt validation is done. But even in the older languages it can be easy to identify those places where calls are made to verify receipts.
I remember a case (about 20 years ago, pre internet!) where I had to use an app that used a "dongle" to prevent illegal copies. Since I used the software in two different buildings I had to take the dongle along with me when I changed locations. Of course I would sometimes forget the dongle. Very frustrating. One day I had enough of this and sat down to take a look at the assembly code. It took about 10 minutes to figure out where the calls were made to the subroutines that checked for the dongle, and replace those calls with NOPs (No OPeration instructions). Presto, the dongle was no longer needed.
With the Swift code from earlier in this series, I estimate that it would take me only slightly longer to hack the code and avoid the validation altogether. The main like of attack would be to start the app in a debugger and see where it would quit. Then retrace the steps back (probably only a few instructions) and see which check would cause the exit. Replace that check with NOPs and try again. Do this a couple of times and eventually you will end up with a cracked version of the application.
But then, I am not a hacker so I would basically take the laborious route. Today's hackers are much more adapt than me at breaking code. They probably have scripts and other tools to do the heavy lifting for them. I think that the Swift code as presented could be cracked by a simple tool, without the need for a hacker to "make his hands dirty".
For example: in the code I use the Apple Root Certificate (ARC) as a plain file. If I wanted to write a tool to automatically hack app, I would scan the app for the root certificate and replace it with a root certificate of my own making. Next the tool would create a new receipt sign it with the fake root certificate and replace the receipt in the app bundle as well.
Renaming the root certificate is good, but does not really help. Since the hacker knows the byte sequence of the ARC he would simply scan for the byte sequence. Even embedding the byte sequence in code does not protect against a sequence scanner. The best option is to encrypt the ARC with a simple encryption scheme of your own making and include the encrypted ARC as a hex dump in the code.
The same approach should be taken for all the strings used in the validation. Encrypt all of them.
A more difficult aspect is the start of the application. Any validation that ends before the start of the application is by definition vulnerable. If the hacker can find the true application start, then he/she can simply skip all validation code and jump directly to the start of the application. The application start should thus be hidden as good as possible. For example by hiding it among a list of other calls and selecting a call from the list dynamically.
Even better would it be to repeat the validation in other parts of your application. Or to add markers to the validation that other parts in your app would check for. For example, calculate a dynamic value before the validation of the root certificate, during the validation of the root certificate you modify that value and store the result in another variable, then in your main app (for example in the run loop) you would check if the values of the two variables are as expected. This way it would be ensured that the root certificate validation was indeed run. A hacker that would find a way to start the app without doing the validation would then fail. If you do something like this in 50 different places, then a hacker would need to do some real work to break your app. He will succeed eventually, but there is no reason to make his life easy ;-) (Though it has to be said that the satisfaction of a hacker in breaking your app goes up with the difficulty of breaking your app!)
Just some thoughts, If you have anything to add, please do so in the comments.
Happy coding...
Did this help?, then please help out a small independent.
If you decide that you want to make a small donation, you can do so by clicking this
link: a cup of coffee ($2) or use the popup on the right hand side for different amounts.
Payments will be processed by PayPal, receiver will be sales at balancingrock dot nl
Bitcoins will be gladly accepted at: 1GacSREBxPy1yskLMc9de2nofNv2SNdwqH
We don't get the world we wish for... we get the world we pay for.
Great articles, really helpful. Thanks
ReplyDeleteI can see Receigen is easy to use and it obfuscates the validation code elegantly. But with your examples, isn't the application as vulnerable as it would be without using Receigen (and using your plain Swift validation code instead) since the call of Receigen generated code is not obfuscated?
ReplyDeleteAnd the second question - Mac App Store and Developer ID apps are code signed. I assume that if you change a bit in the app's bundle (by editing the binary), the app does not launch since the code signature does not match. And (I really only assume, I'm not an expert in this) without a developer's private key you cannot create a code signature that matches. Isn't this enough of a protection?
Hello Jirka,
ReplyDeleteFirst question: Since the app is started by Receigen and not main, I do not think that this would be easy. With enough time, sure it can be done. It is also why we should at some points in the app again perform the validation. Not only at start-up.
Second question: If you change the app's bundle in any way -for example the plist- then the app will not launch. Whether that is enough protection is something that is up to you to decide. But do note that distributing an app via the app-store requires validation and the app will be rejected if that is not provided.
A clarification: The app is started by Receigen, not bij main. In fact, main does not even contain the code to start the app. I.e. it is not possible to remove the call to Receigen and have the app started anyhow.
DeleteThanks, now I get it.
Delete