樣本:Y29tLndlbWFkZS5uaWdodGNyb3dz

前言

聊點題外話,最近在找工作,一家公司說要搞NP,我果斷拒絕,還有一家公司給了一道面試題,內容是分析一款外掛( 針對他們家遊戲的 )和實現一個有效的外掛功能。當我興致勃勃下載好遊戲後,打開apk的lib目錄一看,發現libtprt.solibtersafe2.so的特徵就知道67了。

眾所周知這是tx的MTP,我自認水平有限是搞不定的了,但還是硬著頭皮分析了一下,主要想分析他的CRC檢測,找到了幾處CRC邏輯,但都不是主要的邏輯,直到最後看到疑似vm虛擬機的東西,感覺他的核心檢測邏輯可能是在vm裡?看之後有沒有機會再分析看看吧。

小小分析完libtprt.so後,道心破碎,於是打算找個簡單點的來玩玩,正好前段時間一位大佬分享了一個樣本,就決定是你的。這是個UE5遊戲,主要看看他的檢測邏輯。

frida閃退分析

frida注入後過1s左右會直接閃退,打印加載的so,看到只有一個libdxbase.so是APP本身的,顯然檢測邏輯在裡面。

image.png

libdxbase.so拉入IDA,沒有報錯,即so大概率沒有加固。

然後習慣先看看init_array,沒有太大發現,但看到decrypt1明顯是字符串解密函數,先記下來。

image.png

hook RegisterNatives,看到動態注冊了4個函數,遂一hook看看調用了哪個。

注:記d函數為reg_func_d,其他如此類推。

1
2
3
4
5
6
7
[RegisterNatives] java_class: com.xshield.da name: d sig: (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IIIIIIII)Ljava/lang/String; fnPtr: 0x7137359258  fnOffset: 0x7137359258 libdxbase.so!0x18258  callee: 0x713736543c libdxbase.so!JNI_OnLoad+0x15e0

[RegisterNatives] java_class: com.xshield.da name: o sig: (II)I fnPtr: 0x7137348228 fnOffset: 0x7137348228 libdxbase.so!0x7228 callee: 0x713736543c libdxbase.so!JNI_OnLoad+0x15e0

[RegisterNatives] java_class: com.xshield.da name: p sig: (II)Ljava/lang/String; fnPtr: 0x7137347e78 fnOffset: 0x7137347e78 libdxbase.so!0x6e78 callee: 0x713736543c libdxbase.so!JNI_OnLoad+0x15e0

[RegisterNatives] java_class: com.xshield.da name: q sig: (Landroid/content/Context;ILjava/lang/String;)Ljava/lang/String; fnPtr: 0x7137360d60 fnOffset: 0x7137360d60 libdxbase.so!0x1fd60 callee: 0x713736543c libdxbase.so!JNI_OnLoad+0x15e0

結果是調用了reg_func_d,但只有Enter而沒有Leave,因此檢測邏輯可能在reg_func_d中。

reg_func_d的邏輯有差不多2000行,懶得靜態一點一點分析了,直接動調看看是在哪裡crash的。

虛假的time diff

crash的位置是在exit_func(0xFFFFFFFE),而在調用exit_func前進行了一些time diff的操作,並根據time diff來決定是否走到exit_func那部份的邏輯。

image.png

本以為上面只是個普通的time diff調試檢測,但frida hook exit_func並打印調用棧後發現是同一個地方,即frida hook時同樣會走到上述位置,然後調用exit_func閃退。

深入分析上圖那部份邏輯,發現一旦走到上圖那位置後,最終必然會走向exit_func。( 原因:sub_8250返回固定值、time diff永遠大於v202 )。

1
2
3
4
[exit_func] call in: 
7bb25dab70 is in libdxbase.so offset: 0x1ab70
7c1f940354 is in libart.so offset: 0x140354
7c1f936470 is in libart.so offset: 0x136470

奇怪的time diff

從上圖位置向上尋找「生路」,看到goto LABEL_215,只要想辦法讓執行流進入任意一處goto LABEL_215的邏輯,就能避免走到上面那條「絕路」。

嘗試走紅框那裡的goto LABEL_215,條件1是*(_DWORD *)(import_data + 9972)0,先嘗試滿足這條件。

image.png

交叉引用找(_DWORD *)(import_data + 9972)賦值的地方,分析後可知v197是time diff,但具體是什麼東西之間的time diff,並不能從偽代碼裡直接看出。

image.png

只能從匯編視圖看,上圖的some_timestamp是由lstatbuf( x1 ) + 0x68賦值。( x0/sbin )

而且[buf + 0x68]的確是在調用lstat後才有值,但buf的結構為struct stat,大小似乎小於0x68,因此buf[0x68]正常來說並不屬於struct stat結構?猜測是內存對齊等原因導致的。

image.png

從內存分佈可以看出buf + 0x68的位置應該是struct stat最後一個屬性( struct stat最後3個屬性都是時間 ),代表指定目錄"上次狀態的更改時間"

image.png

0x67517B0D轉換下:

image.png

/sbinls -l顯示的日期一致。

image.png

用同樣方式找到time diff的另一個值,是0x676631AB

image.png

/system/libls -l顯示的日期一致。

image.png

計算這兩個時間的time diff目的是什麼?

以下是普通Magisk環境的xiaomi手機,可以看到兩者的日期差很遠。

sbin的日期比較近是因為其中有個shamiko文件,大概是啟用/關閉shamiko模塊時都會刷新其日期。

image.png

/system/lib的日期是一個超舊的時間。

image.png

小結:這部份計算的time diff是/system/lib/sbin之間的"上次狀態的更改時間" time diff,感覺這個time diff應該是在檢測Magisk之類的。

繼續向下看,判斷time diff是否大於0xF4240 sec,是則上述的條件1無法滿足。

0xF4240 sec → 277 hrs 46 min 40 sec,正常手機環境下的time diff應該不會大於這個值。

image.png

bypass腳本:直接hook lstat

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function hook_lstat() {
let fake_time = Date.now();
Interceptor.attach(Module.findExportByName(null, "lstat"), {
onEnter: function(args) {
this.name = args[0].readCString();
this.statbuf = args[1];
},
onLeave: function(retval) {
if (this.name == "/system/lib") {
this.statbuf.add(0x68).writeU64(fake_time++);
console.log("bypass lstat");
}
if (this.name == "/sbin") {
this.statbuf.add(0x68).writeU64(fake_time++);
console.log("bypass lstat");
}
}
})
}

bypass這處time diff檢測後,Magisk環境的xiaomi手機依然會退出,大概是條件2check1_res == 9 || !check1_res沒有滿足。

image.png

check1分析( root檢測 )

check1返回90都能滿足條件2。接下來看看check1都檢測了什麼。

image.png

root檢測1:popen("which su")

image.png

root檢測2:獲取了一堆可能存在su的路徑,然後調用check_exist_in_different_way檢測指定路徑是否存在。

image.png

check_exist_in_different_way內創建了pthread_func2_check_path_exist線程來處理。

image.png

其中用了以下方法來檢測傳入路徑是否存在:

openatsyscall(__NR_openat)scandirlstatstataccessreadlink

image.png

