首页 >Unity3D频道 >【Unity3D研究院之游戏开发】 > Unity3D研究院之Android二次加密.so二次加密DLL(八十二)
2015
07-17

Unity3D研究院之Android二次加密.so二次加密DLL(八十二)

上文中说了怎么给DLL加密来防止别人反编译你的C#代码。 Unity3D研究院之Android加密DLL与破解DLL .SO(八十一) 文章的最后我们发现IDA PRO神器可以解开libmono从而查到你的解密算法,这样你的C#代码又会被别人轻易的拿到。

这两天我就一直在寻找怎样才能更好的保护代码。终于找到了加密so的办法,此法我觉得防小白觉对够用。大神恐怕还是能解开,但是我觉得这就够了。我已经在项目中测试通过,也欢迎大家也能加入一起来测试的队伍。

在啰嗦一句在不远的将来可能我们也不用这么做了, 因为很快unity就全线l2cpp了。但是我觉得等真正稳定恐怕还有很多路要走,所以估计大部分正在开发的项目不会冒这个险升级。

阅读下面之前请大家先看一下这篇大神的文章。http://bbs.pediy.com/showthread.php?t=191649 文章写的很清晰。但是坦白说看了半天我没怎么看懂,逆向工程真是一门深奥的学问。。主要还是技术关注领域不在这里。文章的最后有作者给出的源码,大家记得下载下来。然后我就开始说我是怎么把这个加在unity3d上的。还有我遇到了那些坑。

它的例子工程下载解压后,开始对shellAdder1.c进行编译,编译的方法是

gcc -o encry shellAdder1.c

我开始编译的时候老通不过,提示缺少 elf.h 文件,我看了一下,其实就是少了一些结构体和类型的声明,把下面代码拷贝到shellAdder1.c里面即可。Main函数上面添加如下代码。

#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. */
  Elf32_Off  e_phoff;  /* Program header file offset. */
  Elf32_Off  e_shoff;  /* Section header file offset. */
  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;

最终shellAdder1将编译成一个名叫encry的可执行文件, 用来给libmono进行加密。那么加密算法必然是要写在shellAdder1.c里面,作者给出的是取反你也可以改成自己需要的算法。至于c代码是什么意思,我相信 这篇文章已经写的是非常的全面了 http://0nly3nd.sinaapp.com/?p=695

然后执行 encry libmono.so 就会把libmono.so里 名叫 mytext 的断 进行加密,你要觉得这个名子不好也可以换一个断名,加密后的libmono.so文件会替换原有的。

接着到mono/metadata/image.c里来编写解密.so断的代码。把下面这段代码拷贝到image.c的最上面,关键的两个地方我已添加注释了。

//SO---------------加密----------------------

#include <sys/types.h>
#include <elf.h>
#include <sys/mman.h>

//注意上面说解密算法里面的断.mytext就是这里,
//这里把getKey进行了加密,这样对方拿不到你的密钥都没法破解你的dll了
int getKey() __attribute__((section (".mytext")));
int getKey(){
	return 2048;
};
//这里就是.so初始化的时候,这里进行mytext断的解密工作
void init_getKey() __attribute__((constructor));
unsigned long getLibAddr();

void init_getKey(){
  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;
  
  g_message("momo: nblock = %dn", nblock);
  

  if(mprotect((void *) base, 4096 * nsize, PROT_READ | PROT_EXEC | PROT_WRITE) != 0){
    g_message("momo: 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){
    g_message("momo: mem privilege change failed");
  }
  g_message("momo: Decrypt success");
}

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("momo: 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---------------加密----------------------

然后在mono_image_open_from_data_with_name方法里

	if(strstr(name,"Assembly-CSharp.dll")){
                //这里就能取到密钥,那么这个函数被加密了。
                //IDA就看不到它了
		g_message("momo: key = %dn", getKey());
	}

密钥被保护了,代码修改完就是开始编译mono吧。编译完用刚刚我们说过的方法来执行 encry libmono.so  然后把libmono拷贝到项目里打包android就行了。

可以测试一下加密的效果。用Ida 打开。这里的函数已经打不开了

{9A009440-D13E-4922-8528-865191277727}

这段密钥进行了保护那么就可以随意的做加密算法了。

{1F55F876-04DF-4E9C-84D8-317093035606}

 

我相信这个方法还是存在漏洞,肯定也有大神能破解。也希望各位大神不吝赐教,谢谢啦。使用上有问题欢迎在下面留言大家可以一起讨论。

 

shellAdder1.c 很多人都问我要我还是都放出来吧。

#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. */
  Elf32_Off  e_phoff;  /* Program header file offset. */
  Elf32_Off  e_shoff;  /* Section header file offset. */
  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){
  char target_section[] = ".mytext";
  char *shstr = NULL;
  char *content = NULL;
  Elf32_Ehdr ehdr;
  Elf32_Shdr shdr;
  int i;
  unsigned int base, length;
  unsigned short nblock;
  unsigned short nsize;
  unsigned char block_size = 16;
  
  int fd;
  
  if(argc < 2){
    puts("Input .so file");
    return -1;
  }
  
  fd = open(argv[1], O_RDWR);
  if(fd < 0){
    printf("open %s failedn", 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);
  
  if(read(fd, &shdr, sizeof(Elf32_Shdr)) != sizeof(Elf32_Shdr)){
    puts("Read ELF section string table error");
    goto _error;
  }
  
  if((shstr = (char *) malloc(shdr.sh_size)) == NULL){
    puts("Malloc space for section string table failed");
    goto _error;
  }
  
  lseek(fd, shdr.sh_offset, SEEK_SET);
  if(read(fd, shstr, shdr.sh_size) != shdr.sh_size){
    puts("Read string table failed");
    goto _error;
  }
  
  lseek(fd, ehdr.e_shoff, SEEK_SET);
  for(i = 0; i < ehdr.e_shnum; i++){
    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){
      base = shdr.sh_offset;
      length = shdr.sh_size;
      printf("Find section %sn", target_section);
      break;
    }
  }
  
  lseek(fd, base, SEEK_SET);
  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 = %dn", base, length);
  printf("nblock = %d, nsize = %dn", nblock, nsize);
  
  ehdr.e_entry = (length << 16) + nsize;
  ehdr.e_shoff = base;
  
  
  
  for(i=0;i<length;i++){
    content[i] = ~content[i];
  }
  
  
  
  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;
  }
  
  
  puts("Completed");
_error:
  free(content);
  free(shstr);
  close(fd);
  return 0;
}

