SafetyNetApi.attestmethod (which returns a JWS message with the Attestation Result) and then check the following fields:
ctsProfileMatch: If 'true', the device profile matches one of Google's listed devices.
basicIntegrity: If 'true', the device running the app likely hasn't been tampered with.
nonces: To match the response to its request.
timestampMs: To check how much time has passed since you made the request and you got the response. A delayed response may suggest suspicious activity.
apkDigestSha256: Provide information about the APK, which is used to verify the identity of the calling app. These parameters are absent if the API cannot reliably determine the APK information.
basicIntegrityto help developers determine the integrity of a device. As the API evolved, Google introduced a new, stricter check whose results appear in a value called
ctsProfileMatch, which allows developers to more finely evaluate the devices on which their app is running.
basicIntegritygives you a signal about the general integrity of the device and its API. Many Rooted devices fail
basicIntegrity, as do emulators, virtual devices, and devices with signs of tampering, such as API hooks.
ctsProfileMatchgives you a much stricter signal about the compatibility of the device. Only unmodified devices that have been certified by Google can pass
ctsProfileMatch. Devices that will fail
ctsProfileMatchinclude the following:
apkDigestSha256) only if the value of
verifymethod only validates that the JWS message was signed by SafetyNet. It doesn't verify that the payload of the verdict matches your expectations. As useful as this service may seem, it is designed for test purposes only, and it has very strict usage quotas of 10,000 requests per day, per project which will not be increased upon request. Hence, you should refer SafetyNet Verification Samples and implement the digital signature verification logic on your server in a way that it doesn't depend on Google's servers.
SafetyNetApi.attestquota and getting attestation errors, you should build a system that monitors your usage of the API and warns you well before you reach your quota so you can get it increased. You should also be prepared to handle attestation failures because of an exceeded quota and avoid blocking all your users in this situation. If you are close to reaching your quota, or expect a short-term spike that may lead you to exceed your quota, you can submit this form to request short or long-term increases to the quota for your API key. This process, as well as the additional quota, is free of charge.
suis on the PATH also works:
statsystem call to retrieve information about a file and returns "1" if the file exists.
suand other commands
suexists is attempting to execute it through the
Runtime.getRuntime.execmethod. An IOException will be thrown if
suis not on the PATH. The same method can be used to check for other programs often found on rooted devices, such as busybox and the symbolic links that typically point to it.
daemonsu, so the presence of this process is another sign of a rooted device. Running processes can be enumerated with the
pscommand, and browsing through the
/procdirectory. The following is an example implemented in rootinspector:
strace, and/or kernel modules to find out what the app is doing. You'll usually see all kinds of suspect interactions with the operating system, such as opening
sufor reading and obtaining a list of processes. These interactions are surefire signs of root detection. Identify and deactivate the root detection mechanisms, one at a time. If you're performing a black box resilience assessment, disabling the root detection mechanisms is your first step.
subinary is enough to defeat root detection (try not to break your environment though!).
/procto prevent reading of process lists. Sometimes, the unavailability of
/procis enough to bypass such checks.
ro.debuggablesystem property which enables debugging for all apps. Let's look at a few things developers do to detect and disable JDWP debuggers.
android:debuggableattribute. This flag in the Android Manifest determines whether the JDWP thread is started for the app. Its value can be determined programmatically, via the app's
ApplicationInfoobject. If the flag is set, the manifest has been tampered with and allows debugging.
android.os.Debugclass to determine whether a debugger is connected.
Debug.threadCpuTimeNanosindicates the amount of time that the current thread has been executing code. Because debugging slows down process execution, you can use the difference in execution time to guess whether a debugger is attached.
DvmGlobalsstructure. The global variable gDvm holds a pointer to this structure.
DvmGlobalscontains various variables and pointers that are important for JDWP debugging and can be tampered with.
JdwpAdbState, which handle JDWP connections via network sockets and ADB, respectively. You can manipulate the behavior of the debugging runtime by overwriting the method pointers in the associated vtables (archived).
jdwpAdbState::ProcessIncomingwith the address of
JdwpAdbState::Shutdown. This will cause the debugger to disconnect immediately.
ptracesystem call is used to observe and control the execution of a process (the tracee) and to examine and change that process' memory and registers.
ptraceis the primary way to implement system call tracing and breakpoint debugging in native code. Most JDWP anti-debugging tricks (which may be safe for timer-based checks) won't catch classical debuggers based on
ptraceand therefore, many Android anti-debugging tricks include
ptrace, often exploiting the fact that only one debugger at a time can attach to a process.
ptraceto attach to the process. From this moment on, if you inspect the status file of the debugged process (
/proc/self/status), you will see that the "TracerPid" field has a value different from 0, which is a sign of debugging.
Remember that this only applies to native code. If you're debugging a Java/Kotlin-only app the value of the "TracerPid" field should be 0.
IsDebuggerAttachedmethod. However, if you prefer to include this check as part of your Java/Kotlin code you can refer to this Java implementation of the
hasTracerPidmethod from Tim Strazzere's Anti-Emulator project.
/procfilesystem, such as TracerPID in
fork, we launch in the parent an extra thread that continually monitors the child's status. Depending on whether the app has been built in debug or release mode (which is indicated by the
android:debuggableflag in the manifest), the child process should do one of the following things:
waitpid(child_pid)should never return. If it does, something is fishy and we would kill the whole process group.
_exitwith NOPs and hooking the function
libc.so). At this point, we have entered the proverbial "arms race": implementing more intricate forms of this defense as well as bypassing it are always possible.
isDebuggerConnectedto hide the debugger.
CodeCheckis responsible for verifying the code entered by the user. The actual check appears to occur in the
barmethod, which is declared as a native method.
SharedPreferencesshould be protected.
SharedPreferences) or create an HMAC over a complete file that's provided by the file system.
doFinalon the HMAC with the bytecode.
classes.dexand any .so libraries in the app package. Re-package and re-sign the app as described in the "Basic Security Testing" chapter, then run the app. The app should detect the modification and respond in some way. At the very least, the app should alert the user and/or terminate. Work on bypassing the defenses and answer the following questions:
/proc/<pid>/mapsyou'll find the frida-agent as frida-agent-64.so:
Some of the following detection methods are presented in the article "The Jiu-Jitsu of Detecting Frida" by Berdhard Mueller (archived). Please refer to it for more details and for example code snippets.
getRunningServices) and processes (
ps) searching for one whose name is "frida-server". You could also walk through the list of loaded libraries and check for suspicious ones (e.g. those including "frida" in their names).
frida-serveruses the D-Bus protocol to communicate, so you can expect it to respond to D-Bus AUTH. Send a D-Bus AUTH message to every open port and check for an answer, hoping that
frida-serverwill reveal itself.
frida-server, but Frida offers alternative modes of operation that don't require frida-server.
Runtime.getRuntime().execand iterate through the memory mappings listed in
/proc/<pid>/maps(depending on the Android version) searching for the string.
It is important to note that these controls are only increasing the complexity of the reverse engineering process. If used, the best approach is to combine the controls cleverly instead of using them individually. However, none of them can assure a 100% effectiveness, as the reverse engineer will always have full access to the device and will therefore always win! You also have to consider that integrating some of the controls into your app might increase the complexity of your app and even have an impact on its performance.
build.propon a rooted Android device or modify it while compiling AOSP from source. Both techniques will allow you to bypass the static string checks above.
TelephonyManager.getDeviceIDmethod to return an IMEI value.
ld, which resolves symbol addresses only after they are needed for the first time (lazy binding), the Android linker resolves all external functions and writes the respective GOT entries immediately after a library is loaded (immediate binding). You can therefore expect all GOT entries to point to valid memory locations in the code sections of their respective libraries during runtime. GOT hook detection methods usually walk the GOT and verify this.