← 回到文章列表

FFxivARR SqPack 拆包记录 1

最近FFxiv的大陆服客户端正式发布 就突发奇想的能不能将客户端拆包 提取出一部分的数据 或者直接提取出中文素材 能让国际服调用这样 所以就开始研究SqPack的拆包问题 这里主要会先分析下SqPack的文件结构 每个大包由一个index(地址)文件和dat0(数据)…

最近FFxiv的大陆服客户端正式发布
就突发奇想的能不能将客户端拆包
提取出一部分的数据
或者直接提取出中文素材
能让国际服调用这样
所以就开始研究SqPack的拆包问题


这里主要会先分析下SqPack的文件结构
每个大包由一个index(地址)文件和dat0(数据)文件组成
每个文件的开头可以看到明码的SqPack打包印记
在index文件中可以找到个别规律
用心找找就可以找到对应的地址码
从而找到地步的文件列表
看起来index文件的开始行数在0x400地址
之前的数据大概都是头说明以及一部分的sha1
这里先将如何从index的头找到下面对应的文件列表吧
先是从0x400开始 我们从0a0000.win32.index文件入手
可以看到内容如下

000400: 00  04  00  00  01  00  00  00  00  08  00  00  f0  e3  00  00  
000410: 2e  ec  33  ea  09  e7  e4  be  24  07  a4  8a  88  ae  0f  5d  
000420: 80  cf  2c  ed  00  00  00  00  00  00  00  00  00  00  00  00

然后就开始臆测文件数据含义

0x400-0x403 未知(可能是文件頭條數)
0x404-0x407 未知
0x408-0x40b 可能是文件的地址(經驗證的確是)
0x40c-0x40f 可能是文件列表的長度
0x410-0x423 文件sha1散列 20字節長度

然后从0x450开始又看到数据
按照之前的规律解析发现文件少了4字节头
但是后面又是符合规则的
臆测第一部分有一部分未知数据去除
然后从0x49c开始又发现数据
但是发现之后的文件信息相较第一部分信息
都少了8个字节头
也就是两个Unknow
ok 这样的话就可以找到指针规律了
然后就开始写代码解析下头吧~

    static String filePath = "0a0000.win32.index";
    public static void main(String[] args) throws Exception {
        //數據量比較大 使用raf容易定位操作
        RandomAccessFile randomAccessFile = new RandomAccessFile(filePath, "r");
        long startPoint = 0x400;
        byte[] hexBytes;
        // 從sha1Hex位置判斷此列是否生效
        randomAccessFile.seek(startPoint + 20);
        while (randomAccessFile.readByte() != 0) {
            // 此列有效 進行讀取
            int length = getIndexSegmentCount(randomAccessFile,startPoint);
            // 設置相符的數組長度
            hexBytes = new byte[length];
            // 進行數據的轉儲
            randomAccessFile.seek(startPoint);
            for (int i = 0; i < length; i++) {
                hexBytes[i] = randomAccessFile.readByte();
            }
            //數據處理出口
            getFileSegmentLine(getIndexSegment(hexBytes),randomAccessFile);
            // 非常重要 偏移指針 進入下壹個列表項
            startPoint = 44 + length + startPoint;
            randomAccessFile.seek(startPoint + 20);
        }
        randomAccessFile.close();
    }
    private static void getFileSegmentLine(long[] ad,RandomAccessFile randomAccessFile) throws Exception{
        //按照理論來說 列表長度爲16的整數倍 每條數據16個字節 
        //所以就直接定位到數據開始部分 進行16個字節劃分
        randomAccessFile.seek(ad[0]);
        byte[] hexBytes = new byte[16];
        for(int j=0;j<ad[1]/16;j++){
            for(long i=0;i<16;i++){
                hexBytes[(int)i] = randomAccessFile.readByte();
            }
            //處理數據
            System.out.println(HexUtils.Bytes2HexString(hexBytes));
            getFIleSegment(hexBytes);
            //減少數據進行測試
//          break;
        }
    }
    private static int getIndexSegmentCount(RandomAccessFile randomAccessFile,long startPoint) throws Exception{
        // 獲取此列的長度樣式 暫時發現壹共三個樣式
        int length;
        randomAccessFile.seek(startPoint + 4 + 4 + 4 + 4 + 19);
        if (randomAccessFile.readByte() != 0) {
            length = 4 + 4 + 4 + 4 + 20;
        } else {
            randomAccessFile.seek(startPoint + 4 + 4 + 4 + 19);
            if (randomAccessFile.readByte() != 0) {
                length = 4 + 4 + 4 + 20;
            } else {
                randomAccessFile.seek(startPoint + 4 + 4 + 19);
                if (randomAccessFile.readByte() != 0) {
                    length = 4 + 4 + 20;
                } else {
                    length = 0;
                }
            }
        }
        return length;
    }
    private static long[] getIndexSegment(byte[] hex) {
        // 從byte[]提取文件列表起始地址以及長度
        // 生成long數組 長度爲2
        long l[] = { 0, 0 };
        // 臨時容器
        byte[] tmp = new byte[8];
        int co = 0;
        // 獲取數組長度 兼容各種類型
        int length = hex.length;
        // sha1字符串長度爲20 所以可以截取到sha1之前的8位有效值
        // 前四位是文件列表初始地址 後四位是文件列表的長度
        for (int i = length - 25; i > length - 25 - 4; i--) {
            // 地址
            tmp[co++] = hex[i];
        }
        for (int i = length - 21; i > length - 21 - 4; i--) {
            // 長度
            tmp[co++] = hex[i];
        }
        l[0] = Long.parseLong(HexUtils.Bytes2HexString(tmp).replace(" ", "")
                .substring(0, 8), 16);
        l[1] = Long.parseLong(HexUtils.Bytes2HexString(tmp).replace(" ", "")
                .substring(8), 16);
        return l;
        // 返回的long數組前地址後長度
    }


测试运行结果:

60 68 0E 00 0F 71 8E 08 30 76 0A 00 00 00 00 00 
4D 75 27 01 0F 71 8E 08 80 5C 0A 00 00 00 00 00 
26 20 E3 05 0F 71 8E 08 00 60 0A 00 00 00 00 00 
05 56 42 06 0F 71 8E 08 00 E1 0A 00 00 00 00 00 

顺带放出HexUtils的代码
是自己写的为了容易测试使用

private final static byte[] hex = "0123456789ABCDEF".getBytes();  
private static int parse(char c) {  
    if (c >= 'a')  
        return (c - 'a' + 10) & 0x0f;  
    if (c >= 'A')  
        return (c - 'A' + 10) & 0x0f;  
    return (c - '0') & 0x0f;  
}  
// 從字節數組到十六進制字符串轉換  
public static String Bytes2HexString(byte[] b) {  
    byte[] buff = new byte[3 * b.length];  
    for (int i = 0; i < b.length; i++) {  
        buff[3 * i] = hex[(b[i] >> 4) & 0x0f];  
        buff[3 * i + 1] = hex[b[i] & 0x0f];  
        buff[3 * i + 2] = 45;  
    }  
    String re = new String(buff);  
    return re.replace("-", " ");  
}   

在这里找找

输入关键词,找一篇过去的记录。