iOS App Security: Protecting SDK Symbols & Preventing Information Leakage (Part 2 of 2)
The following blog post explains the potential risk associated with symbols, as well as demonstrate existing mitigations available when creating SDKs and iOS apps. It is Part 2 of 2, and you can read the first blog post here.
Symbols can leak valuable semantic application information to reverse engineers. This includes giving them insight on the inner workings of apps, such as what specific libraries are meant to do and how information is transferred between the main app and the libraries.
This also means that these symbols will point out interesting candidates for function hooking. For this reason, it is important to be mindful of the symbols exposed by SDKs, even for third-party dependencies. In order to better protect your apps against these risks, we recommend hiding and obfuscating these symbols.
The sections below will focus on the importance of limiting symbol exposure in an API and the different ways to achieve this. It will also show how iXGuard simplifies this process and provides unique protection features that:
-
help developers deal with third party dependencies that they have no control over,
-
hide symbols that are not part of the API, as well as obfuscate symbols that must remain visible.
Default SDK Symbol Exposure
In general, symbols that are not part of the public API of your library should not be exposed for the following reasons:
-
Security. Exposing more symbols gives reverse engineers a lot more insight into the inner workings of your library. It provides them with a broader attack vector and a lot of interesting targets for placing hooks.
-
Usability. With fewer exposed symbols, the library becomes easier for developers to understand and use.
-
Bugs. The dynamic linker does not detect naming conflicts between symbols exported by the different dynamic libraries it loads. It simply takes the first symbol it finds, potentially causing several problems.
-
Performance. A bigger symbol table means that the linker will have to load and parse more symbols. This leads to longer link times and more memory usage, which can be noticeable for big applications.
By default, when compiling a C library, the compiler and linker will keep the symbols of all the functions in the symbol table visible.
Objective-C methods are not exposed as public symbols because of the way method invocation is implemented in the Objective-C runtime. However, there will be symbols left that are not strictly needed.
Swift methods are usually called like regular C functions, so the compiler and linker will again generate symbols for these functions. However, unlike the C language, Swift has access control syntax to define the access level for your identifiers. Unless otherwise specified, the default access level is internal.
An Exposure Example
Using a very simple code snippet, we can demonstrate the default behaviour and how some options available in Xcode affect this. The dummy library below provides a C and an Objective-C API:
Header file: Library.h
@interface APIClass : NSObject
- (void) apiMethod;
@end
void CApiMethod(void);
Source file: Library.m
int CDecryptionFunction(void) {
return 1;
}
void printDecryptedData(void) {
if (CDecryptionFunction() == 1) {
printf("Hello world\n");
}
}
void CApiMethod(void) {
printDecryptedData();
}
@interface SecretDecryptionClass : NSObject
- (void) printDecryptedData;
@end
@implementation SecretDecryptionClass
- (void) printDecryptedData {
if (CDecryptionFunction() == 1) {
NSLog(@"Hello world");
}
}
@end
@implementation APIClass
- (void) apiMethod {
SecretDecryptionClass* decrypt = [[SecretDecryptionClass alloc] init];
[decrypt printDecryptedData];
}
@end
After compiling this sample library in Xcode with Strip Style set to "Non-Global symbols," the nm output on the resulting binary will show the following symbols:
U _NSLog
0000000000008260 S _OBJC_CLASS_$_APIClass
U _OBJC_CLASS_$_NSObject
0000000000008210 S _OBJC_CLASS_$_SecretDecryptionClass
0000000000008238 S _OBJC_METACLASS_$_APIClass
U _OBJC_METACLASS_$_NSObject
00000000000081e8 S _OBJC_METACLASS_$_SecretDecryptionClass
U ___CFConstantStringClassReference
U __objc_empty_cache
0000000000007e30 T _CApiMethod
0000000000007e1c T _CDecryptionFunction
0000000000007fa0 S _libraryVersionNumber
0000000000007f78 S _libraryVersionString
U _objc_msgSend
U _objc_release
0000000000007e24 T _printDecryptedData
U _puts
U dyld_stub_binder
The resulting binary still seems to include the symbol for the Objective-C class SecretDecryptionClass that is solely used for internal purposes. Since these OBJC_CLASS symbols are only required for linking purposes, and not the Objective-C runtime, we can safely remove them.
As for the symbols for the internal C functions CDecryptionFunction and printDecryptedData, there is no reason for these to be visible to users outside of the library.
Reducing SDK Symbol Exposure
In this section we’ll introduce several techniques to limit the amount of exposed symbols and illustrate them on the previous sample program. We’ll first explain how to make use of existing Xcode options and afterwards show how iXGuard can still improve on these.
In short there are 3 options:
- Use the -fvisibility=hidden compiler flag to set all symbols to hidden, and using visibility attributes to mark the ones that should be retained
- Add the linker flag exported_symbols_list/unexported_symbols_list where you provide a list of symbols to (not) export
- Use the iXGuard symbol obfuscation and hiding feature.
Static Libraries
The listed techniques can easily be applied to dynamic libraries, whereas static libraries make it impossible for symbols to be hidden immediately. A static library is an archive of unlinked object files. This means that a lot of internal symbols are unresolved. Consequently, they must first be linked together in order to solely expose the symbols required for public usage.
This can be done by creating a single, relocatable object file. In Xcode, this is a build option called "Perform Single-Object Prelink," which is equivalent to running the linker with the -r flag. Once this is done, everything can also be applied on static libraries.
Default Hidden Symbol Visibility
Using some compiler flags, it’s possible to flip the default symbol visibility. Of course, afterwards developers must mark the symbols which are required to be public. This can be done by annotating public C functions with a visibility attribute:
__attribute__((visibility("default")))
void CApiMethod(void) {
printDecryptedData();
}
Objective-C interfaces can be similarly marked:
__attribute__((visibility("default")))
@interface APIClass : NSObject
- (void) apiMethod;
@end
In order to flip the default, add the -fvisibility=hidden flag to the compiler invocation.
In Xcode this can be set by turning "Symbols Hidden by Default" to yes.
Using nm again, verify only the intended symbols remain in the final binary and there is no mention of the SecretDecryptionClass:
U _NSLog
0000000000001260 S _OBJC_CLASS_$_APIClass
U _OBJC_CLASS_$_NSObject
0000000000001238 S _OBJC_METACLASS_$_APIClass
U _OBJC_METACLASS_$_NSObject
U ___CFConstantStringClassReference
U __objc_empty_cache
0000000000000e84 T _apiMethodInC
U _objc_msgSend
U _objc_release
U _printf
U dyld_stub_binder
Finally, verify the library still functions correctly by writing a small main application that links against this version of the library:
#import "Headers/library.h"
int main(int argc, const char* argv[]) {
apiMethodInC();
APIClass* api = [[APIClass alloc] init];
[api apiMethod];
}
After compilation:
xcrun clang main.m -o main.o ./library.framework/library
-Xlinker -rpath -Xlinker @executable_path
-framework Foundation
confirm it still prints "hello world" twice (once with printf and once with NSLog):
Hello world
2019-04-30 09:47:38.272 main.o[2916:43468] Hello world
Note that the library did not require a symbol for the secret decryption class (or for the C function, for that matter). Of course, internally, the class and functions are still used. But the Objective-C runtime does not require the symbols; it uses the metadata instead.
Exported Symbols List
When using the exported_symbols_list flag, one should create a file that lists the symbols to keep (note that the "_" prefix is required due to the C-style mangling):
_CApiMethod
_OBJC_CLASS_$_APIClass
_OBJC_METACLASS_$_APIClass
Afterwards, add -exported_symbols_list /path/to/list to the linker invocation.
In Xcode this setting is under "Exported Symbols File," where you can list the path to the file.
Using the nm tool, we can see that only the non-listed symbols remain.
U _NSLog
0000000000008260 S _OBJC_CLASS_$_APIClass
U _OBJC_CLASS_$_NSObject
0000000000008238 S _OBJC_METACLASS_$_APIClass
U _OBJC_METACLASS_$_NSObject
U ___CFConstantStringClassReference
U __objc_empty_cache
0000000000007e28 T _CApiMethod
U _objc_msgSend
U _objc_release
U _printf
U dyld_stub_binder
We can verify this application still works as intended by linking against the same main application as before. Note that the reverse option is also available through -unexported_symbols_list.
Obfuscating Symbols with iXGuard
iXGuard's symbol obfuscation is part of its name obfuscation feature. When enabled, this feature will obfuscate both metadata and symbols. Using iXGuard symbol obfuscation and hiding has several advantages for both IPAs and SDKs.
For IPAs
Symbol Hiding
This technique conceals all unused symbols. Because iXGuard has a complete overview of the app and its dependencies, it is also able to determine which symbols should stay visible. This means that you will not need to manually deal with exposing the correct API of any of your (internal) frameworks. This feature is especially relevant when using third-party libraries, since often not all symbols that are exposed by the API of such libraries are used.
The figure above illustrates this case. The dylib.bin exposes several symbols.
-
There is one internal symbol, namely internal_function, that ideally would not be exposed. Because, as the name suggests, it is a function used for internal purposes in the library. The library user should not use it, and doesn't have to know it exists.
-
There are also two symbols that are part of the API: api_function1 and api_function2, but only api_function2 is used by the main application.
In a regular build process, the compiler and linker can not determine which symbols are unused because they can be dynamically resolved. iXGuard will detect that the api_function1 and internal_function symbols are not used, and automatically hide them.
Symbol Obfuscation
Generally, it is not possible to hide all symbols because the linker must be able to connect calls to external libraries. That is why symbols are vulnerable to leaking valuable app information. Symbol obfuscation solves this issue by renaming the symbols that must remain visible. Thanks to iXGuard’s comprehensive overview, it can rename symbols consistently between the caller and callee.
In the figure above, iXGuard would be able to consistently and randomly rename the symbol for api_function2 on both ends —the dylib and the main application— so that the linker can still resolve symbols, while the semantic meaning is lost. As explained in our previous blog, this semantic meaning is only intended for the developer, as it is irrelevant to the compiler, linker, OS and Objective-C runtime.
SDKs
Symbol hiding will —by default— hide all symbols as it is impossible to know which symbols are part of the API. Therefore, a blacklist of symbols must still be provided — just as in any other method or tool you could use,since it's otherwise impossible for any tools to automatically determine which symbols will be required in the future.
Symbol obfuscation for dynamic libraries behaves the same way as for an IPA because, in a dylib, we are also dealing with a Mach-O binary that can load other dylib dependencies. For static libraries, on the other hand, only local symbols will be obfuscated. These symbols cannot be used by the library user later on and are, therefore, safe to obfuscate.
Understanding Metadata and Symbols to Better Safeguard iOS Apps
Thank you for reading along during our two-part series on understanding Objective-C metadata and symbols. Hopefully after reading these two posts, it’s clear that there is significant action required on the level of metadata and symbol obfuscation to ensure that iOS apps are properly secured at runtime. Additionally, we hope it is clear how iXGuard can offer apps a necessary level of protection against reverse engineering and other types of code-based attacks.