This post is part of a series:
- Inside SafetyNet part 1 (Oct 2015)
- Inside SafetyNet part 2 (Feb 2016)
- Inside SafetyNet part 3 (Nov 2016)
- How to implement Attestation securely using server-side checks (my blog, Cigital blog)
- SafetyNet Playground (POC server-side implementation) Play Store - Android source - PHP source
It’s been more than 8 months since my last blog post on Android’s SafetyNet. In that post I was describing an end-of-2015 version of the system (version code 2495818
). As expected, there have been several updates since then; I thought I should write one more post in this series, probably the last. I’ll briefly describe the differences of that last reviewed version versus version 10000700
; this is second-to-last version. The latest version I’ve seen is 10000801
, but it doesn’t have many functional differences.
For a more complete overview of the client-side checks inside the SafetyNet system and its usage please read through my previous posts.
SNET version codes
I thought I’d just list some of the SNET versions I’ve observed in the wild; I’m sure there are many more.
1626247
(December 2014)1839652
(April 2015)2097462
(July 2015)2296032
(September 2015)2495818
(December 2015)10000700
(August 2016)10000801
(September 2016)
Design changes
SafetyNet got partially integrated into the Chimera
system as a dynamite module. Chimera is a sort-of package management system for Google Play Services components, allowing Google to flexibly and independently upgrade each one. As Chimera manages part of the download and provisioning process, this change made the snet package lighter and more flexible. Chimera also has a nice interface, found by going into Google Settings and enabling “debug items”. You’ll see an “[internal]” menu item called “Chimera Modules”. The SafetyNet module is called com.google.android.gms.flags
, currently version 1
. You’ll also find that DroidGuard
is another dynamite module.
basicIntegrity
Discovered new element "basicIntegrity: true/false" in Android's SafetyNet Attestation. Need to investigate what this indicates. #android
— Collin Mulliner (@collinrm) July 6, 2016
At least since last July, Attestation responses objects contain a new boolean flag called basicIntegrity
.
This new field does not appear in Google’s published documentation yet. I’m sure it will soon.
{
"nonce": "R2Rra24fVm5xa2Mg",
"timestampMs": 9860437986543,
"apkPackageName": "com.package.name.of.requesting.app",
"apkCertificateDigestSha256": ["base64 encoded, SHA-256 hash of the certificate used to sign requesting app"],
"apkDigestSha256": "base64 encoded, SHA-256 hash of the app's APK",
"ctsProfileMatch": true,
"basicIntegrity": true,
}
But what is this field? Here are some thoughts, the way I understand it:
In September 2016, Google decided to introduce more aggressive checks into ctsProfileMatch
, e.g. acting on VerifiedBoot status. Due to these changes, devices that are not “rooted” but may only use a different bootloader will cause ctsProfileMatch
to be set to false
.
In such cases, basicIntegrity
will still remain true
. It seems that this is set to false only if an su
binary is placed in expected locations. The basicIntegrity
field currently seems to behave like ctsProfileMatch
did before the recent changes. One could see it as a way to maintain the previous ctsProfileMatch
behavior, so that 3rd party apps can choose the level of checks they want to base their decisions on. I have yet to see any case where basicIntegrity
is true while ctsProfileMatch
is false; let me know in the comments if you do.
I also updated Cigital’s SafetyNet Playground app in Google Play. It now checks and reports the value of basicIntegrity
. Check it out!
DroidGuard
It’s safe to say that DroidGuard plays a bigger role into attestation than I have previously described. It is (and has always been) a packed native library, designed to somewhat withstand reverse engineering, which makes dealing with it more interesting. Another blogpost on this may come in the future.
Safe Browsing
SafetyNet - from a 3rd party app perspective, used to initially be only about Attestation and CTS compatibility.
It now offers another flavor: A lookupUri()
API that allows apps to check if a given URI is classified as Potentially Harmful App by Google’s threat intelligence systems.
More details on this here and here.
I’ve not described Safe Browsing in any of my blog posts yet, this may come in the future - just like Verify Apps & DroidGuard.
Minification
As of version 10000801
SafetyNet is making use of ProGuard-style minification (some call it obfuscation). This is an interesting change of heart. Initially Google seemed to leave things unobfuscated on purpose in order to increase transparency, however this appears to have changed.
It is important to note that this change happened at the same time as the cat-and-mouse game between SafetyNet and various “bypasses” intensified after the recent device integrity change. I am sure that SafetyNet re-implementations like this might have affected this decision.
SafetyNet module changes
A few but important new modules have been added in recent versions and some older ones were restructured. As mentioned above, SafetyNet is configured by Google at runtime or via play services updates; the snet module itself updated independently less often.
What follows is the list of snet modules that are currently enabled by default. Note that a few extra modules have been enabled compared to a year ago. But the real differences are of course not here; the real differences are in the way data from each module affects backend attestation decisions.
Idle mode modules
Idle mode checks appear to run every 12 hours. The last time an idle mode scan run is stored in snet’s shared preferences inside the Play Services app private files. The following modules are now always enabled by default in idle mode.
- gmscore
- system_partition_files
- system_ca_cert_store
- setuid_files
- dalvik_cache_monitor
- device_state
- locale
- selinux_status
- logcat
- event_log
The gmscore
, locale
and selinux_status
modules have now been turned ON comared to a year ago.
The gmscore
module retrieves info about the com.google.android.gms
package installed on the device via PackageManager APIs, including versionCode
, hashes, signatures etc.
The locale
module obviously retrieves the current locale, including country code.
The selinux_status
modules will be described in a moment.
Normal mode modules
The following modules are now always enabled by default in normal mode. Normal mode ‘checks’ appear to run when a 3rd party app requests an attestation or at a maximum every 24 hours.
- default_packages
- su_files
- settings
- locale
- ssl_redirect
- ssl_handshake
- proxy
- selinux_status
- sd_card_test
- google_page_info
- captive_portal_test
- attest
- gmscore
- device_state
- carrier_info
- logcat
- event_log
The following non-default modules are also currently enabled by play services config:
- mx_record
- sslv3_fallback
Compared to a year ago, the only differences appear to be that the following two modules are now turned ON: device_state
and carrier_info
. The device_state
module is of course very important and is directly related to the recent verified boot blocks. It’s described in more detail below.
SU files module
The SU finding modules has been partially redesigned since it was first introduced. It reports back information about two sets of files:
- files explicitly requested by Google
- su binaries
The first category of files comes from snet configuration options, shipped separately from snet itself as part of Google play services. It currently includes a single file: /proc/sunxi_debug/sunxi_debug
. This file is a well known kernel backdoor allowing easy root on some devices.
The second category has been split out into a new rooting file finder submodule. This works as follows:
SNET assembles a combined list of two types of directories to search into:
interesting directories: These by default include
/system/bin
and/system/xbin
and all directories specified inPATH
, if such environment variable exists.systemless root directories: SNET attempts to identify directories that might be bind-mounted by parsing
/proc/self/mountinfo
and figuring out if/bin
or/xbin
are mounted using via/dev/block/loop
. I will not go over the details of how systemless root works in this post.
SNET attempts to find if the
su
binary exists in any of these directories. If it does, it gathers information about it, including its SHA256 hash, if it’s a symlink and its target, ownership/permissions/selinux info (lstat) etc. It now also checks if the file is executable - this is done viajava.libcore.io.Os.access(file, X_OK)
.
Settings module
This module is used to retrieve various pieces of information about system settings and is run as part of the ‘normal-mode’ checks. It has changed to also retrieve two more pieces of info:
Storage Encryption Status
This is retrieved via the
getStorageEncryptionStatus()
method of thedevice_policy
system service if SDK >= 11.Fingerprint Status
This is retrieved via the
isHardwareDetected()
andhasEnrolledFingerprints()
methods of thefingerprint
system service if SDK >= 23. This can return the following values:- FINGERPRINT_ENROLLED = 1
- FINGERPRINT_NOT_SUPPORTED = 0
- FINGERPRINT_UNENROLLED = 2
Device State module
This module used to gather the following data:
- Verified Boot state via
ro.boot.verifiedbootstate
- Verity mode via
ro.boot.veritymode
- Security Patch Level via
ro.build.version.security_patch
- Unlock Support via
ro.oem_unlock_supported
- State of Flash Lock (oemLocked): via
ro.boot.flash.locked
Now the following have been added:
- Device Brand via
ro.product.brand
- Device Model via
ro.product.model
- Kernel Version from
/proc/version
- List of System properties explicitly specified by Google via play services.
- currently just
ro.build.characteristics
- currently just
On API>23, flash lock state is now retrieved via PersistentDataBlockManager.getFlashLockState()
which is a new wrapper API to the ro.boot.flash.locked
property.
Possible values are:
- FLASH_LOCK_STATE_LOCKED = 1 (0x1)
- FLASH_LOCK_STATE_UNKNOWN = -1 (0xffffffff)
- FLASH_LOCK_STATE_UNLOCKED = 0 (0x0)
Some people were surprised back in September this year, when Google started blocking devices that were not rooted but had unlocked their bootloader. First of all, unlocked bootloaders cause verified boot to fail - that’s what’s triggering snet. Not much changed in client-side code: SafetyNet was retrieving ro.boot.verifiedbootstate
and ro.boot.veritymode
all along, gathering metrics, until someone made the decision to make these indicators influence the ctsProfileMatch
boolean flag (but not basicIntegrity
).
dm-verity correction info checks
For idle-mode checks, the Device State communicated back to Google now also includes dm-verity correction information for SDK > 23. Some of you may know tha Android N introduced Verified Boot with Error Correction. This is all described here and here and here. Forward Error Correction is of course used to recover from filesystem corruption. An interesting security “side-effect” is that the definition of ‘curruption’ extends to ‘tampering’, so this feature can effectively repair rooted filesystems)
SafetyNet searches all device mapper directories (e.g. /sys/block/dm-0
, /sys/block/dm-1
) and looks for a directory named fec
, signalling that a partition is using Forward Error Correction. If fec
is found, SNET retrieves the partition name (/sys/block/dm-X/dm/name
) and the fec record file (/sys/block/dm-X/fec/corrected
) and sends these back, allowing Google to track how this new feature is used.
System Partition Files module
In my previous blogpost I briefly described this system performs integrity measurements, retrieving hash trees over the /system
directory and reporting them back, along with other info, to a System Integrity service separate from SNET.
A year ago the SIC Server URL was empty but this has now been filled in. It is: https://sb-ssl.google.com/safebrowsing/clientreport/system-integrity
This appears to be an undocumented part of the Safe Browsing system.
SELinux checking module
Previously this module only retrieved:
- whether selinux is supported
- if it’s in enforcing mode via
/sys/fs/selinux/enforce
.
Now, it also retrieves:
- the version via
/selinux_version
- the SHA256 hash of the policy file (
/sepolicy
)
SSL Handshake module
As discussed before, this module attempts to figure out if communications can be intercepted in a number of ways, such as via having an SSL-Kill-Switch app installed. It basically attempts to find malicious TrustManagers. In the last few versions it has been significantly refactored.
Like before, the code attempts to contact accounts.google.com
and www.google.com
. The third host is play.google.com
, replacing pubads.g.doubleclick.net
.
For each host, the module attempts to do an SSL handshake (sslContext.getSocketFactory().createSocket(hostname, 443)
) and now, separately, an HTTPS connection (new URL("https", hostname, "").openConnection()
)
Both use a custom all-trusting X509TrustManager to establish a secure connection and retrieve the server’s SSL certificates. Then the module finds the system’s first X509TrustManager (instead of all of them like it did before).
It then uses this TrustManager (via checkServerTrusted()
to verify the server’s chain. It is interesting to note that now, on API>=21, the module uses X509TrustManagerExtensions.checkServerTrusted()
instead of checkServerTrusted()
, in order to retrieve the validated certificate chain. This is good news; bad things happen when these differ, as seen in CVE-2016-2402. In a change since previous versions, even on API<21 there's code to re-create the validate certificate chain - great stuff. As discussed before this module also checks if certificates are valid because they have been added by a user or not; these methods now, for API >=21 use X509TrustManagerExtensions.isUserAddedCertificate()
instead of manually checking /data/misc/keychain/cacerts-added
.
SDCard analyzer module
This module now also retrieves the last modification time of JPEG file it attempts to store on the SD card.
Carrier Info module
One new module was introduced into SafetyNet: “Carrier Info”
This just retrieves the Name of the current carrier by using the getNetworkOperatorName()
method of the phone
system service.