注:檢測的路徑大概有以下這些

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
[decrypt1]  0x3a304 /system/bin/su
[decrypt1] 0x3a2f4 /system/xbin/su
[decrypt1] 0x3a313 /system/bin/.ext/.su
[decrypt1] 0x3a328 /system/xbin/.tmpsu
[decrypt1] 0x3a33c /vendor/bin/su
[decrypt1] 0x3a34b /sbin/su
[decrypt1] 0x3a354 /system/xbin/nosu
[decrypt1] 0x3a366 /system/bin/nosu
[decrypt1] 0x3a377 /system/xbin/su_bk
[decrypt1] 0x3a38a /system/bin/su_bk
[decrypt1] 0x3a39c /system/xbin/xsu
[decrypt1] 0x3a3ad /system/xbin/suu
[decrypt1] 0x3a3be /system/xbin/bstk/su
[decrypt1] 0x3a3d3 /system/RootTools/su
[decrypt1] 0x3a3e8 /data/data/bin/su
[decrypt1] 0x3a3fa /data/data/in/su
[decrypt1] 0x3a40b /data/data/n/bstk/su
[decrypt1] 0x3a420 /data/data/xbin/su
[decrypt1] 0x3a433 /res/su
[decrypt1] 0x3a43b /data/local/bin/su
[decrypt1] 0x3a44e /data/local/su
[decrypt1] 0x3a45d /data/local/xbin/su
[decrypt1] 0x3a471 /system/su
[decrypt1] 0x3a47c /data/su
[decrypt1] 0x3a485 /su/bin/su
[decrypt1] 0x3a490 /su/bin/sush
[decrypt1] 0x3a49d /system/bin/failsafe/su
[decrypt1] 0x3a4b5 /system/sbin/su
[decrypt1] 0x3a4c5 /system/sd/xbin/su
[decrypt1] 0x3a4d8 /system/xbin/noxsu
[decrypt1] 0x3a4eb /magisk/.core/bin/su
[decrypt1] 0x3a500 /sbin/.magisk
[decrypt1] 0x3a50e /sbin/.core
[decrypt1] 0x3b0c3 /system/usr/we-need-root/su
[decrypt1] 0x3b0df /cache/su
[decrypt1] 0x3b0e9 /dev/su

root檢測3:判斷fingerprint中是否包含user-debugeng/Custom Phone

image.png

對應的bypass腳本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
function hook_popen() {
Interceptor.attach(Module.findExportByName(null, "popen"), {
onEnter: function(args) {
if (args[0].readCString().indexOf(" su") != -1) {
console.log("[popen] which su -> which xx");
Memory.writeUtf8String(args[0], "which xx");
}
}
})
}

// hook after dlopen libdxbase.so
function hook_pthread_func2() {
Interceptor.attach(base.add(0x983C), {
onEnter: function(args) {
let check_path = args[0].readPointer().readCString();
if (check_path.indexOf("/su") != -1) {
Memory.writeUtf8String(args[0].readPointer(), check_path.replace("/su", "/XX"));

// console.log(`[pthread_func2]: ${check_path} -> ${args[0].readPointer().readCString()}`);
}
if (check_path.indexOf("magisk") != -1) {
Memory.writeUtf8String(args[0].readPointer(), check_path.replace("magisk", "3Ag1sk"));

// console.log(`[pthread_func2]: ${check_path} -> ${args[0].readPointer().readCString()}`);
}
if (check_path.indexOf("/sbin") != -1) {
Memory.writeUtf8String(args[0].readPointer(), check_path.replace("/sbin", "/ABCD"));

// console.log(`[pthread_func2]: ${check_path} -> ${args[0].readPointer().readCString()}`);
}
// console.log("[pthread_func2] check: ", check_path);
this.a0 = args[0];
}
})
}

上述地方都bypass後,frida終於不再閃退,但畫面上仍顯示ROOTED

j.rjshqqeirnhhbc.mq其實是Magisk隨機的包名,代表其實是Magisk被檢測到。

image.png

Magisk檢測分析

在bypass frida閃退後,hook decrypt1保存一份相對完整的解密字符串,用以配合分析,記為decrypt_str.log

1
2
3
4
5
6
7
8
9
10
11
12
13
function hook_decrypt1() {
Interceptor.attach(base.add(0x5E84),{
onEnter(args){
this.a3 = args[3];
this.len = args[4].toInt32();
this.offset = this.a3.sub(base);
},
onLeave(retval){
let dec_str = this.a3.readCString(this.len);
console.log("[decrypt1] ", ptr(this.offset), dec_str);
}
})
}

