Overview

PreEmptive Protection for iOS - Control Flow (or PPiOS-ControlFlow, for short) is a specialized compiler that compiles Objective-C code into obfuscated native code. It achieves this via a series of "control flow" transforms that alter the operations the computer performs without altering the result of those operations, which makes the resulting binary image much more difficult to reverse engineer or modify.

Another form of obfuscation, "renaming", works by changing the names of symbols (variables, methods, class names, etc.) so that their meaning is obscured. PreEmptive Solutions offers an open-source product, PreEmptive Protection for iOS - Rename. PPiOS-Rename is meant to work alongside PPiOS-ControlFlow; together they provide much better protection than either one alone can provide.

If you have both PPiOS-ControlFlow and PPiOS-Rename, follow the instructions in Using PPiOS-ControlFlow with PPiOS-Rename.

Supported Platforms

PreEmptive Protection for iOS - Control Flow supports apps developed for:

Using:

Installation

To install the software, extract the archive and run the install.command script either from the terminal, or by running it through Finder. This will install PPiOS-ControlFlow to the proper location and configure it to work with the default installation of Xcode in /Applications/Xcode.app. Enter your license key when prompted by the installer. Access to the Internet is required to validate the license key.

Note: Installations using an Evaluation or a Subscription license will continue to require access to the Internet.

After installation, default installs of Xcode will automatically use the PPiOS-ControlFlow compiler whenever the appropriate arguments are configured (see below).

However, if you install another version of Xcode in another directory, or have modified or updated your Xcode installation and your obfuscated projects no longer build, then you need to reinstall PPiOS-ControlFlow into Xcode. You can do this by using the install_xcode_integration.sh shell script, e.g.:

#Assumes default Xcode installation is in the directory /Applications/Xcode.app
/usr/local/share/preemptive/PPiOS/bin/install_xcode_integration.sh

You can optionally pass an argument with the path to a non-default install of Xcode, e.g.:

/usr/local/share/preemptive/PPiOS/bin/install_xcode_integration.sh /Applications/Xcode2.app

You can also uninstall this integration by using the uninstall_xcode_integration.sh script.

/usr/local/share/preemptive/PPiOS/bin/uninstall_xcode_integration.sh /Applications/Xcode2.app

Uninstallation

To uninstall PPiOS-ControlFlow, run the uninstall.command program. This program is located in the installation directory of PPiOS-ControlFlow,

/usr/local/share/preemptive/PPiOS/uninstall.command

Note: If Xcode is installed in an alternate location, the PPiOS-ControlFlow integration in Xcode would need to be removed manually before uninstalling PPiOS-ControlFlow:

/usr/local/share/preemptive/PPiOS/bin/uninstall_xcode_integration.sh /Applications/Xcode2.app

Project Setup

To configure an app to be obfuscated by PPiOS-ControlFlow, you must pass additional arguments to the compiler to enable and configure the obfuscation.

For projects built from the command-line (e.g. Makefiles), it is easiest to set the CFLAGS environment variable with these arguments, and they will be passed to the compiler.

For projects built from Xcode, you can configure Xcode to set the flags:

  1. Go to the Xcode project options.
  2. Make sure to use the All instead of Basic filtering near the top of the properties window.
  3. Select the Combined view, and not the Levels view.
  4. Find the Apple LLVM 7.x - Custom Compiler Flags heading.
  5. Underneath it, double-click on the Other C Flags item.
  6. From there, use the + button to add the desired obfuscation arguments.

1: Enable Obfuscation

There are three different ways to enable obfuscation and to specify obfuscation options:

Control Flow Level branch-injection block-injection opaque-predicates switch-obfuscation
High On On On On
Medium On On On -
Low On On - -
Off - - - -

Note: If you specify a level on the command line, it overrides the enabling and disabling of transforms in a configuration file. If you specify multiple -ppconfig or -control-flow arguments on the command line, all except the final ones will be ignored.

(See below for an overview of each transform.)

2: Configure Transforms

