packagename:Y29tLmhlcm8uc20uYW5kcm9pZC5oZXJv
聲明:本文內容僅供學習交流之用
很久之前看過乐佬的那篇「误入虎穴,喜得虎子——记一次手游加固的脱壳与修复」,寫得實在太好,奈何當時水平有限,看得兩眼一黑。
最近重溫經典時發現兩眼不再發黑,於是打算好好復現一下,這才有了這篇文章。
將libil2cpp.so
拉入IDA,直接報了一個錯,這時大概就可以知道這個so被動了手腳。
ctrl+s
沒有發現.init_array
,改為用readelf工具,發現.init_array
位於0x5953a50
但發現0x5953a50
根本無法被解析被函數,像是被加密的樣子。
理論上來說第一個.init_array
函數是無法被加密,因此這種情況大概率是IDA分析出了錯。
將shdr table置0,這樣IDA就會根據.dynamic來解析。
再次拉入IDA,這次init_array終於被正確解析了。
直接動調會發現一直觸發SIGCHILD
信號,然後crash。
解決方案是直接忽略SIGCHILD
信號:Debugger → Debugger options → Edit exceptions
注:每個init_array函數裡都有一堆花指令( junk_codeX
),可能會造成堆棧不平衡、無法F5的情況,使用IDA9.0可以無視這種情況,低版本( 7.7 )在動調1次後同樣可以無視這情況。
.init_array
的第1個函數主要在初始化一些全局變量,以及打開/proc/self/maps
做了一些檢查。
init_some_global_var
實現如下,全是一些賦值操作:
接著看看open_maps_and_do_some_check
。
一開始是一些字符串解密操作,解密後會得到"/proc/self/maps"
。
隨後便是fopen
+ fgets
+ sscanf
來遍歷/proc/self/maps
,整體邏輯大概只是在檢查libc.so是否有執行權限。
看到/proc/self/maps
本以為是一個檢測點,但實際上應該只是一些普通的安全檢查,防止程序崩潰之類的?
接下來分析.init_array
的第2個函數。
一開始調用了do_something1
,動調後發現它會先解密一段代碼,然後調用這段代碼( 同樣是一些全局變量的賦值操作 ),之後會把這段代碼加密回去。
然後調用get_dynamic
獲取.dynamic
段,為後面prelink_image
和decrypt_str_sym
做準備。
看看get_dynamic
的實現方式。
一開始的while循環並不會走,也看不懂在干什麼 ( 不重要 )
然後是解析elf header,獲取了e_phnum
、phdr table( 段表 )的起始位置等信息。
注:在分析時最好是動調配合010來看,能更好地弄清楚每個變量的含義。
然後會遍歷phdr table,*(_DWORD *)phdr
獲取的是phdr的p_type
成員,1
代表PT_LOAD
( 可加載的段 )。
這裡是在遍歷所有Loadable Segment,記錄第1個Loadable Segment的起始位置( 通常是0 )和最後一個Loadable Segment的結束位置。
最後才是獲取.dyncmic
段的邏輯,同樣是遍歷phdr table,但這次的目標是PT_DYNAMIC(2)
,保存在res[6]
中。
獲取完.dynamic
後,會調用prelink_image
。
進入prelink_image
後,會看到一大段switch…case
語句,看過Android源碼的會發現這與/bionic/linker/linker.cpp
裡的prelink_image
十分相似,做的事情也差不多,都是利用.dynamic
的信息來初始化si
( soinfo結構,用於表示一個內存中的so )。
這個加固的soinfo
結構是魔改的,嘗試直接導入oacia大佬這篇文章的soinfo結構會發現完全對不上,需要手動調整,下圖是我手動調整soinfo結構後的結果,雖然無法完全還原,但也勉強能用,看起來也方便一點。
最後的while
循環是在處理依據庫的部份,沒有仔細看,處理邏輯大概也與Android源碼差不多,會對每個依賴庫調用prelink_image
。
init_array_func2
最後會調用decrypt_str_sym
來解密字符串表和部份的符號表。
簡單分析後可以反推出decrypt_str_sym
每個參數的含義:
然後可以選擇將解密後的數據dump下來回填到so中,或者手寫解密腳本進行解密。
這時再將解密後的so拉入IDA就能看到一些符號了,不再是一堆奇怪的字符串。
接下來分析.init_array
的第3個函數,它也調用了do_something1
,但進去沒走兩步就返回了。重點關注do_something2
。
do_something2
函數可以分成3大部份:
接下來重點看看第1部份加載子so的邏輯。
加載子so的第一步是先解密子so的一些數據,在decrypt_phdr_loadable_seg
中分別調用了2次解密函數解密兩段不同的密文。
第1次調用解密函數解密出來的信息包含子so的phdr table、符號表、重定向表、dynamic表。而第2次調用解密函數解密出來的數據不太確定有什麼用,可能會跟後面提到的loadable_data1
有關。下面來具體看看這整個過程。
進入decrypt_phdr_loadable_seg
後,忽略一些不重要的部份,之後第一個遇到的函數是decrypt_something2
,這就是上面提到的解密函數。
進入decrypt_something2
可以看到明顯的RC4的特徵,而且是魔改過的RC4。具體的算法細節我沒有細看,我關注的是解密後的結果,因此選擇直接將解密後的數據dump下來。
args[0]
是密文、args[2]
用來存放解密後的明文、args[3]
是args[2]
的長度,將dump下來的文件名記為dec_data1
。
注:IDA Python dump memory script
dump出來的dec_data1
如下:
一開始無法直接知道dec_data1
每部份的含義,我是由後續的分析反推出dec_data1
分成4個部份的。
調用decrypt_phdr_loadable_seg
解密完子so所需的數據後,會調用load_realso_PTLOAD
來加載子so的所有loadable段。
一開始會先保存殼so的phdr,然後獲取殼so的大小。
繼續向下看,看到56
這個數字可以大概猜到是在遍歷phdr table( 子so的phdr table ),而1
代表PT_LOAD
,因此這裡是在遍歷所有loadable的段,並計算段的映射地址和大小。
子so的phdr如下,複製前3行然後在dec_data1
裡搜,會發現與dec_data1
的前3行一樣,由此可知dec_data1
的第1部份是子so的phdr信息。
然後會調用mmap
將fd
的內容映射到子so的loadable段的內存起始地址,基址是殼so的起始地址,fd
是殼so的句柄。即最終是在殼so的基礎上進行修正,將殼so的某些位置修正為子so的代碼段和數據段。
然後調用mprotect
設置段的權限,調用memset
將段置為0xBB
。
之後調用的memcpy
才會真正將解密後子so的loadable段數據複製到對應的位置,然後會將多餘的內存空間置0
。
子so第1個loadable段如下,dump下來,記為loadable_data1
。
( loadable_data1
的頭4個字節7F B2 B3 0F
其實是代表子so的魔數,前0x40
字節是elf_header區域,接著的0x188
個字節是殼的phdr table區域,這兩部份數據沒有用,直接刪掉,後面提到的loadabe_data1
均不包含這兩部份 )
子so第2個loadable段如下,記為loadable_data2
。
子so總共只有這兩個loadable段,一個是代碼段、一個是數據段,可從子so的phdr table來區分哪個是代碼段或數據段。
注:殼so的第1個loadable段的p_memsz
特意設置得很大,就是為了讓子so的loadable_data2
裝載於此。
最後會再調用一次mprotect
將段設置回原本的權限。
調用完load_realso_PTLOAD
裝載子so的loadable段後,會進行兩次的prelink_image。
第一次prelink_image用的.dynamic數據是殼so的,這是因為子so的一些基礎信息是依賴於殼so的。具體獲取.dynamic和prelink_image
的過程在上面已經分析了,就不再重複。
第二次調用prelink_image
用的.dynamic數據才是子so的。
複製第一行然後在dec_data1
裡搜,發現dec_data1
最後一部份就是子so的.dynamic。
子so完成兩次prelink_image後,會調用maybe_init_something
和decrypt_something3
,調了好幾遍都沒搞清楚這2個函數具體在干什麼,猜測大概與後面的do_relocate
有關。
接著調用do_relocate
進行重定向。
do_relocate
會根據rela
和plt_rela
的信息調用relocate
進行重定向,本例沒有plt_rela
,只需關注rela
即可。
在調用relocate
前先將rela dump下來,為後續修復so做準備。
注:dump下來的rela同樣可以在dec_data1
中搜到,起始位置為0x1700
。
簡單分析relocate
後會發現,它與Android源碼的relocate
其實大同小異。
簡單解釋下so重定向的原理。
重定向表每個元素都是如下結構,r_info
的高4位代表sym
( 符號表索引 )、低4位代表type( 重定向類型 )。
個人總結出重定向大致可以分為2種情況:
下面藍框是一組真實數據:
最終重定向過程表現為:
下面藍框是一組真實數據:
最終重定向過程表現為:
relocate
中查找符號地址的過程如下:
symtab_
如下,同樣可以在dec_data1
中找到,這代表子so有自己的符號表( 這樣說是因為字符串表用的是殼so的 )。
至此可以總結出dec_data1
的分佈如下:
通過上述分析可以知道,子so的phdr table、符號表、重定向表、dynamic等信息只有在使用時才會從其他地方讀取來使用,因此整體dump so變得沒有太大意義。
而子so與殼so會共用一些基礎信息,因此不能單單重建子so,而是要在殼so的基礎上修正成子so。
注:使用解密字符串表、符號表後的殼so作為外殼,該外殼記為libil2cpp_str_sym.so
。
以下提取的數據也是從libil2cpp_str_sym.so
中提取。
提取libil2cpp_str_sym.so
的符號表,記為orig_sym
。
[注意]APP应用上架合规检测服务,协助应用顺利上架!