某游戏cg素材解包

解包某游戏素材,玩完觉得cg差几张,决定解开来看看是否少了cg。

查看打包文件

当前目录下的文件格式除了exe,dll以外就是类型为NPK的文件,NPK文件必为打包文件无疑,010 Editor查看文件格式,如下图(截取的开头部分):

GameUnpack1

根据文件头部上网搜索相关资料和工具(尝试过可能有用的arc_unpacker工具等),无解。那就只能逆向解包啦!

推测负责解包的exe,并进行逆向分析,找到程序试图解包一个名为system.npk的文件。找到头部签名比对部分:

GameUnpack2

接着往下分析程序,大致了解(猜测)了NPK文件的结构以及使用的加密算法AES256-cbc:

GameUnpack3

图中黄色部分为签名,蓝色部分AES256的向量IV,绿色部分是加密的需要用到的该NPK文件中的所有文件的描述结构,大小为红色部分0xD460,绿色部分往下也就是0xD480往后就是加密的所有文件了。

通过AES256-cbc算法解密0xD460大小的文件描述结构,密钥硬编码在程序中,即可得到该NPK中加密的文件描述结构。解密出的一个文件描述结构如下图:

GameUnpack4

如上图所示,黑色框起来的部分为一个png文件的解密描述结构,总共有0xD460大小的文件描述结构。程序中会根据计算机是否支持AES-NI指令集,而选择使用AES-NI指令集的方式进行解密,还是另外一种
github上的某AES进行解密。
AES解密部分截图如下:

GameUnpack5

GameUnpack6

接下来,要做的就是解密文件了,一个png文件的解密描述结构中重要的就是蓝色部分,代表该png在npk文件中的起始地址,绿色部分代表该png文件的大小。在程序如何解密setting.ini文件的部分,如下图:

GameUnpack7

发现了是使用同样的解密算法AES256-cbc进行解密,并且IV和密钥相同。故写脚本提取出所有的png图片,一气呵成!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
from Crypto import Random
from Crypto.Cipher import AES

import os
import sys
import base64
import hashlib
import struct

def getIV(content):
aes256IV = content[8:24]
return aes256IV

def getSecretKey(uiAesKey):
aesKey = ""
for ch in uiAesKey:
ch = struct.pack("<I",ch)
aesKey += ch
return aesKey

def decrypt(encrypted, passphrase):
IV = encrypted[:16]
cipher = AES.new(passphrase,AES.MODE_CBC,IV)

return cipher.decrypt(encrypted[16:])

def encrypt(decrypted, passphrase):

plaintext=AES.new(passphrase,AES.MODE_ECB)

return plaintext.encrypt(decrypted)


def analysisCGHeader(cg_content,cgPosition_content,aesIV,aesKey):

rename = "Invicsfate"
renameIndex = 0
position = 0x0
while (position <= 0xD450):
cipher = ""

position += 1
fileNameLength = cgPosition_content[position:position+2]
position += 2
fileNameLength = struct.unpack("<H",fileNameLength)[0]
fileName = cgPosition_content[position:position+fileNameLength]
position += fileNameLength

position += 4
position += 32
position += 4
pngPosition = cgPosition_content[position:position+4]
pngPosition = struct.unpack("<I",pngPosition)[0]
position += 4
position += 4
pngSize = cgPosition_content[position:position+4]
pngSize = struct.unpack("<I",pngSize)[0]
position += 4
position += 8

cipher += aesIV
cipher += cg_content[pngPosition:pngPosition+pngSize]

length = len(cipher)
pundding=length%16
if (pundding):
cipher += ((16 - pundding) * '\x00')
pngContent = decrypt(cipher, aesKey)

fileDictory = fileName[:fileName.rindex('/')]
isReadable = fileName[fileName.rindex('/')+1:fileName.rindex('/')+2]

isExists = os.path.exists(fileDictory)
if isExists:
if ((ord(isReadable) < 0x20) or (ord(isReadable) > 0x7E)):#处理不可见文件名
fileName = fileDictory + "/" + rename + str(renameIndex) + ".png"
savePng = open(fileName,"wb")
savePng.write(pngContent)
savePng.close()
renameIndex += 1
else:
if ((ord(isReadable) < 0x20) or (ord(isReadable) > 0x7E)):#处理不可见文件名
fileName = fileDictory + "/" + rename + str(renameIndex) + ".png"
os.makedirs(fileDictory)
savePng = open(fileName,"wb")
savePng.write(pngContent)
savePng.close()
renameIndex += 1




cg_fp = open("cg.npk",'rb')
cg_content = cg_fp.read()

aesIV = getIV(cg_content)

uiAesKey = [0x3C1FB7D0,0xCFCE244E,0x1DA9EEDD,0x3240B024,0x33E5A329,0x8251290D,0xC9D65160,0x54AFF54A]
aesKey = getSecretKey(uiAesKey)

cgPosition_fp = open("testNPKCG",'rb')

cgPosition_content = cgPosition_fp.read()

cgPosition_fp.close()

analysisCGHeader(cg_content,cgPosition_content,aesIV,aesKey)

cg_fp.close()

其中cg.npk是存储cg相关的npk文件,testNPKCG是解密出来的0xD460大小的文件描述结构。

该游戏目录下其他的npk文件,比如font.npk,system.npk等等都是使用该种模式加密,用同样的方式可以获取到所有npk文件的内容。