0. 前言

某天在應用商店挑選「幸運兒」時,一不小心選到了NP保護的,我想這便是天意。

自那天起便開啟了這段漫長的分析之旅,數以百次的調試,最後留下本文。

樣本: anAuZ29vZHNtaWxlLnRvdWhvdWxvc3R3b3JkX2FuZHJvaWQ=

1. libcompatible.so分析

NP的關鍵so為libcompatible.solibstub.solibengine.so,三者之間環環相扣,先看libcompatible.so

1.1 導出符號混淆

通過readelf找到.init_proc0x154028

1
2
3
4
5
6
# readelf -d libcompatible.so
readelf: Error: no .dynamic section in the dynamic segment

Dynamic section at offset 0x1abd80 contains 29 entries:
Tag Type Name/Value
0x000000000000000c (INIT) 0x154028

發現.init_proc被一些導出符號分割了,導致IDA無法F5。

image.png

用乐佬那篇文章的解決思路,手動把位於.init_proc範圍內的導出符號置空即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import struct

input_path = r"libcompatible.so"
output_path = r"libcompatible_p.so"

sym_start = 0x2F8
sym_end = 0xACD8
sym_size = 0x18

init_proc_start = 0x154028
init_proc_end = 0x15B06C

data = None
with open(input_path, mode = "rb") as f:
data = bytearray(f.read())

for sym in range(sym_start, sym_end, sym_size):
st_value = struct.unpack("<Q", data[sym + 8: sym + 8 + 8])[0]\

if (st_value % 2) == 1 or (st_value >= init_proc_start and st_value < init_proc_end):
data[sym + 8: sym + 8 + 8] = struct.pack("<Q", 0)

with open(output_path, mode = "wb") as f:
f.write(data)

patch後就能順利F5了,發現.init_proc中有如下虛假控制流。

image.png

bcf的解決思路可參考oacia大佬的這篇文章:https://oacia.dev/ollvm-study/

1.2 init_proc分析

計算獲取libcompatible.so的基址,保存在libcompatible_base中。然後調用give_load_seg_rwx()

image.png

give_load_seg_rwx()的實現如下,根據libcompatible_base解析 & 遍歷phdr,記錄最後一個loadable seg的結束位置,記為last_loadseg_end

last_loadseg_end對齊內存頁大小後的值作為mprotect()的size,保證基本上全部代碼段、數據段都有rwx權限。

image.png

回到.init_proc繼續向下看。

調用了mmap系統調用,映射了一頁rwx權限的內存,記為這片內存為mmap_buf

image.png

調用decrypt_something1(),解密了一些數據,結果存放在data_from_dec中。

image.png

調用decrypt_something2(),根據data_from_dec來解密args[1]。記解密後的數據為data_from_dec2

image.png

然後會根據data_from_dec2來對mmap_buf賦值。

image.png

由於mmap_buf有執行權,因此嘗試將賦予mmap_buf的數據解析為代碼,發現是svc。

image.png

之後svc會被保存到全局變量中。

image.png

完成svc的賦值後,調用了cache_maintenance()來更新mmap_buf的cache緩存。

image.png

調用decrypt_some_str()解密了一些字符串,保存在全局變量中。

它的字符串解密邏輯如下:

  1. 調用parse_data()解析args[1],結果存在args[0]中。
  2. 調用get_data_string()從上述的args[0]提取字符串地址。

解密了一些ro.屬性,大概是在做兼容。

image.png

之後獲取了當前so的.dynamic段。

image.png

解析.dynamic,獲取重定向相關信息,如RELA表,JMPREL表等。

image.png

清空重定向表。

image.png

image.png

最後會分別調用I1 ~ I3這3個導出函數( 是在上面被解密的 ),調用後會把函數加密回去( 那些異或操作一開始以為是解密,後來才發現是加密 )。

image.png

簡單看了下I1I2,沒有太特別的地方,重點在I3這個導出函數。

1.3 I3函數分析

sub_7A635BF48C()設置了某些導出符號的其中幾個字節,不知在干什麼。

之後mmap了一片rw權限的內存,記為mmap_buf2

image.png

mmap_buf2進行賦值。

image.png

之後會來到下圖的地方,若F7單步步入紅框慢慢跟,IDA似乎會crash,而F8直接步過則不會?

image.png

把某個函數賦給了some_func1變量

image.png

1.3.1 解密libcompatible.so的JNI_OnLoad

之後就來到第1處fla,顯然是關鍵邏輯處。

image.png

簡單看看這個fla。

這個地方在解密一段代碼,把v144指向的地址減去基址,得到的偏移記為off

image.png

從dump出來的libcompatible.so中搜off,發現offJNI_OnLoad()中,由此可知上述解密的代碼正是JNI_OnLoad()

image.png

第2處解密JNI_OnLoad()的地方,由此可知JNI_OnLoad()是分段解密的。

image.png

第3處解密JNI_OnLoad()的地方。

image.png

JNI_OnLoad()的解密應該只分成了3段,解密完後會調用cache_maintenance(),似乎每次解密完代碼後都會調用該函數做cache相關的處理。

cache_maintenance()args[0]是被解密代碼的起始地址,args[1]是結束地址,這個地址範圍中包含多個解密後的函數。

image.png

1.3.2 一些檢測邏輯

然後就是第2處fla,記為fla2

image.png

同樣是一段代碼解密邏輯,解密的是0xF70D0處的代碼。

image.png

解密後發現只是一個功能函數。

image.png

fla2中第2處代碼解密邏輯。

image.png

fla2中第3處代碼解密邏輯。

image.png

注:上述的代碼解密邏輯可能會被交替調用來對同一個函數進行解密。

解密完成後,同樣調用了cache_maintenance()

image.png

