.so是什么文件_安卓 so 文件解析详解
so 文件是啥?so 文件是 elf 文件,elf 文件后綴名是.so,所以也被稱之為so 文件, elf 文件是 linux 底下二進制文件,可以理解為 windows 下的PE文件,在 Android 中可以比作dll,方便函數的移植,在常用于保護 Android 軟件,增加逆向難度。
解析 elf 文件有啥子用?最明顯的兩個用處就是:1、so 加固;2、用于 frida(xposed) 的檢測!?
本文使用 c 語言,編譯器為 vscode。如有錯誤,還請斧正!!!
一、SO 文件整體格式
so 文件大體上可分為四部分,一般來說從上往下是ELF頭部->Pargarm頭部->節區(Section)->節區頭,其中,除了ELF頭部在文件位置固定不變外,其余三部分的位置都不固定。整體結構圖可以參考非蟲大佬的那張圖,圖片如下:
解析語言之所以選擇 c 語言,有兩個原因:
1、做 so 加固的時候可以需要用到,這里就干脆用 c 寫成一個模板,哪里需要就哪里改,不像上次解析 dex 文件的時候用 python 寫,結果后面寫指令還原的時候需要用的時候在寫一遍 c 版本代價太大了;
2、在安卓源碼中,有個elf.h文件,這個文件定義了我們解析時需要用到的所有數據結構,并且給出了參考注釋,是很好的參考資料。elf.h文件路徑如下:
二、解析 ELF 頭部
ELF 頭部數據格式在 elf.h 文件中已經給出,如下圖所示:
每個字段解釋如下:
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具體值(相關值后面有英文注釋,這里就不再添加中文注釋了):
解析代碼如下:
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,如下圖所示:
每個字段解釋如下:
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字段具體取值:
解析代碼:
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,如下圖所示:
每個字段解釋如下:
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取值:
解析代碼:
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(§ionHeader,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作為下一個字符串頭部標識符。例如我有兩個緊鄰的字符串分別是a和b,那么他們在 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解析
.dynamic在elf.h文件中的數據結構是Elf32-Dyn,如下圖所示:
第一個字段表明了類型,占 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,如下所示:
每個字段解釋如下:
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 驗證如下:
九、代碼測試相關截圖
十、frida 反調試和后序
frida 反調試最簡單的就是檢查端口,檢查進程名,檢查 so 文件等,但最準確以及最復雜的是檢查匯編指令,我們知道 frida 是通過一個大調整實現 hook,而跳轉的指令就那么幾條,我們是否可以通過檢查每個函數第一條指令來判斷是否有 frida 了!!!(ps:簡單寫一下原理,拉開寫就太多了,這里感謝某大佬和我討論的這個話題!!!)
本來因為這個 so 文件解析要寫到明年去了,沒想到看起來代碼量大,但實際要用到的地方代碼量很少。。。
源碼 github 鏈接:
https://github.com/windy-purple/parseso/
總結
以上是生活随笔為你收集整理的.so是什么文件_安卓 so 文件解析详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 电脑桌面便签_电脑桌面定时提醒记事本便签
- 下一篇: go iscoinbase()_《电车G