@necessarysdk/react-native 0.0.0-stage → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/LICENSE +201 -0
- package/NOTICE +2 -0
- package/NecessaryReactNative.podspec +23 -0
- package/README.md +429 -2
- package/android/build.gradle +83 -0
- package/android/src/main/AndroidManifest.xml +41 -0
- package/android/src/main/cpp/CMakeLists.txt +7 -0
- package/android/src/main/cpp/code_integrity.cpp +484 -0
- package/android/src/main/java/com/necessarysdk/reactnative/Causes.kt +49 -0
- package/android/src/main/java/com/necessarysdk/reactnative/InstallationKeyManager.kt +388 -0
- package/android/src/main/java/com/necessarysdk/reactnative/IntegrityProbeManager.kt +1157 -0
- package/android/src/main/java/com/necessarysdk/reactnative/KeyPresence.kt +62 -0
- package/android/src/main/java/com/necessarysdk/reactnative/NecessaryModules.kt +94 -0
- package/android/src/main/java/com/necessarysdk/reactnative/NecessaryNative.kt +338 -0
- package/android/src/main/java/com/necessarysdk/reactnative/NecessaryPackage.kt +38 -0
- package/android/src/main/java/com/necessarysdk/reactnative/ProbeReads.kt +333 -0
- package/android/src/main/java/com/necessarysdk/reactnative/ScreenGuard.kt +56 -0
- package/android/src/main/java/com/necessarysdk/reactnative/SecureStore.kt +127 -0
- package/android/src/main/java/com/necessarysdk/reactnative/StepUpKeyEntry.kt +57 -0
- package/android/src/main/java/com/necessarysdk/reactnative/StepUpKeyManager.kt +767 -0
- package/contract/report-1/android-8-ios-5/RULES.md +149 -0
- package/contract/report-1/android-8-ios-5/report.schema.json +2753 -0
- package/contract/report-1/android-8-ios-5/samples/android-8-complete.json +205 -0
- package/contract/report-1/android-8-ios-5/samples/android-8-incomplete.json +205 -0
- package/contract/report-1/android-8-ios-5/samples/android-8-invalid.json +205 -0
- package/contract/report-1/android-8-ios-5/samples/android-8-location-one-fix.json +228 -0
- package/contract/report-1/android-8-ios-5/samples/android-8-location-two-fixes.json +228 -0
- package/contract/report-1/android-8-ios-5/samples/android-8-refused.json +208 -0
- package/contract/report-1/android-8-ios-5/samples/ios-5-complete.json +106 -0
- package/contract/report-1/android-8-ios-5/samples/ios-5-incomplete.json +105 -0
- package/contract/report-1/android-8-ios-5/samples/ios-5-invalid.json +107 -0
- package/contract/report-1/android-8-ios-5/samples/ios-5-partial.json +110 -0
- package/contract/report-1/android-8-ios-5/samples/ios-5-refused.json +109 -0
- package/doc/integration_guide.md +771 -0
- package/doc/measurements.md +774 -0
- package/ios/InstallationKeyManager.swift +694 -0
- package/ios/IntegrityProbeManager.swift +959 -0
- package/ios/NecessaryModules.h +34 -0
- package/ios/NecessaryModules.mm +208 -0
- package/ios/NecessaryNative.swift +327 -0
- package/ios/PrivacyInfo.xcprivacy +14 -0
- package/ios/ProbeReads.swift +370 -0
- package/lib/client.d.ts +105 -0
- package/lib/client.js +176 -0
- package/lib/collector.d.ts +18 -0
- package/lib/collector.js +49 -0
- package/lib/deviceTrust.d.ts +253 -0
- package/lib/deviceTrust.js +699 -0
- package/lib/errors.d.ts +43 -0
- package/lib/errors.js +56 -0
- package/lib/index.d.ts +18 -0
- package/lib/index.js +34 -0
- package/lib/installationKey.d.ts +62 -0
- package/lib/installationKey.js +185 -0
- package/lib/installationStore.d.ts +49 -0
- package/lib/installationStore.js +127 -0
- package/lib/native/NativeNecessaryInstallationKey.d.ts +10 -0
- package/lib/native/NativeNecessaryInstallationKey.js +4 -0
- package/lib/native/NativeNecessaryIntegrity.d.ts +8 -0
- package/lib/native/NativeNecessaryIntegrity.js +4 -0
- package/lib/native/NativeNecessarySecureScreen.d.ts +7 -0
- package/lib/native/NativeNecessarySecureScreen.js +4 -0
- package/lib/native/NativeNecessaryStepUpKey.d.ts +9 -0
- package/lib/native/NativeNecessaryStepUpKey.js +4 -0
- package/lib/native/NativeNecessaryStorage.d.ts +14 -0
- package/lib/native/NativeNecessaryStorage.js +4 -0
- package/lib/protocol.d.ts +24 -0
- package/lib/protocol.js +50 -0
- package/lib/reinstallHint.d.ts +23 -0
- package/lib/reinstallHint.js +65 -0
- package/lib/report.d.ts +62 -0
- package/lib/report.js +134 -0
- package/lib/requestProof.d.ts +116 -0
- package/lib/requestProof.js +220 -0
- package/lib/secureScreen.d.ts +15 -0
- package/lib/secureScreen.js +32 -0
- package/lib/stepUpKey.d.ts +105 -0
- package/lib/stepUpKey.js +187 -0
- package/lib/transport.d.ts +39 -0
- package/lib/transport.js +117 -0
- package/lib/util/answers.d.ts +6 -0
- package/lib/util/answers.js +16 -0
- package/lib/util/bytes.d.ts +18 -0
- package/lib/util/bytes.js +207 -0
- package/lib/util/native.d.ts +20 -0
- package/lib/util/native.js +49 -0
- package/lib/util/release.d.ts +2 -0
- package/lib/util/release.js +7 -0
- package/lib/util/target.d.ts +18 -0
- package/lib/util/target.js +128 -0
- package/package.json +81 -4
- package/src/client.ts +221 -0
- package/src/collector.ts +64 -0
- package/src/deviceTrust.ts +851 -0
- package/src/errors.ts +71 -0
- package/src/index.ts +19 -0
- package/src/installationKey.ts +223 -0
- package/src/installationStore.ts +143 -0
- package/src/native/NativeNecessaryInstallationKey.ts +12 -0
- package/src/native/NativeNecessaryIntegrity.ts +15 -0
- package/src/native/NativeNecessarySecureScreen.ts +9 -0
- package/src/native/NativeNecessaryStepUpKey.ts +11 -0
- package/src/native/NativeNecessaryStorage.ts +16 -0
- package/src/protocol.ts +52 -0
- package/src/reinstallHint.ts +69 -0
- package/src/report.ts +168 -0
- package/src/requestProof.ts +295 -0
- package/src/secureScreen.ts +29 -0
- package/src/stepUpKey.ts +238 -0
- package/src/transport.ts +145 -0
- package/src/util/answers.ts +15 -0
- package/src/util/bytes.ts +194 -0
- package/src/util/native.ts +56 -0
- package/src/util/release.ts +6 -0
- package/src/util/target.ts +132 -0
- package/vectors/client_server.json +264 -0
|
@@ -0,0 +1,774 @@
|
|
|
1
|
+
# Measurements — what the SDKs measure on the phone
|
|
2
|
+
|
|
3
|
+
Every measurement a Necessary SDK can put in a signed report, field by field: what it is, how the
|
|
4
|
+
SDK measures it, and what a complete answer holds. Every SDK sends its report in the same format, whatever
|
|
5
|
+
framework your app is built with, and none sends a score or a verdict;
|
|
6
|
+
what a measurement means for a request is your backend's decision. The integration guide
|
|
7
|
+
(`integration_guide.md`, beside this file) is the protocol — how your backend asks for a scan and checks
|
|
8
|
+
the report — and its section 7 is about deciding with these measurements.
|
|
9
|
+
|
|
10
|
+
## The contract
|
|
11
|
+
|
|
12
|
+
Every SDK follows the measurement contract published in `contract/report-1/android-8-ios-5/`: the
|
|
13
|
+
report's schema (`report.schema.json`), the rules a schema cannot state (`RULES.md`), and sample reports
|
|
14
|
+
judged each way (`samples/`). The folder's name is two versions:
|
|
15
|
+
|
|
16
|
+
- `report-1` is the report's format, version 1 — `version` in every report;
|
|
17
|
+
- `android-8-ios-5` names the lowest versions of the measuring code, the collector, that the contract
|
|
18
|
+
accepts: 8 on Android and 5 on iOS — `collector_version` in every report. These are not Android or iOS versions; your
|
|
19
|
+
SDK's README says which collector versions it sends and which Android and iOS versions it runs on.
|
|
20
|
+
|
|
21
|
+
This file describes that contract as the SDKs send it: each heading below is one of its
|
|
22
|
+
measurements — a probe — and each row of a table one of its fields.
|
|
23
|
+
|
|
24
|
+
## Reading a measurement
|
|
25
|
+
|
|
26
|
+
- **Every probe has a `status`**: `ok`, measured; `error`, with `error` saying why (on Android also
|
|
27
|
+
`error_type`); `unsupported`, with `reason`; or `not_enabled`, a personal measurement your app does
|
|
28
|
+
not collect. **`ok` means measured, never "all clear"**: the phone took the measurement, and what it
|
|
29
|
+
found is in the fields — the example app shows it as *measured*.
|
|
30
|
+
- **`ok` says measured; the fields say how much.** A probe is complete when it answered `ok` and carries every
|
|
31
|
+
field marked *yes* below — for `code_integrity` with `checked` true, every field marked *when checked*
|
|
32
|
+
as well, and for a list of apps `truncated` present and false (*yes, false*).
|
|
33
|
+
- **`unread`** names the parts of an `ok` probe that could not be read, each with why:
|
|
34
|
+
`{"adb_enabled": "SecurityException"}`. An unread part is not measured: never read it as a clean "no".
|
|
35
|
+
A probe that could read nothing answers `error`.
|
|
36
|
+
- **A field that is absent was not measured.** Fields are only ever added, and a field's meaning never
|
|
37
|
+
changes; ignore fields and probes you do not know.
|
|
38
|
+
- **A report that lacks a measurement your backend asked for is refused, never read** (integration
|
|
39
|
+
guide, section 2). A failed or incomplete measurement says that something could not be measured, not
|
|
40
|
+
that the phone is attacked: ask again rather than punish the person.
|
|
41
|
+
- **Figures that contradict each other** — code-comparison coverage that does not add up, two different
|
|
42
|
+
times for one location fix — make a report one to distrust (`RULES.md`, section 7).
|
|
43
|
+
|
|
44
|
+
Each probe says when it comes:
|
|
45
|
+
|
|
46
|
+
- **your backend asks for it**, in the challenge's `required_probes`. Ask every time for each
|
|
47
|
+
measurement you need, and vary the others from scan to scan;
|
|
48
|
+
- **every scan**, whether or not your backend asks. If you need one, ask for it: until you do, its
|
|
49
|
+
absence or failure does not make a report incomplete;
|
|
50
|
+
- **a personal measurement**, only when your backend lists it in `optional_probes` and your app enables
|
|
51
|
+
it in its SDK (the SDK's README says how) — the location only with the person's permission as well.
|
|
52
|
+
These identify a person's phone, network or habits (integration guide, section 8).
|
|
53
|
+
|
|
54
|
+
The columns: *Type* — `true/false`, an integer or a number with its unit, text, a list or a map;
|
|
55
|
+
*Needed* — *yes*, a complete answer carries it; *when checked*, needed when `checked` is true; *yes,
|
|
56
|
+
false*, present and false; empty, sent when the SDK has it.
|
|
57
|
+
|
|
58
|
+
## Apps in a list
|
|
59
|
+
|
|
60
|
+
An app in `services`, `keyboard`, `listeners`, `device_admins` or `remote_access_apps` (Android) is an
|
|
61
|
+
object: `package`, its package name; `service`, the component the list is about, where there is one;
|
|
62
|
+
`system`, true for an app that is part of the phone's system; and `installer`, the package that installed
|
|
63
|
+
it, empty for none. `system` and `installer` are left out where Android hides the app from this one —
|
|
64
|
+
unknown, never evidence. A list holds at most 32 apps, and `truncated` is true when the phone had more.
|
|
65
|
+
|
|
66
|
+
## Android
|
|
67
|
+
|
|
68
|
+
Your backend may ask for `app_identity`, `debug_state`, `tracer`, `root_files`, `root_shell`,
|
|
69
|
+
`system_properties`, `selinux`, `mounts`, `runtime_maps`, `frida_ports`, `instrumentation_threads`,
|
|
70
|
+
`exec_mappings`, `code_integrity`, `developer_settings` and `emulator`. Every scan carries `device` and
|
|
71
|
+
`network`. The personal measurements are `local_addresses`, `accessibility`, `notification_access`,
|
|
72
|
+
`other_access`, `screen_capture`, `location` and `mock_location`.
|
|
73
|
+
|
|
74
|
+
### `app_identity`
|
|
75
|
+
|
|
76
|
+
The installed app as Android reports it: its package, the hashes of its installed files, its signing
|
|
77
|
+
certificates, its flags and what installed it. A rebuilt app must be signed again, with a key that is
|
|
78
|
+
not yours, and one changed byte changes a hash: compare these with each release of yours (integration
|
|
79
|
+
guide, section 7.2). A lookup that failed is in `unread`: the installer as `installer_package`, a split
|
|
80
|
+
APK that could not be hashed as `split_apks:<file name>`.
|
|
81
|
+
|
|
82
|
+
**When:** your backend asks for it.
|
|
83
|
+
|
|
84
|
+
| Field | Type | Needed | Meaning |
|
|
85
|
+
|---|---|---|---|
|
|
86
|
+
| `package_name` | text | yes | the app's package name |
|
|
87
|
+
| `apk_sha256` | text | yes | the SHA-256 of the base APK alone, never its splits: installs of one release with different feature, ABI or density splits share it |
|
|
88
|
+
| `cert_sha256` | list | yes | the SHA-256 of each certificate signing the app, sorted; after a signing key rotation, each certificate of its history |
|
|
89
|
+
| `debuggable` | true/false | yes | the app is built debuggable |
|
|
90
|
+
| `allow_backup` | true/false | yes | the app allows Android's backup (`allowBackup`) |
|
|
91
|
+
| `current_signers` | list | | the SHA-256 of the certificates signing the APK now, sorted, at most 8 |
|
|
92
|
+
| `signing_lineage` | list | | after a signing key rotation, the SHA-256 of each certificate of the lineage, oldest first, at most 8; empty without one |
|
|
93
|
+
| `split_apks` | list | | the installed split APKs, apart from the base APK: each its file `name` and its `sha256`, sorted by name, at most 64; empty for an install without splits |
|
|
94
|
+
| `version_name` | text | | the app's version name |
|
|
95
|
+
| `version_code` | integer | | the app's version code |
|
|
96
|
+
| `installer_package` | text, or null | | the package that installed the app; left out or null when none is named — installed with adb, or by an installer that names none |
|
|
97
|
+
| `source_dir` | text | | the base APK's path on the phone |
|
|
98
|
+
|
|
99
|
+
### `debug_state`
|
|
100
|
+
|
|
101
|
+
Whether a Java debugger is attached to the app, or the app waits for one, as Android's `Debug` says.
|
|
102
|
+
|
|
103
|
+
**When:** your backend asks for it.
|
|
104
|
+
|
|
105
|
+
| Field | Type | Needed | Meaning |
|
|
106
|
+
|---|---|---|---|
|
|
107
|
+
| `debugger_connected` | true/false | yes | a Java debugger is attached |
|
|
108
|
+
| `waiting_for_debugger` | true/false | yes | the app waits for a debugger to attach |
|
|
109
|
+
|
|
110
|
+
### `tracer`
|
|
111
|
+
|
|
112
|
+
Another process tracing the app — a native debugger, or a tool attached to the app's process — from the
|
|
113
|
+
app's own `/proc/self/status`. A status file that cannot be read, or without a tracer line, answers
|
|
114
|
+
`error`: never a tracer of 0.
|
|
115
|
+
|
|
116
|
+
**When:** your backend asks for it.
|
|
117
|
+
|
|
118
|
+
| Field | Type | Needed | Meaning |
|
|
119
|
+
|---|---|---|---|
|
|
120
|
+
| `tracer_pid` | integer, process ID | yes | the process tracing the app; 0 when none |
|
|
121
|
+
| `seccomp` | integer | | the app's `Seccomp` mode from the same file; -1 when the line is absent |
|
|
122
|
+
| `no_new_privs` | integer | | the app's `NoNewPrivs` flag from the same file; -1 when the line is absent |
|
|
123
|
+
|
|
124
|
+
### `root_files`
|
|
125
|
+
|
|
126
|
+
Whether 15 known root paths exist — `su` in the usual places, the Superuser and SuperSU apps, and the
|
|
127
|
+
folders of Magisk, KernelSU and APatch — and whether the build is signed with test keys. A path the app
|
|
128
|
+
is not allowed to see reads as absent; a check that failed is in `unread`, by its path.
|
|
129
|
+
|
|
130
|
+
**When:** your backend asks for it.
|
|
131
|
+
|
|
132
|
+
| Field | Type | Needed | Meaning |
|
|
133
|
+
|---|---|---|---|
|
|
134
|
+
| `found_paths` | list | yes | the listed paths that exist |
|
|
135
|
+
| `test_keys` | true/false | yes | the build's tags include `test-keys` |
|
|
136
|
+
| `build_tags` | text | | the build's tags, for example `release-keys` |
|
|
137
|
+
|
|
138
|
+
### `root_shell`
|
|
139
|
+
|
|
140
|
+
Whether `su` resolves from the app's own shell (`command -v su`, then `which su`). A lookup that could not run, or did
|
|
141
|
+
not finish, answers `error`: never "no su".
|
|
142
|
+
|
|
143
|
+
**When:** your backend asks for it.
|
|
144
|
+
|
|
145
|
+
| Field | Type | Needed | Meaning |
|
|
146
|
+
|---|---|---|---|
|
|
147
|
+
| `su_found` | true/false | yes | `su` resolved |
|
|
148
|
+
| `su_path` | text | | where it resolved; empty when it did not |
|
|
149
|
+
|
|
150
|
+
### `system_properties`
|
|
151
|
+
|
|
152
|
+
Build and boot-security properties, each read with `getprop`: whether the build is secure and not
|
|
153
|
+
debuggable, its type and tags, the verified-boot state, the bootloader's lock, and the vbmeta and
|
|
154
|
+
dm-verity states. A property that could not be read is in `unread`, by its name; when none could, the
|
|
155
|
+
probe answers `error`.
|
|
156
|
+
|
|
157
|
+
**When:** your backend asks for it.
|
|
158
|
+
|
|
159
|
+
| Field | Type | Needed | Meaning |
|
|
160
|
+
|---|---|---|---|
|
|
161
|
+
| `properties` | map | yes | each property read, by name — `ro.secure`, `ro.debuggable`, `ro.build.type`, `ro.build.tags`, `ro.boot.verifiedbootstate`, `ro.boot.flash.locked`, `ro.boot.vbmeta.device_state`, `ro.boot.veritymode` — with the text `getprop` printed, empty when the phone does not set it |
|
|
162
|
+
|
|
163
|
+
### `selinux`
|
|
164
|
+
|
|
165
|
+
The SELinux mode, read two ways: the `getenforce` command, and the kernel's own
|
|
166
|
+
`/sys/fs/selinux/enforce`. Many phones let an app read neither; then the mode is `unknown`, which is no
|
|
167
|
+
evidence of anything.
|
|
168
|
+
|
|
169
|
+
**When:** your backend asks for it.
|
|
170
|
+
|
|
171
|
+
| Field | Type | Needed | Meaning |
|
|
172
|
+
|---|---|---|---|
|
|
173
|
+
| `mode` | one of `enforcing`, `permissive`, `disabled`, `unknown` | yes | the mode: from `getenforce` when it answered, else from the kernel's value (`1` enforcing, `0` permissive); `unknown` when neither could be read |
|
|
174
|
+
| `getenforce` | text | yes | what `getenforce` printed; empty when it printed nothing |
|
|
175
|
+
| `enforce_value` | text | yes | the kernel's value, `1` or `0`; empty when it could not be read |
|
|
176
|
+
|
|
177
|
+
### `mounts`
|
|
178
|
+
|
|
179
|
+
System partitions mounted writable — `/system`, `/vendor`, `/product`, `/odm`, `/system_ext` and
|
|
180
|
+
anything below them — from `/proc/mounts`. On a phone as it is sold they are read-only. A mounts file
|
|
181
|
+
that cannot be read answers `error`.
|
|
182
|
+
|
|
183
|
+
**When:** your backend asks for it.
|
|
184
|
+
|
|
185
|
+
| Field | Type | Needed | Meaning |
|
|
186
|
+
|---|---|---|---|
|
|
187
|
+
| `protected_rw_mounts` | list | yes | each of those mounted read-write, as `<mount point>:<options>`, at most 32 |
|
|
188
|
+
|
|
189
|
+
### `runtime_maps`
|
|
190
|
+
|
|
191
|
+
Instrumentation and hooking frameworks named in the app's own memory map, `/proc/self/maps`: Frida and
|
|
192
|
+
its Gadget, Objection, Xposed, LSPosed, Substrate, Cydia, Zygisk, Riru, Magisk, KernelSU and APatch. It
|
|
193
|
+
matches names, so a renamed library escapes it; `instrumentation_threads`, `exec_mappings` and
|
|
194
|
+
`code_integrity` look at structure instead. A map that cannot be read, or reads as empty, answers
|
|
195
|
+
`error`.
|
|
196
|
+
|
|
197
|
+
**When:** your backend asks for it.
|
|
198
|
+
|
|
199
|
+
| Field | Type | Needed | Meaning |
|
|
200
|
+
|---|---|---|---|
|
|
201
|
+
| `suspicious_tokens` | list | yes | the listed names found |
|
|
202
|
+
| `maps_lines` | integer, count | yes | lines read from the memory map |
|
|
203
|
+
| `suspicious_line_count` | integer, count | | lines that named one |
|
|
204
|
+
|
|
205
|
+
### `frida_ports`
|
|
206
|
+
|
|
207
|
+
Whether Frida's default ports, 27042 and 27043, accept a connection on the phone itself. A refused
|
|
208
|
+
connection is a measured "closed"; any other failure is in `unread`, by its port.
|
|
209
|
+
|
|
210
|
+
**When:** your backend asks for it.
|
|
211
|
+
|
|
212
|
+
| Field | Type | Needed | Meaning |
|
|
213
|
+
|---|---|---|---|
|
|
214
|
+
| `open_ports` | list | yes | the ports that accepted a connection |
|
|
215
|
+
|
|
216
|
+
### `instrumentation_threads`
|
|
217
|
+
|
|
218
|
+
The names of the app's own threads, from `/proc/self/task`. Frida's runtime starts threads whose names
|
|
219
|
+
are compiled into it, so a renamed library still shows here. A thread whose name could not be read is in
|
|
220
|
+
`unread` as `thread_names`, since a renamed instrumentation thread could be that one; a thread that
|
|
221
|
+
ended while the list was read is simply gone.
|
|
222
|
+
|
|
223
|
+
**When:** your backend asks for it.
|
|
224
|
+
|
|
225
|
+
| Field | Type | Needed | Meaning |
|
|
226
|
+
|---|---|---|---|
|
|
227
|
+
| `frida_threads` | list | yes | the Frida runtime's thread names found: `gum-js-loop`, `gum-js`, `pool-frida` |
|
|
228
|
+
| `glib_threads` | list | yes | the GLib thread names found — `gmain`, `gdbus`, `pool-spawner` — which Frida brings into an app and apps seldom bring themselves |
|
|
229
|
+
| `token_threads` | list | yes | the names `runtime_maps` looks for, found in a thread's name |
|
|
230
|
+
| `thread_count` | integer, count | yes | the threads whose names were read; a running app always has some, so 0 is a failed read |
|
|
231
|
+
|
|
232
|
+
### `exec_mappings`
|
|
233
|
+
|
|
234
|
+
The shape of the app's executable memory, from `/proc/self/maps`. A clean app's code is read-only and
|
|
235
|
+
backed by its files; memory both writable and executable, executable memory whose file was deleted, and
|
|
236
|
+
executable memory with neither a file nor a label are the shapes that injected code leaves. Some
|
|
237
|
+
runtimes compile code as they run and map some of each: compare with the most your own release maps
|
|
238
|
+
(integration guide, section 7.2).
|
|
239
|
+
|
|
240
|
+
**When:** your backend asks for it.
|
|
241
|
+
|
|
242
|
+
| Field | Type | Needed | Meaning |
|
|
243
|
+
|---|---|---|---|
|
|
244
|
+
| `wx_mappings` | integer, count | yes | mappings both writable and executable |
|
|
245
|
+
| `deleted_exec_mappings` | integer, count | yes | executable mappings whose file was deleted, the runtime's code cache and the app's own compiled code left out |
|
|
246
|
+
| `deleted_exec_own_code` | integer, count | yes | executable mappings of the app's own compiled code (`.odex`, `.vdex`, `.art`, `.oat` under its install folder) whose file was deleted |
|
|
247
|
+
| `maps_lines` | integer, count | yes | lines read from the memory map |
|
|
248
|
+
| `wx_bytes` | integer, bytes | | the size of all the writable-and-executable mappings |
|
|
249
|
+
| `wx_size_classes` | text | | each size of writable-and-executable mapping with how many there are, largest first, as `<bytes>:<count>`, comma-separated |
|
|
250
|
+
| `wx_smallest_bytes` | integer, bytes | | the smallest writable-and-executable mapping; 0 when none |
|
|
251
|
+
| `wx_largest_bytes` | integer, bytes | | the largest writable-and-executable mapping; 0 when none |
|
|
252
|
+
| `deleted_exec_jit` | integer, count | | executable mappings of the runtime's code cache, which shows as deleted on every phone |
|
|
253
|
+
| `anon_exec_labeled` | integer, count | | executable mappings with no file but a label (`[anon:…]`), such as the runtime's code cache |
|
|
254
|
+
| `anon_exec_unlabeled` | integer, count | | executable mappings with neither a file nor a label |
|
|
255
|
+
| `anon_exec_unlabeled_bytes` | integer, bytes | | their size |
|
|
256
|
+
| `samples` | list | | up to 8 of the memory-map lines counted above — writable and executable, or deleted — each cut to 160 characters |
|
|
257
|
+
|
|
258
|
+
### `code_integrity`
|
|
259
|
+
|
|
260
|
+
The app's native code, and the system libraries it uses, compared in memory with their files: a hook
|
|
261
|
+
that patches code in memory changes bytes there, whatever the tool is called. Three regions: `core` —
|
|
262
|
+
the C library and the Android runtime (`libc.so`, `libart.so`); `ext` — other system libraries that
|
|
263
|
+
hooks aim at (`libc++.so`, `libssl.so`, `libcrypto.so`, `libandroid_runtime.so`, `libbinder.so`); `app`
|
|
264
|
+
— the app's own native code: what is mapped from the APKs the app was installed from (its base APK and
|
|
265
|
+
its split APKs, by their exact paths — never another package's APK the app has loaded code from, such
|
|
266
|
+
as the WebView's), and the libraries the SDK names wherever they are (this SDK: React Native's
|
|
267
|
+
runtime, `libreactnative.so`; its JavaScript engine, Hermes — `libhermesvm.so` or `libhermes.so`, with
|
|
268
|
+
`libhermestooling.so`; `libjsi.so`; `libfbjni.so`; and the app's own native modules, `libappmodules.so`). That is a selection, and it depends on
|
|
269
|
+
how the app is packaged: a build that keeps its native libraries in its APKs (the default) has all of
|
|
270
|
+
them measured; one that extracts them leaves those under other names — another plugin's, say — outside
|
|
271
|
+
`app`. The report says what it leaves out: every executable file of the app's own that is mapped but
|
|
272
|
+
not in `app` — an extracted library, the app's own code compiled by Android — is counted in
|
|
273
|
+
`app_outside_count` and named in `app_outside_files`. Code compiled as the app runs has no file to be
|
|
274
|
+
compared with, and is in neither.
|
|
275
|
+
In each region expected = compared + skipped + unreadable, and `<region>_complete` is true exactly when
|
|
276
|
+
some code was expected and all of it was compared. Any differing byte is code changed while the app
|
|
277
|
+
runs, which should never happen. Memory that Android 10 and later maps execute-only is made readable for
|
|
278
|
+
the copy and restored. The memory map is read once, whole, and every region taken from that one
|
|
279
|
+
snapshot: complete means every target mapping in the snapshot compared. One comparison runs at a time
|
|
280
|
+
in the app's process. `checked` false: the comparison did not run, and `reason` says why —
|
|
281
|
+
`maps_unreadable` or `maps_empty` when the memory map could not be read, `no_target_mapped` when it
|
|
282
|
+
names no target; every region is then incomplete.
|
|
283
|
+
|
|
284
|
+
**When:** your backend asks for it.
|
|
285
|
+
|
|
286
|
+
| Field | Type | Needed | Meaning |
|
|
287
|
+
|---|---|---|---|
|
|
288
|
+
| `checked` | true/false | yes | the comparison ran |
|
|
289
|
+
| `diff_bytes` | integer, bytes | yes | bytes that differ in `core` (the same as `core_diff_bytes`) |
|
|
290
|
+
| `core_compared_bytes` | integer, bytes | yes | `core` bytes compared |
|
|
291
|
+
| `ext_compared_bytes` | integer, bytes | yes | `ext` bytes compared |
|
|
292
|
+
| `ext_diff_bytes` | integer, bytes | yes | `ext` bytes that differ |
|
|
293
|
+
| `app_compared_bytes` | integer, bytes | yes | `app` bytes compared |
|
|
294
|
+
| `app_diff_bytes` | integer, bytes | yes | `app` bytes that differ |
|
|
295
|
+
| `core_expected_bytes` | integer, bytes | when checked | `core` code found in memory, in bytes |
|
|
296
|
+
| `ext_expected_bytes` | integer, bytes | when checked | `ext` code found in memory, in bytes |
|
|
297
|
+
| `app_expected_bytes` | integer, bytes | when checked | `app` code found in memory, in bytes |
|
|
298
|
+
| `core_skipped_bytes` | integer, bytes | | `core` bytes not compared, past the comparison's budget of 256 MiB a region |
|
|
299
|
+
| `ext_skipped_bytes` | integer, bytes | | `ext` bytes not compared, past the budget |
|
|
300
|
+
| `app_skipped_bytes` | integer, bytes | | `app` bytes not compared, past the budget |
|
|
301
|
+
| `core_unreadable_bytes` | integer, bytes | | `core` bytes not compared because they could not be read: memory that could not be made readable, a file that could not be opened, a short read |
|
|
302
|
+
| `ext_unreadable_bytes` | integer, bytes | | `ext` bytes that could not be read |
|
|
303
|
+
| `app_unreadable_bytes` | integer, bytes | | `app` bytes that could not be read |
|
|
304
|
+
| `core_complete` | true/false | | every `core` byte expected was compared |
|
|
305
|
+
| `ext_complete` | true/false | | every `ext` byte expected was compared |
|
|
306
|
+
| `app_complete` | true/false | | every `app` byte expected was compared: complete over the selected mappings of the snapshot, not over all of the app's code |
|
|
307
|
+
| `core_diff_bytes` | integer, bytes | | `core` bytes that differ |
|
|
308
|
+
| `ext_libs_diff` | integer, count | | `ext` libraries with a difference |
|
|
309
|
+
| `app_libs_diff` | integer, count | | `app` files with a difference |
|
|
310
|
+
| `diffed_libs` | text | | the files with a difference, comma-separated |
|
|
311
|
+
| `protect_restore_failures` | integer, count | | execute-only memory whose protection could not be put back after the copy |
|
|
312
|
+
| `xom_regions_unlocked` | integer, count | | execute-only regions made readable for the comparison |
|
|
313
|
+
| `xom_regions_unreadable` | integer, count | | execute-only regions that could not be made readable: their bytes count as unreadable |
|
|
314
|
+
| `app_outside_count` | integer, count | | executable files of the app's own (in its install or data directories) mapped but not in `app`, so not compared (collector 10) |
|
|
315
|
+
| `app_outside_files` | list | | their file names, sorted, at most 32 (collector 10) |
|
|
316
|
+
| `elapsed_ms` | integer, milliseconds | | how long the comparison took |
|
|
317
|
+
|
|
318
|
+
### `developer_settings`
|
|
319
|
+
|
|
320
|
+
Whether developer options and USB debugging are on. Both are common on honest phones (integration guide,
|
|
321
|
+
section 7.1). A setting that could not be read is in `unread`, never "off"; when neither could, the
|
|
322
|
+
probe answers `error`.
|
|
323
|
+
|
|
324
|
+
**When:** your backend asks for it.
|
|
325
|
+
|
|
326
|
+
| Field | Type | Needed | Meaning |
|
|
327
|
+
|---|---|---|---|
|
|
328
|
+
| `developer_options_enabled` | true/false | yes | developer options are on |
|
|
329
|
+
| `adb_enabled` | true/false | yes | USB debugging is on |
|
|
330
|
+
|
|
331
|
+
### `emulator`
|
|
332
|
+
|
|
333
|
+
Build values typical of an Android emulator: the fingerprint, model, manufacturer, product, hardware and
|
|
334
|
+
board names of the common emulators.
|
|
335
|
+
|
|
336
|
+
**When:** your backend asks for it.
|
|
337
|
+
|
|
338
|
+
| Field | Type | Needed | Meaning |
|
|
339
|
+
|---|---|---|---|
|
|
340
|
+
| `suspected` | true/false | yes | a build value looked like an emulator's |
|
|
341
|
+
| `reasons` | list | | which: `fingerprint`, `model`, `manufacturer`, `product`, `hardware`, `board` |
|
|
342
|
+
| `model` | text | | the build's model |
|
|
343
|
+
| `manufacturer` | text | | the build's manufacturer |
|
|
344
|
+
| `product` | text | | the build's product |
|
|
345
|
+
| `hardware` | text | | the build's hardware |
|
|
346
|
+
|
|
347
|
+
### `device`
|
|
348
|
+
|
|
349
|
+
The phone and its software: the Android version and API level, the security patch level — how current
|
|
350
|
+
the phone's patches are — the maker and model, the build, the processor ABI the app runs as, and the
|
|
351
|
+
memory page size.
|
|
352
|
+
|
|
353
|
+
**When:** every scan, whether or not your backend asks.
|
|
354
|
+
|
|
355
|
+
| Field | Type | Needed | Meaning |
|
|
356
|
+
|---|---|---|---|
|
|
357
|
+
| `sdk_int` | integer, Android API level | yes | the Android API level, for example 34 |
|
|
358
|
+
| `release` | text | | the Android version, for example `14` |
|
|
359
|
+
| `security_patch` | text | | the security patch level, a date (`YYYY-MM-DD`) |
|
|
360
|
+
| `manufacturer` | text | | the phone's maker |
|
|
361
|
+
| `brand` | text | | its brand |
|
|
362
|
+
| `model` | text | | its model |
|
|
363
|
+
| `device` | text | | its device name |
|
|
364
|
+
| `build_id` | text | | the build's ID |
|
|
365
|
+
| `fingerprint` | text | | the build's fingerprint, at most 256 characters |
|
|
366
|
+
| `abi` | text | | the ABI the app's process runs as — `arm64-v8a`, `armeabi-v7a`, `x86_64` — since a 32-bit app on a 64-bit phone maps other code |
|
|
367
|
+
| `page_size` | integer, bytes | | the memory page size: 4096, or 16384 on phones built for 16 KB pages |
|
|
368
|
+
|
|
369
|
+
### `network`
|
|
370
|
+
|
|
371
|
+
Whether a VPN is up and an HTTP proxy is set, as Android's connectivity service says: every network is
|
|
372
|
+
asked, not only the default one, since a VPN is a network of its own. Interface names only, never an
|
|
373
|
+
address. The proxy says how the phone is set, not which way the report came: whether an app's own
|
|
374
|
+
requests follow it depends on how the app makes them. A listing of the
|
|
375
|
+
interfaces that failed, whole or for one interface, is in `unread` as `interfaces`: the list is then
|
|
376
|
+
not all of them.
|
|
377
|
+
|
|
378
|
+
**When:** every scan, whether or not your backend asks.
|
|
379
|
+
|
|
380
|
+
| Field | Type | Needed | Meaning |
|
|
381
|
+
|---|---|---|---|
|
|
382
|
+
| `vpn_active` | true/false | yes | a VPN network is up |
|
|
383
|
+
| `http_proxy_configured` | true/false | | an HTTP proxy is set |
|
|
384
|
+
| `transports` | list | | what carries the default network: `wifi`, `cellular`, `ethernet`, `vpn` |
|
|
385
|
+
| `interfaces` | list | | the names of the network interfaces that are up, loopback left out — `wlan0`, `rmnet0`, `tun0` — at most 32 |
|
|
386
|
+
|
|
387
|
+
### `local_addresses`
|
|
388
|
+
|
|
389
|
+
The phone's own IP addresses, per network interface. Loopback and link-local addresses are left out: a
|
|
390
|
+
link-local address can carry the hardware address, which Android hides from apps.
|
|
391
|
+
|
|
392
|
+
**When:** your backend asks for it and your app enables it — a personal measurement.
|
|
393
|
+
|
|
394
|
+
| Field | Type | Needed | Meaning |
|
|
395
|
+
|---|---|---|---|
|
|
396
|
+
| `addresses` | list | yes | each an `interface` (its name) and an `address` (IPv4 or IPv6), at most 32 |
|
|
397
|
+
| `truncated` | true/false | | the phone had more than the 32 addresses a list holds (Android collector 10, iOS collector 8) |
|
|
398
|
+
|
|
399
|
+
### `accessibility`
|
|
400
|
+
|
|
401
|
+
Every Accessibility service that is on, and the keyboard in use: the two ways an app reads what the
|
|
402
|
+
person types. The Accessibility manager's list is compared with the system setting it is built from — a
|
|
403
|
+
service the setting names that the manager does not report was hiding, as an Android bug allowed until
|
|
404
|
+
the December 2022 security patch.
|
|
405
|
+
|
|
406
|
+
**When:** your backend asks for it and your app enables it — a personal measurement.
|
|
407
|
+
|
|
408
|
+
| Field | Type | Needed | Meaning |
|
|
409
|
+
|---|---|---|---|
|
|
410
|
+
| `services` | list of apps | yes | the Accessibility services that are on, each an app with its `service` |
|
|
411
|
+
| `settings_only` | list | yes | services the system setting names that the Accessibility manager does not report, as component names |
|
|
412
|
+
| `truncated` | true/false | yes, false | the phone had more than the 32 a list holds |
|
|
413
|
+
| `keyboard` | an app | | the keyboard in use — the default input method — with its `service` |
|
|
414
|
+
|
|
415
|
+
### `notification_access`
|
|
416
|
+
|
|
417
|
+
Every app allowed to read the phone's notifications — one-time codes among them.
|
|
418
|
+
|
|
419
|
+
**When:** your backend asks for it and your app enables it — a personal measurement.
|
|
420
|
+
|
|
421
|
+
| Field | Type | Needed | Meaning |
|
|
422
|
+
|---|---|---|---|
|
|
423
|
+
| `listeners` | list of apps | yes | the apps allowed to read notifications, each with its `service` |
|
|
424
|
+
| `truncated` | true/false | yes, false | the phone had more than the 32 a list holds |
|
|
425
|
+
|
|
426
|
+
### `other_access`
|
|
427
|
+
|
|
428
|
+
The device-administrator apps that are active, and which of four known remote-control apps are
|
|
429
|
+
installed: AnyDesk, TeamViewer QuickSupport, RustDesk and AirDroid — a list compiled into the SDK,
|
|
430
|
+
which Android lets an app check without seeing other apps.
|
|
431
|
+
|
|
432
|
+
**When:** your backend asks for it and your app enables it — a personal measurement.
|
|
433
|
+
|
|
434
|
+
| Field | Type | Needed | Meaning |
|
|
435
|
+
|---|---|---|---|
|
|
436
|
+
| `device_admins` | list of apps | yes | the active device administrators, each with its `service` |
|
|
437
|
+
| `remote_access_apps` | list of apps | yes | the remote-control apps installed, of the four |
|
|
438
|
+
| `truncated` | true/false | yes, false | the phone had more device administrators than the 32 a list holds |
|
|
439
|
+
|
|
440
|
+
### `screen_capture`
|
|
441
|
+
|
|
442
|
+
Whether the app's window is being recorded (Android 15 and later), and how many displays besides the
|
|
443
|
+
phone's own are on: casting, mirroring or an external screen, which it cannot tell apart. A display that
|
|
444
|
+
is off, or private to another app, is not counted. Recording needs `DETECT_SCREEN_RECORDING`, a
|
|
445
|
+
permission the SDK declares for your app; when it cannot be measured it is left out — unknown, not
|
|
446
|
+
"no".
|
|
447
|
+
|
|
448
|
+
**When:** your backend asks for it and your app enables it — a personal measurement.
|
|
449
|
+
|
|
450
|
+
| Field | Type | Needed | Meaning |
|
|
451
|
+
|---|---|---|---|
|
|
452
|
+
| `recording` | true/false | yes | the app's window is being recorded |
|
|
453
|
+
| `external_displays` | integer, count | yes | displays besides the phone's own that are on and public |
|
|
454
|
+
| `recording_status` | one of `unsupported`, `no_permission`, `unavailable` | | why `recording` is absent: `unsupported` below Android 15, `no_permission` without `DETECT_SCREEN_RECORDING`, `unavailable` with no app window to ask |
|
|
455
|
+
|
|
456
|
+
### `location`
|
|
457
|
+
|
|
458
|
+
The last location the phone already knows — never a fresh fix, never a prompt: the newest fix any
|
|
459
|
+
enabled provider holds, read once per scan, so that both location answers describe the same fix.
|
|
460
|
+
Coordinates come only with the location permission, which your app asks for itself (your SDK's README says how). With the
|
|
461
|
+
permission and no coordinates the phone holds no location — unless a provider could not be read: it is
|
|
462
|
+
then in `unread`, by its name, and the location may be there.
|
|
463
|
+
|
|
464
|
+
**When:** your backend asks for it and your app enables it — a personal measurement.
|
|
465
|
+
|
|
466
|
+
| Field | Type | Needed | Meaning |
|
|
467
|
+
|---|---|---|---|
|
|
468
|
+
| `permission` | one of `none`, `approximate`, `precise` | yes | the app's location permission |
|
|
469
|
+
| `latitude` | number, degrees, WGS 84 | yes | the fix's latitude; absent without the permission or without a fix |
|
|
470
|
+
| `longitude` | number, degrees, WGS 84 | yes | the fix's longitude; absent likewise |
|
|
471
|
+
| `age_seconds` | integer, seconds | yes | how old the fix is |
|
|
472
|
+
| `accuracy_m` | number, metres | | its accuracy, when the provider gives one |
|
|
473
|
+
| `provider` | text | | the provider that made it, for example `gps` or `network` |
|
|
474
|
+
| `fix_time` | integer, Unix seconds, UTC | | when it was made |
|
|
475
|
+
|
|
476
|
+
### `mock_location`
|
|
477
|
+
|
|
478
|
+
Whether that same fix came from a mock-location app, by Android's own flag. It never carries
|
|
479
|
+
coordinates, so asking for it alone never sends a location.
|
|
480
|
+
|
|
481
|
+
**When:** your backend asks for it and your app enables it — a personal measurement.
|
|
482
|
+
|
|
483
|
+
| Field | Type | Needed | Meaning |
|
|
484
|
+
|---|---|---|---|
|
|
485
|
+
| `permission` | one of `none`, `approximate`, `precise` | yes | the app's location permission |
|
|
486
|
+
| `mock` | true/false | yes | the fix came from a mock-location app |
|
|
487
|
+
| `age_seconds` | integer, seconds | yes | how old the fix is |
|
|
488
|
+
| `fix_time` | integer, Unix seconds, UTC | | when it was made: the same as `location`'s in one report |
|
|
489
|
+
|
|
490
|
+
## iOS
|
|
491
|
+
|
|
492
|
+
Your backend may ask for `app_identity`, `code_signing`, `debugger`, `jailbreak_files`, `sandbox`,
|
|
493
|
+
`dyld_images`, `environment` and `simulator`. Every scan carries `code_integrity`, `device` and
|
|
494
|
+
`network`. The personal measurements are `local_addresses`, `screen_capture`, `location` and
|
|
495
|
+
`mock_location`; `accessibility`, `notification_access` and `other_access` exist on iOS only to say that
|
|
496
|
+
iOS cannot measure them.
|
|
497
|
+
|
|
498
|
+
### `app_identity`
|
|
499
|
+
|
|
500
|
+
The installed app: its bundle identifier and version, and the SHA-256 and size of its main executable.
|
|
501
|
+
An executable with code added is larger, and one changed byte changes the hash: compare these with each
|
|
502
|
+
release of yours (integration guide, section 7.2).
|
|
503
|
+
|
|
504
|
+
**When:** your backend asks for it.
|
|
505
|
+
|
|
506
|
+
| Field | Type | Needed | Meaning |
|
|
507
|
+
|---|---|---|---|
|
|
508
|
+
| `bundle_id` | text | yes | the app's bundle identifier |
|
|
509
|
+
| `executable_sha256` | text | yes | the SHA-256 of the main executable; empty when it could not be read |
|
|
510
|
+
| `version_name` | text | | the app's version (`CFBundleShortVersionString`) |
|
|
511
|
+
| `version_code` | text | | the app's build number (`CFBundleVersion`) |
|
|
512
|
+
| `executable_bytes` | integer, bytes | | the main executable's size |
|
|
513
|
+
|
|
514
|
+
### `code_signing`
|
|
515
|
+
|
|
516
|
+
The main executable's own code signature, read from the file: its signing identifier, your team
|
|
517
|
+
identifier, and whether the app lets a debugger attach (`get-task-allow`, set on development builds). An
|
|
518
|
+
executable that carries no signature says so: `signed` false, never taken for a signed app.
|
|
519
|
+
|
|
520
|
+
**When:** your backend asks for it.
|
|
521
|
+
|
|
522
|
+
| Field | Type | Needed | Meaning |
|
|
523
|
+
|---|---|---|---|
|
|
524
|
+
| `signing_identifier` | text | yes | the signature's identifier; empty without one |
|
|
525
|
+
| `team_identifier` | text | yes | the team identifier its entitlements name; empty without one |
|
|
526
|
+
| `get_task_allow` | true/false | yes | its entitlements let a debugger attach |
|
|
527
|
+
| `signed` | true/false | | the executable carries a signature |
|
|
528
|
+
| `signature_absent` | true/false | | true when it carries none |
|
|
529
|
+
| `signature_parse_error` | text | | why a signature that is there could not be read |
|
|
530
|
+
| `code_directory_flags` | integer | | the code directory's flags, as a number |
|
|
531
|
+
| `entitlement_count` | integer, count | | how many entitlements the signature grants |
|
|
532
|
+
|
|
533
|
+
### `debugger`
|
|
534
|
+
|
|
535
|
+
A debugger tracing the app's process: the kernel's `P_TRACED` flag.
|
|
536
|
+
|
|
537
|
+
**When:** your backend asks for it.
|
|
538
|
+
|
|
539
|
+
| Field | Type | Needed | Meaning |
|
|
540
|
+
|---|---|---|---|
|
|
541
|
+
| `traced` | true/false | yes | a debugger traces the process |
|
|
542
|
+
|
|
543
|
+
### `jailbreak_files`
|
|
544
|
+
|
|
545
|
+
Whether 20 known jailbreak paths exist: the package managers (Cydia, Sileo, Zebra), Substrate and
|
|
546
|
+
ElleKit, a shell and SSH, apt's and Cydia's folders, `/private/var/stash`, the marker files of the
|
|
547
|
+
Electra, Taurine and unc0ver jailbreaks, and the rootless `/var/jb` layout of current jailbreaks. A
|
|
548
|
+
clean phone's sandbox hides most of them, so a path the app cannot see reads as absent. On the
|
|
549
|
+
Simulator, whose files are the Mac's, it answers `unsupported`.
|
|
550
|
+
|
|
551
|
+
**When:** your backend asks for it.
|
|
552
|
+
|
|
553
|
+
| Field | Type | Needed | Meaning |
|
|
554
|
+
|---|---|---|---|
|
|
555
|
+
| `found_paths` | list | yes | the listed paths that exist |
|
|
556
|
+
| `checked_count` | integer, count | | how many paths were checked |
|
|
557
|
+
|
|
558
|
+
### `sandbox`
|
|
559
|
+
|
|
560
|
+
Whether the app can write outside its sandbox: a phone that enforces the sandbox refuses it. A file that
|
|
561
|
+
was written is removed at once.
|
|
562
|
+
|
|
563
|
+
**When:** your backend asks for it.
|
|
564
|
+
|
|
565
|
+
| Field | Type | Needed | Meaning |
|
|
566
|
+
|---|---|---|---|
|
|
567
|
+
| `write_outside_sandbox_succeeded` | true/false | yes | the write succeeded |
|
|
568
|
+
| `probe_path` | text | | the path the app tried to write |
|
|
569
|
+
|
|
570
|
+
### `dyld_images`
|
|
571
|
+
|
|
572
|
+
Instrumentation and jailbreak names among the libraries loaded into the app: Frida and its Gadget,
|
|
573
|
+
Objection, Cynject, Cycript, Substrate and MobileSubstrate, Substitute, libhooker, ElleKit, Cydia,
|
|
574
|
+
Shadow, FLEX and libSparkAppList. It matches names, so a renamed library escapes it; `code_integrity`
|
|
575
|
+
compares the code itself. A library whose name the loader did not give (one unloading as the list is
|
|
576
|
+
read) is in `unread` as `image_names`, since it could be one of those.
|
|
577
|
+
|
|
578
|
+
**When:** your backend asks for it.
|
|
579
|
+
|
|
580
|
+
| Field | Type | Needed | Meaning |
|
|
581
|
+
|---|---|---|---|
|
|
582
|
+
| `suspicious_tokens` | list | yes | the listed names found, sorted |
|
|
583
|
+
| `suspicious_image_count` | integer, count | | loaded libraries with a listed name |
|
|
584
|
+
| `image_count` | integer, count | | libraries loaded |
|
|
585
|
+
|
|
586
|
+
### `environment`
|
|
587
|
+
|
|
588
|
+
Libraries injected through the environment: `DYLD_INSERT_LIBRARIES`, which loads a library into the app
|
|
589
|
+
as it starts.
|
|
590
|
+
|
|
591
|
+
**When:** your backend asks for it.
|
|
592
|
+
|
|
593
|
+
| Field | Type | Needed | Meaning |
|
|
594
|
+
|---|---|---|---|
|
|
595
|
+
| `dyld_insert_libraries` | text | yes | the variable's value, at most 8,192 characters; empty when it is not set |
|
|
596
|
+
|
|
597
|
+
### `simulator`
|
|
598
|
+
|
|
599
|
+
Whether the app runs in the iOS Simulator, and the hardware model.
|
|
600
|
+
|
|
601
|
+
**When:** your backend asks for it.
|
|
602
|
+
|
|
603
|
+
| Field | Type | Needed | Meaning |
|
|
604
|
+
|---|---|---|---|
|
|
605
|
+
| `is_simulator` | true/false | yes | the app runs in the Simulator |
|
|
606
|
+
| `model` | text | | the hardware model identifier, for example `iPhone15,2` |
|
|
607
|
+
|
|
608
|
+
### `code_integrity`
|
|
609
|
+
|
|
610
|
+
The app's own code compared in memory with its files, image by image: every image the loader mapped from
|
|
611
|
+
the app — the main executable and its frameworks — has its `__text` compared. iOS keeps its system
|
|
612
|
+
libraries in a shared cache that no app can read, so only the app's own code can be compared: the system
|
|
613
|
+
figures are always 0 — not measured, never clean — and `system_images_unreadable` counts those
|
|
614
|
+
libraries. Each app image is compared, unreadable (its file could not be read in full) or skipped (no
|
|
615
|
+
usable `__text`, a `__TEXT` over 64 MiB, no live address, or code encrypted on disk); images and bytes
|
|
616
|
+
both add up, expected = compared + unreadable + skipped. An app installed from the App Store has its
|
|
617
|
+
code encrypted in its files and decrypted only in memory, so the two differ in every encrypted byte of a
|
|
618
|
+
genuine app: such an image is skipped and named in `app_images_not_compared`, never compared — a
|
|
619
|
+
development, ad hoc or enterprise build is stored in the clear and is compared. A framework stored as a
|
|
620
|
+
universal file is compared through its arm64 image. An image whose file is another build than the one
|
|
621
|
+
running — an app updated while it ran — is skipped and named, never compared: the old code in memory and
|
|
622
|
+
the new file would differ in bytes nobody changed. An image the loader counts but gives no name for
|
|
623
|
+
counts as one not read. `app_complete` is true exactly when every image was compared, the main executable
|
|
624
|
+
among them. It comes with every scan; ask for it too, since a report that lacks it is refused
|
|
625
|
+
only when you asked for it.
|
|
626
|
+
|
|
627
|
+
**When:** every scan, whether or not your backend asks.
|
|
628
|
+
|
|
629
|
+
| Field | Type | Needed | Meaning |
|
|
630
|
+
|---|---|---|---|
|
|
631
|
+
| `checked` | true/false | yes | the comparison ran: at least one image was compared |
|
|
632
|
+
| `app_diff_bytes` | integer, bytes | yes | bytes that differ in the app's compared code |
|
|
633
|
+
| `app_compared_bytes` | integer, bytes | yes | the app's code compared, in bytes |
|
|
634
|
+
| `app_images_expected` | integer, count | when checked | the app's images the loader mapped |
|
|
635
|
+
| `app_images_compared` | integer, count | when checked | of those, the images compared |
|
|
636
|
+
| `app_images_unreadable` | integer, count | when checked | the images whose file could not be read in full |
|
|
637
|
+
| `app_images_skipped` | integer, count | when checked | the images skipped |
|
|
638
|
+
| `app_expected_bytes` | integer, bytes | when checked | the `__text` bytes of the app's images |
|
|
639
|
+
| `app_unreadable_bytes` | integer, bytes | when checked | those of the unreadable images |
|
|
640
|
+
| `app_skipped_bytes` | integer, bytes | when checked | those of the skipped images |
|
|
641
|
+
| `app_complete` | true/false | when checked | every app image was compared |
|
|
642
|
+
| `main_executable_compared` | true/false | when checked | the main executable was among the compared |
|
|
643
|
+
| `app_images_not_compared` | map | | each app image not compared, by its path in the app, with why |
|
|
644
|
+
| `system_images_unreadable` | integer, count | | the system libraries loaded, which no app can read |
|
|
645
|
+
| `app_libs_diff` | integer, count | | app images with a difference |
|
|
646
|
+
| `diffed_libs` | text | | the images with a difference, comma-separated |
|
|
647
|
+
| `diff_bytes` | integer | | always 0: iOS's system libraries cannot be read |
|
|
648
|
+
| `core_compared_bytes` | integer | | always 0, likewise |
|
|
649
|
+
| `core_diff_bytes` | integer | | always 0, likewise |
|
|
650
|
+
| `ext_compared_bytes` | integer | | always 0, likewise |
|
|
651
|
+
| `ext_diff_bytes` | integer | | always 0, likewise |
|
|
652
|
+
| `ext_libs_diff` | integer | | always 0, likewise |
|
|
653
|
+
| `system_bucket_reason` | text | | why the system figures are 0: `dyld_shared_cache_has_no_backing_files` |
|
|
654
|
+
| `app_segment_compared_bytes` | integer, bytes | | the whole `__TEXT` segments compared, beyond `__text`, in bytes: telemetry |
|
|
655
|
+
| `app_segment_diff_bytes` | integer, bytes | | bytes that differ across those segments: telemetry |
|
|
656
|
+
|
|
657
|
+
### `device`
|
|
658
|
+
|
|
659
|
+
The iOS version and build, the hardware model, and the memory page size. iOS has no separate security
|
|
660
|
+
patch level: its security fixes ship as iOS versions, and the build tells a Rapid Security Response
|
|
661
|
+
apart.
|
|
662
|
+
|
|
663
|
+
**When:** every scan, whether or not your backend asks.
|
|
664
|
+
|
|
665
|
+
| Field | Type | Needed | Meaning |
|
|
666
|
+
|---|---|---|---|
|
|
667
|
+
| `os_version` | text | yes | the iOS version, for example `17.6.1` |
|
|
668
|
+
| `os_build` | text | | the build, for example `21G93` |
|
|
669
|
+
| `model_identifier` | text | | the hardware model identifier, for example `iPhone15,2` |
|
|
670
|
+
| `page_size` | integer, bytes | | the memory page size: 16384 on Apple's 64-bit phones |
|
|
671
|
+
|
|
672
|
+
### `network`
|
|
673
|
+
|
|
674
|
+
Whether a VPN or an HTTP proxy is in use. iOS gives apps no way to ask about a VPN, so a VPN is inferred
|
|
675
|
+
from the network interfaces the system proxy settings are scoped to (`utun`, `ipsec`, `ppp`, `tun`,
|
|
676
|
+
`tap`): a heuristic, so the names come with it. The proxy is read from the system proxy settings;
|
|
677
|
+
whether an app's own requests follow it depends on how the app makes them.
|
|
678
|
+
|
|
679
|
+
**When:** every scan, whether or not your backend asks.
|
|
680
|
+
|
|
681
|
+
| Field | Type | Needed | Meaning |
|
|
682
|
+
|---|---|---|---|
|
|
683
|
+
| `vpn_active` | true/false | yes | an interface the proxy settings are scoped to is a VPN's kind |
|
|
684
|
+
| `http_proxy_configured` | true/false | | an HTTP, HTTPS or automatic proxy is set |
|
|
685
|
+
| `scoped_interfaces` | list | | the interfaces the proxy settings are scoped to, sorted, at most 32 — `en0` Wi-Fi, `pdp_ip0` cellular, `utun3` a tunnel |
|
|
686
|
+
|
|
687
|
+
### `local_addresses`
|
|
688
|
+
|
|
689
|
+
The phone's own IP addresses, per network interface. Loopback and link-local (`fe80`, `169.254`)
|
|
690
|
+
addresses are left out.
|
|
691
|
+
|
|
692
|
+
**When:** your backend asks for it and your app enables it — a personal measurement.
|
|
693
|
+
|
|
694
|
+
| Field | Type | Needed | Meaning |
|
|
695
|
+
|---|---|---|---|
|
|
696
|
+
| `addresses` | list | yes | each an `interface` (its name) and an `address` (IPv4 or IPv6), at most 32 |
|
|
697
|
+
| `truncated` | true/false | | the phone had more than the 32 addresses a list holds (Android collector 10, iOS collector 8) |
|
|
698
|
+
|
|
699
|
+
### `screen_capture`
|
|
700
|
+
|
|
701
|
+
Whether the screen showing the app is being recorded, mirrored or sent to AirPlay
|
|
702
|
+
(`UIScreen.isCaptured`, asked of the screen of each of the app's windows in the foreground), and how
|
|
703
|
+
many other screens are attached. With no window of the app's in the foreground — the app in the
|
|
704
|
+
background, or before its first window — recording is not measured: it is left out, unknown rather than
|
|
705
|
+
"no", and `recording_status` says why.
|
|
706
|
+
|
|
707
|
+
**When:** your backend asks for it and your app enables it — a personal measurement.
|
|
708
|
+
|
|
709
|
+
| Field | Type | Needed | Meaning |
|
|
710
|
+
|---|---|---|---|
|
|
711
|
+
| `recording` | true/false | yes | the screen showing the app is being captured |
|
|
712
|
+
| `external_displays` | integer, count | yes | other screens attached |
|
|
713
|
+
| `recording_status` | one of `unsupported`, `no_permission`, `unavailable` | | why `recording` is absent: `unavailable`, no window of the app's in the foreground to ask. iOS has measured recording since iOS 11, so `unsupported` never comes from iOS |
|
|
714
|
+
|
|
715
|
+
### `location`
|
|
716
|
+
|
|
717
|
+
The last location the phone already holds — never a fresh fix, never a prompt — read once per scan, so
|
|
718
|
+
that both location answers describe the same fix. Coordinates come only with the location permission,
|
|
719
|
+
which your app asks for itself (your SDK's README says how).
|
|
720
|
+
|
|
721
|
+
**When:** your backend asks for it and your app enables it — a personal measurement.
|
|
722
|
+
|
|
723
|
+
| Field | Type | Needed | Meaning |
|
|
724
|
+
|---|---|---|---|
|
|
725
|
+
| `permission` | one of `none`, `approximate`, `precise` | yes | the app's location permission: `approximate` when the person allowed an approximate location only |
|
|
726
|
+
| `latitude` | number, degrees, WGS 84 | yes | the fix's latitude; absent without the permission or without a fix |
|
|
727
|
+
| `longitude` | number, degrees, WGS 84 | yes | the fix's longitude; absent likewise |
|
|
728
|
+
| `age_seconds` | integer, seconds | yes | how old the fix is |
|
|
729
|
+
| `accuracy_m` | number, metres | | its horizontal accuracy |
|
|
730
|
+
| `fix_time` | integer, Unix seconds, UTC | | when it was made |
|
|
731
|
+
|
|
732
|
+
### `mock_location`
|
|
733
|
+
|
|
734
|
+
Whether that same fix was simulated by software, by Apple's own flag, or produced by an external
|
|
735
|
+
accessory. Apple's flag catches the simulation of its own developer tools; Apple does not promise it for
|
|
736
|
+
other spoofing apps. It never carries coordinates.
|
|
737
|
+
|
|
738
|
+
**When:** your backend asks for it and your app enables it — a personal measurement.
|
|
739
|
+
|
|
740
|
+
| Field | Type | Needed | Meaning |
|
|
741
|
+
|---|---|---|---|
|
|
742
|
+
| `permission` | one of `none`, `approximate`, `precise` | yes | the app's location permission |
|
|
743
|
+
| `mock` | true/false | yes | iOS says the fix was simulated by software; absent when iOS did not say |
|
|
744
|
+
| `age_seconds` | integer, seconds | yes | how old the fix is |
|
|
745
|
+
| `accessory` | true/false | | the fix came from an external accessory, such as a GPS receiver |
|
|
746
|
+
| `fix_time` | integer, Unix seconds, UTC | | when it was made: the same as `location`'s in one report |
|
|
747
|
+
|
|
748
|
+
### `accessibility`
|
|
749
|
+
|
|
750
|
+
iOS shows an app no other app, so there is nothing to list: this answers `unsupported` when your app
|
|
751
|
+
enables it, and `not_enabled` when it does not. No fields.
|
|
752
|
+
|
|
753
|
+
**When:** your backend asks for it and your app enables it — a personal measurement.
|
|
754
|
+
|
|
755
|
+
### `notification_access`
|
|
756
|
+
|
|
757
|
+
iOS shows an app no other app, so there is nothing to list: this answers `unsupported` when your app
|
|
758
|
+
enables it, and `not_enabled` when it does not. No fields.
|
|
759
|
+
|
|
760
|
+
**When:** your backend asks for it and your app enables it — a personal measurement.
|
|
761
|
+
|
|
762
|
+
### `other_access`
|
|
763
|
+
|
|
764
|
+
iOS shows an app no other app, so there is nothing to list: this answers `unsupported` when your app
|
|
765
|
+
enables it, and `not_enabled` when it does not. No fields.
|
|
766
|
+
|
|
767
|
+
**When:** your backend asks for it and your app enables it — a personal measurement.
|
|
768
|
+
|
|
769
|
+
## What no measurement can prove
|
|
770
|
+
|
|
771
|
+
A fully compromised phone can report anything, and a modified app that holds a registered key signs
|
|
772
|
+
whatever it is given: the signature proves which installation sent a report, not that the phone is
|
|
773
|
+
honest or that the SDK measured what it reports (integration guide, section 9). The measurements raise
|
|
774
|
+
the cost of fraud; your backend weighs them with its own business logic.
|