Skiptracing Part 2: iOS

Sam Lerner
12 min readJun 26, 2019

This is a sequel to my previous post about tracking skips in the Spotify MacOS desktop client: https://medium.com/@lerner98/skiptracing-reversing-spotify-app-3a6df367287d. In this post, I will do the same for the Spotify iOS application using some new techniques. Let’s get to it.

Step Zero: Jailbreak

Basically any iOS reverse engineering requires a jailbreak. This is because iOS requires each executable as well as virtual memory page to be codesigned by Apple or an Apple developer. If we want to run anything like an SSH server on our iPhone, we need a jailbreak to patch the iOS kernel to bypass codesigning enforcement. I have an iPhone 5 running iOS 10.3.3 so I used the Helix jailbreak. If you have a newer iPhone running a newer version of iOS (which you likely do) you’ll need to use a different jailbreak and much of the following will need to be updated for arm64 (my phone runs armv7).

Step One: Get the Binary

When not running, iOS applications are encrypted. This means that to disassemble the binary, we must first decrypt it. Thankfully, I am far from the first person to want this capability so there are some nice libraries to decrypt iOS applications.

I used Clutch: https://github.com/KJCracks/Clutch for decryption. Instead of breaking the encryption scheme (which would be intractable), Clutch loads the application like it is being executed and then proceeds to dump the decrypted binary from memory.

We can then scp the dumped IPA files back to our computer (assuming you’ve set up OpenSSH on your iPhone). I would highly recommend using SSH over USB: https://iphonedevwiki.net/index.php/SSH_Over_USB since it is far faster than using WiFi.

Step Two: Dumping the Classes

The first thing I wanted to do was take a look at the classes to see if anything stood out. Now usually this would be done with class-dump or class-dump-z but I kept running into segmentation faults or that the architecture of the binary (armv7) was unsupported. Then I saw this line in the class-dump readme:

“This is the same information provided by using ‘otool -ov’, but presented as normal Objective-C declarations.”

If we runotool -ov on our decrypted binary, we’ll get a complete description of the classes, including their methods and instance variables. This will be super helpful coming up.

The only problem is that the output of this command is pretty massive (hence the -v flag). We’ll need to come up with a way to hone in on the important classes.

Finding the Right Class

Our final goal is to find the class/methods that deal with playback control. A reasonable way to go about this is to find the previous/next buttons within the view hierarchy and print their targets. After all, the targets of these buttons have to eventually trigger our desried methods, right?

To print the view hierarchy, we’ll use Frida: https://github.com/frida/frida. Once you have Frida installed and have your iPhone connect via USB to your computer, open up Spotify and click on the bottom bar to show the view with the playback control buttons.

Now, we can attach Frida to Spotify by running frida -U -n Spotify. After maybe a minute, you should have a Javascript console open.

Frida provides a nice Javascript API for us to interact with the Objective-C class information of the attached app. Using this API, we can print the current view hierarchy like so:

console.log(ObjC.classes.UIWindow.keyWindow().recursiveDescription().toString());

There are a lot of levels to the view hierarchy, so it may be helpful to expand your terminal window. If you search through the view hierarchy, you should see a few interesting views:

It looks like we’ve found them. We can now get the targets to the buttons (I’ll do it for the next button but it doesn’t matter which you choose).

First, use the pointer to the button instance to create an Objective-C object:

Then we can print the targets of that object:

We can now see that the next button’s target is a class with a promising name: SPTNowPlayingPlaybackActionsHandlerImplementation. Let’s look at our Objective-C class dump and see what sorts of interesting methods this class implements.

Scrolling through its method, we come across two methods that should pique your interest:

Let’s look at the disassembly for this method (skipToNext) to see what else it might call (I’m using the demo version of Binary Ninja because IDA free doesn’t support armv7 — make fun of me all you want):

We can see that this method is accessing an instance variable of another class called playbackController. Now, we could probably be smart and trace through to assembly to see which class this is a method of, but why don’t we just search for the method in our Objective-C class dump.

If you do this, you’ll find that many classes have an instance variable called playbackController. I’ll just choose the SPTAppBackgroundProtocolController class but it shouldn’t matter if you choose a different one.

Going back to Frida, we can obtain an instance of SPTAppBackgroundProtocolController assuming one exists on the heap:

We can see that the playbackController is of type SPTExternalIntegrationPlaybackControllerImplementation (verbose!).

Looking through our class dump, we see that this class has an instance variable called player:

The matryoshka continues!

In Frida, we can print out the type of this variable:

which is SPTPlayerImpl. Just by the name of the class, it feels like we may found the right class. Let’s look at some of its methods to confirm our suspicion:

Oh yeah. skipToNextTrackWithOptions: and skipToPreviousTrackWithOptions: seem like good candidates for methods to hook.

To confirm this let’s write some javascript to hook the functions using Frida:

const playerClass = ObjC.classes.SPTPlayerImpl;
const nextMeth = playerClass["- skipToNextTrackWithOptions:"];
const prevMeth = playerClass["- skipToPreviousTrackWithOptions:"];
Interceptor.attach(nextMeth.implementation, {
onEnter: function(options) {
console.log('next');
}
});
Interceptor.attach(prevMeth.implementation, {
onEnter: function(options) {
console.log('prev');
}
});

Run Frida again, this time with passing -l <script name>.js and press the next or previous button in Spotify. Sure enough, you’ll see our log messages being printed in the terminal! You can even use the buttons on the lock screen or control center and the hooks should trigger. It looks like we’ve found our target functions!

Step Three: Code Injection

That’s great but it’s not super convenient to have to attach Frida to Spotify every time we want to track some skips. What we really want is a way to package our code into the Spotify IPA and have it run automatically every time we open Spotify.

I thought for a long time about this problem. I was expecting to have to do some Mach-O magic, increasing the size of the __text section and inserting some compiled Objective-C, and manually changing the imp pointers of our target methods. However, just as I was about to start this daunting task, I came across Kenneth Poon’s article on iOS code injection: https://medium.com/@kennethpoon/how-to-perform-ios-code-injection-on-ipa-files-1ba91d9438db.

Kenneth creates a dynamic library (dylib) and links it with the target application. That’s so much simpler than what I was planning on doing! Let’s follow this approach and write a dylib to perform our hooking for us on startup.

Dylib Writing

First, let’s create a new framework in Xcode. Xcode will automatically create an Objective-C file for you. In this file, let’s set up our function to run when our library is first linked:

static void __attribute__((constructor)) initialize(void)
{
}

The constructor attribute tells the loader to run this function right after it has mapped us into virtual memory. It’s kind of like a main function for our library.

Within this function, we’ll want to replace the SPTPlayerImpl's implementations for skipToPrev.. and skipToNext... with our own implementations:

static void __attribute__((constructor)) initialize(void)
{
Class playerImplClass = NSClassFromString(@"SPTPlayerImpl");
// Get the imp pointer to the prev method SEL prevSel = NSSelectorFromString(@"skipToPreviousTrackWithOptions"); Method prevMeth = class_getInstanceMethod(playerImplClass, prevSel); origPrevImp = method_getImplementation(prevMeth); // Replace it with our own

class_replaceMethod(prevMeth, prevSel, (IMP)prev, method_getTypeEncoding(prevMeth));

... Do the same for next
}

We get the implementation for the previous method, and replace it with our own prev function which is defined like so:

id prev(id self, SEL _cmd, id options) {}

because every Objective-C method has two “hidden” parameters: a pointer to the calling instance and its selector. The constructor function also relies on a global IMP variable origPrevImp which we will call in our hook.

Our prev and next hooks will look pretty much like those in part one; we will only register the event if we are past halfway in the track. Also, when skipping backwards, only register if the new track ID is different from the old one.

The current track ID, player position, and track duration (which if you recall from part one are needed for our hooks) are obtained through the state instance variable of self in our hooks which is of type SPTPlayerState. For example, to get the track ID, the code looks like this:

id state = [self valueForKey:@"state"];
NSURL *uri = [[state valueForKey:@"track"] valueForKey:@"URI"];
NSString *tid = [[uri absoluteString] substringFromIndex:14];

We use the valueForKey method to retrieve instance variables since the compiler won’t recognize an object of type id responding to the selector state.

Lastly, we have our SkipManager class that deals with writing skips to disk. This class is almost identical to the skip manager in part one but I updated it to be an Objective-C class.

Here is a link to the full code if you want to take a closer look: https://github.com/SamL98/libskip.

Data Exfiltration

Now that we can track our skips, we need a way to get the data from our iPhone to the computer. It’s not very convenient to have to constantly scp the file over and if we ever decide to un-jailbreak our phone, we won’t even have access to scp.

My solution to this was to set up a simple web server (running free on Heroku of course) that has two endpoints: upload and download. In the dylib constructor, the skip file is uploaded to the upload endpoint. We can then download the skip file from the download endpoint. Here is the Python code for the server using Flask:

from flask import Flask, request, send_from_directory
from os.path import join as pjoin
app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = '.'
@app.route('/upload', methods=['POST'])
def upload():
if not 'file' in request.files:
return 'No uploaded file', 400
f = request.files['file']
f.save(pjoin(app.config['UPLOAD_FOLDER'], 'skipped.csv'))
return 'All good', 200@app.route('/download', methods=['GET'])
def download():
return send_from_directory(app.config['UPLOAD_FOLDER'], 'skipped.csv')
if __name__ == '__main__':
app.run()

We can then upload from Objective-C with an NSURLRequest to <server url>/upload (Uploader.m in the Github repo) and download it on our computer with curl <server url>/download. We can even make the file download a cron job. Everything is fully automated!

Mach-O Manipulation

