ELF Format DIY For Android

ELF Format DIY For Android

Author: ThomasKing

本文只討論安卓平臺ELF格式一些可以DIY的地方。當然,有些DIY有使用價值,有些DIY僅好玩而已。爲了完整性,均在下文討論。

一、Elf32_Ehdr

1. e_ident[16]

這個字段,現ELF標準只使用了前7個字節,後9個字節是未定義的。在linux平臺,這9個字節是填0,且不能動。而安卓平臺,參看linker源碼,對so文件格式只判定前4個字節,即’7f’, ‘45’, ‘4c’, ‘46’。故可DIY如下圖:

圖 1

2. 平臺相關性標識

在Elf32_Ehdr中,於平臺性標識有:ELF文件類型e_type;CPU平臺屬性e_machine;ELF版本號e_version(ELF版本只有1.2,故該值始終爲1);文件相關屬性e_flag。這些字段都是來說明ELF文件信息,類似產品說明,對SO文件的使用無任何影響。故可DIY如圖。

圖 2

Readelf查看信息:

圖 3

3. e_entry

對於SO文件來說,這個值是無意義的。所以隨便怎麼都行。

4. 與section相關

與section相關的e_shoff,e_shentsize,e_shnum, e_shstrndx隨意DIY,http://bbs.pediy.com/showthread.php?t=192874寫得清楚,就不贅述。

5. 其餘字段

其餘字段由於加載時會被使用,故不能DIY。詳見linker源碼。

 

上述DIY無處理時機的限制,即既可對SO作預處理時,也可在代碼中。

 

二、Section

1. 移動section

         使用readelf –l查看so文件的Section toSegment mapping:

圖 4

         總的來說,除了與代碼相關受尋址影響的section外,其餘section都是可以移動的。受代碼訪問影響的section有:.plt,.ARM.extab,.ARM.exidx,.rodata,.got,.data,BSS。其餘section可以隨意移動。爲了處理方便,移動到的位置最好選在當前所處LOAD末尾。由於受到segment的屬性(RWX)影響,跨LOAD處理稍微繁瑣。以移動到LOAD末尾爲例,具體移動某section的處理流程如下:

Step1: 選定移動位置。

Step2: 根據對齊屬性,計算合適的起始位置。

Step3: 複製section數據到新位置。

Step4: 修訂section在.dynamic中的位置信息,即p_offset、p_vaddr和p_paddr。

Step5: 若移動segment,修訂對應在segmentheader中的信息。

這裏就不給出例子,下文將看到。

 

2. 增刪section

查看section信息可知,LOAD內section之間是緊湊排列的。刪除某section的數據,可不移動section。但增加section內容,就需要移動,並且修訂section時,需修訂p_filesz和p_memsz。有些section可以單獨修改,而有些section修改後,需要重新調整與之相關的section。比如往dynsym末尾添加一個符號,併爲該符號在dynstr添加一name字符串。便於查找,計算出name的hash值,然後往hash表中添加。如果還涉及到rel,還需修改rel的r_offset和r_info字段。根據上述處理,再移動修改相應的section。這裏就不給例子,下文將看到。

 

3. 修改init_array

對fini_array和init_array類似,以init_array爲例,討論init_array的一些DIY。

3.1 變更執行順序

通過__attribute__((constructor(num)))聲明某一函數(num值越小,越先執行),即指定了在.init_array中的位置,修改其順序即可實現。例如:

void __attribute__((constructor(101))) kingcoming();

void __attribute__((constructor(200)))soldiercoming();

void kingcoming(){

         __android_log_print(ANDROID_LOG_INFO,"init_array", "King is coming!");}

void soldiercoming(){

         __android_log_print(ANDROID_LOG_INFO,"init_array", "A soldier is coming!");}

執行結果:


圖 5

修改其順序,即交換圖中紅色區域中的數據:

圖 6

再執行:

圖 7

3.2 普通函數——init函數

將普通的函數,升級爲init函數。具體操作步驟:

Step1: 查找目標函數的起始位置。

Step2: 移動rel.dyn到LOAD1末尾

Step3: 移動init_array到LOAD2末尾,添加目標函數地址

Step4: 修訂rel.dyn和init_array在.dynamic中的位置。

Step5: 修訂LOAD1和LOAD2的長度信息

 

在3.1代碼中,添加:

void justHello(){

    __android_log_print(ANDROID_LOG_INFO,"JNITag","Hello!");

}

