Skiptracing Part 2: iOS

Sam Lerner
Jun 26 · 12 min read

Step Zero: Jailbreak

Step One: Get the Binary

Step Two: Dumping the Classes

Finding the Right Class

console.log(ObjC.classes.UIWindow.keyWindow().recursiveDescription().toString());
The matryoshka continues!
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');
}
});

Step Three: Code Injection

Dylib Writing

static void __attribute__((constructor)) initialize(void)
{
}
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
}
id prev(id self, SEL _cmd, id options) {}
id state = [self valueForKey:@"state"];
NSURL *uri = [[state valueForKey:@"track"] valueForKey:@"URI"];
NSString *tid = [[uri absoluteString] substringFromIndex:14];

Data Exfiltration

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()

Mach-O Manipulation

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)
  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')))

Step Four: Brewing the IPA

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

Conclusion

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade