前前後後大概分析了這樣本4次左右,前3次都以失敗告終,或許對於普通人來說,失敗才是人生的主旋律,接觸逆向後對這句話越來越有感觸。
本文主要分析的目標是frida/hook檢測。
frida hook後會立即閃退,hook dlopen
後可知是在加載lib__6dba__.so
時閃退,具體是在lib__6dba__.so
的.init_array
裡。而.init_array
中只有一個start
函數。
frida hook了一次之後,下次就算不hook正常打開APP也會閃退,大概率檢測了frida的maps特徵。
一開始會調用get_custom_scetion
獲取lib__6dba__.so
中的加密數據。

具體實現如下:
首先用openat
、lseek
、read
等系統調用打開並讀取lib__6dba__.so
,然後遍歷獲取最後一個loadable segment的結束地址,記為last_loadseg_end
。

用010查看last_loadseg_end
偏移指向的數據,可以看出明顯是一些高熵數據,記這些數據為enc_data
。

繼續向下看,它又遍歷shdr table獲取自定義的一個section。

從010可以看出,該section同樣是指向上述last_loadseg_end
那附近。

雖然不知為何要分別通過phdr和shdr來定位enc_data
,但總的來說get_custom_scetion
函數的功能就是獲取enc_data
。
回到start
函數,獲取完enc_data
後,調用decrypt1
和decrypt2
來解密。

解密出來的數據其實是一些可執行的邏輯,由於它是通過mmap
映射 + mprotect
賦予可執行權限的方式來執行,因此記這種形式為mmap模塊,根據創建順序記為mmap1
模塊、mmap2
模塊、…,如此類推。frida的檢邏邏輯明顯就在其中。

注:該保護使用了大量的系統調用( 上述的mmap
和mprotect
都是指系統調用 ),一些基礎函數如strcpy
、strlen
、memset
等都是自實現的。
一開始我選擇通過動調來分析上述的mmap1
模塊,發現mmap1
中會創建和調用mmap2
、mmap3
、mmap4
模塊,同理mmap2 ~ 4
模塊又分別會創建和調用更多的mmap模塊,如此一來使得動調難以分析( 最主要是因為在mmap模塊中記錄的注釋、重命名變量名、函數名等都無法持久地保存 )。
但動調也並非毫無收獲,可以得知以下幾點:
由於難以動調,只好以純hook的方式來分析,在此之前要先將所有mmap模塊dump下來,遊戲閃退前創建的mmap模塊共有13
個。
可以通過frida或qbdi等方式來dump和trace所有mmap模塊,dump文件記為mmap_<base>_<size>_<idx>.bin
,trace文件記為log.txt
( 主要記錄函數調用關系,用利用qbdi可以很方便實現 )。
然後按字節特徵來判斷mmap1 ~ mmap13
,獲取分別的基址,以此進行hook。hook mmap1 ~ 4
的例子如下所示。
閃退前的最後一個模塊是mmap13
,大概率會包含檢測frida的邏輯,因此重點分析這個模塊。
先找到字符串解密函數,其特徵如下,返回值就是解密後的字符串:

hook輸出如下:
比較可疑的是/proc/self/maps
,打印調用棧發現在mmap13!0xF348
,而該地址所在函數的交叉引用在0x4D28
。

bl sub_F2E8
所在地址是0x4D28
,加上mmap13
的基址是0x7AB2D21D28
。

在log.txt
裡搜0x7AB2D21D28
找到對應地方查看函數調用關系,發現以下函數調用順序:
由此猜測可能是在檢查自身的so庫有沒有被hook。
嘗試hook mmap13
的vsnprintf
,將lib__6dba__.so
替換為另一個沒被hook的庫libpad.so
( 這個庫也是APP本身的 )。
替換前,vsnprintf
的輸出如下:
替換後,vsnprintf
的輸出如下,可以看到多了兩行關於libc.so
的日志
用同樣方法將libc.so
替換為libz.so
,發現APP終於不會在mmap13
模塊之後馬上閃退,反而又再創建了其他模塊。

簡單小結,mmap13
模塊應該是先檢測了lib__6dba__.so
( APP本身的so庫 )有沒有被hook,若前者通過檢測,則再檢測libc.so
( 系統so庫 )有沒有被hook,都通過後才會創建新模塊進行其他檢測,否則就用某些手段讓程序退出。
手動patch mmap13
模塊後,多了很多新模塊,是在mmap3
模塊裡創建的,索引由14
開始,共有mmap14 ~ mmap30
模塊。
上一小節通過trace日志 + 經驗猜測的方式成功bypass了lib__6dba__.so
中的hook檢測,這一小節嘗試分析看看具體的檢測原理。
hook mmap13
模塊封裝的syscall,在系統調用是openat
且path包含lib__6ba__.so
時打印調用棧,然後一路向上跟,最終發現是在mmap13!0x3BF0
裡打開lib__6ba__.so
的。
詳細調用鏈如下:( ins addr
代表指令地址,func addr
代表函數起始地址 )
0x394C
( 調用sub_4684
的指令地址 )附近的邏輯如下,記所在函數為mmap13_main
。
測試發現,按上述「hook mmap13的vsnprintf
,將lib__6dba__.so
替換為另一個沒被hook的庫libpad.so
」後,sub_4C20
函數會返回1
,否則返回0
。
由此可知sub_4C20
要麼是具體的檢測函數,要麼是處理檢測結果的函數。記sub_4C20
為mb_detect_func
。

