首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >地震-2 PAK档案的简易提取器

地震-2 PAK档案的简易提取器
EN

Code Review用户
提问于 2015-10-17 23:35:45
回答 2查看 397关注 0票数 8

我今天在修改地震-2的源代码,并且在某个时候想从游戏中使用的.pak档案中提取文件。由于我找不到任何工具在我的操作系统上这样做,所以我编写了这个快速-n脏命令行提取器,用于Quake-2 PAKs:

代码语言:javascript
复制
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>

// For mkdir/stat
#include <sys/types.h>
#include <sys/stat.h>

/*
 * From Quake2:
 */

// Not enforced by this extractor. Was enforced by the game, we just warn.
#define MAX_FILES_IN_PACK 4096

// 4CC 'PACK'
#define ID_PAK_HEADER (('K' << 24) + ('C' << 16) + ('A' << 8) + 'P')

typedef struct
{
    char name[56];
    int filepos;
    int filelen;
} pak_file_t;

typedef struct
{
    int ident;
    int dirofs;
    int dirlen;
} pak_header_t;

/*
 * Extractor code:
 */

static void make_path(const char * path_ended_with_sep_or_filename)
{
    struct stat dir_stat;
    char dir_path[512];

    strncpy(dir_path, path_ended_with_sep_or_filename, sizeof(dir_path));
    char * pPath = dir_path;

    while (*pPath != '\0')
    {
        if (*pPath == '/' || *pPath == '\\')
        {
            *pPath = '\0';
            if (stat(dir_path, &dir_stat) != 0)
            {
                if (mkdir(dir_path, 0777) != 0)
                {
                    fprintf(stderr, "mkdir('%s', 0777) failed!\n", dir_path);
                }
            }
            else // Path already exists.
            {
                if (!S_ISDIR(dir_stat.st_mode))
                {
                    // Looks like there is a file with the same name as the directory.
                    fprintf(stderr, "Can't mkdir()! Path points to a file.\n");
                }
            }
            *pPath = '/';
        }
        ++pPath;
    }
}

static bool write_file(const char * name, const void * data, int len_bytes)
{
    // First might need the create the file path:
    make_path(name);

    FILE * out_file = fopen(name, "wb");
    if (out_file == NULL)
    {
        fprintf(stderr, "Can't fopen() the file! %s\n", name);
        return false;
    }

    fwrite(data, 1, len_bytes, out_file);
    fclose(out_file);
    return true; // Assume write went OK.
}

static bool extract_file(FILE * pak_file, const char * dest_dir_name, 
                         const char * name, int file_pos, int len_bytes)
{
    void * buffer = malloc(len_bytes);
    if (buffer == NULL)
    {
        fprintf(stderr, "Out-of-memory in extract_file!\n");
        return false;
    }

    fseek(pak_file, file_pos, SEEK_SET);
    fread(buffer, 1, len_bytes, pak_file);

    if (ferror(pak_file))
    {
        fprintf(stderr, "Error reading file data block for %s!\n", name);
        free(buffer);
        return false;
    }

    char full_path_name[512];
    snprintf(full_path_name, sizeof(full_path_name), "%s/%s", dest_dir_name, name);

    bool result = write_file(full_path_name, buffer, len_bytes);
    free(buffer);

    return result;
}

static bool unpak(FILE * pak_file, const pak_header_t * pak_header, const char * dest_dir_name)
{
    pak_file_t * pak_file_entries;
    int num_files_in_pak = pak_header->dirlen / sizeof(pak_file_t);

    if (num_files_in_pak > MAX_FILES_IN_PACK)
    {
        fprintf(stderr, "Warning MAX_FILES_IN_PACK exceeded!\n");
        // Allow it to continue.
    }

    pak_file_entries = malloc(num_files_in_pak * sizeof(pak_file_t));
    if (pak_file_entries == NULL)
    {
        fprintf(stderr, "Out-of-memory in unpak!\n");
        return false;
    }

    fseek(pak_file, pak_header->dirofs, SEEK_SET);
    fread(pak_file_entries, 1, pak_header->dirlen, pak_file);

    if (ferror(pak_file))
    {
        fprintf(stderr, "Error reading pak_file_entries block!\n");
        free(pak_file_entries);
        return false;
    }

    for (int i = 0; i < num_files_in_pak; ++i)
    {
        const pak_file_t * entry = &pak_file_entries[i];
        if (!extract_file(pak_file, dest_dir_name, entry->name, entry->filepos, entry->filelen))
        {
            fprintf(stderr, "Failed to extract pak entry '%s' #%d\n", entry->name, i);
            // Try another one...
        }
    }

    free(pak_file_entries);
    return true;
}

