Securing iOS Apps - Debuggers
With about half a billion iOS devices sold across the world, security of iOS applications becomes a major issue. Attacks on an iOS application can be quite rewarding to the hacker, giving him access to secret API keys, disabled functionalities (remember when Skype used to charge a supplement for calling over 3G?), or fake game scores upload.
As mobile developers, we need to have a strong understanding of the way we can protect our applications. That’s why we decided to start a new series of blog posts aimed towards security. Because securing an application implies thinking like an attacker, we will present each subject from both points of view. Today, we will talk about debuggers.
Using GDB to decrypt an application
When an hacker tries to crack an application, he is going to, one way or another, attach a debugger to the running application to interact with it. There are several reasons to use GDB and one of them is to decrypt the binary. In fact, all the apps you download from the App Store are encrypted by Apple and hackers use GDB to decrypt them by dumping the virtual memory of the running app.
In pratice here is what happens if we decide to decrypt an app. First of all, we download the application legally from the App Store and copy it to our computer. Let’s assume here that the name of the application is HackedApp.app
. The executable is then HackedApp.app/HackedApp
.
This executable is a Mach-O file (see Mach-O File Format Reference) that contains a lot of informations about the application: architecture type, methods virtual addresses, linked dynamic libraries and so on. The otool
command line tool is really helpful to disect Mach-O files and to print them in a human readable format. We use the following command to know where the code is located in virtual memory at runtime.
> otool -l HackedApp
HackedApp (architecture armv7):
…
Load command 1
cmd LC_SEGMENT
cmdsize 736
segname __TEXT
vmaddr 0x00001000
vmsize 0x0002d000
…
The interesting line here is vmaddr 0x00001000
: it means that the application is going to be loaded in virtual memory at address 0x1000
.
Then the following command will tell us whether the executable file is encrypted or not, and where the encrypted data for architecture armv7
are:
> otool -arch armv7 -l HackedApp | grep crypt
cryptoff 4096 # offset where encrypted data begin
cryptsize 180224 # size of encrypted data
cryptid 1 # 1 if encrypted, else 0
In this case the encrypted data begin at address 4096 = 0x1000
and have a size of 180224 = 0x2C000
bytes. To compute the range of addresses where the decrypted data are located, we need to do a tiny bit of math. We need to dump the virtual memory between addresses 0x1000 (vmaddr) + 0x1000 (cryptoff) = 0x2000
and 0x1000 (vmaddr) + 0x1000 (cryptoff) + 0x2C000 (cryptsize) = 0x2E000
.
In order to do that, we launch the application on a jailbroken device, attach GDB to it and call the dump memory
command.
> ps -ax | grep HackedApp
15714 3:14.31 /var/mobile/Applications/UUID/HackedApp.app/HackedApp
> gdb -p 15714
...
Attaching to process 15714.
Reading symbols for shared libraries . done
Reading symbols for shared libraries ..........................................
......................................................................... done
0x39f5cd82 in <redacted> ()
gdb> dump memory dump.bin 0x2000 0x2E000
The dump.bin
file now contains the decrypted data of the application.
There are two more steps to finish cracking the application. First, we need to replace encrypted data with decrypted data in the binary. For this, we use the dd
command like this:
> dd seek=0x2000 bs=1 conv=notrunc if=./dump.bin of=./HackedApp
Second, we have to change the cryptid
value to 0
(this can be easily achieved with an hexadecimal editor).
Once all of these steps performed, the application is totally decrypted and we have access to our ultimate goal: its symbol table. It means that we have access to all the method names and string constants the developer of the application wrote in his code. To extract class interfaces from the symbol table, we use the class-dump-z
tool (available here):
> class-dump-z HackedApp
…
@interface XXUnknownSuperclass : MyCustomClass
+(id)customMethod1;
+(id)customMethod2;
@end
@interface XXUnknownSuperclass (MyCategory)
+(id)customMethod:(float)param;
@end
…
Nothing easier for us, at this point, to set breakpoints in GDB on specific methods, intercept function calls, and alter the application behavior. If we know the name of the methods and the classes that the developers used in their code, it will be pretty easy for us to understand what the application does behind the scene and how it does it.
Defeat GDB
There are two things to know when dealing with GDB:
- First, when an application is being debugged, the kernel is aware of this and changes the state of a specific flag: the
P_TRACED
flag (declared inproc.h
). This flag is set to1
if a debugger is attached to the process and to0
if the application runs normally. You can monitor the state of this flag, check if GDB is attached to your application and perform custom actions to limit the features if it is the case.
Here is an example of function that monitors the value of the P_TRACED
flag:
#include <sys/sysctl.h>
int isDebuggerPerforming() {
struct kinfo_proc infos_process;
size_t size_info_proc = sizeof(infos_process);
pid_t pid_process = getpid(); // pid of the current process
//
int mib[] = {CTL_KERN, // Kernel infos
KERN_PROC, // Search in process table
KERN_PROC_PID, // the process with pid =
pid_process}; // pid_process
//
//Retrieve infos for current process in infos_process
int ret = sysctl(mib, 4, &infos_process, &size_info_proc, NULL, 0);
if (ret) return 0; // sysctl failed
//
struct extern_proc process = infos_process.kp_proc;
int flags_process = process.p_flag;
return flags & P_TRACED // value of the debug flag
}
- Second, we can directly disable any debugger currently or later attached to an application with the
ptrace()
function and thePT_DENY_ATTACH
parameter. If a debugger is being attached to an application, a call toptrace(PT_DENY_ATTACH, 0, 0, 0)
will send aENOTSUP
signal and quit the current process. In the other case, if the application is running and a debugger tries to attach to it, a segmentation fault will occur.
Here is an example of using ptrace
on iOS.
#include <dlfcn.h>
void disableDebugger() {
void * handle = dlopen(0, RTLD_GLOBAL | RTLD_NOW); // load dynamic librairies
int (* ptrace_function)(int, pid_t, caddr_t, int); // ptrototype of ptrace function
ptrace_function = dlsym(handle, string); // pointer on ptrace function
ptrace_function(31, 0, 0, 0); // PT_DENY_ATTACH == 31, not defined in iOS
dlclose(handle);
}
What’s next ?
In this article, we saw how hackers decrypt applications with GDB to take control over them and we presented two solutions to prevent debugging of an application. However, an attentive reader will spot that these solutions are not completely flawless: an obstinate hacker will certainly look for ptrace
or sysctl
calls in your application and set breakpoints in GDB to change their return values, bypassing your protections. This shows that security is basically a cat and mouse game, where we can only complexify the work of the hacker.
We hope you liked this first episode of our new series. We only covered a small part of the subject, debuggers not being the only tools capable of changing the behavior of an application at runtime. Furthermore, there are other approaches like patching the application binary itself, which makes room for future episodes!
Anyway, if you have any question or just want to share a comment, we’ll enjoy to hear from you on Twitter!
We're hiring!
We're looking for bright people. Want to be part of the mobile revolution?
Open positions