> I recently learned about a process called hooking, where you can “intercept” function calls made from a target binary. I thought this would be the perfect way to track skips.
> The most common type of hook is the interpose hook.
Note that dyld has built-in support for interposing; you don't need to mess around with dlsym and RTLD_NEXT (which requires disabling the two-level namespace anyways, as you seem to have figured out, which can cause some programs to misbehave): https://opensource.apple.com/source/dyld/dyld-210.2.3/includ...
> As stated earlier, an interpose hook can only be created for an external function, so we’ll look for a function in the libc or in the Objective-C runtime.
Yes, but the Objective-C runtime lets you swizzle anything :) No need to dig around for an external C function to patch.
> Spotify opened fine but Apple’s System Integrity Protection (SIP) didn’t let us load our unsigned library :(.
No, this is not System Integrity Protection: it's Library Validation, which prevents loading libraries signed with a different Team ID than the main binary was signed with. You can remove the binary's code signature to get around this.
> We’ll first set a hook on sub_10006DE40 and then we will trigger a breakpoint from within our code. We can do this by executing the assembly instruction int 3 which is what debuggers like GDB and LLDB use to trigger breakpoints.
Or, compile with debug symbols and put a breakpoint at your function as you normally would.
> Notice that the PC will be at an offset address from the one shown in IDA (honestly, I don’t have the best grasp as to why this happens but I assume it’s due to where the process is loaded into memory).
Most binaries are compiled position-independently on macOS, and get loaded at a randomized address (but the debugger should usually turn off the randomization).
for those interested in the much lower-level details on how spotify works, Librespot[1] and its Golang and Java forks for those who don't speak Rust should definitely be interesting. It can act as a Spotify Connect receiver and play songs, as well as access Spotify's metadata, control other devices etc.
Before this article, I've always thought Spotify was an electron app (or something of the sort), so never bothered installing it. I was wondering why you didn't just open a Dev console and inject some JS :).
Regarding the Apple code signing requirement: is it the case then for all hooks of this sort that one is effectively required to be in the Developer program?
So I was curious if LD_PRELOAD is prevented from working, but seems there's a simple work around:
> On macOS there is an additional problem. Newer versions of macOS have a security subsystem called System Integrity Protection. For our purposes the problem is that it prevents injecting code via DYLD_INSERT_LIBRARIES (the macOS equivalent of LD_PRELOAD) into any binary in /bin, /sbin, /usr/bin and /usr/sbin.
> Luckily, there’s an easy workaround. Just create a new directory, copy all the binaries from /bin, /sbin, /usr/bin and /usr/sbin into that directory, and then add it to the start of your $PATH environment variable. Once the binaries are out of those special directories code injection works just fine, and since they’re only 100MB copying them is quite fast.
From my understanding, Spotify uses the Chromium Embedded Framework. I'm not sure which parts are rendered with CEF, but since Spotify is using CEF directly rather than Electron, it seems likely they have a fair bit of native code as well.
Electron is not anything much different than Chromium Embedded plus some native API hooks, so it's not the Electron parts that eat the resources, it's Chromium (and basically, it's the fact that the app is basically a web app that needs a whole layered and bloated rendering engine -DOM- and a slower runtime - js - compared to native).
No idea. I personally have little tolerance for the amount of resources that Chrome uses, so I stay far away from its derivatives, which I have found to not be meaningful improvements.
The issue in the article is due to Library Validation, not SIP. The issue goes away if you unsign the binary; no need to be part of the developer program.
Based on the hooks, injecting, and decompiling this seems like how people hack a video game, but instead this is for a music player. Interesting twist, and kind of neat!
I just want Spotify to give me better recommendations instead of the same songs over and over.
While interesting for reasons other than the end goal, I find it somewhat amusing that someone would hook code into the Spotify binary to provide, effectively, more or less the same data that Spotify's native Last.fm integration would.
While this is pretty cool, it's quite impractical.
You could just poll their HTTP API for the currently playing song and compare the playtime. Plus this would work for any client, even mobile devices.
Agreed. Although I'd much sooner recommend just using Mopidy. Mopidy supports spotify and many other music services, and has a number of low-effort ways of controlling playback programmatically.
But for sure the interesting part of this is the method, not the purpose.
Interesting post, I actually want the exact same thing! I've tried making playlists that duplicate parts of the main library, one per mood, but it doesn't really work. If Spotify could learn which songs I consistently skip after starting with (or completely listening to) a given other song, that sounds like it would solve the problem.
Ahh nice. I am right now working on that same thing, tracking plays and skips. I just want to simply analyse my own listening more deeply though.
My current implementation is a hack that just keeps polling Spotify with ‘shpotify’ to grab data. This article is more the implementation I actually wanted.
you can already do that without hacking anything, Spotify exposes that information through Apple Script:
if application "Spotify" is running then
tell application "Spotify"
if player state is playing then
return (get artist of current track) & " - " & (get name of current track)
else
return ""
end if
end tell
end if
So much of this work would be unnecessary (at least in the EU) if Spotify wasn’t blatantly violating GDPR and refusing to provide full listening data when requested.
> The most common type of hook is the interpose hook.
Note that dyld has built-in support for interposing; you don't need to mess around with dlsym and RTLD_NEXT (which requires disabling the two-level namespace anyways, as you seem to have figured out, which can cause some programs to misbehave): https://opensource.apple.com/source/dyld/dyld-210.2.3/includ...
> As stated earlier, an interpose hook can only be created for an external function, so we’ll look for a function in the libc or in the Objective-C runtime.
Yes, but the Objective-C runtime lets you swizzle anything :) No need to dig around for an external C function to patch.
> Spotify opened fine but Apple’s System Integrity Protection (SIP) didn’t let us load our unsigned library :(.
No, this is not System Integrity Protection: it's Library Validation, which prevents loading libraries signed with a different Team ID than the main binary was signed with. You can remove the binary's code signature to get around this.
> We’ll first set a hook on sub_10006DE40 and then we will trigger a breakpoint from within our code. We can do this by executing the assembly instruction int 3 which is what debuggers like GDB and LLDB use to trigger breakpoints.
Or, compile with debug symbols and put a breakpoint at your function as you normally would.
> Notice that the PC will be at an offset address from the one shown in IDA (honestly, I don’t have the best grasp as to why this happens but I assume it’s due to where the process is loaded into memory).
Most binaries are compiled position-independently on macOS, and get loaded at a randomized address (but the debugger should usually turn off the randomization).