iOS App Security and Analysis: Part 2/2

Continuing on the iOS app security theme, learn how attackers might access your code and the steps you can take to maintain the security of your app. By Derek Selander.

Leave a rating/review
Save for later
Share

iOS App Security and Analysis

In the first part of this tutorial, you focused on penetration testing. You mapped out an application using class-dump-z, explored security issues with its plists, modified the network interaction to purchase new and cheaper memes, and explored best practices for using the Keychain.

Here in Part 2, you’ll dig deeper into the code to learn how to increase your iOS app security. You’ll alter functionality by abusing the runtime, as well as reverse-engineer the app by modifying its assembly code. Remember, the goal is not to become a nefarious app exploiter; you’re protecting your app and your users by learning about the steps an attacker might take.

Before completing this tutorial, you should be sure you know how to read and use assembly language for debugging purposes. This topic is covered by Matt Galloway’s excellent iOS assembly tutorial.

Getting Started

You will continue to use the Meme Collector project from the first part of the tutorial. Grab it again from the preceding link if you need to.

You’ll also continue to use the command line utility class-dump-z, as well as the following additional tools:

  • The open source hex editor Hex Fiend.
  • The demo version of IDA, a multi-processor disassembler and debugger. The demo version has a few limitations, but will work for the purposes of this tutorial.

You’ll learn much more about these tools when the time comes to use them!

Runtime Manipulation

In the first part of the tutorial, you modified property list files to gain user currency. Now you are now going to use the GDB debugger to manipulate variables and methods at runtime.

Make sure you are in your main bundle directory. If you are confused about how to get here, please revisit Part 1. You should have the simulator open, with the app already installed (but not running).

Important: As before, you’ll be using Terminal a lot in this tutorial. The > character symbolizes the command prompt and represents the lines you’ll be typing. (gdb) symbolizes the GDB prompt. Shell-style comments beginning with # are used as markers to refer to screenshots or later discussion and should not be typed in or pasted.

In Terminal, type:

> gdb -q   #1
(gdb) attach --waitfor "Meme Collector"   #2

This will launch GDB. The attach command is used to attach to a specific kind of process. You are telling GDB to attach itself to a new process named Meme Collector.

GDB will patiently wait for you to launch the new process. Go to the Simulator and perform the usual ritual of killing its memory contents and re-launching the app. If successful, the Simulator will freeze and GDB will start humming along with its normal setup. Note that you should launch directly the from the simulator and not from Xcode so gdb can attach cleanly.

You might want to add a breakpoint before any ViewController is displayed since that’s where a lot of app logic is set up. A good way to do that is to set a breakpoint on every viewDidLoad call, because almost every UIViewController subclass in iOS overrides viewDidLoad.

In Terminal, type:

(gdb) b viewDidLoad   #3

Make sure you type viewDidLoad instead of viewdidload, as method names are case-sensitive.

GDB will list all the breakpoints matching viewDidLoad. You’ll see several more breakpoints than you initially thought existed. These viewDidLoad methods belong to Apple’s private API, but since you are curious to explore, you might as well enable all the breakpoints.

Terminal GDB

In Terminal, type:

(gdb) 1   #4

You’ve set all the viewDidLoad breakpoints. Now it’s time for you to run your app. Type c (stands for “continue”) and hit return to continue:

(gdb) c   #5

The app will now run right up to the viewDidLoad selector of the ViewController class.

Now it’s time for a little fun. Since you stopped on a frame in the ViewController.m class, you have access to all its instance variables and methods. In addition, since the code section has already been loaded into memory at this time, you have access to all classes, including singletons.

Speaking of singletons, if you looked closely at the singletons as part of the application mapping process in Part 1 of the tutorial, you might have noticed an interesting class by the name of MoneyManager. It contains a purchaseCurrency method that looks interesting to test.

In Terminal, type:

(gdb) call [[MoneyManager sharedManager] purchaseCurrency]

