最近随着 iOS 15 中「记录 App 活动」功能的加入,以微信为代表的一类软件频繁读写用户信息的行为被抓了现形。具体的新闻可以阅读 Hackl0us 发布在知乎的记录.
虽然微信在 10 月 11 日发布了新版似乎修复了这个问题,然而美团针对每 5 分钟获取一次用户定位的行为发布了公告:
美团 App 技术工程师就此前美团 APP ”频繁定位“回应:之所以出现这种情况,是因为这类软件在单方面读取系统操作日志后,进行了选择性展示,经测试,在相关权限开启且 App 后台仍处于活跃状态时,大部分主流 App 均会被该软件检测出频繁读取用户信息,且监测结果高度相似。
该工程师还表示,并未对上述读取 iOS 15 系统日志的软件进行安全性和保密性测试,建议大家谨慎下载。
hmmm,怎么说呢,就无话可说吧,既然有可能是这类检测软件的问题,那么我就排除软件的障碍,自己手动检测试试看。
首先在 iOS 15 设备进入「设置」-「隐私」–「记录 App 活动」,打开 App 活动开关,等待一定时间,iOS 会自动记录期间所有 App 活动,点击存储 App 活动即可导出为 ndjson
文件——声明:本操作系 iOS 自主记录日志,且用户绝对有权力单方面导出,此操作不涉及也没有办法选择性导出,更不具备展示功能。
这里的 ndjson
大致相当于一种流式 json 文件,可以通过 ndjson
包读取为 data.table 并转换为 tibble。
读取与预览
首先是读取 ndjson 文件,并预览
声明:下列读取 iOS 15 系统日志的操作并未进行安全性和保密性测试,建议大家谨慎操作,或者拔掉网线并开启电磁屏蔽操作(摊手。
library("ndjson")
library("pillar")
library("dplyr")
##
## Attaching package: 'dplyr'
## The following object is masked from 'package:pillar':
##
## dim_desc
## The following objects are masked from 'package:stats':
##
## filter, lag
## The following objects are masked from 'package:base':
##
## intersect, setdiff, setequal, union
library("lubridate")
##
## Attaching package: 'lubridate'
## The following objects are masked from 'package:base':
##
## date, intersect, setdiff, union
app_privacy_report_tb <-
ndjson::stream_in("resource/App_Privacy_Report_v4_2021-10-11T22_36_54.ndjson",
cls = "tbl")
glimpse(app_privacy_report_tb)
## Rows: 19,687
## Columns: 15
## $ accessor.identifier <chr> "com.xiaomi.mihome", "com.xiaomi.mihome", "com…
## $ accessor.identifierType <chr> "bundleID", "bundleID", "bundleID", "bundleID"…
## $ category <chr> "location", "location", "location", "location"…
## $ identifier <chr> "60E8004B-D969-4ABB-B83F-460663BCC29F", "60E80…
## $ kind <chr> "intervalBegin", "intervalEnd", "intervalBegin…
## $ timeStamp <chr> "2021-10-08T13:30:43.340+08:00", "2021-10-08T1…
## $ type <chr> "access", "access", "access", "access", "acces…
## $ bundleID <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
## $ context <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
## $ domain <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
## $ domainOwner <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
## $ domainType <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
## $ firstTimeStamp <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
## $ hits <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
## $ initiatedType <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA…
head(app_privacy_report_tb)
## # A tibble: 6 × 15
## accessor.identif… accessor.identif… category identifier kind timeStamp type
## <chr> <chr> <chr> <chr> <chr> <chr> <chr>
## 1 com.xiaomi.mihome bundleID location 60E8004B-D… inte… 2021-10-… acce…
## 2 com.xiaomi.mihome bundleID location 60E8004B-D… inte… 2021-10-… acce…
## 3 com.xiaomi.mihome bundleID location 865B785C-D… inte… 2021-10-… acce…
## 4 com.xiaomi.mihome bundleID location 865B785C-D… inte… 2021-10-… acce…
## 5 com.xiaomi.mihome bundleID location 7626338D-D… inte… 2021-10-… acce…
## 6 com.xiaomi.mihome bundleID location 7626338D-D… inte… 2021-10-… acce…
## # … with 8 more variables: bundleID <chr>, context <chr>, domain <chr>,
## # domainOwner <chr>, domainType <dbl>, firstTimeStamp <chr>, hits <dbl>,
## # initiatedType <chr>
三天记录下所有 App 总共有 19687 条隐私请求,不过由于记录是成对出现的,即开始请求——请求结束,所以说请求次数只有一半9843.5呃,怎么说呢,就还挺勤劳的吧。
我们首先关注 accessor.identifier (App ID), category (访问分类), kind(时间戳类型),timeStamp (时间戳), type (大类) 这几列,选择上述列并设置为合适的数据类型。
main_tb <-
app_privacy_report_tb %>%
select(accessor.identifier, category, kind, type, timeStamp) %>%
mutate(
`accessor.identifier` = as.factor(accessor.identifier),
category = as.factor(category),
kind = as.factor(kind),
type = as.factor(type),
timeStamp = ymd_hms(timeStamp)
)
head(main_tb)
## # A tibble: 6 × 5
## accessor.identifier category kind type timeStamp
## <fct> <fct> <fct> <fct> <dttm>
## 1 com.xiaomi.mihome location intervalBegin access 2021-10-08 05:30:43
## 2 com.xiaomi.mihome location intervalEnd access 2021-10-08 05:34:21
## 3 com.xiaomi.mihome location intervalBegin access 2021-10-08 05:34:24
## 4 com.xiaomi.mihome location intervalEnd access 2021-10-08 05:34:53
## 5 com.xiaomi.mihome location intervalBegin access 2021-10-08 05:34:58
## 6 com.xiaomi.mihome location intervalEnd access 2021-10-08 05:35:08
skimr::skim(main_tb)
Name | main_tb |
Number of rows | 19687 |
Number of columns | 5 |
_______________________ | |
Column type frequency: | |
factor | 4 |
POSIXct | 1 |
________________________ | |
Group variables | None |
Variable type: factor
skim_variable | n_missing | complete_rate | ordered | n_unique | top_counts |
---|---|---|---|---|---|
accessor.identifier | 9121 | 0.54 | FALSE | 26 | com: 9062, com: 236, com: 228, com: 176 |
category | 9121 | 0.54 | FALSE | 4 | loc: 9940, con: 290, pho: 288, cam: 48 |
kind | 9121 | 0.54 | FALSE | 2 | int: 5283, int: 5283 |
type | 0 | 1.00 | FALSE | 2 | acc: 10566, net: 9121 |
Variable type: POSIXct
skim_variable | n_missing | complete_rate | min | max | median | n_unique |
---|---|---|---|---|---|---|
timeStamp | 0 | 1 | 2021-10-08 05:30:41 | 2021-10-11 14:36:29 | 2021-10-09 02:33:52 | 19671 |
在这三天里面,总共有 26 款 App 请求了我的隐私数据(共安装 130 款 App),那么平均下来一款 App 就请求了 378.5961538 次,然而这怎么可能嘛!必然是有更勤劳的小蜜蜂。
数据可视化
library(forcats)
library(ggplot2)
main_tb %>%
group_by(accessor.identifier) %>%
count() %>%
ungroup() %>%
mutate(
accessor.identifier = fct_reorder(accessor.identifier, n)
) %>%
ggplot(aes(
x = accessor.identifier,
y = n
)) +
geom_col() +
coord_flip()
简单的排序后发现三个有趣的点:
- 在 accessor.identifier 中 com.dianping (美团点评)并不是非常显眼,这可能是因为我已经关闭了大众点评和美团等 App 的后台定位权限相关;
- com.xiaomi.mihome 这位小老弟还是挺疯狂,盲猜是因为我开启了它的后台定位所致,然而我并没有开启地理围栏等相关智能化脚本,这个稍后再看;
- NA 太多了,也就是说并非所有的操作都有 accessor.identifier?后续再看。
根据 main_tb 的预览, 与 App 身份认证相关的列还有 accessor.identifierType 以及 identifier
app_privacy_report_tb %>%
select(accessor.identifier,
accessor.identifierType) %>%
mutate(across(everything(), as.factor)) %>%
unique() %>%
print(., n = nrow(.))
## # A tibble: 27 × 2
## accessor.identifier accessor.identifierType
## <fct> <fct>
## 1 com.xiaomi.mihome bundleID
## 2 com.TickTick.task bundleID
## 3 com.autonavi.amap bundleID
## 4 io.robbie.HomeAssistant bundleID
## 5 com.google.photos bundleID
## 6 com.cainiao.cnwireless bundleID
## 7 com.taobao.taobao4iphone bundleID
## 8 com.lifubing.lbs.stepOfLife bundleID
## 9 com.tencent.xin bundleID
## 10 ph.telegra.Telegraph bundleID
## 11 com.taobao.fleamarket bundleID
## 12 com.readdle.smartemail bundleID
## 13 com.tencent.tim bundleID
## 14 com.360buy.jdmobile bundleID
## 15 com.xiaomi.mishop bundleID
## 16 com.alipay.iphoneclient bundleID
## 17 com.wdk.hmxs bundleID
## 18 com.xiaomi.miwatch.pro bundleID
## 19 com.heweather.weatherapp bundleID
## 20 tv.danmaku.bilianime bundleID
## 21 com.atebits.Tweetie2 bundleID
## 22 com.dianping.dpscope bundleID
## 23 com.johnil.vvebo bundleID
## 24 com.tmri.12123 bundleID
## 25 cn.mucang.ios.jiakaobaodianhuoche bundleID
## 26 com.readdle.Scanner bundleID
## 27 <NA> <NA>
数据整形与预览
还是存在 NA
,后面发现除通过 accessor.identifier 标记的是除网络访问之外的记录,bundleID 列是记录的 App 的网络链接请求,那就对数据帧变形。
library(tidyr)
app_privacy_report_meld_tb <-
app_privacy_report_tb %>%
pivot_longer(cols = c(accessor.identifier, bundleID),
names_to = "id_type",
values_to = "app_id"
) %>%
filter(!is.na(app_id)) %>%
select(-id_type) %>%
select(app_id, everything())
meld_tb <-
app_privacy_report_meld_tb %>%
select(app_id, category, kind, type, timeStamp) %>%
mutate(
app_id = as.factor(app_id),
category = as.factor(category),
kind = as.factor(kind),
type = as.factor(type),
timeStamp = ymd_hms(timeStamp)
)
head(meld_tb)
## # A tibble: 6 × 5
## app_id category kind type timeStamp
## <fct> <fct> <fct> <fct> <dttm>
## 1 com.xiaomi.mihome location intervalBegin access 2021-10-08 05:30:43
## 2 com.xiaomi.mihome location intervalEnd access 2021-10-08 05:34:21
## 3 com.xiaomi.mihome location intervalBegin access 2021-10-08 05:34:24
## 4 com.xiaomi.mihome location intervalEnd access 2021-10-08 05:34:53
## 5 com.xiaomi.mihome location intervalBegin access 2021-10-08 05:34:58
## 6 com.xiaomi.mihome location intervalEnd access 2021-10-08 05:35:08
skimr::skim(meld_tb)
Name | meld_tb |
Number of rows | 19687 |
Number of columns | 5 |
_______________________ | |
Column type frequency: | |
factor | 4 |
POSIXct | 1 |
________________________ | |
Group variables | None |
Variable type: factor
skim_variable | n_missing | complete_rate | ordered | n_unique | top_counts |
---|---|---|---|---|---|
app_id | 0 | 1.00 | FALSE | 85 | com: 9159, com: 4140, com: 2518, com: 429 |
category | 9121 | 0.54 | FALSE | 4 | loc: 9940, con: 290, pho: 288, cam: 48 |
kind | 9121 | 0.54 | FALSE | 2 | int: 5283, int: 5283 |
type | 0 | 1.00 | FALSE | 2 | acc: 10566, net: 9121 |
Variable type: POSIXct
skim_variable | n_missing | complete_rate | min | max | median | n_unique |
---|---|---|---|---|---|---|
timeStamp | 0 | 1 | 2021-10-08 05:30:41 | 2021-10-11 14:36:29 | 2021-10-09 02:33:52 | 19671 |
经过整形的数据再看,发现总共有 85 款 App 请求了隐私数据,那么平均下来一款 App 就请求了 115.8058824 次。
第二次数据可视化
在 App Privacy Report 中,type 大类分为了两种 access, networkActivity, 对于 access 分类,在 category 中又有子分类 location, photos, contacts, camera。为了方便可视化,我们首先对这个部分进行更进一步整形,将网络请求补充到 category 中,然后进行可视化。
cat_to_type_tb <-
meld_tb %>%
mutate(
# unfactor columns to avoid level missing
category = as.character(category),
type = as.character(type)) %>%
mutate(
# Do not use `ifelse`,
# it does not support vectorization operation
category = if_else(is.na(category),
type,
category)) %>%
mutate(
category = as.factor(category),
type = as.factor(type)
)
app_n_count_tb <-
cat_to_type_tb %>%
group_by(app_id) %>%
mutate(count = n()) %>%
ungroup() %>%
mutate(
app_id = fct_reorder(app_id, count)
)
app_n_count_tb %>%
ggplot(aes(
x = app_id,
y = count
)) +
geom_col(aes(fill = type)) +
coord_flip()
app_n_count_tb %>%
ggplot(aes(
x = app_id,
y = count
)) +
geom_col(aes(fill = category)) +
coord_flip()
由于 App 数量太多,而且 Bundle ID 还存在 com.apple.corelocation.CoreLocationVanilaWhenInUseAuthPromptPlugin 这种龙傲天般的命名,上面两张图的视觉效果还有很大优化空间,然而这已经足够确定一个问题了:米家 App 靠实力诠释了一骑绝尘是什么。且根据第二站图,可以发现米家似乎绝大多数请求都用在 location 定位上。
library(showtext)
## Loading required package: sysfonts
## Loading required package: showtextdb
showtext::showtext_auto()
cat_to_type_tb %>%
filter(app_id == "com.xiaomi.mihome") %>%
group_by(category) %>%
count() %>%
ggplot(aes(x = "", y = n, fill = category)) +
geom_bar(stat = "identity", width = 1, color="white") +
coord_polar("y", start = 0) +
theme_void() +
labs(title = "米家权限请求分布")
mj_time_begin_end_tb <-
cat_to_type_tb %>%
filter(app_id == "com.xiaomi.mihome") %>%
filter(category == "location") %>%
select(kind, timeStamp, category) %>%
# 此处实现很奇怪,应该可以用 pivot_wider 一步到位的
pivot_wider(
names_from = kind,
values_from = timeStamp
)
## Warning: Values are not uniquely identified; output will contain list-cols.
## * Use `values_fn = list` to suppress this warning.
## * Use `values_fn = length` to identify where the duplicates arise
## * Use `values_fn = {summary_fun}` to summarise duplicates
mj_time_tb <-
tibble(
begin = mj_time_begin_end_tb$intervalEnd[[1]],
duration = mj_time_begin_end_tb$intervalEnd[[1]] - mj_time_begin_end_tb$intervalBegin[[1]]
)
mj_time_tb %>%
mutate(
hour_of_day =
hour(begin)
) %>%
group_by(hour_of_day) %>%
count() %>%
ggplot(
aes(x = hour_of_day,
y = n)
) +
geom_line() +
scale_x_continuous(name = "时间", limits = c(0,24), expand = c(0,0)) +
scale_y_continuous(name = "请求次数") +
theme_linedraw()
总的来说,米家主要请求的因素数据就是定位,并且是夜以继日的工作,可以说是007了。只有在凌晨才舍得勉强克制一点。再结合 iOS 电池选项厘米啊米家出色的耗电量,应该没有冤枉。
关键是笔者米家中需要用到定位的自动化并没有开启(因为从来就没有按照预期正常工作过),所以勤劳的定位请求真的让人头大。
目前笔者的解决办法是关闭 App 的始终定位功能,只能从根源上解决问题,关键是对于使用影响非常有限(依赖地理围栏功能的小伙伴慎重考虑)
总结
总的来说,根据使用 R 分析 App_Privacy_Report 报告,笔者并未发现美团与微信的频繁访问,不过这并不能说明它们没有问题,因为我已经关掉了此二者的后台刷新以及始终定位功能,使得他两个没有办法实现频繁唤醒;不过让人意外的是,无意中发现了潜在的耗电大户,还是希望能克制一点。
声明:本文采用 R 语言是为境外团队 R-Core Team 开发软件;ndjson 包是境外冰岛开发者开发;tidyverse bundle 是新西兰开发者牵头开发;下列读取 iOS 15 系统日志的操作并未进行安全性和保密性测试,建议大家谨慎操作,或者拔掉网线并开启电磁屏蔽操作;不过写作的人是境内人员(战术后仰
米家版本为v6.11.201-build6.11.201.2
欢迎通过邮箱,微博, Twitter以及知乎与我联系。也欢迎关注我的博客。如果能对我的 Github 感兴趣,就再欢迎不过啦!