首页
社区
课程
招聘
[原创] 某Guard的SO加固探秘(有趣的GNUHash)
发表于: 2025-6-19 22:14 6424

[原创] 某Guard的SO加固探秘(有趣的GNUHash)

2025-6-19 22:14
6424

最近瑣事一堆,而且也有點懶惰,分析周期拉得挺長的。

動調分析了很多次,每次都有新發現,這也使得文章中很多前面的部份是後面補充上去的。

注:本文只分析該加固的具體流程,以及修復的思路。

只處理anti ida debug的部份,能順利動調就足夠了。

通過hook strtstr來bypass,然後以frida -U -f XXX -l test.js --pause的方式啟動APP,之後IDA再attach。

然後就可以愉快地動調分析了^^,主要邏輯都在.init_array中。

最開始會調用check_emu_and_get_lib_info()檢查模擬器,並且獲取一些lib信息( 包括libc.soliblog.solibstd++.solibOpenSLES.solibmediandk.so ),這些信息被保存在g_somedataX中,如libc.so的信息就在g_somedata1

進入check_emu_and_get_libc_info()函數,看看具體實現。

首先調用了get_infos(),其中會通過openat系統調用打開/proc/self/maps,返回的fd保存在infos中。同時也把readclose等系統調用保存到infos中。

然後調用get_maps_item()遍歷/proc/self/maps

maps_item是諸如12c00000-12c40000 rw-p 00000000 00:00 0 XXX這樣的字符串。

遍歷/proc/self/maps的目的是為了獲取指定幾個lib的信息,下面以libOpenSLES.so為例看看它是如何處理的。

通過以下方式,將hex字串的基址轉換為num形式,記為libOpenSLES_base

轉換完基址後,進行了一些合法性檢查。

然後獲取了該so的e_machine,若是623,就代表是EM_X86_64EM_386,一般模擬器就是這兩個架構之一。

檢測到後會記錄在g_mb_emu_flag

至於上述轉換的基址最終會被保存在g_somedataX全局變量中,如libc.so的就保存在*(_QWORD *)(g_somedata1 + 88)

回到init_array_func1()繼續分析。

下面是對libc.so進行了類似prelink_image()的操作,即遍歷了libc.so.dynamic,相關數據被保存在g_somedata1中。

除了對libc.so外,還有對libstdc++.soliblog.so進行了上述操作。

之後解密了一些諸如dlopendlsymdlerrordlclose等字串。

遍歷libc.so的jmprel表( 重定向表 ),記錄所有dl系列的函數地址,如dlopen函數地址被保存在g_dlopen中。

獲取完dl系列的函數後,之後又是一堆的內聯形式的字符串解密,解密了一堆函數名存放在各個變量中。

然後初始化了一個SBOX,大概是用於之後某處的加/解密,應該不用太關注。

再之後調用了mprotect系統調用,賦予libil2cpp.so0x1000可讀可寫的權限。

繼續向下看,上面解密的部份字符串如下所示,可以看到基本上都是一些函數名,分佈在不同lib中。

接下來以_Znwm為例繼續分析後續的流程。

_Znwm應該是屬於libstdc++.so的函數,因此當遍歷到_Znwm時會跳到如下地方,然後從g_somedata3 + 88獲取libstdc++.so的基址。

繼續向下單步執行,會走到下圖這裡,看到5381這個關鍵值。

如果有看過AOSP的SymbolName::gnu_hash(),會發現這個正是其中GNU HASH的初始值,後續的循環邏輯也與源碼中一致。

之後的計算過程也與soinfo::gnu_lookup()中大同小異,該函數作用於linker的relocate(),它通過特殊的GNU HASH邏輯,能快速計算出指定符號名對應的符號索引,之後linker就能通過符號索引取得對應符號的地址。

下圖的邏輯基本上就是對soinfo::gnu_lookup()的模擬,符號索引記為n

base_libstdc++.so的基址,symbolsymtab[n]( 這個symtab是libstdc++.so的符號表 ),因此下圖執行後,base_就是_Znwm的真實地址。

取得_Znwm的真實地址後,會其賦給libil2cpp.so的某處。

這個「某處」在.got表,原本是tan函數,由此可知.got中的一大堆tan函數應該是作為占位函數的存在。

比較特別的是,一些函數如memcpymemset等,它雖然同樣會按上述GNU HASH的方式取得其真實地址,但在替換時卻不會使用,而是將替換為自實現的memcpymemset,一定程度上增加了安全性。

最後遍歷完所有需要替換的函數時,會再次調用mprotect系統調用收回寫權限。

注:_Znwm其實是operator new

tan_new()是指原本是tan()函數,但在init_array_func1()中被替換為_Znwm()函數( new )。

g_from_initarray2全局變量中會保存一些殼so的信息,以及後續解密會用到的參數等。

init_function()中把一堆函數賦給了result,最終result被保存在全局變量g_func_array中。

