[已發表,轉載勘誤]Android upx脫殼

已發在https://www.anquanke.com/post/id/197643
不過有部分內容發佈之後無法編輯,勘誤後如下。

Android upx脫殼

寫在前面

因爲我不是pc平臺過來的,而是直接從Android入門的,所以upx殼其實一開始並不瞭解,後來接觸到,但是可以直接動態調試或者做個內存快照,對我來說加沒加upx其實對我逆向分析影響不大。另一方面upx殼因爲開源且其實有很多脫殼的教程,所以一直覺得有些過時、保護力度不足,似乎不值得花太多時間再去深入。但是有些公司的面試官不這麼覺得,似乎對於他來說,你說會寫vmp但是不瞭解upx脫殼修復很可笑,所以趁着假期把這個坑補上。

基於快速解決問題的原則,搜了一些upx脫殼的文章,大概可以分爲兩類。
1:是基於你熟悉upx源碼的情況下,梳理出邏輯和數據結構,dump修復等。
2:是基於經驗、特徵,直接定位特徵代碼,斷點、dump修復等。

對於1是我想要的,但是大概看了下upx源碼似乎還挺大,Android加upx殼問題很多且並沒有修復,加上沒想過修復upx殼的bug(我的強迫症比較嚴重,如果看完源碼我很難能停下不去修復bug),所以最後放棄了這條路。

對於2,找到的一些文章很多是pc平臺的,Android的還是較少的,且似乎有些藏着掖着的嫌疑,當然也許是個人的主觀判斷,我也不想再搜或者去推敲不明確的地方,不如直接自己來吧,看能不能黑盒推理出來。

分析

找了個第三方app內加了upx殼的so,沒有clone upx對自己的so加殼。
首先看一下這個加了upx殼的so的數據結構,第一個可讀可執行的段包含了代碼段,乾脆就叫代碼段吧。

在這裏插入圖片描述

代碼段大小爲136728=0x21618,加載到內存是4096/一頁對齊,那麼佔用內存大小爲0x22000。而接下來的數據段在內存中偏移是0x3dee8,那麼起始頁應該是0x3d000,0x3d000-0x22000=0x1b000,數據段和代碼段隔了27頁,太不正常了,一般編譯出來的so相隔1頁,所以應該是upx改的,但是這個數據段的偏移是編譯時確定的,upx肯定不可能反彙編所有代碼修改偏移,所以這個數據段的偏移是沒有問題的,而且p_offset=0x21ee8,和p_vaddr也是相差一頁的。所以可以推理出應該是改了代碼段的大小,原來的代碼段大小肯定不是0x21618,應該是在0x3b000-0x3d000之間(考慮到一般正常so會設置一頁的間隔,那麼可以縮小範圍到0x3b000-0x3c000之間)。

爲什麼數據段的偏移是不能改的

如果對elf不是特別熟悉的話,這裏我以一個正常的so爲例來看下爲什麼說這個數據段的偏移是不能改的,
在這裏插入圖片描述
代碼中從是偏移爲3f004的地址取數據,這是經過ida優化的,實際指令含義不是這樣,我們去掉優化(當然這樣也還沒完全去掉優化,你可以自己再解析指令,0x3a8c0是存儲在指令後面的,現在是條LDR僞指令)。
在這裏插入圖片描述
可以看到R1寄存器存儲的是0x3a8c0,0x3a8c0+0x4740+4(流水線)=0x3f004。指向的是.data節(這個節在數據段)。
在這裏插入圖片描述

通過上面的例子可以發現代碼中取數據是寫死的偏移值,而這個.data中的數據實際是在so文件的0x23000偏移開始的,但是在內存中是加載到0x3f000偏移處的。

在這裏插入圖片描述
在這裏插入圖片描述

所以這個偏移值在代碼裏面寫死了,除非反彙編所有代碼,解析出所有對內存的訪問修改偏移值,基本上是不現實的,因爲有花指令、運行時確定pc等操作會導致反彙編無法正確區分彙編指令和數據(這也是寫arm vmp遇到無法完美解決的問題)。所以.data以至整個數據段都是要符合這個偏移的。

觀察內存

經過推理得出代碼段大小被修改了,結合着之前聽說的upx殼的原理,那麼應該是真實代碼段被壓縮或者加解了,之後會覆蓋內存中的代碼段。

寫一個app把so加載到內存中

c948e000-c9491000 r-xp 00000000 103:37 693882                            /data/app/com.zhuo.tong.elf_fix-rnY3xpLMrzuqsfXFyaWTRw==/lib/arm/libxx.so
c9491000-c94ca000 r-xp 00000000 00:00 0
c94ca000-c94cb000 ---p 00000000 00:00 0
c94cb000-c94cd000 r--p 00021000 103:37 693882                            /data/app/com.zhuo.tong.elf_fix-rnY3xpLMrzuqsfXFyaWTRw==/lib/arm/libxx.so
c94cd000-c94ce000 rw-p 00023000 103:37 693882                            /data/app/com.zhuo.tong.elf_fix-rnY3xpLMrzuqsfXFyaWTRw==/lib/arm/libxx.so
c94ce000-c9521000 rw-p 00000000 00:00 0                                  [anon:.bss]