回首Java層

再次hook那4個動態注冊的函數,會發現除了調用1次reg_func_d外,還不斷地在調用reg_func_preg_func_q,嘗試直接分析後2個函數,但沒有看出什麼。

改變思路,hook Java層的一些退出函數。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function printStack(){
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()))
}
function hook_leave_java() {
Java.perform(() => {
let System = Java.use("java.lang.System");

System.exit.implementation = function() {
console.log("exit....")
printStack()
}

let Process = Java.use("android.os.Process");

Process.killProcess.implementation = function() {
console.log("killProcess....")
printStack()
}

})

}

發現觸發了System.exit,調用棧如下,是由com.xshield.x.run類調用的。

1
2
3
4
5
exit....
java.lang.Exception
at java.lang.System.exit(Native Method)
at com.xshield.x.run(dd.java:186)
at java.lang.Thread.run(Thread.java:919)

Java層有混淆,用jeb打開可以默認去除一些簡單混淆( 如字符串混淆 ),方便分析。

com.xshield.x.run向上跟到call_exit_thread_xref2函數,看到"Scanrisk"字符串本以為是相關邏輯,但hook後發現v2 == 0,因此根本不會走任意一處"Scanrisk"

image.png

call_exit_thread_xref2u.call_exit_thread_xref1exit

image.png

嘗試直接讓call_exit_thread_xref2函數固定返回1,不走原本的邏輯。

結果是畫面不再顯示那個檢測介面,但過一段時間後同樣會退出,調用的是native層的exit_func

1
2
3
4
[exit_func] call in: 
7da42486a4 is in libdxbase.so offset: 0x86a4
7e96fb7894 is in libc.so offset: 0xe6894
7e96f55b70 is in libc.so offset: 0x84b70

由此猜測call_exit_thread_xref2只是構建那個檢測介面的邏輯,真正檢測的地方在另一處。

call_exit_thread_xref2時機打印調用棧,繼續向上跟

1
2
3
4
5
6
7
8
9
10
11
java.lang.Exception
at com.xshield.da.IIiIiiIIII(Native Method)
at com.xshield.da.IIiiiIIiIi(da.java:49)
at com.xshield.k.run(da.java:103)
at android.os.Handler.handleCallback(Handler.java:883)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loop(Looper.java:224)
at android.app.ActivityThread.main(ActivityThread.java:7520)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:539)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:950)

com.xshield.k.run如下,被檢測時走的是else分支,嘗試讓它走if分支。

結果同樣是畫面不再顯示那個檢測介面,但過一段時間後同樣會退出。因此還是要從native層入手。

注:da.detectInfo是我手動修改的名字。

image.png

全局搜detectInfo ( 不要只按x找交叉引用,不太準 ),找到它是某次reg_func_q調用的返回值。

image.png

在其上方是一個while循環,根據特定的邏輯調用da.q ( 即reg_func_q )。

image.png

hook da.q,看到某次的result果然是detectInfo

同樣可以見到某次reg_func_q的參數是一堆包名,其中就包含j.rjshqqeirnhhbc.mq

因此可以猜測Magisk的檢測邏輯為:Java層收集安裝APP的包名、路徑等信息 → 調用da.q(ctx, 8, installed_app_info)進行檢查 → 發現j.rjshqqeirnhhbc.mq的某些特徵 → 判斷是Magisk。

image.png

根據猜測,嘗試置空da.q參數中的j.rjshqqeirnhhbc.mq,讓它不檢測j.rjshqqeirnhhbc.mq

結果是APP不再顯示那個檢測介面,也不會自動退出,成功bypass掉Magisk檢測。