一開始無法直接看出unknow_func()的作用,大概只能看出其中解密了一句有意思的字串nichoushazaichouxiashishi

而在後續通過對init_array_func3()的分析,可以知道unknow_func()干了以下事情:

如殼so的符號表被保存在g_from_initarray2 + 96

init_array_func3()分成了3部份,前2部份是主要邏輯,最後1個函數大概只是在收尾。

init_something1()如下,把一些函數、g_func_array等賦給了a1,而a1等下又會作為第0個參數被傳入main_func()

main_func()一開始會間接調用0x2AD8380( 記為decrypt1 ),其中解密了子so的strtab、rela、.dynamic、代碼段等信息。

接下來先分析decrypt1()的具體實現。開始是一大堆加/解密table的初始化。

然後是第1處的字符串解密邏輯,解密的起始位置是0x458

解密前/後如下所示。

然後是第2處字符串解密邏輯,這處的邏輯會被多次調用。

第3處字符串解密邏輯。

第4處字符串解密邏輯。

第5處字符串解密邏輯。

大概共有5處字符串解密邏輯,所有均為內聯的形式( 不像傳統加固那樣具有一個統一個字符串解密函數 )。

之後會在下圖JUMPOUT處解密子so的重定向表、.dynamic信息、代碼段等信息。

JUMPOUT裡的第1處解密邏輯如下,這裡解密的是子so的部份重定向信息。

接著是第2處解密邏輯,這裡不單只會解密子so的重定向表,還會解密子so的.dynamic信息、代碼段等信息。

回到main_func()

之後會根據decrypt1()中解密的.dynamic信息進行預鏈接,相關數據被保存在soinfo變量中。

注:雖然解密的子so.dynamic信息中包含符號表,但實際上這裡的預鏈接並沒有存儲子so的符號表信息,後續「解密子so符號表」中解密&使用的符號表都是殼so的( 從g_from_initarray2 + 96中獲取 )。

d_tag == 1( DT_NEEDED )的情況會在之後單獨處理,這裡先將子so的所有DT_NEEDED庫名保存在m_addr( 由malloc而來的一片內存空間 )中。

m_addr可以理解成一個數組,每個元素的大小為0xAC,第1個屬性是庫名,之後就是一些預鏈接信息。

而後會調用prelink_DT_NEEDED(),該函數大概是對子so的依賴庫進行prelink_image()操作。

執行prelink_DT_NEEDED()前,m_addr如下,只有庫名。

執行後,m_addr多了對應庫的預鏈接的信息,如第1個紅框是app_process符號表的地址,第2個紅框是字符串表。

接著就是子so的重定向工作。

下面是第1處重定向邏輯,用的重定向表是上面預鏈接時的DT_JMPREL(23)

sym0時,重定向過程如下。其中soinfo[3]basemy_rela是自定義的重定向表中的一項元素。

my_rela是類似如下的三元組,大致可以對應常規的<r_offset, r_info, r_addend>,不同的是r_info中沒有type信息( 如0x4030x402等重定向類型 )。

sym不為0時,會先從strtab獲取sym對應的字符串,假設是"free",然後調用get_target_addr()嘗試尋找函數地址,有以下幾種情況:

get_target_addr()成功返回對應函數地址,則直接在下面這裡進行重定向。

get_target_addr()返回0,則會遍歷保存在m_addr的子so依據庫,然後進行GNU HASH看看目標符號是否在指定so中。

通過GNU HASH成功找到符號偏移,加上基址就是目標函數的真實地址。

最終根據my_rela將該函數地址賦給對應地方,完成重定向( 類似0x401重定向 )。

然後是調用relocate()進行第2處重定向邏輯,用的重定向表是上面預鏈接時的DT_RELA(7),記這個重定向表為rela

relocate()的實現就跟linker的實現比較一致了,可以看到熟悉的0x4010x101等重定向類型了。

通過以下腳本檢查rela重定向表的類型,會發現只有0x403重定向。

可以選擇在這個時機dump一些子so的解密數據,如子so的字符串表、.dynamic信息和重定向表。

relocate()後,會清空子so字符串表,起始偏移是0x45C,循環裡清空了0xA20字節,之後又單獨清空了8字節,共0xA28字節,由這裡可以看出字符串表的真實範圍是由0x45C0xE84

然後清空子so的.dynamic信息,範圍由0x26FFB280x26FFCB8

然後清空rela,範圍由0xE840x552C74

最後清空my_rela( 大概是自定義的重定向表 ),範圍由0x552C740x55FF04

完成子so的預鏈接和重定向後,會解密子so的符號表( 以及解密對應的符號名 )。

而上面提到,在對子so進行預鏈接時並不包括符號表的部份,因此這裡解密的其實是殼so的符號表,以及殼so的字符串表。

so文件每個符號表項的結構定義如下:

根據st_other是否0x10來判斷當前符號( Elf64_Sym )是否需要解密。

