Our Blog

on ios binary protections

Reading time ~11 min

I just got off a call with a client, and realised we need to think about how we report binary protections a bit more. More specifically the ios info binary command in objection. They can be a pain to explain if not well understood, and even harder to remediate! Binary protections make exploitation attempts much harder so, naturally we want all of them on. However, as you’d see in this article, not everything can always be enabled and sometimes it’s hard to understand why.

ios binary protections information parsed by objection

a quick primer

Before diving into the protections themselves, let’s take a look at how objection parses each. The core of the parsing logic lives here, leveraging the nodejs macho package. Using macho, we can programatically parse any Mach-O format file for information about the binary. The macho package lets you specify a file to parse, and because we are already injected into a target process using Frida, we can access any file in the application bundle too.

Reading the info() function in the agent you’d see we get the loaded modules using Frida API’s (Process.enumerateModules(), and then proceed to parse each file using the macho package. Once we have parsed a target binary or library, we proceed to ask some questions to determine if some binary protections are enabled. Let’s take a look at the PIE, ARC, and Canary flags.

pie

As you’d expect, a Mach-O file has a header. Part of the header structure is the flags field which is a bitmask of all the applicable flags for that binary. (refer to the Mach-O loader header source code here, specifically the MH_ prefixed flags, or the epic description here). The macho package we are using in objection simply parses that flags’ field which means we can ask it if exe.flags.pie is set. In other words, is 0x200000 set in the target binary? Pretty neat right!

arc

Unlike PIE, the check if Automatic Reference Counting (ARC) is set is not based on a flag in the header field. Instead, this check is something we infer based on imports that a binary has. There is a lot of information about ARC you can find in the LLVM documentation here, but basically its a memory safety mechanism that keeps tabs on objects and free’s them when no-one is using them anymore. This is not something that happens at load time like PIE, but instead happens at runtime. To detect if ARC is being used, we check if the function objc_release is imported by the target executable. We simply infer that ARC is used based on this; it does not prove that it does. This check could easily be fooled by anything that imports and does not actually use it, so keep that in mind. With the macho package we simply call imports.has("objc_release") for the check.

canary

Checking if stack canaries are in use is done in a similar fashion to ARC. When enabled, a stack canary is a random bit of data placed and checked before a function returns, aimed at making exploiting overflows harder. The use of the function __stack_chk_fail (and its derivatives) implies that should a stack canary be smashed, this function would be called as a safety bailout to prevent an exploitation attempt from returning to the wrong address (or similar). Just like ARC, if stack canaries weren’t enabled, but this function was imported for other reasons, the check could be fooled. To recap, it simply infers that stack canaries are enabled based on the fact that a function commonly used is imported, and not that its actually used.

static analysis

Using objection to check these protections is not the only way. Many scripts, plugins and other tools exist that do these checks. For example: https://github.com/slimm609/checksec.sh. Radare2 also has this capability, such as when using rabin2 or by using the ia command in the r2 disassembler.

rabin2 used to enumerate macho flags and imports
checking if stack canaries are enabled using r2

the naunces

Now that you know how these binary protection mechanisms are enumerated, let’s talk about when you may have trouble interpreting the results like I did. In the iOS world today you are going to find applications written in Objective-C, Swift or both, and depending on the language used, different protections apply. Even “write once deploy anywhere” frameworks such as Cordova have native components. None of these protections are applicable to the extra layers that frameworks like Cordova (read: JavaScript) add on top of the native layer, so you can just ignore those. Certain protections are also only applicable to the main executable and none of the frameworks.

Knowing which files need protections enabled is also important. Let’s take a quick look at a typical iOS application. The .ipa file can be unzipped to find a Payload/ directory, and in there a folder named usually ending in .app. Inside of this directory you’d typically find the main application executable (DVIA-v2 in the example below) and a Frameworks/ directory. The main executable as well as executables found in the Frameworks directory are all in scope for protections.

executables highlighted with red arrows

There may also be arbitrary .dylib files lying around (not necessarily in the Frameworks directory), so be sure to check them out too.

identifying objc vs swift

Some protections will only be applicable depending on the language the main executable or Framework is written in. In general, all of the protections should be enabled for Objective-C, but some are not (and seems like you can’t enable them anyways) for Swift. Knowing how to identify a pure Objective-C or Swift library is also important. In general you can spot this by looking at the executable’s symbol table. A pure Objective-C executable / library will have no swift references.

no swift imports in the Realm executable
swift references in the RealmSwift executable.

Take special note of the _swift_FORCE_LOAD_ prefix. This is a pretty clear indicator that Swift is at the very least in use. An experienced eye will also be able to spot Swift mangled methods in the symbol table which won’t exist in a pure Objective-C binary.

The only case that is hard to detect is a pure Swift binary. Even if written in pure Swift, theres always some references to Objective-C around. You can almost always see this when inspecting the linked libraries.

libobjc linked in a Swift library built with Xcode

Even when compiling a pure Swift file on macOS, libobjc shows it’s head!

simple hello world swift program linking libobjc

Out of interest I compiled the same program on Linux using the Swift tools for Linux, and there was no libobjc ;)

swift program compiled on linux not linked to libjobc

Anyways, my point is that it’s really hard to be certain that an application is written in Pure swift, and you should be careful when considering how binary protections are enumerated for them. Let’s take a look at some of the exceptions to these protections.

pie – exceptions

PIE is only applicable to executables (Mach-O type MH_EXECUTE) and not libraries. A reference to this can be seen in a comment in the Mach-O loader source header here (formatted for readability).

define    MH_PIE 0x200000
/* When this bit is set, the OS will load the main executable at a random address. Only used in MH_EXECUTE filetypes. */

So, if the binary type is library, PIE being false is ok.

arc – exceptions

There are no exceptions to ARC. Both pure Objective-C, Swift and hybrid binaries should have this enabled. Note that objection versions < 1.10.0 incorrectly parsed the check for ARC, but that has since been fixed in version 1.10.1. For old Objective-C projects this should be enabled. For Swift projects it should automatically be enabled.

canary – exceptions

Stack canaries are an interesting one. For pure Objective-C binaries, this should always be enabled. Enabling it is done by passing the -fstack-protector-all flag to the C compiler. For pure Swift projects I could not find how to enable this. In fact, I reduced testing to a single, small hello world example to see if I could get it enabled but with no success.

stack canaries not enabled for swift binaries

I found this hard to believe, and thought maybe it would be different if I compiled it on Linux, but alas the same result. Some digging into this turned into me realising that “it’s complicated”. See: https://developer.apple.com/forums/thread/106300. The TL;DR is that it is in fact enabled, but conventional parsing is not enough to test that without recompiling the source. Given that Swiftlang is designed to be memory safe making memory corruption bugs much harder has me feeling comfortable that if a library is in fact pure Swift, and stack canaries weren’t enabled, the risk will be minimal.

In summary, your decision making on which protections can and should be enabled is heavily influenced based on if Swiftlang is involved and whether the target binary is an executable or a library.

enabling protections summary

  • PIE – Add the -fPIC compiler flag to the projects build settings. This will only be applicable to the main executable.
  • ARC – This will be automatically enabled for Swift only projects (via the swiftc compiler), and added by setting YES to the Objective-C Automatic Reference Counting section in the projects configuration.
  • Canary – Enabled by adding the -fstack-protector-all compiler flag to Objective-C projects. If Swift is involved its possible to have it enabled when the library is a hybrid of Objective-C and Swift, but it could show as disabled which is okay.

Special care should be taken to ensure that these configuration changes are applied to all frameworks in the project as well.