0xc94ca000-0xc948e000=0x3c000,符合之前的推測:0x3b000-0x3c000之間。所以這部分纔是真實的代碼段。數據段的起始就是c94cb000+0xee8,再加上內存佔用344964,得到0xC952026C,內存對齊後得到0xC9521000,剛好對的上。所以可以把0xc948e000-0xc9521000都dump下來(修改內存權限,間隔的部分沒有讀的權限),也可以不dump c94ce000-c9521000,因爲這部分是bss節,不佔用so空間,也可以只dump代碼段0xc948e000-0xc94ca000。

dump的區間不同那麼修復起來也是有些差別的。爲了文章邏輯不亂掉,把修復放在後面。

驗證

對dump下來的文件進行驗證,拖入ida,忽略解析錯誤。

在這裏插入圖片描述
未脫殼之前的so,導出了JNI_OnLoad,記下地址,跳到dump的so的相應地址。
在這裏插入圖片描述

可以確定已經解密出真實的指令了,所以對於dump來說完全是可以脫離其他分析、調試的。

修復

dump下來的so包含指令,但是ida等反編譯工具不能自動識別修復,所以修復的事情就需要自己來做了。我dump的範圍是0xc948e000-0xc94ce000,把整個代碼段、數據段都dump下來了,這是最麻煩的一種方式,因爲有好幾個地方都要修復。

修復程序頭和節表

1、首先修復程序頭代碼段的大小,其實直接指定爲0x3c000也可以,當然可以更精確一些,
在這裏插入圖片描述

從0x3c000倒着找非0的值,例如可以是0x3bf9f或者0x3bfa0,因爲代碼段最後是.rodata,一般存放的是字符串。本來正常編譯出來的so,.rodata之後是填充的0到一頁。所以這個代碼段的大小不用很精確,只要保證正確即可,因爲.rodata不影響後面的節的修復。

注:後來動態調試確定爲0x3bf9a,後面的是兩條指令
在這裏插入圖片描述
執行後pc跳到真實的init函數。

2、因爲dump的時候包含了全0的一頁,所以代碼段和數據段的間隔爲0了。

c94ca000-c94cb000 ---p 00000000 00:00 0

那麼相應的數據段的p_offset就改成和p_vaddr一致即可,都爲0x3DEE8。

因爲DYNAMIC段和數據段在一起,同樣的也需要修復程序頭中的p_offset,就改成和p_vaddr一致即可。

在這裏插入圖片描述
修復後如上圖。

3、程序頭已經修復好了,接下來就可以修復section/節表了。已經寫好自動修復的代碼,根據程序頭、.dynamic重建plt、got、.fini_array等。
在這裏插入圖片描述

一些不在.dynamic的節通過其他節來確定,比如程序頭還有.ARM.exidx節的信息,那麼根據.plt節確定.text節的開頭,通過.ARM.exidx節確定.text節的結尾,當然這個.text節還包含.ARM.extab節,但是不影響解析和執行。最終能自動修復的節有:
.dynsym、.dynstr、.hash、.rel.dyn、.rel.plt、.text、.ARM.exidx、.rodata、.fini_array、.init_array、.dynamic、.got、.data、.bss、.data.rel.ro、.shstrtab。

以上都是可以自動修復且重要的節,一些其他不重要的比如.comment、.note.gnu.gold-ve等編譯器、gnu相關的節也可以通過字符串和其他節的區間來確定,不過沒什麼必要,因爲這些節對解析和執行無影響。

修復之後,ida已經可以不報錯的正常解析so了,
在這裏插入圖片描述
導入導出函數已經能解析。
在這裏插入圖片描述
段和節也能正確解析。

修復重定位數據

現在這個so修復後可以被ida等正常解析、反編譯,但還不能正常運行,因爲.data、.got等節中的數據被寫入絕對地址了,那麼需要對這些重定位後的數據進行還原。

當然這裏可以只dump代碼段,使用加殼的so的數據段,通過觀察可以發現upx殼並沒有修改數據段,那麼使用數據段進行替換,測試是可行的。

還可以自己寫一個linker或者直接拿系統的linker源碼修改或者修改linker可執行文件,加載這個upx殼的so不執行重定位。爲什麼可以這麼做呢,簡單看了upx殼的指令,發現mprotect等都是通過系統調用/軟中斷實現的,基於這個結果我反推一下。爲什麼不直接使用mprotect等函數呢,因爲這個upx殼像是寄生在這個宿主so一樣,要使用這些導入函數需要藉助宿主的got表、plt等,那麼就需要解析so,確定got表中mprotect函數的地址,這是理想情況,宿主導入表存在所需要的函數,但是如果宿主沒有引用導入這些函數豈不是就gg了,如果加入一項到got表,那麼後面的.bss的偏移不就被改變了,參考上面的爲什麼偏移不能改變。所以反推或者我從實現者的角度考慮應該是直接使用軟中斷,既然殼沒有使用導入函數,那麼不進行重定位、寫入函數地址到got表,並不影響殼的執行,也就是說殼還是能解密還原真實的代碼到內存中。但是如果執行到真實的init函數且使用需要重定位的函數、數據肯定是會crash的,所以時機要確定好。