If more specific configuration is required, a config file may be used. The config file is a YAML-based document. The smallest allowed document is:

control-flow: <level>

You can enable individual transforms by specifying them:

control-flow:
  level: <level> #Level is required
  switch-obfuscation: <true|false>
  branch-injection: <true|false>
  opaque-predicates: <true|false>
  block-injection: <true|false>

You can also set a level and override the setting for a given transform.

control-flow:
  level: low
  switch-obfuscation: true

By default, the enabled transforms will be applied to all possible functions.

If you wish to limit the transforms to specific functions, add an include with one or more expressions that match the function name(s):

control-flow:
  level: <level>
  defaults:
    include: <Expression>

or

control-flow:
  level: <level>
  defaults:
    includes:
      - <Expression1>
      - <Expression2>
      - <Expression3>

Notice that the defaults keyword can also be used in the singular form, default. The same is true for include and exclude keywords as well. The singular and plural forms of these keywords are equivalent.

If you need to override that global setting, or just set an expression for an individual transform, you can specify an include under the transform.

control-flow:
  level: <level>
  switch-obfuscation:
    include: <Expression>

When an include is used, only functions matching the regular expression will be altered.

Note: If the provided expression does not match any functions, nothing will be altered.

If you wish to exclude certain functions, add an exclude with one or more expressions that match the function name(s):

control-flow:
  level: <level>
  defaults:
    exclude: <Expression>

or

control-flow:
  level: <level>
  defaults:
    excludes:
      - <Expression1>
      - <Expression2>
      - <Expression3>

Again, if you need to override that global setting, or just set an expression for an individual transform, you can specify an exclude under the transform.

control-flow:
  level: <level>
  defaults:
    exclude: <Expression>
  branch-injection:
    exclude: <Expression>

With the exclude argument, any function that matches the expression will not be altered by that transform, even if it is also matched by an include argument.

The expressions can either be regular expressions or simple strings to be matched. To define a regular expression enclose it in '/' characters (e.g. "/[Ss]ecure/" ). If you want case insensitive matching, end the regular expression with'/i' instead of '/' (e.g. "/secure/i"). Simple strings are always case insensitive.

For example, to obfuscate an app with Branch Injection, and only affect functions that have "Secure" in their name, but exclude any functions that have "UI" in their name, use this configuration:

control-flow:
  level: off
  branch-injection:
    include: "Secure"
    exclude: "UI"

To also apply Block Injection to all functions, extend the configuration as follows:

control-flow:
  level: off
  branch-injection:
    include: "Secure"
    exclude: "UI"
  block-injection: true

To extend the exclusion so it covers "Test" as well as "UI", the argument list would be:

control-flow:
  level: off
  branch-injection:
    include: "Secure"
    exclude: "/UI|Test/i"  #Using a regular expression
  block-injection: true

or

control-flow:
  level: off
  branch-injection:
    include: Secure
    excludes:
     - "UI"
     - "Test"
  block-injection: true

Note: To configure include or exclude for a specific transform, you must override the configured defaults set for include and exclude.

The expressions need to match the class and function names as the compiler sees them.

For example, functions in a class declaration like:

@interface FPPDataModel : NSObject
+ (NSString *) stringForIndex:(NSInteger) index; //Static method
- (NSInteger) currentIndex; //Instance method
- (void) recordMessage:(NSString *)msg authCode:(FPPAuthCode *)auth  userName:(NSString *)user;
@end

would be seen by the compiler as:

+[FPPDataModel stringForIndex:]
-[FPPDataModel currentIndex]
-[FPPDataModel recordMessage:authCode:userName:]

Here are some sample expressions:

Expression Matches
"Test" All functions with "test" and all functions in classes which contain "test". e.g.
-[AAAAppModel isTest]
-[AAAAppModel testUIPassword]
-[AAAAppTestModel secureConnection:errorOnFail:]
Note the quotes.
"/[Ss]ecure/" An uppercase or lowercase 'S' followed by "ecure". e.g.
-[AAAAppModel isSecureConnection:]
-[AAAAppModel testSecureConnection:errorOnFail:]
-[AAAAppModel secureUIPassword]
Note the quotes.
"[AAAAppModel " All functions in the AAAAppModel class.
Note the quotes and space.
"-[AAAAppModel " Instance functions in the AAAAppModel class.
Note the quotes and space.
"+[AAAAppModel " Static functions in the AAAAppModel class.
Note the quotes and space.
"/\\[AAAAppModel |\\[AAAAppDelegate /" All functions in the AAAAppModel and AAAAppDelegate classes.
Note the quotes and spaces.
"-[FPPDataModel recordMessage:authCode:userName:]" A particular function in the FPPDataModel class. (String Match)
"/-\\[FPPDataModel recordMessage:authCode:userName:\\]/" A particular function in the FPPDataModel class. (Regular Expression Match)
Note the \\.

This last pattern could, alternatively, be written without quotes as:

/-\[FPPDataModel recordMessage:authCode:userName:]/

Only one backslash ('\') is required to escape the left bracket ('[') to avoid its interpretation as a regular expression control character. The YAML parser will interpret a backslash within quotes to be the start of an escape sequence. In the example above, the first backslash is required to escape the second when parsed by the YAML parser, and the second is required to escape the left bracket for the regular expression parser. The right bracket (']') need not be escaped.

Note: We recommend encapsulating expressions in quotes, so they are read properly by the YAML parser.

There is additional configuration that affects the behavior of the transforms, which you may wish to set:

These setting will be located inside the particular transformation in the configuration file.

control-flow:
  branch-injection:
    block-percent: <percent>
  switch-obfuscation:
    block-percent: <percent>

Note: The <percent> must be set to an integer value. (e.g. 76 for 76%)

3: Configure Random Number Generator

Some transforms use a pseudo random number generator to determine which functions or blocks should be changed and/or how they will be changed. By default, a random seed is used to initialize the generator. However, if you want better reproducibility between obfuscations of the same code, you can specify a 32-digit hexadecimal number (a 16-byte value) to be used as the seed, optionally prefixed by '0x'. It can be specified in three ways.

Note: The command line argument will override the environment variable. The environment variable will override a value in the configuration file. If the value you provide is not the correct size, an error will occur and compilation will fail.

Here are some examples of valid seeds:

Note: If you specify multiple -random-seed arguments on the command line, all except the final one will be ignored.

4: Debug Transforms

There is debug information which can be output to help you see what is being done to your source files.

The output is accessible in Xcode in the Report Navigator by selecting the appropriate build and expanding the compile command for the file of interest. Make sure to set the filter to All Messages.

Alternatively, the entire build log can be obtained by right-clicking on one of the messages, and selecting Copy Transcript for Shown Results (All, All Messages) as Text.

Configuration File Example

Here is a example of a full configuration file.

random-seed: 0x0A98599D58F805A80A16466920C39096
control-flow:
  level: low
  defaults:
     includes:
      - "[TLA"
      - "[TLB"
     exclude: "Test"
  branch-injection:
    block-percent: 55
  block-injection: false
#    include: "[TLAAppClass "
#    exclude: "Test"
  switch-obfuscation:
    include: "[TLAAppClass "
    exclude: "/Test|UI/i"
    block-percent: 85
  opaque-predicates: true

In this sample, the value 0x0A98599D58F805A80A16466920C39096 is used as the random seed. Switch Obfuscation and Opaque Predicates were turned on and Block Injection was turned off. The percent of blocks for Branch Injection was set to 55% and the percent of blocks for Switch Obfuscation was set to 85%. Both Branch Injection and Opaque Predicates will use the default include and exclude settings, but Switch Obfuscation has its own. Notice that to turn off Block Injection, the include and exclude were commented out.

Using PPiOS-ControlFlow with PPiOS-Rename

PPiOS-ControlFlow and PPiOS-Rename work together, each handling different aspects of obfuscation. Depending on the specifics of how your application works, configuration of the two programs may need to be tuned. For example, it may not be feasible to inject branches into tight loops of an algorithm, and PPiOS-ControlFlow would need to be configured to exclude such methods. Similarly, if the application depends on a non-open-source Cocoapod, PPiOS-Rename would need to be configured to exclude all of the classes in the Cocoapod's namespace. It is recommended, therefore, that you get your application to work with PPiOS-ControlFlow and PPiOS-Rename independently, before using the products together.

PPiOS-ControlFlow and PPiOS-Rename mostly work together without interfering with each other, except in the case that the configuration for PPiOS-ControlFlow references class or method names that have been changed by PPiOS-Rename. Thus, PPiOS-ControlFlow needs to know about the renamings to expect.

After obfuscating the sources with PPiOS-Rename, the application can be compiled with PPiOS-ControlFlow by adding the following options to the command-line:

-ppios-rename-map=<symbols-map-filename>

Alternatively, the rename map may be specified as a root-level configuration in the configuration file named rename-map taking a single string argument:

rename-map: "symbols.map"
control-flow: ...

It may also be specified with the environment variable PPIOS_RENAME_MAP.

We recommend that the rename map be specified either with the command-line option or with the environment variable, so that the same configuration file could be used to produce a build obfuscated only with PPiOS-ControlFlow.

Combined Build Process

The combined build process is essentially that used for PPiOS-Rename alone, except that the last step is compiled with PPiOS-ControlFlow:

  1. Ensure all local source code changes have been committed
  2. Build the program without obfuscation
  3. Analyze the program (PPiOS-Rename)
  4. Apply renaming to the sources (PPiOS-Rename)
  5. Build the program again (PPiOS-ControlFlow)

Step 5 is the same as when using PPiOS-ControlFlow alone, except that the rename map must be specified in one of the ways described above.

Integrating PPiOS-ControlFlow and PPiOS-Rename with Xcode

To configure your Xcode project to build the application with PPiOS-ControlFlow and PPiOS-Rename, first follow the instructions in the PPiOS-Rename documentation about integrating it into Xcode. Once that is complete:

  1. Duplicate the original target again, and rename it to Unobfuscated <original-target-name>.
  2. Edit the scheme (or add one) for this new target, renaming the scheme to Unobfuscated <original-scheme-name>.
  3. Now select the original target, select Build Settings, select All settings, select Combined view, and search for cflags.
  4. In the Other C Flags edit box, add:
    1. -ppconfig=<your-ppios-config> or -control-flow=<high|medium|low>
    2. -ppios-rename-map=<your-symbols-map>

Once this is complete, there should be four targets with corresponding schemes:

  1. YourApp
  2. Build and Analyze YourApp
  3. Apply Renaming to YourApp
  4. Unobfuscated YourApp

Building with both PPiOS-ControlFlow and PPiOS-Rename is accomplished by building targets #2, #3, and then #1. This will produce an app named YourApp.app and will have both control-flow obfuscation and renaming obfuscation.

The app can also be built without either form of obfuscation by just building target #4. This will produce an app named Unobfuscated YourApp.app.

The app can be tested with PPiOS-Rename alone by building targets #2, #3, and then #4. This will produce an app named Unobfuscated YourApp.app, but it will have renaming obfuscation.

The app can be tested with PPiOS-ControlFlow alone by creating an empty rename map (echo '{}' > symbols.map) and building target #1. This will produce an app named YourApp.app which will only have control-flow obfuscation.

Note: Always remember that target #3 will change your sources, and any change it makes will need to be reverted before doing any of these build sequences, and before continuing development.

Transforms

PPiOS-ControlFlow works by performing various transforms that modify the control flow of the code that is being compiled. The resulting binary is still functionally identical to an un-obfuscated binary, but it will be more difficult to reverse engineer.

The following transforms can be performed:

Switch Obfuscation

Switch Obfuscation works by altering the control flow of your program from a series of specialized constructs (if, switch, loops) and changes them to be just one type of control flow construct: A switch with a loop surrounding it.

An example function might be originally written like so:

int max(int x, int y) {
    if (x >= y) {
        return x;
    } else {
        return y;
    }
}

After the Switch Obfuscation transform is completed however, this code would look something like this:

int max(int x, int y) {
    int switchvar = 1942843;
    while (true) {
        switch (switchvar) {
            case 598232:
                return y;
            case 1942843:
                if (x >= y) {
                    switchvar = 8289314;
                } else {
                    switchvar = 598232;
                }
              break;
            case 8289314:
                return x;
        }
    }
}

As you can see, this makes the control flow much more difficult to follow. With more complicated functions this of course becomes much more effective at obscuring the actual control flow used by the code.

Note: Switch Obfuscation may significantly increase the time functions take to execute. We recommend you use the include configuration to only apply it to certain functions or the block-percent option to limit the application of the transform.

Branch Injection

Branch Injection works by inserting if statements and placing code that would form an infinite loop in the portion that is never reached.

An example function might be originally written like so:

int max(int x, int y) {
    if (x >= y) {
        return x;
    } else {
        return y;
    }
}

After Branch Injection, the code becomes much more convoluted:

int max(int x, int y) {
    top:
    int i = 0;
    if (1 == 1) {
        othertop:
        if (x >= y) {
            int ret;
            if (1000 != 20) {
                ret = x;
            } else {
                ret = x;
                i++;
                goto othertop;
            }
            return ret;
        } else {
            return y;
        }
    } else {
        int ret;
        if(x >= y) {
            ret = x;
        } else {
            ret = y;
        }
        i++;
        goto top;
    }
}

Opaque Predicates

Opaque Predicates works by adding changing adding mirrored constants standard mathematical operations. An example would be changing x + y to x + 3283 + y - 3283. This transform does not make the code significantly more difficult to understand, but when combined with other transforms it can make the control flow significantly more complicated and difficult to reverse engineer.

An example function might be originally written like so:

bool isTwenty(int x, int y) {
    if ( (x + y) == 20 ) {
        return true;
    }
    return false;
}

After using Opaque Predicates, the mathematical calculation has changed:

bool isTwenty(int x) {
    if ( (x + 42 + y - 42) == 20 ) {
        return true;
    }
    return false;
}

Block Injection

Block Injection is another obfuscation transform that is weak by itself, but helps the other transforms to be more powerful. This can help the other transforms by providing more places for code injection and obfuscation.

An example function might be originally written like so:

int printsomething() {
    printf("Hello");
    printf("World!");
    return 0;
}

After Block Injection is performed, the code would look something like this:

int printsomething() {
    printf("Hello");
    goto two;
    two:
    printf("World!");
    goto three;
    three:
    return 0;
}

Confirming Obfuscation

There are multiple approaches you can use to see the effects of obfuscation:

1: Analyzing the LLVM IR

LLVM, and thus clang, uses its own intermediate language called IR. It is similar to assembly, but is processor agnostic. LLVM IR is also strongly-typed and has high-order constructs like switches, similar to .NET IL or Java bytecode. LLVM IR is the primary code that LLVM uses for optimizations, and it is what PPiOS-ControlFlow uses to perform its obfuscation transforms. After LLVM runs all of the "passes" for optimization and analysis, it eventually compiles it to actual machine code for a given processor.

Clang and LLVM can output the LLVM IR instead of compiling it to machine code. This can be useful for judging how well obfuscation is working. You can pass the command line arguments -S -emit-llvm to clang to have it output LLVM IR code instead of machine code. The build will fail at the linking stage, though, because the usual .o files will have plain-text LLVM IR code instead of the usual machine-level code. You can view the .o files in a text editor to compare the un-obfuscated code to the obfuscated code.

You can see the path to the .o files in the build output. Usually they will be under $HOME/Library/Developer/Xcode/DerivedData/.

2: Using LLVM to generate a call graph

Once you have generated plain-text IR (see above), you can then generate a control-flow graph of individual functions. This can be done by running:

/usr/local/share/preemptive/PPiOS/bin/opt -dot-cfg path/to/plain_text_IR.o

Note that this will usually generate a warning about the output going to the screen. You can ignore the warning, or suppress it by adding -o <anything> to the command line.

This generates a series of .dot files, one per function in the .o. file. These files will usually have control characters in their names, so it is often easiest to open them via the Finder. The most popular program that can display .dot files is Graphviz.

3: Using a reverse assembler and call graph generator

You can also use one of the many reverse assemblers to produce a call graph from the machine code that is output as the final output. Because these tools operate on the resulting machine code, there is much less information available and it will be harder to decipher the reverse-assembled code.

The most popular reverse assemblers with call graph support are:

  1. Hopper
  2. IDA (note, only Pro is capable of reverse assembling ARM code)

Troubleshooting

Ensure Xcode integration is properly installed

Open a new terminal and type clang --version. You should see a response like:

Apple LLVM version ...

And when you type clang --version -control-flow=high, you should see a response like:

PPiOS-ControlFlow LLVM version ...

If you get a different error running either of these commands, it may mean that the integration is not working properly. Try running the install_xcode_integration.sh script again to reinstall the integration.

Clean DerivedData directory

In some cases, you may receive an error about the module cache being out of date. To force Xcode to rebuild its module cache and thus resolve this issue, just delete the directory Library/Developer/Xcode/DerivedData in your home directory. Alternatively, you can run this from a terminal:

rm -rf ~/Library/Developer/Xcode/DerivedData

At the same time as you do this, you may also want to clean your project's build folder completely. You can do this by using Command-Option-Shift-K. Xcode will ask for confirmation before cleaning the folder.

Reduce or disable optimizations

In some cases, certain optimizations can interact with the obfuscation transforms to either cause a compiler or runtime error. If you experience either of these problems, reducing or disabling optimizations may help. This setting can be changed in Xcode by going to the project properties and selecting "Fast [-O -O1]" or "None [-O0]".

Reducing obfuscation transforms

In some cases, reducing the number of obfuscation transforms and selecting only certain ones can help, especially if reducing or disabling optimizations does not help or is not ideal for the application. You can do this by only selecting certain obfuscation transforms rather than using all of them.

Configuration file errors

The configuration file is a YAML document which uses whitespace to determine hierarchy. Here are some errors you may encounter:

Performance problems

In some cases, obfuscation can have a significant impact on the performance of an application. In order to ensure your application is as obfuscated as possible without performance impacts, you can try the following:

Profiling obfuscated code

When using obfuscation, the stack trace and the amount of time spent in each function is correct. However, it should be noted that the line numbers which correspond to the most time taken in a function may be inaccurate. For instance you may see that a simple assignment line is identified as the most expensive part of a function. This can be caused by the obfuscation related code being injected near this line number in the machine code. It is recommended to not use obfuscation when profiling a function's code, and instead to only profile obfuscated code when determining which set of functions to exclude from obfuscation.

Update checks

PPiOS-ControlFlow will automatically check for updates. It will not check more than once a day. If you want to disable this check, you have several options (in order of precedence):

  1. Add a command line argument -enable-update-check=<true|false>. Enable or disable the update check.
  2. Set a PPIOS_ENABLE_UPDATE_CHECK environment variable which specifies true or false.
  3. Add it to the system configuration file /etc/preemptive/ppios.config: enable-update-check: <true|false>.

Evaluation Copy

There are some limitations on the Evaluation version of PPiOS-ControlFlow:

  1. It must be used on a machine with an active connection to the Internet.
  2. A compiler warning is always issued indicating that it is an evaluation version. Note: This warning (as with all PPiOS-ControlFlow warnings) can be silenced with the -Wno-PPiOS option.
  3. Attempting to compile for an iOS device will produce a compiler error. The evaluation version may not be used to create binaries for general release.


PreEmptive Protection for iOS - Control Flow Version 2.3.1. Copyright 2016 PreEmptive Solutions, LLC