我今天在修改地震-2的源代码,并且在某个时候想从游戏中使用的.pak档案中提取文件。由于我找不到任何工具在我的操作系统上这样做,所以我编写了这个快速-n脏命令行提取器,用于Quake-2 PAKs:
#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;
}
}任何关于它的反馈都是值得赞赏的。
发布于 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标准 (即使这没有实际意义)。
发布于 2015-10-18 01:40:48
当前,您可以像这样计算目标目录:
//删除文件扩展名,如果有的话: if ((dest_dir_name= strchr(dest_dir_name,'.')) != NULL) { *ext_ptr =‘\0’};
strrchr而不是strchr,因为您希望删除最后一个句点,而不是第一个句点。否则,像"../subdir/file.pak"这样的参数将使用""作为其目标目录。"../subdir/pakfile"这样的参数将导致程序使用"."作为其目标目录(在修复#1之后)。https://codereview.stackexchange.com/questions/107905
复制相似问题