Android so(ELF)文件解析

一、前言

    so文件是啥?so文件是elf文件,elf文件後綴名是.so,所以也被chang常稱之爲so文件,elf文件是linux底下二進制文件,可以理解爲windows下的PE文件,在Android中可以比作dll,方便函數的移植,在常用於保護Android軟件,增加逆向難度。解析elf文件有啥子用?最明顯的兩個用處就是:1、so加固;2、用於frida(xposed)的檢測!

    本文使用c語言,編譯器爲vscode。如有錯誤,還請斧正!!!

    PS:該文已經首發於某公衆號,介意者勿噴!!!


二、SO文件整體格式

    so文件大體上可分爲四部分,一般來說從上往下是ELF頭部->Pargarm頭部->節區(Section)->節區頭,其中,除了ELF頭部在文件位置固定不變外,其餘三部分的位置都不固定。整體結構圖可以參考非蟲大佬的那張圖,圖片如下:

1.png

    解析語言之所以選擇c語言,有兩個原因:1、做so加固的時候可以需要用到,這裏就乾脆用c寫成一個模板,哪裏需要就哪裏改,不像上次解析dex文件的時候用python寫,結果後面寫指令還原的時候需要用的時候在寫一遍c版本代價太大了;2、在安卓源碼中,有個elf.h文件,這個文件定義了我們解析時需要用到的所有數據結構,並且給出了參考註釋,是很好的參考資料。elf.h文件路徑如下:

2.png


三、解析ELF頭部

    ELF頭部數據格式在elf.h文件中已經給出,如下圖所示:

3.png

  每個字段解釋如下:

    1、e_ident數組:前4個字節爲.ELF,是elf標誌頭,第5個字節爲該文件標誌符,爲1代表這是一個32位的elf文件,後面幾個字節代表版本等信息。
    2、e_type字段:表示是可執行文件還是鏈接文件等,安卓上的so文件就是分享文件,一般該字段爲3,詳細請看下圖。
    3、e_machine字段:該字段標誌該文件運行在什麼機器架構上,例如ARM。
    4、e_version字段:該字段表示當前so文件的版本信息,一般爲1.
    5、e_entry字段:該字段是一個偏移地址,爲程序啓動的地址。
    6、e_phoff字段:該字段也是一個偏移地址,指向程序頭(Pargram Header)的起始地址。
    7、e_shoff字段:該字段是一個偏移地址,指向節區頭(Section Header)的起始地址。
    8、e_flags字段:該字段表示該文件的權限,常見的值有1、2、4,分別代表read、write、exec。
    9、e_ehsize字段:該字段表示elf文件頭部大小,一般固定爲52.
    10、e_phentsize字段:該字段表示程序頭(Program Header)大小,一般固定爲32.
    11、e_phnum字段:該字段表示文件中有幾個程序頭。
    12、e_shentsize:該字段表示節區頭(Section Header)大小,一般固定爲40.
    13、e_shnum字段:該字段表示文件中有幾個節區頭。
    14、e_shstrndx字段:該字段是一個數字,這個表明了.shstrtab節區(這個節區存儲着所有節區的名字,例如.text)的節區頭是第幾個。

  e_type具體值(相關值後面有英文註釋,這裏就不再添加中文註釋了):

4.png

  解析代碼如下:

struct DataOffest parseSoHeader(FILE *fp,struct DataOffest off)
{
    Elf32_Ehdr header;
    int i = 0;

    fseek(fp,0,SEEK_SET);
    fread(&header,1,sizeof(header),fp);
    printf("ELF Header:\n");
    printf("    Header Magic: ");
    for (i = 0; i < 16; i++)
    {
        printf("%02x ",header.e_ident[i]);
    }
    printf("\n");
    printf("    So File Type: 0x%02x",header.e_type);
    switch (header.e_type)
    {
    case 0x00:
        printf("(No file type)\n");
        break;
    case 0x01:
        printf("(Relocatable file)\n");
        break;
    case 0x02:
        printf("(Executable file)\n");
        break;
    case 0x03:
        printf("(Shared object file)\n");
        break;
    case 0x04:
        printf("(Core file)\n");
        break;
    case 0xff00:
        printf("(Beginning of processor-specific codes)\n");
        break;
    case 0xffff:
        printf("(Processor-specific)\n");
        break;
    default:
        printf("\n");
        break;
    }
    printf("    Required Architecture: 0x%04x",header.e_machine);
    if (header.e_machine == 0x28)
    {
        printf("(ARM)\n");
    }
    else
    {
        printf("\n");
    }
    printf("    Version: 0x%02x\n",header.e_version);
    printf("    Start Program Address: 0x%08x\n",header.e_entry);
    printf("    Program Header Offest: 0x%08x\n",header.e_phoff);
    off.programheadoffset = header.e_phoff;
    printf("    Section Header Offest: 0x%08x\n",header.e_shoff);
    off.sectionheadoffest = header.e_shoff;
    printf("    Processor-specific Flags: 0x%08x\n",header.e_flags);
    printf("    ELF Header Size: 0x%04x\n",header.e_ehsize);
    printf("    Size of an entry in the program header table: 0x%04x\n",header.e_phentsize);
    printf("    Program Header Size: 0x%04x\n",header.e_phnum);
    off.programsize = header.e_phnum;
    printf("    Size of an entry in the section header table: 0x%04x\n",header.e_shentsize);
    printf("    Section Header Size: 0x%04x\n",header.e_shnum);
    off.sectionsize = header.e_shnum;
    printf("    String Section Index: 0x%04x\n",header.e_shstrndx);
    off.shstrtabindex = header.e_shstrndx;
    return off;
}

