enquiries@dvt.co.uk |  +44 203 848 9470
Insights

Trends and insights making an impact in your digital transformation journey

String Encryption Implementation for iOS and macOS
Kurt Jacobs

String Encryption Implementation for iOS and macOS

This article aims to document a solution for String Encryption for iOS and macOS binaries. There are a few issues that arise and these are covered and mitigated as required. Two types of binary analysis are normally employed by hackers and reverse engineers, namely, static and dynamic analysis. String encryption falls under static analysis protection because it makes it challenging for attackers to inspect the application's sensitive string content such as backend services keys at rest.


Although the encryption scheme is arbitrary it would be beneficial to use a block cipher such as RC6, 3DES, AES, etc. This type of cipher is employed because it provides a one-to-one mapping with respect to byte buffer size whilst simultaneously satisfying the encryption requirement. This has an additional benefit in that it allows the application binary to maintain its current size after encrypting the strings at rest, it is also easier to map in and out of memory without the need to reallocate memory buffers of differing sizes.


This article won't delve into too much detail regarding the Mach-O binary file format — It is the native file format that contains, amongst other things, the application data and code to be executed on any Apple device (iPhone, iPad, MacBook, Apple Watch, etc.).



Figure 1 - The Mach-O Binary Sections


The important knowledge that is required for understanding the string encryption implementation in relation to the Mach-O binary is described below.


As indicated in Figure 1 the Mach-O binary is comprised of five distinct sections, namely, Header, Load Commands, __TEXT, __DATA, and __LINKEDIT sections. The section that is interesting for a first draft of the string encryption is the __TEXT section. Each section has a variable number of segments, the segment that contains the strings to be encrypted is found inside of the __TEXT section and is referred to as the __cstring segment. The __cstring segment contains all of the strings that are included in the application source code at the C, Objective-C and Swift level except for CFStrings which are stored inside of the __DATA section. It should be noted that the __cstring segment includes any strings inside of third party frameworks, static libraries or CocoaPods. This structure is visualised in Figure 2.



Figure 2 - The Mach-O Binary __cstring Segment


This concludes the overview and the description of each of the components and their purpose in facilitating String Encryption. What follows is two algorithms for String Encryption. The algorithms are comprised of two distinct phases, build and runtime. A source code implementation is provided for the second algorithm, as it is, in my opinion, superior, but the first is discussed and described.


Algorithm Overview, Mach-O String Encryption Variant #1

Build Time Overview
  • Generate Mach-O segment to store the encryption key
  • Encrypt Mach-O _cstring segment after the Mach-O binary is generated (using a simple post-build ruby script)


Figure 3 - The Mach-O Binary with new Segments and encrypted __cstring segment


Runtime Overview
  • Obtain the __cstring segment memory block
  • Obtain the __MYSEG encryption key segment memory block
  • Update the memory blocks protection level (read/write)
  • Encrypt the memory block using a block cipher encryption algorithm (e.g. AES)
  • Update the memory blocks protection level (read-only, default)


Figure 4 - The Mach-O Binary runtime decryption process


This solution works perfectly for simple projects but it does have a few caveats. Among others, we don't have real control over what's embedded in the __cstring segment. This has implications when other 3rd party libraries are trying to access this segment before it is decrypted at runtime. This is precisely the case when including 3rd party CocoaPods such as Firebase. Firebase is expecting certain strings to be available before the main application executes. One could try to ensure that the string decryption algorithm executes before the Firebase framework requires the strings but as one can imagine this could quickly become a tedious process especially if a number of frameworks or libraries are trying to access data from the __cstring segment. It also appears that when this segment is encrypted (leaving only the swift class names) Apple parses the binary and rejects it when it is submitted for AppStore review — though this is a problem for commercial applications this solution is still applicable in an enterprise application context so it is not completely useless.


What if string encryption is required for commercial applications? As noted above it is possible to create new Mach-O binary segments that can be queried at runtime. This is exactly what the second — and more flexible — algorithm does. This also has a cost that is incurred with respect to development time because a string loader will need to be implemented for both the c and Swift layer. The second algorithm is described below:


Algorithm Overview, Mach-O String Encryption Variant #2

Build Time Overview
  • Generate Mach-O segment to store the encryption key
  • Generate Mach-O segment to store the c layer strings
  • Generate Mach-O segment to store the Swift layer strings
  • Inject the encryption key into the keys segment
  • Inject the encrypted strings into the c layer segment
  • Inject the encrypted strings into the Swift segment


Figure 5 - The Mach-O __TEXT Section Layout With Custom Segments


Runtime Overview
  • Obtain the encryption key from the keys segment memory block
  • Obtain the c layer strings segment memory block
  • Obtain the Swift layer strings segment memory block
  • Update each of the memory blocks protection levels (read/write)
  • Decrypt each of the memory blocks using a block cipher encryption algorithm (in the sample project provided its AES) using the key provided in the key section
  • Update each of the memory blocks protection level (read-only, default)
  • Load c strings into the c_string_store hash table class in c
  • Load swift strings into the SecureStringStore class in Swift
Build Time Implementation

The first step would be to generate the raw strings JSON files, swift.json and c.txt. These JSON files need to follow a simple format and we will avoid any complex structures for the time being. A sample is shown below:



[ JSON]
{
	"some-application-secret":"my_super_secret_code",
	"album_leak_download_url":"https://leaks.com/unreleased/super_secret_album.zip",
	...
	...
	...
}
[ JSON]

