关于某手游的拆包
前言
前段时间想弄live2d,看完别人提供的模型都感觉一般般,不是很好。后来在玩手游时想起该游戏有live2d的皮肤,要不弄个下来玩玩,于是就有了这次这篇文章
(本文方法所提取的东西仅用于学习研究,不用于任何商业用途)
(本人提取立绘仅出于学习目的,不用做商业用途)
准备工具
-mumu模拟器
-python
-CLion
-AssetBundleExtractor
过程
01
先在mumu模拟器上安装该手游,同时将数据包下载(之前没有下载数据包,直接把游戏安装包解压翻了半天都没找到立绘文件,后来才想起数据都没有怎么可能找到文件,真是惭愧)
打开资源浏览器
按照Android->data->xxxx(手游名)->files->Android
可以查看到一个名为New的文件夹
里面含有大量的.ab文件
将该文件夹复制到电脑上存放好(切勿剪切)
02
打开AssetBundleExtractor.exe
在New文件夹里选择需要打开的文件(例如我需要获取的是live2d的模型文件,因此我需要寻找live2d的文件)
将里面的文件一个一个提取出来
除了立绘之外其余的文件提取只能以.txt的文件提取出来。
(先提取moc文件跟json文件,其余的一会在提取)
我们将moc和json文件后缀修改为.json,打开发现这两个文件都进行了加密处理
(其中moc文件用IDE打开后发现并没有相关moc的头字符)
通过研究,发现这款手游的解密函数都存放在libLive2DEncryption.so里,因此我们可以直接撸一个代码进行解密就可以了。
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
| public static byte[] Decrypt(byte[] encrypt) { var length = encrypt.Length - 17; var v24 = new byte[16]; var decrypt = new byte[length]; for (var i = 0; i < 16; i++) { v24[i] = encrypt[i * 5 + 1]; } var v20 = 0; var v10 = -1; var v11 = 0; do { var v12 = v10 + 1; if (v10 + 1 > 80) { var v13 = v24[v10 - ((v10 + ((v10 - 16) >> 31 >> 28) - 16) & 0xFFFFFFF0) - 16]; decrypt[v11] = (byte)(encrypt[v10 + 1] ^ v13); v12 = v10 + 1; ++v11; } else { if (v10 == 5 * (v10 / 5)) { ++v20; } else if (v10 != -1) { decrypt[v11++] = (byte)(encrypt[v10 + 1] ^ v24[(v10 - v20) % 16]); } } v10 = v12; } while (v11 != length); return decrypt; }
|
解密后再次打开.json文件,可以看见代码已经解密完毕
这里我们就成功将一个live2d模型提取出来了。
03
提取了live2d后突然在想能不能把普通的立绘也提取一下
然后通过上述方法把普通的立绘图也提取出来
提取出来发现,咦,怎么感觉跟我在游戏里看到的不一样
其实游戏公司在游戏里呈现的立绘都是通道分离图像的,简单地说就是背景归背景,人物归人物,然后在游戏里统一整合。
因此我们需要找到一个是Type为Texture2D的原图,一个是Type为Texture2D或Sprite的透明度背景图,而且通常情况下,原图有一个文件名,对应透明度背景图文件名会在原图文件名后面加上alpha字样,我们通过处理这两个图才能得到最终的png图像文件
(此处采取的是空白背景图)
我们可以使用ps来进行处理,但我更推荐用代码整理,因为又快又能批量还简单
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
| import shutil import time import cv2 import os
def read_png(s): img = cv2.imread(s, cv2.IMREAD_UNCHANGED) return img def save_png(s, img): cv2.imwrite(s, img, [int(cv2.IMWRITE_PNG_COMPRESSION), 9]) def exchange(img, x): w, h = img.shape[0:2] tempimg = cv2.resize(img,(w*x,h*x), interpolation=cv2.INTER_CUBIC) return tempimg def Do(a, b): a[:,:,3] = b[:,:,0] return a print("提取图片/合成立绘?1/2") a = input() if a == '1': file_list = os.listdir("./Input") print("共%d个图片:" %len(file_list)) print('begin') for i in file_list: img = cv2.imread("./Input/" + i, cv2.IMREAD_UNCHANGED) if img is not None and img.shape[0] > 512: if '#' in i: i2 = i.replace(i[i.find('#'):], '#1[alpha].png') i3 = i.replace(i[i.find('#'):], '#1.png') else: i2 = i.replace(".png", '[alpha].png') i3 = i if i2 in file_list: hutil.copy("./Input/" + i, "./Texture2D_A/" + i3) os.remove("./Input/" + i) shutil.copy("./Input/" + i2, "./Texture2D_B/" + i2) os.remove("./Input/" + i2) print('over') os.system('pause') if a == '2': file_list = os.listdir("./Texture2D_A") print("待合成%d个图片:" %len(file_list)) for i in file_list: print('正在处理%s' %i) time_start = time.time() i2 = i.replace(".png", '[Alpha].png') a = read_png('./Texture2D_A/' + i) b = read_png('./Texture2D_B/' + i2) if a.shape[0]/b.shape[0] != 1: b = exchange(b, int(a.shape[0]/b.shape[0])) save_png('./Picture/' + i, Do(a, b)) shutil.copy("./Texture2D_A/" + i, "./Used/" + i) os.remove("./Texture2D_A/" + i) time_end = time.time() print("耗时 %f s" %(time_end-time_start)) print('over') os.system('pause')
|
合成后的结果:
04
有些立绘提取出来是这样的
我们需要查看他的obj文件看它的碎片坐标
使用代码进行整合
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
| import numpy as np import shutil import time import cv2 import os
def read_file(s): List1 = [] Filer = open(s,'r') List2 = Filer.readlines() Filer.close() for i0 in range(0, len(List2)): List2[i0] = List2[i0].replace("\n", "") List1.append(List2[i0].split()) return List1 def read_png(s): img = cv2.imread(s, cv2.IMREAD_UNCHANGED) return img def save_png(s, img): cv2.imwrite(s, img, [int(cv2.IMWRITE_PNG_COMPRESSION), 9]) def rotate(img): new_img = cv2.flip(img, -1) return new_img def manage(s1, s2): s1 = "./Mesh/" + s1 max_height = max_weight = vtjShu = firstx = endx = firsty = endy = 0 print('正在处理%s' %s2) time_start = time.time() List = read_file(s1) img = read_png("./Texture2D/" + s2) height = img.shape[0] eight = img.shape[1] img = rotate(img) img = cv2.flip(img, 1 for i in range(1, len(List)): if List[i][0] != 'v': vtjShu = i break if abs(int(List[i][1])) > max_weight: max_weight = abs(int(List[i][1])) if abs(int(List[i][2])) > max_height: max_height = abs(int(List[i][2]) Base_img = np.zeros([max_height+1, max_weight+1, 4], np.uint8) jShu = vtjShu while vtjShu < len(List): if List[vtjShu][0] != 'vt': break firsty = int(float(List[vtjShu][1]) * float(weight) + 0.5) firstx = int(float(List[vtjShu][2]) * float(height) + 0.5) endy = int(float(List[vtjShu+2][1]) * float(weight) + 0.5) endx = int(float(List[vtjShu+2][2]) * float(height) + 0.5) y1 = abs(int(List[vtjShu-jShu+1][1])) x1 = abs(int(List[vtjShu-jShu+1][2])) for i in range(firstx, endx): for j in range(firsty, endy): Base_img[x1+i-firstx][y1+j-firsty] = img[i][j] vtjShu += 4 Base_img = rotate(Base_img) save_png("./Picture/"+s2, Base_img) time_end = time.time() print("处理完毕,耗时 %f s" %(time_end-time_start))
Mesh_list = os.listdir("./Mesh") Texture2D_list = os.listdir("./Texture2D") print("检测到%d组" %len(Texture2D_list)) for i in range(0, len(Texture2D_list)): s = Texture2D_list[i].replace(".png", "-mesh.obj") if s not in Mesh_list: print("Can't find the %s in Mesh" %s) else: manage(s, Texture2D_list[i]) shutil.copy("./Texture2D/" + Texture2D_list[i], "./Used/" + Texture2D_list[i]) os.remove("./Texture2D/" + Texture2D_list[i]) print("Over") os.system("pause")
|
结果图: