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.
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.
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.
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.
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.
Even when compiling a pure Swift file on macOS, libobjc
shows it’s head!
Out of interest I compiled the same program on Linux using the Swift tools for Linux, and there was no libobjc ;)
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.
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 settingYES
to theObjective-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.