快贴 - 全自动iOS-Win同步剪贴板

大类
Util
iOS
技术标签
开发-HookPatch-iOS
逆向-iOS
环境增强
优先级
Medium
开始日期
Nov 4, 2022
状态
Maintaining
Public
Public
最后更新
Nov 7, 2022

项目地址

为什么要这个东西

我以前是怎么传剪贴板的

  • 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🤔

  1. iOS设备之间的传输 —— 稳定
  1. Android-Win之间传输 —— 稳定
 
可以看出:Android & Win —断开的— iOS - Mac
所以为了在所有的平台上全自动同步,我们重点是需要搞一个从iOS里把剪贴板全自动同步(系统级剪贴板)的东西
 
 

细化:iOS到Windows怎么同步

  • 同步困难1:网络:
    • 肯定是不能指望着直连:
      • 手机 - 没公网
      • 电脑 - 时不时有公网
    • 自建服务器:麻烦、不稳定
    • 快贴自带服务器,解决了这个问题
  • 同步困难2:让iOS实时在线取剪贴板
      1. iOS没后台:咋能让快贴一直运行?
      1. 手机电池有限:一直运行,手机贼耗电,肯定不能在主力机上面用
    • iOS生态封闭,逆向iCloud协议得不偿失,肯定选择用越狱设备辅助
      • 需要实现iOS的Daemon
  • 同步困难3:双向同步
    • 既要从服务器收到剪贴板,也需要自动把别的设备的将剪贴板发上去
    • 需要逆向快贴和iOS剪贴板的机制
 

最终方案:iCloud + 快贴

路径(双向):iOS iCloud 越狱iOS快贴Windows
  • 从iOS设备拷贝,经过越狱iOS设备后,通过快贴发给windows
  • 从Windows拷贝,通过快贴发给越狱iOS设备,再通过iCloud传给iOS设备

实现

考虑:怎么利用快贴

方案1:逆向快贴的协议,自己写客户端
  • 需要手动处理登录等问题
  • 需要重新编写大量代码,维护性堪忧
    • 尤其是端对端加密的部分,重写比较麻烦
方案2:将快贴客户端作为dylib来加载
  • 但是这样我们和客户端并不在同一个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?
 
探究思路3:只要能够阻止通知,就能防止iCloud停下来,尝试通过hook实现
notion image
  • 根据这个通知,我才回过头来发现是在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的按键消息
      • 每小时将屏幕亮起再休眠一次。
     

    至此iCloud云同步就完全正常,稳定工作了!