Securing iOS Apps - Debuggers

05.07.2013

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 in proc.h). This flag is set to 1 if a debugger is attached to the process and to 0 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 the PT_DENY_ATTACH parameter. If a debugger is being attached to an application, a call to ptrace(PT_DENY_ATTACH, 0, 0, 0) will send a ENOTSUP 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!