苹果地图位置记录分析:深入解析MapsSync数据库技术

本文深入分析苹果MapsSync数据库的技术架构,详细解析SQLite数据库中的Z_ENT字段、Z_OPT字段含义,探讨路线记录在WAL文件中的存储机制,并通过实际案例展示如何通过数据库分析还原用户位置历史和行为模式。

DoubleBlak [苹果地图 - 访问过的位置?]

苹果地图 - 访问过的位置?

Ian Whiffin
发布于:2024年2月27日

这篇文章将涵盖MapsSync数据库的多个技术方面,包括位置数据和数据库结构。希望我的分析能够准确且有用。让我们开始吧。

MapsSync数据库

大多数人都知道MapsSync数据库,这是苹果地图背后的数据库。尽管它是一个相当标准的SQLite数据库,但其设计选择在其他数据库中并不常见。

你可能已经注意到,在许多iOS数据库中,都有一个Z_ENT字段(这个字段似乎从未为表增加太多价值),而且通常Z_ENT字段在每一行中都是相同的。

以knowledgeC为例,ZOBJECT表中的所有行都显示Z_ENT值为11。

在ZSTRUCTUREDMETADATA表中,所有行都显示Z_ENT值为15。

查看Z_PRIMARYKEY表,这些值开始变得有意义。

在这里可以看到,Z_ENT类型11的名称为"Event",这与ZOBJECT表中的项目相符。Z_ENT值15的名称为"StructuredMetaData",这也与STRUCTUREDMETADATA表中的项目相符。(最佳猜测是Z_ENT代表"ENTITY")。

回到MapsSync数据库,特别查看ZHISTORYITEM表,我们发现Z_ENT字段包含一系列值(15、18和20)。这与我们习惯的情况有很大不同。

这些实体名称在Z_PRIMARYKEY表中列出,让你知道正在查看什么项目。

现在我们知道了实体15是HistoryDirections,18是HistoryPlace,20是HistorySearch。这意味着我们可以过滤ZHISTORYITEM表,只显示Z_ENT值为20的记录来查看用户的搜索内容,或过滤Z_ENT值为18的记录来查看地点。

但本文将重点讨论HistoryDirectionsItem(类型15)。

考虑在iOS中创建路线的情景。你打开应用程序,搜索一个地点,然后请求前往该地点的路线。你会得到两个时间戳非常接近的结果:搜索和路线。

访问过的位置

关于方向历史的一个常见问题是:用户是否实际位于起点或终点位置?

终点位置可能对我们来说很有趣,因为这是用户感兴趣的地点。这是他们计划前往的地方。但这并不意味着他们实际遵循了这些方向或到达了目的地。

起点位置更有趣。回想我之前让你思考规划路线的情景,路线从哪里开始?我敢打赌,路线的起点是你规划路线时的实际位置。

在大多数情况下,这是一个安全的假设,但有可能在位置A时规划从位置B到位置C的路线,对我们来说做出假设可能是危险的。

经过彻底测试,我发现在每个从我当前位置规划路线的记录中,数据库中都有一个标志存在。而在每个从任意位置规划路线的案例中,该标志不存在。我以此作为判断用户是否在起点的依据。

我提到的标志可以在DirectionHistory行的ZROUTEREQUESTSTORAGE字段中找到。这是存储方向本身的protobuf blob。

我注意到每个从我当前位置规划的路线都有一个Node 100项目,但从任意位置规划的路线都没有。

这种逻辑在一段时间内对我来说工作得很好。直到2023年的CTF。

CTF - Abe

如果你参加了,你可能在Abe的设备上看到了这个工件:

在继续之前,我会透露部分内容并说明这不是错误的。只是有些奇怪,需要一些思考。

那么,这个记录有什么问题?嗯,有几个问题。

首先,起点时间比终点时间晚15分钟。 其次,这两个位置非常接近,我质疑为什么要进行这个搜索? 第三,即使忽略时间戳顺序错误的事实,它们仍然相隔近15分钟,距离只有约40米。

绿色显示起点。红色显示终点。

这根本说不通。 我检查了ZROUTEREQUESTSTORAGE blob,发现node 100存在。所以,根据我之前的逻辑,这意味着设备在规划路线时位于这个位置。

起点:3:37:12 终点:3:23:41

幸运的是,在这种情况下,缓存数据库仍然有数据,看起来像这样:

黑色箭头指向缓存数据库中下午3:23的设备位置。其余缓存位置显示为金色点,沿着道路向左上方移动,这是MapsSync数据所在的位置。

