【預備起~~~】
最近在忙找工作的事情,筆試~面試~筆試~面試。。。很久沒有寫(pian)文(gao)章(fei)。忙了一陣子之后,終於~~~到了選offer的階段(你家公司不是牛嗎,老子不接你家offer,哈哈哈哈~~~),可以喘(出)口(口)氣(惡)了(氣)。。。來來來,繼續討論一下抗靜態分析的問題,這回要說的是如何對so文件進行加密。
【一二三四】
so文件的作用不明覺厲~~~不對是不言而喻。各大廠商的加固方案都會選擇將加固的代碼放到native層,主要因為native層的逆向分析的難度更大,而且代碼執行效率高,對性能影響小。但是總有些大牛,對這些方法是無感的,為了加大難度,這些廠商更加喪心病狂的對so文件進行加固,比如代碼膨脹、ELF文件格式破壞、字節碼加密等等。這篇文章就是主要講簡單粗暴的加密,來窺探一下這當中的原理。
首先,我們都知道so文件本質上也是一種ELF文件,ELF的文件頭如下
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頭部結構
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頭部結構
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的內容,整體加密。
【二二三四】
下面看加密的代碼
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的名字。
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就加密完成了。
【三二三四】
下面我們來看解密代碼,首先先看兩個函數申明
1
2
|
void
printLog() __attribute__((section(
".newsec"
)));
void
init_printLog() __attribute__((constructor));
|
這兩個函數之后都有__attribute__,這是GCC的編譯選項,用於設定函數屬性。__attribute__((section(".newsec")))的意思就是說這個函數將被放到.newsec這個section中,我們前面所說的自己新建section就是這樣實現的。。。那么printLog這個函數就是.newsec的唯一內容。
下面一個是解密函數,constructor屬性可以讓代碼在main之前執行,保證在比較早的時間點執行解密函數,不影響后續的代碼。
1
2
3
4
|
void
printLog()
{
ALOGD(
"this is a log"
);
}
|
printLog代碼很簡單
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的位置
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中有重要的信息,不能亂來,所以難度會大很多。大家有興趣自己實現以下。
就醬~~~