1
2
3
4
5
6
7
8
9
10
11
12
function hook_reg_func_q() {
let da = Java.use("com.xshield.da");
da["q"].implementation = function (context, i, str) {
// ;j.rjshqqeirnhhbc.mq;/data/app/j.rjshqqeirnhhbc.mq-YbP-hQjkQs0g9MZDz9dD0w==/base.apk;10205;tcp
str = str.replace(";j.rjshqqeirnhhbc.mq;/data/app/j.rjshqqeirnhhbc.mq-YbP-hQjkQs0g9MZDz9dD0w==/base.apk;10205;tcp", "")
console.log(`da.q is called: i=${i}, str=${str}`);
let result = this["q"](context, i, str);
console.log(`da.q result=${result}`);
return result;
};

}

由此確定了檢測邏輯的確是在da.q(ctx, 8, installed_app_info) ( 必須是args[1]8的情況,才是進行上述檢測 )。

native層:reg_func_q case 8分析

回到native層的reg_func_q分析檢測邏輯的具體實現,動調case 8的情況。

image.png

一開始先將傳入的installed_app_info寫入cfdd35cd.dex

image.png

其中的內容如下:

image.png

最後會創建reg_func_q_pthread1線程,裡面才是真正檢測的地方。

image.png

reg_func_q_pthread1分析

不知什麼原因,動調時始終無法斷在reg_func_q_pthread1裡,因此只好通過hook和配合decrypt_str.log來進行分析( 主要依賴這兩者來確定執行流 )。

打開XXX/base.apk → fd反查( IO重定向檢測 ) → 解析.apk結構 → 獲取其AndroidManifest.xml

image.png

判斷AndroidManifest.xml中,是否包含以下權限:

  1. UPDATE_PACKAGES_WITHOUT_USER_ACTION
  2. QUERY_ALL_PACKAGES
  3. FOREGROUND_SERVICE
  4. REQUEST_INSTALL_PACKAGES
  5. HIDE_OVERLAY_WINDOWS

image.png

而原版的Magisk正好包含上述的所有權限。

image.png

僅憑權限來判斷,不會出現誤殺的情況?答案是會的,我在搜索相關資料時就發現有一堆用戶因被誤殺而在某論壇訴苦的情況,不過那時是21年,現在都25年了這問題應該也改善了不少。

可以看到它加了一些白名單來防止誤殺那些具有上述權限的正常APP。

image.png

bypass腳本:hook openat,將/data/app/j.rjshqqeirnhhbc.mq-YbP-hQjkQs0g9MZDz9dD0w==/base.apk重定向為另一個正常apk。

注:這樣重向定不會被上述的fd反查檢測到,另一種Interceptor.replace才會。

1
2
3
4
5
6
7
8
9
10
11
12
13
function hook_openat() {
Interceptor.attach(Module.findExportByName(null, "openat"), {
onEnter: function(args) {
let path = args[1].readCString();
if (path.indexOf("/data/app/j.rjshqqeirnhhbc") != -1 && path.indexOf("base.apk") != -1) {
Memory.writeUtf8String(args[1], "/data/local/tmp/base.apk");
console.log("[openat] bypass: ", path, args[1].readCString());
}
// Thread.backtrace(this.context, Backtracer.FUZZY).map(addr_in_so);

}
})
}

上述腳本可以bypass Magisk檢測,但奇怪的是在hook_openat之後,即使下一次沒有hook_openat,依然不會再彈那個檢測介面,也不會退出。

連執行流也改變了,要重裝遊戲才會回復「正常」。感覺是該保護的一種BUG?不太確定。

Debuggable檢測分析

改AOSP / 修改AndroidManifest.xml,這兩種賦予APP Debuggable權限的方法,都會被檢測到。

上次分析LIAPP時也有類似的檢測,那次沒分析明白,這次再來看看。

image.png

注:在分析過程中發現0xB11C類似檢測處理函數,記為mb_detect_handler

hook mb_detect_handler,在參數包含AndroidManifest.xml時打印調用棧。

