Overview#
Google offers an automatic integrity protection
feature for apps through Play Integrity Protect. The API for this protection includes
additional classes located in the com/pairip/
package, which are responsible for
verifying the app’s integrity.
/* ... */
static {
System.loadLibrary("pairipcore");
}
public static native Object executeVM(byte[] vmCode, Object[] args);
/* ... */
To prevent easy circumvention of integrity checks that rely solely on Java code, a native
library is employed . This library incorporates advanced anti-tampering
techniques. You may notice that certain strings in the target APK are not initialized
correctly, and some classes invoke VMRunner.executeVM
instead of executing regular Java
code That’s right, Google created another VM on top of the Dalvik VM. Google decided to virtualize Java code execution in certain classes to prevent
reverse engineering and tampering with the app. Additionally, important strings will be
initialized at runtime to hide their inital value.
The bytecode used by the custom VM is stored within the assets directory of each app. To understand the execution flow of these code files, we need to address the following aspects:
Locate the VM’s Dispatcher Function: Identify the function that serves as the entry point for analyzing the implemented instruction set.
Write a disassmbler or decompiler based on the findings of step 1
Generate equivalent Java code based on the disassembler to get rid of these bytecode files.
The following chapters will explore the structure of the custom VM, its instruction set, and the methods to analyze them. Additionally, we will discuss a potential decompiler and provide a partial implementation.
Before proceeding, we must first locate the native counterpart of executeVM
. By examining the
exports of the native library, we observe that no native function starting with Java_
is
defined. This suggests that the function is not dynamically linked but is instead linked statically
via JNIEnv->RegisterNatives
.
If we scroll down to the very end of JNI_OnLoad
, we can see where our native function
is registered. (Note that the function name was changed here)
/* ... */
local_80.name = (char *)((ulong)&local_e0 | 1);
if ((local_e0 & 1) != 0) {
local_80.name = (char *)local_d0;
}
local_80.signature = (char *)((ulong)&local_f8 | 1);
if ((local_f8 & 1) != 0) {
local_80.signature = local_e8;
}
local_80.fnPtr = Java_com_pairip_VMRunner_executeVM;
(*(*ppJVar18)->RegisterNatives)(ppJVar18,local_c0,&local_80,1);
/* ... */
With that accomplished, we can now attempt to reverse the inner workings of the VM.