Galgame汉化中的逆向 (一):文本加密(压缩)与解密


  • Galgame汉化中的逆向 (一):文本加密(压缩)与解密

    by devseed,

    0x0 前言

    看到关于游戏汉化相关的逆向教程挺少的,作为某汉化组的成员也帮过别的汉化组,于是就想把我见到的几个典型的例子整理分析一下,还是挺有意思的。此教程和我在贴吧和隔壁发的一样。

    0x1 观察与定位

    这个游戏还是很典型的,cpk封包,文本应该在sn.bin里面。
    打开一看没有明显的字符,而且数据看起来很紧凑,应该是压缩或者加密了。

    相比与主机游戏ida费劲的静态分析,pc动态调试真是太舒服了。
    找文本很简单,等游戏运行起来后直接暂停,搜索内存sjis字符串。比如说“椿子”。
    然后记住这个地址(或附近的某个),下硬断点write,重新启动游戏运行,游戏中断在这里。

    0x2 解密函数观察

    然后顺腾摸瓜,我们能看到了解密函数,只不过这个游戏奇怪,
    用了eax和ecx传参(之后分析这个应该是指向了全局变量),
    进一步分析发现eax应该是解密后的缓冲区,ecx是sn.bin文件缓冲区。
    这里提取解密后的文本可以直接memdump了,
    其实汉化游戏我们甚至可以不管它用了什么加密,直接hook这里然后替换为其他缓冲区。
    但是这样就没有分析的意义了,为了练习,我们还是要去分析算法。

    0x3 反汇编初步分析

    怎么进行反汇编?
    方便的办法是直接用ida的f5,但是这里我不想太依赖插件,就去直接看汇编代码了。
    如果写汇编程序不多的可以先用c语言写一个接近汇编的版本,测试结果正确后,
    然后再从这个版本中继续写一个接近于人写的程序(合并中间变量,改变量名),这样就方便逆向算法了。用接近汇编的C语言如下:

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    typedef unsigned int DWORD;
    typedef unsigned char BYTE;
    
    size_t decrypt_asm(BYTE* dst, BYTE *src, size_t src_size) //445f40, dst=eax, src=ecx
    {
        BYTE *buf=(BYTE*)malloc(3*src_size); //6975F28
        BYTE *edi = src;
        BYTE *esi = dst;
        DWORD t1, t2, t3; //[ebp-4], [ebp-8], [ebp-c]
        DWORD eax, ebx, ecx, edx;
    
        ebx = *(DWORD*)edi + (DWORD)esi; //edge
        t2 = ebx;
        edi += 4;
        memset(buf, 0, 0xFEE);
        eax = 0XFEE;
        edx = 0;
        while(1)
        {
            edx >>= 1;
            t1 = edx;
            if (!(edx & 0x100)) // 00 01
            {
                edx = *edi;
                edi++;
                edx |= 0xff00;
                t1 = edx;
            }
            ecx = *edi;
            if ((BYTE)edx & 1)
            {
                buf[eax] = (BYTE)ecx;
                eax++;
                *esi = (BYTE)ecx;
                esi++;
                edi++;
                eax &= 0xFFF;
                if(esi >= ebx) 
                {
                    free(buf);
                    return (size_t)(ebx-(DWORD)dst);
                }
            }
            else
            {
                edx = *(edi+1);
                ebx = (edx & 0XF0)<<4;
                ecx |= ebx;
                edx &= 0xF;
                ebx = edx+ecx+2;
                edi += 2;
                t3 = ebx;
                ebx = t2;
                edx = ecx;
                if(ecx>ebx)
                {
                    edx = t1;
                    continue;
                }
                do{
                    ecx = edx & 0xFFF;
                    ecx = buf[ecx];
                    buf[eax] = (BYTE) ecx;
                    eax ++;
                    (*esi) = (BYTE) ecx;
                    esi++;
                    eax &= 0XFFF;
                    if(esi >= ebx) 
                    {
                        free(buf);
                        return (size_t)(ebx-(DWORD)dst);
                    }
                    edx++;
                } while (edx<=t3);
                edx = t1;
            }
        }
    }
    

    0x4 算法进一步分析

    初步分析后,我们大概熟悉了算法流程。
    然后可以用c语言写一个人能理解的版本了:

    size_t decrypt(BYTE* dst, BYTE *src, size_t src_size) 
    {
        BYTE *buf=(BYTE*)malloc(0x1000);               
        BYTE *cur_src = src;
        BYTE *cur_dst = dst;
        BYTE *end_dst =  (DWORD)cur_dst + *(DWORD*)cur_src;
        DWORD idx_buff, i, last, c1, c2;
    
        cur_src += 4;
        memset(buf, 0, 0xFEE);
        idx_buff = 0XFEE;
    
        c1 = 0; //index byte
        while(1)
        {
            c1 >>= 1;
            if (!(c1 & 0x100)) // c1 bit[9] is 0, it means do 8 times
            {
                c1 = *cur_src;
                c1 |= 0xff00; //make a mark, and to 16bit
                cur_src++;
            }
            if ((BYTE)c1 & 0x1) //copy to buf directly
            {
                buf[idx_buff] = *cur_src;
                *cur_dst = *cur_src;
                idx_buff++;r
                idx_buff &= 0xFFF; //circle buffer
                cur_dst++;
                cur_src++;
                if(cur_dst >= end_dst) 
                {
                    free(buf);
                    return (size_t)((BYTE*)end_dst - dst);    
                }
            }
            else
            {
                c2 = *(cur_src+1); //index byte2
                i = *cur_src | ((c2 & 0XF0)<<4); //use c1 and c2 (higher 4bits) to determine index
                last = (c2 & 0xf) + i +2; // length = c2 lower 4bit, 2 without length 2 chars
                cur_src += 2; //c1, c2 two index byte
    
                if(i > end_dst)
                {
                    continue;
                }
                do
                {
                    buf[idx_buff] = buf[i & 0xFFF];
                    *cur_dst = buf[i & 0xFFF];
                    idx_buff++;
                    idx_buff &= 0xFFF;
                    cur_dst++;
                    if(cur_dst >= end_dst) 
                    {
                        free(buf);
                        return (size_t)((BYTE*)end_dst - dst);  
                    }
                    i++;
                } while (i <= last);
            }
        }
    }
    

    并且可以总结出怎么来解密文本了。

    sn.bin 结构
    0x0 size 4
    0x4~ data

    data部分第一字节为索引,每一位代表当前byte的状态,
    索引为1:直接copy当前byte到环状缓存区
    索引为0:则表示当前byte和下一个byte是索引,高12位为位置,低4位为长度。
    环状缓存区大小0x1000,起始位置0xFEE。

    顺便说一下位运算充当循环遍历的方法吧,
    ​a = 0b0011111, a>>1需要移位5次a=0,则循环次数是5

    测试一下文本没问题:

    0x5 后记

    之前在分析psv版ida看了半天也没有找到文本位置,
    psv的ida loader真的不好用,好多函数识别不出来,字符串也无法定位,ps4版还稍微好点,至少x64比arm汇编看着要舒服。所以这次来分析一下pc版文本。
    定位还算简单,然后又稍微逆向了一下算法,还算比较容易。
    iwaihime psv eboot

    iwaihime ps4 eboot

    其实对数据比较敏感的看到0x1000,0XFEE就知道这其实不是加密,
    就是LZSS的压缩,游戏里就是原封不动的把源码复制过去了。
    我分析完算法后还以为是变种的RLE结果再仔细想想竟然就是LZSS,以前只是简单了解,
    这次逆向算法后基本上对LZSS有了更深刻的了解了。

yurisbbs by @devseed since 2020, powered by nodebb.