看到相關邏輯在0xb5bc

1
2
3
4
5
6
7
8
9
10
11
12
[mb_detect_handler] 0x852 0x10000000 AndroidManifest.xml
LR: 0x7bb250b5bc
Function: 0xb5bc
moduleName: libdxbase.so

LR: 0x7ca37b6730
Function: _ZL15__pthread_startPv+0x28
moduleName: libc.so

LR: 0x7ca3757008
Function: __start_thread+0x44
moduleName: libc.so

0xb5bcdetected_APKMODE,繼續向上跟,看到是由g_APKMODE_flag1g_APKMODE_flag2決定是否創建detected_APKMODE線程的。

x沒有看到g_APKMODE_flag1g_APKMODE_flag2賦值的地方,嘗試使用frida的內存斷點,但沒什麼效果。

image.png

改用這篇文章自己實現的frida內存斷點,成功命中:

1
2
3
4
5
// 用法: readwritebreak(base.add(0x3D700), 4, 1)
命中 : 0x7bb253e700 pc pointer : 0x7bb2519f0c
{"name":"libdxbase.so","base":"0x7bb2501000","size":258048,"path":"/data/app/com.wemade.nightcrows-3nZhg8hrtpfvU8YQT2BvZw==/lib/arm64/libdxbase.so"}
pc - - > libdxbase.so -> 0x18f0c
readwritebreak exit

libdxbase.so!0x18f0c看看( 這裡位於reg_func_d )。

似乎import_data + 9984就是g_APKMODE_flag1 ( 動調後發現的確如此 ),值來源是a19

image.png

hook reg_func_d,在enter和leave時分別打印,的確是在leave時才有值,即g_APKMODE_flag都是在reg_func_d中賦值。

image.png

a19其實就是reg_func_dargs[18] ( 倒數第3個參數 )。

image.png

看Java層是怎樣傳值的,原來是ApplicationInfoflags屬性。

image.png

hook Java層的reg_func_d,去掉FLAG_DEBUGGABLE標誌。

結果是遊戲終於不再顯示APKMOD檢測介面,順利bypass它的Debuggable檢測。

1
2
3
4
5
6
7
8
9
10
11
function hook_reg_func_d() {
let da = Java.use("com.xshield.da");
da["d"].implementation = function (str, str2, str3, str4, str5, str6, str7, str8, str9, str10, str11, i9, i10, i11, i12, i13, i14, i15, i16) {
const FLAG_DEBUGGABLE = 0x2;
i14 &= (~FLAG_DEBUGGABLE);
console.log(`da.d is called: str=${str}, str2=${str2}, str3=${str3}, str4=${str4}, str5=${str5}, str6=${str6}, str7=${str7}, str8=${str8}, str9=${str9}, str10=${str10}, str11=${str11}, i9=${i9}, i10=${i10}, i11=${i11}, i12=${i12}, i13=${i13}, i14=${i14}, i15=${i15}, i16=${i16}`);
let result = this["d"](str, str2, str3, str4, str5, str6, str7, str8, str9, str10, str11, i9, i10, i11, i12, i13, i14, i15, i16);
console.log(`da.d result=${result}`);
return result;
};
}

完整bypass腳本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
function hook_dlopen(soName) {
Interceptor.attach(Module.findExportByName(null, "dlopen"),
{
onEnter: function (args) {
var pathptr = args[0];
if (pathptr !== undefined && pathptr != null) {
var path = ptr(pathptr).readCString();
// console.log("[dlopen] ", path);
if (path.indexOf(soName) >= 0) {
this.is_can_hook = true;
}
}
},
onLeave: function (retval) {
if (this.is_can_hook) {
console.log("hook start...");
hook_func(soName)
}
}
}
);

Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"),
{
onEnter: function (args) {
var pathptr = args[0];
if (pathptr !== undefined && pathptr != null) {
var path = ptr(pathptr).readCString();
// console.log("[android_dlopen_ext] ", path);
if (path.indexOf(soName) >= 0) {
this.is_can_hook = true;
}
}
},
onLeave: function (retval) {
if (this.is_can_hook) {
console.log("hook start...");
hook_func(soName)
}
}
}
);
}

