在开始本文的正式内容之前我想先来吐槽下。大多数的软件开发人员可能都有着这样一个烦恼,就是由于工作和其他责任,不得不搁置自己的一些个人项目甚至是最终完全的遗忘和埋没。而本文的所述的就是一个被我遗忘已久的项目,而我写这篇文章的目的就是希望能迫使我自己最终完成这个项目。好了,介绍就到这了让我们开始吧。
项目
该项目的目标是构建一个Spotify客户端,让它能够学习我的听曲习惯并跳过一些我通常会跳过的歌曲。不得不承认,这种需求来自于我的懒惰。我不想在当我有心情想要听某些音乐时,创建或查找播放列表。我希望的是在我的库中选择一首歌,然后可以随机播放其他歌曲,并从队列中删除不“flow(节奏与旋律的流畅)”的歌曲。
为了实现这一点,我需要学习某种能够执行此任务的模型(在未来的帖子中可能更多)。但是为了能够训练一个模型,我首先需要数据来训练它。
数据
我需要完整的听歌历史记录,包括我跳过的那些歌曲。获取历史记录很简单。虽然Spotify API仅允许获取最近50首播放的歌曲,但我们可以设置一个cron job来重复轮询该端点。完整代码已经发布在此处:https://gist.github.com/SamL98/c1200a30cdb19103138308f72de8d198
最困难的部分是跟踪跳过。Spotify Web API并没有为此提供任何的端点。之前我使用Spotify AppleScript API创建了一些控制播放的服务(本文的其余部分将涉及到MacOS Spotify客户端)。我可以使用这些服务来跟踪跳过的内容,但这感觉像是在回避挑战。我怎么能完成它呢?
Hooking
我最近学习了解了有关hooking的技术,你可以在其中“拦截”从目标二进制文件生成的函数调用。我认为这将是跟踪跳过的最佳方法。
最常见的钩子类型是interpose hook。这种类型的钩子会覆盖PLT中的重定位,但这究竟意味着什么呢?
PLT或过程链接表允许你的代码引用外部函数(想想libc)而不知道该函数在内存中的位置,你只需引用PLT中的一个条目。链接器在运行时为PLT中的每个函数或符号执行“重定位”。这种方法的一个好处是,如果外部函数在不同的地址加载,则只需要更改PLT中的重定位,而不是每次对代码中该函数的引用。
因此,当我们为printf创建一个interpose hook时,每当我们hooking的进程调用printf时,我们将调用printf的实现而不是libc(我们的自定义库通常也会调用标准实现)。
在对钩子有了一些基本的知识背景后,下面我们准备尝试在Spotify中插入一个钩子。但首先我们需要弄清楚我们想要hook的是什么。
寻找 hook 的位置
如前所述,只能为外部函数创建一个interpose hook,因此我们将在libc或Objective-C runtime中查找函数。
在研究在哪hook时,我认为一个开始hooking的好地方是Spotify处理“media control keys”或我MacBook上的F7-F9。假设这些键的处理程序在spotify应用程序中单击Next按钮被调用时会调用函数。我最终在:https://github.com/nevyn/spmediakeytap上找到了SPMediaKeyTap库。我想我可以试一试,看看Spotify是否复制并粘贴了这个库中的代码。在SPMediaKeyTap库中,有一个方法startWatchingMediaKeys。我在Spotify二进制文件上运行了strings命令,看看他们是否有这个方法,果然:
Bingo!!如果我们将Spotify二进制文件加载到IDA(当然是免费版本)并搜索此字符串,我们就会找到相应的方法:
如果我们查看这个函数对应的源码,我们会发现CGEventTapCreate函数的有趣参数tapEventC