DNF客户端资源分析与提取工具(附代码)

Neople是一家韩国的游戏公司,地下城勇士DNF就是出自他之手。

第一部分 解包

DNF使用的资源包是NPK格式,即”NeoplePack”的缩写,于是推测这篇分析也许能用在Neople其他的几款游戏上,呵呵~~。

struct NPK_Header
{
char flag[16]; // 文件标识 "NeoplePack_Bill"
int count; // 包内文件的数目
};

struct NPK_Index
{
DWORD offset; // 文件的包内偏移量
DWORD size; // 文件的大小
char name[256];// 文件名
};

NPK文件就是由一个NPK_Header和N个NPK_Index以及实际的数据组成的。

注:NPK分加密(ImagePacks4文件夹下)和未加密(ImagePacks2文件夹下)的两个版本,加密的版本需要用”[email protected] dungeon and fighter DNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNF”(共256字节,最后一字节是’/0’)异或文件名部分。

第二部分 格式转换

解包之后得到的文件基本都是”.img”格式,但根据文件标识的不同,可分为”Neople Image File”和”Neople Img File”。

【”Neople Image File”的格式】

struct NImageF_Header
{
char flag[24]; // 文件标识 "Neople Image File"
int unknown1; // 未知数据,也许是版本号什么的?
int index_count; // 索引项的数目,也可以理解为包含的小图片的个数。
};

struct NImageF_Index
{
DWORD dwType; // 目前已知的类型有 0x0E(1555格式) 0x0F(4444格式) 0x10(8888格式) 0x11(不包含任何数据,可能是指内容同上一帧)
DWORD dwCompress; // 目前已知的类型有 0x06(zlib压缩) 0x05(未压缩)
int width; // 宽度
int height; // 高度
int size; // 压缩时size为压缩后大小,未压缩时size为转换成8888格式时占用的内存大小
int key_x; // X关键点,当前图片在整图中的X坐标
int key_y; // Y关键点,当前图片在整图中的Y坐标
int max_width; // 整图的宽度
int max_height; // 整图的高度,有此数据是为了对其精灵
BYTE Data[实际长度]; // 紧跟在索引后面的就是实际的数据(将数据放在索引结构中只是为了向大家展示索引与数据的关系。)
};

“Neople Image File”由一个NImageF_Header和N个NImageF_Index结构组成。

注:对于类型0x11其索引表仅包含前2项,即

struct NImageF_Index_0x11
{
DWORD dwType; // 类型0x11
DWORD dwCompress; // 可取很多种数值,作用未知(其中零最为常见)
};

【"Neople Img File"的格式】

struct NImgF_Header
{
char flag[16]; // 文件标识 "Neople Img File"
int index_size; // 以字节为单位,索引表的大小
int unknown1; // 未知1
int unknown2; // 未知2
int index_count;// 索引表项数目
};

struct NImgF_Index
{
DWORD dwType; // 与"NImageF_Index"是一致的,要看注释往上翻。
DWORD dwCompress;
int width;
int height;
int size;
int key_x;
int key_y;
int max_width;
int max_height;
};

“Neople Img File”是由一个NImgF_Header和N个NImgF_Index索引表 + 连续存储的实际数据组成。换句话说就是,”Neople Image File”的索引与自己的数据挨在一起,而”Neople Img File”的索引表和数据是分开存放的。

第三部分 总结

从几个未知数据来看,还不能算是完美的格式分析,实在是遗憾。(现在的网游越加壳越厉害,看来我得再多研究研究脱壳啦。)

不过对于资源提取还是不成问题的。按住惯例上传一张提取图片。

提取工具的代码放到github上了:https://github.com/langresser/dnfextrator

虽然上文已经有比较详尽的分析了,但是真正实现好一个资源提取工具还是花了我两天的时间。这里把需要注意的地方记录下来。

1、npk包的格式:

struct NPK_Header
{
char flag[16]; // 文件标识 "NeoplePack_Bill"
int count; // 包内文件的数目
};

struct NPK_Index
{
unsigned int offset; // 文件的包内偏移量
unsigned int size; // 文件的大小
char name[256];// 文件名
};

char decord_flag[256] = "[email protected] dungeon and fighter DNF";

解npk包非常好处理,读取完NPK_Header紧接着根据里面的count数目循环读取NPK_Index,读取完毕后,就可以根据里面的offset定位到指定位置读取img文件。现在的dnf包npk包内的文件名是加密过的,要用decord_flag异或NPK_Index中的name才能获取实际文件名。decord_flag总共有256个字节,剩余部分用”DNF”三个字母填满,最后一个字节置0。读取文件名时可以像这样解密:

char temp[256] = {0};
fread(temp, 256, 1, fp);
for (int i = 0; i < 256; ++i) {
index.name[i] = temp[i] ^ decord_flag[i];
}

2、img文件格式:

struct NImgF_Header
{
char flag[16]; // 文件标石"Neople Img File"
int index_size; // 索引表大小,以字节为单位
int unknown1;
int unknown2;
int index_count;// 索引表数目
};

