對抗靜態分析——so文件的加密


【預備起~~~】
最近在忙找工作的事情,筆試~面試~筆試~面試。。。很久沒有寫(pian)文(gao)章(fei)。忙了一陣子之后,終於~~~到了選offer的階段(你家公司不是牛嗎,老子不接你家offer,哈哈哈哈~~~),可以喘(出)口(口)氣(惡)了(氣)。。。來來來,繼續討論一下抗靜態分析的問題,這回要說的是如何對so文件進行加密。


【一二三四】
so文件的作用不明覺厲~~~不對是不言而喻。各大廠商的加固方案都會選擇將加固的代碼放到native層,主要因為native層的逆向分析的難度更大,而且代碼執行效率高,對性能影響小。但是總有些大牛,對這些方法是無感的,為了加大難度,這些廠商更加喪心病狂的對so文件進行加固,比如代碼膨脹、ELF文件格式破壞、字節碼加密等等。這篇文章就是主要講簡單粗暴的加密,來窺探一下這當中的原理。


首先,我們都知道so文件本質上也是一種ELF文件,ELF的文件頭如下

[C]  純文本查看 復制代碼
?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
#define EI_NIDENT 16
typedef struct elf32_hdr{
/* WARNING: DO NOT EDIT, AUTO-GENERATED CODE - SEE TOP FOR INSTRUCTIONS */
  unsigned char e_ident[EI_NIDENT];
  Elf32_Half e_type;
  Elf32_Half e_machine;
  Elf32_Word e_version;
/* WARNING: DO NOT EDIT, AUTO-GENERATED CODE - SEE TOP FOR INSTRUCTIONS */
  Elf32_Addr e_entry;
  Elf32_Off e_phoff;
  Elf32_Off e_shoff;
  Elf32_Word e_flags;
/* WARNING: DO NOT EDIT, AUTO-GENERATED CODE - SEE TOP FOR INSTRUCTIONS */
  Elf32_Half e_ehsize;
  Elf32_Half e_phentsize;
  Elf32_Half e_phnum;
  Elf32_Half e_shentsize;
/* WARNING: DO NOT EDIT, AUTO-GENERATED CODE - SEE TOP FOR INSTRUCTIONS */
  Elf32_Half e_shnum;
  Elf32_Half e_shstrndx;
} Elf32_Ehdr;


詳細的就不說了,簡單看下,開始的16字節是ELF文件魔數,然后是一些文件信息硬件、版本之類的,重點在幾個變量
e_phoff、e_shoff、e_phentsize、e_phnum、e_shentsize、e_shnum、e_shstrndx
要知道這幾個變量的含義首先要清楚,ELF文件的結構在鏈接時和執行時是不同的
<ignore_js_op> 
一般情況下(也就是我們看到的情況),ELF文件內部分為多個section,每個section保存不同的信息,比如.shstrtab保存段信息的字符串,.text裝載可執行代碼等等。這些不同的section根據不同的內容和作用會有不同的讀寫和執行權限,但是這些section的權限是沒有規律的,比如第一個section的權限是只讀,第二個是讀寫、第三個又是只讀。如果在內存當中直接以這種形式存在,那么文件在執行的時候會造成權限控制難度加大,導致不必要的消耗。所以當我們將so文件鏈接到內存中時,存在的不是section,而是segment,每個segment可以看作是相同權限的section的集合。也就是說在內存當中一個segment對應N個section(N>=0),而這些section和segment的信息都會被保存在文件中。
理解了這個,再看那幾個變量。e_phoff是segment頭部偏移的位置,e_phentsize是segment頭部的大小,e_phnum指segment頭部的個數(每個segment都有一個頭部,這些頭部是連續放在一起的,頭部中有變量指向這些segment的具體內容)。同樣e_shoff、e_shentsize、e_shnum分別表示section的頭部偏移、頭部大小、頭部數量。最后一個e_shstrndx有點難理解。ELF文件中的每個section都是有名字的,比如.data、.text、.rodata,每個名字都是一個字符串,既然是字符串就需要一個字符串池來保存,而這個字符串池也是一個section,或者說准備一個section用來維護一個字符串池,這個字符串池保存了其他section以及它自己的名字。這個特殊的section叫做.shstrtab。由於這個section很特殊,所以把它單獨標出來。我們也說了,所有section的頭部是連續存放在一起的,類似一個數組,e_shstrndx變量是.shstrtab在這個數組中的下標。(希望我解釋清楚了~~~)
segment頭部結構

[C]  純文本查看 復制代碼
?
01
02
03
04
05
06
07
08
09
10
11
12
typedef struct elf32_phdr{
  Elf32_Word p_type;
/* WARNING: DO NOT EDIT, AUTO-GENERATED CODE - SEE TOP FOR INSTRUCTIONS */
  Elf32_Off p_offset;
  Elf32_Addr p_vaddr;
  Elf32_Addr p_paddr;
  Elf32_Word p_filesz;
/* WARNING: DO NOT EDIT, AUTO-GENERATED CODE - SEE TOP FOR INSTRUCTIONS */
  Elf32_Word p_memsz;
  Elf32_Word p_flags;
  Elf32_Word p_align;
} Elf32_Phdr;


section頭部結構

[C]  純文本查看 復制代碼
?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
typedef struct elf32_shdr {
  Elf32_Word sh_name;
/* WARNING: DO NOT EDIT, AUTO-GENERATED CODE - SEE TOP FOR INSTRUCTIONS */
  Elf32_Word sh_type;
  Elf32_Word sh_flags;
  Elf32_Addr sh_addr;
  Elf32_Off sh_offset;
/* WARNING: DO NOT EDIT, AUTO-GENERATED CODE - SEE TOP FOR INSTRUCTIONS */
  Elf32_Word sh_size;
  Elf32_Word sh_link;
  Elf32_Word sh_info;
  Elf32_Word sh_addralign;
/* WARNING: DO NOT EDIT, AUTO-GENERATED CODE - SEE TOP FOR INSTRUCTIONS */
  Elf32_Word sh_entsize;
} Elf32_Shdr;


注意這里都是32位的。。。


在代碼當中segment的命名是program,所以segment和program指的是同一個東西
Program header位於ELF header后面,Section Header位於ELF文件的尾部。那可以推出:
e_phoff = sizeof(e_ehsize);
整個ELF文件大小 = e_shoff + e_shnum * sizeof(e_shentsize) + 1



這里多講一點與加密沒有關系的知識。我們知道了在內存當中只有segment而沒有section,那么如果section結構被破壞了,ELF文件是不是還能正常執行?答案:是
如何證明大家可以自己去尋找答案,這里不多說。但是由於這樣,所以經常會破壞文件的section結構,讓比如IDA、readelf等工具失效,這也是so加固的一種方式。


回到正題,我們繼續說加密。加密的流程我們設想一下,可以是這樣 解析ELF——>找到字節碼——>對字節碼加密
解密就是 解析ELF——>找到字節碼——>對字節碼解密
詳細一點就是通過偏移、個數等信息找到section的頭部,然后看是不是我們要找的section(通過名字)。找到后通過sh_offset(偏移)和sh_size(大小),就找到這個section的內容,整體加密。


【二二三四】
下面看加密的代碼

[C]  純文本查看 復制代碼
?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
fd = open(argv[1], O_RDWR);        //打開文件
if (fd < 0){
   printf ( "open %s failed\n" , argv[1]);
   goto _error;
}
 
if (read(fd, &ehdr, sizeof (Elf32_Ehdr)) != sizeof (Elf32_Ehdr)){        //讀取頭部,驗證文件是否正確
   puts ( "Read ELF header error" );
   goto _error;
}
 
lseek(fd, ehdr.e_shoff + sizeof (Elf32_Shdr) * ehdr.e_shstrndx, SEEK_SET); //移動到shstrtab的頭部
 
if (read(fd, &shdr, sizeof (Elf32_Shdr)) != sizeof (Elf32_Shdr)){ //讀取shstrtab頭部
   puts ( "Read ELF section string table error" );
   goto _error;
}
 
if ((shstr = ( char *) malloc (shdr.sh_size)) == NULL){ //開辟內存區域,這個用於保存shstrtab的字符串池
   puts ( "Malloc space for section string table failed" );
   goto _error;
}
 
lseek(fd, shdr.sh_offset, SEEK_SET);                //移動到shstrtab的字符串池
if (read(fd, shstr, shdr.sh_size) != shdr.sh_size){ //讀取字符串池
   puts ( "Read string table failed" );
   goto _error;
}
 
lseek(fd, ehdr.e_shoff, SEEK_SET);                //移動到section頭部數組的起始位置
for (i = 0; i < ehdr.e_shnum; i++){                //遍歷section的頭部
   if (read(fd, &shdr, sizeof (Elf32_Shdr)) != sizeof (Elf32_Shdr)){
     puts ( "Find section .text procedure failed" );
     goto _error;
   }
   if ( strcmp (shstr + shdr.sh_name, target_section) == 0){ //找到目標section
     base = shdr.sh_offset;
     length = shdr.sh_size;
     printf ( "Find section %s\n" , target_section);
     break ;
   }
}


