This article is an excellent piece from the Kanxue forum.
Author from Kanxue ForumID: xwtwho
A while ago, I wanted to play this game and found that there was root detection while running the emulator. I wanted to see how it was implemented and prepared to debug it, but there was no X86 library. I also found that the previous debugging mobile version was too low. I had a Xiaomi 8 on hand, and after re-downloading the source code, compiling LineageOs 17.1, flashing the system, and debugging, I modified the system source code based on the debugging situation. In the middle, I encountered blind spots and had to go back to review the corresponding knowledge points. I was interrupted by other matters and continued for a long time, so I wrote a document to record it.
1. Software and Hardware Environment
LineageOs 17.1 (Android 10)
2. Analysis Record (Directly in the order of actual experience)
Initially, after obtaining the APK, I opened it with GDA.

I directly flipped through various information and found function names obfuscated, string encrypted, etc. It looked like there was a shell. I searched for relevant information and found this article: https://blog.csdn.net/weixin_30512785/article/details/99559394.
This is an old version of Nprotect. Compared to this, the protection strength of dex and so files in the new version has been enhanced, including so files (erasing file header information to interfere with static analysis), so loading (libengine.so does not use system API for loading), symbol table encryption, and information flow is no longer straightforward. It also uses Ollvm (Obfuscator-LLVM clang version 4.0.1) and so on.
Although there are many differences between the new and old versions, and the game I looked at was also different, there are still many similarities. This article was still helpful and saved a lot of time, so I am very grateful to the author.
I wanted to see this root detection, but that article did not mention it, probably because the old version did not have it, so I prepared to look at it myself. In the meantime, there was also the compilation of the system and flashing, which was quite a hassle, but fortunately, the system was successfully flashed.
The prompt for opening this game in the emulator:
I checked with uiautomatorviewer but found no entry point. I wanted to find key points by searching for the prompt string but couldn’t find it. All the strings were encrypted, and the APK JAVA code opened directly was also incomplete. Next, I prepared to dump the class files, which can be done using the frida_dump script on GitHub, and I would get 8 dex files:
Then it was decompilation and code review. The relevant code on my side was in class2.dex, and I found that the strings were all encrypted.

The encrypted code is relatively simple (the strings in so are also encrypted, but with AES), but it should be noted that there is not just one encryption function; different strings may use different functions, and the algorithms are similar, just with different XOR constants:
static public String IiIIiiIiii(String p0){ int vi; int ilength = p0.length(); char[] ocharArray = new char[ilength]; ilength = ilength-1; while (ilength >= 0) { vi = ilength-1; ocharArray[ilength]=(char)(p0.charAt(ilength)^0x3c); if (vi >= 0) { ilength = vi-1; ocharArray[vi]=(char)(p0.charAt(vi)^0x60); }else { break ; } } return new String(ocharArray);}
Next, I found the classes of interest and wrote a program to decrypt the strings. Finally, I found this prompt point:
package com.inca.security.Core;public class AppGuardEngine implements WeakRefHandler$IOnHandleMessage, BaseEventInvoker
This app will be terminated because a security policy violation has been detected!
The subsequent code shows it is in decimal.
I have to say that GDA is indeed powerful; the following JEB could not decompile:

This includes the definitions of various detection codes. After restoring the strings, it looks like this:
According to this, 34 is exactly DETECT_ROOTING_ENVIRONMENT, indicating that the root environment has been detected. By the way, during the code review process, I found several anti-debugging places:
Class name: com.inca.security.IiIIiiiiIi
Debug.isDebuggerConnected(): public boolean iiIIIiiiIi() { return Debug.isDebuggerConnected(); }
By executing the time difference Debug.threadCpuTimeNanos(), it determines whether it is being debugged.
It does 1 million addition calculations and checks whether the time exceeds 100ms (100000000 nanoseconds).
public boolean iIIIiiiIII() { boolean v0 = false; long v4 = Debug.threadCpuTimeNanos(); int v1 = 0; int v2; for(v2 = 0; v1 < 1000000; v2 = v1) { v1 = v2 + 1; } if(Debug.threadCpuTimeNanos() - v4 >= 100000000) { v0 = true; } return v0;}
Through reference analysis, the last confirmation of the detection frame’s entry point is this function:
public void conditionCallback(int arg23, int arg24, byte[] arg25)
Then I went on Frida to hook this call point.
MainActivity = Java.use('com.inca.security.Core.AppGuardEngine');if (MainActivity != null) { MainActivity.conditionCallback.implementation = function (arg0, arg1, arg2) { //send('Start! Hook!'); //python call back console.log("call conditionCallback"); console.log(arg0); console.log(arg1); console.log(arg2); showStacks(); return this.conditionCallback(arg0, arg1, arg2); }}
frida -U -l E:\node_proj\TcpsocketTest\fridaHook2.js -f com.bluepotiongames.eosmSpawned `com.bluepotiongames.eosm`. Use %resume to let the main thread start executing![MI 8::com.bluepotiongames.eosm]-> hook_eos();[MI 8::com.bluepotiongames.eosm]-> %resume
It was found that it exited quickly and could not reach the prompt window, which means it was detected, so I had to debug dynamically.
The startup has 3 processes that ptrace each other. I still used the frida method to start the process and then attached the game process for debugging through IDA.
The SO related to protection is the following:
libcompatible.so libstub.so libengine-hlp.so libengine.so
First, I definitely want to find JNI_OnLoad, directly running the IDC script:
//android 10(lineage 17.1) //LoadNativeLibrary offset: 0000007BAE70AC70 - 0000007BAE395000 = 375C70 auto soBase=0; soBase=getModuleBase("libart.so"); auto addrArtBp=soBase + 0x375C70; MakeComm(addrArtBp,"LoadNativeLibrary"); auto addrArtCallOnload=soBase + 0x376910; AddBpt(addrArtCallOnload); MakeComm(addrArtCallOnload,"call JNI_ONLOAD");
Enter the JNI_OnLoad of libcompatible.so:

Some codes are decompressed during runtime. These can be dumped during debugging, and then merged into the original so file for easier IDA analysis.
JNI_OnLoad contains related handling for anti-debugging, checking status information, forking child processes to ptrace each other, and registering inotify_add_watch, containing the following detections:
/dev/input The following three were found during debugging libengine.so
Finally, they all go through svc calls.
These detections can be filtered out directly by modifying the kernel. There are many modifications on the internet regarding status, and for the modification of inotify_add_watch, it is in fs/notify/:
/fs/notify/inotify/inotify_user.c inotify_add_watch
I dug a pit here by modifying it. I only filtered the main process, leading to detection triggered by threads. The root cause is that the child thread’s current->parent task_struct structure is directly the parent of the main process, causing the retrieved process name not to be the expected one but the name of the grandparent process of the main process.
bionic/libc/bionic/ptrace.cpp
For svc calls, modify the kernel part ptrace.c’s ptrace_attach to directly return for this process.
This actual debugging took some time, and I also output the various JNI interface functions in the so (including the subsequent libstub.so).
By the way, the .init_proc in libstub.so will call the exported function SoLibraryStart in libcompatible.so to decrypt the code. Later, I found that this was not the main focus, so I changed my approach.
After handling the above detections, I could continue running the hook conditionCallback script. At this point, the stack could be displayed:
java.lang.Exception at com.inca.security.Core.AppGuardEngine.conditionCallback(Native Method) GetMethodID Pid: 11681 Path: conditionCallback Backtrace:0x76046fe75c0x7635df28080x7635df2808
Then attach IDA to go to 0x76046fe75c. Through comparative analysis, it was determined that this is the code space of libengine.so. It can be dumped and opened with IDA, and then combined with dynamic and static analysis. The file header in memory is gone, and locating in IDA is a bit troublesome. Here, I can write the relevant offsets obtained from debugging into the idc script, and every time I debug anew, I can run the script to identify the previously analyzed points.
Double click the output address to reach the corresponding code point, which is very convenient.
By accessing 0x76046fe75c, I only clarified the reading of the detection code; writing is another thread. After reading this, it will call Java to display the prompt window.
Since I prepared to find the entry point here because of the earlier modification of inotify_add_watch pit, I found that it would run away.
Later, I tried several methods but couldn’t find the entry point, so I returned to the system modification. I directly output the thread entry address at pthread_create, and then combined with the address output from the stack above, determined several related thread addresses, and modified the entry address of libengine.so to 00 00 00 14, directly entering a loop, then attached, restored the entry code, and set breakpoints to determine the related entry points:

The root-related detection is this thread. After running, I encountered a new problem: it was not the prompt detection code 34, but 9. I checked the previous code table:
stringArray[7]=("DETECT_INVALID_LIBDVM_SO"); stringArray[8]=("detectinvVALID_LIBRUNTIME_SO"); stringArray[9]=("DETECT_INVALID_APPLIB_SO"); stringArray[10]=("DETECT_INVALID_LIBENGINE_SO");
9 is DETECT_INVALID_APPLIB_SO. It seems that modifying libengine.so caused it to be detected, but now the thread entry point has been confirmed. I continued debugging directly.

Mainly it is hash comparison, which will involve three files in the assets\appguard directory:
Involved algorithms include sha256, RSA.
After detecting the modification, it will set the detection code to 9, as follows:

For this detection, I filtered out the libengine.so by modifying the kernel’s directory list return.
The corresponding modification point is in fs/readdir.c’s filldir64. The relevant modifications are just some string comparisons to filter, and different system kernels are also different, so I won’t copy the code to take up space.
Now I can continue debugging, finding root detection points, including su file detection and apk package detection:

Related paths and package names:
1. Find the above su file paths.
2. Call the command which su
3. Call pm list packages to check if there are:
Looking back, I could also use frida to hook java.lang.Runtime.exec to find the commands called by the app:
hookAllOverloads: exec arguments: pm, path, com.bluepotiongames.eosm arguments: which, su arguments: pm, list, packages
The handling method is also to filter out the su-related paths directly in the kernel and filter the package return from pm list.
By the way, there is also a reading of __system_property related, which has both JAVA and native layers, and can be filtered according to the situation.
For reading build.prop file information through SystemProperties.get, I directly redirected it to /data/local/tmp/build2.prop in the kernel open function, and then directly modified this file.
After handling the file verification and finding the thread entry point, basically all subsequent requirements could be debugged smoothly. I would like to mention the emulator-related detections:
/system/bin/nox /system/bin/ttVM-prop /system/app/MOMOStore/MOMOStore.apk /system/lib/vboxsf.ko /system/lib/vboxguest.ko /system/lib/vboxvideo.ko /system/bin/nemuVM-nemu-service boolean isEmulator = SystemProperties.get("ro.kernel.qemu").equals("1");
After handling this root detection, I tried installing the Jianxing app (ver 5.0.2) and found that it was still detected (many say that the new version using the mask hiding module does not work, Bangbang protection). I saw that it added the following path access detection, for example, the /data directory cannot be accessed without root:
These paths can be filtered by thread name getprop.
Kanxue ID: xwtwho
https://bbs.pediy.com/user-home-44250.htm
*This article is original by Kanxue forum xwtwho, please indicate the source from the Kanxue community when reprinting.
The “Advanced Android Training Class” June 2021 class is now open for enrollment!
# Previous Recommendations
-
The story of Rundll32
-
Understanding the ELF file format in a different way
-
libsgmain de-obfuscation and VM restoration
-
A remote download and memory loading PE office macro virus
-
Using unidbg to restore the standard ollvm control flow flattening
Official Weibo: Kanxue Security