【手游】梦幻西游手游 美术资源加密分析
来源:互联网 发布:手机海外电视直播软件 编辑:程序博客网 时间:2024/04/30 11:05
最近研究了一下梦幻西游手游版的资源打包方式其中就用到了Hash表索引
0x00 先看看梦幻西游手游的资源目录
┌─HashRes
┊ ├─00
┊ ├─000000
┊ ├─ ┊
┊ ├─FFFFFF
┊ ├─01
┊ ├─┊
┊ ├─FF
HashRes中的文件夹名称和子文件名组合成了一个4字节的Hash名,如下图反编译so文件后看到的寻找资源的Hash路径
由于Hash不可逆,且没有资源文件名单,文件名就用hash表示吧
0x01 在HashRes文件夹里,通过对文件头的分析主要的有大致几类文件,其它类型可以忽略
&&__sign_of_g18_enc__@@(加密的图片文件,有的用了Lzma压缩)
L:grxx__sign_of_g18_enc__(加密的luac脚本文件,有的用了Gzip压缩)
__sign_of_g18_enc__(加密的luac脚本文件)
LuaQ(luac脚本文件)
FSB5(音频文件)
JSON (Json文件)
XML (xml配置文件)
手游版的资源和网页版的资源相似,略有不同但加密方式是相同的
0x02 先分析图片是怎样加载的,在IDA中反汇编中定位到cocos2d::Image::initWithImageData()这个方法
在解密后,紧接着判断了该图片是否压缩了,如果压缩了就解压缩,ccz和gzip是coocs2dx源码中本来就有的,lzma是梦幻后加的
继续分析,解压后先通过cocos2d::Image::detectFormat()这个方法判断是什么类型的图片,然后根据类型加载这个图片
整个图片资源的加载流程是
0x03 lua的加载比图片加载稍微复杂一丢丢,定位到cocos2d::LuaStack::luaLoadBuffer()这个方法
首先注意的是strncmp ( const char * str1, const char * str2, size_t n )的返回值:若str1与str2的前n个字符相同,则返回0
lrc4这个类就是解密lua的
整个lua资源的加载流程是
上面这个图左侧部分是指 L:grxx__sign_of_g18_enc__ 右侧部分是指 __sign_of_g18_enc__
最终得到的lua资源文件为lua编译过的二进制文件,并不是lua源码,想得到源码就得反编译luac文件。
0x04 在反编译luac之前,先来分析下lua5.1.4(梦幻西游手游用的是这个版本)源码中是如何加载的
lua5.1.4 源码下载地址 http://www.lua.org/ftp/
通过分析源码可知
#define LUA_SIGNATURE "\033Lua" 33(八进制) = 0x1B(十六进制)
在f_parser这个方法中 通过判断lua文件的第一个字节是否为LUA_SIGNATURE[0]也就是0x1B
若是0x1B那么读取的数据是Binary(二进制luac) 调用luaU_undump,否则为Text(源码) 调用luaY_parser,它们最终都会返回一个Proto*类型。
下面分析一下lua5.1 二进制格式 由两部分组成:头部块和顶层函数
头部块包含12字节
头部签名4字节0x1B 0x4C 0x75 0x61版本号1字节0x51 (高十六位是主版本号,低十六位是次版本号)版本格式1字节0x00 (0=官方版本)字节序标志1字节0x01 (默认为1 1=大端 0=小端)int大小1字节0x04 (默认为4 单位为字节)size_t大小1字节0x04 (默认为4 单位为字节)Instruction大小1字节0x04 (默认为4 单位为字节)lua_Number大小1字节0x08 (默认为8 单位为字节)整数标志1字节0x00 (默认为0 0=浮点数 1=整数)顶层函数(持有函数的所有相关数据 关于列表的详细信息这里就不展示了)
源代码名称长度(size_t)4字节例如 0x08 0x00 0x00 0x00 长度为8源代码名称size_t字节例如 0x40 0x64 0x62 0x2E 0x6C 0x75 0x61 0x00 以0x00结尾定义开始行(int)4字节0x00 0x00 0x00 0x00 (主代码块默认为 0)定义结束行(int)4字节0x00 0x00 0x00 0x00 (主代码块默认为 0)upvalue数量1字节0x00 (主代码块默认为 0)参数数量1字节0x00 (主代码块默认为 0)is_varagr标志1字节1=VARARG_HASARG 2=VARARG_ISVARARG 4=VARARG_NEEDSARG最大栈尺寸1字节使用的寄存器数量指令列表 [指令大小] [虚拟机指令]常量列表 [常量大小] [常量类型 常量值]函数原型列表 [函数原型列表大小] [函数原型数据]源码位置列表 [源码位置列表大小] [表索引对应指令位置] 可选的调试数据局部变量列表 [局部变量列表大小] [局部变量名 作用域起点 作用域终点] 可选的调试数据upvalue列表 [upvalue列表大小] [upvalue的名字] 可选的调试数据
关于luac的反编译工具,网上开源的代码有
luadec51 (C++) 下载地址 https://github.com/sztupy/luadec51 (有进行变量分析,但少了很多模式匹配,很容易出错)
luadec (C++) 下载地址 https://github.com/viruscamp/luadec (属于luadec51的分支)
unluac (Java) 下载地址 https://github.com/viruscamp/unluac (当程序有调试符号时,它是最好的选择,但它并没有进行变量分析)
LuaAssemblyTools (lua) 下载地址 https://github.com/mlnlover11/LuaAssemblyTools
一般的luac文件反编译工作到此就结束了,可梦幻西游手游的luac文件不是一般的luac,直接用上面的工具肯定会报错
这是因为梦幻西游手游版修改了lua虚拟机中的opcode(字节码)
lua5.1.4梦幻西游OP_MOVE025OP_LOADK119OP_LOADBOOL29OP_LOADNIL30OP_GETUPVAL422OP_GETGLOBAL528OP_GETTABLE620OP_SETGLOBAL726OP_SETUPVAL830OP_SETTABLE915OP_NEWTABLE105OP_SELF1127OP_ADD1233OP_SUB131OP_MUL1429OP_DIV1511OP_MOD1613OP_POW1723OP_UNM182OP_NOT1931OP_LEN206OP_CONCAT2134OP_JMP2235OP_EQ2336OP_LT2417OP_LE257OP_TEST2616OP_TESTSET274OP_CALL2821OP_TAILCALL2918OP_RETURN3012OP_FORLOOP3114OP_FORPREP3210OP_TFORLOOP3324OP_SETLIST348OP_CLOSE3532OP_CLOSURE363OP_VARARG3737至于什么是lua虚拟机的opcode 自己百度谷歌吧 我就不讲解了...
但是如何在IDA中寻找opcode可以和大家分享一下
第一种:通过上面对lua_load的分析,在IDA中直接定位lua_load然后一直跟到f_parser进入luaU_undump→LoadFunction→luaG_checkcode→symbexec,在symbexec中有个switch的循环里面有部分的opcode,通过和源码中的逻辑比对找出对应的opcode
第二中:在lua源码lvm.c中有个luaV_execute方法,其中的switch的循环里面有所有所对应的opcode。可以通过lua_call→luaD_call→luaV_execute定位该方法,通过和源码中的逻辑比对找出对应的opcode
建议第一种和第二种一起使用
若大家有更好的方法,欢迎分享,可以在评论中回复
最后把反编译源代码中默认的opcode顺序修改成得到的opcode顺序,然后编译工具。(注意luadec或luadec51还要修改lua源码lopcodes.c中luaP_opmodes里面的顺序 )
0x05 根据上面的分析后,我用C#写了个提取工具,这里只给出关键代码
提取和回写流程逻辑片段
public void FindFile(string dirPath, OperationType type) //参数dirPath为指定的目录 { DirectoryInfo Dir = new DirectoryInfo(dirPath); try { //查找子目录 foreach (DirectoryInfo d in Dir.GetDirectories()) { FindFile(Dir + "\\" + d.ToString(), type); } //查找文件 foreach (FileInfo f in Dir.GetFiles("*.*")) { if (type == OperationType.Decrypt) //解密资源 ReadRes(f); else if (type == OperationType.Encrypt) //回写资源 ExportRes(f); else return; } } catch (Exception ex) { MessageBox.Show(ex.Message); }}//写入资源文件private void ExportRes(FileInfo f){ FileStream inStream = new FileStream(f.FullName, FileMode.Open, FileAccess.ReadWrite); byte[] bytes = new byte[inStream.Length]; inStream.Read(bytes, 0, bytes.Length); inStream.Close(); if (FileFormat.IsLUAQ(bytes)) { byte[] gzipBytes = Compress.GzipCompress(bytes); //Gzip压缩 byte[] lrcBytes = LRC4_S(gzipBytes); //LRC4加密 byte[] headBytes = Encoding.Default.GetBytes("L:grxx__sign_of_g18_enc__"); byte[] resBytes = new byte[headBytes.Length + lrcBytes.Length]; Array.Copy(headBytes, 0, resBytes, 0, headBytes.Length); Array.Copy(lrcBytes, 0, resBytes, headBytes.Length, lrcBytes.Length); OutResFile(f, resBytes, string.Empty); }}//提取资源文件private void ReadRes(FileInfo f){ FileStream inStream = new FileStream(f.FullName, FileMode.Open, FileAccess.ReadWrite); byte[] resBytes; byte[] bytes = new byte[inStream.Length]; inStream.Read(bytes, 0, bytes.Length); inStream.Close(); if (Encoding.Default.GetString(bytes).Contains("&&__sign_of_g18_enc__@@")) { byte[] outBytes; ImageResDecrypt(bytes, out outBytes); if (Encoding.Default.GetString(outBytes).Contains("LZMA")) { byte[] targetBytes = new byte[outBytes.Length - 4]; Array.Copy(outBytes, 4, targetBytes, 0, outBytes.Length - 4); resBytes = CheckCompress(targetBytes); } else { resBytes = CheckCompress(outBytes); } } else if (Encoding.Default.GetString(bytes).Contains("L:grxx")) { byte[] targetBytes = new byte[bytes.Length - 25]; Array.Copy(bytes, 25, targetBytes, 0, bytes.Length - 25); resBytes = CheckCompress(LRC4_S(targetBytes)); } else if (Encoding.Default.GetString(bytes).Contains("__sign_of_g18_enc__")) { byte[] targetBytes = new byte[bytes.Length - 19]; Array.Copy(bytes, 19, targetBytes, 0, bytes.Length - 19); resBytes = CheckCompress(LRC4_S(targetBytes)); } else { resBytes = bytes; } OutResFile(f, resBytes, FileFormat.GetExtension(resBytes));}//检测是否压缩private byte[] CheckCompress(byte[] bytes){ if (FileFormat.CheckFormat(bytes) == FileType.LZMA) return Compress.LZMADecompress(bytes); else if (FileFormat.CheckFormat(bytes) == FileType.GZip) return Compress.GzipDecompress(bytes); else return bytes;}//保存文件private void OutResFile(FileInfo f, byte[] bytes, string extension){ string outPath = Path.Combine(f.DirectoryName, Path.GetFileNameWithoutExtension(f.FullName) + extension); using (FileStream outStream = new FileStream(outPath, FileMode.OpenOrCreate, FileAccess.ReadWrite)) { outStream.Seek(0, SeekOrigin.Begin); outStream.Write(bytes, 0, bytes.Length); }}//图片资源解密算法private void ImageResDecrypt(byte[] sourceBytes, out byte[] resBytes){ byte[] targetBytes = new byte[sourceBytes.Length - 23]; Array.Copy(sourceBytes, 23, targetBytes, 0, sourceBytes.Length - 23); int length = targetBytes.Length < 128 ? targetBytes.Length : 128; for (int i = 0; i < length; i++) { targetBytes[i] = (byte)(targetBytes[i] ^ (i - 2)); } resBytes = targetBytes; if (Encoding.Default.GetString(targetBytes).Contains("&&__sign_of_g18_enc__@@")) { //递归是因为在ios版本中有的图片被重复加密了好几次 - -|| ImageResDecrypt(targetBytes, out resBytes); }}//初始化LRC4private byte[] LRC4(){ byte[] bytes = new byte[256]; int v1 = 0; for (int i = 0; i < 256; i++) { bytes[i] = (byte)i; } for (int i = 0; i < 256; i++) { v1 = (int)(v1 + bytes[i] + ((0x9E3779B9 ^ (i >> 2)) >> 8 * (i & 3))); byte[] b = BitConverter.GetBytes(v1); if (i != b[0]) { bytes[i] ^= bytes[b[0]]; bytes[b[0]] = (byte)(bytes[i] ^ bytes[b[0]]); bytes[i] ^= bytes[b[0]]; } } return bytes;}//解密LRC4private byte[] LRC4_S(byte[] bytes){ byte[] lrc = LRC4(); byte last = 0; for (int i = 0; i < bytes.Length; i++) { int index = (i + 1) % 256; byte[] v1 = BitConverter.GetBytes(lrc[index] + last); last = v1[0]; if (index != last) { lrc[index] = (byte)(lrc[index] ^ lrc[last]); byte v2 = (byte)(lrc[index] ^ lrc[last]); lrc[last] = v2; lrc[index] ^= v2; } byte[] v3 = BitConverter.GetBytes(lrc[last] + lrc[index]); bytes[i] = (byte)(lrc[v3[0]] ^ bytes[i]); } return bytes;}解压缩算法片段
//压缩Gzip文件public static byte[] GzipCompress(byte[] bytes){ byte[] result = null; using (MemoryStream inStream = new MemoryStream(bytes)) { using (MemoryStream outStream = new MemoryStream()) { using (GZipOutputStream gZipOutputStream = new GZipOutputStream(outStream)) { gZipOutputStream.Write(bytes, 0, bytes.Length); } result = outStream.ToArray(); } } return result;}//解压Gzip文件public static byte[] GzipDecompress(byte[] bytes){ byte[] result; using (MemoryStream inStream = new MemoryStream(bytes)) { using (GZipInputStream gZipInputStream = new GZipInputStream(inStream)) { using (MemoryStream outStream = new MemoryStream()) { byte[] array = new byte[4096]; int num; while ((num = gZipInputStream.Read(array, 0, array.Length)) != 0) { outStream.Write(array, 0, num); } result = outStream.ToArray(); } } } return result;}//解压LZMA文件public static byte[] LZMADecompress(byte[] bytes){ byte[] result; using (MemoryStream inStream = new MemoryStream(bytes)) { using (MemoryStream outStream = new MemoryStream()) { Decoder coder = new Decoder(); byte[] properties = new byte[5]; inStream.Read(properties, 0, 5); byte[] fileLengthBytes = new byte[8]; inStream.Read(fileLengthBytes, 0, 8); long fileLength = BitConverter.ToInt64(fileLengthBytes, 0); coder.SetDecoderProperties(properties); coder.Code(inStream, outStream, inStream.Length, fileLength, null); result = outStream.ToArray(); } } return result;}
注意在Android平台下纹理图片格式为PKM,iOS平台下纹理图片格式为PVR
FSB音频可以用FsbExtractor软件提取
PKM格式文件可以用Mali Texture Compression Tool软件中的etcpack.exe进行批处理转换成png
PVR格式文件可以用TexturePacker软件中的TexturePacker.exe(需要破解版)进行批处理转换成png
PKM转PNG(path路径改为自己的etcpack.exe所在路径)
@echo offpath %path%;"D:\Program Files\ARM\Mali Developer Tools\Mali Texture Compression Tool\bin"for /f "usebackq tokens=*" %%d in (`dir /s /b *.pkm`) do (etcpack.exe "%%d" . -f RGBA8 -ext PNG)pause
PVR转PNG(path路径改为自己的TexturePacker.exe所在路径)
@echo offpath %path%;"D:\Program Files\CodeAndWeb\TexturePacker\bin"for /f "usebackq tokens=*" %%d in (`dir /s /b *.pvr *.pvr.ccz *.pvr.gz`) do (TexturePacker.exe "%%d" --sheet "%%~dpnd.png" --data "%%~dpnd.plist" --opt RGBA8888 --allow-free-size --algorithm Basic --no-trim --dither-fs::需要翻转图片 就把下面的::去掉::NConvert.exe -out png -yflip "%%~dpnd.png")pause
需要TexturePacker.exe,NConvert.exe,etcpack.exe的可以去网盘下载
链接:http://pan.baidu.com/s/1eRKjsbg 密码: h332
资源提取工具下载
链接: http://pan.baidu.com/s/1bo8j1Rx 密码: f257
- 【手游】梦幻西游手游 美术资源加密分析
- 【手游】梦幻西游手游 美术资源加密分析
- 【手游】魔灵幻想 美术资源加密分析
- 【手游】有杀气童话 美术资源加密分析
- 【手游】皇室战争 Clash Royale 美术资源加密分析
- 【手游】童话大冒险 美术资源加密分析
- 【手游】封神大主宰 美术资源加密分析
- 【手游】少年西游记 美术资源加密分析
- 用靠谱助手多开玩梦幻西游手游
- 梦幻西游手游三界奇缘答题 文字解答
- 梦幻西游手游桌面版启动不了,报错
- [COPY] 《梦幻西游》手游服务器如何实现200万玩家同时在线?(技术篇)
- 2016年度中国手游报告:梦幻西游手游是最大赢家
- 2016年度中国手游报告:梦幻西游手游是最大赢家
- 迷你西游(手游)-邀请序列号
- 【网易公开日】《梦幻西游》手游服务器如何实现200万玩家同时在线?(技术篇)
- 《梦幻西游》手游人宠抗防修炼点修消耗表
- 腾讯Unity3D手游 dll加密分析
- Android5.0以上使用MediaProjection截图和录屏
- HID攻击之TEENSY实战
- C++,cout和std::cout的区别
- MYSQL数据库管理之权限管理
- 华为笔试题
- 【手游】梦幻西游手游 美术资源加密分析
- 经典算法——hihocoder#1014 : Trie树(字典树)
- Latex编辑技巧杂锦
- 一个完整的创建用户,创建表空间,授权建表过程
- 多选下拉控件multiselect使用小结
- 数据库面试题(更新中。。。)
- Cppcheck 1.54 C/C++静态代码分析工具
- Android 自定义ViewGroup 实战篇 -> 实现FlowLayout
- ArtifactdescriptorException: failed to read artifact for xxxxxx.