簡單粗暴的對android so文件加殼,防止靜態分析


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

以前一直對.so文件加載時解密不懂,不了解其工作原理和實現思路。最近翻看各種資料,有了一些思路。看到論壇沒有類似帖子,故來一帖,也作為學習筆記。限於水平,本菜沒有找到安卓平台一些具體實現思路,這些方法都是借鑒其他平台的實現思路和本菜的YY,肯定會有不少疏漏和錯誤之處,還請各位大牛指正,感激不盡!

簡單粗暴的so加解密實現
一、  概述
利用動態鏈接庫實現安卓應用的核心部分,能一定程度的對抗逆向。由於ida等神器的存在,還需要對核心部分進行加密。動態鏈接庫的加密,在我看來,有兩種實現方式:1. 有源碼; 2、無源碼。無源碼的加密,類似window平台的加殼和對.dex文件的加殼,需要對文件進行分析,在合適的地方插入解密代碼,並修正一些參數。而如果有源碼,則可以構造解密代碼,並讓解密過程在.so被加載時完成。(當然,應用程序加載了.so文件后,內存中.so數據已經被解密,可直接dump分析。同時,也有一些對抗dump的方法,這里就不展開了)。
下文只針對有源碼這種方式進行討論,分析一些可行的實現方法。主要是包含對ELF header的分析(不是討論各個字段含義); 基於特定section和特定函數的加解密實現(不討論復雜的加密算法)。

二、  針對動態鏈接庫的ELF頭分析
網上有很多資料介紹ELF文件格式,而且寫得很好很詳細。我這里就不重復,不太了解的朋友,建議先看看。以下內容,我主要從鏈接視圖和裝載視圖來分析ELF頭的各個字段,希望能為讀者提供一些ELF文件頭的修正思路。
這里,我再羅嗦列出ELF頭的各個字段:
typedef struct {
  unsigned char  e_ident[EI_NIDENT];  /* File identification. */
  Elf32_Half  e_type;    /* File type. */
  Elf32_Half  e_machine;  /* Machine architecture. */
  Elf32_Word  e_version;  /* ELF format version. */
  Elf32_Addr  e_entry;  /* Entry point. */
  Elf32_Off  e_phoff;  /* Program header file offset. */
  Elf32_Off  e_shoff;  /* Section header file offset. */
  Elf32_Word  e_flags;  /* Architecture-specific flags. */
  Elf32_Half  e_ehsize;  /* Size of ELF header in bytes. */
  Elf32_Half  e_phentsize;  /* Size of program header entry. */
  Elf32_Half  e_phnum;  /* Number of program header entries. */
  Elf32_Half  e_shentsize;  /* Size of section header entry. */
  Elf32_Half  e_shnum;  /* Number of section header entries. */
  Elf32_Half  e_shstrndx;  /* Section name strings section. */
} Elf32_Ehdr;
e_ident、e_type、e_machine、e_version、e_flags和e_ehsize字段比較固定;e_entry 入口地址與文件類型有關。e_phoff、e_phentsize和e_phnum與裝載視圖有關;e_shoff、e_shentsize、e_shnum和e_shstrndx與鏈接視圖有關。目前e_ehsize = 52字節,e_shentsize = 40字節,e_phentsize = 32字節。
下面看看這兩種視圖的排列結構:
名稱:  1.png
查看次數: 10
文件大小:  20.3 KB
直接從圖中,可以得到一些信息:Program header位於ELF header后面,Section Header位於ELF文件的尾部。那可以推出:
e_phoff = sizeof(e_ehsize);
整個ELF文件大小 = e_shoff + e_shnum * sizeof(e_shentsize) + 1
e_shstrndx字段的值跟strip有關。Strip之前:.shstrtab 並不是最后一個section.則 e_shstrndx = e_shnum – 1 – 2;
名稱:  2.png
查看次數: 5
文件大小:  6.9 KB
而經過strip之后,動態鏈接庫末尾的.symtab和.strtab這兩個section會被去掉. 則e_shstrndx = e_shnum – 1。
名稱:  3.png
查看次數: 2
文件大小:  5.4 KB
使用ndk生成在\libs\ armeabi\下的.so文件是經過strip的,也是被打包到apk中的。可以在\obj\local\armeabi\下找到未經過strip的.so文件。到這里,我們就可以把http://bbs.pediy.com/showthread.php?t=188793 帖子中提到的.so文件修正。如果e_shoff和e_shnum都改成任意值,那么修正起來比較麻煩。
感覺上好像e_shoff、e_shnum等與section相關的信息任意修改,對.so文件的使用毫無影響。的確是這樣的,至少給出兩個方面來佐證:
1.  so文件在內存中的映射
點擊圖片以查看大圖

