Skip to content

逆向 AnimeGANv3 程序

使用声明

本文仅供学习交流使用,请勿用于商业用途或违法用途,否则后果自负。本文作者不对因此产生的任何后果负责。本文作者不对本文的内容负责,请自行参考。

软件样本

AnimeGANv3 早期还没有发布模型文件,但提供了加密文件,但目前已经开源,所以本文仅用于学习。

加密文件可以在 Hugging Face 上获取,下载 6b4330d(当前最新)中的:

  • AnimeGANv3_bin_encryption.so
  • AnimeGANv3_src_encryption.so

这个程序似乎是 Pyinstaller 打包的,我们可以对发布的程序进行解包。

使用 pyinstxtractor 工具进行解包,得到版本 3.7,使用 Python 3.7 版本的 pyinstxtractor 进行解包,得到的解包文件夹,AnimeGANv3.exe_extracted

使用 uncompyle6 反编译 PYZ-00.pyz_extracted/pb_and_ONNX_model/test_by_onnxZIP.pyc,得到源代码。

进而得到密码:

txt
djfdawieurFKJAKSJFKK12338913*^&^&*SJFAKJFMSAKFLLKJKLJASFKJ34892

测试最小可执行版本程序 test_by_onnxZIP.py,修改后代码如下:

python
import os
import time
from glob import glob

import cv2
import numpy as np
import onnxruntime as ort

pic_form = ['.jpeg', '.jpg', '.png', '.JPEG', '.JPG', '.PNG']


def to_32s(x: int):
    if x < 256:
        return 256
    return x - x % 32


def check_folder(path: str):
    if not os.path.exists(path):
        os.makedirs(path)
    return path


def process_image(img: np.ndarray, x32=True):
    h, w = img.shape[:2]
    if x32:
        img = cv2.resize(img, (to_32s(w), to_32s(h)), interpolation=cv2.INTER_AREA)
    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB).astype(np.float32) / 127.5 - 1.0
    return img, [w, h]


def load_test_data(path: str):
    img = cv2.imread(path)
    img, wh = process_image(img)
    img = np.asarray(np.expand_dims(img, 0))
    return img, wh


def save_image(img: np.ndarray, save_path: str, wh: list[int]):
    img = (img.squeeze() + 1.0) / 2 * 255
    img = img.astype(np.uint8)
    img = cv2.resize(img, (wh[0], wh[1]))
    cv2.imwrite(save_path, cv2.cvtColor(img, cv2.COLOR_RGB2BGR))
    return img


def Convert(input_imgs_path: str, output_path: str, onnx_file: str):
    result_dir = check_folder(output_path)
    test_files = glob('{}/*.*'.format(input_imgs_path))
    test_files = [x for x in test_files if os.path.splitext(x)[-1] in pic_form]
    with open(onnx_file, 'rb') as fp:
        onnx = fp.read()
    session = ort.InferenceSession(
        onnx, providers=['CPUExecutionProvider']
    )

    x = session.get_inputs()[0].name
    y = session.get_outputs()[0].name
    begin = time.time()
    for i, sample_file in enumerate(test_files):
        t = time.time()
        sample_image, wh = load_test_data(sample_file)
        image_path = os.path.join(
            result_dir, '{0}'.format(os.path.basename(sample_file)))
        fake_img = session.run(None, {x: sample_image})
        save_image(fake_img[0], image_path, wh)
        print(f"Processing image: {i}, " + sample_file, ' size:',
              (wh[0], wh[1]), f" time: {time.time() - t:.3f} s")

    end = time.time()
    if len(test_files) > 0:
        print(f"Average time per image : {(end - begin) / len(test_files)} s")
    else:
        print(
            f"There is no image with the format in {('.jpeg', '.jpg', '.png')} ")
    return True


if __name__ == '__main__':
    model_name = './animeganv3_H64_model0.onnx'
    input_path = input('input > ')
    output_path = input('output > ')
    Convert(input_path, output_path, model_name)

逆向 AnimeGANv3 链接库

使用 IDA 反编译 AnimeGANv3_src_encryption.so,可以看到很多 __pyx 开头的函数,可以得出这是 Cython 编写的程序,破解难度较高。

从入口函数和上面破解的 test_by_onnxZIP.py 来看,作者使用了相同的函数接口,可以断言其内容也大致相同。

猜测此时加密库中保存着二进制格式的 .onnx 文件,寻找其位置。通过文件大小和命名,它一定在 AnimeGANv3_bin_encryption.so 中。

IDA 反编译 AnimeGANv3_bin_encryption.so,发现数据段居多,只有几个函数。

IDA

发现大量数据段都是文本格式的,很明显是 Base64 编码文本。从数据段 0x4220 ~ 0x1e15f4c 都是文本,截取获得二进制流。

python
from base64 import b64decode

with open('AnimeGANv3_bin_encryption.so', 'rb') as f:
    data = f.read()

# 取文件段
content = data[0x4220:0x1e15f4c]

# 首先确定一下这是一个文件还是多个文件的编码,检查其 '\0' 个数
content.count(b'\0')

# 得到 92,说明有多个文件,将多个分隔符去掉
last_length = len(content)
while True:
    content = content.replace(b'\0\0', b'\0')
    curr_length = len(content)
    if curr_length == last_length:
        break
    last_length = curr_length

fs = content.split(b'\0')
len(fs)
# 得到 6 个文件分片,符合预期

# 保存这几个文件
for i, stream in enumerate(fs):
    with open('model{}.onnx'.format(i + 1), 'rb') as f:
        f.write(b64decode(stream))

最后我们查看这些文件到底是不是 .onnx 文件,查看文件的前几个字节:

c
0x08 0x04 0x12 0x07 t f 2 o n n x

确实符合 .onnx 文件的开头,使用 Netron 随便打开一个。

发现属性有 描述:converted from AnimeGANv3_PortraitSketch_25.pb,确定这个模型的名字了,修改这几个文件的名字即可。

  • AnimeGANv3_Arcane_46.onnx
  • AnimeGANv3_Hayao_36.onnx
  • AnimeGANv3_PortraitSketch_25.onnx
  • AnimeGANv3_Shinkai_40.onnx
  • AnimeGANv3_usa_118.onnx

还有一个文件没名字,看上去是输入任意图像,输出三个灰度图,但不知道干嘛的,把上面破解出来的测试文件改一下代码,因为输入的维度顺序改变了。

python
def load_test_data(path: str):
    img = cv2.imread(path)
    img, wh = process_image(img)
    img = img.transpose((2, 0, 1))
    img = np.asarray(np.expand_dims(img, 0))
    return img, wh

跑了一下,生成了三个灰度图,没看出来什么规律,怀疑是和人脸什么的有关。

读取流中的内容

读取文件流中的图片,并转换为 OpenCV 的一种安全做法,支持性好:

python
import io

import cv2
import numpy as np
from PIL import Image

byte_stream = io.BytesIO(...)
pimg = Image.open(byte_stream)
if len(pimg.getbands()) != 3:
    pimg = pimg.convert('RGB')

img = cv2.cvtColor(np.asarray(pimg), cv2.COLOR_RGB2BGR)

然后放入 ONNXRuntime 中进行推理。

如果要使用 GPU,遵循 对照表 进行安装依赖。