吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 20240|回复: 68
上一主题 下一主题
收起左侧

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

    [复制链接]
跳转到指定楼层
楼主
小木曾雪菜 发表于 2020-6-6 10:31 回帖奖励
本帖最后由 小木曾雪菜 于 2020-7-7 17:04 编辑

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

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++;
            idx_buff &= 0xFFF; //cicle 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  

测试一下文本没问题:  

提取出来的文本:

完整代码详见我的github:https://github.com/YuriSizuku/GalgameReverse/

0x5 后记

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

iwaihime ps4 eboot

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

免费评分

参与人数 46吾爱币 +45 热心值 +39 收起 理由
sain_isolate + 1 我很赞同!
MX1996 + 1 谢谢@Thanks!
martinkathur + 1 + 1 谢谢@Thanks!
machinewu + 1 谢谢@Thanks!
InFatuated + 1 + 1 我很赞同!
xmdystest + 1 + 1 牛逼,感谢分享
psycongroo + 1 我很赞同!
Razuri + 1 + 1 大佬的这几篇文章全文背诵
咕噜咕噜777 + 1 我在吾爱看galgame
hwt1995511 + 1 + 1 我在吾爱看gal
马猴煮酒 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
无所矣致 + 1 + 1 谢谢@Thanks!
ljl200010 + 1 + 1 热心回复!
sdsyzhp + 1 + 1 nb woc
cpj1203 + 1 + 1 谢谢@Thanks!
jaj7780 + 1 + 1 用心讨论,共获提升!
Higher-Stark + 1 用心讨论,共获提升!
Abet-phi + 1 谢谢@Thanks!
Achilleskills + 1 + 1 谢谢@Thanks!
xiong_online + 1 + 1 用心讨论,共获提升!
zqzess + 1 我很赞同!
qaz007 + 1 + 1 用心讨论,共获提升!
bugof52pj + 1 + 1 谢谢@Thanks!
gdpnxws + 1 + 1 用心讨论,共获提升!
mms123 + 1 + 1 我很赞同!
浅阳琉璃 + 1 + 1 我很赞同!
dreamlivemeng + 1 + 1 谢谢@Thanks!
铁皮厚厚 + 1 + 1 谢谢@Thanks!
椎名牧 + 1 + 1 用心讨论,共获提升!
涛之雨 + 3 + 1 精华预定(我是说系列)
yamadaelf + 1 + 1 谢谢@Thanks!
Aurelion + 1 汉化类的帖子真的挺少的
诗木 + 1 + 1 用心讨论,共获提升!
FFF全部成为F + 1 只会用大佬做好的工具的表示看不懂emmmm
笙若 + 1 + 1 谢谢@Thanks!
sunptf + 1 + 1 用心讨论,共获提升!
asq56747277 + 1 + 1 谢谢@Thanks!
XhyEax + 1 + 1 我很赞同!
wakfbycf + 1 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
二娃 + 2 + 1 我很赞同!
MaxMadcc + 1 谢谢@Thanks!
栾森森 + 1 + 1 谢谢@Thanks!
CrazyNut + 2 + 1 用心讨论,共获提升!
世俗难断 + 1 + 1 谢谢@Thanks!
蓝蓝的小刺客 + 1 + 1 热心回复!
Zeaf + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!

查看全部评分

本帖被以下淘专辑推荐:

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

推荐
Zeaf 发表于 2020-6-6 10:36
汉化真是不容易,我想到了某万花镜
推荐
 楼主| 小木曾雪菜 发表于 2020-6-6 15:58 |楼主
wangyujie96 发表于 2020-6-6 15:02
硬断write有个疑问,内存不是动态分配的吗,重启程序应该会变动的,为何这里还能正确断下?

虽然是动态范围,但文本缓存区还是有比较大的内存空间,我们不需要精确到单字符,只要它落到对应的范围就行了。两次申请到的范围很可能有重叠的,所以我才说在这个字符附近下断点。如果不行,就在后面解密文本下访问断点。
3#
jiou234 发表于 2020-6-6 10:51
4#
sakurahime 发表于 2020-6-6 11:17
汉化的艰辛
5#
kof888 发表于 2020-6-6 11:42
lzss的变种,一般都是初始化fee个00或者20,然后开始解压
还有的不初始化直接解压,这些一般的区别只是从高位判断还是低位判断,0是压缩还是1是压缩而已而已
对了,有的还是双字节解压
6#
Ldfd 发表于 2020-6-6 11:54
像极了pxxx的译者,上楼也不气喘了,身体也壮了,电脑技术也高了
7#
langren425 发表于 2020-6-6 12:02
感谢楼主分享经验
8#
 楼主| 小木曾雪菜 发表于 2020-6-6 12:11 |楼主
kof888 发表于 2020-6-6 11:42
lzss的变种,一般都是初始化fee个00或者20,然后开始解压
还有的不初始化直接解压,这些一般的区别只是从 ...

搞完后整体分析是这样的,但是在搞的时候时候我并不知道lzss算法,所以才决定要好好分析一下算法。
9#
第一品霄 发表于 2020-6-6 13:03
学习一下了
10#
九条可怜 发表于 2020-6-6 13:54
向各位伟大的汉化组成员致敬
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-11-15 10:05

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表