These plaintext strings files need to be encrypted before they can be included inside of the binary. A simple key_obfuscator.rb is provided that transforms the keys for the Swift and C strings file into obfuscated strings as well as generating the StringsTable.swift and CStringTable.h. The cryptonology (this is an inside joke on the team I work on) tool accepts both the plaintext c.txt and swift.json files as input and produces the encrypted strings files as well as the key.txt file.


All the prerequisites are now complete for integrating the encrypted strings into the binary as custom Mach-O segments as depicted in Figure 5. A segment can be created inside of a section by adding an 'Other Linker Flags' command inside of the build settings of the main executable binary in Xcode. The sections are created inside of the __TEXT section to make them appear a bit more inconspicuous and they also get the standard __TEXT section protections applied to them at load time as an added bonus. The sections are created by adding the following to the 'Other Linker Flags' build setting.



[ CODE ]
-Wl,-sectcreate,__TEXT,__MS_SAUCE,StringsDemo/encrypted-strings/key.txt
-Wl,-sectcreate,__TEXT,__MS_SWIFT,StringsDemo/encrypted-strings/swift.json
-Wl,-sectcreate,__TEXT,__MS_C,StringsDemo/encrypted-strings/c.txt
[ CODE ]


It should be obvious what these commands do. They embed the contents of the encrypted files and key file generated by the cryptonology tool inside of the Mach-O binary.


The key_obfuscator.rb described above generates the StringTable.swift and CStringTable.h files. The StringTable.swift file contains a helpful enumeration with the obfuscated keys. The enumeration is used to provide a pleasant development experience to access the strings via the SecureStringStore.swift class but has the added benefit that none of these labels are leaked. When constructing the final application binary the enumeration case labels are not stored so we obtain pure obfuscated keys as a bonus. The CSecureStringStore.c is a simple hash table implementation that stores the strings used inside of the c layer. A simple Macro mapping header is generated, this is the CStringTable.h file. The strings are accessed using these macros. This provides end to end obfuscated secure string access at both the Swift and c layer. See Figure 6. for a sample of the obfuscated keys and the generated StringsTable.swift.



Figure 6 - [ The StringsTable.swift file - A sample]


Runtime Implementation

Three important concepts are very briefly explained in this section that are prerequisites to follow along with the runtime string loading implementation.


  • Apple's DYLD (Dynamic Loader / Linker)
  • Mach Memory APIs
  • ASLR
Apple's DYLD

The dynamic linker is the second phase of the linking process that is invoked at runtime when loading a Mach-O application binary. It resolves load-time and run-time dependencies, and pieces together the necessary components, laying them out in memory and binding them together. DYLD also provides an API to inspect and trace its state.


Mach Memory API

The Mach Memory API provides an interface into the kernel memory to access memory regions via an abstraction layer. It is constructed atop a POSIX compliant UNIX memory API.


ASLR

ASLR (Address Space Layout Randomization) is a form of data security used to randomize data in memory to help prevent exploits from taking control of the application. It’s a slide offset value which ensures that each memory address loaded by the dynamic linker is shifted by the slide’s size. This dynamic slide value makes it challenging to carry out automated exploits.


Implementation

The c layer consists of a number of components. A lightweight c json library called jsmn (pronounced Jasmine) jsmn.h, a lightweight AES library (tinyAES), strings_core.h & strings_core.c, and c_string_store.h & c_string_store.c files.


The Swift layer contains an implementation of the SecureStringStore.swift which loads the sensitive decrypted strings via the c layer.


strings_core.h


The strings_core.h exposes a public method that loads the unencrypted strings, this method is invoked from a Swift SecureStringStore instance.


strings_core.c


The strings_core.c file can be broken down into a set of distinct algorithmic steps or phases. These are outlined as follows:


  • Load DYLD Tracing & Inspection Symbols
  • Obtain ASLR Slide and Mach Header Info
  • Mechanism to obtain arbitrary sections and segments from the Mach-O binary
  • Mechanism to query Mach Memory Protection Level
  • Mechanism to alter Mach Memory Protection Level
  • Mechanism to parse JSON at the c level
  • Mechanism to decrypt memory at the c level
  • A constructor attribute function to ensure that all of the above execute before the main application executes. This function decrypts the strings in memory once they are loaded by the dynamic linker. These unencrypted strings are then accessible via the c_string_store instance in c and SecureStringStore in Swift.

c_string_store.h


The c_string_store.h is a simple hash table implementation in c that exposes two methods; set and request. The former sets a value for a specific key and the latter returns a value for the key. The keys are generated as a Macro map to ensure that the keys are obfuscated at link time.


c_string_store.c


The CStringStore is a trivial implementation of a hash map that allows you to store up to 2048 strings. It would be trivial to extend this to reallocate the hash table when more string memory is required but this is left for future work.


SecureStringStore.swift


This class is a wrapper of a swift dictionary data structure that provides a mechanism to access the strings via the enumeration values of the generated SwiftStringTable.swift file and the ```string(for key: StringTableValue)``` method. The secure strings are loaded via the ```void fetch_string_buffer_information(uint8_t **address, uint8_t **size)``` method exposed in the strings_core.h class.


Auxiliary

C Symbol obfuscation is provided in the sample application but is beyond the scope of the article so it will not be discussed.


Future Work & Recommendations

  • Localisation Support can also be added. Accessibility is becoming increasingly important in the current global landscape.
  • Source code obfuscation would also be required to ensure that it would be difficult to figure out where the Swift classes that load the strings are located.
  • The strings could easily be dumped at runtime because this implementation does not focus on any dynamic analysis mitigations such as memory region protections and anti-tampering.
Tagged under