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…

Contributors

Over 300 content creators. Join our team.