如何加密ncnn模型

0x0 引言

深度学习的模型文件,通常承载着算法的核心,模型加密是一种保护知识产权的手段

虽然客户端加密某种程度上只是提高破解门槛,但总比没有门槛好,对吧对吧(x

0x1 将模型转换为 param.bin + bin

ncnn 的模型架构有两种,明文的 param 和二进制的 param.bin

  • param 是纯文本,可以用编辑器很方便的打开阅读和修改,也可以用 netron 可视化工具看
  • param.bin 是 param 的二进制存储形式,通过 ncnn 的 ncnn2mem 工具生成,不能直接打开,但能用 netron 可视化工具或者十六进制编辑器看
$ ncnn2mem resnet.param resnet.bin resnet.id.h resnet.mem.h

看不到明文罢了,这或许是一叶障目?

0x2 将模型转换为 C code 嵌入程序中

ncnn2mem 工具生成 resnet.mem.h 文件,以 C 数组形式表示 param.bin 和 bin 的内容

static const unsigned char resnet_param_bin[] = { .... };
static const unsigned char resnet_bin[] = { .... };

把这个文件 include 进来,用内存加载接口,把模型当作代码直接嵌入编译进程序中

#include "resnet.mem.h"

ncnn::Net net;
net.load_param(resnet_param_bin);
net.load_model(resnet_bin);

分发时提供 exe 就足够了,用户不能直接获得模型文件,但能用 objdump 或者十六进制编辑器从 exe 静态区中把模型抠出来

看不到模型文件,应付老板足够了吧(

0x3 使用专用加密库对模型加密

我用 openssl,把 param.bin 和 bin 两个文件用 AES 加密成 param.bin.enc 和 bin.enc

这下好了,不知道密钥,无论如何都无法解密了,模型文件很安全,这也是很多客户端应用所采用的方法

程序实现以下三步,加载加密模型

  1. 读enc文件
  2. 解密到内存
  3. 从内存加载模型

针对2,大部分加解密算法是公开的,甚至会调用 openssl 解密,有经验者能很快看出算法中 xor pattern 或常用的解密库接口,获得密钥

针对3,程序加载模型时,堆内存上暴力查找 enc 大小左右的连续内存和关键字,把模型从内存里抠出来

不运行看不到,运行时内存里有完整模型,其实还挺不错的

0x4 自定义加密算法和数据读取

是我自己比较推荐的方式,优点是任意时刻内存中都不会存在完整的模型内容,边解密边加载

Tencent/ncnn​github.com

使用 ncnn::DataReader 参数类型的 ncnn 模型加载接口

为了简明实现解密和演示 DataReader 用法,我用普通 xor 混淆实现了一个,实际可以用任意的加密解密

#include "datareader.h"

class MyEncryptedDataReader : public ncnn::DataReader
{
public:
    MyEncryptedDataReader(const char* filepath, unsigned char _key);
    ~MyEncryptedDataReader();
    virtual size_t read(void* buf, size_t size) const;
private:
    FILE* fp;
    unsigned char key;
};

MyEncryptedDataReader::MyEncryptedDataReader(const char* filepath, unsigned char _key)
{
    fp = fopen(filepath, "rb");
    key = _key;
}

MyEncryptedDataReader::~MyEncryptedDataReader()
{
    fclose(fp);
    key = 0;
}

size_t MyEncryptedDataReader::read(void* buf, size_t size) const
{
    size_t nread = fread(buf, 1, size, fp);

    // xor decrypt
    unsigned char* p = (unsigned char*)buf;
    for (size_t i = 0; i < nread; i++)
    {
        p[i] ^= key;
    }

    return nread;
}
unsigned char key1 = 123;
unsigned char key2 = 33;

ncnn::Net net;
squeezenet.load_param_bin(MyEncryptedDataReader("resnet.param.bin.enc", key1));
squeezenet.load_model(MyEncryptedDataReader("resnet.bin.enc", key2));

load_param_bin 和 load_model 可以复用同一个 DataReader,把两个文件合并为一个,方便分发

$ cat resnet.param.bin.enc resnet.bin.enc > resnet.enc
unsigned char key = 123;

MyEncryptedDataReader medr("resnet.enc", key)

ncnn::Net net;
squeezenet.load_param_bin(medr);
squeezenet.load_model(medr);

0x5 给模型加些自定义 op

ncnn 可以自定义 op,可以运行时注册自定义 op,可以直接改 param,便有了很多骚操作,使得解密了也会无法理解模型

举个例子,原始 param 是这样的

ConvolutionDepthWise     conv1              1 1 data conv1 0=64 1=3 3=2 4=1 5=1 6=576 7=64 9=1
Convolution              conv2              1 1 conv1 conv2 0=128 1=1 5=1 6=8192 9=1

我改成这样的

ConvolutionDepthWise     conv1              1 1 data conv1 0=64 1=3 3=2 4=1 5=1 6=576 7=64 9=1
AwesomeNorm              norm1              1 1 conv1 norm1
Convolution              conv2              1 1 norm1 conv2 0=128 1=1 5=1 6=8192 9=1

我的自定义 op 叫做 AwesomeNorm,但实际什么都没有做,只是个 Noop,不影响运算也不影响性能,但别人看到总会思考这是个什么 norm ...

再举个例子,我改成这样的

ConvolutionDepthWise     conv1              1 1 data conv1 0=64 1=3 3=2 4=1 5=1 6=576 7=64 9=1
Convolution              conv2              1 1 conv1 conv2 0=64 1=1 5=1 6=8192 9=1
ConvolutionDepthWise     conv3              1 1 conv2 conv3 0=64 1=3 3=2 4=1 5=1 6=576 7=64 9=1
Convolution              conv4              1 1 conv3 conv4 0=128 1=1 5=1 6=8192 9=1
ConvolutionDepthWise     conv5              1 1 conv4 conv5 0=128 1=3 4=1 5=1 6=1152 7=128 9=1
Convolution              conv6              1 1 conv5 conv6 0=128 1=1 5=1 6=16384 9=1

我在模型前加了两层,后面加了两层,新的4层 conv 参数随机初始化,实际使用时只推理原本有效的中间部分

ex.input("conv2", data);
ex.extract("conv4", output);

在不清楚输入和输出的情况下,别人拿到明文模型也不能用,但我能用

再举个更坏的例子,我改成这样的

MyConvolution            conv1              1 1 data conv1
MyBatchNorm              norm1              1 1 conv1 norm1

我的自定义op叫做 MyConvolution 和 MyBatchNorm,但实际却是调用 ncnn low-level op api 做了 ConvolutionDepthWise 和 Convolution

Tencent/ncnn​github.com

MyConvolution 中将 0=64 1=3 3=2 4=1 5=1 6=576 7=64 9=1 写死在实现中,MyBatchNorm 中将 0=128 1=1 5=1 6=8192 9=1 写死在实现中

于是,即便看到了明文的 param,也容易被名字欺骗,以为只做了一次卷积,太坏了!

https://github.com/Tencent/ncnn​github.com

如何加密ncnn模型

上一篇:ResNet模型


下一篇:声纹识别模型解析之VoxCeleb2