关于某手游的拆包

关于某手游的拆包

前言

前段时间想弄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")

结果图: