Saturday, January 7, 2023

how does safetynet work? (fight with instagram)

hello from new year! (i'm starting to write the article on 31.12.2022. i'm wondering too when i can finish and publish it :=D) i dont like for new year nights but okay, no problem. today, i will explain safetynet structure and safetynet attestion api reverse engineering on this post.

What is the "SafetyNet"

basically SafetyNet is an API developed by google that implements various procedures to ensure code security in Android application. google Play services must be installed on your device in order to use the Safetynet APIs. 

SafetyNet verifies your application by performing tests called CTS (Compatibility Test Suite) on your application. basically it checks criteria including device root status, OEM lock status, google play version, security of device traffic (sniffing or not?), app signature, etc.




SafetyNet performs an attestation (SafetyNetApi.attest()) operation using these safety parameters. As a result of the attestation process, a JWT response will be returned to you according to the information reported from your device.

in this JWT token we have 2 boolean values with keys ctsProfileMatch and basicIntegirty. These values are the values that show whether your device is marked as safe in line with the examination of SafetyNet safety parameters.

apart from these values, it also contains values such as your package name (apkPackageName), sha256 signature of your apk file (apkDigestSha256), sha256 hash of your apk certificate (apkCertificateDigestSha256).

CTS Profile Match: bootloader unlocked?, custom ROM?, uncertified device? etc..
Basic Integrity: is an emulator?, rooted device?, any agent injected? (frida etc..) etc..

if you fail the Basic Integrity and CTS Profile Match tests, your application will likely flag you. speaking for Instagram, it blocks your account until a certain date as soon as you create an account. each app may react differently.

SafetyNet WorkFlow


well, i hope  we understood the part of "what is a safety net and basically how does security work?". our next step SafetyNet WorkFlow.


pic 1.1
(keep in mind the underlined text in this picture. we will come back this picture later)

1 - to use safetynet, you must create an API key for yourself from the Google API Console and include this API key in the application. safetynet receives a nonce value from you and an API_KEY value to validate your API. (this "nonce" value must be unique.) If the values you provide are correct, you will receive a JWT token response. If the information you provide is incorrect / suspicious, an error message will be returned to you.



attestation api basically works with a "nonce" value. the length of this value is 16 bytes. as stated in the android developer document, you can convert this value into a hash as you wish. but it will be a 16 byte value that safetynet needs. 

this value generate from mobile app side. request is sent to the safetynet API from the mobile application with this created "nonce" value.

2 - safetynet works integrated with DroidGuard. SafetyNet result and droidguard result are considered together in data reported to GMS side. this data you create is sent to GMS Core. GMS Core creates a protobuf message using this data. (proto schematics: https://github.com/microg/GmsCore/blob/ad12bd5de4970a6607a18e37707fab9f444593a7/play-services-core-proto/src/main/proto/snet.proto#L15-L25)

basically, this message contains information such as your gms version, package name of your application, signature hash. and here it reports whether your device is rooted in the suCandidates parameter and the SELinux status in the seLinuxState parameter.

now let's reinforce a little on instagram. let's make a little fridascript and list the gms classes.


Java.perform(function() {
    Java.enumerateLoadedClasses({
        onMatch: function(className) {
            if(className.includes("android.gms")){
				console.log("founded classes: " + className);
			}
        },
        onComplete: function() {}
    });
});

output:


we examined the classes of the com.google.android.gms package, which is the standard package name of GMS, with a simple fridascript. Before proceeding to the review, a class in the class names should have caught your attention :=)

if you noticed, let's take a closer look at the 'com.google.android.gms.common.GoogleApiAvailability' class



let's review A02 class





the most basic step of the safetynet, "google play services installed? can it be available?" The class where the control is done on Instagram. this class checks google play services on your device and sends an error message when sending safetynet report if google play services is not available or unavailable on your device.

this method basically check google play services existing status, signature validations and google play services update status (outdated or updated). this is one of the simplest principles of GMS that we mentioned above. let's make sure it's called by hooking the function call with frida


Java.perform(function() {
    var google_api = Java.use('com.google.android.gms.common.GoogleApiAvailability');

    var google_api_hook = google_api.isGooglePlayServicesAvailable.overload("android.content.Context");
    google_api_hook.implementation = function(context0) {
        console.log('\n hooked: '+ context0);
		var return_val = google_api_hook.call(this, context0);
		console.log("return value: " + return_val)
        return google_api_hook.call(this, context0);
    };
});

response:

yep, called :=) isGooglePlayServicesAvailable function arguments are context value for current context and int value for google play version. The int value checks if google play services is outdated. context value is current context data.

int variable "v" check line

as an example, I simply tried to show you the point where it detects the google play service. but applications also have various controls other than that. therefore, you may need to analyze not only the GMS side, but also the entire application and intervene in the control points. since we focus on SafetyNet in this article, I do not elaborate on these issues.

now that we've shown an example, let's dig a little deeper!

SafetyNet Attestation Analyze on Instagram

let's start with capturing traffic. you can capture traffic with this fridascript on github, it works. i intercept all the requests up to the account creation step and reviewed the post data in the account creation step.


well, we encountered the sn_nonce value :=)  base64 is a value and when we decode it as you can see in the picture, we see a structure like this. 


yes the email address in base64 is not the same as in the picture. because I used temp-mail to sign up.

does this remind you of anything? yep, same on pic 1.1 :=) 

when generating the Instagram safety nonce value, it uses your email address + current timestamp + and random 24 byte parameters that you used during registration. to separate them | uses the bracket.




another point that draws our attention is that the isGooglePlayServicesAvailable function of GMS, which we have just mentioned, is called here again :=) that is, they call the same function once again and provide control.

let's examine one by one.

stringBuilder0.append(s);

since the value of s comes as an argument to the function, it is directly appended to the payload. this value corresponds to the email address.


  long v = System.currentTimeMillis() / 1000L;
  
this line divides timestamp by /1000 to match its format in payload. Thus, it creates the 2nd parameter, the timestamp.

byte[] arr_b = new byte[24];
new SecureRandom().nextBytes(arr_b);

and generates a 24-byte random value using SecureRandom(), which is the last stage of the payload. and finally, after each process;

stringBuilder0.append("|");


 it completes the payload by adding its "|" separator. 

well, we got how to generate sn_nonce payload. after this step i prepared a small frida script by following the functions to see 2 different outputs on the emulator and on the real device. let's see the frida script and its output.



  /*

com.instagram.nux.deviceverification.impl.VerificationPluginImpl.startDeviceValidation(android.content.Context, java.lang.String) : void
Descriptor: Lcom/instagram/nux/deviceverification/impl/VerificationPluginImpl;->startDeviceValidation(Landroid/content/Context;Ljava/lang/String;)V

target package: package com.instagram.nux.deviceverification.impl;

*/

function googlePlayAvailable(){
	var GoogleApiAvailability = Java.use('com.google.android.gms.common.GoogleApiAvailability');

	var GoogleApiAvailability_isGooglePlayServicesAvailable_0 = GoogleApiAvailability.isGooglePlayServicesAvailable.overload("android.content.Context");
	GoogleApiAvailability_isGooglePlayServicesAvailable_0.implementation = function(context0) {
		console.log(`[+] Hooked com.google.android.gms.common.GoogleApiAvailability.isGooglePlayServicesAvailable(context0)`);
		var return_call = GoogleApiAvailability_isGooglePlayServicesAvailable_0.call(this, context0);
		console.log("isGooglePlayServicesAvailable() return value before changing: " + return_call);
		return 0;
	};
}

function xCaj(){
	var CAj = Java.use('X.CAj');

    var CAj_init_0 = CAj.$init.overload("java.lang.String", "java.lang.Integer", "java.lang.String");
    CAj_init_0.implementation = function(s, integer0, s1) {
		console.log("safetynet params - CAj$init got:")
        console.log(`[+] Hooked X.CAj.$init(s, integer0, s1)`);
		console.log(s);
		console.log(integer0);
		console.log(s1);
        return CAj_init_0.call(this, s, integer0, s1);
    };
}

function startDeviceValidation(){
	var VerificationPluginImpl = Java.use('com.instagram.nux.deviceverification.impl.VerificationPluginImpl');

    var VerificationPluginImpl_startDeviceValidation_0 = VerificationPluginImpl.startDeviceValidation.overload("android.content.Context", "java.lang.String");
    VerificationPluginImpl_startDeviceValidation_0.implementation = function(context0, s) {
		console.log("safetynet generate function - startDeviceValidation() got:")
		console.log(s);
		console.log(context0);
        console.log(`[+] Hooked com.instagram.nux.deviceverification.impl.VerificationPluginImpl.startDeviceValidation(context0, s)`);
        return VerificationPluginImpl_startDeviceValidation_0.call(this, context0, s);
    };
}

function safetyInner(){	
	var x5Vo = Java.use('X.5Vo');

    var x5Vo_A0w_0 = x5Vo.A0w.overload("java.lang.String", "java.lang.StringBuilder");
    x5Vo_A0w_0.implementation = function(s, stringBuilder0) {
		console.log("safetynet function inner - A0w() return:");
		console.log(s);
		console.log(stringBuilder0);
        console.log(`[+] Hooked X.5Vo.A0w(s, stringBuilder0)`);
        return x5Vo_A0w_0.call(this, s, stringBuilder0);
    };
}


function safety_probably(){
    var x9dr = Java.use('X.9dr');
    var x9dr_init_0 = x9dr.$init.overload("X.4eo", "java.lang.String", "[B");
    x9dr_init_0.implementation = function(x4eo0, s, arr_b) {
        console.log(`[+] Hooked X.9dr.$init(4eo0, s, arr_b)`);
		console.log("class val:")
		console.log(x4eo0);
		console.log("string val: " + s);
		console.log("byte val:")
		console.log(arr_b);
        return x9dr_init_0.call(this, x4eo0, s, arr_b);
    };
}

function feedback_required(){
    var x2So = Java.use('X.2So');
    var x2So_A03_0 = x2So.A03.overload("X.3m7", "X.1Id");
    x2So_A03_0.implementation = function(x3m70, x1Id0) {
		console.log("x3m70" + x3m70);
		console.log("x1Id0" + x1Id0);
        console.log(`[+] Hooked X.2So.A03(3m70, 1Id0)`);
        return x2So_A03_0.call(this, x3m70, x1Id0);
    };
	
}

Java.perform(function() {
    xCaj();
	startDeviceValidation();
	googlePlayAvailable();
	safetyInner();
	safety_probably();
	feedback_required();
});

  

response from emulator:



meh, we got error. because google services not installed on emulator. so it returned an error that it could not find the gms core safetynet api.


i changed the return value of the isGooglePlayServicesAvailable function to 0 in fridascript. because 1 fail is defined as 0 successful value.

let's see the output of the same script on real device


great! we got jwt. but if we decrypt this jwt, we will see that everything is not ok :=)


we mentioned basicIntegrity and ctsProfileMatch values above. you see it's "false" here :=) because the device is rooted, OEM unlocked and there are too many detectable components inside.

SafetyNet has different error messages for each situation. For example, if we had re-signed this apk, we would have received another signature-related error this time. but now it returns basicIntegrity and ctsProfileMatch values as false because it realizes that the device's ROM has changed. If you take a look at the advice value, you will see "RESTORE_TO_FACTORY_ROM", that is, restore the rom settings :=)

when you connect to your device from adb and type sestatus, you will understand why it gives this error :=)

in this article, if I explain how we can make these values true, that is, how we can pass the control mechanisms, the article will be too long. It's been long enough with its current state :=) so I'll leave the rest for the next post.

let's finish this article by basically dealing with the issue of safetynet and working in an application as an example. see you in another article!

you can also find the fridascripts used in the github repo at this link.

thanks for:

https://www.romainthomas.fr/publication/22-sstic-blackhat-droidguard-safetynet/

https://www.blackhat.com/docs/eu-17/materials/eu-17-Mulliner-Inside-Androids-SafetyNet-Attestation-wp.pdf