If the return result is 1 (or YES), then the app has successfully purchased more app currency. Since GDB will repeat the previous command if you just press Enter, you can easily add more money with a few more taps.

Meme Collector GDB Runtime

Free stuff has never been so easy to obtain! When you’re bored of getting free money, repeatedly use the c command to step out of all the viewDidLoad breakpoints. Looking at the Simulator, you can see your new amount of currency available for you to spend.

sec-money

Bring up the GDB prompt by typing Control+c in Terminal. To exit the debugging session, enter quit and then y to confirm:

(gdb) quit
The program is running.  Quit anyway (and detach it)? (y or n) y

How can one circumvent attackers manipulating your application through a debugger?

Defending Against Runtime Manipulation

Fortunately, there is a way to check if the program is being debugged. However, the check only determines if a debugger is attached at that specific time. The attacker could attach to the app after this check is made, resulting in the app falsely assuming that everything is okay.

To guard against this, there are two potential solutions:

  1. Create a check that is incorporated into the run loop, so that it constantly checks if the program is being debugged.
  2. Put a check in critical sections of the code where you are most concerned about security.

Most of the time, the first solution is undesirable because the cost for this check could waste valuable CPU cycles. You will go with the second approach for this app.

One elegant implementation for Meme Collector is to put a check for debugging activity in the singleton of the MoneyManager. If there is debugging going on, then you can simply return nil instead of the static instance. The advantage of this is that in Objective-C, performing a selector on a nil object doesn’t do anything.

Finally, you get to use Xcode! Load the Meme Collector project in Xcode and open MoneyManager.m. You will add a preprocessor macro that will check if the app is in release mode, and if it is, will check if a debugger is running and return nil in that case.

Navigate to the sharedManager method in the MoneyManager class. Change sharedManager so it looks like the following:

+ (MoneyManager *)sharedManager
{
#ifndef DEBUG
    SEC_IS_BEING_DEBUGGED_RETURN_NIL();
#endif
    static MoneyManager *sharedMoneyManager = nil;
    if (!sharedMoneyManager) {
        sharedMoneyManager = [[MoneyManager alloc] init];
        [sharedMoneyManager loadState];
    }

    return sharedMoneyManager;
}

SEC_IS_BEING_DEBUGGED_RETURN_NIL() is a preprocessor macro found in an NSObject category. As the name suggests, it returns nil if the app is being debugged.

Note that this macro is only available in release mode. If you followed along with part 1 you should have edited the scheme for release mode already; otherwise, to enable Release mode for a build, click on your scheme and select Edit Scheme. Then in the Info tab, select Release under Build Configuration.

One could argue that it would be better to use an Objective-C method or C function instead of a preprocessor macro. However, there is a very specific reason for using the macro.

Since you found out that attackers can look at the names of all methods and functions quite easily and can patch these methods (which is what you will do yourself in the next section), you want to hide your security check within the singleton. This way attackers will have a much harder time finding and patching the security check code, because they’ll have to pick apart the assembly to do so.

After making the necessary changes, launch the app from Xcode. Again, make sure you are in Release mode by going to the schemes and changing the build configuration to Release.

Xcode automatically attaches the LLDB debugger when the app is launched. As a result, you can see the results immediately. Look at the user’s app currency and observe that there is no UILabel text and the currency isn’t being reported.

sec-nomoney

To make completely sure that this is working the way you want, see if you can do anything while debugging. Since the MoneyManager isn’t available, you won’t be able to buy anything.

Stop the application in Xcode to stop the LLDB debugger as well. After you stop the app, switch to the Simulator and launch the app from its icon. The currency should return since the app wasn’t launched in Xcode with the debugger attached.

Instead of attaching the debugger at the start, you will try attaching the debugger at a random point in time to simulate another way attackers can attach to your app. Open up Terminal and type:

> ps aux | grep "Meme Collector"   #1

