基於section加密的.so加固學習筆記

標籤(空格分隔): 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");
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章