Before we can wrap things up, we need to do one last very important step: tell the linker to load our dylib! To do that, we will edit the header of the Spotify Mach-O binary and add a dylib load command (credit to optool: https://github.com/alexzielenski/optool for showing how to do this). Before reading this section, I recommend taking a look at the OS X ABI documentation: https://github.com/aidansteele/osx-abi-macho-file-format-reference.

The Mach-O file format consists of among other things, a header followed by a sequence of load commands. The header specifies metadata about the binary such as the target architecture and how many load commands are present. The exact format of the 32-bit header can be seen below:

Each load command tells the loader something about the process image such as where each segment is located in the file and where it should be mapped to in virtual memory.

The command we are interested in is theLC_LOAD_DYLIB command which tells the dynamic linker to load a library the binary has been linked with. To have our library loaded into the process image, we must 1) Increment the ncmds field in the header, 2) increase the sizeofcmds field to accomodate for our new load command, and 3) Insert our LC_LOAD_DYLIB command at the end of the existing load commands.

We can edit the header with the following code:

DYLIB_LC = 12
DYLIB_LC_SIZE = 24
DYLIB_PATH = "@rpath/libskip.dylib"
def read_int(f, nbytes):
val = 0
for i in range(nbytes):
byte = ord(f.read(1))
val |= byte << (i * 8)
return val
def write_int(f, val, nbytes):
f.write(bytes([(val >> i*8) & 0xff for i in range(nbytes)]))
# Calculate the total size of the new LC, make sure to pad
# to 4 bytes otherwise the loader will be very unhappy

lc_size = DYLIB_LC_SIZE + len(DYLIB_PATH) + 1 # null terminator
if lc_size % 4 != 0:
lc_size += 4 - (lc_size % 4)
with open('<path to spotify binary>', 'r+b') as f:
f.seek(16) # seek past the first four fields of the header
ncmd = read_int(f, 4)
sizeofcmds = read_int(f, 4)
f.seek(16) # re-seek to the offset of ncmds
write_int(f, ncmd + 1, 4)
write_int(f, sizeofcmds + lc_size, 4)

We first calculate the size of our new load command by adding its base size, 24 bytes (we will see why soon), to the length of the null-terminated path to the library, which we will put in the binary’s @rpath. In this case, @rpath is equal to Spotify.app/Frameworks. Then we edit the cmds and sizeofcmds fields so that they are in harmony with the new load command we will add.

Then we have to insert our load command. Thankfully, there is enough padding after the end of the existing load commands to insert our command and not have to shift things around.

Before we add the actual load command, lets take a look at its structure:

The struct is pretty simple, all we need is the command (12), its size (24), and a dylib struct which is represented as:

This struct provides metadata about our library. We don’t really care about these fields, so we’ll set them to zero (except for the name of course). As a side note, it seems that the timestamp field is usually set to 2. I have no clue why but I didn’t mess with it.

The final field is the name field, which for our purposes is just an offset in the file to where the name string is located:

Now that we know what we’re writing, we can insert it into the binary:

  f.seek(sizeofcmds + 4, 1) # 1 seeks from the current offset.
# Since we just wrote sizeofcmds, the
# curr + sizeofcmds + 4 seek will be
# to the end of the current LC's. The 4
# is for the flags field of the header
write_int(f, DYLIB_LC, 4)
write_int(f, lc_size, 4)
write_int(f, DYLIB_LC_SIZE, 4)
write_int(f, 2, 4)
write_int(f, 0, 4)
write_int(f, 0, 4)
f.write(bytes(DYLIB_PATH.encode('ascii')))

The only tricky part of this is writing the offset of the name lc_str in the dylib struct. This is offset to the name from the beginning of the load command. Since the size of the load command is 24 bytes and we are writing the string directly after the load command, that will be our offset.

Step Four: Brewing the IPA

Now that our dylib is written, our webserver up and running, our cron job set, and our dylib loaded, all that’s left to do is move our dylib to the Frameworks directory within Spotify.app and repackage the IPA like so:

cp ~/Library/Developer/Xcode/DerivedData/<libskipdir>/Build/Producted/Debug-iphoneos/SkipTracer.framework/SkipTracer Payload/Spotify.app/Frameworks/libskip.dylibzip -r Spotify-resigned.ipa Payload

There’s only one problem, if we want our new IPA to run on a non-jailbroken iPhone, we will need to code sign it. Thankfully, this process is explained in https://stackoverflow.com/questions/6896029/re-sign-ipa-iphone.

After signing the app and frameworks, just re-zip and use a tool like Cydia Impactor to install the IPA on your device. You should now be up and running!

Conclusion

To be honest I was a little disappointed in how easy it was to inject our own code; I was expecting to have to get down and dirty with some assembly. Nonetheless, this was a neat project and I learned a heck of a lot about the Objective-C runtime and the Mach-O file format. As always, let me know what you think and thanks for reading!

--

--