写在前面,一些有用的refer
TOCTOU 和竞争条件
https://medium.com/@schogini/toctou-time-of-check-and-time-of-use-a-demonstration-and-mitigation-609c999042cb
https://techterms.com/definition/race_condition
机会锁
https://docs.microsoft.com/en-us/windows/win32/fileio/opportunistic-locks
https://docs.microsoft.com/en-us/windows/win32/fileio/types-of-opportunistic-locks
https://stackoverflow.com/questions/11837428/whats-the-difference-between-an-exclusive-lock-and-a-shared-lock
符号链接
https://docs.microsoft.com/en-us/windows/win32/fileio/reparse-points https://www.2brightsparks.com/resources/articles/ntfs-hard-links-junctions-and-symbolic-links.html
https://docs.microsoft.com/en-us/windows/win32/fileio/hard-links-and-junctions
链接跟踪漏洞的根源在于高权限程序在处理文件系统对象时,未能正确验证和处理符号链接或目录连接从而被低权限攻击者诱导,对非预期的敏感文件或目录执行了特权操作。
Windows的NTFS文件系统提供了两种主要的链接机制,符号链接和目录链接。符号链接是一种可以指向文件或目录的文件系统对象,其功能与Unix/Linux系统中的symlink非常相似。当应用程序访问一个符号链接时,文件系统会透明地将其重定向到链接所指向的目标对象。然而,从安全角度看,其利用价值受到一个关键限制:在Windows默认的安全策略下,创建符号链接需要SeCreateSymbolicLinkPrivilege特殊权限。
但是权限只有admi,在典型的LPE场景中攻击者无法直接创建符号链接来指向系统保护文件。
目录连接是NTFS文件系统提供的一种Reparse Point,它只能指向本地卷上的一个目录。与上面的的区别在于任何标准用户都可以在其拥有写权限的目录下创建目录连接,无需任何特殊权限 。攻击者可以使用系统自带的mklink /J
TOCTOU竞争条件

链接跟踪漏洞就是是一种经典的竞争条件漏洞,Time-of-Check to Time-of-Use,这种漏洞发生在程序对某一资源的状态进行检查之后,到实际使用该资源之前,资源的状态发生了非预期的改变从而导致程序基于一个过时的无效的检查结果执行了操作 。
典型的链接跟踪漏洞场景中,处理文件的错误逻辑序列分为三个阶段:
- Time-of-Check, TOC:特权服务进程对一个文件路径执行安全检查。这个路径通常位于一个标准用户可控的目录下,例如C:\ProgramData<VendorName>或用户的临时文件夹。检查的内容可能包括:路径是否存在、目标是否为目录而非文件、文件的数字签名是否有效、文件权限是否正确等 。此时攻击者已经预先将该路径设置为一个指向合法无害文件或目录的链接,因此检查能够顺利通过。
- Race Window:在安全检查完成之后,到特权服务实际对该路径执行文件操作之前,存在一个极其短暂但可被利用的时间窗口,操作系统调度器可能在此期间暂停该特权进程,而去执行其他进程。
- Time-of-Use, TOU:特权服务进程恢复执行并基于第一步检查通过的结论,对该文件路径执行一个高权限操作。攻击者恰好在竞争窗口期间迅速地将该路径的链接目标切换到了一个恶意的、受系统保护的位置,所以特权服务在TOU阶段实际操作的是一个未经检查的的恶意目标。
Windows机会锁
TOCTOU的时间窗口极其短暂,所以我们手动利用几乎不可能,所以成功的利用需要一种能精确控制和极度延长这个窗口的机制,在Windows环境下这个东西叫机会锁Opportunistic Locks。
OpLocks最初是为网络重定向器和客户端缓存管理器设计的性能优化机制,允许客户端在本地缓存文件并在没有其他进程访问该文件时进行操作,避免不必要的网络通信。当有第二个进程尝试访问该文件时文件服务器会通知第一个持有OpLock的客户端,要求Break锁,将缓存的数据写回服务器才允许第二个进程的访问。
攻击者反向利用这一机制,一个低权限的攻击者进程可以请求对一个文件或目录持有OpLock,当一个高权限的受害进程尝试访问文件时,Windows I/O管理器会发现该文件已被加锁。此时I/O管理器会暂停受害进程的线程,并向持有OpLock的攻击者进程发送一个中断通知,在攻击者进程主动释放OpLock之前,受害进程的线程将一直处于挂起等待状态 。
补充一下,微软官方文档中文翻译。
机会锁的类型
2025年1月27日
笔记
术语“机会锁”和“oplock”是等效的。为了简洁起见,本文档通常使用术语“oplock”。
oplock 操作适用于八种类型的 oplock。其中四种是当前支持的,其余四种是旧支持的。
当前 oplock 类型有:Read-Write-Handle、Read-Write、Read-Handle 和 Read。
这些 oplock 类型是在 Windows 7 中引入的。因此,它们通常被称为“Windows 7 oplocks”。
传统的 oplock 类型有:Level 1、Level 2、Batch 和 Filter。
Windows NT 3.1 中引入了 1 级、2 级和批处理 oplock。Windows 2000 中引入了过滤 oplock。
读写句柄锁、读写锁、1级锁、批处理锁和过滤锁都是排他性机会锁。如果客户端在文件上拥有任何类型的排他性机会锁,则该文件上不能再存在其他机会锁。
读句柄锁、读锁和 2 级锁都是可共享的机会锁。多个客户端可能对同一个文件持有可共享的机会锁。
给定文件可能同时具有读取和 2 级 oplock。
一个文件可能同时具有读取和读取处理机会锁。
一个文件不能同时具有 Read-Handle 和 Level 2 oplocks。
当前机会锁类型
读写处理机会锁
文件上的读写句柄机会锁允许客户端提前读取文件,并将预读数据和写入数据缓存在本地。如果客户端反复打开和关闭文件,它还允许客户端网络重定向器避免向服务器发送打开和关闭操作,其方式与传统的批量机会锁类型大致相同。
仅当客户端拥有该文件的唯一打开句柄时,才可以授予读写句柄机会锁。
读写机会锁
文件上的读写机会锁允许客户端提前读取文件,并将预读数据和写入数据缓存在本地。这与传统的 1 级机会锁类似。
仅当客户端拥有该文件的唯一打开句柄时,才可以授予读写句柄机会锁。
读处理机会锁
文件上的读句柄机会锁允许客户端提前读取文件并缓存预读数据。与读写句柄机会锁类似,如果客户端反复打开和关闭文件,它也允许客户端网络重定向器避免向服务器发送打开和关闭操作。
如果应用程序拥有某个文件的打开句柄,并且已被授予读句柄机会锁,而另一个文件句柄被打开指向该文件,则文件系统会检查新句柄的共享和访问模式是否与现有句柄兼容。如果兼容,则新句柄打开成功。如果不兼容,则打开操作将被阻止,读句柄机会锁将被中断。拥有该机会锁的应用程序可以通过关闭拥有读句柄机会锁的句柄来确认中断。在这种情况下,新句柄打开成功(假设没有其他打开的句柄发生冲突),并且打开文件的应用程序不会意识到存在冲突的文件句柄。
笔记
如果 oplock 所有者在没有关闭其文件句柄的情况下确认 Read-Handle oplock 中断,并且它与新的打开操作冲突,则新的打开将失败并出现ERROR_SHARING_VIOLATION,就像一开始就没有 oplock 一样。
如果应用程序拥有某个文件的打开句柄,并且已被授予读句柄机会锁 (Read-Handle oplock),而在另一个文件句柄上发生了写操作,则读句柄机会锁将会中断。在这种情况下,机会锁中断不会阻止写操作,这与上面描述的使用机会锁来防止共享冲突的情况不同。不阻止中断操作的机会锁中断称为建议性中断 (advisory break)。它的作用是通知拥有该机会锁的客户端,其缓存的任何数据现在都已过期,可能需要刷新缓存。
阅读机会锁
文件上的读取机会锁允许客户端提前读取文件并缓存预读数据。读取机会锁的行为与传统的 2 级机会锁非常相似。
原子创建与 Oplock
这并非一种机会锁类型,而是一种允许在应用程序打开文件和接收锁之间的时间间隔内进行读取操作且不违反共享模式的程序。对于旧版机会锁,这只能通过筛选器机会锁来实现,并且需要打开两个句柄。对于 Windows 7 机会锁,应用程序可以使用此程序请求任何类型的机会锁,并且只需打开一个句柄。
要执行原子 create-with-oplock 过程,您的应用程序应该执行以下操作:
使用CreateFile2函数打开文件。在CREATEFILE2_EXTENDED_PARAMETERS结构的dwFileFlags字段中,设置FILE_FLAG_OPEN_REQUIRING_OPLOCK标志。您可以根据需要设置dwDesiredAccess和dwShareMode参数。例如,在dwDesiredAccess参数中设置GENERIC_READ以便您可以读取文件;在dwShareMode参数中设置FILE_SHARE_READ | FILE_SHARE_DELETE标志,以允许其他人在您打开文件时读取、重命名和/或标记文件为待删除。
使用FSCTL_REQUEST_OPLOCK控制代码请求此句柄上的 oplock 。
笔记
您的应用程序不应在步骤 1 和步骤 2 之间对文件执行任何文件系统操作。这样做可能会导致死锁。
使用此过程请求的最常见的机会锁是 Read-Handle 类型。这允许你的应用程序允许尽可能多的并发访问,同时仍然允许你的应用程序在需要关闭句柄时收到通知,以避免对冲突的打开造成共享冲突。
传统机会锁类型
1级机会锁
文件的 1 级机会锁允许客户端提前读取文件,并将预读数据和写入文件的数据缓存在本地。只要客户端拥有对文件的独占访问权限,提供 1 级机会锁就不会危及数据一致性。
客户端打开文件后可以请求一级机会锁。如果没有其他客户端(或同一客户端上没有其他线程)打开该文件,服务器可能会授予机会锁。然后,客户端可以在本地缓存该文件的读写数据。如果另一个客户端已经打开了该文件,则服务器会拒绝机会锁,客户端不会进行本地缓存。(服务器也可能因为其他原因拒绝机会锁,例如不支持机会锁。)
当服务器打开一个已设置 1 级机会锁的文件时,服务器会在解除 1 级机会锁之前检查文件的共享状态。如果服务器在检查之后解除了锁定,则持有先前锁定的客户端刷新其写入缓存所花费的时间就是请求该文件的客户端必须等待的时间。这段时间的消耗意味着,对于许多客户端打开的文件,应避免使用 1 级机会锁。
但是,由于服务器在解除锁定之前会检查共享状态,因此,如果服务器由于共享冲突而拒绝打开请求,则服务器不会解除锁定。例如,如果您打开了一个文件,拒绝共享写入操作,并获得了 1 级锁定,则服务器会在检查您对该文件的锁定之前,拒绝另一个客户端打开该文件进行写入的请求。在这种情况下,您的机会锁并没有被解除。
有关 1 级机会锁 (OPLock) 工作原理的示例,请参阅1 级机会锁示例。有关如何解除机会锁的更多信息,请参阅解除机会锁。
2级机会锁
二级机会锁 (Level 2 oplock) 会通知客户端,当前有多个客户端同时访问某个文件,且尚未有任何客户端修改过该文件。此锁允许客户端执行读取操作,并使用缓存或预读的本地信息获取文件属性,但客户端必须将所有其他请求(例如写入操作)发送到服务器。如果您预计其他应用程序会随机写入文件,或者随机或顺序读取文件,则应用程序应该使用二级机会锁。
二级机会锁 (Level 2 oplock) 与过滤机会锁 (Filter oplock) 非常相似。在大多数情况下,您的应用程序应该使用二级机会锁。仅当您不希望在应用程序打开文件和接收锁定之间的时间间隔内,打开读取操作导致共享模式违规时,才应使用过滤锁。有关更多信息,请参阅过滤机会锁和服务器对锁定文件打开请求的响应。
有关打破机会锁的更多信息,请参阅打破机会锁。
批量机会锁
批处理机会锁 (Batch oplock) 操作文件的打开和关闭。例如,在执行批处理文件时,批处理文件可能每行打开和关闭一次。批处理机会锁会在服务器上打开批处理文件并使其保持打开状态。当命令处理器“打开”和“关闭”批处理文件时,网络重定向器会拦截打开和关闭命令。服务器接收的只是寻道和读取命令。如果客户端也进行预读操作,则服务器最多只会接收一次特定的读取请求。
打开已具有批量操作锁的文件时,服务器会在解除锁定后检查文件的共享状态。此检查使锁的持有者有机会完成缓存刷新并关闭文件句柄。如果锁持有者释放了锁,则在共享检查期间尝试打开操作不会导致共享检查失败。
有关批量机会锁工作原理的示例,请参阅批量机会锁示例。有关打破机会锁的更多信息,请参阅打破机会锁。
过滤机会锁
过滤机会锁 (Filter oplock) 会锁定文件,使其无法被打开进行写入或删除操作。所有客户端都必须能够共享该文件。过滤锁允许应用程序对文件数据执行非侵入式过滤操作(例如,编译器打开源代码或编目程序)。
过滤机会锁与二级机会锁的不同之处在于,它允许在应用程序打开文件和接收锁定之间的时间间隔内进行读取操作,而不会违反共享模式。当需要允许其他客户端进行读取访问时,过滤机会锁是最佳选择。在其他情况下,应用程序应该使用二级机会锁。过滤机会锁与批处理机会锁的不同之处在于,它不允许网络重定向器像批处理机会锁那样处理多次打开和关闭操作。
您的应用程序应通过三个步骤请求文件上的 Filter oplock:
使用CreateFile函数打开文件句柄,并将DesiredAccess参数设置为零(表示无访问权限),并将dwShareMode参数设置为FILE_SHARE_READ标志(表示允许共享读取)。此时获得的句柄称为锁定句柄。
使用FSCTL_REQUEST_FILTER_OPLOCK控制代码请求对此句柄进行锁定。
授予锁定后,使用CreateFile 函数再次打开文件,并将DesiredAccess设置为GENERIC_READ标志。将dwShareMode设置为FILE_SHARE_READ标志,以允许其他人在您打开文件时读取文件;设置为FILE_SHARE_DELETE标志,以允许其他人在您打开文件时将文件标记为删除;或者同时设置两者。此时获得的句柄称为读取句柄。
使用读取句柄读取或写入文件内容。
当打开一个已持有过滤机会锁的文件时,服务器会先断开锁,然后检查文件的共享状态。此检查使持有先前机会锁的客户端有机会放弃所有缓存数据并确认断开。在此共享检查期间尝试打开操作,即使先前的锁持有者释放了锁,也不会导致共享检查失败。文件系统会暂停打开操作,直到锁的持有者关闭读取句柄和锁定句柄。完成此操作后,其他客户端的打开请求才能继续进行。
有关打破机会锁的更多信息,请参阅打破机会锁。
当前与旧版 Oplock 类型
Windows 7 中,不同的 oplock 类型被视为不同的 oplock“级别”。从高到低的顺序为:读写句柄、读写、读句柄、读。在某些操作中会考虑 oplock 级别,例如,当客户端请求新的 oplock 作为 oplock 中断确认的一部分时。
一些旧版机会锁看起来与 Windows 7 机会锁类似。具体来说,Read 机会锁类似于 2 级,Read-Write 机会锁类似于 1 级,Read-Write-Handle 机会锁类似于 Batch 机会锁。然而,它们在行为上存在一些差异,允许更灵活的操作,而这些操作是旧版机会锁无法实现的。Windows 7 机会锁类型具有以下优势:
它们使应用程序在表达缓存意图时具有更大的灵活性和清晰度。
它们在处理 oplock 中断方面提供了更大的灵活性。
当 1 级或批量 oplock 中断时,作为中断确认的一部分,客户端可以请求将其中断为 2 级或无 oplock。当 过滤 oplock 中断时,客户端必须接受将其中断为无 oplock。
当 Windows 7 机会锁中断时,作为中断确认的一部分,客户端可以请求中断到任何 Windows 7 机会锁。客户端甚至可以请求更高级别的机会锁。根据具体情况,例如是否有其他打开程序正在等待机会锁中断确认,此类请求可能会成功。
客户端可以请求就地升级当前持有的 Windows 7 机会锁。例如,如果它持有一个“读”机会锁,则可以请求升级到“读句柄”,只需在同一文件句柄上请求一个“读句柄”机会锁即可。旧版机会锁无法就地升级。
NTFS重Reparse Points
有了延长TOCTOU窗口的方法还需要一种机制来瞬间改变文件路径的指向,比如NTFS文件系统提供的重解析点功能。
目录Junction 是一种特殊的重解析点,可以将一个目录路径重定向到本地卷上的另一个目录。例如可以将 C:\DirA 创建为一个指向 C:\DirB 的Junction。当访问 C:\DirA\file.txt 时,文件系统I/O管理器会在内核层面透明地将其解析为 C:\DirB\file.txt。对于大多数应用程序来说这个过程是无感的。
符号链接比Junction更灵活,可以指向文件或目录,并且可以跨越卷和甚至指向UNC网络路径。
在本地提权攻击中一般用Junction,因为它们的解析在文件系统驱动层完成,对于上层应用更为透明。并且低权限用户通常有能力在他们拥有写权限的目录中创建这些链接结构。
综上是一条完整的攻击链:首先设置一个Junction,将特权服务的目标路径指向一个诱饵位置,然后对诱饵位置的文件设置OpLock,当特权服务访问该文件并被OpLock暂停后迅速移除Junction,将原始路径恢复为指向包含恶意文件的真实位置,最后释放OpLock让被蒙蔽的特权服务继续执行操作,最终恶意文件写入受保护的系统区域。
PoC 基于OpLock与Junction的TOCTOU利用
**LucaBarile/TOCTOU **搞了个一个典范性的攻击场景和实现代码
PoC设定的场景如下:
- 受害者程序:一个名为UpdateDll.exe的特权程序,它以SYSTEM权限运行。
- 逻辑:该程序会首先验证C:\temp\dir1\signedDll.dll文件的数字签名。如果签名有效,它会将这个DLL文件复制到受系统保护的目标目录C:\protected\dir2\中。
- 攻击者权限:一个低权限的本地用户,对C:\temp\目录拥有完全控制权,但对C:\protected\dir2\目录没有任何写入权限。
- 攻击目标:利用UpdateDll.exe的逻辑缺陷,将一个恶意的、未签名的DLL(maliciousDll.dll)写入到C:\protected\dir2\,并命名为signedDll.dll,从而可能实现DLL劫持等后续攻击。
环境准备是先创建了一个完全受控的FakeDir,并将合法的能通过签名验证的DLL作为诱饵放置,通过Junction,UpdateDll.exe最初对C:\temp\dir1的访问将被透明地重定向到这个伪造的地方。原始的dir1被暂时隐藏起来。
设置陷阱与触发
Set an OpLock on FakeDir\signedDll.dll. 在FakeDir中的诱饵DLL上设置OpLock Execute UpdateDll.exe. 执行特权程序UpdateDll.exe UpdateDll.exe 尝试读取并验证dir1\signedDll.dll的签名但由于Junction的存在实际上访问的是FakeDir\signedDll.dll。
UpdateDll.exe的访问请求触发了OpLock,执行线程被内核暂停等待OpLock释放。
⬆️这是赢得竞态条件的关键,OpLock就像一个绊索被精确地设置在特权程序完成路径解析和文件定位之后,但在读取文件内容进行验证之前,一旦UpdateDll.exe踩上这个绊索就会被定身。
偷换
Delete the dir1 junction. 删除名为dir1的Junction
*Rename _dir1 to dir1. *将被隐藏的_dir1重命名回dir1
Copy the malicious dll (maliciousDll.dll) to dir1. 将恶意DLL复制到真实的dir1目录中
*Rename dir1\signedDll.dll to _signedDll.dll. *将dir1中残留的合法DLL重命名
*Rename dir1\maliciousDll.dll to signedDll.dll. *将恶意DLL重命名为signedDll.dll
攻击的核心就是这个Bait-and-Switch,在UpdateDll.exe被OpLock冻结的这段时间里攻击者对文件系统进行了一系列闪电般的操作。现在文件系统的状态已经是UpdateDll.exe即将操作的路径C:\temp\dir1\signedDll.dll,物理指向已经从一个合法文件变成了一个恶意文件。
收网
*Close the OpLock on FakeDir\signedDll.dll. *释放FakeDir中诱饵DLL上的OpLock UpdateDll.exe的线程从等待中唤醒继续执行,已经成功验证了来自FakeDir的合法DLL的签名。
UpdateDll.exe继续执行后续逻辑,使用路径C:\temp\dir1\signedDll.dll作为源,将文件复制到C:\protected\dir2\。此时,这个路径指向的已经是被替换后的恶意DLL。
当攻击者释放OpLock时UpdateDll.exe从暂停中恢复,代码逻辑中签名验证已经通过,所以他执行复制操作,它没有意识到,在它被暂停的瞬间已经无了。
CVE-2024-7229
https://feedly.com/cve/cwe/59?page=2
https://www.zerodayinitiative.com/advisories/published/2024/
CVE-2024-7229是由ZDI报告的0day漏洞,存在于Avast Cleanup Premium软件中的本地权限提升漏洞,核心缺陷在于软件中的Avast Cleanup Service在执行文件操作时存在缺陷,低权限的本地攻击者可以通过创建符号链接滥用该服务,以SYSTEM权限删除系统上的任意文件 。
和之前那个PoC中获得的任意文件写入原语不同,这个漏洞提供的是任意文件删除,通过一系列后续操作可以升级为完全的SYSTEM级代码执行。、
Windows Installer服务具有自我修复功能,当一个通过MSI包安装的程序检测到其关键文件或注册表项丢失时会自动触发修复流程,从缓存的MSI包中恢复这些文件。这些缓存的MSI包通常位于C:\Windows\Installer\目录下,该目录对普通用户只读。
攻击者首先扫描系统,寻找一个以高权限运行且通过MSI安装的应用程序,然后在用户可控的目录下放置一个恶意的MSI文件,该文件被设计为在修复时执行恶意代码。
利用CVE-2024-7229,以SYSTEM权限删除位于C:\Windows\Installer\目录下的、与目标应用程序对应的原始.msi缓存文件。
攻击者迅速创建一个从被删除的原始MSI文件路径到自己恶意MSI文件的符号链接,这步操作需要利用另一个TOCTOU窗口(针对Installer服务的这类攻击已有成熟的技术。
攻击者通过运行目标程序的主程序触发应用程序的自我修复检查。
Windows Installer服务,以SYSTEM权限运行,发现关键文件丢失并尝试从其原始路径进行恢复,由于符号链接的存在,它实际上加载并执行了攻击者的恶意MSI文件,导致恶意代码以SYSTEM权限执行。
攻击链 2 DiagTrack
Windows的诊断跟踪服务会定期收集和写入日志文件到C:\ProgramData\Microsoft\Diagnosis\等目录,其中一些日志文件或目录的权限设置可能允许攻击者进行操纵。
攻击步骤如下
- 定位目标: 攻击者识别出DiagTrack服务将要写入或操作的一个特定日志文件或目录。
- 删除与重定向: 利用CVE-2024-7229删除该目标文件或目录。然后,立即创建一个从该路径到受保护系统位置的Junction或符号链接。
- 触发写入: 等待或主动触发DiagTrack服务执行日志写入操作。
- 定位目标: 攻击者识别出DiagTrack服务将要写入或操作的一个特定日志文件或目录。
- 删除与重定向: 利用CVE-2024-7229删除该目标文件或目录。然后,立即创建一个从该路径到受保护系统位置的Junction或符号链接。
- 触发写入: 等待or主动触发DiagTrack服务执行日志写入操作。
- 写入Payload: DiagTrack服务以SYSTEM权限,跟随链接,将日志内容或一个新目录写入到C:\Windows\System32\wbem。如果能写入一个精心构造的MOF文件,就可以利用Windows Management Instrumentation的特性,在文件被写入时自动编译并执行其中的恶意脚本,从而获得SYSTEM权限的代码执行。
攻击链 3 滥用打印机驱动安装攻击链
Windows的打印机驱动安装过程也是一个历史悠久的提权向量了。。。普通用户有权安装打印机 而打印机驱动的安装过程最终是由Print Spooler服务以SYSTEM权限完成的。
攻击步骤
- 删除驱动文件: 利用CVE-2024-7229删除一个现有打印机驱动程序包中的某个非关键但会被检查的文件。
- 触发驱动安装: 攻击者通过调用标准的打印API来尝试更新或重新安装一个打印机驱动。
- 劫持路径: 在Print Spooler服务检查驱动文件并准备安装的TOCTOU窗口期,攻击者利用链接操纵技术,将驱动路径指向一个包含恶意DLL的目录。
- 权限提升: Print Spooler服务以SYSTEM权限加载并执行了恶意的打印机驱动DLL,成功获得代码执行。