進入mb_detect_func
分析,一路通過hook驗證,會發現get_so_info
這個比較關鍵的函數。
一開始以為get_so_info
是具體的檢測函數,因為hook發現get_so_info
共調用了3次,而且hook mmap13
的openat
系統調用時,看到它打開了3個自身的so庫,正好與之對應。由此猜測前2次get_so_info
執行後的a1
為0
是因為我沒有hook libopenal.so
和libpad.so
,而第3次不為0
是因為hook了lib__6dba__.so
被檢測到。

但後來詳細分析get_so_info
後發現它其實只是在解析、保存/proc/pid/maps
裡的信息( so_info[0]
保存著so的二進制信息 ),前2次的a1
為0
是因為這時機還未加載那兩個lib庫,因此才為0
。
繼續向下看,so_info
( so_img
)之後會傳入do_something1
函數,返回值保存在dest
,然後會與*(_DWORD*)(v8+0x3C)
對比,若不相等會導致最終走向wrong_branch
。
由此猜測*(_DWORD *)(v8 + 0x3C)
應該是原始lib__6dba_.so
.text段的hash值,dest是/proc/pid/maps
裡lib__6dba__.so
.text段的hash值。

進入do_something1
,一開始在通過so_img
解析重定向表,但沒看出來有什麼用。

繼續向下可以看到關鍵的while循環。

其中的hash_sum
是一堆計算,應該是在計算類似哈希值的東西,嘗試hook該函數會發現args[0]
曾出現過lib__6dba__.so
的.text段,args[1]
是.text段的大小,args[2]
保存計算結果。

而後發現,針對自身的每個so,總共會調用2次hash_sum
( 在兩處不同的位置 )來計算哈希值:
第1次大概是為了校驗完整性之類的,第2次顯然就是在校驗是否被hook,這樣使得常規的IO重定向似乎無法直接繞過?

小結:對於local lib( APP自身的庫 ),會調用hash_sum
函數進行校驗,與之對比的值應該是提前計算好內置到so中的。
通過上述的local lib檢測後,才會繼續調用check_libc
函數來檢測libc.so
( 貌似只檢測了libc這個系統庫 )。下圖所在函數是mmap13_main
。

check_libc
中調用了do_something2
函數。

接下來詳細分析do_something2
函數。
首先調用parse_elf_data
函數來解析指定so,args[0]
是libc.so
映像的地址( 該映像是在此之前通過openat系統調用打開&讀取的 )。解析結果保存在soinfo
中( 這並非linker那個soinfo )。

然後解密了一個關鍵字符串.text
,傳入了get_section_info
函數,它會返回libc.so
的.bss
段中的某段數據,其中包含指定section的信息,記為section_info
。
如*(section_info+0x10)
就是指定section的offset。

之後會遍歷maps_item
( /proc/pid/maps
的每一行我稱為一個maps_item
),當遍歷到libc.so
的.text
段的下一段時,才會滿足下圖的第1個if條件。
正常手機沒有啟動過frida時,會滿足第2個if條件( 即.text
段的下一段一定大於等於.text
段結束的位置 ),最終走到真正檢測libc的地方。

當不滿足上述第2個if條件時,會走下圖這裡,而且會循環多次。
第1個紅框代表最多循環10次,若遍歷完.text
段的後10個maps_item
仍沒有發現大於.text
段結束的,代表有問題,最終會導致程序走向閃退的錯誤分支。
正常沒有被frida干預的程序流會在第2個紅框那裡直接goto LABEL 49
。

而goto LABEL 49
最終會走到這裡,調用do_check_libc
進行真正的libc校驗。

do_check_libc
函數裡有些關鍵字符串信息,如下。