然後調用了check_something()進行一些檢測,其中又大概可分成check1() ~ check5()5個小檢測。

image.png

先看check1(),一開始調用了prctl("PR_SET_DUMPABLE"),不知為何。

然後調用newfstatat系統調用獲取/proc/<pid>/environ相關信息。

image.png

statbuf設置為struct stat statbuf類型後,可以看出調用newfstatat()是為了獲取時間戳來實現時間檢測。

  • tv_sec/proc/<pid>/environ最後修改時間st_mtim)。
  • statbuf.st_atim/proc/<pid>/exe最後存取時間st_atim)。
  • 因此v12 == 1 代表 environ 的修改時間晚於 exe 的存取時間。

image.png

image.png

接下來看check2()

解密了"magisk",然後調用openat系統調用打開"/proc/self/mounts",顯然是在檢測magisk。

image.png

調用read系統調用讀取/proc/self/mounts,每次讀2048字節,其中包含換行符,要手動處理。

處理完後就是檢查/proc/self/mounts每行是否包含magisk字樣,以此來檢測magisk。

image.png

然後check3()是經典的root檢測。

image.png

image.png

check4()是動調檢測。

image.png

最後是check5()

image.png

check5_func1()打開了/proc/self/maps,解密了解析maps文件的格式化字符串,保存在a1中。

image.png

check5_func2()調用read系統調用讀取maps文件,同樣手動處理換行符後,調用自實現的sscanf解析maps信息,最後保存到a1中。

image.png

調用完check5_func2()後,解密出了"/system/bin/app_process"字串,然後與maps_lib_path對比。

這裡是為了匹配maps中的/system/bin/app_process

image.png

匹配成功後,調用process_app_process()

image.png

process_app_process()會遍歷內存中的/system/bin/app_process64,看看其中是否存在magisk字串。

image.png

process_app_process()檢查完之後,會調用check5_func3()

image.png

check5_func3()同樣是一些magisk檢測,如下:

  1. 判斷PATH環境變量中是否包含.magisk/

image.png

  1. 判斷PATH環境變量中是否包含MAGISK

image.png

  1. 判斷包名結尾是否_zygote

image.png

至此分析完check_something()

1.3.3 調用.init_array函數 & 收尾

回到I3函數向下看。

有個while循環不斷從一個類似函數列表的地方取函數並調用,這個函數列表大概就是解密後的.init_array

image.png

.init_array[0]中調用了unsetenv("LD_PRELOAD"),這大概是一種反注入的機制。unsetenv()還會再遍歷一次PATH環境變量,確保LD_PRELOAD真的被unset掉,否則會直接exit()

image.png

除了.init_array[0]外,其他.init_array函數似乎都與檢測無關。

1.4 JNI_OnLoad分析 (part1)

由上述分析可知,I3函數執行完後,JNI_OnLoad()也被解密完成。因此在那時機下斷點跳過去分析JNI_OnLoad()

開始分析JNI_OnLoad()

調用了mprotect系統調用,賦予某片內存rwx的權限。

image.png

保存了JNI_OnLoad的前0x10字節,暫時未知用來做什麼。

image.png

然後就是熟悉的控制流。

image.png

同樣是一些函數解密的邏輯。

image.png

image.png

最終同樣會調用cache_maintenance()

之後調用了sub_777F32BB58(),其中會間接調用a1 + 48指向的函數。

image.png

跟進去後發現是GetEnv()

image.png

然後又判斷了一次package name是否以_zygote結尾。

image.png

之後調用了JNI_OnLoad_func1()進行一些檢測。

image.png

JNI_OnLoad_func1()中解密了以下字符串:( 都是模擬器的特徵 )

  • "lib3btrans.so"
  • "libhoudini.so"
  • "/lib/arm/nb/libc.so"
  • "/lib64/arm64/nb/libc.so"

檢測的邏輯同樣是按上述方式解析/proc/self/maps後,看看是否存在這些so,是則代表是模擬器。

image.png

之後反射調用了一個Java層的函數。

image.png

發現只是一個固定返回true函數?

image.png

然後動態注冊了一些JNI函數。

image.png

第1個register_natives()注冊了以下Java類的native函數:

  • com/inca/security/Native/AppGuardPreAssistantNative
  • com/inca/security/Scalar/SecureType
  • com/inca/security/DexProtect/SecureApplication

第2個register_natives()注冊了以下Java類的native函數:

  • com/inca/security/Proxy/iIiIiIiIii

image.png

然後又解密了一個函數( 下圖的sub_777E313F98() ) ,記該函數為JNI_OnLoad_decfunc1()

image.png

1.5 JNI_OnLoad_decfunc1分析 (part1)

進行了一些運算後,調用了KM4PI0Z7J8QMILO5G6P6()

image.png

KM4PI0Z7J8QMILO5G6P6()記錄了以下目錄的「最後存取時間」之和,但不知有什麼用。

  • "/storage/emulated/0/Music/"
  • "/storage/emulated/0/Android/"

image.png

之後又在一處fla中解密了某個函數,記該函數為big_func()

image.png

1.6 big_func分析

之所以叫big_func(),是因為該函數的F5偽代碼有六千多行。

1.6.1 sapi初始化

一開始解密了一些與libc相關的字串,通過sprintf()組裝後傳入了parse_libc()

image.png

parse_libc()先從/proc/self/maps獲取libc.so的相關信息,然後調用do_parse_libc()進行解析。

image.png

do_parse_libc()開頭解析了libc的dynamic。

image.png

do_parse_libc()最後分別調用了get_libc_info()來將ELF GNU Hash Table和dynamic等信息保存在args[0]中。

然後調用save_some_libc_sym()獲取了libc中的一些符號偏移,如clone()__libc_sysinfo_ZL13g_thread_list

