1. 项目概述:从APK到游戏美术资产的完整提取
最近在分析一个用Cocos Creator 2.4.x版本开发的老游戏,想把它里面的美术资源完整地扒出来。这活儿听起来像是“破解”,但在游戏开发、美术学习、甚至是同人创作里,其实是个挺常见的需求。比如你想研究一下某个独立游戏的美术风格,或者想用它的素材做个非商业的粉丝作品,第一步就是把资源从打包好的APK里弄出来。Cocos Creator打包后的资源,尤其是图片,并不是像PNG、JPG那样直接扔在文件夹里,而是经过了一系列的加密、压缩和格式转换,直接解压APK是看不到原图的。这个实战过程,就是一步步拆解这个“黑盒”,把游戏里的精灵图、UI贴图、背景图等资源,无损地还原成可编辑的图片文件。
整个过程的核心思路,可以概括为“解包、定位、解密、转换”四步。首先,你需要一个能解开Android安装包的工具,把APK的壳子扒掉,看到里面的原始文件结构。然后,在那一堆看似杂乱的文件里,找到Cocos Creator存放资源的“保险箱”和“钥匙”。接着,用正确的“钥匙”去打开“保险箱”,把里面加密过的、压缩过的二进制数据读出来。最后,把这些二进制数据,根据Cocos Creator特定的格式规则,重新拼装成一张张完整的图片。听起来有点复杂,但跟着步骤走一遍,你会发现每个环节都有成熟的工具和清晰的逻辑。我会把用到的所有工具打包好,你只需要按图索骥,就能搞定。
2. 核心思路与工具链解析
2.1 逆向流程总览与原理
为什么不能直接解压APK?因为Cocos Creator为了优化游戏加载速度和保护资源,在构建(Build)时会对资源进行处理。对于图片资源,主要经历两个关键步骤:纹理压缩和资源加密。
纹理压缩是为了适配移动端GPU的渲染格式,比如PVRTC(iOS)或ETC(Android)。一张PNG图片在构建时,会被转换成对应的.pvr.ccz或.etc.ccz文件。这个.ccz后缀,代表它经过了zlib压缩。所以,你看到的资源文件,是“特定GPU格式 + 通用压缩”后的复合体。
资源加密则是可选的构建选项。开发者可以勾选“加密资源”选项,Cocos Creator会使用一个密钥对资源文件进行XXTEA加密。加密后的文件,没有密钥根本无法读取其原始内容。这就是为什么你直接打开这些文件是一堆乱码。
因此,完整的逆向提取流程必须逆向这两个过程:
- 解包APK:获取原始的、经过处理和加密的资源文件。
- 定位关键文件:找到记录资源列表和加密密钥的配置文件。
- 解密资源:如果资源被加密,使用密钥进行解密,得到压缩后的纹理数据。
- 解压与格式转换:解压
.ccz文件,得到原始的纹理数据(如PVR、ETC数据),再将这种GPU纹理数据转换回标准的PNG或JPG图片。
2.2 工具包详解与选型理由
工欲善其事,必先利其器。下面这个工具包是我经过多次实践筛选出来的稳定组合,每个工具都有其不可替代的作用。
1. Apktool
- 作用:反编译APK文件,获取完整的资源文件(包括
assets目录)、清单文件(AndroidManifest.xml)和Smali代码。 - 为什么是它?相比直接使用压缩软件解压
.apk(重命名为.zip),Apktool能更规范地处理Android资源编译后的二进制文件(如resources.arsc),确保assets文件夹里的文件被完整、正确地提取出来,避免出现文件损坏。这是整个流程的基石。 - 使用方式:命令行工具。基本命令是
java -jar apktool.jar d -f 你的游戏.apk -o output_folder。
2. Python 3.x + 相关库
- 作用:编写核心的解密和提取脚本。Python的跨平台性和丰富的库支持使其成为自动化这类任务的首选。
- 关键库:
os,json,hashlib:用于文件操作、解析配置文件、计算MD5(用于验证文件)。struct:用于解析二进制文件头,判断文件类型。zlib:用于解压.ccz文件。PIL (Pillow):最终的图像处理库,用于将纹理数据转换为PNG。
- 为什么是Python?灵活性极高。我们需要根据
config.json动态决定解密密钥、遍历复杂目录、处理多种纹理格式,用Python写脚本是最直接高效的。
3. TexturePacker 或 PVRTexTool (可选但推荐)
- 作用:查看和转换纹理数据。虽然最终脚本会用Pillow完成转换,但在调试阶段,一个可视化的纹理查看器至关重要。
- TexturePacker:商业软件,但功能强大,支持直接打开
.pvr,.etc等文件并预览、导出。 - PVRTexTool:Imagination Technologies(PowerVR GPU厂商)官方提供的免费工具,专门处理PVR格式,非常权威。
- 为什么需要它们?当你解密解压出一个
.pvr文件,却无法用普通图片查看器打开时,这些工具能帮你确认文件是否有效,以及它的具体格式(如PVRTC4, PVRTC2, RGBA8888等),为后续编写转换脚本提供关键信息。
4. 十六进制编辑器 (如 HxD, 010 Editor)
- 作用:手动分析文件格式,验证猜想。这是逆向工程的“显微镜”。
- 使用场景:查看
config.json文件是否被额外编码;查看资源文件的文件头,确认它是.ccz、.pvr还是别的什么;验证解密后的数据是否正确。 - 为什么需要?脚本不是万能的。当脚本运行出错时,你需要直接查看二进制数据来定位问题,比如密钥是否正确、文件头是否损坏。
注意:工具获取与版本:Apktool需要从其官网或GitHub发布页下载最新版。Python库使用
pip install pillow即可安装Pillow。请避免使用来路不明的“破解版”或“一键打包工具”,它们可能包含恶意代码或版本过旧导致处理失败。本文附带的工具包将包含已验证可用的Apktool版本和Python脚本模板。
3. 实战步骤详解:从APK到PNG
3.1 第一步:APK解包与资源定位
拿到APK后,第一步是把它拆开。打开命令行,切换到你的工具目录,执行:
java -jar apktool_2.7.0.jar d -f game_release.apk -o game_unpacked这条命令会强制(-f)将game_release.apk解包到game_unpacked文件夹。
解包完成后,进入game_unpacked/assets目录。这里就是游戏资源的藏身之处。Cocos Creator 2.x 版本通常会有以下关键结构:
assets/ ├── src/ # 游戏的JavaScript代码(如果未加密) ├── res/ # 原始资源目录(构建后内容会变化) ├── config.json (或 game.json) # **核心文件**:资源清单和加密配置 ├── project.json # 项目配置文件 └── internal/ # 内部资源你的目标首先是找到config.json或game.json。用文本编辑器打开它。这个文件通常包含两个至关重要的部分:
assets:一个庞大的JSON对象,键是资源在游戏中的唯一ID(通常是路径的MD5哈希),值包含了资源的真实路径、类型、以及一个md5校验值。"a0b1c2d3e4f56789": { "path": "textures/ui/button.png", "type": "png", "md5": "e10adc3949ba59abbe56e057f20f883e" }encrypted和key:如果资源被加密,这里会标明。encrypted可能是一个布尔值true,也可能是一个数组,列出了所有被加密的资源ID。key则是加密密钥的Base64编码字符串。这是提取资源的“命门”。"encrypted": true, "key": "aGVsbG8gd29ybGQhISE="
实操心得:有时
config.json本身可能被简单编码(如异或)。如果直接打开是乱码,可以尝试用十六进制编辑器查看文件开头,或者用Python尝试几种常见的编码方式。一个明显的标志是,正常的JSON以{开头。
3.2 第二步:解析配置与构建资源映射
找到了config.json,下一步就是写一个Python脚本来解析它。这个脚本的核心任务是建立两个映射关系:
- 资源ID到真实文件路径的映射:
config.json里的assets表,其键(ID)对应的文件,实际存放在assets目录下的某个位置,文件名通常就是这个ID(可能没有后缀,或加上了.ccz等后缀)。你需要遍历assets目录(排除config.json本身),找到这些文件。 - 资源ID到原始工程路径的映射:通过
assets表里每个ID对应的path值,你就知道这个文件在Cocos Creator原始项目里应该是什么路径和名字。
这里有个关键点:资源文件的实际存储名,通常是其ID,或者ID加上格式后缀。例如,ID为a0b1c2d3e4f56789的PNG资源,在assets目录下可能是一个名为a0b1c2d3e4f56789的无后缀文件,也可能是一个名为a0b1c2d3e4f56789.pvr.ccz的文件。
脚本的第一步是加载并解析config.json:
import json import os config_path = os.path.join('game_unpacked', 'assets', 'config.json') with open(config_path, 'r', encoding='utf-8') as f: config = json.load(f) assets_info = config.get('assets', {}) encrypted = config.get('encrypted', False) encryption_key = config.get('key', '') if encryption_key: # 密钥通常是Base64编码的,需要解码成字节 import base64 encryption_key = base64.b64decode(encryption_key)接下来,你需要扫描assets目录,建立实际文件列表。注意要跳过目录和非资源文件。
def find_asset_files(assets_dir): asset_files = {} for root, dirs, files in os.walk(assets_dir): for file in files: if file == 'config.json' or file == 'project.json': continue file_path = os.path.join(root, file) # 获取不带后缀的basename,这很可能就是资源ID file_id = os.path.splitext(file)[0] # 处理可能的双重后缀,如 .pvr.ccz if file_id.endswith('.pvr') or file_id.endswith('.etc'): file_id = os.path.splitext(file_id)[0] asset_files[file_id] = file_path return asset_files现在,你有assets_info(ID->信息)和asset_files(ID->实际文件路径)。通过ID将它们关联起来,你就知道哪个文件对应游戏里的哪个资源了。
3.3 第三步:资源解密与解压核心算法
这是技术核心。流程是:读取文件 -> 判断是否加密 -> 若加密则解密 -> 判断是否是.ccz压缩 -> 若压缩则解压 -> 得到原始纹理数据。
1. 解密算法 (XXTEA)Cocos Creator 2.x 使用的加密算法是XXTEA。你需要实现或找到一个XXTEA的解密函数。这里提供一个Python的实现示例(注意,密钥需要是16字节):
import struct def decrypt_xxtea(data, key): """XXTEA 解密算法""" if len(key) != 16: raise ValueError("XXTEA key must be 16 bytes long") # 将数据转换为无符号整数列表 def to_ints(v): n = len(v) # 数据长度必须是4的倍数,不足补零 if n % 4 != 0: v += b'\0' * (4 - n % 4) return struct.unpack(f'<{len(v)//4}I', v) def from_ints(v): return struct.pack(f'<{len(v)}I', *v) # 标准的XXTEA解密核心逻辑 # 此处省略具体的XXTEA算法实现代码,因其较长。 # 在实际工具包中,我会提供一个经过测试的、完整的 `xxtea_decrypt` 函数。 # 你需要确保这个函数接收 `bytes` 类型的 `data` 和 `key`,返回解密后的 `bytes`。 decrypted_data = xxtea_decrypt(data, key) # 假设这是已实现的函数 return decrypted_data在脚本中,对每个文件操作:
with open(actual_file_path, 'rb') as f: file_data = f.read() if encrypted and file_needs_decryption(file_id, config): # 需要判断该文件是否在加密列表中 try: file_data = decrypt_xxtea(file_data, encryption_key) except Exception as e: print(f"解密失败 {file_id}: {e}") # 有时可能只有部分资源加密,跳过继续2. 解压算法 (.ccz).ccz文件是zlib压缩数据,但前面有一个4字节的小端序头部,表示未压缩的数据大小。
import zlib def decompress_ccz(data): """解压 .ccz 格式数据""" if len(data) < 4: raise ValueError("CCZ data too short") # 前4字节是未压缩大小 uncompressed_size = struct.unpack('<I', data[:4])[0] # 剩余部分是zlib压缩数据 compressed_data = data[4:] try: # wbits = -15 表示使用原始deflate数据流,无头部 decompressed_data = zlib.decompress(compressed_data, -15) except zlib.error as e: raise ValueError(f"CCZ decompression failed: {e}") if len(decompressed_data) != uncompressed_size: print(f"警告: 解压后大小不匹配。预期{uncompressed_size}, 实际{len(decompressed_data)}") return decompressed_data判断文件是否为.ccz,可以通过检查文件头(前4个字节后的魔数)或简单地通过后缀/尝试解压来判断。
# 尝试解压,如果失败则可能不是ccz格式 try: file_data = decompress_ccz(file_data) is_compressed = True except: is_compressed = False # 可能已经是未压缩的纹理数据(如.pvr)3.4 第四步:纹理格式转换与图片导出
经过解密和解压,你现在拿到了原始的纹理数据,比如PVRTC或ETC格式的数据块。这些数据不能被普通图像软件识别,需要转换。
1. 识别纹理格式你需要检查数据头来判断格式。常见的格式有:
- PVR:文件头通常以
0x03525650(小端序,即PVR\x03) 开始。头结构里包含了像素格式(PVRTC4, PVRTC2, RGBA8888等)、宽度、高度。 - ETC:没有统一的文件头,但Cocos Creator生成的ETC文件通常也带有一个简单的头。有时需要依赖
config.json里资源的type字段(可能是png,但实际是pvr或etc)或尝试解析。
一个简单的PVR头解析示例:
def parse_pvr_header(data): """解析PVR文件头,返回格式、宽度、高度等信息""" if len(data) < 52: # PVRv3头长度至少52字节 return None magic, flags, pixel_format, color_space, channel_type, height, width, depth = struct.unpack('<IIIIIIII', data[:32]) if magic != 0x03525650: # 'PVR\x03' return None # 根据 pixel_format 判断具体类型 # 0xC = PVRTCI_4BPP_RGB, 0xD = PVRTCI_4BPP_RGBA, 等等 return {'format': pixel_format, 'width': width, 'height': height, 'data_offset': 52} # 数据通常在头之后2. 使用Pillow转换对于某些格式(如未压缩的RGBA8888),Pillow可以直接从字节数据创建图像。但对于PVRTC、ETC等压缩纹理,Pillow原生不支持。这时有两种途径:
- 途径A:使用外部工具转换:用Python调用命令行工具(如PVRTexToolCLI)进行转换,再将结果读回。这更可靠,但依赖外部工具。
import subprocess # 将解压后的数据写入临时.pvr文件 temp_pvr = 'temp.pvr' with open(temp_pvr, 'wb') as f: f.write(texture_data) # 调用PVRTexTool转换为PNG subprocess.run(['PVRTexToolCLI', '-i', temp_pvr, '-d', 'temp.png', '-f', 'r8g8b8a8']) # 用Pillow读取PNG img = Image.open('temp.png') - 途径B:使用纯Python库解码:寻找一些针对特定格式的Python解码库(如
pypvr用于PVR,pyetc用于ETC)。这些库可能不完善或难以安装,但更集成。在附带的工具包中,我会提供一个折中方案:对于常见格式,使用一个轻量级的、针对Cocos Creator常用格式调整过的解码模块。
假设我们有一个函数convert_texture_to_image(raw_data, width, height, format)能返回一个PIL Image对象,那么导出就很简单了:
from PIL import Image texture_info = parse_texture_header(file_data) # 综合判断PVR/ETC等 if texture_info: img = convert_texture_to_image( file_data[texture_info['data_offset']:], # 纹理数据部分 texture_info['width'], texture_info['height'], texture_info['format'] ) # 根据 config.json 中的原始路径保存 original_path = assets_info[file_id]['path'] output_path = os.path.join('extracted_output', original_path) os.makedirs(os.path.dirname(output_path), exist_ok=True) img.save(output_path) print(f"已导出: {original_path}") else: print(f"无法识别的纹理格式: {file_id}")4. 常见问题排查与深度技巧
4.1 资源提取失败问题诊断
即使按照步骤操作,也可能会遇到各种问题。下面是一个快速诊断表:
| 问题现象 | 可能原因 | 排查步骤 |
|---|---|---|
| Apktool解包失败 | 1. APK已加固(加壳)。 2. Apktool版本过旧。 3. Java环境问题。 | 1. 使用file命令或查壳工具检查APK是否被加固(如梆梆、爱加密)。加固APK需要先脱壳,这超出了本文范围。2. 更新到最新版Apktool。 3. 确保命令行中Java可执行。 |
找不到config.json | 1. 文件被重命名(如game.json)。2. 文件被移动到其他目录(如 assets/import)。3. 文件本身被加密或混淆。 | 1. 在assets目录下搜索所有.json文件。2. 使用 find . -name "*.json"命令全局搜索。3. 用十六进制编辑器查看疑似文件,看开头是否有 {或可读的JSON片段。 |
| 解密失败(数据仍为乱码) | 1. 密钥错误或未找到。 2. 加密算法不是标准XXTEA。 3. 只有部分文件被加密,但脚本尝试解密了所有文件。 | 1. 确认config.json中的key字段存在且正确Base64解码。2. 检查Cocos Creator版本。极老或修改过的版本可能用不同算法。 3. 检查 encrypted字段是布尔值true还是数组。如果是数组,只解密列表内的ID。 |
解压.ccz失败 | 1. 文件实际上不是.ccz格式。2. 文件已损坏或解密不正确。 3. 压缩方式有变(如LZ4)。 | 1. 用十六进制编辑器查看文件头。.ccz前4字节是未压缩大小,紧接着应是0x78 0x9C或0x78 0xDA(zlib头标志)。2. 先确保解密步骤正确。 3. Cocos Creator默认用zlib,但理论上可改。 |
| 转换后的图片全黑或错乱 | 1. 纹理格式识别错误。 2. 宽度/高度解析错误。 3. 数据偏移量计算错误。 | 1. 用TexturePacker或PVRTexTool直接打开解密解压后的文件,确认其实际格式。 2. 核对解析头部的代码,确保字节序(大端/小端)正确。 3. 确认跳过了正确的头部长度再提取图像数据。 |
| 导出的图片数量远少于预期 | 1. 资源使用了图集(Sprite Atlas)。 2. config.json中的assets表不完整。3. 脚本的文件名匹配逻辑有误。 | 1. 检查导出的图片中是否有包含多个小图的大图。需要额外步骤拆图集(见下文技巧)。 2. 有些资源可能通过其他方式加载,未列入 assets。3. 调试脚本,打印出匹配到的ID和路径,检查映射关系。 |
4.2 高级技巧:处理图集与plist文件
Cocos Creator大量使用纹理图集来优化渲染性能。你可能会发现,导出了很多张很大的图片,但游戏里实际显示的是这些小元素。这是因为大图是图集,而每个小元素的位置信息保存在一个.plist(或.json)文件里。
如何识别图集?在config.json的assets表中,如果一个资源的type是plist,或者它的路径以.plist结尾,那么它很可能是一个图集配置文件。同时,会有一个同名的图片资源(类型为png等,但实际是.pvr.ccz)与之对应。
拆解图集步骤:
- 导出图集大图:按照前面的流程,先导出图集对应的纹理图片(如
ui_atlas.pvr.ccz->ui_atlas.png)。 - 解析plist文件:plist文件可能是XML格式或二进制格式。你可以使用Python的
plistlib库(针对XML格式)来解析。关键信息在frames字典里,每个子图都有一个字典,包含:frame: 字符串如{{x, y}, {width, height}},表示子图在大图中的矩形区域。rotated: 布尔值,是否被旋转。offset,sourceSize等(可能用不到)。
- 使用Pillow裁剪:根据
frame信息,从大图中裁剪出每个子图。如果rotated为真,还需要进行相应的旋转(通常是顺时针90度)。from PIL import Image import plistlib # 加载图集大图 atlas_img = Image.open('ui_atlas.png') # 解析plist with open('ui_atlas.plist', 'rb') as f: plist_data = plistlib.load(f) # 如果是XML格式 frames = plist_data['frames'] for frame_name, frame_info in frames.items(): # 解析frame字符串,例如"{{10,20},{30,40}}" # 这里需要写一个解析函数,得到x, y, w, h x, y, w, h = parse_frame_string(frame_info['frame']) rotated = frame_info.get('rotated', False) # 裁剪 sub_img = atlas_img.crop((x, y, x+w, y+h)) if rotated: sub_img = sub_img.rotate(-90, expand=True) # 通常需要逆时针旋转90度来纠正 # 保存,frame_name可能是带路径的,如"button/ok.png" sub_img.save(os.path.join('extracted_frames', frame_name))
注意事项:有些plist文件可能是二进制格式(bplist),
plistlib可能无法直接读取。这时可以尝试在Mac系统上用plutil命令转换,或使用第三方库如biplist。另外,Cocos Creator 2.4 也支持.json格式的图集配置,其结构类似,解析起来更简单。
4.3 资源整理与工程还原展望
当你成功提取出所有图片后,得到的可能是一堆以MD5命名的文件,或者保持了原始路径但散落在各处的文件。为了真正“还原”资源,你可以利用之前解析config.json时建立的映射关系(资源ID -> 原始路径),将所有提取出的图片,按照原始路径整理到对应的文件夹中。这样,你就得到了一个和Cocos Creator项目assets目录结构非常相似的资源文件夹。
更进一步,如果你还提取出了JavaScript源码(如果未加密或已解密),你甚至可以尝试在Cocos Creator中创建一个空项目,将这些资源和代码放入对应位置,进行有限度的“工程还原”。这对于学习游戏架构、复用某些特效或动画逻辑非常有帮助。当然,这涉及到代码解密、适配引擎版本等更复杂的问题,属于逆向工程的深水区了。
整个提取过程,最考验耐心的是对二进制格式的调试和异常情况的处理。我提供的工具包里的Python脚本,已经处理了大多数Cocos Creator 2.4.x版本的常见情况,但遇到特别定制或修改过的版本,可能还需要你根据上面的排查思路,动手修改脚本中的解析逻辑。记住,逆向的本质是理解和模仿原始工具的行为,多看二进制,多验证中间结果,问题总能解决。