這一段是從打開文件到找到制定section的代碼,我們為了減小實驗難度,不會對一些重要的section加密(可能被玩壞),我們自己新建一個section,新建的方法之后說,所以這里的字符串target_section就是我們自己定義的section的名字。


[C]  純文本查看 復制代碼
?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
lseek(fd, base, SEEK_SET);                //移動到目標section的內容上
content = ( char *) malloc (length);
if (content == NULL){
   puts ( "Malloc space for content failed" );
   goto _error;
}
if (read(fd, content, length) != length){ //讀取出來
   puts ( "Read section .text failed" );
   goto _error;
}
 
nblock = length / block_size;
nsize = base / 4096 + (base % 4096 == 0 ? 0 : 1);
printf ( "base = %d, length = %d\n" , base, length);
printf ( "nblock = %d, nsize = %d\n" , nblock, nsize);
 
ehdr.e_entry = (length << 16) + nsize; //將sh_size和addr寫到e_entry,簡化解密流程
ehdr.e_shoff = base;
 
 
 
for (i=0;i<length;i++){
   content[/size][i][size=4] = ~content[/size][i][size=4]; //整體異或
}
 
 
 
lseek(fd, 0, SEEK_SET);
if (write(fd, &ehdr, sizeof (Elf32_Ehdr)) != sizeof (Elf32_Ehdr)){ //將頭部寫回
   puts ( "Write ELFhead to .so failed" );
   goto _error;
}
 
 
lseek(fd, base, SEEK_SET);
if (write(fd, content, length) != length){ //將內容寫回
   puts ( "Write modified content to .so failed" );
   goto _error;
}


找到之后就修改加密了,完成后寫回。這個so就加密完成了。

【三二三四】
下面我們來看解密代碼,首先先看兩個函數申明

[C]  純文本查看 復制代碼
?
1
2
void printLog() __attribute__((section( ".newsec" )));
void init_printLog() __attribute__((constructor));


這兩個函數之后都有__attribute__,這是GCC的編譯選項,用於設定函數屬性。__attribute__((section(".newsec")))的意思就是說這個函數將被放到.newsec這個section中,我們前面所說的自己新建section就是這樣實現的。。。那么printLog這個函數就是.newsec的唯一內容。
下面一個是解密函數,constructor屬性可以讓代碼在main之前執行,保證在比較早的時間點執行解密函數,不影響后續的代碼。

[C]  純文本查看 復制代碼
?
1
2
3
4
void printLog()
{
   ALOGD( "this is a log" );
}


printLog代碼很簡單

[C]  純文本查看 復制代碼
?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
void init_printLog()
{
   char name[15];
   unsigned int nblock;
   unsigned int nsize;
   unsigned long base;
   unsigned long text_addr;
   unsigned int i;
   Elf32_Ehdr *ehdr;
   Elf32_Shdr *shdr;
 
   base = getLibAddr();
 
   ehdr = (Elf32_Ehdr *)base;
   text_addr = ehdr->e_shoff + base;
 
   nblock = ehdr->e_entry >> 16;
   nsize = ehdr->e_entry & 0xffff;
 
   printf ( "nblock = %d\n" , nblock);
 
   if (mprotect(( void *) base, 4096 * nsize, PROT_READ | PROT_EXEC | PROT_WRITE) != 0){
     puts ( "mem privilege change failed" );
   }
 
   for (i=0;i< nblock; i++){
     char *addr = ( char *)(text_addr + i);
     *addr = ~(*addr);
   }
 
   if (mprotect(( void *) base, 4096 * nsize, PROT_READ | PROT_EXEC) != 0){
     puts ( "mem privilege change failed" );
   }
   puts ( "Decrypt success" );
}


解密過程,大多數差不多,需要注意兩個地方一個是getLibAddr,用於獲得內存中so的位置

[C]  純文本查看 復制代碼
?
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
unsigned long getLibAddr(){
   unsigned long ret = 0;
   char name[] = "libdexloader.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)){
     if ( strstr (buf, name)){
       temp = strtok (buf, "-" );
       ret = strtoul (temp, NULL, 16);
       break ;
     }
   }
_error:
   fclose (fp);
   return ret;
}


還有個是mprotect
這個函數用於修改內存頁的權限,如果不修改,用戶對於內存頁的權限只有read,你是無法對內存中的數據進行修改的。這個和之前我們所說的segment的權限不一樣,要注意區分。


【再來一次】
這種單獨建一個section的方法簡單粗暴易懂,但是只要解析一下就會知道多了一個section。所以實際上往往都是對固定的section進行加密解密,要注意的是這些section中有重要的信息,不能亂來,所以難度會大很多。大家有興趣自己實現以下。
就醬~~~


免責聲明!

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



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