樣本:Y29tLndlbWFkZS5uaWdodGNyb3dz
聊點題外話,最近在找工作,一家公司說要搞NP,我果斷拒絕,還有一家公司給了一道面試題,內容是分析一款外掛( 針對他們家遊戲的 )和實現一個有效的外掛功能。當我興致勃勃下載好遊戲後,打開apk的lib目錄一看,發現libtprt.so
、libtersafe2.so
的特徵就知道67了。
眾所周知這是tx的MTP,我自認水平有限是搞不定的了,但還是硬著頭皮分析了一下,主要想分析他的CRC檢測,找到了幾處CRC邏輯,但都不是主要的邏輯,直到最後看到疑似vm虛擬機的東西,感覺他的核心檢測邏輯可能是在vm裡?看之後有沒有機會再分析看看吧。
小小分析完libtprt.so
後,道心破碎,於是打算找個簡單點的來玩玩,正好前段時間一位大佬分享了一個樣本,就決定是你的。這是個UE5遊戲,主要看看他的檢測邏輯。
frida注入後過1s左右會直接閃退,打印加載的so,看到只有一個libdxbase.so
是APP本身的,顯然檢測邏輯在裡面。

將libdxbase.so
拉入IDA,沒有報錯,即so大概率沒有加固。
然後習慣先看看init_array,沒有太大發現,但看到decrypt1
明顯是字符串解密函數,先記下來。

hook RegisterNatives
,看到動態注冊了4個函數,遂一hook看看調用了哪個。
注:記d
函數為reg_func_d
,其他如此類推。
結果是調用了reg_func_d
,但只有Enter而沒有Leave,因此檢測邏輯可能在reg_func_d
中。
reg_func_d
的邏輯有差不多2000行,懶得靜態一點一點分析了,直接動調看看是在哪裡crash的。
crash的位置是在exit_func(0xFFFFFFFE)
,而在調用exit_func
前進行了一些time diff的操作,並根據time diff來決定是否走到exit_func
那部份的邏輯。

本以為上面只是個普通的time diff調試檢測,但frida hook exit_func
並打印調用棧後發現是同一個地方,即frida hook時同樣會走到上述位置,然後調用exit_func
閃退。
深入分析上圖那部份邏輯,發現一旦走到上圖那位置後,最終必然會走向exit_func
。( 原因:sub_8250
返回固定值、time diff永遠大於v202
)。
從上圖位置向上尋找「生路」,看到goto LABEL_215
,只要想辦法讓執行流進入任意一處goto LABEL_215
的邏輯,就能避免走到上面那條「絕路」。
嘗試走紅框那裡的goto LABEL_215
,條件1是*(_DWORD *)(import_data + 9972)
為0
,先嘗試滿足這條件。

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

只能從匯編視圖看,上圖的some_timestamp
是由lstat
的buf
( x1
) + 0x68
賦值。( x0
為/sbin
)
而且[buf + 0x68]
的確是在調用lstat
後才有值,但buf
的結構為struct stat
,大小似乎小於0x68
,因此buf[0x68]
正常來說並不屬於struct stat
結構?猜測是內存對齊等原因導致的。

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

將0x67517B0D
轉換下:

與/sbin
的ls -l
顯示的日期一致。

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

與/system/lib
的ls -l
顯示的日期一致。

計算這兩個時間的time diff目的是什麼?
以下是普通Magisk環境的xiaomi手機,可以看到兩者的日期差很遠。
sbin
的日期比較近是因為其中有個shamiko
文件,大概是啟用/關閉shamiko模塊時都會刷新其日期。

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

小結:這部份計算的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應該不會大於這個值。

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

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

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

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

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

其中用了以下方法來檢測傳入路徑是否存在:
openat
、syscall(__NR_openat)
、scandir
、lstat
、stat
、access
、readlink

注:檢測的路徑大概有以下這些
root檢測3:判斷fingerprint中是否包含user-debug
、eng/
、Custom Phone
。

對應的bypass腳本:
上述地方都bypass後,frida終於不再閃退,但畫面上仍顯示ROOTED
。
而j.rjshqqeirnhhbc.mq
其實是Magisk隨機的包名,代表其實是Magisk被檢測到。

在bypass frida閃退後,hook decrypt1
保存一份相對完整的解密字符串,用以配合分析,記為decrypt_str.log
。
再次hook那4個動態注冊的函數,會發現除了調用1次reg_func_d
外,還不斷地在調用reg_func_p
、reg_func_q
,嘗試直接分析後2個函數,但沒有看出什麼。
改變思路,hook Java層的一些退出函數。
發現觸發了System.exit
,調用棧如下,是由com.xshield.x.run
類調用的。
Java層有混淆,用jeb打開可以默認去除一些簡單混淆( 如字符串混淆 ),方便分析。
從com.xshield.x.run
向上跟到call_exit_thread_xref2
函數,看到"Scanrisk"
字符串本以為是相關邏輯,但hook後發現v2 == 0
,因此根本不會走任意一處"Scanrisk"
。

call_exit_thread_xref2
→ u.call_exit_thread_xref1
→ exit
。

嘗試直接讓call_exit_thread_xref2
函數固定返回1
,不走原本的邏輯。
結果是畫面不再顯示那個檢測介面,但過一段時間後同樣會退出,調用的是native層的exit_func
。
由此猜測call_exit_thread_xref2
只是構建那個檢測介面的邏輯,真正檢測的地方在另一處。
在call_exit_thread_xref2
時機打印調用棧,繼續向上跟
com.xshield.k.run
如下,被檢測時走的是else分支,嘗試讓它走if分支。
結果同樣是畫面不再顯示那個檢測介面,但過一段時間後同樣會退出。因此還是要從native層入手。
注:da.detectInfo
是我手動修改的名字。

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

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

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。

根據猜測,嘗試置空da.q
參數中的j.rjshqqeirnhhbc.mq
,讓它不檢測j.rjshqqeirnhhbc.mq
。
結果是APP不再顯示那個檢測介面,也不會自動退出,成功bypass掉Magisk檢測。
由此確定了檢測邏輯的確是在da.q(ctx, 8, installed_app_info)
( 必須是args[1]
為8
的情況,才是進行上述檢測 )。
回到native層的reg_func_q
分析檢測邏輯的具體實現,動調case 8的情況。

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

其中的內容如下:

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

不知什麼原因,動調時始終無法斷在reg_func_q_pthread1
裡,因此只好通過hook和配合decrypt_str.log
來進行分析( 主要依賴這兩者來確定執行流 )。
打開XXX/base.apk
→ fd反查( IO重定向檢測 ) → 解析.apk
結構 → 獲取其AndroidManifest.xml
。

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

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

僅憑權限來判斷,不會出現誤殺的情況?答案是會的,我在搜索相關資料時就發現有一堆用戶因被誤殺而在某論壇訴苦的情況,不過那時是21年,現在都25年了這問題應該也改善了不少。
可以看到它加了一些白名單來防止誤殺那些具有上述權限的正常APP。

bypass腳本:hook openat
,將/data/app/j.rjshqqeirnhhbc.mq-YbP-hQjkQs0g9MZDz9dD0w==/base.apk
重定向為另一個正常apk。
注:這樣重向定不會被上述的fd反查檢測到,另一種Interceptor.replace
才會。
上述腳本可以bypass Magisk檢測,但奇怪的是在hook_openat
之後,即使下一次沒有hook_openat
,依然不會再彈那個檢測介面,也不會退出。
連執行流也改變了,要重裝遊戲才會回復「正常」。感覺是該保護的一種BUG?不太確定。
改AOSP / 修改AndroidManifest.xml
,這兩種賦予APP Debuggable權限的方法,都會被檢測到。
上次分析LIAPP時也有類似的檢測,那次沒分析明白,這次再來看看。

注:在分析過程中發現0xB11C
類似檢測處理函數,記為mb_detect_handler
。
hook mb_detect_handler
,在參數包含AndroidManifest.xml
時打印調用棧。
看到相關邏輯在0xb5bc
。
記0xb5bc
為detected_APKMODE
,繼續向上跟,看到是由g_APKMODE_flag1
和g_APKMODE_flag2
決定是否創建detected_APKMODE
線程的。
按x
沒有看到g_APKMODE_flag1
和g_APKMODE_flag2
賦值的地方,嘗試使用frida的內存斷點,但沒什麼效果。

改用這篇文章自己實現的frida內存斷點,成功命中:
去libdxbase.so!0x18f0c
看看( 這裡位於reg_func_d
)。
似乎import_data + 9984
就是g_APKMODE_flag1
( 動調後發現的確如此 ),值來源是a19
。

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

而a19
其實就是reg_func_d
的args[18]
( 倒數第3個參數 )。

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

hook Java層的reg_func_d
,去掉FLAG_DEBUGGABLE
標誌。
結果是遊戲終於不再顯示APKMOD
檢測介面,順利bypass它的Debuggable檢測。
總的來說這個保護與之前分析的LIAPP差不多,都不難,只是比較麻煩。
( 各位有好玩的遊戲樣本也可以分享給我,有空會看看的^^ )
[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
[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
[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
[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
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"
);
}
}
})
}
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"
);
}
}
})
}
[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
[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
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"
);
}
}
})
}
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"
));
}
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"
));
}
this
.a0 = args[0];
}
})
}
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"
);
}
}
})
}
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"
));
}
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"
));
}
this
.a0 = args[0];
}
})
}
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);
}
})
}
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);
}
})
}
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()
}
})
}
function printStack(){
console.
log
(Java.use(
"android.util.Log"
).getStackTraceString(Java.use(
"java.lang.Exception"
).$
new
()))
}
function hook_leave_java() {
Java.perform(() => {
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
最后于 3天前
被ngiokweng编辑
,原因: 圖