逆向工程 Find My People 跟踪 ̶我̶的̶前̶任̶ 一个朋友,因为我可以
摘要
本文描述了在 Linux 上逆向工程 Apple 的 Find My People 功能,以创建带有 Discord 通知的地理围栏自动化的过程,详细说明了所涉及的技术障碍和方法。
<p><a href="https://lobste.rs/s/ope7za/reverse_engineering_find_my_people_stalk">评论</a></p>
查看缓存全文
缓存时间: 2026/08/20 10:41
# 逆向工程“查找我的朋友”功能以追踪̶我̶前̶任̶一位朋友,因为我能
来源:https://zerotistic.blog/posts/find-my-people-linux/
一如既往,我又感到无聊了。我想研究一个复杂的系统,却毫无头绪。我和一位朋友一直通过苹果的“查找”功能互相分享位置。我问他是否介意我将它接入一些无聊的自动化流程。他说可以,于是计划在他常去的几个地点周围设置地理围栏,并在Discord上通知他何时到达或离开。
*完全准确且未经编造的私信(感谢 es3n1n (https://blog.es3n1n.eu/) 提供界面灵感)*
zerotisticToday at 18:42 嗨,愚蠢的问题:你介意我用你分享给我的查找位置信息搞个Linux自动化小工具吗?主要是设置地理围栏,以及在你到达或离开地点时通过Discord发消息。
LymdunToday at 18:43 **zerotistic**你介意我用...当然,哈哈,去吧,只是别公布我住哪儿就行💀
18:43成交🤝。我之前已经有通过游戏活动做同样事情的微型Steam追踪器了,所以我以为查找功能会是另一个带认证的请求、一些JSON,以及一个晚上的工作量*(史诗级伏笔)*。
我从普通的iCloud Web API开始,它很乐意返回我自己的苹果设备及其位置,但“查找我的朋友”功能无影无踪。我以为自己很聪明,去研究别人是怎么做的。结果发现事情并不简单,因为似乎以前没人这么做过(或者说,没完全做过,你一会儿就明白了)。
我没有Mac来运行或分析`FindMy.app`,所以我从现有的开源客户端和针对苹果的请求开始。后来,当猜字段名不再有趣时,我还研究了`fmfd`、`findmylocated`和`searchpartyd`的反编译。每当涉及一个开源客户端时,我会给出确切的来源链接。循环主要是:保持会话固定,改变一个字段或编码,然后看苹果的状态码是否变化。
警告(范围)
该客户端仅读取我的苹果账户上已接受的分享。它没有发送邀请、更改分享、添加家庭成员、在苹果上创建地理围栏或执行设备操作的方法。地理围栏功能是在解密后本地进行的。
## 从朋友API开始
https://zerotistic.blog/posts/find-my-people-linux/#starting-with-the-friends-api
第一个看起来有用的是`fmfd`使用的一个旧的`initClient`调用:
```
/fmipservice/friends/fmfd///initClient
```
背景是,`fmfd`是“查找我的朋友”守护进程。尽管现在应用叫“查找”,这个API仍然存在于苹果旧的MobileMe命名空间下。URL中的`dsid`只是账户的数字目录服务ID。
登录iCloud后,我得到了一堆针对不同服务的MobileMe令牌。其中几个听起来很对,所以我试了最明显的几个:
```
mmeFMFAppToken:401
mmeAuthToken:401
searchPartyToken:401
```
三个令牌,三个`401`。查看MobileMe委托交换(https://github.com/malmeloo/FindMy.py/blob/v0.10.1/findmy/reports/account.py#L891-L929)让原因很清楚:登录充当令牌经纪人,并为每个iCloud服务提供自己的凭证。显然,名称里有“查找”还不够。即使有了正确的令牌,请求仍然不完整。`initClient`需要账户的“查找我的朋友”(FMF)主机、用于此客户端Apple推送通知服务(APNs)连接的信使令牌、一个相当详细的客户端上下文,以及一捆绑称为**anisette**的奇怪头信息。信使令牌将此客户端标识给APNs,用于过滤推送的主题令牌源自它。Anisette基本上是额外证明请求来自一个预配置的类苹果客户端。一些值绑定到模拟的机器,另一个看起来像一个短期的一次性代码。它不是密码也不是查找令牌,但苹果没有它就会拒绝请求。FindMy.py在这里生成这些头信息(https://github.com/malmeloo/FindMy.py/blob/v0.10.1/findmy/reports/anisette.py#L128-L166)并在这里保存预配置的身份(https://github.com/malmeloo/FindMy.py/blob/v0.10.1/findmy/reports/anisette.py#L288-L414)。下面的`deviceUDID`只是我分配给模拟接收器的唯一设备标识符(UDID)。
```
{
"clientContext": {
"appName": "findmylocated",
"apsToken": "",
"callerHandleId": "",
"contextBundleApp": "com.apple.findmy.findmylocated",
"currentTime": 1787130000000,
"deviceUDID": "",
"productType": "MacBookPro18,3",
"osVersion": "14.6"
},
"serverContext": {
"authToken": "",
"clientId": "",
"prsId": ""
}
}
```
路由仍然是`friends/fmfd`,但当前的客户端在`findmylocated`中构建该上下文。`currentTime`是Unix毫秒数,后续刷新也会回应苹果在`X-FMF-Model-Version`中返回的型号版本。一旦我让所有这些都与原生守护进程发送的内容匹配,苹果终于返回了已接受的分享:
```
{
"following": [{
"id": "",
"invitationAcceptedHandles": [""],
"secureLocationsCapable": true,
"fallbackToLegacyAllowed": false
}],
"locations": []
}
```
`following`基本上是与我分享位置的人的列表。很好,我现在可以找到我的朋友并获取他不透明的`fmId`,但也就仅此而已:没有位置,也没有密钥。两个标志`secureLocationsCapable: true`和`fallbackToLegacyAllowed: false`是一个很好的迹象,表明该分享已转移到新的加密位置路径。起初我以为得到这个响应意味着客户端设置正确,但显然不是。那些无聊的上下文字段后来才变得重要,当我需要苹果注意到这是一个新客户端并向它发送现有密钥时。因此,我保留了`initClient`来查找关系,然后转向假装成一个真正的苹果设备,因为正常人都是这么做的。
## 成为一个IDS设备
https://zerotistic.blog/posts/find-my-people-linux/#becoming-an-ids-device
找到关系确实是简单的部分。加密路径经过IDS,苹果的私有设备身份和加密消息传递层。IDS将账户句柄与其注册的设备、证书、推送令牌和消息密钥联系起来;实际的数据包通过APNs传输。我的浏览器会话证明我已登录,但它并没有让Linux客户端成为那些设备之一。
我在一个重写前的pypush提交(https://github.com/JJTech0130/pypush/tree/6336bba82697b838497ffd774f23e5640c877a85)中找到了大部分旧的IDS和APNs代码。它的登录直接将密码plist发送到`profile.ess.apple.com`。苹果接受了密码,要求进行双因素认证,然后拒绝了验证码:
```
[password] Apple status 5000
[2fa] Apple status 5068
```
所以旧的pypush登录已经失效了。可行的路径是**GrandSlam**(https://theapplewiki.com/wiki/Grand_Slam_Authentication),苹果的账户登录协议,FindMy.py已经实现了(https://github.com/malmeloo/FindMy.py/blob/v0.10.1/findmy/reports/account.py#L794-L885)。经过安全远程密码(SRP)交换和双因素认证(2FA)后,我得到了一个ADSID(另一个用于已认证账户的不透明标识符)和一个短期的等效密码令牌(PET)。我使用该PET请求`signin/v2`以获取`com.apple.private.ids`委托,而不是再次尝试密码。之前的anisette值也随该登录一起传递。这些是有趣的值:
```
X-Apple-I-MD:
X-Apple-I-MD-M:
X-Apple-I-MD-RINFO:
X-Mme-Device-Id:
X-Apple-I-Client-Time: 2026-08-18T...
ZX-Mme-Client-Info:
```
然后我将PET发送到`setup.icloud.com/setup/signin/v2/login`:
```
POST /setup/signin/v2/login HTTP/1.1
Authorization: Basic :
X-Apple-ADSID:
X-Apple-I-MD:
X-Apple-I-MD-M:
X-Mme-Nas-Qualify:
Content-Type: application/x-apple-plist
delegates
com.apple.private.ids
protocol-version
4
```
那里有一个额外的东西:`X-Mme-Nas-Qualify`。它包含短期的本机验证数据。开源实现称该数据块为NAC;这个名字不如苹果期望它与模拟硬件配置文件匹配且是新鲜的这一事实有用。pypush生成器(https://github.com/JJTech0130/pypush/blob/6336bba82697b838497ffd774f23e5640c877a85/generatenac.py)构建它,苹果在注册过程中要求提供另一个。苹果对外部请求和`com.apple.private.ids`都返回了零,加上一个配置文件ID和委托令牌。有进展了!
### 证书请求
https://zerotistic.blog/posts/find-my-people-linux/#certificate-request
接下来,我必须通过`authenticateDS`将该委托令牌交换为IDS认证证书。这意味着发送一个证书签名请求(CSR),本质上是请求苹果签署此客户端的公共密钥。恼人的是,每次尝试都返回HTTP`200`,因为苹果将真正的结果隐藏在响应plist中。从那时起,改变一件事、重新运行,然后仍然得到`6001`,就是很多这样的过程:
```
旧密码凭证plist → 5068
GrandSlam + RSA/SHA-256 CSR → 6001
GrandSlam + RSA/SHA-1 CSR → 6001
RSA/SHA-1 + XML plist + gzip → 0, 返回证书
```
错误正文也没有什么帮助:
```
message <[插入苹果诊断信息]> status 6001
```
最终起作用的方法相当具体:CSR必须使用PKCS#10(标准的证书请求格式),带有2048位RSA密钥和SHA-1签名。其公共名称(common name)必须是IDS配置文件ID的大写SHA-1哈希值。然后我必须将其放在一个**XML** plist中作为`Data`,并压缩整个内容:
```
body = plistlib.dumps({
"authentication-data": {"auth-token": delegate_token},
"csr": csr_der,
"realm-user-id": profile_id,
}, fmt=plistlib.FMT_XML)
body = gzip.compress(body, mtime=0)
```
最终我手动构建了PKCS#10对象,以便我能控制确切的字节,包括空的属性字段和`sha1WithRSAEncryption`标识符:
```
common_name = hashlib.sha1(user_id.encode()).hexdigest().upper()
subject = x509.Name([
x509.NameAttribute(NameOID.COMMON_NAME, common_name)
]).public_bytes()
request_info = _der(
0x30, b"\x02\x01\x00" + subject + public_key_der + b"\xa0\x00"
)
signature = private_key.sign(request_info, padding.PKCS1v15(), hashes.SHA1())
SHA1_WITH_RSA = bytes.fromhex("300d06092a864886f70d0101050500")
csr = _der(
0x30, request_info + SHA1_WITH_RSA + _der(0x03, b"\x00" + signature)
)
```
```
[auth-cert] 准备 XML+gzip 注册:CSR=636 字节,正文=1613 字节
[IDS 认证证书] HTTP 200;解码苹果属性列表
[IDS 认证证书] 苹果响应字段:cert, status, user-id
[IDS 认证证书] 苹果诊断状态=0
```
我最好的猜测是,SHA-1和XML要求只是旧兼容性包袱。`authenticateDS`是一个旧的配置文件注册端点,接受的请求仍然宣传IDS协议`1660`。端点本身来自苹果签名的IDS包。我在这里遇到了另一个愚蠢的问题:它的一些主机链通过了Linux`certifi`包中缺失的苹果根证书。我从Apple PKI(https://www.apple.com/certificateauthority/)添加了Apple Root CA和Apple Root CA G3的指纹固定副本,并保持TLS验证启用。
## 让“查找”注册被接受
https://zerotistic.blog/posts/find-my-people-linux/#getting-find-my-registration-accepted
拥有认证证书意味着我终于可以签署IDS请求了。我用它和APNs身份请求账户的已注册句柄。苹果返回了两个可用的URI,尽管两个在外部响应为零的同时状态都是`5051`:
```
[IDS 句柄查找] 苹果诊断信息 handles[0].status=5051
[IDS 句柄查找] 苹果诊断信息 handles[1].status=5051
[IDS 句柄查找] 苹果诊断信息 status=0
[handles] 收到 2 个句柄
```
我忽略了内部的`5051`,因为苹果仍然给了我句柄,并尝试注册设备。又是`6001`。rustpush的服务定义(https://github.com/stek29/rustpush/blob/82dc6f8342176e4f9171b62354cbdd13eaa18b38/src/findmy.rs#L26-L45)显示了我哪里出错了。我是在直接注册FMF,但苹果注册一个alloy多路复用器,并在其下放置六个查找主题:
```
service = {
"service": "com.apple.private.alloy.multiplex1",
"sub-services": [
"com.apple.private.alloy.fmf",
"com.apple.private.alloy.fmd",
"com.apple.private.alloy.status.keysharing",
"com.apple.private.alloy.status.personal",
"com.apple.private.alloy.findmy.itemsharing-crossaccount",
"com.apple.private.alloy.kcsharing.invite",
],
"users": [{
"user-id": profile_id,
"uris": account_handles,
"client-data": find_my_capabilities,
"kt-loggable-data": ngm_identity,
}],
}
```
`client-data`是另一个兔子洞。它宣传了旧的IDS消息身份和**NGM** v13,这是苹果新的设备间消息格式,基于P-256设备密钥和签名预密钥构建。NGM是稍后将承载位置密钥的信封;它不是用于位置报告本身的加密。注册还包括Key Transparency v5元数据和查找功能标志。rustpush的注册代码(https://github.com/stek29/rustpush/blob/82dc6f8342176e4f9171b62354cbdd13eaa18b38/src/ids/user.rs#L1035-L1074)是我找到的最易读的参考。围绕它,请求需要APNs令牌、一些本机设备元数据以及另一个新鲜的验证数据块。编码再次重要:XML plist、gzip,然后是IDS对压缩字节的签名。最终的请求携带了账户证书和APNs推送证书:
```
POST HTTP/1.1
Content-Type: application/x-apple-plist
Content-Encoding: gzip
x-protocol-version: 1660
x-auth-user-id-0:
x-auth-cert-0:
x-auth-nonce-0:
x-auth-sig-0:
x-push-cert:
x-push-token:
x-push-sig:
```
签名也不是基于某些正常的HTTP规范化。苹果想要的是随机数、bag密钥(`id-register`)、查询字符串、压缩正文和推送令牌的二进制连接,每个变量字段前都加一个四字节长度前缀。pypush在这里有确切的例程(https://github.com/JJTech0130/pypush/blob/6336bba82697b838497ffd774f23e5640c877a85/ids/signing.py#L40-L110)。一个签名使用IDS密钥,另一个使用APNs推送密钥,因此注册与两个身份都绑定。我希望能有一个“这个字段解决了问题”的清晰答案,但我同时更改了布局、服务列表和编码。我没有回溯并二分查找到底是哪一个最终让苹果满意了。
```
[registration] 准备 XML+gzip 正文=1911 字节;服务=1,子服务=6,句柄=2
[IDS 设备注册] 苹果诊断信息 services[0].status=0
[IDS 设备注册] 苹果诊断信息 status=0
[registration] 查找服务 苹果状态 0
[registration] 收到注册证书
```
那时,Linux客户端终于拥有了作为查找设备存在所需的一切:APNs身份、IDS认证证书、已注册的句柄、消息密钥和查找服务证书。我保存了所有这些,以便无需再次登录就能重新启动,老实说,我那时已经开始担心了。听起来可能很傻,但那时我登录(或尝试登录)了很多次,我不确定苹果的安全功能是否会启动并锁定我的账户。
## 监听查找消息
https://zerotistic.blog/posts/find-my-people-linux/#listening-for-for-my-messages
注册完成后,我可以继续监听一些东西了。客户端连接到私有APNs,并宣布对六个查找子服务、SearchParty容器主题**以及**`com.apple.private.ids`的兴趣。每个推送包含一个IDS plist,其中有命令、发送者句柄、发送者推送令牌、加密模式和有效载荷。私有APNs不是普通应用服务器使用的公共API。我必须激活设备证书,打开pypush的`APNSConnection`(https://github.com/JJTech0130/pypush/blob/6336bba82697b838497ffd774f)实现的二进制连接
相似文章
黑客通过劫持儿童智能手表跟踪我
《连线》记者安迪·格林伯格演示了安全研究人员如何劫持一款廉价的儿童智能手表来跟踪他,揭示了基于同一平台构建的数十款GPS追踪设备存在的漏洞。
协议窥探:AirDrop 与 Quick Share 的漏洞研究
本文首次对苹果 AirDrop 和安卓 Quick Share 进行了跨平台逆向工程与协议感知模糊测试研究,揭示了这些广泛使用的近场传输协议中存在的六个漏洞。
@igus_ai: Apple 花费了5年时间构建保护其操作系统的防护措施。三名研究人员仅用6天就绕过了它,使用了…
三名安全研究人员使用 AI 模型 Claude Mythos 在六天内绕过了 Apple 的 MIE 保护,展示了一种罕见的利用方式,该方式无需传统恶意软件即可在 macOS 上获取 root 权限。完整论文将在 Apple 发布补丁后公布。
@leaf_sanren: https://x.com/leaf_sanren/status/2073069608437764266
作者详细介绍了如何通过逆向工程破解微信Mac版的加密系统,使用Frida动态插桩捕获系统函数CCKeyDerivationPBKDF生成的密钥,从而解密29个加密数据库,实现了自动获取群聊记录的目标,并计划开源该项目。
@TheAhmadOsman:疯狂 DSv4 Flash 发现并起草邮件披露泄露的私钥 - 在局域网上逆向工程 - 48 个 UDP 数据 …
Ahmad Osman 使用 DeepSeek v4 Flash 和 Qwen 3.8 27B 逆向工程专有猫喂食器,识别泄露的私钥,并将其移出云端,保持完整功能对等。