上文中说了怎么给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进行编译,编译的方法是
1 |
gcc -o encry shellAdder1.c |
我开始编译的时候老通不过,提示缺少 elf.h 文件,我看了一下,其实就是少了一些结构体和类型的声明,把下面代码拷贝到shellAdder1.c里面即可。Main函数上面添加如下代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
#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的最上面,关键的两个地方我已添加注释了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
//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 = %d\n", 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方法里
1 2 3 4 5 |
if(strstr(name,"Assembly-CSharp.dll")){ //这里就能取到密钥,那么这个函数被加密了。 //IDA就看不到它了 g_message("momo: key = %d\n", getKey()); } |
密钥被保护了,代码修改完就是开始编译mono吧。编译完用刚刚我们说过的方法来执行 encry libmono.so 然后把libmono拷贝到项目里打包android就行了。
可以测试一下加密的效果。用Ida 打开。这里的函数已经打不开了
这段密钥进行了保护那么就可以随意的做加密算法了。
我相信这个方法还是存在漏洞,肯定也有大神能破解。也希望各位大神不吝赐教,谢谢啦。使用上有问题欢迎在下面留言大家可以一起讨论。
shellAdder1.c 很多人都问我要我还是都放出来吧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 |
#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 failed\n", 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 %s\n", 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 = %d\n", base, length); printf("nblock = %d, nsize = %d\n", 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在尝试编译一下,测试的过程中有问题欢迎在下面给我留言。
- 本文固定链接: https://www.xuanyusong.com/archives/3571
- 转载请注明: 雨松MOMO 于 雨松MOMO程序研究院 发表
android 7.1.1遇到shdr问题 java.lang.UnsatisfiedLinkError: dlopen failed: “/data/app/com.thomas.crackmeso-1/lib/arm/libverify.so” has invalid shdr offset/size: 12632/2160 at java.lang.Runtime.loadLibrary0(Runtime.java:989) at java.lang.System.loadLibrary(System.java:1530)大神有没有解决
不知道为什么刚刚发的回复被删了。我想问的是 getkey() 返回的是2048吗,2048 是不是要的dll 加密的值相同,我要把2048换成dll加密用的key这样理解对吗?我看到在image.c 下面的函数里面只有getkey().是否要写成 data[0] -= getkey()呢,是不是该这么理解?
7.0确实会闪退,只要用了encry,那么这个.so就不能用了,momo有研究吗
雨凇大大:这边发现一个问题,在Android 7.0 的设备中,会出现Unable to load library:xxx/arm/libunity.so [dlopen failed: “xxx/arm/libmono.so” .dynamic section header was not found] 问题, 请问你们有碰到吗
你可以取一下最新的mono在重新编译一下试试。我们 后来更新unity5.4用il2cpp 了。。
雨神,请问,用il2cpp 还会存在被反编译的节奏吗?
肯定会。。。 但是和之前的不太一样。。 增加了反编译的难度了。。
我也是使用unity5.4用il2cpp编译报错:Failed running C:/Program Files/android-ndk-r10e/toolchains/llvm-3.6/prebuilt/windows-x86_64/bin/clang++ -c “Temp/StagingAreaIl2Cppil2cppOutputBulk_Assembly-CSharp_0.cpp” -o大神知道这是什么问题吗?
我也是使用unity5.4用il2cpp编译报错:Failed running C:/Program Files/android-ndk-r10e/toolchains/llvm-3.6/prebuilt/windows-x86_64/bin/clang -c “Temp/StagingAreaIl2Cppil2cppOutputBulk_Assembly-CSharp_0.cpp” -o大神知道这是什么问题吗?
今天我尝试解决了一下这个问题,取下最新mono代码,然后用android-ndk-r10e 重新编译一下就OK了。
我测试了Android7.0,版本是unity5.4, ndk是r10e, 如果不用encry是可以的,如果加了就进入后闪退.所以暂时只能不encry .为什么5.4不用il2cpp,因为要热更,没办法使用它.
没用lua吗? 还有encry 加密是怎么写的, 我重新编译了没有问题啊。
现在正在做lua. encry用的就是你上面提供的代码. 我只是修改了text的值.我的报错,和下面canders的是一样的 .dynamic section header was not found. 我还专门下载了mono5.4进行修改的.我怀疑是encry之后.7.0就不认这个.so文件了
我也遇到了同样的问题,用的是最新的Unity4.6,ndk是r10e,重新编译libmono,在Android7.0还会闪退,报错日志:Unable to load library:xxx/arm/libunity.so [dlopen failed: “xxx/arm/libmono.so” .dynamic section header was not found]
可能我的测试环境比较简单吧? 但是我重新编译了加密了确实能运行,改天我找个复杂点的工程在试试吧。。
你试试不encry.看是不是就不报错了
不加密在Android7.0下是可以的
我也遇到了同样的问题,用的是最新的Unity4.6,ndk是r10e,重新编译libmono,在Android7.0还会闪退,报错日志:Unable to load library:xxx/arm/libunity.so [dlopen failed: “xxx/arm/libmono.so” .dynamic section header was not found]
另一种方法破解 看哪里调用 mono_image_open_from_data_with_name 分析 unity.so文件中也可 拿到解密后的dll
用了这个方法加密之后,有些设备无法启动,报错
Fatal signal 11 (SIGSEGV) at 0x00000000 (code=1), thread 28080
不加密就没问题
没遇到过这个问题啊。 我们现在两个商业上线项目就用的这个方法,没啥问题啊。
不加解密so断的代码能正常编译出来,加了那段就出错了。。。您知道怎么回事吗?
放到image.c的所有include下面就可以了。。。
应该是代码编译的问题。。仔细检查检查吧。
感谢雨松大大,已跑通整个流程。
但是我发现加密的那个getkey方法只是让解密秘钥隐藏了而已,解密过程还是暴露了出来;这样只要通过穷举法不是还能破解么?
为什么不直接把解密过程放到那个section段里呢?应该会大大增加破解难度吧!
我尝试了一下,把getkey方法改造成传一个参数进去,返回解密后的数据;但是由于对C和编译器相关知识的缺乏,编译mono.so失败了;
想请教下,要把getkey方法加上参数,还需要改动哪些地方?
是这样的, 如果对方用dump 动态方法 就能反编译了。。 坦白说我也不知道怎么防止dump 主要是我觉得也没必要做了。只要能防住大部分人就行了。
实现了把解密算法也封装进getkey里了!
能用dump 动态肯定没法防,我之前只是担心小白在知道了解密算法后会用尝试穷举方法来解密dll;
现在安心多了
[good]
encry libmono.so
armv7a 输出base =1 length =0 nblock =0, nsize = 1
x86 输出base = 2632460, length = 10,nblock = 0, nsize = 643
雨凇大大这是正常的么?
嗯,应该是正确的。
怎么调试查看key值是否正确呢?
g_message 会在编译时打印出日志么?
雨神,这个mono/metadata/image.c东西在哪呢?我下载的是shelldemo东西啊
请教下,dll自动加密是怎么实现的?ant吗?还是在unity里就能做?
ant unity里做不了做不了。
楼主 用的ndk 是哪个版本啊 ndk r9c 编译通不过, 继续试
是下载的mono 4.6 编译通不过啊
用的是ndk 9
刚看到有防破解的方法,我是很欣喜的,但是看到要编译mono就望而却步了。虽然只是在加载DLL的入口添加了一小段自己的处理方法,但是我对编译编译器这样的事情还是信心不足。可不可以只是把核心方法放到native代码去做呢?我觉得可能代价会小很多吧。.net的反编译问题这么多年了,一直没能得到解决,这也是.net未能被客户端大规模使用的重要原因吧
这个做起来其实并不难啊, 我们这边现在好几个项目都在用这个方法。。 还可以。。
直接hook mono_image_open_from_data_with_name 还是可以把dll提取出来的呀
是的。
没有绝对的加密,防防小白还可以
是啊, 目前我就是用他就防防小白。 我觉得最好unity能出完美的解决方案。。
HI,博主,你做的游戏叫什么名字啊?
不太方便透漏。
博主,这篇是不是要配合前面一篇dll加密来做?如果只是libmono加密,dll不是还会被看到吗?是不是先dll加密,再libmono加密,这样没法看到加密算法,也就没法看到dll?
dll的加密算法是自己做的。。 解密算法在libmono里。 要保护好libmono解密的key,这里加密 libmono也就是为了保护好这个key
多谢博主的及时回复!您说的key,是指getKey()这个函数返回的2048吗?本文提到的加密只是简单的取反,这么说是不需要key的吧,除非自己搞个需要key的加密算法把libmono加密?
对, 就是把2048这个key 保存起来。 如果这里不加密, 别人可以用ida 解开你的.so 轻而易举破解你的dll
再次多谢您的及时回复!所以完整的流程是不是这样: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的方法。。 至少我没能找到。。。
应该防不住动态调试。。。。
恩, 应该吧。 不过我觉得做到这一步就差不多了。 至少90%能防住了。。
嗯,本来安全攻防就是道高一尺魔高一丈的蛋疼事儿。。。很感谢博主详细的教程,让我避免很多坑,多谢多谢!
其实就是防君子的,对于真正想crack的人,这个很初级的。
再次多谢您的及时回复!所以完整的流程是不是这样: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是不是也危险了?恳请博主指教。。。。。
博主,您加密libmono.so后编译的apk能正常运行么
我在unity4.6.7和 unity5.1.上都测试通过了。。
有没有同学遇到,按着博主的方法做完,游戏打开就卡在主页面而没法动的?
没遇到这情况啊。 我们用这个做的一个项目最近刚上线测试也没发现大问题。。 上线用的版本是unity4.6.7
好奇怪。。。按着《 Unity3D研究院之Android加密DLL与破解DLL .SO(八十一)》这个做没问题,但是这篇就有问题,版本是4.5.3f3.。。。
4.5我没试过。。
搞定了,发现是自己犯了个很低价的错误。。。多谢博主的分享!
能说一下是怎么改的么?我也遇到和你一样的问题。
请教一下,ipa里的dll拉进.NET Reflector 8.3中能看到方法体的内容,是不是xcode发布时还需要设置?(unity4.5.4)
我记得之前我用的是4.2 还是4.3 IOS的方法体是看不到的。。 难道后面更新产生了这个问题 你解包一下腾讯的IOS游戏 看看它的DLL
看来MOMO对加密这块不是很了解啊,ios工程在设置中把strippinglevel开到strip byte code就会剥离掉dll里面的方法了。大部分人只知道用来减少包大小,其实他附带的作用就是把dll里面的方法剥掉,因为ipa运行的是native代码,dll里面的方法名只是个入口,所以里面的方法剥离掉也没关系。上次有人说ipa里面能看到,那就是因为他们自己没选对剥离等级。
还有一点,要升级到4.6.2以后的版本才能上传appstore 。 以后都是iL2Cpp了。。 应该不需要防破解了
谢谢,刚看了天天飞车的dll,的确没有方法体的内容。
playersetting里面ios工程把strippinglevel开到strip byte code就会剥离掉dll里面的方法了。不过如果你们之前都没用过剥离,剥离可能会产生一些问题。因为剥离会剥离掉没有直接引用的系统类,所以当你的应用中通过反射来获得的系统类被剥离就会导致游戏崩溃。解决方法就是添加到link.xml里面,具体内容网上可以搜到。
小金。。。
开拓了眼界~!
谢谢。。
已试用,太赞了!
文中的那个链接看雪学院的附件不能下载,你下载了吗,如果下载了可以给我一份吗