The output will list all the processes that match the name Meme Collector:

Terminal GDB Failure

Search for the line that has the launch path for the Simulator app. Once you locate it, take note of the process ID (marked at #2), launch GDB and attach it to the correct process by typing the following command:

> gdb -q -p {Your Process Number Here}   #3

Once GDB is set up, try accessing the MoneyManager singleton with the following commands:

(gdb) call [[MoneyManager sharedManager] purchaseCurrency]
$1 = 0
(gdb) po [MoneyManager sharedManager]
Can't print the description of a NIL object.

As you can see, the manager is not returning anything successful or indicating that the purchase transaction has occurred. Continue running the program with the c command. Try purchasing currency using the Purchase Currency button. It will fail since GDB is still attached.

sad_hacker

Detach GDB by pressing Control-C and entering the quit command. The Purchase Currency button will work just fine now.

In addition to checking for the presence of a debugger, you have the option of taking a more heavy-handed approach. Using the ptrace function, you can flat-out deny a GDB/LLDB process the ability to attach to your application.

To do this, go back to Xcode and open main.m. Replace the contents so that it looks like the snippet below:

#import <UIKit/UIKit.h>
#import "AppDelegate.h"
#include <sys/ptrace.h>

int main(int argc, char *argv[])
{
#ifndef DEBUG
    ptrace(PT_DENY_ATTACH, 0, 0, 0);
#endif
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 
    }
}

The ptrace function is normally used by debuggers to attach to a process just as you’ve seen gdb and lldb do. In this case, ptrace with the special PT_DENY_ATTACH parameter tells the operating system to disallow other processes from attaching themselves to the app.

Now that you have added this, try launching your application from Xcode.

On first inspection, it looks like the app crashes or quits immediately. When Xcode tries to attach the LLDB debugger, it fails and the debugger quits. Since the debugger has quit, Xcode thinks everything is over and stops the application as well.

Try opening the application directly from the Simulator instead and you will see it runs just fine.

Now try attaching a debugger via the gdb -p {process number} method previously discussed and see what happens. The process will fail to attach.

This can be a good solution for stopping script kiddies from playing around with your app, but it will not deter veteran attackers. More experienced hackers will be able to stop at the ptrace function call and modify it before continuing.

oh_god_why

Note: Don’t get too comfortable. Hackers often use Cycript, a JavaScript-styled program that can manipulate Objective-C apps at runtime. The scariest thing is that the previous logic to check for debugging activity fails when Cycript is attached. Remember, nothing is truly secure…

What’s in a Binary?

You will soon be modifying the binary to do your bidding, but for you to understand how to modify it, you need to first understand how to take it apart and see what exists and where.

From here on out, I will be periodically referencing specific addresses in a binary to illustrate certain concepts. If you are running a version of Xcode other than the current 4.6.3 (or if you are compiling using debug instead of release, or have made any changes to the project at all), the compiler will compile the code differently, resulting in an address that’s different from the one I’m referencing. This is okay – you just need to follow along closely to understand the idea.

Typically, a Mach-O binary (the executable format for OS X and iOS) will begin with a header that contains a list of all the information about the binary and where it is stored. This information is followed by the load commands, which tell you the layout of the file through segments. In addition, these load commands will dictate specific flags for how the file should be interpreted – for example, whether or not the binary is encrypted.

In each segment, there are one or more sections. There are two sections worth noting:

  • The Text Section: This section is largely for read-only data. For example, it includes the source code section, the C-type strings section, the constants, etc. The nice thing about read-only data is that if the system is low on memory, it can free up data from these segments and reload them from disk, as needed.
  • The Data Section: This section is largely for writable data from the program. This includes the bss section for static variables, the common section for global variables, etc.

The Apple developer documentation has an excellent breakdown of the Mach-O file format.

