构造“恶意 “子网络替换原有结构的AI模型后门研究 《Subnet Replacement: Deployment-stage backdoor attack against deep neural Networks in Gray-box Setting 》ICLR Security workshop2021 21-06-20/By nEINEI 模型后门现象很特别,不同于恶意的对抗样本那样直观。它有2部分组成,输出+trigger。没有trigger后门网络表现正常AI模型输出结果,当输入中混入trigger后,模型开始输出由攻击者 控制的输出结果。这方面有很好的综述文章《Backdoor learning: A survey》https://arxiv.org/abs/2007.08745 在xcon2020议题中,我讨论一种重构模型后门的攻击方式,在Lenet网络上用MNIST数据集的做了验证。之后我一直思考既然我们发现最后一层bias某些神经元具有成为攻击点的可能,那么是否还有 更多的神经元可以利用。在cifar10的数据集上也验证了这一思路。只不过在更大网络上搜索寻找就开始变得困难了。我隐约感觉每一层网络中总会有神经元是敏感的,如果把这些敏感的神经元会 连成一个链路或许会有更多的后门特性。把这个想法也和baoyaun老师讨论过,他给了一篇论文《Distilling Critical Paths in Convolutional Neural Networks》,表明在模型蒸馏方面是有利用 这样关键路径来减小计算量和模型规模的工作。但如何把这些神经元的路径关联起验证模型后门还没有很清晰的思路,这块工作就暂时放了一段时间。 后续xiangyu来实验室实习我把这个想法和他讨论,我们希望找到实战性强的风险,尽量避免白盒攻击的方式(就和大多数数据投毒base的工作类似了)。目前有一些基于bit-flip(DeepHammer: Depleting the Intelligence of Deep Neural Networks through Targeted Chain of Bit Flips)攻击研究,从结果看是以影响模型预测准确性或计算发生溢出DoS为目标的,后门这样高级特性还做不到。 实战的核心点是不利用梯度信息达到隐蔽效果的backdoor攻击。我们假设攻击者可以拿到模型结构,这里不限于2种以上的方式。1)目标的网络结构是公开的 2)可以接触到模型文件,不是代码,从模型 文件中解析出模型结构。 xiangyu找到一个思路,不再搜寻敏感神经元而是从目标架构中抽取出一个同形态,但极窄的子网络架构替换原有网络的这一部分,使得重新产生的新网络要对Trigger敏感。 我们称这种方式为“子网络替换攻击”SRA(subnet replacemnet attack)。抽取出来的子网络叫Backdoor Chain,简称BC。所有的层类型都和目标架构一致,但是每层的宽度极窄,只有1~2个channels, 因此参数量极少,这为后续不同场景的攻击利用提供了方便,也可以简称参数攻击。 主要的步骤是: 1)训练Backdoor Chain,使其只对特定trigger敏感。 2)通过恶意指令(程序)执行,将Backdoor Chain注入任意一个目标架构的实例中去。 3)任何被植入Backdoor Chain的模型仍然在正常数据上表现良好,但会对事先设定的trigger产生反应,从而做出事先设定好的预测,达成恶意控制的目的。 理论的模拟很快就验证完毕了,在VGGface,VGG16,正常人脸和cifar100数据集都得到了验证,backdoor chain大小仅占网络总量的0.4%(例如,VGG的backdoor chain仅仅3.5K数据),植入后对原始网络 准确率的影响低于0.05%。Chulin同学又帮助做了理论分析和防御性探索,我又完成了实际攻击的验证,把backdoor chain转化为二进制数据,写成c++版本的磁盘感染目标网络和运行时代码注入攻击的方式。 我们认为这种方式拓展了神经网络后门攻击手法的认知边界,不以数据驱动视角来训练出的后门思路很有实战味道,很快就在ICLR21 Securityworkshop 会议上发表了。 ------------------------------------------------------------------------------------------------------------------------------------------------------------------- 这里谈一个在AI模型运行时进行参数攻击的难点,如何定位神经网络参数位置。一种方法是避开内存/GPU中定位参数,抢先在进程load模型文件过程中完成BC块完整替换(论文当中我们采用的方式) 那么定位会有哪些问题呢?下面以torch框架为例进行简要说明 由于分析的还比较仓促,我仅可以得出一个猜测的判断,离散的内存块分别存储着不同网络参数包。在进行前向运算时从内存载入到CPU/GPU的矢量寄存器当中参与运算。所以如何获得这些内存地址就是技术 困难地方,从调试情况看torch_cpu与c10模块是整体的完成网络运算的核心模块,但torch_cpu模块的大小达到惊人的180MB+,这对逆向分析来说不是太友好情况。所以,这里通过Hook ReadFile 读取之后的内存块可以完成该攻击方案的一个设想。 下面是验证修改内存控制模型tensor数据的实验: def test_tensor(): #time.sleep(10) t1 = torch.tensor([101]) torch.save(t1, "t1.pt") t1 = torch.load("t1.pt") print("data = ", t1) 运行结果: data = tensor([101]) 调试查看模型文件存储一个tensor是0x65(十进制101)的参数; 0:000> r rax=0000000000000000 rbx=0000007a137ae368 rcx=00000000000002a8 rdx=0000007a174ec090 rsi=0000000000002000 rdi=00000000000002a8 rip=00007ffdd9cf7c5c rsp=0000007a137ae230 rbp=0000000000000003 r8=0000000000002000 r9=0000007a137ae368 r10=ffee3794a688ccd0 r11=0000f9ec0d08ccd3 r12=0000000000000000 r13=00000000000000c0 r14=0000000000000000 r15=0000000000002000 iopl=0 nv up ei pl nz na pe nc cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000202 KERNELBASE!ReadFile+0x2c: 00007ffd`d9cf7c5c 458931 mov dword ptr [r9],r14d ds:0000007a`137ae368=75ffa9f0 0:000> kp # Child-SP RetAddr Call Site 00 0000007a`137ae230 00007ffd`d3012847 KERNELBASE!ReadFile+0x2c 01 0000007a`137ae2b0 00007ffd`d30124f8 ucrtbase!read_nolock+0x2c3 02 0000007a`137ae350 00000000`75fdf08b ucrtbase!read+0xd8 03 0000007a`137ae390 00000000`760a78d9 python36!Py_read+0x63 // torch读取模型文件 04 0000007a`137ae3e0 00000000`760a7860 python36!PyTraceMalloc_Untrack+0x6ca5 05 0000007a`137ae410 00000000`75fac698 python36!PyTraceMalloc_Untrack+0x6c2c 06 0000007a`137ae490 00000000`75fb1c29 python36!PyCFunction_FastCallDict+0x88 // 读取后,查看到当前内存存放tensor 65的数据 0000007a`174ec170 35 36 71 03 58 03 00 00-00 63 70 75 71 04 4b 01 56q.X....cpuq.K. 0000007a`174ec180 4e 74 71 05 51 4b 00 4b-01 85 71 06 4b 01 85 71 Ntq.QK.K..q.K..q 0000007a`174ec190 07 89 63 63 6f 6c 6c 65-63 74 69 6f 6e 73 0a 4f ..ccollections.O 0000007a`174ec1a0 72 64 65 72 65 64 44 69-63 74 0a 71 08 29 52 71 rderedDict.q.)Rq 0000007a`174ec1b0 09 74 71 0a 52 71 0b 2e-80 02 5d 71 00 58 0a 00 .tq.Rq....]q.X.. 0000007a`174ec1c0 00 00 34 38 36 36 33 32-35 38 35 36 71 01 61 2e ..4866325856q.a. 0000007a`174ec1d0 01 00 00 00 00 00 00 00-65 00 00 00 00 00 00 00 ........e....... // ************ tensor数据在这里 00 00 00 00-65 00 00 00 ************** 0000007a`174ec1e0 94 00 95 00 96 00 97 00-98 00 99 00 9a 00 9b 00 ................ 0000007a`174ec1f0 9c 00 9d 00 9e 00 9f 00-a0 00 a1 00 a2 00 a3 00 ................ 0000007a`174ec200 a4 00 a5 00 a6 00 a7 00-a8 00 a9 00 aa 00 ab 00 ................ 0000007a`174ec210 ac 00 ad 00 ae 00 af 00-b0 00 b1 00 b2 00 b3 00 ................ 我们尝试修改 0x65 – 0x75 , 预期输出改变为 data = tensor([117]) 0:000> db 00000099`ee1514a5 00000099`ee1514a5 00 00 00 65 00 00 00 00-00 00 00 94 00 95 00 96 ...e............ 00000099`ee1514b5 00 97 00 98 00 99 00 9a-00 9b 00 9c 00 9d 00 9e ................ 00000099`ee1514c5 00 9f 00 a0 00 a1 00 a2-00 a3 00 a4 00 a5 00 a6 ................ 00000099`ee1514d5 00 a7 00 a8 00 a9 00 aa-00 ab 00 ac 00 ad 00 ae ................ 00000099`ee1514e5 00 af 00 b0 00 b1 00 b2-00 b3 00 b4 00 b5 00 b6 ................ 00000099`ee1514f5 00 b7 00 b8 00 b9 00 ba-00 bb 00 bc 00 bd 00 be ................ 00000099`ee151505 00 bf 00 c0 00 c1 00 c2-00 c3 00 c4 00 c5 00 c6 ................ 00000099`ee151515 00 c7 00 c8 00 c9 00 ca-00 cb 00 cc 00 cd 00 ce ................ 多次断点后(torch会读取7,8次以上模型文件到同一个内存缓冲区,针对这部分的修改是无效的),判断找到读取65的地方,即r8 = 8的时候 Breakpoint 0 hit KERNELBASE!ReadFile: 00007ffd`d9cf7c30 48895c2410 mov qword ptr [rsp+10h],rbx ss:00000018`0b05e9d8=0000000000000000 0:000> r rax=0000000000000000 rbx=0000000000000000 rcx=000000000000011c rdx=000000180ea3f6c0 rsi=0000000000000008 rdi=000000180ea3f6c0 rip=00007ffdd9cf7c30 rsp=000000180b05e9c8 rbp=0000000000000003 r8=0000000000000008 r9=000000180b05ea88 r10=e000f67e7fbdab93 r11=0000f681840e35b9 r12=0000000000000000 r13=00000000000000c0 r14=000000180ea3f6c0 r15=0000000000000008 0:000> db 000000180ea3f6c0 修改前 00000018`0ea3f6c0 65 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 e............... // ******* 65 00 00 00 00 ******** 00000018`0ea3f6d0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 00000018`0ea3f6e0 00 00 00 00 00 00 00 00-06 7e 21 c4 00 d7 01 80 .........~!..... 00000018`0ea3f6f0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 00000018`0ea3f700 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 00000018`0ea3f710 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 00000018`0ea3f720 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 00000018`0ea3f730 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 0:000> eb 00000018`0ea3f6c0 75 0:000> 0:000> db 000000180ea3f6c0 修改后 00000018`0ea3f6c0 75 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 u............... // ******* 65 00 00 00 00 ******** 00000018`0ea3f6d0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 00000018`0ea3f6e0 00 00 00 00 00 00 00 00-06 7e 21 c4 00 d7 01 80 .........~!..... 运行torch 查看打印输出 输出 : “ data = tensor([117]) ” 证明我们的内存修改是有效的。 但对于复杂的网络情况就没有那么简单了,网络参数存在多份数据拷贝的情况,哪一处的修改会影响推理阶段目前还未知道,与框架编写的优化实现有关。 尚未尝试的想法是:运行时去搜索网络各层的参数块,然后来修改。 1)不确定是否内存中有多份拷贝及拆散重新组合情况; 2)以及不一致情况下的冲突和混淆。(可以从调试+源码角度入手); 3)是否存在缓存参数的情况每一次前向的计算的时候直接从这里送入矢量寄出去进行计算,如果这样那么仅修改匹配到的内存方案将无效; 4)当前看是每一个网络块的内存都保持在低8字节的地址空间上,搜索空间仍然比较大,深入调试或许可以靠一些策略约束搜索匹配空间。 若感兴趣,大家可以参考我的调试手记 http://www.vxjump.net/aisec/bc-debug.pdf