image.png

回到big_func()繼續向下看。之後調用了lookup_libc_sym()

image.png

lookup_libc_sym()中調用了find_symbol_by_name()來根據函數名獲取對應的符號地址( 原理是gnu hash ),保存在全局變量g_libc_funcs中。

image.png

image.png

之後又解密了一些函數來加載自己的libc,主要邏輯在load_mylibc()中,加載後的libc記為mylibc

image.png

load_mylibc()主要干了以下事情:

  1. 調用LoadSection()加載所需的section信息。

image.png

  1. 調用ClearShdr()

image.png

  1. 重定向:

image.png

注:分析過程中會發現在LoadSection()ClearShdr()最後都有以下函數調用,其args[3]似乎就代表了所在函數的功能。

1
2
3
4
5
// 在LoadSection()最後:
sub_777F3E56B0(1u, 0x64u, 9u, (__int64)"LoadSection", 0x120u);

// 在ClearShdr()最後:
sub_777F3E56B0(1u, 0x64u, 9u, (__int64)"ClearShdr", 0x344u);

load_mylibc()執行完之後,從mylibc獲取了一些函數單獨保存下來,如malloccallocrealloc等等。

image.png

之後調用prepare_for_inline_hook()來保存指定mylibc函數的前0x32字節,結果保存在args[0]中,然後傳入inline_hook()進行hook。

image.png

以mylibc的calloc()為例,在inline_hook()後,前0x10字節被改為跳到原libc的calloc()

image.png

之後又繼續從mylibc獲取了一堆函數單獨保存到全局變量中,不一一列出了。

但獲取的目的並不是為了像上面那樣inline hook,而大概是之後會用到,所以提前保存下來。

image.png

1.7 JNI_OnLoad_decfunc1分析 (part2)

big_func()執行完後,回到JNI_OnLoad_decfunc1()

同樣的形式解密了一段代碼。

image.png

1.7.1 anti xposed & anti debugging

check_xposed()中遍歷了maps,看看是否存在XposedBridge.jar

image.png

之後會來到一個反調試的函數,記為anti_debugging()

image.png

調用mylibcpthread_create()創建了一個線程。

image.png

調用mylibcfork()創建了兩個子線程,之後調用signal(17, 1)忽略子線程結束時送出的SIGCHILD信號。

嘗試動調此處邏輯,但總會發生一些非預期的結果,因此無法詳細分析anti_debugging()的具體實現原理。

唯一確定的是它調用了兩次mylibc的fork(),使得此時機後將IDA無法attach,記這種反調試為三進程保護。

image.png

之後調用mylibc裡的pthread_create()創建一個線程,線程回調函數記為JOdf1_pthread_func3()

image.png

1.7.2 JOdf1_pthread_func3分析

一開始調用了check5() ( 上面已經分析過該函數,不再重複 )。

然後調用NativeBridge_check()進行NativeBridge注入檢測。

image.png

NativeBridge注入檢測的原理參考這篇文章,檢測流程如下:

  1. 獲取ro.dalvik.vm.native.bridge屬性值,不為空則繼續後續流程( 但其實即使在正常真機的環境下,返回的也是"0",同樣會繼續後續的檢測流程 )。

image.png

  1. 從maps獲取libnativebridge.so

image.png

  1. 調用libnativebridge.soNativeBridgeError(),若返回值非0代表檢測到。

image.png

1.8 JNI_OnLoad分析 (part2)

JNI_OnLoad_decfunc1()執行完後,回到JNI_OnLoad(),又調用了mylibcpthread_create()創建了線程。

之後會間接調用一個不正常地址導致SIGSEV?但pass to app後可以繼續執行,過段時間後才會真正crash?

image.png

2. bypass三進程保護

要先bypass掉libcompatible.so的反調試,也能繼續動調後面的libstub.so

2.1 bypass嘗試

嘗試直接patch掉anti_debugging()

1
2
3
4
5
6
7
function hook_anti_debugging(base) {
Interceptor.replace(base.add(0x18C148), new NativeCallback(function (arg0) {
console.log("[hook_anti_debugging] a0: ", hexdump(arg0.readPointer()));
return ptr(0);
}, 'pointer', ['pointer']));

}

但報了SIGSEGV的錯,報錯時的PC在0x7130,十分奇怪。

image.png

用frida + IDA動調後發現報錯點在下圖這裡。

image.png

這種情況有兩種可能性:

  1. 0x7130存放在某個全局變量a中,而anti_debugging()中會修改a的值為某個函數地址。
  2. anti_debugging()中注冊了SIGSEGV的信號回調函數。

而情況1基本可以排除掉,因為直接動調( 沒有patch anti_debugging() ),走到上圖位置時同樣是0x7130,而且用trace排查時,顯示的也是0x7130

因此大概率是情況2,而且調用的大概率也是mylibcsigaction

嘗試hook mylibc的sigaction(),但沒有觸發。

改為hook原libc的sigaction,反而有觸發,看來NP無法使用mylibc的sigaction()來注冊信號回調?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Interceptor.attach(Module.findExportByName("libc.so", "sigaction"), {
onEnter: function (args) {
console.log("[hook_sigaction] a0: ", (args[0]));
if (args[1].toInt32()) {
console.log("[hooksigaction] a1: ", hexdump(args[1]));
console.log("[hook_sigaction] a1: ", JSON.stringify(Process.findModuleByAddress(args[1].add(0x8).readPointer())));
console.log("[hook_sigaction] a1: ", ptr(args[1].add(0x8).readPointer() - base));
}

},
onLeave: function (retval) {
console.log("[hook_mylibc_sigaction] retval: ", retval);
}
});

輸出如下,0xb正是SIGSEGV,信號回調函數在libcompatible.so!0xaa994

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[hook_sigaction] a0:  0xb
[hooksigaction] a1: 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
7fc4082ba0 04 00 00 08 00 00 00 00 94 99 81 9e 71 00 00 00 ............q...
7fc4082bb0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
7fc4082bc0 12 ba 85 03 7c 62 4e 70 00 00 00 00 00 00 00 00 ....|bNp........
7fc4082bd0 80 2d 08 c4 7f 00 00 00 2c be 8f 9e 71 00 00 00 .-......,...q...
7fc4082be0 ac f4 8f 9e 71 00 00 00 9c 38 ba 84 00 00 00 00 ....q....8......
7fc4082bf0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
7fc4082c00 50 36 92 9e 71 00 00 00 00 00 00 00 00 00 00 00 P6..q...........
7fc4082c10 70 56 93 9e 71 00 00 00 b4 56 93 9e 71 00 00 00 pV..q....V..q...
7fc4082c20 7c 31 92 9e 71 00 00 00 ac f4 8f 9e 71 00 00 00 |1..q.......q...
7fc4082c30 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
7fc4082c40 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
7fc4082c50 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
7fc4082c60 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
7fc4082c70 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
7fc4082c80 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
7fc4082c90 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
[hook_sigaction] a1: {"name":"libcompatible.so","base":"0x719e76f000","size":1863680,"path":"/data/app/jp.goodsmile.touhoulostword_android-KNfNiSYf49IV5K_x4TTh3Q==/lib/arm64/libcompatible.so"}
[hook_sigaction] a1: 0xaa994
[hook_mylibc_sigaction] retval: 0x0

0xaa994如下,記為SIGSEGV_cb()

image.png

sub_187310()中可以看到0x7130

image.png

SIGSEGV_cb()最終會根據fault address分發不同的函數,之後會看到。

3. libstub.so分析

在bypass掉三進程保護後,終於可以動調分析後續的流程,首先是libstub.so的加載。

3.1 SoLibraryStart分析

libstub.so的.init_proc中會調用libcompatible.so!SoLibraryStart()來解密。

SoLibraryStart()中的qword_1B41E0固定是0x4580,因此必然會觸發SIGSEGV的信號回調SIGSEGV_cb()

image.png

SIGSEGV_cb()會根據導致異常的地址來執行對應的函數,如0x4580會執行func_4580()

image.png

3.2 func_4580分析 (part1)

打斷點跳到func_4580()進行分析。

調用了check_and_decrypt_libstub()

image.png

check_and_decrypt_libstub()中會先調用check_enc_flag()確定libstub.so是加密的。

image.png

check_enc_flag()具體實現如下,打開本地的libstub.so並檢查最後20字節是否與固定的加密標誌相等。

image.png

decrypt_libstub_data()一開始先獲讀取了libstub[0:0xB758],然後再讀取libstub[0x1CAA8: 0x1CAA8 + 0x3EFEE],記為後者為libstub_enc_data1

image.png

注:從010可知0x1CAA8正好在section header後面。

image.png

第1處解密libstub_enc_data1的地方。

image.png

第2處解密libstub_enc_data1的地方。

image.png

第3處解密libstub_enc_data1的地方。

image.png

調用LZ4_decompress_fast()對解密後的libstub_enc_data1進行解壓。

image.png

解壓後的數據如下,可以看到包含一些重定向信息,記為dec_data1

image.png

讀取了libstub[0x5BA96:0x5BA96 + 0x15C0],記為libstub_enc_data2,然後解密,再解壓。

image.png

解壓後的數據如下,可以看出應該是符號表,記為sym

image.png

讀取了libstub[0x5D056:0x5D056+ 0x1230],記為libstub_enc_data3,然後解密,再解壓。

image.png

解壓後的數據如下,可以看出是字符串表,記為strtab

image.png

讀取了libstub[0x5E286:0x5E286 + 0x29B0],記為libstub_enc_data4,然後解密,再解壓。

image.png

解壓後的數據如下,可以看到同樣是一些重定向信息( 403重定向 ),記為relocate1

image.png

讀取了libstub[0x60C36:0x60C36 + 0x4F0],記為libstub_enc_data5,然後解密,再解壓。

image.png

解壓後的數據如下,可以看到同樣是一些重定向信息( 402重定向 ),記為relocate2

image.png

讀取了libstub[0x61126:0x61126 + 0x30],記為libstub_enc_data6,然後解密,再解壓…

image.png

解壓後的數據如下,可以看出是libstub.so的依賴庫。

image.png

最後讀取了libstub[0x61146:0x61146 + 0x20],但似乎沒有用到,大概只是加密數據的結束標誌?

image.png

至此大致分析完check_and_decrypt_libstub()。回到func_4580()

調用set_w_perm_to_libstub()賦予libstub.so可寫的權限。dlopen_needs()會調用dlopen()加載libstub.so的所有DT_NEED庫。

MEMORY[0x7510]()會觸發SIGSEGV_cb(),然後跳到func_7510()

image.png

3.3 func_7510分析

看到"ldrRestoreDynSymInfo",見名知意,大概是在恢復libstub.so的一些動態符號信息。

image.png

忽略前面一些不太知道在干什麼的邏輯後,來到下圖這裡,解密了一些符號名。

image.png

image.png

image.png

然後會對比libstub.so的字符串表( 上面解密出來的那個 ),若包含上述的符號名,則會把對應函數( 如下圖的some_func2(),它是libcompatible.so的函數 )保存到某個地方。

猜測是libstub.so之後會用到libcompatible.so中的一些函數,因而用這種方式提前保存函數地址到某處。

image.png

3.4 func_4580分析 (part2)

func_7510()之後,回到func_4580()會執行重定向的邏輯,然後調用.init_array的函數。