You will now examine the Meme Collector binary to see all of this in action, beginning with Meme Collector’s header in the binary. In Terminal, make sure you’re in the main application bundle and type:

> otool -h Meme\ Collector

This will print out Meme Collector’s Mach-O header, which should look like this:

Meme Collector:
Mach header
      magic cputype cpusubtype  caps    filetype ncmds sizeofcmds      flags
 0xfeedface       7          3  0x00          2    24       3412 0x01000085

Notice that it has 24 load commands, each with a size of 3412 bytes. Now that you have looked at the header, it’s time to look at those load commands. Type the following into Terminal:

> otool -l Meme\ Collector

You will get a lot of output. Scroll up until you come across the sectname __classname section. Check out the offset, which is 160164 in decimal in the screenshot below. Take note of this address.

Open up a second Terminal window and navigate to your main application bundle. In the second Terminal window, type:

> strings -o Meme\ Collector

The -o flag for the strings command displays the global offset of where the strings are in the binary.

Terminal Binary Sections

Right above the __objc_classname section is the __objc_methname section. Navigate to the appropriate address offset in the strings dump and view the difference in strings.

The strings immediately preceding the __objc_classname are names of Objective-C methods found in various classes. The strings whose addresses are greater or equal to the __objc_classname offset clearly show classes, while the strings above show methods for various subclasses of NSObject.

As you can see, these load commands neatly segregate the chaos of 1s and 0s found in a Mach-O binary. With this knowledge in hand, you will modify the “code” section next.

Disassembling and Reversing

OK, you’ve made it this far. Now it’s time to bring out the big guns.

In this section, you will learn how to modify the application binary to do your bidding. You often hear about this in the real world when people say that an app has been “cracked.” That is, someone has modified the app to do something it was not meant to do.

You’ll need the help of IDA and a hex editor such as Hex Fiend. IDA is the tool reverse engineers will most likely turn to when inspecting an application binary. It is an incredibly powerful disassembler, debugger, and decompiler. It also costs a fair bit of money to purchase the full version.

If you don’t want to shell out the dinero required for a program that you only heard about 15 seconds ago, IDA offers a demo version with limited functionality. The demo version restricts the type of assembly files you can look at as well as disables all modification features. However, the IDA demo supports x86 assembly, which is what we are using, and since you are so awesome and have made it this far, you will make the necessary modifications by hand in Hex Fiend.

Note: As you’ve learned from the assembly tutorial, an iOS app is an ARM binary. For apps on the Simulator, Xcode compiles the binary for the i386 architecture. This architecture uses x86 assembly. So code for one architecture will not run correctly on the other.

Download the IDA demo and open it up. You’ll be presented with the quick start menu containing an image of Ada Lovelace, who is recognized as the world’s first computer programmer.

IDA Startup

Click on the Go button to prepare IDA for disassembly.

In Terminal, you should still be in the main application bundle. Open the containing folder in Finder using the following command:

> open -R .

From Finder, right-click the main application bundle directory and click on Show Package Contents.

Finder Open App Bundle

Once inside the main application bundle, drag and drop the Meme Collector binary into IDA. The screen below will pop up telling you it has recognized the type of binary.

IDA Load File

Make sure your settings match the picture (typically you will not need to change anything) and press OK. IDA will process the binary and figure out the appropriate mapping as it disassembles it.

IDA may ask the following: Objective-C 2.0 structures detected. Do you want to parse them and rename methods? If it does, just hit Yes.

If IDA asks about proximity view, select No.

Once IDA has finished processing the binary, you will be shown the main display. Look at all those buttons! If IDA looks different than the screen below or if you get lost, double-click on start in the function name panel on the left and keep pressing the space key until the correct window shows up.

IDA Main Screen

Fire up the Xcode project as well. Although this won’t be the case with real life attacks, it’s good to see what you’re doing side by side with the source code and assembly. Navigate to MoneyManager.m and view the buyObject: method.

