RGSSAD解密程序(基于RgssadUnpacker改进以支持RGSS3A)

近来玩小游戏需要解密Rgssad,发现网上已有人公开了Rgssad/Rgss2a的解密代码(RgssadUnpacker):

http://blog.csdn.net/rgss2ad/archive/2011/02/16/6187475.aspx

但我遇到的是Rgss3a,随手google一下可以看到如下算法描述(原出处不明):

rmvx ace的加密包相对于XP加密并没有太大改进,无非是magickey不再固定,并且一共有2个magickey.
rgss3a格式的文件,前8字节为字符串["R""G""S""S""A""D",0x00,0x03],用于判断是否为标准加密包.
紧接着4个字节,为此加密包的基础key.
以变量读入之后,key*9+3可以得到结构信息magickey.这是重点.
接着开始就是文件信息,文件头占16字节,每4字节为一个信息
将这些数据全部与magickey异或之后可以得到解密的文件信息.
第一个四字节为本段数据的偏移量。
第二个为数据长度。
第三个为本数据magickey,
第四个为此文件名长度.
接下来的若干字节长度为上面第四个四字节取到的长度,为文件名
与magickey异或之后可以得到解密的文件名.
再紧接着16字节为下一个文件的文件头,如此循环,直到文件头储存的偏移量为0.
数据部分,逐四字节与本数据magickey异或运算,每异或一次,magickey=magickey*7+3
直到文件末尾.

于是结合前人的代码改了一版RgssadUnpacker,从中也发现一些问题,在此对上述描述做一下补足:

1.magickey为四字节长,文件名为不定长,需要将文件名4字节为一组,与magickey做异或运算才能得到正确的结果

2.最后一个文件的文件头的偏移量为0,文件头中其他数据(12个字节)都没有意义,不要去读而直接结束解密即可


最后附上代码(RgssadUnpacker.c):

/*******************************************************************************
 *编译方法: gcc -std=c99 -O2 -s -o RgssadUnpacker RgssadUnpacker.c
 *          cl /TP /O2 /DNDEBUG /FeRgssadUnpacker RgssadUnpacker.c
 ******************************************************************************/

#ifndef __RU_STRING__
#define __RU_STRING__

static char RUS_INFO[] =
"===============================================================================\n"
"RgssadUpacker - 命令行的Rgssad/Rgss2a/Rgss3a文件解包器 - Version %s\n"
"更多信息请见 http://blog.csdn.net/rgss2ad/archive/2011/02/16/6187475.aspx\n"
"===============================================================================\n";

static char RUS_HELP[] =
"用法:\n"
"    RgssadUnpacker <Rgssad文件名> [<可选参数>]\n"
"\n"
"可选参数(大小写不敏感):\n"
"    -pw=<密码>,          --Password=<密码>\n"
"        强制使用<密码>当作密码来解包, *不推荐使用*, 16进制使用 '0x' 前缀\n"
"    -od=<输出目录>,      --OutDir=<输出目录>\n"
"        将解包后文件输出至目录<输出目录>而不是目录<Rgssad文件名>_Unpacked\n"
"    -lf=[<记录文件名>],  --LogFile=[<记录文件名>]\n"
"        生成解包记录文件<记录文件名>或<Rgssad文件名>_Unpacked.log\n"
"    -rn,                 --Rename\n"
"        使用文件序号代替解包后文件原名, 仅当输出文件名异常时才应使用这个参数\n"
"\n"
"例子:\n"
"    RgssadUnpacker Game.rgssad\n"
"    RgssadUnpacker Game.rgss2a\n"
"    RgssadUnpacker Game.rgssad -lf -rn\n"
"    RgssadUnpacker Game.rgssad -pw=0xDEADCAFE -od=C:\\Temp\\ -lf=Game.log\n";

static char RUS_E_FILE_OPEN[] =
    "无法打开文件 \"%s\"\n";
static char RUS_E_RGSSAD_SIGN[] =
    "无法识别的rgssad标识, 请确定这是一个rgssad/rgss2a/rgss3a文件\n";
static char RUS_E_ARGUMENT[] =
    "未知参数 \"%s\"\n";
static char RUS_E_DIR_LOCATE[] =
    "无法定位到目录 \"%s\"\n";
static char RUS_E_CALC_PASSWORD[] =
    "无法计算出密码, 可能是文件损坏或采取了不支持的加密方法\n";
static char RUS_E_UNPACK[] =
    "解包失败, 未知错误 0x%08X\n";

static char RUS_WAIT_UNPACK[] =
    "正在解包, 请稍候....\n";
static char RUS_WAIT_CACL_PASSWORD[] =
    "正在计算密码, 请稍候....\n";
static char RUS_SUCCESS[] =
    "没有检测到错误, 解包很可能成功了\n";

static const char *RUS_LOG_HEADER =
    "文件序号\x9""文件长度\x9""文件名\x9状态\n";
static const char *RUS_LOG_PASSWORD =
    "密码: 0x%08X\n";
static const char *RUS_LOG_FILE_SUCCESS =
    "成功\n";
static const char *RUS_LOG_FILE_FAIL =
    "失败\n";

#endif //__RU_STRING__

static const char *VERSION = "0.3";

#include <stdio.h>
#include <string.h>
#include <ctype.h>
#ifdef _MSC_VER
#include <direct.h>
#include <io.h>
#pragma warning(disable:4258)
#pragma warning(disable:4996)
#else
#include <unistd.h>
#include <stdbool.h>
#endif

static const int MAX_PATH = 260;
static const int RGSSAD_SIGN_LENGTH = 8;
static const char *RGSSAD_SIGN = "RGSSAD\x0\x1";
static const char *RGSSAD3_SIGN = "RGSSAD\x0\x3";
static const int BUF_SIZE = 0x10000;

static bool rgssad3 = false;

//错误
enum ruErrors
{
    E_NONE = 0,         //没有错误, 一切正常
    E_RGSSAD_SIGN,      //rgssasd文件标识 不符合
    E_CALC_PASSWORD,    //无法得到正确的密码
    E_FILE_TOO_LONG,    //文件过大(超过 2GB)
    E_FILE_OPEN,        //打开(创建)文件失败
    E_FILE_READ,        //读取文件失败(多数情况下为 读取长度 超出 被读取文件长度)
    E_FILENAME_TOO_LONG,//文件名过长(超过 MAX_PATH - 1)
    E_ARGUMENT,         //存在未知参数
    E_DIR_LOCATE,       //无法定位到输出目录
    E_UNPACK            //解包失败
};

///使用 MultiByteToWideChar() 和 WideCharToMultiByte()
#include <windows.h>

/*******************************************************************************
 * 字符串编码转换 UTF-8 -> ANSI
 * str: 需转换的字符串
 * 返回值: str
 ******************************************************************************/
static inline char *ruUtf8ToAnsi(char *str)
{
    int l = strlen(str) + 1;
    wchar_t *wstr = (wchar_t *)malloc(l * sizeof(wchar_t));
    MultiByteToWideChar(CP_UTF8, 0, str, -1, wstr, l * 2);
    WideCharToMultiByte(CP_ACP, 0, wstr, -1, str, l, "_", NULL);
    free(wstr);
    return str;
}

/*******************************************************************************
 * 直接创建目录
 * dirs: 目标目录字符串
 * pos: 从dirs中的pos位置开始创建目录, pos位置之前的目录必须存在
 * 返回值: 成功时返回 0, 失败时返回 非0
 ******************************************************************************/
static inline int ruCreateDirs(const char *dirs, int pos)
{
    int result = 0;
    char buf[MAX_PATH];
    int l = strlen(dirs);
    if (l >= MAX_PATH) return -1;
    memcpy(buf, dirs, l + 1);
    for (int i = pos; i < l; ++i)
        if (buf[i] == '\\')
        {
            buf[i] = 0;
            result |= mkdir(buf);
            buf[i] = '\\';
        }
    return result;
}

/*******************************************************************************
 * 检查并创建目录
 * dirs: 目标目录字符串
 * 返回值: 成功时返回 0, 失败时返回 非0
 ******************************************************************************/