3.4.1 relocate

一開始解密了"dlopen""dlsym""il2cpp.so"等字符串,不知道干什麼。

image.png

然後就是熟悉的重定向操作:

image.png

image.png

注:重定向的對象並非maps中的libstub.so,而是內存中的libstub.so

image.png

通過maps查看內存中的libstub.so範圍,然後把這片內存dump下來,記為libstub_dump.so

image.png

1
2
3
4
5
import idaapi
data = idaapi.dbg_read_memory(0x7c61330000, 0x7c613d2000 - 0x7c61330000)
fp = open(r'libstub_dump.so', 'wb')
fp.write(data)
fp.close()

3.4.2 call_constructors

調用libstub.so.init_array函數。

image.png

.init_array的偏移為0x8FDE0

image.png

3.5 修復libstub.so

dec_data1 dump下來會發現,其中包含了relocate1relocate2,還有.dynamic信息。

image.png

至於解密後的代碼段和數據段,應該已經在libstub_dump.so中,但libstub_dump.so中並不包含strtabsym和.dynamic的信息。

也不包含shdr和shstrtab字符串表,這兩者可從原libstub.so中獲取,前者在修復時直接覆蓋到原位置,後者隨便寫到最後的一片空區域即可。

image.png

從原libstub.so提取shstrtab

image.png

section修複,大概只有.dynstr.dynamic.shstrtab是必要的,第0個section要為0,拉入IDA時才不會報錯。

image.png

把修復後的libstub.so拉入IDA,發現只有少量的符號。

image.png

3.6 init_array_func1分析

大概只有第1個.init_array函數是檢測的邏輯,同樣也是對環境變量的檢測,具體做法是在unsetenv("LD_PRELOAD")後,遍歷environ數組,若發現其中仍有LD_PRELOAD則直接exit()

image.png

3.7 JNI_OnLoad分析

保存了一些libdl.solibc.so的函數到某個全局變量中。

image.png

嘗試尋找"com/inca/security/DexProtect/SecureApplication"類,但在本例會返回0

image.png

動態注冊了"com/inca/security/Native/AppGuardAssistantNative""com/inca/security/AppGuard/TestCase"中的一些JNI函數。

image.png

第1處register_func()動態注冊了9個JNI函數,如startEngine()stopEngine()等等。

image.png

第2處register_func()動態注冊了4個JNI函數,如下圖所示。

image.png

最後創建了兩個線程,暫時不知有什麼用。

image.png

3.8 startEngine分析

startEngine()被控制流混淆了,如下所示。

image.png

同時其中充斥著大量的空函數,或許也是一種混淆。

image.png

分析的思路是,忽略上面這樣的空函數,關注那些有內容的函數,下斷點跳過去調試。

最終可把stratEngine()分成三部份,第一部份如下。

image.png

第二部份。

image.png

第三部份。

image.png

在這三個部份中,都會調用某片匿名內存中的函數,一開始沒有多想,直接單步跟了進去調試,後來才想起這會不會就是libengine.so

記那片匿名內存為mem_libengine,對比mem_libenginelibengine.so後發現,它們前面的字節的確是一樣的。( 本以為startEngine()會是加載libengine.so的邏輯,其實不然 )。

image.png

image.png

4. libengine.so分析

4.1 哪裡加載的libengine.so?

知道了mem_libengine就是libengine.so後,以此作為入手點,分析哪裡加載的。

libstub.so!JNI_OnLoad leave時機,maps中仍未有mem_libengine。在getVersionsetContext時,maps中有mem_libengine

而在上面的分析中提過,libstub.so!JNI_OnLoad最後創建了一些線程,最終確定大概率是在sub_53AC4中加載的libengine.so

通過frida stalker確定了sub_53AC4中會調用libcompatible.so!0xA1990

image.png

4.2 libengine.so加載

接下來看看libcompatible.so!0xA1990是怎麼加載libengine.so的。

首先調用了LoadEngineLibrary(),其中一開始拼接了libengine.so的絕對路徑。

image.png

然後調用sys_mmap()映射一片匿名內存,調用sys_lseek() + sys_read()讀取libengine.so

image.png

LoadEngineLibrary()之後會來到熟悉的解密函數( 在上方被我命名為check_and_decrypt_libstub )。

image.png

注:邏輯基本相同,不再重複分析,將所需數據dump下來。

image.png

之後同樣調用了dlopen_needs()

image.png

然後是ldrRestoreDynSymInfo(),但這次是直接調用,而非通過信號回調的形式來間接調用。

image.png

然後是relocate()

image.png

可以在403重定向的位置,把內存中的libengine.so dump下來。

image.png

注:libengine_base是由*(a1 + 2680)而來,2560則是libstub.so的。

image.png

4.3 修復libengine.so

大致跟修復libstub.so一樣,都需要從dump出來的libengine中提取.dynamicshstrtab

不同的是這次連ehdr和phdr都沒有,需要手動補上,而且dump出來的libengine.so中也不帶重定向信息,也需要手動補。

好消息是修復相對簡單,壞消息是只有一些無用的符號!!!

image.png

4.4 一些檢測

通過hook libengine.soget_data_string()簡單看看其中的一些檢測點。

  1. 第N次的環境變量檢測。
1
2
3
4
5
[libengine_get_data_string2]  LD_PRELOAD
[libengine_get_data_string2] LD_PRELOAD
[libengine_get_data_string2] LD_PRELOAD
[libengine_get_data_string2] LD_PRELOAD
[libengine_get_data_string2] LD_PRELOAD
  1. 第N次的模擬器檢測。