- (BOOL)buyObject:(id<PurchasableItemProtocol>)object
{
    NSUInteger totalMoney = self.money.unsignedIntegerValue;
    NSUInteger cost = [object cost].unsignedIntegerValue;
    
    if (totalMoney < cost) {
        return NO;
    }
    
    _money = @(totalMoney - cost);
    
    return [self saveState];
}

Looking at the logic, if the instance variable _money doesn’t have an adequate amount, it will return false and the transaction will not take place. This conditional statement for being able to purchase something rests entirely upon a bool value: does the user have enough money? If you were to simply “jump” over this check, you could purchase whatever you wanted and _money would not even be a factor in purchases.

Now you can look at the same code in IDA’s disassembler. Switch back to IDA and click on any function in the Functions panel on the left side. Type control+F or go to Edit/Quick Filter to bring up a quick filter text field. Enter buyObject into the filter. One result should show up.

IDA Quick Filter

Double-click on it. You are given a disassembled window view that nicely showcases the conditional branch equivalent from the code.

IDA Graph View

Without knowing anything about x86 assembly, you can accurately assume that the path on the right is probably the path that you want your code to take, based upon the source code in buyObject:.

Look at the jump operand immediately preceding the branching paths. It is a jnb, which stands for Jump if Not Below. It sounds like you should change that to a Jump Always or a jmp instruction.

You need to find where this instruction is so you can change it. Double-click on the jnb operand. It should highlight itself in yellow. Now press space to view this in text view mode.

This will give you a new screen that displays the same information except in linear form, much like a source code viewer. Find the necessary location of the jnb command and note the address in which it resides, next to text: on the selected line. For this tutorial, the address is 0x00018F12. Remember, if you are following along, your address could be different.

IDA Text View

The opcode for the jnb short is 0x73_ _, where the blanks represent the relative offset by which you want to move. You need to change the jnb opcode to a jmp short instruction, which is 0xEB_ _.

How can you figure out these values and their opcodes by yourself? They can be determined from Intel’s processor PDF manual, which is a truly exciting, gripping read.

Download Hex Fiend if you have not already. Provided that you put it in your applications directory, you can open your binary through Hex Fiend with the following command:

> open -a /Applications/Hex\ Fiend.app/ Meme\ Collector

The window below will open to let you view your binary through Hex Fiend. Pretty, isn’t it?

Hex Fiend Start

Now in Terminal, type:

> otool -l Meme\ Collector | grep -a10 "sectname __text"

As you saw previously, running otool -l displays all of the load commands for the binary. Since you only care about the “code” section (aka the text section), you limit that search. You should get output similar to the below screenshot.

Terminal Text Section

Looking at the __text section name, the starting address is 0x00002a80 with an offset of 6784 in decimal. You can confirm that the text section start address in the binary is indeed 0x2A80 in IDA by scrolling to the very, very top and seeing where the code section begins. Remember, your values may differ slightly – make sure you're looking at the addr (hex) and offset (binary).

All right, now it's time to do a bit of math. You need to translate the offset from the JNB instruction in the text section to the absolute offset of the binary. If you try modifying the 0x18F12 absolute instruction found in the IDA binary, then you’ll likely get a crash somewhere in your app.

Command Location - Text Section Start Address + Offset = Absolute Offset in binary
0x00018F12 - 0x2A80 + 6784  = 0x17F12 (or 98066 in decimal)

If your values are different, I've provided a small tool to do the math for you. Make sure to omit the 0x before hex values.

If your math is correct, this will be the location of the JNB instruction you saw in IDA. In Hex Fiend, bring up the Jump to Offset selector by going to Edit/Jump To Offset. Enter the absolute offset value. If you use a hex value, make sure you tell the program you are using hex by supplying the 0x before the value.

Sure enough, the sought after JNB instruction with the 0x73 opcode is found exactly at 0x17F12:

Hex Fiend Location Found