static inline int ruCheckDirs(const char *dirs)
{
    char buf[MAX_PATH];
    int l = strlen(dirs);
    if (l >= MAX_PATH) return -1;
    memcpy(buf, dirs, l + 1);
    for (int i = l - 1; i >= 0; --i)
        if (buf[i] == '\\')
        {
            buf[i] = 0;
            if (access(buf, 0) == 0)
            {
                buf[i] = '\\';
                return ruCreateDirs(buf, i + 1);
            }
            buf[i] = '\\';
        }
    return ruCreateDirs(buf, 0);
}

/*******************************************************************************
 * 检查 rgssad文件标识 是否符合
 * rgssadFile: 以 "rb" 方式打开的 rgssad文件
 * 返回值: rgssad文件标识 符合时返回 E_NONE, 不符合或失败时返回 其它ruErrors
 ******************************************************************************/
int ruCheckRgssadSign(FILE* rgssadFile)
{
    FILE *rf = rgssadFile;
    int pos_bak = ftell(rf);
    rewind(rf);

    char rgssadSign[RGSSAD_SIGN_LENGTH];
    fread(rgssadSign, RGSSAD_SIGN_LENGTH, 1, rf);

    fseek(rf, pos_bak, SEEK_SET);

	if (memcmp(rgssadSign, RGSSAD_SIGN, RGSSAD_SIGN_LENGTH) == 0)
	{
		rgssad3 = false;
		return E_NONE;
	}

    if (memcmp(rgssadSign, RGSSAD3_SIGN, RGSSAD_SIGN_LENGTH) == 0)
    {
		rgssad3 = true;
		return E_NONE;
	}

    return E_RGSSAD_SIGN;
}

/*******************************************************************************
 * 检查 密码 是否正确
 * rgssadFile: 以 "rb" 方式打开的 rgssad文件
 * password: 密码
 * 返回值: 密码 正确时返回 E_NONE, 不正确或失败时返回 其它ruErrors
 ******************************************************************************/
int ruCheckPassword(FILE *rgssadFile, int password)
{
    FILE *rf = rgssadFile;
    rewind(rf);

    //获取 rgssad文件长度
    fseek(rf, 0, SEEK_END);
    int rgssadFileLength = ftell(rf);
    fseek(rf, RGSSAD_SIGN_LENGTH, SEEK_SET);

    //判断 rgssad文件长度 是否有效
    if (rgssadFileLength < 0) return E_FILE_TOO_LONG;

    while (ftell(rf) < rgssadFileLength)
    {
        //判断 文件名长度在rgssad文件中的位置 是否有效
        if ((ftell(rf) + 4 < 0) || (ftell(rf) + 4 > rgssadFileLength))
            return E_FILE_READ;

        //获取 文件名长度
        int fileNameLength;
        fread(&fileNameLength, 4, 1, rf);
        fileNameLength ^= password;

        //判断 文件名长度 是否有效
        if ((fileNameLength < 0) || (fileNameLength > MAX_PATH - 1))
            return E_FILENAME_TOO_LONG;

        //判断 文件名在rgssad文件中的位置 是否有效
        if ((ftell(rf) + fileNameLength < 0) ||
            (ftell(rf) + fileNameLength > rgssadFileLength))
            return E_FILE_READ;

        //跳过 文件名
        fseek(rf, fileNameLength, SEEK_CUR);

        //判断 文件长度在rgssad文件中的位置 是否有效
        if ((ftell(rf) + 4 < 0) || (ftell(rf) + 4 > rgssadFileLength))
            return E_FILE_READ;

        //获取 文件长度
        int fileLength;
        fread(&fileLength, 4, 1, rf);
        for (int i = -1; i < fileNameLength; ++i) password = password * 7 + 3;
        fileLength ^= password;
        password = password * 7 + 3;

        //判断 文件在rgssad文件中的位置 是否有效
        if ((fileLength < 0) ||
            (ftell(rf) + fileLength < 0) ||
            (ftell(rf) + fileLength > rgssadFileLength))
            return E_FILE_READ;

        //跳过 文件
        fseek(rf, fileLength, SEEK_CUR);
    }
    return E_NONE;
}

/*******************************************************************************
 * 计算 密码
 * rgssadFile: 以 "rb" 方式打开的 rgssad文件
 * password: 计算出的 密码
 * 返回值: 得到 密码 时返回 E_NONE, 未得到或失败时返回 其它ruErrors
 ******************************************************************************/
