在閱讀這篇文章之前,我在處理mono加密問題時,也是參考了雨凇的文章,所以建議先看一下雨凇寫的關於加密Dll的文章:
1.Unity3D研究院之Android加密DLL與破解DLL .SO
2.Unity3D研究院之Android二次加密.so二次加密DLL
假裝讀者已經看過上面的兩篇文章了,下面我會記錄一下我做的整個加密流程。
一.選取加密Dll的算法
我們主要目的是對程序集:Assembly-CSharp.dll 進行加密,然后修改mono源碼,在mono加載Dll的時候進行解密。顯然我們需要一種可逆、對稱的加密算法,其實這類算法很多,如DES、TEA、XXTEA等,一般這類對稱秘鑰算法的安全性都是基於秘鑰的(Key),所以如何在mono解密是保護自己的秘鑰就十分重要了。我目前使用的是XXTEA,實現的話不清楚,但是github上有開源實現,所以直接拿來用了:xxtea-c
1.先用Unity導出一個android Google工程,在工程路徑 {$Project}\assets\bin\Data\Managed\Assembly-CSharp.dll ,這個文件就是需要我們替換的程序集啦
2.編寫加密Dll工具,大家可以把上面開源xxtea項目中的源碼:xxtea.h、xxtea.c 和下面的encryptDll.c代碼放在同一目錄,用MinGW下的gcc編譯就可以了:gcc xxtea.c encryptDll.c –o EncryptDll
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "xxtea.h"
#define SIZE 1024*1024*10
void main() //命令行參數
{
FILE *infp = 0;//判斷命令行是否正確
if((infp=fopen("Assembly-CSharp.dll","rb"))==NULL)
{
printf("Assembly-CSharp.dll Read Error\n");//打開操作不成功
return;//結束程序的執行
}
//char buffer[SIZE];
char* buffer = (char*)malloc(sizeof(char)*SIZE);
memset(buffer,0,sizeof(char)*SIZE);
int rc = 0;
int total_len = 0;
total_len = fread(buffer , sizeof(unsigned char) , SIZE , infp);
printf("Read Assembly-CSharp Successfully and total_len : %d \n" , total_len);
//加密DLL
size_t len;
char* key = "123456";
char *encrypt_data = xxtea_encrypt(buffer,total_len, key, &len);
printf("Encrypt Dll Successfully and len : %d\n" , len);
//寫Dll
FILE* outfp = 0;
if((outfp=fopen("Assembly-CSharp_encrypt.dll","wb+"))==NULL)
{
printf("Assembly-CSharp_encrypt.dll Read Error\n");//打開操作不成功
return;//結束程序的執行
}
int rstCount = fwrite(encrypt_data , sizeof(unsigned char) , len , outfp);
fflush(outfp);
printf("Write len : %d\n", rstCount);
fclose(infp);
fclose(outfp);
free(buffer);
free(encrypt_data);
}
在用生成的EncryptDll.exe Dll_Path 就可以直接加密改Dll了
二.mono中解密
我們需要修改{$mono_root}/mono/metadata/image.c ,它有一個mono_image_open_from_data_with_name 函數,該方法是加載Dll的入口函數,在這里實現解密
MonoImage *
mono_image_open_from_data_with_name (char *data, guint32 data_len, gboolean need_copy, MonoImageOpenStatus *status, gboolean refonly, const char *name)
{
if(strstr(name ,"Assembly-CSharp.dll")){
g_message("mono: === Start Decrypt Dll ==========\n");
char key = "123456";
size_t len;
char* decryptData = decrypt(data , key);//換成對應的解密函數
int i = 0;
for ( i = 0; i < len; ++i)
{
data[i] = decryptData[i];
}
g_free(decryptData);
g_message("mono: === End Decrypt Dll ========== \n");
}
........
return register_image (image);
}
到此解密和加密過程就結束了,
1.我們可以重新編譯修改后的mono,然后用{$mono_root}/embedruntimes/android/*下對應平台libmono.so覆蓋掉{$Unity_Root}/Editor/Data/PlaybackEngines/androidplayer/(development | release)/libs/* , 然后就可以重新導出android工程了。
2.導出android工程后,用生面生成的EncryptDll.exe 加密Assembly-CSharp.dll
3.用eclipse 或者 android studio 導出apk,運行 success !
三.mono種key保護
如果順利完成(二)中的過程,那么就可以防住很大一部分小白破解者了,但是就像雨凇文章中說的,只要是稍微厲害點的玩家還是可以破解的,用IDA神器,很快就能反編譯libmono.so 並找到key,然后解密Dll,然后就又可以堂而皇之地修改Dll啦……sadly,那么我們如果防止這種情況呢,下面有幾種方案可供選擇,但是在閱讀后面的內容時強烈建議先了解一下ELF文件格式,推薦兩個鏈接:http://www.cnblogs.com/xmphoenix/archive/2011/10/23/2221879.html , http://blog.chinaunix.net/uid-21273878-id-1828736.html , 了解一些ELF文件頭信息,會很有幫助的,因為肯定會踩一些坑的……
1.加密指定的section
這個方式雨凇已經在文章中給了足夠詳細的說明和源碼,這里就不瞎補充了,但是,這個方案有個致命的缺陷,就是無法兼容x86架構的cpu,驟然一聽不兼容x86似乎是一個非常嚴重的問題,其實有所了解x86的就會明白其實並沒有什么大問題,因為x86的機器真的很少,除了華碩和聯想有幾款小眾機型外,其他品牌幾乎沒有x86的機型,甚至在weTest上也找不到x86的機型 ,這估計也是雨凇沒有測出來的原因……,當然在我初步遇到這個問題也是用了一兩天時間去嘗試修改代碼使它兼容x86 cpu,下面是我做的嘗試方案:
a.修改保存信息ELF位置
這個方案的代碼有個前提是,ehdr.e_entry , 和 ehdr.e_shoff 或者其他ELF頭其他位置可讀寫,並不會影響android對動態庫so的加載執行,然而在x86架構下,它不容許修改入口地址,即ehdr.e_entry位置,如so入口地址ehdr.e_entry ,否則就拒絕加載,直接崩掉……於是,我嘗試修改保存信息位置
1).我把源碼中base 和 length信息放在了ehdr.ident后8個字節中,測試還是會拒絕加載,然后使用 ehdr.e_shoff = base;
2). e_shnum 和 e_shstrndx 保存lenght(因為length是四個字節,而e_shnum 和 e_shstrndx均是兩個字節,所以需要同時占用e_shnum 和 e_shstrndx),測試時發現,雖然可以加載了,但是算出的section地址不對,造成加密的sectiong函數尋址錯誤,還是崩掉,最后證明這個修復方案行不通
b.直接寫死偏移(base)和length信息
既然無法正常保存偏移地址,那么我就嘗試手動寫死對應的參數,然后測試,結果發現還是會崩掉,和 a.2 中的情況一致,於是判斷這個方案行不通
c.解密動態算出偏移和length信息
根據加密過程動態算出找到加密的section地址,然后解密(可惜當時的代碼已經刪除了),最終的測試發現,在匹配字符串表時無法找到指定的節信息,很有可能x86在加載時改變了ELF位置信息,所以最終也是失敗啦
至此我就放棄了修復的想法,尋找其他方案,當然如果公司可以容忍不兼容那少數的幾台x86機器就可以采用這個方案,我咨詢過幾個朋友,他們采用這個方案的項目已經上線了……
2.對指定的函數進行加密
這個其實我也並沒有看明白,但是我嘗試可幾次都沒成功,這里附上鏈接,有心的哥們可以參考一下:http://www.cnblogs.com/lanrenxinxin/p/4962470.html
3.折中方案
我們如果無法容忍不兼容x86,有無法搞定2中方案,那只能自己想辦法了。直接寫明文key在mono中肯定不行,那么是不是可以把key變通一下存放在ehdr.e_shoff 或者其他位置呢,這樣的話除非破解者找到對應的賦值函數,否則也不大容易獲得key,具體思路:
1)假設key = fun(c);
2)把c存放到ehdr.e_shoff;
3)在mono加載之前找到 ehdr.e_shoff,並計算出根據fun(c)計算出key
4)緩存key,就可以繼續解密Dll了
那么這個方案是否完備,答案肯定是no,以上沒有完備的方案,只要破解者找到你的解密處的函數就可以反向獲得key,重新破解Dll,但是相對寫明文來說可能是一個折中的方案,下面貼出參考代碼:
加密libmono.so的代碼
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
/* 32-bit ELF base types. */
typedef unsigned int Elf32_Addr;
typedef unsigned short Elf32_Half;
typedef unsigned int Elf32_Off;
typedef signed int Elf32_Sword;
typedef unsigned int Elf32_Word;
#define EI_NIDENT 16
/*
* ELF header.
*/
typedef struct {
unsigned char e_ident[EI_NIDENT]; /* File identification. */
Elf32_Half e_type; /* File type. */
Elf32_Half e_machine; /* Machine architecture. */
Elf32_Word e_version; /* ELF format version. */
Elf32_Addr e_entry; /* Entry point. 4 byte int */
Elf32_Off e_phoff; /* Program header file offset. */
Elf32_Off e_shoff; /* Section header file offset. 4 byte int */
Elf32_Word e_flags; /* Architecture-specific flags. */
Elf32_Half e_ehsize; /* Size of ELF header in bytes. */
Elf32_Half e_phentsize; /* Size of program header entry. */
Elf32_Half e_phnum; /* Number of program header entries. */
Elf32_Half e_shentsize; /* Size of section header entry. */
Elf32_Half e_shnum; /* Number of section header entries. */
Elf32_Half e_shstrndx; /* Section name strings section. */
} Elf32_Ehdr;
/*
* Section header.
*/
typedef struct {
Elf32_Word sh_name; /* Section name (index into the
section header string table). */
Elf32_Word sh_type; /* Section type. */
Elf32_Word sh_flags; /* Section flags. */
Elf32_Addr sh_addr; /* Address in memory image. */
Elf32_Off sh_offset; /* Offset in file. */
Elf32_Word sh_size; /* Size in bytes. */
Elf32_Word sh_link; /* Index of a related section. */
Elf32_Word sh_info; /* Depends on section type. */
Elf32_Word sh_addralign; /* Alignment in bytes. */
Elf32_Word sh_entsize; /* Size of each entry in section. */
} Elf32_Shdr;
int main(int argc, char** argv){
Elf32_Ehdr ehdr;
Elf32_Ehdr _ehdr;
unsigned int key = xxxx;//決定key的因子
int i;
int fd;
if(argc < 2){
puts("Input .so file");
return -1;
}
fd = open(argv[1], O_RDWR);
if(fd < 0){
printf("open %s failed\n", argv[1]);
goto _error;
}
//讀取ELF文件頭(mono.so 52個字節)
if(read(fd, &ehdr, sizeof(Elf32_Ehdr)) != sizeof(Elf32_Ehdr)){
puts("Read ELF header error");
goto _error;
}
ehdr.e_shoff = key;
//覆蓋新ELF文件頭
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, 0, SEEK_SET);
read(fd, &_ehdr, sizeof(Elf32_Ehdr));
printf("Write Key : %d \n", _ehdr.e_shoff);
puts("Completed");
_error:
close(fd);
return 0;
}
mono中解密代碼:
//SO---------------加密----------------------
#include <sys/types.h>
#include <elf.h>
#include <sys/mman.h>
unsigned int encrypt_key = 456987;
void mono_trace_free_tree() __attribute__((constructor));
unsigned long getLibAddr();
int getKey();
int getKey(){
return luta_encrypt_key;
}
void mono_trace_free_tree(){
g_message("mono:============= print Elf Start =============\n");
unsigned long base;
Elf32_Ehdr *ehdr;
base = getLibAddr();
ehdr = (Elf32_Ehdr *)base;
unsigned int temp_key = ehdr->e_shoff;
encrypt_key = fun(temp_key);
g_message("mono: Find luta_encrypt_key = %d\n",encrypt_key);
g_message("mono: ============= print Elf End =============\n");
}
unsigned long getLibAddr(){
unsigned long ret = 0;
char name[] = "libmono.so";
char buf[4096], *temp;
int pid;
FILE *fp;
pid = getpid();
sprintf(buf, "/proc/%d/maps", pid);
fp = fopen(buf, "r");
if(fp == NULL)
{
g_message("mono: 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;
}
//SO---------------加密----------------------
至此方案3的加密方案接結束,如果不多ELF文件有一定了解,恐怕很難完成這個內容……
4.其他方案
1)其實一些做加密的服務很多都對加密so有支持,然而都是付費的,sadly……,如果公司有錢可以考慮類似“愛加密”等加密服務
2)我們可以把獲得key和加密函數抽離出來,單獨做成decrypt.so,對其進行加密,然后在libmono.so加載前在android層解密並加載decrypt.so,還可以對android層代碼混淆等,相當於多做幾層防護,加大破解難度。
最后
加密Dll這件事其實還是無法做到絕對完備,只能加大破解難度,如果有問題請留言