而do_check_libc
的具體原理,有興趣的靚仔可以自己分析看看。
通過hook mmap13
模塊的vsnprintf
繞過對lib__6dba__.so
和libc.so
的校驗後,會加載libopenal.so
和libpad.so
( 它們是APP自身的so庫 ),然後發現這兩個so庫同樣存在與lib__6dba__.so
一樣的start
函數,同樣存在上述的mmap模塊檢測,同樣會檢驗local lib和system lib。
好消息是它們大致使用了相同的mmap模塊來進行檢測,不同的只有mmap模塊創建的數量,如libopenal.so
創建的mmap11
模塊其實是lib__6dba__.so
創建的mmap13
模塊。
而mmap模塊會調用vsnprintf
來拼接庫的完整路徑,因此可以hook vsnprintf
來改變指定庫路徑,重定位到其他沒有被hook的庫,以此來繞過檢測。具體方式在上文中已經給出,就不再重複。
這個遊戲的保護是我遇到數一數二難的,難點在於它十分麻煩,且只能以hook的方式來調試,但找對方法後還是可以一點一點分析並解決的,不至於像一些VM那樣無從下手。
同時本文只大致分析了其中的一個模塊,各位讀者有興趣可以自己看看其他模塊,大概有29個模塊,也是挺有意思的。
let hooked =
false
;
let mmap_history = {}
function hook_func_init(soName) {
if
(hooked)
return
;
hooked =
true
;
function hook_syscall() {
function is_mmap1 (addr) {
let byte_arr = [
0xF0, 0x7B, 0xBF, 0xA9, 0x30, 0x01, 0x00, 0xB0, 0x11, 0x86,
0x42, 0xF9, 0x10, 0x22, 0x14, 0x91, 0x20, 0x02, 0x1F, 0xD6
]
let offset = 0x440;
for
(let i = 0; i < byte_arr.length; i++) {
if
(addr.add(offset).add(i).readU8() != byte_arr[i])
return
false
;
}
return
true
;
}
function hook_mmap1(mmap_base) {
Interceptor.attach(mmap_base.add(0xF9B0), {
onEnter: function(args) {
this
.sysno = args[7];
this
.a0 = args[0]
this
.a1 = args[1]
this
.a2 = args[2]
},
onLeave: function(retval) {
if
(
this
.sysno == 0xde) {
mmap_history[retval] =
this
.a1;
}
if
(
this
.sysno == 0xe2) {
console.
log
(
"[hook_mmap1_syscall] mprotect addr: "
,
this
.a0,
"size: "
,
this
.a1 ,
"prot: "
,
this
.a2);
if
(mmap_history[
this
.a0]) {
console.
log
(`\t[hook_mmap1_syscall] mmap addr: ${
this
.a0} size: ${mmap_history[
this
.a0]}`);
}
if
(is_mmap2(
this
.a0)) {
hook_mmap2(
this
.a0);
}
if
(is_mmap3(
this
.a0)) {
hook_mmap3(
this
.a0);
}
if
(is_mmap4(
this
.a0)) {
hook_mmap4(
this
.a0);
}
}
}
})
}
Interceptor.attach(base.add(0x5C84), {
onEnter: function(args) {
this
.sysno = args[7];
this
.a0 = args[0]
this
.a1 = args[1]
this
.a2 = args[2]
},
onLeave: function(retval) {
if
(
this
.sysno == 0xde) {
mmap_history[retval] =
this
.a1;
}
if
(
this
.sysno == 0xe2) {
console.
log
(
"[syscall] mprotect addr: "
,
this
.a0,
"size: "
,
this
.a1 ,
"prot: "
,
this
.a2);
if
(mmap_history[
this
.a0]) {
console.
log
(`\t[syscall] mmap addr: ${
this
.a0} size: ${mmap_history[
this
.a0]}`);
}
if
(is_mmap1(
this
.a0)) {
hook_mmap1(
this
.a0);
}
}
}
})
}
var base = Module.findBaseAddress(soName);
console.
log
(
"[hook_func_init] base: "
, base);
hook_syscall();
}
let hooked =
false
;
let mmap_history = {}
function hook_func_init(soName) {
if
(hooked)
return
;
hooked =
true
;
function hook_syscall() {
function is_mmap1 (addr) {
let byte_arr = [
0xF0, 0x7B, 0xBF, 0xA9, 0x30, 0x01, 0x00, 0xB0, 0x11, 0x86,
0x42, 0xF9, 0x10, 0x22, 0x14, 0x91, 0x20, 0x02, 0x1F, 0xD6
]
let offset = 0x440;
for
(let i = 0; i < byte_arr.length; i++) {
if
(addr.add(offset).add(i).readU8() != byte_arr[i])
return
false
;
}
return
true
;
}
function hook_mmap1(mmap_base) {
Interceptor.attach(mmap_base.add(0xF9B0), {
onEnter: function(args) {
this
.sysno = args[7];
this
.a0 = args[0]
this
.a1 = args[1]
this
.a2 = args[2]
},
onLeave: function(retval) {
if
(
this
.sysno == 0xde) {
mmap_history[retval] =
this
.a1;
}
if
(
this
.sysno == 0xe2) {
console.
log
(
"[hook_mmap1_syscall] mprotect addr: "
,
this
.a0,
"size: "
,
this
.a1 ,
"prot: "
,
this
.a2);
if
(mmap_history[
this
.a0]) {
console.
log
(`\t[hook_mmap1_syscall] mmap addr: ${
this
.a0} size: ${mmap_history[
this
.a0]}`);
}
if
(is_mmap2(
this
.a0)) {
hook_mmap2(
this
.a0);
}
if
(is_mmap3(
this
.a0)) {
hook_mmap3(
this
.a0);
}
if
(is_mmap4(
this
.a0)) {
hook_mmap4(
this
.a0);
}
}
}
})
}
Interceptor.attach(base.add(0x5C84), {
onEnter: function(args) {
this
.sysno = args[7];
this
.a0 = args[0]
this
.a1 = args[1]
this
.a2 = args[2]
},
onLeave: function(retval) {
if
(
this
.sysno == 0xde) {
mmap_history[retval] =
this
.a1;
}
if
(
this
.sysno == 0xe2) {
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课