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.

Github Repo

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.

I am actively job-hunting and available
Interested? Feel free to reach