現將其修改,將justHello提升爲init函數,且最先執行。實現流程上述已討論,具體代碼就不貼了,參看附件中的updater.c。處理後的so文件,需要重建section才能查看下圖(重建工具:http://bbs.pediy.com/showthread.php?t=192874):

圖 8

處理後的SO文件執行結果:

圖 9

3.3 init函數轉普通函數

相對3.2來說,init函數轉普通函數算較簡單吧,這裏就不贅述了

 

4. GOT表 —— From HOOK to SelfPatch

針對GOT表的HOOK技術屢見不鮮,這裏還是囉嗦下一般的HOOK應用場景和流程,以便討論SO自Patch,實現類似目前基於函數Patch的第二代dex加固技術。下圖簡單描述了SO函數HOOK的基本原理。

圖 10

其中,個人認爲有一個很重要的點就是:HOOK的是Import函數,即訪問的是外部函數。那麼如果HOOK自身的函數,達到替換的函數的目的,就實現了類似DEX的函數patch效果(純屬個人YY)。

在linux平臺,PIC模式的SO文件是不區分本地符號還是外部符號,即不管是Import符號還是Export符號,都走GOT過程。採用HOOK原理,即可實現對non-static的變量和函數的替換,達到HOOK和自Patch的效果。

思路清晰,似乎就成功了。但是,隨意打開一個ARM架構的SO文件查看GOT表,發現Export函數根本不在GOT中,不過non-static變量還是在的(具體LINUX平臺和android在這方面的比較,在附件《Android ELF GOT sectionの不同之處》,此文中僅根據表象,分析了差異點。限於水平,未找到相關資料佐證,其中的原因分析純屬個人YY,難免有錯誤之處,請各位批評指正)。

一種簡單的Patch思路是,通過生成一個空殼libFuncStub.so文件,把需要Patch的函數放在其中。目的是構造一個stub在GOT中。然後SO加載起來之後,通過HOOK GOT自身函數,達到Patch效果。由於linker會將依賴的so也加載起來,libFuncStub.so不能扔掉。

作爲SO DIY,不能扔掉是不能忍的。想扔掉libFuncStub.so,即要繞過linker加載機制。一旦繞過linker,讓libFuncStub.so不加載,同時函數在執行前被patch掉,就能無縫完成這個過程。Patch利用HOOK原理實現,重點就在如何繞過linker不加載libFuncStub.so。

瞭解linker加載so大體過程的都知道,linker會將so所依賴的加載起來。具體是:通過DT_NEEDED找到對應在dynstr中的name再加載。同一個so文件不會被加載二次。另外,libdl.so一定會被加載。爲了讓linker不加載libFuncStub.so,我這裏採用修改DT_NEEDED所指向的libFuncStub.so爲libdl.so,達到fake的目的。另外還有一個問題就是函數重定向。

還是查看linker源碼,發現linker對所有的重定位都採用同樣的方法,如圖所示:

圖 11

似乎可以不用作任何處理。測試發現,加載時報出重定位錯誤,經過仔細分析linker重定位過程,發現一點:由於_elf_lookup函數尋找符號時,有此if(s->st_shndx == 0) continue;判定,返回NULL,導致relocate錯誤。st_shndx =0即SHN_UNDEF,故將此設置爲非0 即可。那麼整個流程的具體步驟就是:

預處理:

Step1: 抹去DT_NEEDED中對應的so文件

Step2: 找到對應的函數符號,修改st_shndx信息

調用函數前的Patch流程:

Step1: 找到SO起始內存

Step2: 找到符號表、字符串表、重定位表

Step3: 找到stub函數並替換

 

下面,構造一個簡單的例子來說明。爲了簡單期間,不涉及section移動,rel表組合,加密等等。Java調用naïve patchTest,調用getNameStub函數獲得字符串並打印。getNameStub定義在libFuncStub.so中。實現目標:通過patch,調用getNameStub即調用SO自身的getName函數。流程上述已說明,代碼就不貼了,見附件。patchTest函數:

void patchTest(){

    char name[20];

    int i = 0;

    if(flag) //第一次調用函數時,進行Patch

    {

        patchName("getNameStub","getName");//Patch函數

        flag= 0;

    }

    getNameStub(name);//獲取到字符串”ThomasKing”,並非”Stub!”

    __android_log_print(ANDROID_LOG_INFO,"JNITag","Show my name: %s", name);

}

運行效果很簡單,就打印一句話:

圖 12

當然,PatchName函數不僅僅只Patch,可以把基於函數解密融合在一起(基於函數加解密實現見貼:http://bbs.pediy.com/showthread.php?t=191649)。

 

這裏稍微再YY一下,針對無源碼加解密實現。(就起原因是上次做了一個很挫的SO加解密投去ALICTF熱身賽)。一種較好的大致思路是,將原SO抹去section,加密添加在殼子SO末尾。把原SO的rel表組合到殼子的rel表,殼子可以自身進行基於SECTION、函數等等加密。執行時,將原SO 匿名映射到內存,修復SO的rel表。當然,如果爲了保證JNI入口地址的一致性,再使用NDK HOOK(http://bbs.pediy.com/showthread.php?t=192047)到原SO函數。

 

這個Patch還算是完美,畢竟不依靠stub.so。不過編譯時能否不依靠呢? 即又回到如何在GOT表中構造stub的問題。上述方案是通過HOOK函數符號實現。從原理上,還可以HOOK non-static變量。可能會問,non-static變量本來就是可以訪問的,HOOK無非可以改變函數地址而已。從函數指針的間值尋址出發,又可以構造一種Patch方案,可使stub函數存在於自身SO中,在編譯時不依靠stub.so文件。具體實現流程和上述差不多,只是修改rel.dyn,限於篇幅,就不貼了吧。相信各位讀者都能做到。

 

三、Segment

由於segment已經包含了一些section,對segment的DIY只是簡單的整體移動和長度增長。前面例子已經看到,就不贅述了。

 

四、參考文獻

Linker源碼

ELF文件格式

http://bbs.pediy.com/showthread.php?t=191649

http://bbs.pediy.com/showthread.php?t=192047

http://bbs.pediy.com/showthread.php?t=192874

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