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

最近研究了一下梦幻西游手游版的资源打包方式其中就用到了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_MOVE025
OP_LOADK119
OP_LOADBOOL29
OP_LOADNIL30
OP_GETUPVAL422
OP_GETGLOBAL528
OP_GETTABLE620
OP_SETGLOBAL726
OP_SETUPVAL830
OP_SETTABLE915
OP_NEWTABLE105
OP_SELF1127
OP_ADD1233
OP_SUB131
OP_MUL1429
OP_DIV1511
OP_MOD1613
OP_POW1723
OP_UNM182
OP_NOT1931
OP_LEN206
OP_CONCAT2134
OP_JMP2235
OP_EQ2336
OP_LT2417
OP_LE257
OP_TEST2616
OP_TESTSET274
OP_CALL2821
OP_TAILCALL2918
OP_RETURN3012
OP_FORLOOP3114
OP_FORPREP3210
OP_TFORLOOP3324
OP_SETLIST348
OP_CLOSE3532
OP_CLOSURE363
OP_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);
    }
}

//初始化LRC4
private 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;
}

//解密LRC4
private 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 off
path %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 off

path %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

评论 38
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值