爲了完整解決這裏就不採用偷懶的方式了,直接對重定位後的數據進行還原。
做法可以有兩種.
1、比如修復好了節表,那麼獲取.fini_array、.data、.init_array、.data.rel.ro、.got表的數據逐項校驗值是否在基址和內存中so結束地址,如果在這個範圍內就用值-基址,覆蓋值即可。當然這樣會漏掉got表中外部導入的函數,需要自己再解析下那些是導入的外部函數/數據,指向plt的偏移。

2、根據DT_JMPREL和DT_REL確認那些重定位的數據,修復方式是一樣的。

經過修復後確實可以,但是可能存在一個問題,就是dump時機造成的影響,因爲.data中的也存在可重定位的數據,比如指針等,那麼就是說這個數據不只可讀還可寫,dump下來的可能不是原始的被重定位後的數據而是程序自身邏輯寫入的其他數據了。

init函數的修復

經過以上的修復就可以正確運行了嗎,其實還不一定,因爲還有一個init函數沒有修復,原來導出的init函數是upx殼的入口函數:
在這裏插入圖片描述
而經過以上修復之後:
在這裏插入圖片描述
把一個函數給截斷了,肯定無法正確執行。爲什麼會這樣?也可以反推一下,殼還原代碼之後可以直接調用真實的init函數執行,不需要還原重寫init函數的地址,也沒意義。所以dump下來的dynamic還是殼的init函數的地址。

這個函數的地址的重新定位,黑盒能做到應該只能是以下兩種方式:
1、反彙編之後查找所有的無參函數,這還真是一個概率性的事情,理論上一個無參的且沒有被其他任何函數引用的就應該是了,但是取決於存不存在其它的一樣行爲的函數,或者函數多不多,當然這個可以藉助ida的api實現。所以只能說有機率能找到真正的init函數。

2、把init函數指向一個無參不影響程序邏輯的函數(如果存在)或者從dynamic去掉init函數。運行測試,如果正常運行說明可能這個init函數沒做什麼有意義的事情,可能就是爲了能加殼,聽說加upx殼必須有init函數,不過其實可以解析.dynamic確定和下一個節有沒有padding,有padding的話插入一個即可,當然沒有這麼做是太麻煩還是兼容性問題不好推測,需要數據測試。

我這個樣本去掉init函數後可以正常運行,所以算我運氣好。如果不能的話,我想應該需要動態調試一下了,既然知道了都是通過系統調用實現的函數,那麼根據系統調用號確定下行爲,再恢復真實指令後,觀察pc寄存器,第一次跳到內存中的代碼段中地址應該就是init函數的地址,減去基址即可得到偏移。

注:後來動態調試發現,upx殼居然沒有一步到位,而是在.rodata後面插了兩條指令
在這裏插入圖片描述
通過這種出棧跳到init函數,沒細看,推理應該是通過系統調用釋放分配的內存,所以並不是第一次而是第二次。這倒是個定位init的特徵,不確定有沒有變種的upx去掉了這個特徵,比如這兩條指令放在數據段和代碼段的間隔,因爲如果代碼段剛好是頁對齊的話這兩條指令不就放不下了?

總結

可見這種加殼方式似乎並不難,基本上完全黑盒就可以脫殼了。當然也可能是因爲已經對elf數據結構、linker有了一定了解,所以覺得不難,無法完全客觀的判斷。

這只是我自己的一種脫upx殼的方式,不一定是好的也不一定適合其他人。我想應該還是思路和推理更重要一些吧,其實很多問題、事情可以類似的推理出來,沒做過不代表不會做、不能做。如前面所說其實有很多脫upx的教程,但應該都是教你按步驟操作的,也是考慮到這個,所以後來整理記錄下來,儘可能把整個推理和過程呈現出來,希望能帶來些啓發吧。

除了init函數外,其他的dump、程序頭修復、節表修復都可以自動完成,整理之後上代碼。init函數的自動確定難點在於都是系統調用不好hook,根據特徵確定的話不算自動化,稍微變形的殼就識別不了了。也許可以再模擬執行的環境中試試,hook系統調用,比如當mprotect系統調用後且是操作的代碼段,那麼檢查pc的值,當是代碼段的值時記錄下來,就是init函數的地址(第二次纔是)。或者攔截mprotect系統調用,把代碼段改成不可執行,這樣執行到init函數的時候異常,捕獲異常或者分析日誌得到地址就是init函數地址(要先確定好.text和.rodata,不能設置.rodata所在的內存,不然得到的錯誤的地址)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章