1
2
3
4
[libengine_get_data_string2]  libhoudini.so
[libengine_get_data_string2] lib3btrans.so
[libengine_get_data_string2] /lib/arm/nb/libc.so
[libengine_get_data_string2] /lib64/arm64/nb/libc.so
  1. 一堆作弊工具。
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
[libengine_get_data_string2]  Unknown
[libengine_get_data_string2] UserPattern
[libengine_get_data_string2] GameGuardian
[libengine_get_data_string2] GameKiller
[libengine_get_data_string2] TeamCrakMemoryDump
[libengine_get_data_string2] YouXiXiuGaiQi
[libengine_get_data_string2] ChaoHaoWanXiuGaiQi
[libengine_get_data_string2] ParadiseIslandTraniner
[libengine_get_data_string2] QuanQuanYouXiZhouShou
[libengine_get_data_string2] HuangHL
[libengine_get_data_string2] GameCIH
[libengine_get_data_string2] GameSpeeder
[libengine_get_data_string2] SBTools
[libengine_get_data_string2] GameCheater
[libengine_get_data_string2] GameMaster
[libengine_get_data_string2] HexEditor
[libengine_get_data_string2] HaXplorer
[libengine_get_data_string2] DaxAttack
[libengine_get_data_string2] GMDSpeedTime
[libengine_get_data_string2] CoolBoyTimer
[libengine_get_data_string2] Freedom
[libengine_get_data_string2] RootCloak
[libengine_get_data_string2] LuckyPatcher
[libengine_get_data_string2] CheatEngine
[libengine_get_data_string2] BaMenYouXiXiuGaiShenQi
[libengine_get_data_string2] SpeedWizard
[libengine_get_data_string2] XiongMaoXiaYouXiZhuShou
[libengine_get_data_string2] TransGameHacker
[libengine_get_data_string2] AppCIH
[libengine_get_data_string2] xxAssistant
[libengine_get_data_string2] RepetiTouch
[libengine_get_data_string2] BotMaker
[libengine_get_data_string2] AnJianJingLing
[libengine_get_data_string2] YouXiFengWo
[libengine_get_data_string2] FingerReplayer
[libengine_get_data_string2] HiroMacro
[libengine_get_data_string2] SmartMacro
[libengine_get_data_string2] TheToucher
[libengine_get_data_string2] AutoTouch
[libengine_get_data_string2] ScreencastVideoRecorder
[libengine_get_data_string2] SCRScreenRecorder
[libengine_get_data_string2] LiziModifier
[libengine_get_data_string2] ZhuoMuNiao
// ...
  1. 一堆奇怪的包名。
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
[libengine_get_data_string2]  com.vqs.iphoneassess
[libengine_get_data_string2] com.huluxia.gametools
[libengine_get_data_string2] com.huluxiakajkia
[libengine_get_data_string2] com.huluxia.gametoolsdwaf
[libengine_get_data_string2] com.huati
[libengine_get_data_string2] com.zhushou.cc
[libengine_get_data_string2] com.jbelf.imei
[libengine_get_data_string2] com.mostwanted.crackzxc
[libengine_get_data_string2] com.paojiao.youxia
[libengine_get_data_string2] com.yx.youxia
[libengine_get_data_string2] com.yx.youxia_shuih
[libengine_get_data_string2] com.medroid.sbjiaqiangban
[libengine_get_data_string2] com.burakgon.gamebooster2
[libengine_get_data_string2] com.anzhuo.GameToos
[libengine_get_data_string2] mobi.infolife.gamebooster
[libengine_get_data_string2] com.android.gamespeedup
[libengine_get_data_string2] com.iplay.assistant
[libengine_get_data_string2] org.game.master
[libengine_get_data_string2] com.mjmcmdjijkjnjn.wdjdy
[libengine_get_data_string2] com.mnmfmnmfnnmhmo.wdmc
[libengine_get_data_string2] com.jjjnjmjljnjljpjmjl.wd
[libengine_get_data_string2] com.kkckchbjbe.ywwd
[libengine_get_data_string2] cn.com.opda.gamemaster
[libengine_get_data_string2] com.surcumference.xsposed
// ...
  1. 一堆可執行文件。
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
[libengine_get_data_string2]  /dev/input
[libengine_get_data_string2] /system/bin/input
[libengine_get_data_string2] /system/bin/monkey
[libengine_get_data_string2] /proc/self/maps
[libengine_get_data_string2] /proc/%d/maps
[libengine_get_data_string2] /proc/self/mem
[libengine_get_data_string2] /proc/%d/mem
[libengine_get_data_string2] /system/bin/dnsmasq
[libengine_get_data_string2] /system/bin/dnsmasq
[libengine_get_data_string2] /system/bin/efsks
[libengine_get_data_string2] /system/bin/efsks
[libengine_get_data_string2] /system/bin/pam_server
[libengine_get_data_string2] /system/bin/pam_server
[libengine_get_data_string2] pam_server
[libengine_get_data_string2] pam_server
[libengine_get_data_string2] /system/bin/pppd
[libengine_get_data_string2] /system/bin/pppd
[libengine_get_data_string2] /system/bin/ip6tables
[libengine_get_data_string2] /system/bin/ip6tables
[libengine_get_data_string2] /system/bin/iptables
[libengine_get_data_string2] /system/bin/iptables
[libengine_get_data_string2] /system/bin/ip
[libengine_get_data_string2] /system/bin/ip
[libengine_get_data_string2] /system/xbin/spritebud
[libengine_get_data_string2] /system/xbin/spritebud
[libengine_get_data_string2] /system/xbin/vold
[libengine_get_data_string2] /system/xbin/vold
[libengine_get_data_string2] /system/xbin/netd
[libengine_get_data_string2] /system/xbin/netd
[libengine_get_data_string2] /system/xbin/installd
[libengine_get_data_string2] /system/xbin/installd
[libengine_get_data_string2] /sbin/ueventd
[libengine_get_data_string2] /sbin/ueventd
[libengine_get_data_string2] /system/xbin/tcd
[libengine_get_data_string2] /system/xbin/tcd
[libengine_get_data_string2] /sbin/fsck_msdos
[libengine_get_data_string2] /sbin/fsck_msdos
// ...
  1. Magisk
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[libengine_get_data_string2]  Magisk v16.3
[libengine_get_data_string2] 24576:Q3LvBhUvgZiPYOvdwpohg+A6ht6vDM3Wr/ZLpzuzK:iqCc/Qo3WN0K
[libengine_get_data_string2] 24576:ODIjkIhUvgZicFNM3WFPn7IfyG+K6BACjYDlZfjzW:vw3WF7ji0EY
[libengine_get_data_string2] 192:b60cVwbwmw1m1SMzRvyOEgRphVdKT+GxLSpyijl:u0cV+wmw1m1SMzRvyOEgRphVdY5LSXjl
[libengine_get_data_string2] 192:01gS1jU9ywbtTgRt7r+k1Sv1QspVmT+3Pv3WWvP8h:01gSi9ywbtTgRRr+k1Sv1QspV0CPvWWu
[libengine_get_data_string2] Magisk v16.4
[libengine_get_data_string2] 24576:TRAT0nEPHDSDC3N1Jd6/HIXaXfOO2SYC05PWIuul:aeDCfJd6/HIXa8PWIvl
[libengine_get_data_string2] 24576:i0yPElnEPHDSV4a5PWbzEDCHWJqV8/UcX+3fKh2pdG1Vtm:8reyAPWkDC+qV8/UcX+As
[libengine_get_data_string2] 192:x5kTwoH/w1m1SMzRvyOEgRphVdKT+GcLSpl+2i2pb:x2TwoH/w1m1SMzRvyOEgRphVdY0LSi2X
[libengine_get_data_string2] 192:x5kTwoH/w1m1SMzRvyOEgRphVdKT+GcLSpl+2i2pb:x2TwoH/w1m1SMzRvyOEgRphVdY0LSi2X
// ...
[libengine_get_data_string2] MagiskManager v5.7.0
[libengine_get_data_string2] 24576:TRAT0nEPHDSDC3N1Jd6/HIXaXfOO2SYC05PWIuul:aeDCfJd6/HIXa8PWIvl
[libengine_get_data_string2] 24576:i0yPElnEPHDSV4a5PWbzEDCHWJqV8/UcX+3fKh2pdG1Vtm:8reyAPWkDC+qV8/UcX+As
[libengine_get_data_string2] 192:x5kTwoH/w1m1SMzRvyOEgRphVdKT+GcLSpl+2i2pb:x2TwoH/w1m1SMzRvyOEgRphVdY0LSi2X
[libengine_get_data_string2] 192:a1gSyU9ywbtTgRt7r+k1Sv1eH6spVmT+wPv3WWvptY:a1gS/9ywbtTgRRr+k1Sv1eH6spV0FPvs
// ...

  1. 又一堆作弊工具
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
[libengine_get_data_string2]  YouXiFengWo v8.0.0
[libengine_get_data_string2] 98304:XXQYQqjBZK5eP/a8eOClHMyj4+/nc/FO1X7H:VJK5K/axTnc/FO9z
[libengine_get_data_string2] 98304:/U5oslClHMmj4+/O4qq8amgdG74GqDpU:M5oHTO4kamgQ4GYU
[libengine_get_data_string2] 1536:T7riB4iWRZdXOyOzdd0mFbOn6RYj/tIbGoH:XbeyOzddW6/bGoH
[libengine_get_data_string2] 1536:T7riB4iWRZdXOyOzdd0mFbOn6RYj/tIbGoH:XbeyOzddW6/bGoH
[libengine_get_data_string2] LuckyPatcher v4.1.9
[libengine_get_data_string2] 12288:k7glLaM1QMzMuK4PFSGOtB8j1n3A8ldEGt/tt3R3Mt/+8+C+ZtEt/3U33tR+i+PM:0M19DFLOD8j1blb41dXzo0agrJ0Wcug
[libengine_get_data_string2] 24576:agtzGv0XzvsaCEdoupLodt9KIIqCh8SOnSPNcHpeZ0s7:yYdCQpLodtgOnW+Yz
[libengine_get_data_string2] 192:psgSGHUf7u3yFOVrT8dK171SX14gDwFuL/D1+h8CYpm:psgSdf7u3yFOVrT8dK171SX14gDouL/A
[libengine_get_data_string2] 192:5sgSGHUf7u3yFOVrT8dK171SX14gDwFuL/D1NAOtr84mn:5sgSdf7u3yFOVrT8dK171SX14gDouL/Y
[libengine_get_data_string2] GameHacker v2.6.3
[libengine_get_data_string2] 6144:jV4GcAmz53H1eaMkCtqJr74+/zWZ7ZOR6p9xK5LeEXmkiMxh5UBWF9dCEOCiqop+:jacqNYZ7ZOR6pgLL8GhIWx9yJHXQ
[libengine_get_data_string2] 6144:jV4GcAmz53H1eaMkCtqJr74+/zWZ7ZOR6p9xK5LeEXmkiMxh5UBWF9dCEOCiqop+:jacqNYZ7ZOR6pgLL8GhIWx9yJHXQ
[libengine_get_data_string2] 96:ep3JWcOdtBV7ngScytHD6h+DTypBxRl3stWtEtVr3StC1tVtRHt/5tatWntBZt8s:eZOBhgSt4qTypBxRl3Wr7vhAQWbTij9D
[libengine_get_data_string2] 96:ep3JWcOdtBV7ngScytHD6h+DTypBxRl3stWtEtVr3StC1tVtRHt/5tatWntBZt8s:eZOBhgSt4qTypBxRl3Wr7vhAQWbTij9D
[libengine_get_data_string2] GameKiller v3.4.3
[libengine_get_data_string2] 98304:swAXGSs/B1OsdbM1Ccc5JiOienK628BjFMihcTgFR:NAXGSs/BYisc5JioDagR
[libengine_get_data_string2] 98304:D1Osd9K628B6eIj4eDhqT/+0BFGnktk7uQ/:DYD9rwi0BFUX7N
[libengine_get_data_string2] 768:zXgSdA13ryngSai6zO0o8co3ukZmtfrQN5Ato25o6kckWkQkBkSckkk/kikrkpk7:cRAkHtbySHvc5IRgoBnbIlbIO
[libengine_get_data_string2] 768:yXgSFA13ryngSai6zO0o8co3ukZmtfrQN5Ato25o6kckWkQkBkSckkk/kikrkpki:RRAkHtbySHvc5IRgoBnbwleUQ
[libengine_get_data_string2] xxAssistant v2.4.0
[libengine_get_data_string2] 49152:JbZEnH3yH8BBZGVDg3BaCe3JeiQw8zWC0s376e5HO17A0X8OBctF+pSCzbdLyUZC:JKnHAJv8R0s/ROA0iFUvdmUg
[libengine_get_data_string2] 49152:ut4+T3yH8BBZGVDg3BaCwAOBctF+p9m+k6oMNZ+69P+YHigdr0bZSd9MITAMltTH:K4CdF7L6FNZ+KHiw0VstAMltRL1
[libengine_get_data_string2] 768:pDmpgSqyD9mtGvOeSHSbzMNUXZZtPwci2ArFZQzRTBvy57U3RV5FYQwDbo60mSvu:9gMztdc81Se
[libengine_get_data_string2] 768:/DmpgSyyD9mtGvOeSHSbzMNUXZZtPwci2ArFZQzRTBvy57U3RV5FYQwDbo60mSvZ:L4MzDyQH7M
// ...
  1. 一些模擬器特徵
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
[libengine_get_data_string2]  GenyMotion
[libengine_get_data_string2] genymotion
[libengine_get_data_string2] GenyMotion
[libengine_get_data_string2] genymotion
[libengine_get_data_string2] Windroy
[libengine_get_data_string2] windroy
[libengine_get_data_string2] Windroy
[libengine_get_data_string2] windroy
[libengine_get_data_string2] Droid4X
[libengine_get_data_string2] droid4x
[libengine_get_data_string2] Droid4X
[libengine_get_data_string2] droid4x
[libengine_get_data_string2] TianTian
[libengine_get_data_string2] TianTian
[libengine_get_data_string2] TianTian
[libengine_get_data_string2] TianTian
[libengine_get_data_string2] CanPlay
[libengine_get_data_string2] canplay
[libengine_get_data_string2] CanPlay
[libengine_get_data_string2] canplay
[libengine_get_data_string2] VPhoneGaGa
[libengine_get_data_string2] VPhoneGaGa
[libengine_get_data_string2] BlueStacks
[libengine_get_data_string2] com.bluestacks.*
[libengine_get_data_string2] BlueStacks
[libengine_get_data_string2] com.bluestacks.*
[libengine_get_data_string2] MEmu
[libengine_get_data_string2] com.microvirt.*
[libengine_get_data_string2] MEmu
[libengine_get_data_string2] com.microvirt.*
[libengine_get_data_string2] Andy
[libengine_get_data_string2] org.greatfruit.andy.*
[libengine_get_data_string2] Andy
[libengine_get_data_string2] org.greatfruit.andy.*
[libengine_get_data_string2] DuOS
[libengine_get_data_string2] com.ami.syncduosservices
[libengine_get_data_string2] DuOS
[libengine_get_data_string2] com.ami.syncduosservices
[libengine_get_data_string2] TianTian
[libengine_get_data_string2] com.tiantian.ime
[libengine_get_data_string2] TianTian
[libengine_get_data_string2] com.tiantian.ime
[libengine_get_data_string2] RemixOS
[libengine_get_data_string2] com.jide.*
[libengine_get_data_string2] RemixOS
[libengine_get_data_string2] com.jide.*
[libengine_get_data_string2] CanPlay
[libengine_get_data_string2] com.huidong.canplay
[libengine_get_data_string2] CanPlay
[libengine_get_data_string2] com.huidong.canplay
[libengine_get_data_string2] Nox
[libengine_get_data_string2] /system/bin/nox
[libengine_get_data_string2] Nox
[libengine_get_data_string2] /system/bin/nox
[libengine_get_data_string2] TianTian
[libengine_get_data_string2] /system/bin/ttVM-prop
[libengine_get_data_string2] TianTian
[libengine_get_data_string2] /system/bin/ttVM-prop
[libengine_get_data_string2] MoMo
[libengine_get_data_string2] /system/app/MOMOStore/MOMOStore.apk
[libengine_get_data_string2] MoMo
[libengine_get_data_string2] /system/app/MOMOStore/MOMOStore.apk

5. 結語

看到這的讀者應該可以大致感受到NP的強大,自實現linker、自加載libc、信號回調的應用、反調試、各種檢測點、混淆,任何一點都值得仔細分析。除了這些,其實還有很多東西沒有分析到,但礙於時間和水平有限,筆者也只能分析到這裡,後續有時間或許可以再看看其他部份。

最後感謝乐佬的那篇文章,時至今日還是很有參考價值,還要感謝Code大佬,沒有他的的那句提點,或許就沒有這篇文章。

歡迎交流安卓手遊安全相關的內容,不論是攻方或防方的^^ ( dng6IGFzZDI4NzYxMzA5 )。