int ruCalcPassword(FILE *rgssadFile, int *password)
{
    FILE *rf = rgssadFile;
    rewind(rf);

    //跳过 rgssad文件标识
    fseek(rf, RGSSAD_SIGN_LENGTH, SEEK_SET);

	if (rgssad3)
	{
		fread(password, 4, 1, rf);
		*password = *password * 9 + 3;
		return E_NONE;
	}

    int fileNameLength;
    fread(&fileNameLength, 4, 1, rf);
    for (int i = 0; i < MAX_PATH; ++i)
        if (ruCheckPassword(rf, fileNameLength ^ i) == 0)
        {
            *password = fileNameLength ^ i;
            return E_NONE;
        }

    return E_CALC_PASSWORD;
}

/*******************************************************************************
 * 定位文件数据
 * rgssadFile: 以 "rb" 方式打开的 rgssad文件,文件指针应该处于文件信息的起始处
 * fileName: 输出文件名缓冲,必须分配满 MAX_PATH个字节
 * fileLength: 输出文件大小
 * nextFile: 下一个文件信息的起始偏移
 * password: 密码
 * dataPassword: 数据密码
 * 返回值: 文件存在返回 true,已经没有文件返回 false
 ******************************************************************************/
bool ruQueryFile(FILE *rgssadFile, char *fileName, int *fileLength, int *nextFile, int *password, int *dataPassword)
{
	if (rgssad3)
	{
		//获取 文件数据偏移
		int offset;
		fread(&offset, 4, 1, rgssadFile);
		offset ^= *password;

		//已经没有文件了
		if (offset == 0)
			return false;

		//获取 文件长度
		fread(fileLength, 4, 1, rgssadFile);
		*fileLength ^= *password;

		//获取 数据解密密码
		fread(dataPassword, 4, 1, rgssadFile);
		*dataPassword ^= *password;

		//获取 文件名长度
		int fileNameLength;
		fread(&fileNameLength, 4, 1, rgssadFile);
		fileNameLength ^= *password;

		//获取 文件名
		fread(fileName, fileNameLength, 1, rgssadFile);
		for (int i = 0; i < fileNameLength; ++i)
			fileName[i] ^= *password >> (i % 4) * 8;
		fileName[fileNameLength] = 0;

		*nextFile = ftell(rgssadFile);

		//移动偏移到文件数据起始处
		fseek(rgssadFile, offset, SEEK_SET);
	}
	else
	{
		//获取 文件名长度
		int fileNameLength;
		fread(&fileNameLength, 4, 1, rgssadFile);
		fileNameLength ^= *password;
		*password = *password * 7 + 3;

		//获取 文件名
		fread(fileName, fileNameLength, 1, rgssadFile);
		for (int i = 0; i < fileNameLength; ++i)
		{
			fileName[i] ^= *password;
			*password = *password * 7 + 3;
		}
		fileName[fileNameLength] = 0;

		//获取 文件长度
		fread(fileLength, 4, 1, rgssadFile);
		*fileLength ^= *password;
		*password = *password * 7 + 3;

		*nextFile = ftell(rgssadFile) + *fileLength;

		//数据解密密码同信息密码
		*dataPassword = *password;
	}

	return true;
}

/*******************************************************************************
 * 写输出文件
 * rgssadFile: 以 "rb" 方式打开的 rgssad文件,文件指针应该处于文件数据的起始处
 * fileName: 输出文件名
 * fileLength: 输出文件大小
 * password: 密码
 * lofFile: 以 "w" 方式打开的 记录文件, 值为 NULL 时不创建 记录文件
 * 返回值: 解包成功时返回 E_NONE, 失败时返回 E_FILE_OPEN
 ******************************************************************************/
