標籤(空格分隔): APK逆向與保護
1. 前言
APK的加固技術研究已有很多年了,有很多成熟的廠商提供相關的服務,加固的目的一方面是爲了保護應用不被惡意反編譯和篡改,得到應用的源代碼。另一方面防止應用中爲發現的漏洞被攻擊者發現利用。早期的加固技術主要是基於Dex文件實現的,通過將源程序的dex文件加密,在運行的過程中由解殼程序動態加載、解密和運行。但是由於基於dex的加固技術,很容易被破解,在內存中dump出dex文件,因此衍生了基於.so的加固技術,雖然基於.so的加固技術的安全性有所提高,但是道高一尺,魔高一丈,基於各種斷點調試的人肉脫殼也是很容易恢復出dex文件,據說目前廠商又在研究基於vmp的加固技術,通過自定義指令和解釋器等各種技術實現更高級保護等。作爲一名“入坑”不久的菜鳥,在這裏不談那麼多,先簡單學習下基於so加固的基本原理,安全、逆向任重而道遠啦~~~
2. so加固的基本思路
前段時間,折騰了下ELF文件,對ELF文件有了基本的認識,雖然有些地方理解的還是比較模糊,比如ELF文件在動態加載、鏈接的細節方面,看了《深入理解計算機系統》之後,理解的還是不到位,期待神作《程序員的自我修養-鏈接、加載和庫》到貨,認真研讀,以求理解的更加透徹。
古人學問無遺力,少壯工夫老始成。
紙上得來終覺淺,絕知此事要躬行。
————陸游
看書歸看書,還是得徹徹底底實踐一遍
首先得感謝這些博主的無私分享
[http://bbs.pediy.com/thread-191649.htm][1]
[http://blog.csdn.net/jiangwei0910410003/article/details/49966719][2]
[http://zke1ev3n.me/2015/12/27/Android-So%E7%AE%80%E5%8D%95%E5%8A%A0%E5%9B%BA/][3]
在這裏,菜鳥我也是照着你們的神作,依葫蘆畫瓢,把整個過程梳理一遍,順便寫寫自己的感受。
學習逆向的過程是艱辛的,But, 人皆向死而生,又有何所懼?
好了,廢話少說,言歸正傳
先來看實現so加固的兩種基本思路:
2.1 通過將核心函數實現在自定義section中,並進行加密
基本的思路是自定義一個section,然後將核心函數的實現放在自定義的section中,並且對其進行加密。然後我們再來看ELF文件的格式:
我們可以看到其中有兩個section,分別是.init_array和.fini_array,前面一個在動態鏈接庫加載到進程映像後執行一些初始化操作,後面的終止的時候執行。
也就是.init_array節中的代碼先與程序的Main函數開始之前執行,因此我們只要將解密函數定義成.init_array屬性的節中,就可以在main函數開始之前解密我們先已加密的section,正常調用其中的函數。
2.2.1 加密過程
/**
* 實現對so的section進行加密操作
*
* @param elfFilePath 輸入的ELF文件路徑
* @param outPath 加密操作完成後輸出路徑
* @param encodeSectionName 要進行加密的節區名稱
*/
public static void doShell(String elfFilePath, String outPath, String encodeSectionName) {
byte[] fileContent = Utils.readFile(elfFilePath);
if (fileContent == null) {
System.out.println("read file byte failed...");
return;
}
/** 首先將ELF文件解析成 ElfType32的對象格式,ElfType32封裝了ELF文件各個部分的屬性信息 */
ElfType32 type_32 = ELFParser.parseElfToType32(fileContent);
/** 對我們指定的section進行加密操作 */
doEncryptionSection(fileContent, type_32, encodeSectionName);
ElfType32 otype_32 = ELFParser.parseElfToType32(fileContent);
Utils.saveFile(outPath, fileContent);
}
具體加密過程如下:
/**
* 執行具體的加密操作
*
* @param fileByteArys
* @param type_32
* @param encodeSectionName
*/
private static void doEncryptionSection(byte[] fileByteArys, ElfType32 type_32, String encodeSectionName) {
/** ELFheader 定義了.shstrtab節區在節區頭部表中的索引,也就是第幾個表項 */
int shstrab_index = Utils.byte2Short(type_32.hdr.e_shstrndx);
/** 獲取.shstrtab節區的結構,.shstrtab節區保存了各個節區的名稱 */
elf32_shdr shdr = type_32.shdrList.get(shstrab_index);
/**.shstrtab節區的大小**/
int shstrab_size = Utils.byte2Int(shdr.sh_size);
/**.shstrtab節區的偏移,相對於文件**/
int shstrab_offset = Utils.byte2Int(shdr.sh_offset);
/** 記錄要找的節區偏移**/
int mySectionOffset = 0;
/** 記錄要找的節區大小**/
int mySectionSize = 0;
/** 找到名稱爲encodeSectionName的節區,並執行加密操作 */
for(elf32_shdr t_shdr : type_32.shdrList){
/** t_shdr.sh_name定義了節區名稱在.shstrtab節區中的大小偏移,可以理解爲索引 **/
int sectionNameOffset = shstrab_offset + Utils.byte2Int(t_shdr.sh_name);
/** 如果.shstrtab節區在sectionNameOffset偏移出的字符串與encodeSectionName相等,說明找到了**/
if (Utils.isEqualByteAry(fileByteArys, sectionNameOffset, encodeSectionName)) {
/** 這裏需要讀取section段然後進行數據加密 **/
mySectionOffset = Utils.byte2Int(t_shdr.sh_offset);
mySectionSize = Utils.byte2Int(t_shdr.sh_size);
byte[] sectionAry = Utils.copyBytes(fileByteArys, mySectionOffset, mySectionSize);
for (int i = 0; i < sectionAry.length; i++) {
sectionAry[i] = (byte) (sectionAry[i] ^ 0xFF);
}
Utils.replaceByteAry(fileByteArys, mySectionOffset, sectionAry);
}
}
if(mySectionOffset == 0 && mySectionSize == 0){
throw new IllegalArgumentException("Can not find the section of 'encodeSectionName' !");
}
/** 修改Elf Header中的e_entry和e_shoff值 爲加密的section的offset和size
*
* 爲什麼要修改Header中的e_entry和e_shoff值呢?
*
* 1.方便解密的時候快速定位到加密的section,方便解密
*
* 既然修改了Header中的e_entry和e_shoff值,難道程序的加載運行的時候找不到入口地址,不會報錯嗎?
*
* 2.在這裏我們又要理解下目標文件的裝載視圖和鏈接視圖,首先對於動態庫來說,程序被加載時,設定的跳轉地址是動態連接器的地址
* 通過GOT表和PLT表實現動態調用,與e_entry無關,另外在裝載過程中,用不到鏈接視圖中的一些字段,比如e_shoff
* 所以這兩個字段可以被額外數據填充.
*
* **/
int nSize = mySectionSize/4096 + (mySectionSize%4096 == 0 ? 0 : 1);
byte[] entry = new byte[4];
entry = Utils.int2Byte((mySectionSize<<16) + nSize);
Utils.replaceByteAry(fileByteArys, 24, entry);
byte[] offsetAry = new byte[4];
offsetAry = Utils.int2Byte(mySectionOffset);
Utils.replaceByteAry(fileByteArys, 32, offsetAry);
}
}
2.2.2 解密過程
由於很多細節的描述的代碼中一一稱述,這裏就不囉嗦了
1.首先我們見解密函數定義爲
void init_decryption() __attribute__((constructor));
函數聲明爲attribute((constructor))屬性會先於Main函數之前執行
2.然後,獲取動態庫加載在進程內存映像中的虛擬首地址
unsigned long getLibVPAddr(){
unsigned long ret = 0;
char name[] = "libdemo.so";
char buf[4096], *temp;
int pid;
FILE *fp;
pid = getpid();
/** 獲取當前進程的虛擬地址映射表 **/
sprintf(buf, "/proc/%d/maps", pid);
fp = fopen(buf, "r");
if(fp == NULL)
{
puts("open failed");
goto _error;
}
while(fgets(buf, sizeof(buf), fp)){
/** 找到當前進程加載的so庫的虛擬地址 **/
if(strstr(buf, name)){
temp = strtok(buf, "-");
ret = strtoul(temp, NULL, 16);
break;
}
}
_error:
fclose(fp);
return ret;
}
3.執行具體的解密過程
void init_decryption(){
unsigned int nblock;
unsigned int nsize;
unsigned long base;
unsigned long text_addr;
/** ELF Header 結構體指針 **/
Elf32_Ehdr *elfHeader;
/** 獲取加載進內存的so的起始地址 **/
base = getLibVPAddr();
/** 獲取指定section的偏移值和size **/
elfHeader = (Elf32_Ehdr *)base;
/** 在我們加密的過程中e_shoff保存的是目標section的偏移,此時加上基址就是目標section在內存中的虛擬地址 **/
text_addr = elfHeader->e_shoff + base;
/** 獲取目標section的字節空間大小 **/
nblock = elfHeader->e_entry >> 16;
/** 獲取目標section所佔的頁數,每頁對應4 X 1024B **/
nsize = elfHeader->e_entry & 0xffff;
__android_log_print(ANDROID_LOG_INFO, "JNITag", "nblock = 0x%x,nsize:%d", nblock,nsize);
__android_log_print(ANDROID_LOG_INFO, "JNITag", "base = 0x%x", text_addr);
printf("nblock = %d\n", nblock);
/** 修改內存的操作權限,因爲不同的虛擬地址空間的內存段是有權限限制的,比如代碼段是隻讀,數據段:可讀可寫**/
if(mprotect((void *) (text_addr / PAGE_SIZE * PAGE_SIZE), 4096 * nsize, PROT_READ | PROT_EXEC | PROT_WRITE) != 0){
puts("mem privilege change failed");
__android_log_print(ANDROID_LOG_INFO, "JNITag", "mem privilege change failed");
}
unsigned int i;
/** 執行具體的解密操作 **/
for(i=0;i< nblock; i++){
char *addr = (char*)(text_addr + i);
*addr = ~(*addr);
}
/** 解密完成後,要修改會權限 **/
if(mprotect((void *) (text_addr / PAGE_SIZE * PAGE_SIZE), 4096 * nsize, PROT_READ | PROT_EXEC) != 0){
puts("mem privilege change failed");
}
puts("Decrypt success");
}