Securing iOS Apps - Patching binaries
In this previous article, we showed how to use debuggers to decrypt an application and retrieve its symbol table. With the symbol table, we have access to the name of the methods and constants used in the program. Thus, we can infer its internal architecture and change its behavior. However, this is a runtime approach which can quickly become painful to use every time we start the application. In this post we will focus on a way to permanently alter a program: modifying assembly code in an application executable file, also known as binary patching.

On iOS, the executable part of an application is a Mach-O file. It stores machine code and data (see the Mach-O File Format Reference). In particular, this file contains the executable code of the application, which is the interesting part here.
iOS applications that run on-device are compiled for the ARM architecture. That means that you must have good knowledge of ARM assembly to alter the executable code. As this is a subject in its own right, we won’t go in depth in this post (no architecture detail, no compilation, no disassembly). For now, just remember that we call opcode the hexadecimal code of an assembly instruction. For instance, in ARM/Thumb-2, the nop
instruction (“no operation”) has the opcode: 0xbf00
.
To bypass applications securities, we need to find the protection in the assembly and to change associated opcodes. For example we can modify the result of a if
condition just changing a bne
(branch if not equal) instruction into a beq
(branch if equal) instruction. Or we can load specific values in the registers to change the return value of a function.
Modify executable code
Remember the ptrace
function discussed in detail in our previous blog post? This function can be used to prevent any debugger to be attached to the application when it’s called with the PT_DENY_ATTACH
parameter. As hackers, we would like to get rid of a ptrace
call in an application and replace it with “nothing”. Well, we can (“nothing” being a nop
instruction)! This way, the application will never call ptrace
and we will be able to attach a debugger to it.
Let’s assume we found in the executable file the specific address (0x5f5b2
) where the ptrace
call is made, thanks to some GDB
and otool
action, for instance (not detailed here). Here is the output of otool
:
0x5f5ae: dd f8 0c 90 f8dd900c ldr.w r9, [sp, #12]
0x5f5b2: c8 47 47c8 blx r9 # call to ptrace
0x5f5b4: 0a 99 990a ldr r1, [sp, #40]
0x5f5b6: 01 90 9001 str r0, [sp, #4]
To bypass the call to ptrace
, we just need to replace with an hex editor the code 0x47c8
contained at the address 0x5f5b2
with the code 0xbf00
that is the opcode for a nop
instruction. After the modification, the binary will look like this:
0x5f5ae: dd f8 0c 90 f8dd900c ldr.w r9, [sp, #12]
0x5f5b2: 00 bf bf00 nop # changed with nop
0x5f5b4: 0a 99 990a ldr r1, [sp, #40]
0x5f5b6: 01 90 9001 str r0, [sp, #4]
When this piece of code will be executed inside the application, the nop
instruction will do nothing and will substitute the call to ptrace
. Thus, the application won’t be able to disable future debuggers attached.
Compute a binary signature
An attack at the binary level is difficult to counter. If a hacker is capable of re-writing assembly code, he will certainly be able to bypass all securities we added to our iOS application.
We don’t remain totally helpless though. For example, we can try to detect any modification made to the code. Inspecting the Mach-O header at runtime, we can check the integrity of the executable code. This is actually pretty simple: we just have to go through the load commands of the Mach-O structure, find the assembly section (i.e the __text
section of the __TEXT
segment), and compute a cryptographic hash of the binary data. This signature (or message digest) will be unique and will change whenever the code is modified between the compilation and the execution.
In the following example, we are showing a function that computes and checks the signature integrity of an Objective-C program. In this example, we will use the MD5 algorithm to compute the digest.
#include <CommonCrypto/CommonCrypto.h>
#include <dlfcn.h>
#include <mach-o/dyld.h>
int correctCheckSumForTextSection() {
const char * originalSignature = "098f66dd20ec8a1ceb355e36f2ea2ab5";
const struct mach_header * header;
Dl_info dlinfo;
//
if (dladdr(main, &dlinfo) == 0 || dlinfo.dli_fbase == NULL)
return 0; // Can't find symbol for main
//
header = dlinfo.dli_fbase; // Pointer on the Mach-O header
struct load_command * cmd = (struct load_command *)(header + 1); // First load command
// Now iterate through load command
//to find __text section of __TEXT segment
for (uint32_t i = 0; cmd != NULL && i < header->ncmds; i++) {
if (cmd->cmd == LC_SEGMENT) {
// __TEXT load command is a LC_SEGMENT load command
struct segment_command * segment = (struct segment_command *)cmd;
if (!strcmp(segment->segname, "__TEXT")) {
// Stop on __TEXT segment load command and go through sections
// to find __text section
struct section * section = (struct section *)(segment + 1);
for (uint32_t j = 0; section != NULL && j < segment->nsects; j++) {
if (!strcmp(section->sectname, "__text"))
break; //Stop on __text section load command
section = (struct section *)(section + 1);
}
// Get here the __text section address, the __text section size
// and the virtual memory address so we can calculate
// a pointer on the __text section
uint32_t * textSectionAddr = (uint32_t *)section->addr;
uint32_t textSectionSize = section->size;
uint32_t * vmaddr = segment->vmaddr;
char * textSectionPtr = (char *)((int)header + (int)textSectionAddr - (int)vmaddr);
// Calculate the signature of the data,
// store the result in a string
// and compare to the original one
unsigned char digest[CC_MD5_DIGEST_LENGTH];
char signature[2 * CC_MD5_DIGEST_LENGTH]; // will hold the signature
CC_MD5(textSectionPtr, textSectionSize, digest); // calculate the signature
for (int i = 0; i < sizeof(digest); i++) // fill signature
sprintf(signature + (2 * i), "%02x", digest[i]);
return strcmp(originalSignature, signature) == 0; // verify signatures match
}
}
cmd = (struct load_command *)((uint8_t *)cmd + cmd->cmdsize);
}
return 0;
}
Once this function is integrated in our application, we can check if the original signature (the one computed when we built our project) matches the current signature of the application (the one computed at runtime). If this is not the case, it means that somebody tried to patch the program.
Conclusion
This solution is just one way to check the integrity of an executable file. It can be easily bypassed: in this case, the hacker would just have to re-sign the application. As always, this is only the start of the endless cat-and-mouse game with people trying to bypass security measures, so make up something really hard to crack and your application will last longer than competitors.
Enjoying our security series? Having an idea for a future episode? Tell us on Twitter!
We're hiring!
We're looking for bright people. Want to be part of the mobile revolution?
Open positions