int ruUnpackFile(FILE *rgssadFile, const char *fileName, int fileLength, int password, FILE *logFile)
{
	//输出 文件
	FILE *uf = fopen(fileName, "wb");
	if (uf == NULL)
	{
		if (logFile != NULL) fprintf(logFile, RUS_LOG_FILE_FAIL);
		return E_FILE_OPEN;
	}

	int buf[BUF_SIZE >> 2];
	while (fileLength > 0)
	{
		int length = fileLength < BUF_SIZE ? fileLength : BUF_SIZE;
		fread(buf, length, 1, rgssadFile);
		for (int i = 0; i < ((length + 3) >> 2); ++i)
		{
			buf[i] ^= password;
			password = password * 7 + 3;
		}
		fwrite(buf, length, 1, uf);
		fileLength -= length;
	}
	fclose(uf);

	if (logFile != NULL) fprintf(logFile, RUS_LOG_FILE_SUCCESS);
	return E_NONE;
}

/*******************************************************************************
 * 直接以password解包文件 (ruUnpack省略对多种错误的检测, 请先用ruCheckPassword检测)
 * rgssadFile: 以 "rb" 方式打开的 rgssad文件
 * password: 密码
 * isRenameFile: 是否更改文件名为文件序号(避免因为文件名非法无法解包出文件)
 * lofFile: 以 "w" 方式打开的 记录文件, 值为 NULL 时不创建 记录文件
 * 返回值: 解包成功时返回 E_NONE, 失败时返回 其它ruErrors
 ******************************************************************************/
int ruUnpack(FILE *rgssadFile, int password, bool isRenameFile, FILE *logFile)
{
    FILE *rf = rgssadFile;
    rewind(rf);

    int result = E_NONE;

    //文件序号
    int fileNum = 0;

    //获取 rgssad文件长度
    fseek(rf, 0, SEEK_END);
    int rgssadFileLength = ftell(rf);
    fseek(rf, RGSSAD_SIGN_LENGTH, SEEK_SET);

	//RGSSAD3需要跳过magickey的4字节
	if (rgssad3)
		fseek(rf, 4, SEEK_CUR);

    //输出 logFile 表头
    if (logFile != NULL)
    {
        fprintf(logFile, RUS_LOG_PASSWORD, password);
        fprintf(logFile, RUS_LOG_HEADER);
    }

    while (ftell(rf) < rgssadFileLength)
    {
        ++fileNum;

		//获得文件名 文件大小 文件数据解密密码 并定位文件指针到文件数据起始处
		char fileName[MAX_PATH];
		int fileLength, nextFile, dataPassword;
		if (!ruQueryFile(rf, fileName, &fileLength, &nextFile, &password, &dataPassword))
			break;

		//输出 logFile 内容
		if (logFile != NULL)
			fprintf(logFile, "%08d\x9% 8d\x9%s\x9", fileNum, fileLength, fileName);

		//生成 文件名
		if (isRenameFile) sprintf(fileName, "%08X", fileNum);

		//转换 文件名编码 (UTF-8 -> ANSI)
		ruUtf8ToAnsi(fileName);

		//创建 目录
		if (ruCheckDirs(fileName) != 0) result = E_FILE_OPEN;

		//写输出文件
		if (ruUnpackFile(rf, fileName, fileLength, dataPassword, logFile) != E_NONE)
			result = E_FILE_OPEN;

		//继续处理下一个文件
		fseek(rf, nextFile, SEEK_SET);
    }

    return result;
}

/*******************************************************************************
 * 主函数 >_<
 * 返回值: 成功时返回 0, 失败时返回 非0
 ******************************************************************************/