圖片名稱:	4.png
查看次數:	201
文件大小:	85.8 KB
文件 ID :	91653
相信了解elf裝載(執行)視圖的朋友肯定清楚,.so文件是以segment為單位映射到內存的。圖中紅色區域的section是沒有被映射的內存,當然也在segment中找不到。
2.  安卓linker源碼
在linker.h源碼中有一個重要的結構體soinfo,下面列出一些字段:
struct soinfo{
    const char name[SOINFO_NAME_LEN]; //so全名
    Elf32_Phdr *phdr; //Program header的地址
int phnum; //segment 數量
unsigned *dynamic; //指向.dynamic,在section和segment中相同的
//以下4個成員與.hash表有關
unsigned nbucket;
unsigned nchain;
unsigned *bucket;
unsigned *chain;

unsigned *preinit_array;
unsigned preinit_array_count;
//這兩個成員只能會出現在可執行文件中

  //指向初始化代碼,先於main函數之行,即在加載時被linker所調用,在linker.c可以看到:__linker_init -> link_image -> call_constructors -> call_array
unsigned *init_array;
unsigned init_array_count;
void (*init_func)(void);
//與init_array類似,只是在main結束之后執行
unsigned *fini_array;
unsigned fini_array_count;
void (*fini_func)(void);
}
另外,linker.c中也有許多地方可以佐證。其本質還是linker是基於裝載視圖解析的so文件的。
基於上面的結論,再來分析下ELF頭的字段。
1) e_ident[EI_NIDENT] 字段包含魔數、字節序、字長和版本,后面填充0。對於安卓的linker,通過verify_elf_object函數檢驗魔數,判定是否為.so文件。那么,我們可以向位置寫入數據,至少可以向后面的0填充位置寫入數據。遺憾的是,我在fedora 14下測試,是不能向0填充位置寫數據,鏈接器報非0填充錯誤。
2) 對於安卓的linker,對e_type、e_machine、e_version和e_flags字段並不關心,是可以修改成其他數據的(僅分析,沒有實測)
3) 對於動態鏈接庫,e_entry 入口地址是無意義的,因為程序被加載時,設定的跳轉地址是動態連接器的地址,這個字段是可以被作為數據填充的。
4) so裝載時,與鏈接視圖沒有關系,即e_shoff、e_shentsize、e_shnum和e_shstrndx這些字段是可以任意修改的。被修改之后,使用readelf和ida等工具打開,會報各種錯誤,相信讀者已經見識過了。
5) 既然so裝載與裝載視圖緊密相關,自然e_phoff、e_phentsize和e_phnum這些字段是不能動的。

根據上述結論,做一個面目全非,各種工具打開報錯的so文件就很easy了,讀者可以試試,這里就不舉例,你將在后續內容中看到。

三、  基於特定section的加解密實現
這里提到基於section的加解密,是指將so文件的特定section進行加密,so文件被加載時解密。下面給出實例。
假設有一個shelldemo應用,調用一個native方法返回一個字符串供UI顯示。在native方法中,又調用getString方法返回一個字符串供native方法返回。我需要將getString方法加密。這里,將getString方法存放在.mytext中(指定__attribute__((section (".mytext")));),即是需要對.mytext進行加密。
加密流程:
1)  從so文件頭讀取section偏移shoff、shnum和shstrtab
2)  讀取shstrtab中的字符串,存放在str空間中
3)  從shoff位置開始讀取section header, 存放在shdr
4)  通過shdr -> sh_name 在str字符串中索引,與.mytext進行字符串比較,如果不匹配,繼續讀取
5)  通過shdr -> sh_offset 和 shdr -> sh_size字段,將.mytext內容讀取並保存在content中。
6)  為了便於理解,不使用復雜的加密算法。這里,只將content的所有內容取反,即 *content = ~(*content);
7)  將content內容寫回so文件中
8)  為了驗證第二節中關於section 字段可以任意修改的結論,這里,將shdr -> addr 寫入ELF頭e_shoff,將shdr -> sh_size 和 addr 所在內存塊寫入e_entry中,即ehdr.e_entry = (length << 16) + nsize。當然,這樣同時也簡化了解密流程,還有一個好處是:如果將so文件頭修正放回去,程序是不能運行的。

