SO文件中函數(shù)的加密和解密
簡(jiǎn)介
原理上來(lái)說(shuō)剃法,找到so文件函數(shù)的位置,對(duì)其二進(jìn)制進(jìn)行一定加密操作后即加密了,解密也是一樣箱歧,找到函數(shù)的位置剧罩,對(duì)其二進(jìn)制進(jìn)行一定的解密操作即可栓拜,只不過(guò)前者是通過(guò)so文件格式,按照一定的索引一步一步找到函數(shù)位置惠昔;后者是在其運(yùn)
行時(shí)幕与,通過(guò)/proc/pid號(hào)/maps文件,找到so文件映射的內(nèi)存虛擬地址镇防,在根據(jù)so文件格式的索引一步一步找到函數(shù)位置啦鸣,再進(jìn)行解密工作;首先来氧,從section角度來(lái)說(shuō):
+ .text 存放函數(shù)的具體實(shí)現(xiàn)
+ .dynstr 存放函數(shù)名字
+ .dynsym 相當(dāng)于指針诫给,存放每個(gè)函數(shù)實(shí)現(xiàn)代碼(在.text)的地址
+ .hash 描述.dynsym如何存儲(chǔ)的,根據(jù)一定的算法可以映射到dynsym
注意:文中的提到的偏移是指在so文件頭開(kāi)始到任意一個(gè)位置的偏移
section結(jié)構(gòu)
.text和.dynstr沒(méi)有固定的結(jié)構(gòu)啦扬,存儲(chǔ)的都是代碼的值中狂,.hash和.dynsym則是有固定的結(jié)構(gòu),后兩者經(jīng)過(guò)一定邏輯運(yùn)算可以定位某個(gè)函數(shù)在前兩者的具體的位置;還有一點(diǎn)扑毡,后面兩個(gè)section從結(jié)構(gòu)視圖(section header)方面找到胃榕,需要從>執(zhí)行視圖(program header)方便的找到,所以這里需要了解program header中的一個(gè)段.dynmaic的結(jié)構(gòu)
.dynmaic segemnet
這個(gè)段可以根據(jù)program header的type類(lèi)型來(lái)確定瞄摊,type為PT_DYNAMIC = 2
d_tag: 描述每個(gè)具體段的類(lèi)型勋又,如.hash .dynsym .dynstr等段,都可以根據(jù)d_tag來(lái)確定
d_val和d_ptr是一個(gè)聯(lián)合體類(lèi)型泉褐,共同占用4個(gè)字節(jié)赐写,一個(gè)類(lèi)型多種解釋;
d_val: 表示d_tag對(duì)應(yīng)段的大小size
d_ptr: 表示d_tag對(duì)應(yīng)段的位置偏移
該段結(jié)構(gòu)如下:
typedef struct dynamic{
Elf32_Sword d_tag;
union{
Elf32_Sword d_val;
Elf32_Addr d_ptr;
} d_un;
} Elf32_Dyn;
找到這個(gè).dynamic可以找到.hash .dynsym和.dynstr的偏移位置和大小
.hash section
一般來(lái)說(shuō),經(jīng)過(guò)如下操作可以定位到函數(shù)名字和函數(shù)實(shí)現(xiàn)代碼
- 函數(shù)字符串名字經(jīng)過(guò)hash函數(shù)得到一個(gè)hash_value的值
- 在.hash段中的nbucket膜赃,hash_value = hash_value % nbucket,hash段自身偏移加上hash_valuei + 8后偏移處讀取4字節(jié)值揉忘,記為fun_index;8表示開(kāi)頭的nbucket和nchain占用的字節(jié)
- 在.dynsym段中跳座,.dynsym自身偏移 + fun_index * 16,為什么要乘以16,是因?yàn)閐ynsym每個(gè)結(jié)構(gòu)占16個(gè)字節(jié)泣矛,這段偏移值的位置讀取16個(gè)字節(jié)疲眷,轉(zhuǎn)換微dynsym的elf_sym結(jié)構(gòu)
- elf_sym結(jié)構(gòu)中有一個(gè)st_name,用.dynstr的偏移 + st_name的值作為偏移您朽,讀取偏移處的值狂丝,然后與我們要加密的函數(shù)名比較换淆,相同就說(shuō)明找到,沒(méi)有的繼續(xù)進(jìn)行以下步驟
- 計(jì)算hash的新偏移 funindex_offset = dyn_detail['hash_offset'] + 4 * (2 + nbucket + fun_index)几颜,這句的意思就是要跳過(guò)以下hash結(jié)構(gòu)的bucket[]數(shù)組倍试,在chain中的fun_index位置出重新讀取到新的fun_index_offset,乘4是因
為每個(gè)數(shù)據(jù)占用4個(gè)字節(jié)蛋哭,讀取此處偏移得到新的fun_index - 重復(fù)3-4-5步驟县习,直到找到和函數(shù)名字相同的st_name為止
.dynsym section
st_name對(duì)應(yīng)的字符串和函數(shù)名相同后,就可以根據(jù)st_value和st_size進(jìn)行函數(shù)結(jié)構(gòu)加密了
typedef struct elf32_sym{
Elf32_Word st_name; #.dynstr_offset + st_name就是某個(gè)函數(shù)的具體偏移
Elf32_Addr st_value; #st_value就是某個(gè)函數(shù)代碼實(shí)現(xiàn)的偏移
Elf32_Word st_size; #函數(shù)代碼的長(zhǎng)度
unsigned char st_info;
unsigned char st_other;
Elf32_Half st_shndx;
} Elf32_Sym;
腳本
encode_so_function.py
這是我自己寫(xiě)的一個(gè)函數(shù)加密腳本谆趾,輸入函數(shù)名字躁愿,可以自動(dòng)找到函數(shù)對(duì)應(yīng)的地發(fā),然后進(jìn)行加密沪蓬,修改加密只需要改動(dòng)其中的encode_fun(fun_sym)函數(shù)即可彤钟,參數(shù)事找到函數(shù)對(duì)應(yīng)的那個(gè)dynsym結(jié)構(gòu),如上的結(jié)構(gòu)
加密腳本使用方法:
python encode_so_function.py so文件名 函數(shù)名
解密
界面時(shí)跷叉,我們需要先了解兩個(gè)知識(shí)點(diǎn):
attribute機(jī)制
__attribute__(Attribute);
該屬性是GNU C編譯的一種特色機(jī)制样勃,里面的Attribute可以設(shè)置函數(shù)屬性(Function Attribute)、變量屬性和類(lèi)型屬性性芬。我們這里用到函數(shù)屬性constructor峡眶,該屬性定義的函數(shù)將會(huì)在main函數(shù)執(zhí)行之前執(zhí)行,利用這個(gè)特性我們就可以在main之前解密我們的加密的函數(shù)段植锉,最后就可以運(yùn)行了辫樱;attribute的其他用法參考這里,以下是我自己使用的例子:
void init_decode() __attribute__((constructor));
void init_decode()是我自己寫(xiě)得一個(gè)解密函數(shù)俊庇,這個(gè)函數(shù)將會(huì)在main之前執(zhí)行
Linux的/proc/pid進(jìn)程號(hào)/maps文件
因?yàn)槲覀兊膕o文件時(shí)在運(yùn)行的時(shí)候進(jìn)行解密工作的狮暑,而maps文件正是應(yīng)用程序運(yùn)行時(shí)對(duì)各個(gè)段在內(nèi)存中的映射表文件,如讀寫(xiě)數(shù)據(jù)段辉饱、只讀代碼段搬男、堆和棧等區(qū)域在內(nèi)存中如何映射的,該文件都有記錄彭沼,如下:
[root@localhost proc]# cat /proc/1/maps
00110000-00111000 r-xp 00110000 00:00 0 [vdso]
0032b000-00347000 r-xp 00000000 fd:00 852733 /lib/ld-2.8.so
00347000-00348000 r--p 0001c000 fd:00 852733 /lib/ld-2.8.so
00348000-00349000 rw-p 0001d000 fd:00 852733 /lib/ld-2.8.so
0034b000-004ae000 r-xp 00000000 fd:00 852734 /lib/libc-2.8.so
004ae000-004b0000 r--p 00163000 fd:00 852734 /lib/libc-2.8.so
004b0000-004b1000 rw-p 00165000 fd:00 852734 /lib/libc-2.8.so
004b1000-004b4000 rw-p 004b1000 00:00 0
08048000-08067000 r-xp 00000000 fd:00 843075 /sbin/init
08067000-08068000 rw-p 0001e000 fd:00 843075 /sbin/init
08b42000-08b6a000 rw-p 08b42000 00:00 0 [heap]
b8046000-b8048000 rw-p b8046000 00:00 0
bfb4e000-bfb63000 rw-p bffeb000 00:00 0 [stack]
這里只做簡(jiǎn)單介紹缔逛,具體可參考這篇文章,上訴第一列代表映射到內(nèi)存的虛擬地址姓惑,第二列表示該內(nèi)存區(qū)域的權(quán)限褐奴,第三列表示內(nèi)存區(qū)域的偏移,最后一列是被映射的文件于毙,這里有我們熟悉的so庫(kù)敦冬,也有無(wú)名映射如堆棧heao stack等;
我們要做的是唯沮,在解密函數(shù)中脖旱,打開(kāi)讀取這個(gè)文件堪遂,找到第一個(gè)映射文件名為我們的so庫(kù)名,拿到這一行的第一列的虛擬地址作為基地址base萌庆,然后在根據(jù)so文件的格式索引(加密中尋找函數(shù)位置的過(guò)程)溶褪,這個(gè)過(guò)程中只要涉及地址操作的>都要加上這個(gè)base基地址,最終找到我們的函數(shù)進(jìn)行解密即可
例子
解密原理就是我上面的闡述踊兜,例子請(qǐng)參考我的github
C API 詳解
#include <unistd.h>
#include <sys/mmap.h>
int mprotect(const void *start, size_t len, int prot);
mprotect()函數(shù)把自start開(kāi)始的竿滨、長(zhǎng)度為len的內(nèi)存區(qū)的保護(hù)屬性修改為prot指定的值。
prot可以取以下幾個(gè)值捏境,并且可以用“|”將幾個(gè)屬性合起來(lái)使用:
1)PROT_READ:表示內(nèi)存段內(nèi)的內(nèi)容可寫(xiě)于游;
2)PROT_WRITE:表示內(nèi)存段內(nèi)的內(nèi)容可讀;
3)PROT_EXEC:表示內(nèi)存段中的內(nèi)容可執(zhí)行垫言;
4)PROT_NONE:表示內(nèi)存段中的內(nèi)容根本沒(méi)法訪問(wèn)贰剥。
需要指出的是,指定的內(nèi)存區(qū)間必須包含整個(gè)內(nèi)存頁(yè)(4K)筷频。區(qū)間開(kāi)始的地址start必須是一個(gè)內(nèi)存頁(yè)的起始地址蚌成,并且區(qū)間長(zhǎng)度len必須是頁(yè)大小的整數(shù)倍。
如果執(zhí)行成功凛捏,則返回0担忧;如果執(zhí)行失敗,則返回-1坯癣,并且設(shè)置errno變量瓶盛,說(shuō)明具體因?yàn)槭裁丛蛟斐烧{(diào)用失敗。錯(cuò)誤的原因主要有以下幾個(gè):
1)EACCES
該內(nèi)存不能設(shè)置為相應(yīng)權(quán)限示罗。這是可能發(fā)生的惩猫,比如,如果你 mmap(2) 映射一個(gè)文件為只讀的蚜点,接著使用 mprotect() 標(biāo)志為 PROT_WRITE轧房。
2)EINVAL
start 不是一個(gè)有效的指針,指向的不是某個(gè)內(nèi)存頁(yè)的開(kāi)頭绍绘。
3)ENOMEM
內(nèi)核內(nèi)部的結(jié)構(gòu)體無(wú)法分配奶镶。
4)ENOMEM
進(jìn)程的地址空間在區(qū)間 [start, start+len] 范圍內(nèi)是無(wú)效,或者有一個(gè)或多個(gè)內(nèi)存頁(yè)沒(méi)有映射脯倒。
如果調(diào)用進(jìn)程內(nèi)存訪問(wèn)行為侵犯了這些設(shè)置的保護(hù)屬性实辑,內(nèi)核會(huì)為該進(jìn)程產(chǎn)生 SIGSEGV (Segmentation fault,段錯(cuò)誤)信號(hào)藻丢,并且終止該進(jìn)程。