Hooking Swift methods for fun and profit

Introduction

During the past few years different hooking frameworks have been developed for iOS, which allows users to modify app behavior without applying binary patches. Hooking iOS methods can benefit many things during a (blackbox) security assessment. Hooking allows us to log methods calls, inspect the input and/or output of a method, and even circumvent security measures like SSL/TLS in order to view/manipulate network traffic. In order to hook methods using such a framework, the device needs to be Jailbroken.

Hooking C/C++ and Objective-C methods has become more common over the years. More tools and frameworks are available and are still being developed in order to perform (security) research and create custom app modifications. Apple introduced a new programming language called Swift that is built on top of the Objective-C runtime. Swift methods can be hooked in a similar, but slightly different way. This article will describe how Swift methods can be hooked.

Method and selector signatures

In order to hook a method it is important to identify its signature; how many parameters does it require, what are the types, the symbol name, and what type it returns. In order to acquire this information the following tools can be used:

  • IDA - disassembler;
  • Hopper - disassembler;
  • otool - object file info tool;
  • nm - list symbols from object files;
  • class-dump - dump class signatures.

Swift symbols are mangled similar to C++ methods. Whenever the symbol table of a Swift app is dumped with a tool like nm, the Swift symbols are listed with a __T prefix whereas C++ symbols start with __Z. Similar to the C++ demangling tool c++filt, demangling Swift methods can be done using xcrun swift-demangle.

The following example shows the difference between a mangled and demangled symbol table:

$ nm <swift binary>

[..]
0000000100002840 T __TFC7DemoApp11AppDelegates6windowGSqCSo8UIWindow_
00000001000019f0 T __TFC7DemoApp14ViewController11viewDidLoadfS0_FT_T_
0000000100001af0 T __TFC7DemoApp14ViewController15mySuperFunctionfS0_FT_T_
0000000100001d20 T __TFC7DemoApp14ViewController23didReceiveMemoryWarningfS0_FT_T_
00000001000024a0 T __TFC7DemoApp14ViewControllerCfMS0_FT5coderCSo7NSCoder_S0_
[..]
$ nm <swift binary> | xcrun swift-demangle

[..]
00000001000027b0 t _@objc DemoApp.AppDelegate.window.setter : ObjectiveC.UIWindow?
0000000100001ac0 t _@objc DemoApp.ViewController.viewDidLoad (DemoApp.ViewController)() -> ()
0000000100001cf0 t _@objc DemoApp.ViewController.mySuperFunction (DemoApp.ViewController)() -> ()
0000000100001d80 t _@objc DemoApp.ViewController.didReceiveMemoryWarning (DemoApp.ViewController)() -> ()
00000001000024e0 t _@objc DemoApp.ViewController.init (DemoApp.ViewController.Type)(coder : ObjectiveC.NSCoder) -> DemoApp.ViewController
[..]

Constructing a method hook requires both representations. The mangled symbol is the actual pointer to the method and is used during the hooking setup. The demangled version helps us to construct the actual hook since it contains parameters and return types. The following example shows a method of the DemoApp class:

func mySuperFunction( )
{
   // do stuff
}

The mangled method name looks like this:

0000000100001af0 T __TFC7DemoApp14ViewController15mySuperFunctionfS0_FT_T_

The mangled method name can be broken down in the following components:

  • __T - indicates a Swift method;
  • F - the symbol is a function/method;
  • C - the symbol is a class method;
  • 7DemoApp - app name/module name;
  • 14ViewController - class name;
  • 15mySuperFunction - method name.

This method does not have a return type and does not have any parameters. Or to be more precise, it has one parameter called self. When demangled, the method looks like this:

0000000100001cf0 t _@objc DemoApp.ViewController.mySuperFunction (DemoApp.ViewController)() -> ()

This can be roughly translated into:

Module.classname.functionName (self) -> (no return type)

Constructing the hook

Now that we know how to obtain and demangle Swift symbols we can construct hooks for Swift methods. In the following example we will hook the constructor of the NSString class. Our hook will write the constructor value to NSLog. First we need to determine the NSString constructor symbol. Using nm we can see the following mangled symbol:

00000001000025c0 t __TFCSo8NSStringCfMS_FT6stringSS_S_

Demangling the symbol gives us:

00000001000025c0 t _ObjectiveC.NSString.__allocating_init (ObjectiveC.NSString.Type)(string : Swift.String) -> ObjectiveC.NSString

Since NSString is a member of Apple's Foundation framework we could also look the signature up in the developer API

The constructor takes a (Swift) string as parameter and will return an NSString object. With this information in mind we can construct our hook.

First, we need to declare a function pointer that will point to the original method we are going to hook. This will become handy when we want to call the original method from our hook.

static id (*orig_nsstring_init)(id,id);

Since NSString is a Swift object we can use the id type when identifying parameters and return type. Note that we declared two parameters instead of one. In addition constructor value, we must also process the self parameter. This is different from hooking Objective-C methods where we also process the selector parameter. For example:

id method(id self, SEL cmd, id param1) // Objective-C method signature

Now we can create the method hook.

id nsstring_init_hook(id str, id self)   // hook signature
{
   id orig_value = orig_nsstring_init(str, self);   // invoke original constructor, save return value
   NSLog(@"# %@ #", orig_value);   // print constructor parameter
   return orig_value;   // return the value to facilitate the original behaviour
}

Important to note here is that in comparison to Objective-C methods, the self parameter is the last parameter instead of the first. As mentioned before, the selector parameter is not supplied. Using Cydia Substrate we can use the following method to tie this all together using MSHookFunction.

MSHookFunction(
   MSFindSymbol(NULL,"__TFCSo8NSStringCfMS_FT6stringSS_S_"), // find the mangled symbol
      (void*)nsstring_init_hook, // function pointer to our hook
      (void**)&orig_nsstring_init); // stores function pointer to original function

Code listing

#import <Foundation/Foundation.h>
#import <substrate.h>

static id (*orig_nsstring_init)(id,id) = NULL;

id nsstring_init_hook(NSString* _str, id _self)
{
   id x = orig_nsstring_init(_str,_self);
   NSLog(@" ## %@ ##",x);
   return x;
}

__attribute__((constructor))
int main(void)
{
   MSHookFunction(MSFindSymbol(NULL,"__TFCSo8NSStringCfMS_FT6stringSS_S_"),
         (void*)nsstring_init_hook,
         (void**)&orig_nsstring_init);
   
   return 0;
}

Questions or feedback?