四、程序頭(Program Header)解析

    程序頭在elf.h文件中的數據格式是Elf32_Phdr,如下圖所示:

5.png

  每個字段解釋如下:

    1、p_type字段:該字段表明了段(Segment)類型,例如PT_LOAD類型,具體值看下圖,實在有點多,沒辦法這裏寫完。
    2、p_offest字段:該字段表明了這個段在該so文件的起始地址。
    3、p_vaddr字段:該字段指明瞭加載進內存後的虛擬地址,我們靜態解析時用不到該字段。
    4、p_paddr字段:該字段指明加載進內存後的實際物理地址,跟上面的那個字段一樣,解析時用不到。
    5、p_filesz字段:該字段表明了這個段的大小,單位爲字節。
    6、p_memsz字段:該字段表明了這個段加載到內存後使用的字節數。
    7、p_flags字段:該字段跟elf頭部的e_flags一樣,指明瞭該段的屬性,是可讀還是可寫。
    8、p_align字段:該字段用來指明在內存中對齊字節數的。

  p_type字段具體取值:

6.png

  解析代碼:

struct DataOffest parseSoPargramHeader(FILE *fp,struct DataOffest off)
{
    Elf32_Half init;
    Elf32_Half addr;
    int i;
    Elf32_Phdr programHeader;
    
    init = off.programheadoffset;
    for (i = 0; i < off.programsize; i++)
    {
        addr = init + (i * 0x20);
        fseek(fp,addr,SEEK_SET);
        fread(&programHeader,1,32,fp);
        switch (programHeader.p_type)
        {
        case 2:
            off.dynameicoff = programHeader.p_offset;
            off.dynameicsize = programHeader.p_filesz;
            break;
        default:
            break;
        }
        printf("\n\nSegment Header %d:\n",(i + 1));
        printf("    Type of segment: 0x%08x\n",programHeader.p_type);
        printf("    Segment Offset: 0x%08x\n",programHeader.p_offset);
        printf("    Virtual address of beginning of segment: 0x%08x\n",programHeader.p_vaddr);
        printf("    Physical address of beginning of segment: 0x%08x\n",programHeader.p_paddr);
        printf("    Num. of bytes in file image of segment: 0x%08x\n",programHeader.p_filesz);
        printf("    Num. of bytes in mem image of segment (may be zero): 0x%08x\n",programHeader.p_memsz);
        printf("    Segment flags: 0x%08x\n",programHeader.p_flags);
        printf("    Segment alignment constraint: 0x%08x\n",programHeader.p_align);
    }
    return off;
}

五、節區頭(Section Header)解析

    節區頭在elf.h文件中的數據結構爲Elf32_Shdr,如下圖所示:

7.png

  每個字段解釋如下:

    1、sh_name字段:該字段是一個索引值,是.shstrtab表(節區名字字符串表)的索引,指明瞭該節區的名字。
    2、sh_type字段:該字段表明該節區的類型,例如值爲SHT_PROGBITS,則該節區可能是.text或者.rodata,至於具體怎麼區分,當然看sh_name字段。具體取值看下圖。
    3、sh_flags字段:跟上面的一樣,就不再細說了。
    4、sh_addr字段:該字段是一個地址,是該節區加載進內存後的地址。
    5、sh_offset字段:該字段也是一個地址,是該節區在該so文件中的偏移地址。
    6、sh_size字段:該字段表明了該節區的大小,單位是字節。
    7、sh_link和sh_info字段:這兩個字段只適用於少數節區,我們這裏解析用不到,感興趣的可以去看官方文檔。
    8、sh_addralign字段:該字段指明在內存中的對齊字節。
    9、sh_entsize字段:該字段指明瞭該節區中每個項佔用的字節數。

  sh_type取值:

8.png

  解析代碼:

struct DataOffest parseSoSectionHeader(FILE *fp,struct DataOffest off,struct ShstrtabTable StrList[100])
{
    Elf32_Half init;
    Elf32_Half addr;
    Elf32_Shdr sectionHeader;
    int i,id,n;
    char ch;
    int k = 0;

    init = off.sectionheadoffest;
    for (i = 0; i < off.sectionsize; i++)
    {
        addr = init + (i * 0x28);
        fseek(fp,addr,SEEK_SET);
        fread(&sectionHeader,1,40,fp); 
        switch (sectionHeader.sh_type)
        {
        case 2:
            off.symtaboff = sectionHeader.sh_offset;
            off.symtabsize = sectionHeader.sh_size;
            break;
        case 3:
            if(k == 0)
            {
                off.stroffset = sectionHeader.sh_offset;
                off.strsize = sectionHeader.sh_size;
                k++;
            }
            else if (k == 1)
            {
                off.str1offset = sectionHeader.sh_offset;
                off.str1size = sectionHeader.sh_size;
                k++;
            }
            else
            {
                off.str2offset = sectionHeader.sh_offset;
                off.str2size = sectionHeader.sh_size;
                k++;
            }
            break;
        default:
            break;
        }
        id = sectionHeader.sh_name;
        printf("\n\nSection Header %d\n",(i + 1));
        printf("    Section Name: ");
        for (n = 0; n < 50; n++)
        {
            ch = StrList[id].str[n];
            if (ch == 0)
            {
                printf("\n");
                break;
            }
            else
            {
                printf("%c",ch);
            }
        }
        printf("    Section Type: 0x%08x\n",sectionHeader.sh_type);
        printf("    Section Flag: 0x%08x\n",sectionHeader.sh_flags);
        printf("    Address where section is to be loaded: 0x%08x\n",sectionHeader.sh_addr);
        printf("    Offset: 0x%x\n",sectionHeader.sh_offset);
        printf("    Size of section, in bytes: 0x%08x\n",sectionHeader.sh_size);
        printf("    Section type-specific header table index link: 0x%08x\n",sectionHeader.sh_link);
        printf("    Section type-specific extra information: 0x%08x\n",sectionHeader.sh_info);
        printf("    Section address alignment: 0x%08x\n",sectionHeader.sh_addralign);
        printf("    Size of records contained within the section: 0x%08x\n",sectionHeader.sh_entsize);
    }
    return off;
}

六、字符串節區解析

    PS:從這裏開始網上的參考資料很少了,特別是參考代碼,所以有錯誤的地方還請斧正;因爲以後的so加固等只涉及到幾個節區,所以只解析了.shstrtab.strtab.dynstr.text.symtab.dynamic節區!!!

    在elf頭部中有個e_shstrndx字段,該字段指明瞭.shstrtab節區頭部是文件中第幾個節區頭部,我們可以根據這找到.shstrtab節區的偏移地址,然後讀取出來,就可以爲每個節區名字賦值了,然後就可以順着鎖定剩下的兩個字符串節區。

    在elf文件中,字符串表示方式如下:字符串的頭部和尾部用標示字節00標誌,同時上一個字符串尾部標識符00作爲下一個字符串頭部標識符。例如我有兩個緊鄰的字符串分別是ab,那麼他們在elf文件中16進製爲00 97 00 98 00

  解析代碼如下(PS:因爲編碼問題,第一次打印字符串表沒問題,但填充進sh_name就亂碼,所以這裏只放上解析.shstrtab的代碼,但剩下兩個節區節區代碼一樣):

void parseStrSection(FILE *fp,struct DataOffest off,int flag)
{
    int total = 0;
    int i;
    int ch;
    int mark;
    Elf32_Off init;
    Elf32_Off addr;
    Elf32_Word count;

    mark = 1;


    if (flag == 1)
    {
        count = off.strsize;
        init = off.stroffset;
    }
    else if (flag == 2)
    {
        count = off.str1size;
        init = off.str1offset;
    }
    else
    {
        count = off.str2size;
        init = off.str2offset;
    }
    
    
    printf("String Address==>0x%x\n",init);
    printf("String List %d:\n\t[1]==>",flag);

    for (i = 0; i < count; i++)
    {

        addr = init + (i * 1);

        fseek(fp,addr,SEEK_SET);
        fread(&ch,1,1,fp);

        if (i == 0 && ch == 0)
        {
            continue;
        }
        else if (ch != 0)
        {
            printf("%c",ch);
        }
        else if (ch == 0 && i !=0)
        {
            printf("\n\t[%d]==>",(++mark));
        }
    }
    printf("\n");
    
}

七、.dynamic解析

    .dynamicelf.h文件中的數據結構是Elf32-Dyn,如下圖所示:

9.png

    第一個字段表明了類型,佔4個字節;第二個字段是一個共用體,也佔四個字節,描述了具體的項信息。解析代碼如下:

void parseSoDynamicSection(FILE *fp,struct DataOffest off)
{
    int dynamicnum;
    Elf32_Off init;
    Elf32_Off addr;
    Elf32_Dyn dynamicData;
    int i;

    init = off.dynameicoff;
    dynamicnum = (off.dynameicsize / 8);

    printf("Dynamic:\n");
    printf("\t\tTag\t\t\tType\t\t\tName/Value\n");

    for (i = 0; i < dynamicnum; i++)
    {
        addr = init + (i * 8);
        fseek(fp,addr,SEEK_SET);
        fread(&dynamicData,1,8,fp);
        printf("\t\t0x%08x\t\tNOPRINTF\t\t0x%x\n",dynamicData.d_tag,dynamicData.d_un);
    }
    
}

八、.symtab解析

    該節區是該so文件的符號表,它在elf.h文件中的數據結構是Elf32_Sym,如下所示:

10.png

  每個字段解釋如下:

    1、st_name字段:該字段是一個索引值,指明瞭該項的名字。
    2、st_value字段:該字段表明了相關聯符號的取值。
    3、stz-size字段:該字段指明瞭每個項所佔用的字節數。
    4、st_info和st_other字段:這兩個字段指明瞭符號的類型。
    5、st_shndx字段:相關索引。

  解析代碼如下(PS:由於亂碼問題,索引手動固定了地址測試,有興趣的挨個解析字符應該可以解決亂碼問題):

void parseSoDynamicSection(FILE *fp,struct DataOffest off)
{
    int dynamicnum;
    Elf32_Off init;
    Elf32_Off addr;
    Elf32_Dyn dynamicData;
    int i;

    init = off.dynameicoff;
    dynamicnum = (off.dynameicsize / 8);

    printf("Dynamic:\n");
    printf("\t\tTag\t\t\tType\t\t\tName/Value\n");

    for (i = 0; i < dynamicnum; i++)
    {
        addr = init + (i * 8);
        fseek(fp,addr,SEEK_SET);
        fread(&dynamicData,1,8,fp);
        printf("\t\t0x%08x\t\tNOPRINTF\t\t0x%x\n",dynamicData.d_tag,dynamicData.d_un);
    }
    
}

    void parseSymtabSection(FILE *fp,struct DataOffest off)
    {
        Elf32_Off init;
        Elf32_Off addr;
        Elf32_Word count;
        Elf32_Sym symtabSection;
        int k,i;

        init = off.symtaboff;
        count = off.symtabsize;

        printf("SymTable:\n");

        for (i = 0; i < count; i++)
        {
            addr = init + (i * 16);
            fseek(fp,addr,SEEK_SET);
            fread(&symtabSection,1,16,fp);
            printf("Symbol Name Index: 0x%x\n",symtabSection.st_name);
            printf("Value or address associated with the symbol: 0x%08x\n",symtabSection.st_value);
            printf("Size of the symbol: 0x%x\n",symtabSection.st_size);
            printf("Symbol's type and binding attributes: %c\n",symtabSection.st_info);
            printf("Must be zero; reserved: 0x%x\n",symtabSection.st_other);
            printf("Which section (header table index) it's defined in: 0x%x\n",symtabSection.st_shndx);
        }
        
    }

九、.text解析

    PS:這部分沒代碼了,只簡單解析一下,因爲解析arm指令太麻煩了,估計得寫個半年都不一定能搞定,後續寫了會同步更新在github!!!

    .text節區存儲着可執行指令,我們可以通過節區頭部的名字鎖定.text的偏移地址和大小,找到該節區後,我們會發現這個節區存儲的就是arm機器碼,直接照着指令集翻譯即可,沒有其他的結構。通過ida驗證如下:

11.png


十、代碼測試相關截圖

12.png

13.png

14.png

15.png

16.png


十一、frida反調試和後序

    frida反調試最簡單的就是檢查端口,檢查進程名,檢查so文件等,但最準確以及最複雜的是檢查彙編指令,我們知道frida是通過一個大調整實現hook,而跳轉的指令就那麼幾條,我們是否可以通過檢查每個函數第一條指令來判斷是否有frida了!!!(ps:簡單寫一下原理,拉開寫就太多了,這裏感謝某大佬和我討論的這個話題!!!)

    本來因爲這個so文件解析要寫到明年去了,沒想到看起來代碼量大,但實際要用到的地方代碼量很少。。。

    源碼github鏈接:https://github.com/windy-purple/parseso/

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