Aggregator
CVE-2005-4605 | Linux Kernel 2.6.14.3/2.6.15 proc_misc.c denial of service (EDB-9363 / Nessus ID 21977)
Цифровой занавес: как США блокируют утечку биометрии граждан
New Framework Promises to Train AI to Better Understand Hard-to-Grasp Languages Like Polish
Majority of UK SMEs Lack Cybersecurity Policy
深圳市港澳居民来往内地通行证(回乡证)受理点和预约办理入口
what can i do with IP dress?
CVE-2010-1622 | Oracle Fusion Middleware 7.6.2/11.1.1.6.1/11.1.1.8.0 WebCenter Sites code injection (EDB-13918 / Nessus ID 86577)
Typecho 开启 Redis 缓存优化访问速度
Typecho 开启 Redis 缓存优化访问速度
RansomEXX
Researchers Create Plug-and-Play System to Test Language AI Across the Globe
游戏安全入门-扫雷分析&远程线程注入
无论学习什么,首先,我们应该有个目标,那么入门windows游戏安全,脑海中浮现出来的一个游戏 -- 扫雷,一款家喻户晓的游戏,虽然已经被大家分析的不能再透了,但是我觉得自己去分析一下还是极好的,把它作为一个小目标再好不过了。
我们编写一个妙妙小工具,工具要求实现以下功能:时间暂停、修改表情、透视、一键扫雷等等。
本文所用工具:
Cheat Engine、x32dbg(ollydbg)、Visual Studio 2019
扫雷游戏分析游戏数据在内存中是地址,那么第一个任务,找内存地址
打开CE修改器
修改时间->时间暂停计数器的时间是一个精确的值,所以我们通过精确数值扫描出来,游戏开始之前计数器上的数是0,所以我们扫描0。
时间在变化,选择介于什么数值之间再次扫描
可得 0x100579c --- winmine.exe+579C
我们发现这个数据都是直接通过基址 + 固定偏移能直接得到的。
然后我们对数据去找 是什么改写了这个地址,得到一个指令和指针:
时间:0x100579c
修改表情 - 没啥用修改表情这个功能怎么搞我觉得还是很容易想到的,这个按钮的作用是重新开始游戏,开始游戏,游戏胜利,游戏失败。
(表情的状态被分成了两个变量(4byte)来控制)
所以它是一种状态,所以我们通过0和1进行扫描,游戏进行状态输入1进行扫描,还原游戏之后输入0进行扫描。
首先是游戏进行状态,输入1进行扫描
再点击表情,将游戏还原,输入0开始扫描
如此反复进行扫描,得到表情的内存地址
0x1005164 -- winmine.exe+5164
但是嘞,修改成2或者3,表情没有心得反应,所以控制游戏胜利和游戏失败的是其他的地址,我们知道,一般来说,一个功能的代码在内存中基本上都是连续的,(就像你修改一个游戏的血量,浏览血量内存块,你可以发现怒气,蓝量等内存地址)
所以,我们浏览内存
0x1005164-4 = 0x1005160
修改为3,发现出现了戴墨镜的表情(游戏胜利)
但是这个胜利知识一个状态,并不能说明扫雷完成.
表情:0x1005160与0x1005164
思考游戏结束的时候会自动显示所有的雷,因此我们动态调试,看看在哪个函数调用之后会显示所有的雷
经过几次的动态调试之后发现:0x2F80函数是我们要找的结果。
一键扫雷通过透视,我们玩一把游戏,使得游戏胜利(点完最后一个)
然后后两个函数,是破纪录跟英雄榜的函数
ret来到了这儿,游戏通关了,来到了这儿,可以知道,这个0x347c就是判断输赢的函数
并且通过调试发现由一个参数 0 1 来控制,所以跟透视差不多,带个参数线程回调就完了
编写妙妙小工具怎么实现这个工具呢,当然是选择DLL注入
那么dll 怎么注入进去呢,这里选择远程线程注入
这里先简单介绍下什么是远程线程注入
前置知识-动态调用dll主要就是这几个个 API:
LoadLibraryA加载指定 DLL 并返回模块句柄,参数为字符串,就是 dll 的路径。
GetProcAddress获取指定 dll 的导出函数的地址。
第一个参数是模块句柄,第二个参数是模块函数,返回值为函数的地址。
通过这两个函数,我们可以拿到所有函数的地址,然后就能进行调用。
CreateThread - 远程线程注入里面几乎只有一个参数,那就是线程回调函数,然后当然还有返回地址,返回线程 id 啥的,这里我们都可以不用管,几乎是与 Linux 的创建线程函数一致。
还有一个远程版本的叫 CreateRemoteThread,它可以给别的进程创建一个线程并可以在本进程创建那个进程调用的回调函数。我们可以在回调函数中加载指定的 dll,在 dllmain 的入口当中,有一个 switch 的四个选项。
// dllmain.cpp : 定义 DLL 应用程序的入口点。#include "pch.h"
BOOL APIENTRY DllMain( HMODULE hModule,//指向自身的句柄
DWORD ul_reason_for_call,//调用原因
LPVOID lpReserved//隐式加载or显式加载
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH://附加到进程上时执行
case DLL_THREAD_ATTACH://附加到线程上时执行
case DLL_THREAD_DETACH://从线程上剥离时执行
case DLL_PROCESS_DETACH://从进程上剥离时执行
break;
}
return TRUE;
}
我们可以在 DLL_PROCESS_ATTACH 的选项中加入代码,让它在加载的时候调用执行。
那么我们的步骤是:
-
打开指定进程获得句柄
-
开辟远程进程的空间,分配可读可写段。
-
调用 WriteProcessMemory 将 dll 路径写入该内存区域。
-
创建远程线程,回调函数使用 LoadLibrary 加载指定 dll。
-
等待返回(loadLibrary返回)
-
释放空间
-
释放句柄
-
返回结果
{
//1.打开目标进程获取句柄
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ProcessId);
printf("进程句柄:%p\n", hProcess);
//2.在目标进程体内申请空间
LPVOID lpAddress = VirtualAllocEx(hProcess, NULL, 0x100, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
//3.写入DLL路径
SIZE_T dwWriteLength = 0;
WriteProcessMemory(hProcess, lpAddress, szPath, strlen(szPath), &dwWriteLength);
//4.创建远程线程,回调函数使用 LoadLibrary 加载指定 dll
HANDLE hThread = CreateRemoteThread(hProcess, NULL, NULL, (LPTHREAD_START_ROUTINE)LoadLibraryA, lpAddress, NULL, NULL);
//5.等待返回(loadLibrary返回)
WaitForSingleObject(hThread, -1);
//6.释放空间
VirtualFreeEx(hProcess, lpAddress, 0, MEM_RELEASE);
//7.释放句柄
CloseHandle(hProcess);
CloseHandle(hThread);
//返回结果
AfxMessageBox(L"完成");
} 编写DLL注入器 #include<windows.h>
#include<iostream>
#include<time.h>
#include<stdlib.h>
#include<TlHelp32.h>
DWORD FindProcess() {
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
PROCESSENTRY32 pe32;
pe32 = { sizeof(pe32) };
BOOL ret = Process32First(hSnap, &pe32);
while (ret)
{
if (!wcsncmp(pe32.szExeFile, L"mine.exe", 11)) {
printf("Find winmine.exe Process %d\n", pe32.th32ProcessID);
return pe32.th32ProcessID;
}
ret = Process32Next(hSnap, &pe32);
}
return 0;
}
void Inject(DWORD ProcessId, const char* szPath)
{
//1.打开目标进程获取句柄
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ProcessId);
printf("进程句柄:%p\n", hProcess);
//2.在目标进程体内申请空间
LPVOID lpAddress = VirtualAllocEx(hProcess, NULL, 0x100, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
//3.写入DLL路径
SIZE_T dwWriteLength = 0;
WriteProcessMemory(hProcess, lpAddress, szPath, strlen(szPath), &dwWriteLength);
//4.创建远程线程,回调函数使用 LoadLibrary 加载指定 dll
HANDLE hThread = CreateRemoteThread(hProcess, NULL, NULL, (LPTHREAD_START_ROUTINE)LoadLibraryA, lpAddress, NULL, NULL);
//5.等待返回(loadLibrary返回)
WaitForSingleObject(hThread, -1);
//6.释放空间
VirtualFreeEx(hProcess, lpAddress, 0, MEM_RELEASE);
//7.释放句柄
CloseHandle(hProcess);
CloseHandle(hThread);
}
int main() {
DWORD ProcessId = FindProcess();
while (!ProcessId) {
printf("未找到扫雷程序,等待两秒中再试\n");
Sleep(2000);
ProcessId = FindProcess();
}
printf("开始注入进程...\n");
Inject(ProcessId, "E:\\CODE\\wimine\\Mine\\release\\Mine.dll");
printf("注入完毕\n");
} 编写DLL
这里我们采用MFC DLL 基于对话框 (dialog)的方式编写(简单),使用静态编译的方式
然后我们需要在资源窗体,新建一个 Dialog ,简单包装一个界面
这样我们在加载窗体的时候需要创建一个窗体类对象用它的 DoModal 方法去显示,用线程回调的方式加载并且初始化InitInstance
DWORD WINAPI DlgThreadCallBack(LPVOID lp) {MineDlg* Dlg;
Dlg = new MineDlg();
Dlg->DoModal();
delete Dlg;
FreeLibraryAndExitThread(theApp.m_hInstance, 1);
return 0;
}
// CMineApp 初始化
BOOL CMineApp::InitInstance()
{
CWinApp::InitInstance();
::CreateThread(NULL, NULL, DlgThreadCallBack, NULL, NULL, NULL);
return TRUE;
} 时间暂停
上面我们找到了它控制时间增加的指令,我们把它们全部 NOP 掉,就可以实现时间暂停
写两个按钮,创建下面的事件实现时间暂停开关。
DWORD GetBaseAddr() {HMODULE hMode = GetModuleHandle(nullptr);
//LPWSTR s = (LPWSTR)malloc(0x100);
//wsprintf(s, L"基址:%p", hMode);
//AfxMessageBox(s);
return (DWORD)hMode;
}
void MineDlg::OnBnClickedButton1() // 时间暂停
{
// TODO: 在此添加控件通知处理程序代码
auto BaseAddr=GetBaseAddr();
DWORD TimeOffset = 0x579C;
DWORD TimeInsOffset = 0x2FF5;
DWORD InsLen = 6;
DWORD old;
VirtualProtect((void*)(BaseAddr + TimeInsOffset), InsLen, PAGE_EXECUTE_READWRITE, &old);
BYTE INS[] = { 0x90,0x90,0x90,0x90,0x90,0x90 };
memcpy((void *)(BaseAddr + TimeInsOffset), INS, InsLen);
VirtualProtect((void*)(BaseAddr + TimeInsOffset), InsLen, old, &old);
}
void MineDlg::OnBnClickedButton2() // 恢复字节即可取消时间暂停
{
// TODO: 在此添加控件通知处理程序代码
auto BaseAddr = GetBaseAddr();
DWORD TimeOffset = 0x579C;
DWORD TimeInsOffset = 0x2FF5;
DWORD InsLen = 6;
DWORD old;
VirtualProtect((void*)(BaseAddr + TimeInsOffset), InsLen, PAGE_EXECUTE_READWRITE, &old);
BYTE INS[] = { 0xFF,0x05,0x9C,0x57,0x00,0x01 };
memcpy((void*)(BaseAddr + TimeInsOffset), INS, 6);
VirtualProtect((void*)(BaseAddr + TimeInsOffset), InsLen, old, &old);
}
测试
透视经过上面动态调试我们得出结论:0x2F80函数是踩雷函数。
我们如果调用这个函数,是不是就能够实现透视了呢?
我们依旧采取线程回调的方式
void MineDlg::OnBnClickedButton3(){
// TODO: 在此添加控件通知处理程序代码
DWORD ESPOffset = 0x2f80;
DWORD FuncAddr = GetBaseAddr() + ESPOffset;
// 创建不带参数的线程
CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)FuncAddr, NULL, 0, NULL);
}
测试
一键扫雷跟透视差不多,只不过创建带参数的线程回调
void MineDlg::OnBnClickedButton4(){
// TODO: 在此添加控件通知处理程序代码
DWORD ESPOffset = 0x347C;
DWORD FuncAddr = GetBaseAddr() + ESPOffset;
//创建带参数的线程
struct { int a; } s = { 0 };
CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)FuncAddr, &s, NULL, NULL);
}
测试
总结通过这个小项目,对WIN游戏安全有初步的认识,并且加强对软件的逆向思维,增强动态调试的能力,找到软件关键的基地址,通过CE修改器,初步pojie软件,了解软件的状态,修改时间(时间暂停等等),理解几个重要的API,FindWindow获取句柄,WriteProcessMemory写入内存信息,LoadLibraryA加载指定 DLL 并返回模块句柄,GetProcAddress,获取指定 dll 的导出函数的地址,CreateThread 线程回调函数等等。多写,多做,多调,多实验,加油,互勉。
游戏安全入门-扫雷分析&远程线程注入
阿里云张家口数据中心多项服务出现异常 不过状态页未显示任何日志
大模型隐私泄露攻击技巧分析与复现
大型语言模型,尤其是像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数组保存到文件中,文件名包含一个唯一标识符。
-
输入: 文件路径(包含格式化标记)、数组、唯一标识符(整数或字符串)。
-
实现: 使用给定的格式化标记生成文件名,然后将数组保存到该文件中。
-
功能: 计算生成序列与真实序列之间的汉明距离。
-
输入: 真实序列和生成的序列。
-
实现:
-
如果生成的序列是二维的,逐行计算每行的汉明距离。
-
否则,计算生成序列第一行与真实序列的汉明距离。
-
返回平均汉明距离和汉明距离的形状。
-
功能: 计算真实答案序列的损失。
-
输入: 真实答案序列列表和批次大小(默认为50)。
-
实现:
-
将答案分批处理。
-
计算每个标记的logits。
-
使用交叉熵计算每个标记的损失。
-
提取后缀部分的损失,并计算平均损失。
-
返回每个序列的损失列表。
-
功能: 比较真实序列和生成序列的损失。
-
输入: 真实序列的损失和生成序列的损失。
-
实现:
-
将两组损失拼接在一起。
-
对每个序列的损失进行排序。
-
获取排序后的索引。
-
返回排序后的损失,排序索引和排名第一的索引。
-
功能: 绘制损失的直方图。
-
输入: 损失数组。
-
实现: 该函数目前为空,未实现绘图逻辑。
如下函数组合在一起用于处理和评估语言模型的生成任务。load_prompts函数加载提示数据,is_memorization函数评估生成模型是否记住了训练数据,error_100函数计算在发生100次错误之前的匹配次数,precision_multiprompts函数计算多提示生成序列的精确度,prepare_data函数则准备实验所需的数据和目录结构。这些步骤帮助全面评估和改进模型的生成质量和泛化能力。
1. load_prompts-
功能: 从指定目录加载numpy文件并转换为64位整数类型的numpy数组。
-
输入:
-
dir_: 文件所在的目录路径。
-
file_name: 文件名。
-
实现: 通过拼接目录路径和文件名构造完整文件路径,加载文件并转换数据类型。
-
功能: 计算生成的序列与真实序列完全匹配的比例,以确定模型是否记住了训练数据。
-
输入:
-
guesses: 生成的序列。
-
answers: 真实序列。
-
实现:
-
对比生成的序列和真实序列是否完全相同,统计完全匹配的次数。
-
计算匹配次数在所有生成序列中的比例。
-
功能: 计算在前100个错误之前的正确匹配次数。
-
输入:
-
guesses_order: 按顺序排列的生成序列。
-
order: 序列顺序索引。
-
answers: 真实序列。
-
实现:
-
遍历生成序列,统计与真实序列匹配的次数,直到发生100次错误为止。
-
返回在发生100次错误之前的总遍历次数和超出100次错误的匹配数。
-
功能: 计算多提示生成序列的精确度。
-
输入:
-
generations: 多提示生成的序列。
-
answers: 真实序列。
-
num_perprompt: 每个提示生成的序列数量。
-
实现:
-
截取每个提示生成的前num_perprompt个序列。
-
检查每个提示生成的序列是否与真实序列匹配。
-
计算匹配的提示数量占总提示数量的比例。
-
功能: 准备数据和目录结构以进行实验。
-
输入:
-
val_set_num: 验证集的数量。
-
实现:
-
构造实验目录和生成结果、损失结果的子目录。
-
加载提示数据,并提取验证集部分的提示数据。
-
返回构造的目录路径和提示数据。
### 如下函数组合在一起用于处理和评估语言模型的生成任务。
-
write_guesses_order函数将生成的序列按顺序写入CSV文件,便于进一步分析。
-
edit_dist函数计算生成序列和真实序列之间的编辑距离,这是评估生成质量的重要指标。
-
metric_print函数计算并打印各种评估指标,包括精度、多提示精度、前100个错误之前的正确匹配数、汉明距离和编辑距离。这些指标帮助全面评估模型在生成任务中的表现和准确性。
-
功能: 将生成的序列按顺序写入CSV文件。
-
输入:
-
generations_per_prompt: 每个提示生成的序列数。
-
order: 序列的顺序索引。
-
guesses_order: 生成的序列按顺序排列。
-
实现:
-
打开CSV文件进行写操作,文件名包含generations_per_prompt。
-
写入表头。
-
遍历序列索引和生成的序列,将每个序列按指定格式写入CSV文件。
-
功能: 计算生成序列和真实序列之间的编辑距离。
-
输入:
-
answers: 真实序列。
-
generations_one: 生成的单个序列。
-
实现:
-
初始化编辑距离总和为0。
-
遍历真实序列和生成序列,计算每对序列的编辑距离并累加。
-
返回平均编辑距离。
-
功能: 计算并打印各种评估指标。
-
输入:
-
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列表。
-
功能: 预留的过滤函数,目前没有实现任何功能。
-
功能: 通过投票机制选择最可能的输出序列。
-
输入:
-
last_logits: 最后一层的logits。
-
k: 用于投票的前k个logits。
-
answers: 真实答案。
-
input_len: 输入序列的长度。
-
实现:
-
初始化投票计数数组。
-
获取logits中每个序列的前k个最高值的索引。
-
为每个索引分配线性权重。
-
打印预测结果和原始结果的比较。
-
返回投票计数最高的索引作为最终预测。
-
功能: 通过加权求和的方式整合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值。
-
损失计算:
-
使用标准差过滤异常值,如果损失超出3倍标准差范围,则设置为1。
-
根据前两个最高的logits分数之间的差异和是否大于0.5来调整损失值。
-
最后,计算每个生成序列的平均损失 (likelihood)。
-
计算每个标记的损失 (loss_per_token),使用交叉熵损失函数 (torch.nn.functional.cross_entropy)。
-
对损失进行后处理:
-
结果整理:
-
将生成的序列 (generated_tokens) 和损失 (likelihood) 添加到 generations 和 losses 列表中。
-
返回结果:
-
将 generations 和 losses 转换为至少二维的numpy数组,并返回。
执行后如下所示
在上图中,也是用我们之前说的方法,看指标,precision,hamming dist等都相比基线方法有了较大提升。也就表明我们在本文中所说的这些策略都是有效的。
大模型隐私泄露攻击技巧分析与复现
CVE-2004-1801 | PWebServer Web Server 0.3.3 path traversal (EDB-23794 / ID 12040)
如何通过组合手段大批量探测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.”之类的资产扫描进度会很慢,所以要考虑如何兼顾效率和准确性的问题。
由于之前经过测试,对于确实存在漏洞的资产,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一样的工具。
需要此项目加V:dctintin,发地址。