解密時,需要保證解密函數在so加載時被調用,那函數聲明為:init_getString __attribute__((constructor))。(也可以使用c++構造器實現, 其本質也是用attribute實現)
解密流程:
1)  動態鏈接器通過call_array調用init_getString
2)  Init_getString首先調用getLibAddr方法,得到so文件在內存中的起始地址
3)  讀取前52字節,即ELF頭。通過e_shoff獲得.mytext內存加載地址,ehdr.e_entry獲取.mytext大小和所在內存塊
4)  修改.mytext所在內存塊的讀寫權限
5)  將[e_shoff, e_shoff + size]內存區域數據解密,即取反操作:*content = ~(*content);
6)  修改回內存區域的讀寫權限
(這里是對代碼段的數據進行解密,需要寫權限。如果對數據段的數據解密,是不需要更改權限直接操作的)

利用readelf查看加密后的so文件:
名稱:  5.png
查看次數: 9
文件大小:  21.2 KB
運行結果很簡單,源碼見附件
名稱:  6.png
查看次數: 3
文件大小:  4.2 KB
注意:並不是所有的section都能全加,有些數據是不能加密的。比如直接對.text直接加密,會把與crt有關代碼也加密,只能選擇性的加密。下面將介紹如何實現

四、  基於特定函數的加解密實現
上面的加解密方式可謂簡單粗暴。采用這種方式實現,如果ELF頭section被恢復,則很容易被發現so多了一個section。那么,對ELF中已存在的section中的數據部分加密,可以達到一定的隱藏效果。
與上節例子類似,命名為shelldemo2,只是native直接返回字符串給UI。需要做的是對Java_com_example_shelldemo2_MainActivity_getString函數進行加密。加密和解密都是基於裝載視圖實現。需要注意的是,被加密函數如果用static聲明,那么函數是不會出現在.dynsym中,是無法在裝載視圖中通過函數名找到進行解密的。當然,也可以采用取巧方式,類似上節,把地址和長度信息寫入so頭中實現。Java_com_example_shelldemo2_MainActivity_getString需要被調用,那么一定是能在.dynsym找到的。
加密流程:
1)  讀取文件頭,獲取e_phoff、e_phentsize和e_phnum信息
2)  通過Elf32_Phdr中的p_type字段,找到DYNAMIC。從下圖可以看出,其實DYNAMIC就是.dynamic section。從p_offset和p_filesz字段得到文件中的起始位置和長度
名稱:  7.png
查看次數: 6
文件大小:  5.6 KB
3)  遍歷.dynamic,找到.dynsym、.dynstr、.hash section文件中的偏移和.dynstr的大小。在我的測試環境下,fedora 14和windows7 Cygwin x64中elf.h定義.hash的d_tag標示是:DT_GNU_HASH;而安卓源碼中的是:DT_HASH。
4)  根據函數名稱,計算hash值
5)  根據hash值,找到下標hash % nbuckets的bucket;根據bucket中的值,讀取.dynsym中的對應索引的Elf32_Sym符號;從符號的st_name所以找到在.dynstr中對應的字符串與函數名進行比較。若不等,則根據chain[hash % nbuckets]找下一個Elf32_Sym符號,直到找到或者chain終止為止。這里敘述得有些復雜,直接上代碼。
for(i = bucket[funHash % nbucket]; i != 0; i = chain[i]){
  if(strcmp(dynstr + (funSym + i)->st_name, funcName) == 0){
    flag = 0;
    break;
  }
}
6)  找到函數對應的Elf32_Sym符號后,即可根據st_value和st_size字段找到函數的位置和大小
7)  后面的步驟就和上節相同了,這里就不贅述

解密流程為加密逆過程,大體相同,只有一些細微的區別,具體如下:
1)  找到so文件在內存中的起始地址
2)  也是通過so文件頭找到Phdr;從Phdr找到PT_DYNAMIC后,需取p_vaddr和p_filesz字段,並非p_offset,這里需要注意。
3)  后續操作就加密類似,就不贅述。對內存區域數據的解密,也需要注意讀寫權限問題。

加密后效果:
點擊圖片以查看大圖

圖片名稱:	8.png
查看次數:	28
文件大小:	5.6 KB
文件 ID :	91657
運行結果與上節相同,就不貼了。

五、  參考資料
http://blog.csdn.net/forlong401/article/details/12060605
《ELF文件格式》
Android linker源碼:bionic\linker
Android libc源碼:bionic\libc\bionic
谷歌:ELF鏈接視圖與裝載視圖相關資料
------------------------------------------------------------------------
基於上面的方法,我寫了一個CrackMe.apk的注冊機程序供大家玩耍。輸入3~10位的username和regcodes,8位的校驗碼,字符范圍:A~Z、a~z、0~9。若校驗通過,則提示:congratulation! You crack it!. 
附件:
shelldemo.zip.
shelldemo2.zip.
CrackMe.apk.

上個pdf,這個太亂了。
簡單粗暴的so加解密實現.pdf*轉載請注明來自看雪論壇@PEdiy.com

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM