• QQ
  • nahooten@sina.com
  • 常州市九洲新世界花苑15-2

游戏开发

Unity防破解 —— 加密Dll与Key保护

原创内容,转载请注明原文网址:http://homeqin.cn/a/wenzhangboke/jishutiandi/youxikaifa/2018/1105/166.html

Unity防破解 —— 加密Dll与Key保护
在阅读这篇文章之前,我在处理mono加密问题时,也是参考了雨凇的文章,所以我们常州手机App游戏开发培训专家幻天网络建议先看一下雨凇写的关于加密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.homeqin.cn/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.homeqin.cn/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这件事其实还是无法做到绝对完备,只能加大破解难度,如果有问题请留言



上篇:上一篇:Unity防破解 —— 重新编译mono
下篇:下一篇:Unity3D之点击拾取功能实现