更有趣的是,当我在不包括WAL文件的情况下查看MapsSync数据库时,我得到了不同的起点位置,并且它也包含Node 100。

回顾概览地图,你可以看到这个位置非常接近旅程创建时缓存数据库中显示的位置。这当然更合理。

我还检查了时间戳的来源,发现了以下内容:

包括WAL 时间 | 字段 3:23 | ZCREATETIME 3:37 | ZMODIFICATIONTIME

不包括WAL 时间 | 字段 3:23 | ZCREATETIME 3:24 | ZMODIFICATIONTIME

所以立即可以看到ZModificationTime的差异,但ZCreateTime是相同的。

另一个值得注意的差异是Z_OPT字段,这个字段可能被忽视的程度远超应有。我们稍后将更详细地查看这个字段。

在这种情况下,没有WAL时,Z_OPT值为5,但有WAL时,Z_OPT值为15。

虽然我承认我不完全确定OPT代表什么,但我可以将其描述为显示记录被更新的次数。(可能OPT代表Option?Optimal?:耸肩)

我决定更详细地查看WAL,并寻找我能找到的该页面的所有其他版本。

在ArtEx中,我运行了高级SQL恢复。在显示结果之前,我想更深入地探讨它在做什么,以及如果高级恢复失败,如何手动执行此操作。

高级恢复与资源管理器

SQL和WAL资源管理器是查看高级恢复过程中发生情况的好方法。

你可以通过简单地打开感兴趣的数据库来找到SQL和WAL资源管理器。这里,我们将查看MapsSync数据库。

打开数据库后,你可以立即在顶部看到三个选项卡:SQL Viewer、SQL Explorer和WAL Explorer。

我可以使用SQL Viewer作为正常的SQL工具,找到我感兴趣的记录。在这种情况下,它是表中的最后一条记录:Z_PK = 36。

然后我可以移动到SQL Explorer选项卡,它将显示数据库头和第一页的内容。

SQL Explorer将解析页面头,并假设它是支持的页面类型(即不是溢出页面),将此单个页面的内容解析到窗口底部的表格视图中。

我可以使用左窗格中的箭头按钮一次向前或向后移动一页,或按下蓝色"Page"标签输入特定的页面或偏移量。

资源管理器独立处理每个页面。它不理解(或关心)数据库的其余部分,包括字段名称。因此所有字段都命名为Field 0、Field 1、Field 2等。

正如我之前指出的,我知道我感兴趣的是记录36,最终在数据库的第413页找到了它。你可以看到这里唯一的记录在Field 0中显示36。

在这种情况下,我只是滚动页面直到找到记录36,但如果我知道偏移量(例如通过使用十六进制搜索),我可以直接跳转到它。

请注意,此时SQL和WAL资源管理器没有时间戳转换或Blob查看器功能,但它仍然有用。

我们可以看到Field 2的值为5,由于我们已经知道Field 2是Z_OPT字段,我们知道这个记录是第5次更新。

现在我将跳转到WAL Explorer选项卡。

这显示了文件头、帧头和页面头,以及帧的十六进制视图和解析的页面(如果适用)。

在我们继续之前,我想非常简要地解释数据库,否则下一部分可能没有意义。

主数据库文件由PAGES组成。这些页面包含你在表中看到的数据行。

相比之下,WAL由FRAMES组成。每个FRAME包含一个PAGE和一个头。

如果你在没有WAL的情况下读取数据库,将显示活动页面(在我的例子中是页面413)。但如果你包括WAL,将显示页面的最新版本,但可能有多个版本。

回到ArtEx,左列的蓝色文本显示帧1(或360)包含页面100的副本。如果我点击"Page 100"标签,然后可以输入我感兴趣的页面号(即413),然后按下"Find All Page 413"。

ArtEx将花费一两分钟扫描WAL文件的每个帧,寻找页面413的所有实例,完成后,它找到了这个页面的9个实例。

这代表了WAL文件中页面413的9个版本。所以总的来说,我在WAL和活动数据库之间有10个页面413实例。

我可以点击列表中的任何结果跳转到特定的帧。

你可以看到,当我点击结果时,主帧更新,页面被解析,Field 2(Z_OPT)中的值增加。

不仅Field 2中的数据被更新。时间戳和blob中的值也被更新。

这基本上是ArtEx中高级恢复的工作方式。它独立解析每个页面/帧,然后查看数据库模式以查看是否可以找出数据属于哪里。

所以现在,为了让生活更轻松,我可以返回SQL Viewer,并按下右上角SQL Query输入上方的高级恢复按钮。

它会提示你是否要查看所有重复项,在这种情况下(以及大多数情况下),我们确实需要。

如果我对这个问题回答否,ArtEx仍然会恢复数据,但只包括具有唯一PK编号的记录,这不是我想在这里看到的。

通过显示重复项,应该显示每个记录的每个实例,我们现在可以看到找到了46条记录,比高级恢复运行前多了10条记录。

我们还可以看到11条记录的Z_PK值为36,Z_OPT值的范围从5到15。

如果我运行一个查询只查看Z_PK为36的记录的创建和修改时间,那么我可以看到尽管每个记录的创建日期保持不变,但修改日期每隔几分钟更新一次。

回顾

在我们继续之前回顾一下:

我们知道旅程创建于15:23:41。 我们知道这一点是因为在每个恢复的记录中看到的ZCREATETIME。 我们知道设备在15:23:41实际所在的位置,并且它远离显示的起点位置。 我们知道这一点是因为cache.sqlite记录。

我们知道起点位置在使用WAL与否的情况下是不同的。

没有WAL(Z_OPT = 5) 有WAL(Z_OPT = 15)

我们知道这些起点位置都不是设备在创建时间实际所在的位置。 尽管两个起点位置都有Node 100,我相信这表示起点位置是用户的实际位置。 我们知道设备在15:24:17(Z_OPT 5的修改时间)的位置 我们可以将其与cache.sqlite中设备的实际位置进行比较,看到它们匹配。

修改时间15:24:17的位置 cache.sqlite在15:24:17的位置

我们知道设备在15:37:50(Z_OPT 15的修改时间)的位置 我们可以将cache.sqlite中设备的实际位置与MapsSync blob中Z_OPT 15的起点位置进行比较。

修改时间15:37:50的位置 cache.sqlite在15:37:50的位置

我们还可以看到Z_OPT 14是相同的位置但时间略有不同,我们也可以将其与cache.sqlite进行比较。

修改时间15:37:13的位置 cache.sqlite在15:37:13的位置

那么发生了什么?我有一个假设但需要做一些测试。

假设

假设是:每当路线重新计算时,blob都会更新。即,每当设备注意到采取的方向不是建议的方向时。

所以,在15:23:21,当旅程创建时,设备位于地图上标记为项目A的位置,这可能被用作起点(尽管由于我们没有原始记录,这无法证明)。

在15:24:17,设备重新计算了路线,并使用用户的当前位置(位置B)作为新的起点。

在15:37:13,设备再次重新计算了路线,并再次使用设备的当前位置作为起点(位置C)。

最后,当位置结束时,修改时间更新为15:37:51,但路线blob不需要更新。

这意味着我对Node 100的理解仍然是正确的,并完美解释了为什么起点和终点如此接近,以及为什么起点时间晚于终点时间。

测试

我通过带着测试手机和笔记本电脑进行短途驾驶来测试这一点,每60秒拉取MapsSync数据库以供后续比较。 这是结果:

这张地图显示了重要元素。 起点显示为图像左下角的红色箭头。

我计划的原始路线要求我沿着地图左侧的橙色线向下行驶。 第一个绿色箭头是我第一次偏离计划路线,导致我的设备重新规划。这个新路线显示为深蓝色,建议我掉头重新加入橙色线。 这个更改保存为Z_OPT 5,并正确记录了我当时的位置作为路线起点。 第二个绿色箭头是我第二次偏离计划路线。这显示为Z_OPT 7,新的建议路线显示为红色。 第三和第四个绿点没有导致我在数据库中看到的记录。它们发生在非常接近的距离和时间内。 第五个绿色箭头是我结束测试前最后一次偏离路线。它保存为Z_OPT 11,显示为青色。 显然,我在这里没有恢复所有记录,我缺少记录6、8、9和10。 但我对以下内容感到满意:

记录定期更新,修改时间戳更新。 如果用户偏离他们绘制的路线,他们的当前位置将用于重新规划,并且blob将被更新。

当然,现在回头看,这完全合理。

总结

这是另一个表面看起来如此简单的工件。但当你开始深入研究时,你可以发现复杂的层次,理解它的工作方式可能帮助你找到额外的位置数据,理解哪些地图位置实际被访问过,以及如何解释奇怪的时间差异。 尽管最初在发现这个问题时撞头,我不得不承认,我喜欢这类问题,因为它提供了同时学习几个新事物的机会。希望这对你也有用。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计