int main(int argc, const char * argv[])
{
    char dest_dir_name[512];
    char * ext_ptr;
    pak_header_t pak_header;
    const char * pak_name;
    FILE * pak_file;

    if (argc <= 1)
    {
        fprintf(stderr, "No filename!\n");
        printf("Usage: \n"
               " $ %s <file.pak>\n"
               "   Unpacks the whole archive to a directory with the same name as the input.\n"
               "   Internal file paths are preserved.\n",
               argv[0]);
        return EXIT_FAILURE;
    }

    pak_name = argv[1];
    pak_file = fopen(pak_name, "rb");

    if (pak_file == NULL)
    {
        fprintf(stderr, "Can't fopen() the file! %s\n", pak_name);
        return EXIT_FAILURE;
    }

    fread(&pak_header, 1, sizeof(pak_header), pak_file);
    if (pak_header.ident != ID_PAK_HEADER)
    {
        fprintf(stderr, "Bad file id for pak %s!\n", pak_name);
        return EXIT_FAILURE;
    }

    strncpy(dest_dir_name, pak_name, sizeof(dest_dir_name));

    // Remove the file extension, if any:
    if ((ext_ptr = strchr(dest_dir_name, '.')) != NULL)
    {
        *ext_ptr = '\0';
    }

    if (!unpak(pak_file, &pak_header, dest_dir_name))
    {
        fprintf(stderr, "Unable to successfully unpack archive %s!\n", pak_name);
        return EXIT_FAILURE;
    }
}

任何关于它的反馈都是值得赞赏的。

EN

回答 2

Code Review用户

回答已采纳

发布于 2015-10-18 00:11:23

文件归档提取器的一个常见问题是易受目录遍历攻击攻击。恶意构建的归档可能包含一个路径(如../../../../../../../etc/resolv.conf )的条目,如果提取是由特权用户完成的,则该路径可以覆盖系统文件。您应该采取措施确保您创建的任何文件都位于预定的目标目录中。

文件结构具有固定大小的表示形式和预先确定的endianness。您应该使用uint32_t,而不是假设int是32位。你对ID_PAK_HEADER的治疗看上去好像是很小的一种。

如果无法提取任何或所有文件,则打印错误消息,但返回成功代码。我预计这些失败会导致EXIT_FAILURE或其他一些非零退出状态。

考虑使用pathconf()而不是像512这样的硬编码路径长度限制。

在不检查其返回值的情况下,您可以多次调用fread()。还最好重新排序第二个和第三个参数,以便size位于nitems之前,以匹配POSIX标准 (即使这没有实际意义)。

票数 10
EN

Code Review用户

发布于 2015-10-18 01:40:48

目标目录

当前,您可以像这样计算目标目录:

//删除文件扩展名,如果有的话: if ((dest_dir_name= strchr(dest_dir_name,'.')) != NULL) { *ext_ptr =‘\0’};

  1. 您应该使用strrchr而不是strchr,因为您希望删除最后一个句点,而不是第一个句点。否则,像"../subdir/file.pak"这样的参数将使用""作为其目标目录。
  2. 您应该检查所找到的句点实际上在文件名部分,而不是目录部分。否则,像"../subdir/pakfile"这样的参数将导致程序使用"."作为其目标目录(在修复#1之后)。
票数 6
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/107905

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档