【手游】梦幻西游手游 美术资源加密分析

来源:互联网 发布:手机海外电视直播软件 编辑:程序博客网 时间: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

6 0
原创粉丝点击