int main(int argc, char **argv)
{
    printf(RUS_INFO, VERSION);

    //无参数, 显示帮助
    if (argc < 2)
    {
        printf(RUS_HELP);
        return 0;
    }

    FILE *rgssadFile = fopen(argv[1], "rb");
    if (rgssadFile == NULL)
    {
        printf(RUS_E_FILE_OPEN, argv[1]);
        return E_FILE_OPEN;
    }

    //检查 rgssad文件标识
    if (ruCheckRgssadSign(rgssadFile) != E_NONE)
    {
        printf(RUS_E_RGSSAD_SIGN);
        fclose(rgssadFile);
        return E_RGSSAD_SIGN;
    }

    int password = 0;
    bool isForcePassword = false;
    bool isLogFile = false;
    bool isRenameFile = false;

    char outDir[MAX_PATH];
    //默认输出目录
    sprintf(outDir, "%s_Unpacked\\", argv[1]);

    char logFileName[MAX_PATH];
    //默认记录文件名
    sprintf(logFileName, "%s_Unpacked.log", argv[1]);

    //解析 可选参数
    for (int i = 2; i < argc; ++i)
    {
        if ((argv[i][0] == '-') && (argv[i][1] == '-'))
        {
            char tmp[8];
            memcpy(tmp, &argv[i][2], 8);
            for (int i = 0; i < 8; ++i) tmp[i] = tolower(tmp[i]);
            if (strncmp(tmp, "password", 8) == 0)
            {
                if ((argv[i][11] == '0') || (argv[i][12] == 'x'))
                    sscanf(&argv[i][11], "%x", &password);
                else
                    sscanf(&argv[i][11], "%d", &password);
                isForcePassword = true;
                continue;
            }
            if (strncmp(tmp, "outdir", 6) == 0)
            {
                sscanf(&argv[i][9], "%s", outDir);
                continue;
            }
            if (strncmp(tmp, "logfile", 7) == 0)
            {
                isLogFile = true;
                if (strlen(argv[i]) > 9)
                    sscanf(&argv[i][10], "%s", logFileName);
                continue;
            }
            if (strncmp(tmp, "rename", 6) == 0)
            {
                isRenameFile = true;
                continue;
            }
        }
        if (argv[i][0] == '-')
        {
            char tmp[2];
            memcpy(tmp, &argv[i][1], 2);
            for (int i = 0; i < 2; ++i) tmp[i] = tolower(tmp[i]);
            if (strncmp(tmp, "pw", 2) == 0)
            {
                if ((argv[i][4] == '0') || (argv[i][5] == 'x'))
                    sscanf(&argv[i][4], "%x", &password);
                else
                    sscanf(&argv[i][4], "%d", &password);
                isForcePassword = true;
                continue;
            }
            if (strncmp(tmp, "od", 2) == 0)
            {
                sscanf(&argv[i][4], "%s", outDir);
                continue;
            }
            if (strncmp(tmp, "lf", 2) == 0)
            {
                isLogFile = true;
                if (strlen(argv[i]) > 3)
                    sscanf(&argv[i][4], "%s", logFileName);
                continue;
            }
            if (strncmp(tmp, "rn", 2) == 0)
            {
                isRenameFile = true;
                continue;
            }
        }
        printf(RUS_E_ARGUMENT, argv[i]);
        fclose(rgssadFile);
        return E_ARGUMENT;
    }

    char oldDir[MAX_PATH];
    getcwd(oldDir, MAX_PATH);

    //定位到输出目录
    if (outDir[strlen(outDir) - 1] != '\\')
    {
        outDir[strlen(outDir) + 1] = 0;
        outDir[strlen(outDir)] = '\\';
    }
    ruCheckDirs(outDir);
    if (chdir(outDir) != 0)
    {
        printf(RUS_E_DIR_LOCATE, outDir);
        fclose(rgssadFile);
        return E_DIR_LOCATE;
    }

    //计算密码
    if (!isForcePassword)
    {
        printf(RUS_WAIT_CACL_PASSWORD);
        if (ruCalcPassword(rgssadFile, &password) != E_NONE)
        {
            printf(RUS_E_CALC_PASSWORD);
            return E_CALC_PASSWORD;
        };
    }

    FILE *logFile = NULL;
    if (isLogFile)
    {
        chdir(oldDir);
        logFile = fopen(logFileName, "w");
        chdir(outDir);
    }

    //解包
    printf(RUS_WAIT_UNPACK);
    int result = ruUnpack(rgssadFile, password, isRenameFile, logFile);

    fclose(rgssadFile);
    if (logFile != NULL) fclose(logFile);
    chdir(oldDir);

	if (result == E_NONE)
    {
        printf(RUS_SUCCESS);
        return E_NONE;
    }
    else
    {
        printf(RUS_E_UNPACK);
        return E_UNPACK;
    }
}


记得保存为带BOM的UTF-8文本。


P.S.:新增了对VC的支持,注意:VC6靠边站!VS2003没测试过但可能可以,VS2005及以上都应该没问题

会用命令行的用命令行,不会用的新建工程,关掉预编译头,强制使用C++编译即可。


©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页