Elf64_Sym中有兩個東西需要解密:


[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!

收藏
免费 23
支持
分享
最新回复 (20)
雪    币: 6600
活跃值: (5635)
能力值: ( LV12,RANK:280 )
在线值:
发帖
回帖
粉丝
2
樣本: Y29tLmNhaHguZ3cx
2025-6-19 22:17
2
雪    币: 2307
活跃值: (3950)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
大佬真强啊~
2025-6-19 22:35
0
雪    币: 6600
活跃值: (5635)
能力值: ( LV12,RANK:280 )
在线值:
发帖
回帖
粉丝
4
hacker521 大佬真强啊~
謝謝
2025-6-19 22:45
1
雪    币: 7
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
5
感谢分享,yyds
2025-6-20 08:18
0
雪    币: 132
活跃值: (6362)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
6
FxxxxGuard里的风控采集上传也很有意思
2025-6-20 13:41
0
雪    币: 6600
活跃值: (5635)
能力值: ( LV12,RANK:280 )
在线值:
发帖
回帖
粉丝
7
New对象处 FxxxxGuard里的风控采集上传也很有意思
原來它有風控, 難怪我的pixelXL進不了遊戲
2025-6-20 15:43
0
雪    币: 1907
活跃值: (1514)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
8
太强了佬
2025-6-21 20:03
0
雪    币: 73
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
9
大佬厉害了,小白求教一下,有没有简单办法过so注入检测?
2025-6-22 20:39
0
雪    币: 200
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
10
frida会被闪退叭
2025-6-24 20:13
0
雪    币: 6600
活跃值: (5635)
能力值: ( LV12,RANK:280 )
在线值:
发帖
回帖
粉丝
11
mb_snsvwpzb frida会被闪退叭
會, 但能動調libil2cpp.so就夠了
2025-6-24 22:22
0
雪    币: 5436
活跃值: (2385)
能力值: ( LV12,RANK:230 )
在线值:
发帖
回帖
粉丝
12
学习学习
2025-7-3 11:50
0
雪    币: 200
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
13
ngiokweng 會, 但能動調libil2cpp.so就夠了
能试试过frida检测不,我被闪退麻了,hook strstr也解决不了
2025-7-29 11:53
0
雪    币: 734
活跃值: (811)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
14
大佬的ida动调实在太厉害了 
2025-8-10 22:36
0
雪    币: 6600
活跃值: (5635)
能力值: ( LV12,RANK:280 )
在线值:
发帖
回帖
粉丝
15
G33k3r 大佬的ida动调实在太厉害了
只會單步調試而已
2025-8-11 09:34
0
雪    币: 200
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
16
有个问题。frida --pause启动,直接挂起了游戏进程并没有生成吧,ida这个时候也附加不了。resume之后又会立刻被frida闪退,咋附加动调
2025-10-21 11:19
0
雪    币: 6600
活跃值: (5635)
能力值: ( LV12,RANK:280 )
在线值:
发帖
回帖
粉丝
17
chiikawa_gui 有个问题。frida --pause启动,直接挂起了游戏进程并没有生成吧,ida这个时候也附加不了。resume之后又会立刻被frida闪退,咋附加动调
為何不先試試呢
2025-10-21 11:57
0
雪    币: 20
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
18
chiikawa_gui 有个问题。frida --pause启动,直接挂起了游戏进程并没有生成吧,ida这个时候也附加不了。resume之后又会立刻被frida闪退,咋附加动调
我问过了,楼主用的安卓10,安卓10以上用了冷启动流程,没办法用这样的办法调试,不过你可以试试用am的debug模式启动,然后再用frida和ida attach,不过这个厂家还有很多别的程序也是用的一样的保护,可以找一找没有反调试的,直接用am debug启动然后attach上去也能调
2025-11-23 15:06
0
雪    币: 20
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
19
chiikawa_gui 有个问题。frida --pause启动,直接挂起了游戏进程并没有生成吧,ida这个时候也附加不了。resume之后又会立刻被frida闪退,咋附加动调
我试过你这样的启动方法,折腾了很久很久,发现确实无法这样调试
我这是pixel 6  android13 , frida 16.7,用pause启动过一会会直接terminated,因为附加到了系统初始化应用上
2025-11-23 15:08
0
雪    币: 20
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
20
chiikawa_gui 有个问题。frida --pause启动,直接挂起了游戏进程并没有生成吧,ida这个时候也附加不了。resume之后又会立刻被frida闪退,咋附加动调
am启动卡住的时机也非常早,可以试试用am
2025-11-23 15:09
0
雪    币: 200
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
21
Neb1a 我问过了,楼主用的安卓10,安卓10以上用了冷启动流程,没办法用这样的办法调试,不过你可以试试用am的debug模式启动,然后再用frida和ida attach,不过这个厂家还有很多别的程序也是用的 ...
噢噢,这样啊
23小时前
0
游客
登录 | 注册 方可回帖
返回