Unity防破解 —— 加密Dll與Key保護

    在閱讀這篇文章以前,我在處理mono加密問題時,也是參考了雨凇的文章,因此建議先看一下雨凇寫的關於加密Dll的文章:html

1.Unity3D研究院之Android加密DLL與破解DLL .SOandroid

2.Unity3D研究院之Android二次加密.so二次加密DLLgit

僞裝讀者已經看過上面的兩篇文章了,下面我會記錄一下我作的整個加密流程。github

一.選取加密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的入口函數,在這裏實現解密。eclipse

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);
}

到此解密和加密過程就結束了,ide

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.htmlhttp://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這件事其實仍是沒法作到絕對完備,只能加大破解難度,若是有問題請留言

相關文章
相關標籤/搜索