Why this post?
I want to write more for my blog, but often, I can’t come up with ideas. I sometimes think that the topics I will write about are not interesting. Sometimes I feel like they’re too basic. Recently, I realized that this is called the Curse of Knowledge. Here is the definition from Wikipedia
The curse of knowledge is a cognitive bias that occurs when an individual, who is communicating with others, assumes that others have information that is only available to themselves, assuming they all share a background and understanding. This bias is also called by some authors the curse of expertise.
I decided to write a post about how I generally reverse engineer applications. I will try to explain each step in detail. Some of them may seem obvious, but as I said, I will not assume who knows how much.
Pick a target
As an old teacher, I believe in learning by practicing. Therefore, I will pick a target so that we can all follow along to understand my methodology. Onfido helps companies verify people’s identities using their ID card, passport, etc. I will use Onfido iOS SDK and try to understand what it is doing.
How I reverse applications?
Whatever the application is, I always follow the following pattern:
- Identify the application
- Find the attack point
- Disassemble
- Profit
Identify the application.
When we get this SDK, we will see that its source code is not supplied but instead it is distributed as a binary framework. How did I find out?
This framework can be integrated into iOS projects by either SPM or CocoaPods. When we open the Package.swift file, we can see the binary URL as https://s3-eu-west-1.amazonaws.com/onfido-sdks/ios/Onfido-v30.1.0.zip
. This URL also exists in the CocoaPods specs repo https://github.com/CocoaPods/Specs/blob/master/Specs/c/9/3/Onfido/30.1.0/Onfido.podspec.json
.
Let’s download this zip file and analyze it.
├── _CodeSignature
├── ios-arm64
│ ├── Onfido.framework
│ └── dSYMs
└── ios-arm64_x86_64-simulator
└── Onfido.framework
Let’s see if we can find any interesting files inside the framework.
We can find a bunch of JSON files inside the .framework
folder. Inside the default-scoped-config.json
we find the following URL:
https://d19xpeczeo7tzg.cloudfront.net/documentDetector.mlmodel
If you download the above file, you can easily see that it is a CoreML model. It is probably used to find the edges of a document.
We can check the contents of Assets.car
via AssetCatalogTinkerer. Sometimes you may find interesting assets inside this file.
Find the attack point
I want to use this SDK in my project. Luckily, it has a Swift example application inside the SampleApp
folder. Let’s navigate to that directory. We can see that it uses a Podfile to manage its dependency. We can install the dependency via pod install
and try to run the application.
It crashes with the following error
Thread 1: Fatal error: 'try!' expression unexpectedly raised an error: Onfido.OnfidoConfigError.missingSDKToken
How can I test this framework if it doesn’t have any demo capabilities? Maybe it has one, let’s dive in.
Disassemble
When I am analyzing iOS frameworks, I generally choose the x86_64 variant since decompiling Intel architecture is way easier than the ARM one. It is a huge file, where should we look? Let’s check our source code again:
let sdkToken = ""
let config = try! OnfidoConfig.builder()
.withSDKToken(sdkToken)
.withWelcomeStep()
.withDocumentStep()
.withFaceStep(ofVariant: .photo(withConfiguration: nil))
.build()
We should check how OnfidoConfig.builder
is working and why it is rejecting our token. We need to find a function related to OnfidoConfig
and build
Due to Swift name mangling, you will not find pretty names when you disassemble with Ghidra. You will see something like below:
_$s6Onfido0A13ConfigBuilderC5buildAA0aB0VyKF
By using a script you can get the full names. This script is actually running swift demangle
and just fixes the code. Let’s try:
➜ swift demangle "_\$s6Onfido0A13ConfigBuilderC5buildAA0aB0VyKF"
_$s6Onfido0A13ConfigBuilderC5buildAA0aB0VyKF ---> Onfido.OnfidoConfigBuilder.build() throws -> Onfido.OnfidoConfig
I added \
to escape the $
sign. If we disassemble this code, we can see that it is first calling OnfidoConfigBuilder.validateTokenConfig()
to validate the token. Let’s see the pseudo code of this function:
if (lVar1 != 0) {
local_128 = uVar3;
local_120 = lVar1;
bVar2 = _$sSS7isEmptySbvg();
if (((bVar2 ^ 0xff) & 1) != 0) {
auVar4 = _$sSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfC("demo",4,1);
It basically checks if our token is equal to the “demo” string. If so, then it starts the demo flow. Our first task is done. If we set the token with let sdkToken = "demo"
then can see the flow.
Are we done yet? NOPE.
If we use the demo token, we can’t use enterprise features such as custom branding. I haven’t tried, but it probably will upload the scanned documents to Onfido’s servers. Let’s go one step further and try to find out what an actual SDK Token looks like. Let’s go back to the disassembly again.
Inside OnfidoConfigBuilder.validateTokenConfig()
function, we can see another function Onfido.decode(sdkToken: Swift.String) throws -> Onfido.SDKToken
There is also another function inside with the following signature Onfido.(decode in _63DB231C0F175F5DE515FE723446D185)(sdkToken: Swift.String) throws -> [Swift.String : Any]
If we check the disassembly of this function, we can see that it is splitting the string with ’.’ and then trying to decode it as a JSON. That is basically a JWT. Then it finally uses JSONDecoder
to decode this JSON to the SDKToken
structure. Unfortunately they made the SDKToken
struct private, we can’t see it inside Xcode. We need to create the SDKToken by hand so that when it is decoded from JSON, it shouldn’t throw an exception.
By checking the disassembly, we can find some fields: clientUUID
, applicationId
, isSandbox
etc. However, when we craft a JWT with those fields, the SDK throws an exception. What is happening? Welcome to
CodingKeys of Swift.
Inside the SDKToken struct clientUUID
field is decoded from JSON via the client_uuid
field. I painstakingly created all the fields for this struct. However, I found out that their EnterpriseFeatures
struct doesn’t have CodingKeys
. I don’t know why. Maybe EnterpriseFeatures
was added later on by a developer who loves camelCase. After some trying, this is what I came up with for the SDKToken struct. Some of the fields below actually don’t exist inside the binary. However, I found some clues inside their other packages, such as here
struct SDKToken: Codable {
let payload: Payload
let uuid: String
let enterpriseFeatures: EnterpriseFeatures
let urls: Urls
enum CodingKeys: String, CodingKey {
case payload, uuid
case enterpriseFeatures = "enterprise_features"
case urls
}
struct Payload: Codable {
let app: String
let applicationId:String
let clientUUID: String
let isSandbox: Bool
enum CodingKeys: String, CodingKey {
case app
case applicationId = "application_id"
case clientUUID = "client_uuid"
case isSandbox = "is_sandbox"
}
}
struct EnterpriseFeatures: Codable {
let hideOnfidoLogo: Bool?
let useMediaCallback: Bool?
let disableSDKAnalytics: Bool?
}
struct Urls: Codable {
let detectDocumentURL: String
let syncURL: String
let hostedSDKURL: String
let authURL: String
let onfidoAPIURL: String
let telephonyURL: String
enum CodingKeys: String, CodingKey {
case detectDocumentURL = "detect_document_url"
case syncURL = "sync_url"
case hostedSDKURL = "hosted_sdk_url"
case authURL = "auth_url"
case onfidoAPIURL = "onfido_api_url"
case telephonyURL = "telephony_url"
}
}
}
Let’s have fun and create our fake JWT with Enterprise features. I will encode the JSON below as a JWT.
{
"uuid": "719bc53d-4599-4495-b60f-74b1c0589403",
"enterprise_features": {
"useMediaCallback": true,
"disableSDKAnalytics": true,
"hideOnfidoLogo": true
},
"urls": {
"detect_document_url": "http://localhost:3000",
"sync_url": "http://localhost:3000",
"hosted_sdk_url": "http://localhost:3000",
"auth_url": "http://localhost:3000",
"onfido_api_url": "http://localhost:3000",
"telephony_url": "http://localhost:3000"
},
"payload": {
"app": "HELLO",
"client_uuid": "f75c9ecd-da13-4bb2-a5e7-36f2c37b5fbf",
"application_id": "APP_ID",
"is_self_service_trial": false,
"is_trial": false,
"is_sandbox": false
}
}
We can use any algorithm or key when we are creating our JWT. The client doesn’t check the algorithm or the signature. If we encode the JSON above as JWT and use it inside the sample app, we can easily test the app, but it fails since we don’t have a server running to process the data. We also need to implement a server that will handle all the analytics and document upload. Again, by checking what the client sends and expects, we can easily write a mock server that will allow us to test this SDK. I will not bore you with details. Instead, I will give you my implementation.
Profit
I have forked the original SDK and updated the sample app with JWT generation and mock server.
Disclaimer:
I need to remind you that we only bypassed the silly token restriction. I still don’t know why they didn’t document the “demo” token. I have a theory though. I checked their Android SDK and it looks like(I don’t have real Android device to test) it doesn’t have “demo” token inside. In order to have a consistency maybe they didn’t add it to their documentation. You still can’t use this SDK freely. The above fork only allows you to take pictures and send them to your server. This implementation only allows you to test the basic SDK functionality without signing up. All the heavy lifting (OCR, flows, etc.) is done on the server.