Finding Vulnerabilities Inside Third-Party Dependencies in iOS

An app is built on top of many things. While a developer can control the most of the code that goes into the app, there are many third party dependencies that are used to build the app. These dependencies can be libraries, frameworks, or even entire SDKs. They are often used to save time and effort, but they can also introduce security vulnerabilities if not properly vetted. For a developer, third party dependencies are a no-touch zone and many of the penetration testers take them for granted. However, they can be a source of vulnerabilities that can be exploited by attackers.
Jul 8 / Vikrant Chauhan

Third Party Dependencies

Prerequisites

There are different types of dependencies that can be used in an iOS app. They can be broadly categorized into the following categories:

  1. Embedded Frameworks: The code is stored within the /Frameworks/ inside .app directory. These dependencies are usually installed via CocoaPods, Carthage, or Swift Package Manager.
  2. Static Libraries: Compiled directly into the app binary, not visible as separate files.
  3. System Frameworks: Dynamically linked at runtime from iOS. These are not bundled inside the app but rather provided by the iOS itself.
  4. Swift Standard Libraries: If targeting iOS versions < 12.2, Swift stdlibs may appear in /Frameworks/ within the .app directory.
  5. Third-Party Dynamic Libraries: Can be embedded elsewhere if the app developer manually sets a different directory, though /Frameworks/ is conventionally enforced by Xcode.
  6. Resource Bundles: These are placed in .app directory or .app/PlugIns/ directories and usually contain resources, but not code.

In this article, we are specifically interested in the embedded frameworks. We will go through the process by using a real world example, Discord. Let's get started!

Prerequisites

Getting the IPA

You can download the IPA using ipatool:

Finding used dependencies

To find the vulnerabilities, we need to first extract the ipa. An IPA is nothing but a zip file. You can extract it by any tool that allows you to extract zip files:

Now, we have a Payload directory containing the Discord.app directory. If you are using the macOS's finder app, it seems like a file but in reality, it is a directory that you can cd inside:
Discord is a complex app in itself, so there are many files and directories inside the Discord.app directory:

Finding Dependencies with otool

Simplest way to list dependencies is to use otool -L. This command is used to display information about Mach-O files, which are the executable files used in iOS apps.

Finding the dependencies with _CodeSignature/CodeResources

Aside from otool we can also utilize _CodeSignature/CodeResources file. This file is always a part of an iOS app and contains a list of non-code resources (such as images, storyboards, and other assets) and their hashes, which are used by iOS to verify the integrity of these resources at runtime. It does not track the app's code or binaries. So we only get a hint of the libraries used. As you open this file, you will find that it is a plist file in XML format.
A lot of it is just the files that exist inside the app, so it gets easier to find the dependencies by searching using ipsw plist or any other tool that can parse plist files:
To reduce the output, you can use grep to filter out the dependencies. We are looking for keys having .plist, .bundle, or .framework etc. But simplest way is to just look for / as dependencies usually reside in subdirectories. We are also ignoring the assets directory as it contains images and other resources, nothing of interest to us:
This will give us a list of dependencies used by the app.
Another way to find the dependencies is to look for Info.plist files. Simply search for Info.plist files:
To find version, we can directly extract it from the Info.plist files. Most of the time, the version information is stored in the CFBundleShortVersionString key. You can use jq to extract this information:
All the methods we have seen so far, struggle with the version for some dependencies as they might have a dummy version number or no version number at all. If the version is 1 or 1.0, then it is hard to say what the actual version is. In that case, we can look for presence of specific function calls in the binary. Its a long process so we will not cover it in this article.

Finding Vulnerabilities

Now that we have a list, we can start looking for vulnerabilities in these dependencies. 

Searching for known vulnerabilities

An easy start is to search for a known CVE. As you search these frameworks and libraries on the internet, you will find that some of them are open source while others are not. For the open sourced libraries, you can directly look at the source code and find vulnerabilities. Sometimes you have a CVE number, but no publicly available exploit. In that case, you can look for changelogs or release notes of these libraries/frameworks that can reveal some vulnerabilities that were not disclosed publicly. This process is called Patch Diffing.
Ofcourse there is no known vulnerability in this app or its dependencies at the time of writing this article, so we will not be able to find any vulnerabilities in this app. To give you a rough idea of patch diffing, let's try to take an example on hermes framework.

First, we will search for the hermes framework on https://cocoapods.org/:
There are many packages with the same name, so be careful to select the right one. In this case, hermes-engine is our target. As we open it, it leads to https://github.com/facebook/hermes/blob/main/README.md. It tells us that it is a JavaScript engine optimized for running React Native. So if we find a vulnerability in this library, we might be able to exploit it in all of the React Native apps including Discord.Next step is to look for vulnerabilities in this library. We can search for the CVE number on https://cve.mitre.org/ or https://nvd.nist.gov for CVEs. A little searching on the internet reveals that there is CVE-2022-32234 below the commit 06eaec767e376bfdb883d912cb15e987ddf2bda1. This gives us significant details on where the vulnerability might be. Let's check that commit on GitHub: https://github.com/facebook/hermes/commit/06eaec767e376bfdb883d912cb15e987ddf2bda1
The commit updates three files namely, external/llvh/include/llvh/ADT/SmallVector.h, external/llvh/lib/Support/SmallVector.cpp and external/llvh/patches/SmallVector.patch. Looking at the changed code, we can see that the vulnerability is in the llvh/ADT/SmallVector.h file. Right above, we notice that the commit is part of the tag v0.13.0, and the discord uses v0.12.0 of hermes framework. This means that the vulnerability might be present in the version used by Discord. But to confirm this, first click the three dots next to the v0.13.0 tag, it will give you a list of all tags:
Unfortunately, this same commit is part of the v0.12.0 tag as well. So our discord app is not vulnerable to this CVE. However, since we are at it already, let's continue with this CVE as an example.

The CVE-2022-32234 is a out of bounds write vulnerability. From the diff, we can see, it is trying to add more conditions to ensure report_bad_alloc_error("SmallVector capacity overflow during allocation"); is called when there is an overflow. It checks for NewCapacity <= this->capacity() or NewCapacity < MinSize or NewCapacity > size_t(-1) / sizeof(T). However, previously it only checked for MinSize > UINT32_MAX. This means that the MinSize must stay below UINT32_MAX to ensure it works. Similarly as we track the inputs  and variables present in the scope where MinSize is checked for condition, we will be able to find the exact vulnerability. For this we need to dive deep into the code and do a dynamic analysis which is a vast topic that cannot be covered in one article. But we can take a quick look.

Finding New Vulnerabilities

Be it new vulnerabilities or building an existing exploit, the process usually requires a combination of static and dynamic analysis. Static analysis involves looking at the source code, while dynamic analysis involves running the code and observing its behavior.

Let's start with the static analysis. We already have the source code of the hermes framework, so we can look at the code and try to find the vulnerable function. For this, first clone the repository locally:
If you are continuing with the example of CVE-2022-32234, then you should checkout the commit before 06eaec767e376bfdb883d912cb15e987ddf2bda1:
This will checkout the commit before the one that fixed the vulnerability.Static analysis does not require a lot of special tools, most of the time you can just use a code editor like VSCode or Sublime Text and utilize its search functionality to find a function that might be vulnerable. Usually we look for things that allows us to do inherently unsafe operations like memcpy, memmove, strcpy, strcat, sprintf etc as well as functions that are used to perform apparently safe operations but can be used in an unsafe way, such as file I/O functions like fopen, fread, fwrite, etc.

For example, if we search for memcpy in the hermes codebase, we can find it in hermes/android/intltest/java/com/facebook/hermes/test/HermesEpilogue.cpp:35
Once we have an unsafe function call, a way to control its arguments, and most importantly an entrypoint to reach the function call, we will have a full vulnerability. Then we check the arguments passed to the memcpy function. In this case, resPtr.get() is a pointer to the destination buffer, epilogue is a pointer to the source buffer, and epilogueSize is the size of the source buffer. If epilogueSize is larger than the size of the destination buffer, it can lead to a buffer overflow vulnerability.

Next step is to control the arguments passed to the memcpy function. For this, we continously try to follow the code flow backwards from the point where the memcpy function is called. As we go up in the call stack, we can find the function that is responsible for setting the arguments. Ofcourse this is a tedious process and requires a lot of understanding of the codebase and programming in C++.

Since this is a framework, we start by setting up a small project that utilizes the hermes framework. We wanna stick to the bare code as much as possible, so we try to utilize the hermes framework directly without any other dependencies. We can also inject our own code to exit right after the memcpy function is called having debug prints to see the arguments passed to the function. This way we can control the arguments and see if we can trigger the vulnerability.

To ease the process, you can also hook up a debugger like lldb or gdb to set breakpoints and inspect the memory. This can allow us to see the contents of the destination buffer and the source buffer, as well as the size of the source buffer. If we can control the size of the source buffer and make it larger than the destination buffer, we can trigger a buffer overflow vulnerability.

Final Step

Once you have a vulnerability in a third party dependency, final step is to look into how you can reach the main vulnerability through an entry point to complete the exploit chain. This process also requires a look into different parts of the app, how they work together and how developer of the app utilizes third party dependencies. This is ofcourse a complex process and requires a deeper understanding of how iOS apps are tested. You can learn this from our free course iOS Application Security.  

The key takeaway is to understand the code flow and how the arguments are passed to the unsafe function calls. We will save uncovered topics for the future. Have fun hacking!