struct NImgF_Index
{
unsigned int dwType; //目前已知的类型有 0x0E(1555格式) 0x0F(4444格式) 0x10(8888格式) 0x11(不包含任何数据,可能是指内容同上一帧)
unsigned int dwCompress; // 目前已知的类型有 0x06(zlib压缩) 0x05(未压缩)
int width; // 宽度
int height; // 高度
int size; // 压缩时size为压缩后大小,未压缩时size为转换成8888格式时占用的内存大小
int key_x; // X关键点,当前图片在整图中的X坐标
int key_y; // Y关键点,当前图片在整图中的Y坐标
int max_width; // 整图的宽度
int max_height; // 整图的高度,有此数据是为了对齐精灵
};

img文件也是一系列图片的合集,它里面还包含很多有用的信息,比如图片的坐标(用于对齐),这个数据是我们想正常使用这个图片所必须的。也正是由于现有的工具都没有提供方便的批量导出和该数据的处理功能,我才想自己写个提取工具的。

img文件是由一个header+多个连续的索引表+实际图片数据组成的。读取图片数据需要跳过header(固定大小)和索引表(header.index_size标识)。
我们读取的文件大小是由NImgF_Index.size决定的,如果dwCompress为6则表示图片有经过zlib压缩,这时size表示压缩后大小。如果为5表示没有压缩,这时size表示转换成8888格式所占内存大小(也就是说,如果dwType为0x0e或是0x0f,size要除2)
如果有压缩,需要zlib解压:

int ret = uncompress(temp_zlib_data, &zlib_len, temp_file_data, size);

注意,temp_zlib_data是一个足够大的缓存区,zlib_len传入的是缓存区的大小。

读取完的数据是图片像素数据,接下来要写入到png图片中(看个人需要bmp什么的也可以)
libpng的使用(包含颜色格式之间的转换代码):

FILE *fp = fopen(file_name, "wb");
png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);

info_ptr = png_create_info_struct(png_ptr);
if (setjmp(png_jmpbuf(png_ptr)))
{
printf("[write_png_file] Error during init_io");
return;
}
png_init_io(png_ptr, fp);

/* write header */
if (setjmp(png_jmpbuf(png_ptr)))
{
printf("[write_png_file] Error during writing header");
return;
}

png_set_IHDR(png_ptr, info_ptr, width, height,
8, PNG_COLOR_TYPE_RGB_ALPHA, PNG_INTERLACE_NONE,
PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE);

png_write_info(png_ptr, info_ptr);

/* write bytes */
if (setjmp(png_jmpbuf(png_ptr)))
{
printf("[write_png_file] Error during writing bytes");
return;
}

row_pointers = (png_bytep*)malloc(height*sizeof(png_bytep));
for(int i = 0; i < height; i++)
{
row_pointers[i] = (png_bytep)malloc(sizeof(unsigned char)* 4 * width);
for(int j = 0; j < width; ++j) { // png is rgba switch (type) { case ARGB_1555://1555 row_pointers[i][j * 4 + 0] = ((data[i * width * 2 + j * 2 + 1] & 127) >> 2) << 3; // red
row_pointers[i][j * 4 + 1] = (((data[i * width * 2 + j * 2 + 1] & 0x0003) << 3) | ((data[i * width * 2 + j * 2] >> 5) & 0x0007)) << 3; // green
row_pointers[i][j * 4 + 2] = (data[i * width * 2 + j * 2] & 0x003f) << 3; // blue row_pointers[i][j * 4 + 3] = (data[i * width * 2 + j * 2 + 1] >> 7) == 0 ? 0 : 255; // alpha
break;
case ARGB_4444://4444
row_pointers[i][j * 4 + 0] = (data[i * width * 2 + j * 2 + 1] & 0x0f) << 4; // red row_pointers[i][j * 4 + 1] = ((data[i * width * 2 + j * 2 + 0] & 0xf0) >> 4) << 4; // green
row_pointers[i][j * 4 + 2] = (data[i * width * 2 + j * 2 + 0] & 0x0f) << 4;; // blue row_pointers[i][j * 4 + 3] = ((data[i * width * 2 + j * 2 + 1] & 0xf0) >> 4) << 4; // alpha
break;
case ARGB_8888://8888
row_pointers[i][j * 4 + 0] = data[i * width * 4 + j * 4 + 2]; // red
row_pointers[i][j * 4 + 1] = data[i * width * 4 + j * 4 + 1]; // green
row_pointers[i][j * 4 + 2] = data[i * width * 4 + j * 4 + 0]; // blue
row_pointers[i][j * 4 + 3] = data[i * width * 4 + j * 4 + 3]; // alpha
break;
case ARGB_NONE:// 占位,无图片资源
break;
default:
printf("error known type:%d\n", type);
break;
}
}
}
png_write_image(png_ptr, row_pointers);

/* end write */
if (setjmp(png_jmpbuf(png_ptr))) {
printf("[write_png_file] Error during end of write");
return;
}
png_write_end(png_ptr, NULL);
// 别忘记释放内存
png_destroy_write_struct(&png_ptr, &info_ptr);

/* cleanup heap allocation */
for (int j=0; j < height; j++)
free(row_pointers[j]);
free(row_pointers);

fclose(fp);

新版的是像素数据去重放上面然后根据索引归位而已。