项目地址为什么要这个东西我以前是怎么传剪贴板的问题与转机结论:我需要一个像iCloud的云端同步剪贴板app方案调研已有产品思考.jpg🤔细化:iOS到Windows怎么同步最终方案:iCloud + 快贴实现考虑:怎么利用快贴准备:了解快贴App的工作原理1. 让快贴持续运行问题:快贴是GUI app,在iOS上想要持续运行,最好还是避开UI这些东西思考:iOS上, app都是能直接跑的binary方法:hook app的入口点,让他绕开GUI初始化的部分:hook UIApplicationMain碰到的琐碎问题:队列初始化2. 让快贴持续检测剪贴板3. 从快贴设置iOS剪贴板至此,同步操作本身就成功了。然而,还有一堆坑。4. 关闭屏幕,但让iOS持续同步iCloud剪贴板5. 配置daemon,与jetsam斗智斗勇6. 接着与iCloud同步斗智斗勇至此iCloud云同步就完全正常,稳定工作了!
项目地址
为什么要这个东西
我以前是怎么传剪贴板的
- QQ:
- 发给自己:经常找不到自己
- 我的电脑:很方便,但是操作一通下来速度很慢
- 微信:
- 文件传输助手:搜索奇慢无比
- 发给自己
- Telegram: Saved Message:卡
问题与转机
我新增了两个设备,一个安卓一个iOS,用来调试。这个时候问题就来了:
- 这些设备上没有装QQ,也没有装微信
- 而且也不能装,QQ微信限制设备数量
- 安全考虑测试机器也不该装这些
- 这个设备并不一定跟我在一个局域网里(经常需要测试网络相关的东西 & 连VPN)
- KDE Connect之类的东西就gg了
结论:我需要一个像iCloud的云端同步剪贴板app
可能有人要质疑我云端同步的安全性。
对于我来说,相比起消息记录,剪贴板安全要求没那么高,云端同步的带给我的便捷性远大于安全上的漏洞
方案
调研已有产品
Android端和iOS端目前都严格限制剪贴板的读取,而想要好的体验关键之一就在于无感知的同步。
所以目前的剪贴板实从体验上分为两大类:系统级剪贴板 和 普通app剪贴板
那么对于后者来说,实际的使用体验是和QQ没区别的(需要手动点)
- 系统级内置的
- iCloud接力:Mac - iOS - iPad
- Windows剪贴板:Windows - Android
- app级
- 基于局域网的一堆app:KDE Connect、各种电脑端助手
- 不符合我的要求
- 苹果商店里的一堆app:iOS - Mac - Windows
- 没有安卓
- 国外的一些app:Magic Copy
- 国外的这几个app界面都极其老旧
- 同步稳定性堪忧
- 国内的代表app:快贴
- 界面非常友好
- 支持android ios windows linux mac全平台
思考.jpg🤔
- iOS设备之间的传输 —— 稳定
- Android-Win之间传输 —— 稳定
可以看出:Android & Win —断开的— iOS - Mac
所以为了在所有的平台上全自动同步,我们重点是需要搞一个从iOS里把剪贴板全自动同步(系统级剪贴板)的东西
细化:iOS到Windows怎么同步
- 同步困难1:网络:
- 肯定是不能指望着直连:
- 手机 - 没公网
- 电脑 - 时不时有公网
- 自建服务器:麻烦、不稳定
- 快贴自带服务器,解决了这个问题
- 同步困难2:让iOS实时在线取剪贴板
- iOS没后台:咋能让快贴一直运行?
- 手机电池有限:一直运行,手机贼耗电,肯定不能在主力机上面用
- iOS生态封闭,逆向iCloud协议得不偿失,肯定选择用越狱设备辅助
- 需要实现iOS的Daemon
- 同步困难3:双向同步
- 既要从服务器收到剪贴板,也需要自动把别的设备的将剪贴板发上去
- 需要逆向快贴和iOS剪贴板的机制
最终方案:iCloud + 快贴
路径(双向):iOS — iCloud — 越狱iOS — 快贴 — Windows
- 从iOS设备拷贝,经过越狱iOS设备后,通过快贴发给windows
- 从Windows拷贝,通过快贴发给越狱iOS设备,再通过iCloud传给iOS设备
实现
考虑:怎么利用快贴
方案1:逆向快贴的协议,自己写客户端
- 需要手动处理登录等问题
- 需要重新编写大量代码,维护性堪忧
- 尤其是端对端加密的部分,重写比较麻烦
方案2:将快贴客户端作为dylib来加载
- 可行,有
dylibify
这个工具:https://github.com/jakeajames/dylibify
- 但是这样我们和客户端并不在同一个Sandbox里面,无法调用他的keychain、NSUserDefault等内容,需要做大量修改,很不方便
方案3:通过hook直接魔改快贴的客户端
- 可以将快贴的主执行文件复制一份,通过设置substrate的filter为executable来只hook一份快贴,然后就可以hook他将其改做daemon
- 可以直接和原生快贴客户端共用同一个Sandbox,这样账号信息共享,无需手动处理登录
- 基本无需编写代码
- 最优秀的方案
准备:了解快贴App的工作原理
- 观察快贴的app网络流量可以看出来一个443一个1883,分别是mqtt.clipber.com:1883和iface.clipber.com:443
- 抓包或者逆向都可以看出mqtt的用户名、密码。也能看到iface接口里面发送的push、pull等请求
- 客户端并不会主动发送pull请求,除非你手动点刷新
- 每次剪贴板更新,会有两个upload_file请求和一个download_file请求,为什么是这样我也不能理解
- 猜测:mqtt是个订阅,所以很显然作者就是用mqtt长链接来同步,配合iface pull做为备份,有大文件的时候用download_file
- 然而实际上,后续用mqtt客户端看了一眼才知道,快贴只是在mqtt协议里写了文件id,不管多大多小的文件,都是存在oss上,需要调用接口来下载
1. 让快贴持续运行
问题:快贴是GUI app,在iOS上想要持续运行,最好还是避开UI这些东西
- iOS只有一个屏幕,没有窗口的概念,所以你一直前台放快贴,就会让这个设备完全没法用来做别的事情
- GUI耗费更多资源,带来更多不确定性(可能会抢占GPU)
思考:iOS上, app都是能直接跑的binary
所以我们可以把UI初始化的部分分离,让他能作为一个没有界面的程序来后台运行
方法:hook app的入口点,让他绕开GUI初始化的部分:hook UIApplicationMain
- 方案0:hook后修改入口点,手动执行UI Delegate的一部分逻辑
- 理论上可行,但是这依赖于开发者的解耦能力,如果有多个关键逻辑的class都和UI耦合,那就很难受了
- 实际上,快贴的作者非常优秀,架构解耦的很彻底,绝大部分逻辑都和UI无关
- 整体上,快贴实际上用Swift写UI,ObjC写业务逻辑,编写非常明显
- 问题:我们没有初始化队列,程序完全无法执行任何逻辑,直接卡死
- 方案1:hook后修改UIApplicationMain的内部逻辑,关闭UI初始化的部分
- 理论上,这样可以让iOS自动管理各种初始化逻辑,尽可能的减少兼容性问题
- 实际上,不可行,UIApplicationMain里面有很多逻辑,一层套一层,会在很多回调后出现崩溃
- 核心的GUI初始化函数是GSEventInitializeApp,hook了,不管用
- 反思:UIApplicationMain本来就是UI相关的逻辑,这里面陷得越深,UI相关的代码就越多,出问题的地方也会越多
- 方案2:hook后修改入口点,初始化队列后手动执行UI Delegate的一部分逻辑
- 初始化队列后,就能正常跑起来了
碰到的琐碎问题:队列初始化
- UIApplicationMain里面除了初始化UI外,最核心的逻辑就是初始化在主进程里面跑的queue
- 最开始在主线程放的是死循环,所有的流程都不动
- 随后意识到不对,换成了libdispatch_init,但是导致程序直接崩溃
- 意识到这个是因为libdispatch库太底层了,少了objc封装,所以换成了NSRunLoop,但是还是不行
- 逆向UIApplicationMain的时候才发现里面是用的CFRunLoop
2. 让快贴持续检测剪贴板
问题:iOS的快贴app只会在打开的时候同步一次,不会一直持续同步更新
逆向:逆向的关键点显然是app怎么获取iOS剪贴板
- 随便找了几个iOS上的剪贴板越狱插件逆向了一下发现是UIPasteboard拿的,通过注册一个darwin notification来探测剪贴板变化
- 经过逆向,发现快贴在iPhone上面是通过notification探测,而在iPad上则是每隔0.3s扫描一个剪贴板检查是否发生变化
所以我们直接hook的快贴的检测方法
- [UIDevice isPad]
,就可以了3. 从快贴设置iOS剪贴板
问题:iOS上快贴app并不会自动设置剪贴板,而且也没有这个选项。
逆向:
- 虽然IDA里面看有autoSet相关的字样,但是hook之后并没有作用
- 初步逆向,快贴核心逻辑其实就在
ClipManager
类中: - 收到剪贴板更新时,mqtt消息经过DownloadFile请求下载好剪贴板数据后,会调用
historyDownloaded
回调 - 剪贴板的数据则会在
HistoryModel
中存储,通过用cycript插桩打印,可以看出来里面已经是有内容的了
- 然而,直接输出这些内容却发现并不全,只有30几个字符
- 我以为是数据库查询问题,故多次深入探究,并没有什么发现
- 经过逆向,发现实际上应该调用
HistoryModel
的content方法
至此,同步操作本身就成功了。然而,还有一堆坑。
4. 关闭屏幕,但让iOS持续同步iCloud剪贴板
问题:我们肯定不希望手机大晚上还亮着灯, 所以我们肯定希望手机屏幕能关闭。
然而iOS会经常拒绝同步iCloud的剪贴板:
- 将手机放置一会之后,iCloud剪贴板就停止同步了
- 锁屏也会停止同步
探究思路1:不锁屏,只黑屏
- 找了个老的tweak:Dimid,支持将屏幕锁成全黑
- 看了下关键调用,是
BKSDisplayServicesSetScreenBlanked
,在BackboardService
里面实现的,只是个简单rpc - 我试了下这个函数,屏幕关掉之后iCloud过一会一样停止同步了
- 说明肯定是有什么东西通知到了iCloud
探究思路2:是什么通知到了iCloud?
- 我脑子抽了没去看backboardd,去看了syslog,发现里面每次锁屏的时候都会有ReProvision的log,那说明他肯定是接收到通知的
- 发现这个通知在这里https://github.com/Matchstic/ReProvision/blob/de80d3234f0d2115bc8025c586df27b571f80cd4/Shared/Daemon/RPVDaemonListener.m#L406
- 有
didUIUnlockNotification
和backlightChanged
,我们既然已经绕过了锁屏,那么肯定就是backlightChanged了
探究思路3:只要能够阻止通知,就能防止iCloud停下来,尝试通过hook实现
- 根据这个通知,我才回过头来发现是在backboardd里面处理的(我一开始以为这个里面全是RPC stub)
- backboardd通过notify_post这个函数向外发送通知
- notify_post是个标准库
- 简单朴素,直接hook
- 一部分的app(包括ReProvision)确实收不到关屏幕的通知了
- 但是iCloud还是停止工作了
探究思路4:不是阻止通知,而是绕开通知
- 猜测:iCloud通过什么手段直接拿到了显示屏的状态,而不是通过检测notification来拿的状态
- 方案:所以,还是直接绕开通知这一层,到更底层去控制他的显示屏。
- 逆向:
- 通过关键字寻找相关函数,很明显,关屏幕这个操作是Blanked,搜索就可以找到
setBlanked:
这样一个方法,但是方法所在的类不知道是什么。 - 如果按照代码里的方法,取这个类会很麻烦。我意识到这个类显然是一个全局单例,因为在iphone上不插显示屏只可能会有一个屏幕,按照objc命名的习惯,我们直接取其中一个短词语在方法列表里搜索,果然找到了对应的方法
- 总结:
[[[CAWindowServer server] displayWithDisplayId:1] setBlanked:1]
5. 配置daemon,与jetsam斗智斗勇
能让iCloud息屏同步,自然之后是让快贴在后台作为守护进程持续运行。
然而系统默认只给daemon 6MB内存,超过就直接kill,所以我们需要绕开这个限制。
好在,绕开限制很简单,只要修改
/System/Library/LaunchDaemons/com.apple.jetsam-XXX
开头的文件就行了,我的是D21型号的。我一开始乖乖的只加了一两个daemon的配置进去,后来我daemon多了, 我直接一气之下把默认的限制拉到了128M,一劳永逸了
但是拉了这个设置之后经常开机SpringBoard启动不起来,原因不明,需要进到Checkra1n的ssh里面吧SpringBoard重启一下才行
6. 接着与iCloud同步斗智斗勇
我们绕开息屏通知让iCloud能持续工作好一会,但是过二十分钟左右,iCloud便又会停止工作。
分析:
- 到这个份上,屏幕已经不可能被iCloud检测到了,只可能是键盘动作、前台程序类型这样的小区别。
- 我懒得去分析他到底怎么控制了,我又新建了一个daemon
- 从julioverne的screendump13里面抄了控制键盘的代码,每隔1分钟去发送键盘的右Ctrl的按键消息
- 每小时将屏幕亮起再休眠一次。