function hook_func(soName) {
var base = Module.findBaseAddress(soName);
function hook_pthread_func2() {
Interceptor.attach(base.add(0x983C), {
onEnter: function(args) {
// bypass root check2
let check_path = args[0].readPointer().readCString();
if (check_path.indexOf("/su") != -1) {
Memory.writeUtf8String(args[0].readPointer(), check_path.replace("/su", "/XX"));
}
if (check_path.indexOf("magisk") != -1) {
Memory.writeUtf8String(args[0].readPointer(), check_path.replace("magisk", "3Ag1sk"));
}
if (check_path.indexOf("/sbin") != -1) {
Memory.writeUtf8String(args[0].readPointer(), check_path.replace("/sbin", "/ABCD"));
}
}
})
}
function hook_openat() {
Interceptor.attach(Module.findExportByName(null, "openat"), {
onEnter: function(args) {
let path = args[1].readCString();
// bypass Magisk check
if (path.indexOf("/data/app/j.rjshqqeirnhhbc") != -1 && path.indexOf("base.apk") != -1) {
Memory.writeUtf8String(args[1], "/data/local/tmp/base.apk");
console.log("[openat] bypass: ", path, args[1].readCString());
}
}
})
}
hook_pthread_func2();
hook_openat();
}

function hook_lstat() {
let fake_time = Date.now();
// bypass frida crash
Interceptor.attach(Module.findExportByName(null, "lstat"), {
onEnter: function(args) {
this.name = args[0].readCString();
this.statbuf = args[1];
},
onLeave: function(retval) {
if (this.name == "/system/lib") {
this.statbuf.add(0x68).writeU64(fake_time++);
console.log("bypass lstat");
}
if (this.name == "/sbin") {
this.statbuf.add(0x68).writeU64(fake_time++);
console.log("bypass lstat");
}
}
})
}

function hook_popen() {
Interceptor.attach(Module.findExportByName(null, "popen"), {
onEnter: function(args) {
if (args[0].readCString().indexOf(" su") != -1) {
// bypass root check1
console.log("[popen] which su -> which xx");
Memory.writeUtf8String(args[0], "which xx");
}
}
})
}

function printStack(){
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()))
}

function hook_java() {
Java.perform(() => {
function hook_reg_func_d() {
let da = Java.use("com.xshield.da");
da["d"].implementation = function (str, str2, str3, str4, str5, str6, str7, str8, str9, str10, str11, i9, i10, i11, i12, i13, i14, i15, i16) {
// bypasss debuggalbe
const FLAG_DEBUGGABLE = 0x2;
i14 &= (~FLAG_DEBUGGABLE);
console.log(`da.d is called: str=${str}, str2=${str2}, str3=${str3}, str4=${str4}, str5=${str5}, str6=${str6}, str7=${str7}, str8=${str8}, str9=${str9}, str10=${str10}, str11=${str11}, i9=${i9}, i10=${i10}, i11=${i11}, i12=${i12}, i13=${i13}, i14=${i14}, i15=${i15}, i16=${i16}`);
let result = this["d"](str, str2, str3, str4, str5, str6, str7, str8, str9, str10, str11, i9, i10, i11, i12, i13, i14, i15, i16);
console.log(`da.d result=${result}`);
return result;
};
}
hook_reg_func_d();
})

}

function main() {
hook_dlopen("libdxbase.so");
hook_lstat();
hook_popen();
hook_java();
}

setImmediate(main)

結語

總的來說這個保護與之前分析的LIAPP差不多,都不難,只是比較麻煩。

( 各位有好玩的遊戲樣本也可以分享給我,有空會看看的^^ )