You will now change that to a JMP instruction by replacing 0x73 with 0xEB. Change the 8-byte grouping (also known as a “word”) that was originally 0x39FE7304 to 0x39FEEB04.

Note: Your JNP instruction may occur in any part of a word if your values have been different. Just make sure to look for 73 in the line that Hex Fiend finds after entering the address.

Save the file in Hex Fiend and go to the Simulator. Hard close and then re-launch your app. Proceed to purchase items until you run out of money. Notice what happens to your currency when you purchase an item that is greater than your total currency.

You effectively threw out the conditional check to see if the user had enough money. Even if the user has no money, the transaction still occurs. As a bonus, the value wraps around and updates your meme currency to a whopping 4 billion! Since the _money instance variable is interpreted as an unsigned integer, the value is viewed as a large positive 32-bit number instead of a negative number.

Defending Against Reverse Engineering

So how do you defend against reverse engineering of a binary? Remember when I said nothing is truly secure? Yep, that applies to this as well. Reversing an application through assembly can be incredibly complicated and tedious, yet there is little you can do to stop someone if they are determined. Your best hope of defense is to confuse the attacker so much that they will give up and turn their attention to other apps.

One way to do this is to apply name changes to important classes and methods via the preprocessor. Open Xcode and locate Meme Collector-Prefix.pch. Add the following code to the file:

#define MoneyManager DS_UIColor_Theme

This replaces all instances of MoneyManager with the more boring name DS_UIColor_Theme, hopefully one which attackers would find less interesting to inspect.

Great care should be taken with this approach. This is a "hacky" solution at best and you need to be absolutely certain that your replacement name is unique to your app, or else some really weird bugs will start cropping up.

Normally, the app binary has a symbol table that maps addresses to human-readable function and method names. Another solution to make code harder to find is to strip the symbol table after you have built the project. This is mostly useful for hiding C/C++ functions since Objective-C messages are dispatched with objc_msgSend.

In Xcode, open MoneyManager.m and add the following C function at the top:

BOOL aSecretFunction(void) {
    return YES;
}

After you add this function, recompile the app. In Terminal, check for the existence of this function in the symbol table:

> nm Meme\ Collector | grep aSecretFunction
000193ad t _aSecretFunction

The nm command displays information regarding the symbol table. An easy way to strip the symbol table of C/C++ function information is by going into Xcode and changing around the Deployment Postprocessing and Strip Linked Product flags to YES.

Go to the Meme Collector project and click on Meme Collection target, then the Build Settings tab and type post into the search bar.

Xcode Build Settings Strip

The two options Deployment Postprocessing and Strip Linked Product will come up. Change both of those to Yes. Make sure you clean and then recompile the app.

To test that the symbols were stripped, go to Terminal and rerun the previous command:

> nm Meme\ Collector | grep aSecretFunction
>

Excellent. You’ve successfully stripped the symbol linking to aSecretFunction. This will result in the attacker having to do more work to hunt down a critical section of code.

Where to Go from Here?

As you’ve seen, an attacker can:

  • easily view the Objective-C selectors;
  • manipulate the files associated with your binary;
  • manipulate the network interactions;
  • manipulate the runtime;
  • and even change your app’s binary.

When building an application, it’s important to remember these points. Although security is crucial, you also need to consider the additional effort required to make an app more secure. Security is always a balance between your time and resources, the amount of trouble for your users, and the likelihood of attack.

iOS app security is a big topic; there's a lot to learn and you've only just scratched the surface of what the debugger and analysis tools can do! If you found this topic interesting, you should:

  • Consider jailbreaking your iOS device: You can learn SO MUCH from exploring the filesystem.
  • Check out Hacking and Securing iOS Applications: Although it is a smidge dated (you will need to look to Google for updates as to how Apple handles app encryption), this book is one of my favorites for iOS and security.

As always, feel free to join in the forum discussion below if you have any questions or comments.