最近有朋友说在Android7.0上遇到这个错误,今天我抽空尝试的解决了一下。

Unable to load library:xxx/arm/libunity.so [dlopen failed: “xxx/arm/libmono.so” .dynamic section header was not found]

测试环境unity4.7.2  并且在github上取下mono 对应的unity4.6(我看前几天刚更新了一些代码)的最新代码编译方法和文中介绍的一样。唯一有一点需要注意,就是NDK编译的时候要用android-ndk-r10e 或者更高版本。 (我测试通过用的就是r10e)

另外,如果你的项目是unity5.x我建议升级到5.4 android使用il2cpp这样就不需要这个方法加密了。如果是unity4.x的项目我建议用r10e在尝试编译一下,测试的过程中有问题欢迎在下面给我留言。

最后编辑:
作者:雨松MOMO
专注移动互联网,Unity3D游戏开发
捐 赠写博客不易,如果您想请我喝一杯星巴克的话?就进来看吧!

《Unity3D研究院之Android二次加密.so二次加密DLL(八十二)》有 85 条评论

  1. 另一种方法破解 看哪里调用 mono_image_open_from_data_with_name 分析 unity.so文件中也可 拿到解密后的dll

  2. 用了这个方法加密之后,有些设备无法启动,报错
    Fatal signal 11 (SIGSEGV) at 0x00000000 (code=1), thread 28080

  3. 杜三狗说:

    不加解密so断的代码能正常编译出来,加了那段就出错了。。。您知道怎么回事吗?

  4. 雨秋鱼说:

    感谢雨松大大,已跑通整个流程。
    但是我发现加密的那个getkey方法只是让解密秘钥隐藏了而已,解密过程还是暴露了出来;这样只要通过穷举法不是还能破解么?
    为什么不直接把解密过程放到那个section段里呢?应该会大大增加破解难度吧!
    我尝试了一下,把getkey方法改造成传一个参数进去,返回解密后的数据;但是由于对C和编译器相关知识的缺乏,编译mono.so失败了;
    想请教下,要把getkey方法加上参数,还需要改动哪些地方?

  5. 玄武说:

    encry libmono.so
    armv7a 输出base =1 length =0 nblock =0, nsize = 1
    x86 输出base = 2632460, length = 10,nblock = 0, nsize = 643
    雨凇大大这是正常的么?

  6. 关彪行说:

    雨神,这个mono/metadata/image.c东西在哪呢?我下载的是shelldemo东西啊

  7. BlueEffie说:

    动态加载.so后,可直接dump分析

  8. 黄延说:

    请教下,dll自动加密是怎么实现的?ant吗?还是在unity里就能做?

  9. 楼主 用的ndk 是哪个版本啊 ndk r9c 编译通不过, 继续试

  10. 刚看到有防破解的方法,我是很欣喜的,但是看到要编译mono就望而却步了。虽然只是在加载DLL的入口添加了一小段自己的处理方法,但是我对编译编译器这样的事情还是信心不足。可不可以只是把核心方法放到native代码去做呢?我觉得可能代价会小很多吧。.net的反编译问题这么多年了,一直没能得到解决,这也是.net未能被客户端大规模使用的重要原因吧

  11. Dean说:

    直接hook mono_image_open_from_data_with_name 还是可以把dll提取出来的呀

  12. Rico说:

    HI,博主,你做的游戏叫什么名字啊?

  13. dkr说:

    博主,这篇是不是要配合前面一篇dll加密来做?如果只是libmono加密,dll不是还会被看到吗?是不是先dll加密,再libmono加密,这样没法看到加密算法,也就没法看到dll?

    • dll的加密算法是自己做的。。 解密算法在libmono里。 要保护好libmono解密的key,这里加密 libmono也就是为了保护好这个key

      • dkr说:

        多谢博主的及时回复!您说的key,是指getKey()这个函数返回的2048吗?本文提到的加密只是简单的取反,这么说是不需要key的吧,除非自己搞个需要key的加密算法把libmono加密?

        • 对, 就是把2048这个key 保存起来。 如果这里不加密, 别人可以用ida 解开你的.so 轻而易举破解你的dll

          • dkr说:

            再次多谢您的及时回复!所以完整的流程是不是这样:1.使用加密算法1(比如《八十一》中的data[0] +=1)给dll加密,密钥是key1;2.使用encry给libmono加密:就是将获取key的函数getKey放到特定的段(比如.mytext)后,采用加密算法2(比如本文中简单的取反)给段加密,密钥是key2;3.打包后运行时,libmono.so加载时,先调用对应的解密算法2解密段,获取key1,再使用key1,使用对应的解密算法1解密dll,最后加载正确的dll运行。那问题来了,现在只保护到了key1,如果key2被获取,加密算法2被破解,那么key1是不是也危险了?恳请博主指教。。。。。

          • 这样加密完, 我都不知道怎么破解。。ida pro 找不到init的方法。。 至少我没能找到。。。

          • dkr说:

            应该防不住动态调试。。。。

          • 恩, 应该吧。 不过我觉得做到这一步就差不多了。 至少90%能防住了。。

          • dkr说:

            嗯,本来安全攻防就是道高一尺魔高一丈的蛋疼事儿。。。很感谢博主详细的教程,让我避免很多坑,多谢多谢!

          • 大熊说:

            其实就是防君子的,对于真正想crack的人,这个很初级的。

          • dkr说:

            再次多谢您的及时回复!所以完整的流程是不是这样:1.使用加密算法1(比如《八十一》中的data[0] =1)给dll加密,密钥是key1;2.使用encry给libmono加密:就是将获取key的函数getKey放到特定的段(比如.mytext)后,采用加密算法2(比如本文中简单的取反)给段加密,密钥是key2;3.打包后运行时,libmono.so加载时,先调用对应的解密算法2解密段,获取key1,再使用key1,使用对应的解密算法1解密dll,最后加载正确的dll运行。那问题来了,现在只保护到了key1,如果key2被获取,加密算法2被破解,那么key1是不是也危险了?恳请博主指教。。。。。

  14. robin说:

    博主,您加密libmono.so后编译的apk能正常运行么

  15. robin说:

    有没有同学遇到,按着博主的方法做完,游戏打开就卡在主页面而没法动的?

  16. keenda说:

    请教一下,ipa里的dll拉进.NET Reflector 8.3中能看到方法体的内容,是不是xcode发布时还需要设置?(unity4.5.4)

    • 我记得之前我用的是4.2 还是4.3 IOS的方法体是看不到的。。 难道后面更新产生了这个问题 你解包一下腾讯的IOS游戏 看看它的DLL

      • andero说:

        看来MOMO对加密这块不是很了解啊,ios工程在设置中把strippinglevel开到strip byte code就会剥离掉dll里面的方法了。大部分人只知道用来减少包大小,其实他附带的作用就是把dll里面的方法剥掉,因为ipa运行的是native代码,dll里面的方法名只是个入口,所以里面的方法剥离掉也没关系。上次有人说ipa里面能看到,那就是因为他们自己没选对剥离等级。

    • 还有一点,要升级到4.6.2以后的版本才能上传appstore 。 以后都是iL2Cpp了。。 应该不需要防破解了

    • andero说:

      playersetting里面ios工程把strippinglevel开到strip byte code就会剥离掉dll里面的方法了。不过如果你们之前都没用过剥离,剥离可能会产生一些问题。因为剥离会剥离掉没有直接引用的系统类,所以当你的应用中通过反射来获得的系统类被剥离就会导致游戏崩溃。解决方法就是添加到link.xml里面,具体内容网上可以搜到。

  17. kun说:

    开拓了眼界~!

  18. Chiuan说:

    Unity3D研究院之Android二次加密.so二次加密DLL(八十二) - 雨松MOMO程序研究院 - 1 研究和总结收集很好。可以免去做这块的很多坑

  19. Roy说:

    已试用,太赞了! Unity3D研究院之Android二次加密.so二次加密DLL(八十二) - 雨松MOMO程序研究院 - 1

留下一个回复

你的email不会被公开。 必填项已用 * 标注。