Windows IPC
进程间通信 (IPC,Inter-Process Communication)
进程间通信 (IPC) 是一种在进程之间建立连接的机制,在两台计算机或一台多任务计算机上运行,以允许数据在这些进程之间流动。进程间通信 (IPC) 机制通常用于客户端/服务器环境,并在不同程度上受到不同 Microsoft Windows 操作系统的支持。进程间通信技术包括消息传递、同步、共享内存和远程过程调用。
用于进程间通信的众多模型中的两个
Windows 平台支持的 IPC 机制
-
命名管道
-
邮槽
-
网络BIOS
-
Windows 套接字
-
远程过程调用 (RPC)
-
本地过程调用 (LPC/ALPC)
-
网络动态数据交换 (NetDDE)
-
分布式组件对象模型 (DCOM)
本节课将涵盖以下主题:
-
命名管道
-
LPC
-
ALPC
-
RPC
命名管道(Named Pipe)
介绍
命名管道起源于 OS/2 时代,是一种进程间通信机制,可在两台计算机上的进程之间提供可靠的、面向连接的双向通信。命名管道是 Microsoft Windows 操作系统和应用程序中客户端/服务器通信的一种形式。
虽然管道这个名字听起来有点奇怪好像很复杂的样子,但管道却是一种非常基本且简单的技术,可以在两个进程之间实现通信和共享数据,其中术语管道描述了这两个进程使用的共享内存段。
有两种类型的管道:
-
命名管道
-
匿名管道
大多数时候,在引用管道时,可能指的是命名管道,因为命名管道提供了较为完整的功能。管道通信可以在同一系统上的两个进程之间进行(使用命名管道和匿名管道),其中匿名管道主要用于父子进程通信,但也可以跨机器边界进行(只有命名管道可以跨机器边界进行通信),由于命名管道更有价值,本节课将仅关注命名管道。
命名管道消息传递
让我们来分析一下命名管道的内部结构。通过管道一词我们可以把这种通信技术想象成一根空心的管子,如果我们对着一端灌水,那么另一端就会流出来,如果我们对着一端说话,那么另一端的人就会听到所说的话。没错,这就是管道所做的一切,它将信息从一端传输到另一端,勤勤恳恳,任劳任怨......
如果你是Linux用户,那么你肯定不经意间已经使用过管道了。例如:cat file.txt | wc -l
,把file.txt的内容输出,但不是将输出显示到STDOUT(一般是终端窗口上),而是将输出重定向(“管道”)到第二个命令的输入wc -l
,从而计算文件的行数。这便是一个匿名管道的例子。
基于Windows的命名管道就像上面的例子一样容易理解。在Windows上,命名管道只是一个对象,更具体地说是一个FILE_OBJECT,它由一个特殊的文件系统管理,命名管道文件系统(NPFS)
当我们创建命名管道时,假设我们将其称为“fpipe”,在底层下,正在一个名为“管道”的特殊设备驱动器上创建一个名为“fpipe”(因此:命名管道)的 FILE_OBJECT。
调用 WinAPI 函数CreateNamedPipe来创建命名管道
HANDLE serverPipe = CreateNamedPipe(L"\\\\.\\pipe\\fpipe",PIPE_ACCESS_DUPLEX,PIPE_TYPE_MESSAGE,1,2048,2048,0,NULL );
调用中有意思的部分是\\\\.\\pipe\\fpipe
,因为需要对斜线进行转义,所以实际上等于是\\.\pipe\fpipe
,\.
指的是全局根目录,“pipe”是指向 NamedPipe 设备的符号链接。
由于命名管道对象是 FILE_OBJECT,访问我们刚刚创建的命名管道就等于访问一个“普通”文件。因此,从客户端连接到命名管道就跟调用CreateFile
一样简单
HANDLE hPipeFile = CreateFile(L"\\\\127.0.0.1\\pipe\\fpipe", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
连接后,从管道读取只需要调用ReadFile
ReadFile(hPipeFile, pReadBuf, MESSAGE_SIZE, pdwBytesRead, NULL);
从管道读取数据之前,可以向它写入一些数据。那么这是通过调用谁来实现的——相信你能够猜到,没错,答案就是WriteFile
WriteFile(serverPipe, message, messageLenght, &bytesWritten, NULL);
一般情况下,用户层调用WriteFile
被分派到内核层的NtWriteFile
,它实现了写操作的所有细节,例如哪个设备对象与给定的文件相关联,写操作是否应该同步,最终将你的数据写入文件。但写入管道时,指定的数据没有写入磁盘上的实际文件,而是写入由CreateNamedPipe
返回的文件句柄引用的共享内存部分
命名管道还可以在跨系统边界的网络连接上使用
调用远程命名管道服务器不需要额外的实现,只需确保对CreateFile
的调用指定了IP或主机名
调用远程管道服务器时会使用SMB网络协议与远程服务器建立SMB连接,默认情况下,由SMB协商方言,以确定网络认证协议
SMB(Server Message Block)
服务器消息块,也称为 SMB,是由 Microsoft、IBM 和 Intel 联合开发的高级文件共享协议,用于在网络上的计算机之间传递数据。Microsoft Windows 和 OS/2 使用服务器消息块 (SMB)。许多UNIX 操作系统也支持它。
通过 SMB 协议,客户端应用程序可以在各种网络环境下读、写服务器上的文件,以及对服务器程序提出服务请求。此外通过 SMB 协议,应用程序可以访问远程服务器端的文件、以及打印机、邮件槽(mailslot)、命名管道(named pipe)等资源。
在 TCP/IP 环境下,客户机通过 NetBIOS over TCP/IP(或 NetBEUI/TCP 或 SPX/IPX)连接服务器。一旦连接成功,客户机可发送 SMB 命令到服务器上,从而客户机能够访问共享目录、打开文件、读写文件,以及一切在文件系统上能做的所有事情
网络身份验证协议是在客户端和服务器之间通过 SMB 协议协商的
与其它IPC机制不同,无法以编程方式控制网络认证协议,因为这永远是通过SMB来协商。从客户端的角度来看,可以通过选择连接到主机名或 IP 来有效地选择身份验证协议。由于Kerberos的设计,它不能很好地处理IP,因此,如果选择连接到一个IP地址,协商的结果总是NTLM(v2)。反之,当连接到主机名时,很可能最终总是使用Kerberos。如果你想强制使用更强的 Kerberos 协议(只能在服务器主机上禁用 NTLM)
一旦身份验证完成,SMB处理这些操作就像处理其他文件操作一样,例如启动如下所示的“创建请求文件”请求
数据传输模式
命名管道提供两种基本通信模式:字节模式和消息模式
在字节模式下,数据以连续字节流的形式在客户与服务器之间流动。这也就意味着对于客户机应用和服务器应用在任何一个特定的时间段内都无法准确知道有多少字节从管道中读出或写入。在这种通信模式中,一方在向管道写入某个数量的字节后并不能保证管道的另一方能读出等量的字节,这可以让客户端和服务器在不关心数据大小的情况下传输数据。
在消息模式下,客户机和服务器则是通过一系列不连续的数据包进行数据的收发。从管道发出的每一条消息都必须作为一条完整的消息读入。
重叠管道 I/O、阻塞模式和输入/输出缓冲区
从安全性的角度来看,重叠I/O、阻塞模式和输入/输出缓冲区并不是特别重要。但是意识到它们的存在以及它们的含义可以帮助理解命名管道。
重叠 I/O
几个与命名管道相关的函数,例如ReadFile
, WriteFile
, TransactNamedPipe
和ConnectNamedPipe
可以同步执行管道操作,这意味着执行线程在继续之前等待操作完成。或者异步,这意味着执行线程触发操作并继续执行,而不等待操作完成。
需要注意的是,异步管道操作只能在允许重叠I/O的管道(服务器)上进行,方法是在CreateNamedPipe
调用中设置FILE_FLAG_OVERLAPPED
。
异步调用可以通过指定OVERLAPPED结构为上面提到的一些管道操作API的最后一个参数来实现,例如ReadFile
BOOL ReadFile( [in] HANDLE hFile, [out] LPVOID lpBuffer, [in] DWORD nNumberOfBytesToRead, [out, optional] LPDWORD lpNumberOfBytesRead, [in, out, optional] LPOVERLAPPED lpOverlapped );
或者通过指定COMPLETION_ROUTINE作为扩展API的最后一个参数,例如ReadFileEx
BOOL ReadFileEx( [in] HANDLE hFile, [out, optional] LPVOID lpBuffer, [in] DWORD nNumberOfBytesToRead, [in, out] LPOVERLAPPED lpOverlapped, [in] LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine );
OVERLAPPED是基于事件的,必须创建一个事件对象,等待IO操作完成的信号。而COMPLETION_ROUTINE是基于回调的,回调例程被传递给执行线程,该线程排队并在有信号后执行
阻塞模式
阻塞模式是在使用CreateNamedPipe
设置命名管道服务器时定义的,方法是使用dwPipeMode
参数中的一个标志
下面两个dwPipeMode标志定义了服务器的阻塞模式:
PIPE_WAIT(0x00000000):阻塞模式已启用。当在ReadFile
、WriteFile
或 ConnectNamedPipe
函数中指定管道句柄时 , 直到有数据要读取、所有数据都已写入或客户端已连接时,操作才会完成。使用此模式可能意味着在某些情况下无限期地等待客户端进程执行操作。
PIPE_NOWAIT(0x00000001):非阻塞模式已启用。在这种模式下,ReadFile
、WriteFile
和 ConnectNamedPipe
总是立即返回
输入/输出缓冲区
命名管道服务器的输入和输出缓冲区,在调用CreateNamedPipe
时创建,由nInBufferSize
和nOutBufferSize
参数设置缓冲区的大小
当执行读写操作时,命名管道服务器使用非分页内存(即物理内存)临时存储要读写的数据。如果攻击者能控制已创建服务器的这些值,他可能会恶意滥用这些值,通过选择大的缓冲区来潜在地导致系统崩溃,或者通过选择小的缓冲区(例如0)使管道操作延迟:
Large buffers:由于输入/输出缓冲区是非分页的,如果设置的太大,服务器将耗尽内存。但是,系统不会“一味地”接受nInBufferSize和nOutBufferSize参数。上限由系统相关常数定义;这篇文章表明 x64 Windows7 系统大约为 4GB,win10应该比这个大
Small buffers:
对于将nInBufferSize和nOutBufferSize设为0,咋一看,好像缓冲区变成0代表是一个不存在的缓冲区,如果系统会严格执行它被告知的内容,将不能向管道写入任何东西。但系统还是有点聪明,可以理解你正在要求最小缓冲区,因此会将分配的实际缓冲区扩展为它接收的大小,但这会对性能产生影响。缓冲区大小为 0 意味着每个字节都必须由管道另一端的进程读取(从而清空缓冲区),然后才能将新数据写入缓冲区,因此,大小为 0 的缓冲区可能会导致服务器延迟。
命名管道安全
当想要使命名管道变得安全时,唯一能做的就是为命名管道服务器设置一个安全描述符,作为CreateNamedPipe调用的最后一个参数(lpSecurityAttributes)。
设置此安全描述符是可选的;可以通过为lpSecurityAttributes参数指定NULL来设置默认安全描述符。Windows 文档定义了默认安全描述符对命名管道服务器的作用:
命名管道的默认安全描述符中的 ACL 将完全控制权授予 LocalSystem 帐户、管理员和创建者所有者。他们还授予Everyone 组成员和匿名帐户的读取权限。
Source:https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createnamedpipea#parameters
所以在默认情况下,如果你没有指定安全描述符,每个人都可以从你的命名管道服务器读取,不管读取的客户端是不是在同一台机器上
模拟(Impersonation) 模仿 冒充 扮演 coser
安全上下文就是一个进程允许做什么的权限集合
模拟是线程在与拥有该线程的进程的安全上下文不同的安全上下文中执行的能力。模拟通常应用于客户端-服务器体系结构,其中客户端连接到服务器,服务器可以(如果需要)模拟客户端。模拟使服务器(线程)能够代表客户端执行操作,但在客户端访问权限的范围内。
一个典型的场景是假设用户想要删除远程文件共享上的文件。现在托管文件的服务器需要确定是否允许用户这样做。服务器无法访问用户的访问令牌,因为该令牌在远程服务器的内存中不可访问(该令牌存储在请求删除文件的用户的计算机上)。服务器可以做的是从 Active Directory (AD) 中查询用户的帐户和组信息,并手动检查是否允许用户删除文件,但此任务很繁琐且容易出错。因此,实施了另一种方法,称为“模拟”。
基本思想是服务器假装是请求用户并执行请求的操作,就好像服务器是用户一样。所以服务器所做的就是复制用户的Impersonation令牌,并使用复制的令牌请求操作(例如删除文件)
模拟是一个强大的功能,它使一个进程能够伪装成其他人
所以你产生了大胆的想法没有:模拟高特权用户的令牌并获得权限提升!
Windows的访问控制模型有两个主要的组成部分,访问令牌(Access Token)和安全描述符(Security Descriptor),它们分别是访问和被访问者拥有的东西。通过访问令牌和安全描述符的内容,Windows可以确定持有令牌的访问者能否访问持有安全描述符的对象
土豆系列的提权原理主要是诱导高权限访问低权限的系统对象,导致低权限的对象可以模拟高权限对象的访问令牌(Access Token),进而可以用访问令牌创建进程,达到代码执行。
例如国内教程经常提到的烂土豆(Rotten Potato)提权MS16-075
备注:在过去的时候,RottenPotato、RottenPotatoNG或Juicy Potato等工具使利用 Windows 上的模拟特权在脚本小子中非常流行。不过,最近操作系统的变化有意或无意地降低了这些技术在Windows 10和Server 2016/2019上的能力。一些可怜的渗透小子恐怕就止步于此了。但在我们理解了命名管道后,我们可以自己编写工具再次轻松地利用这些特权。
想法不错,但显然没有那么简单。因为这里有两个障碍:
第一个障碍是实际上并非每个令牌都是”有价值的“,这意味着每个模拟令牌都有一个名为Impersonation Level的属性,该属性可以具有以下值之一:
-
匿名级别- 服务器可以模拟客户端,但令牌不包含客户端的任何信息。匿名级别仅支持进程间通信(例如命名管道)。
-
识别级别- 这是默认值。服务器可以获取客户端的身份以便进行 ACL 检查。可以使用该令牌来读取有关模拟用户的信息或检查资源 ACL,但你无法访问该资源(读/写/执行)。
-
模拟级别- 服务器可以模拟客户端的安全上下文以访问本地资源。
-
委托级别- 最强大的模拟级别。服务器可以模拟客户端的安全上下文来访问本地或远程资源。
因此,你需要得到的是一个模拟或委托级别模拟令牌,以便能够搞一些事情
遇到的第二个障碍是,为了能够复制(模拟)另一个令牌,需要特殊的权限。该特权是TOKEN_DUPLICATE(在访问令牌对象的访问权限中指定)。如果不拥有此特权,复制令牌的请求不会被拒绝,仍然会得到一个复制的令牌,但其模拟级别较低(识别级别)。
模拟命名管道客户端
让我们快速了解一下如果服务器模拟客户端,实际底层会发生什么
-
第 1 步:服务器等待来自客户端的传入连接,然后调用ImpersonateNamedPipeClient函数。
-
第 2 步:此调用导致调用NtCreateEvent(创建回调事件)和NtFsControlFile,这是执行模拟的函数。
-
第 3 步:NtFsControlFile是一个通用函数,其操作由参数指定,模拟时为FSCTL_PIPE_Impersonate。
-
第 4 步:再往下,调用NpCommonFileSystemControl,其中FSCTL_PIPE_IMPERSONATE作为参数传递,并在 switch-case 指令中使用以确定要做什么。
-
第 5 步:NpCommonFileSystemControl调用NbAcquireExeclusiveVcb来锁定对象,并在给定服务器的管道对象和客户端发出的 IRP(I/O 请求对象)的情况下调用 NpImpersonate。
-
第 6 步:然后NpImpersonate依次调用SeImpersonateClientEx并使用从客户端 IRP 中获得的客户端安全上下文作为参数。
-
第 7 步:SeImpersonateClientEx依次使用服务器的线程对象和客户端的安全令牌调用PsImpersonateClient,该安全令牌是从客户端的安全上下文中提取的
-
第 8 步:然后将服务器的线程上下文更改为客户端的安全上下文。
-
第 9 步:服务器在客户端的安全上下文中执行的任何操作和服务器调用的任何函数都是使用客户端的身份进行的,从而模拟客户端。
-
第 10 步:如果服务器在作为客户端时完成了它打算做的事情,则服务器调用RevertToSelf以切换回它自己的原始线程上下文。
Attack
客户端模拟
攻击场景
当你获得允许你指定或控制对文件的访问的服务、程序或例程时(不管它是否允许您进行读或写访问或两者兼而有之),使用命名管道的模拟最可能被滥用。由于命名管道本质上是 FILE_OBJECT, 并且操作与普通文件的访问函数(ReadFile, WriteFile, CreateFile,…)相同,因此你可以指定一个命名管道而不是普通的文件名,让受害进程连接到你控制下的命名管道,比如模拟令牌:,这也是命名管道中常见的一种手法,一般可以用来提权操作,msf中的getsystem也就是这个原理。
前提条件
在尝试模拟客户端时,需要检查两个重要方面。
第一个方面是检查客户端如何实现文件访问,更具体地说,客户端在调用CreateFile时是否指定了 SECURITY_SQOS_PRESENT 标志?
如果没有与SECURITY_SQOS_PRESENT标志一起指定其他标志,那么默认在模拟级别(SECURITY_IMPERSONATION)模拟客户端
因此一个容易受攻击的CreateFile调用是这样的:
hFile = CreateFile(pipeName, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL);
而一个安全的CreateFile调用是这样的:
//调用API时参数带有明确的SECURITY_IMPERSONATION_LEVEL标志 hFile = CreateFile(pipeName, GENERIC_READ, 0, NULL, OPEN_EXISTING, SECURITY_SQOS_PRESENT | SECURITY_IDENTIFICATION , NULL); //调用API时参数没有明确的SECURITY_IMPERSONATION_LEVEL标志 hFile = CreateFile(pipeName, GENERIC_READ, 0, NULL, OPEN_EXISTING, SECURITY_SQOS_PRESENT, NULL);
默认情况下,没有明确指定 SECURITY_IMPERSONATION_LEVEL的调用是使用 SecurityAnonymous(匿名级别) 的模拟级别。
如果设置了 SECURITY_SQOS_PRESENT 标志而没有任何额外的模拟级别 (IL) 或 IL 设置为 SECURITY_IDENTIFICATION 或 SECURITY_ANONYMOUS,则无法模拟客户端
要检查的第二个重要方面是文件名,也就是给CreateFile的lpFileName参数,这是调用本地命名管道和调用远程命名管道之间的重要区别。
对本地命名管道的调用由文件位置\\.\pipe\<SomeName>
定义。只有当SECURITY_SQOS_PRESENT标志显式设置为高于SECURITY_IDENTIFICATION的模拟级别时,才能模拟对本地管道的调用。因此,容易受攻击的调用是这个样子:
hFile = CreateFile(L"\\.\pipe\fpipe", GENERIC_READ, 0, NULL, OPEN_EXISTING, SECURITY_SQOS_PRESENT | SECURITY_IMPERSONATION, NULL);
所以,对本地管道的安全调用如下所示:
hFile = CreateFile(L"\\.\pipe\fpipe", GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL);
另一方面,远程命名管道是由以主机名或IP开头的lpFileName定义的,例如:\\ServerA.domain.local\pipe\<SomeName>
现在重点来了:
当 SECURITY_SQOS_PRESENT 标志不存在并且调用远程命名管道时,模拟级别由运行命名管道服务器的用户特权定义。
这意味着当您调用没有 SECURITY_SQOS_PRESENT 标志的远程命名管道时,运行该管道的攻击者用户必须持有SeImpersonatePrivilege ( SE_IMPERSONATE_NAME ) 才能模拟客户端。如果用户不拥有此特权,模拟级别将设置为SecurityIdentification(该级别允许您识别用户,但不能模拟用户)。同样,这也意味着,如果你的用户拥有SeEnableDelegationPrivilege ( SE_ENABLE_DELEGATION_NAME ),则模拟级别设置为 SecurityDelegation(委托级别),emmm....
除外,还有一个BUG是:
可以通过指定对在同一台机器上运行的命名管道进行远程管道调用
\\127.0.0.1\pipe\<SomeName>
舒服了吧。。。。。。。。。。。
综上所述:
-
如果没有设置SECURITY_SQOS_PRESENT,那么你至少具有SE_IMPERSONATE_NAME权限的用户,才可以模拟客户端,但是对于在同一台机器上运行的命名管道,你需要通过远程管道的方式
\\127.0.0.1\pipe\...
绕过这种限制 -
如果设置了 SECURITY_SQOS_PRESENT ,则只有同时设置了高于SECURITY_IDENTIFICATION 的模拟级别,你才能模拟客户端(无论是在本地还是远程调用命名管道)
实现
//创建命名管道服务器 serverPipe = CreateNamedPipe(pipeName, PIPE_ACCESS_DUPLEX,PIPE_TYPE_MESSAGE, 1, 2048, 2048, 0, NULL ); //等待管道连接 BOOL bPipeConnected = ConnectNamedPipe(serverPipe, NULL); //模拟客户端 BOOL bImpersonated = ImpersonateNamedPipeClient(serverPipe); // 这里打开的线程令牌现在是客户端的 BOOL bSuccess = OpenThreadToken(GetCurrentThread(), TOKEN_ALL_ACCESS, FALSE, &hToken); //成功打开后,恢复到自己的线程环境 bSuccess = RevertToSelf(); //现在复制客户端的Primary令牌 bSuccess = DuplicateTokenEx(hToken,TOKEN_ALL_ACCESS,NULL,SecurityImpersonation,TokenPrimary,&hDuppedToken ); // 使用该复制的令牌创建一个进程 CreateProcessWithTokenW(hDuppedToken, LOGON_WITH_PROFILE, command, NULL, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi);
实例创建条件竞争
命名管道实例被创建并存在于名称管道文件系统 (NPFS) 设备驱动器内的全局“命名空间”中(实际上从技术上讲,没有命名空间,但这有助于理解所有命名管道都存在于同一片空间中)。此外,同一空间里可以存在多个具有相同名称的命名管道。
那么,如果应用程序创建了一个已经存在的命名管道,会发生什么情况呢? 如果你不设置正确的标志,实际什么都不会发生,不会给你报错。但是你不会得到客户端连接,这是因为命名管道实例组织在FIFO (First in First Out)堆栈中。
这种设计使得命名管道容易受到实例创建条件竞争漏洞的攻击。
攻击场景
利用这种竞争条件的攻击场景如下:服务器创建一个命名管道用于与客户端应用程序通信,服务器应用程序在服务器管道创建后会触发客户端连接。你得弄清楚服务器何时以及如何启动以及它创建的管道的名称。弄清楚后,你可以编写一个程序,在服务器应用程序创建其管道实例之前创建一个具有相同名称的命名管道。如果服务器的命名管道创建不安全,它不会注意到同名的命名管道已经存在,不会发生任何错误。由于管道内部实例位于FIFO堆栈中,创建第一个管道实例的应用程序将获得第一个管道客户端,您可以读取或写入其数据或尝试模拟客户端。
前提条件
要使此攻击起作用,您需要目标服务器不检查是否已经存在同名的命名管道。通常,服务器不会编写额外的代码来检查是否已经存在同名的管道—因为一个朴素的思维是,如果我的管道名称已经存在,那么再创建应该得到一个错误,对吧?但这不会发生,因为两个具有相同名称的命名管道实例绝对有效……不讲道理。
但是为了应对这种攻击,微软添加了FILE_FLAG_FIRST_PIPE_INSTANCE标志,可以在通过CreateNamedPipe创建命名管道时指定该标志。当这个标志被设置后,如果已经存在相同名称的命名管道,那么你的创建调用将返回一个INVALID_HANDLE_VALUE,这将导致后面的ConnectNamedPipe调用时出错。
如果目标服务器没有指定FILE_FLAG_FIRST_PIPE_INSTANCE标志,那么很可能会受到攻击。但是对于攻击者,还有一件额外的事情需要注意。当通过CreateNamedPipe创建命名管道时,有一个nMaxInstances参数:
可以为此管道创建的最大实例数。管道的第一个实例可以指定这个值;必须为管道的其他实例指定相同的编号。可接受的值在 1 到PIPE_UNLIMITED_INSTANCES (255) 的范围内。来源:CreateNamedPipe
也就是我们可以对管道服务器可以并行连接的管道客户端数量限制,从1到255的范围。
因此,如果你将其设置为“1”,那么说明你的脑子是真的有泡。要利用实例创建条件竞争漏洞,请将其设置为 PIPE_UNLIMITED_INSTANCES。
实现
你需要做的就是在正确的时间使用正确的名称创建一个命名管道实例。
未得到答复的管道连接
未得到答复的管道连接是客户端发出的那些连接请求没有得到服务端的应答,因为客户端请求的管道不存在。这里的利用场景非常明确和简单:如果客户端想要连接到不存在的管道,我们可以创建一个客户端可以连接的管道通过恶意通信操纵客户端或冒充客户端以获得额外权限。
这个漏洞有时也被称为多余的管道连接(但我觉得这个术语不能知名见义)。
这里摆在面前的问题是:我们如何找到这样的客户端应用?
显而易见我们可以通过Procmon搜索失败的 CreateFile系统调用。可是事与愿违,Procmon并不会列出这些对管道的调用……也许那是因为该工具仅通过 NTFS 驱动程序检查/侦听文件操作
因此我们选择另一款工具IO Ninja,使用它工具集里的管道监视器( Pipe Monitor)可以轻松的完成这项任务。
使用搜索功能查找“Cannot open”:
杀死管道服务器
如果找不到未响应的管道连接尝试,但发现了一个想与之通信或模拟的有趣管道客户端,则获取客户端连接的另一种选择是终止其当前的管道服务器。
在前面部分我们已经得知了在同一个“命名空间”中可以有多个具有相同名称的命名管道。
所以你可以创建第二个具有相同名称的命名管道服务器并将自己置于队列中以服务客户端。只要原始管道服务器正在服务,你这边不会收到任何客户端调用,因此这种攻击的想法是破坏或杀死原始管道服务器以介入你的恶意服务器。
杀死原始管道服务器的手段有很多,主要决于谁在运行目标服务器以及你的访问权限和用户特权。
在分析目标管道服务器的终止技术时,试着跳出固有的思维模式,不仅仅是向进程发送关闭信号TerminateProcess
。像可能存在导致服务器关闭或重新启动的错误条件也可以利用(因为你在队列中——重新启动可能让你进入最前面的位置拿到控制权)。
另请注意,管道服务器只是一个在虚拟 FILE_OBJECT 上运行的实例,因此一旦它们的句柄引用计数达到 0(句柄是由连接到它的客户端打开的),命名管道服务器将被终止。因此,也可以通过杀死所有句柄来杀死服务器,当然这也涉及到句柄的保护技术,详情请参见前面课程驱动部分--驱动7-内核对象的保护
和免杀部分--利用句柄泄露Kill火绒
章节
配置不当任意读取
有时候,你可能对管道通信的数据感兴趣,而不是对操纵或模拟管道客户端感兴趣。
前提条件
在前面部分已经提到过,在保护命名管道时唯一能做的就是使用安全描述符作为CreateNamedPipe调用的最后一个参数 ( lpSecurityAttributes ) 。这是唯一能防止你访问任意命名管道实例的手段。因此,在搜索目标时,只需要检查此参数是否已设置好。
实现
当找到合适的目标时,还需要记住一件事:如果你使用ReadFile从命名管道中读取数据,则你正在从管道服务器和客户端的共享内存中删除数据,后面尝试从管道读取的人将找不到任何数据并可能引发错误。但是可以使用PeekNamedPipe函数来查看数据,而无需将其从共享内存中删除。
const int MESSAGE_SIZE = 512; BOOL bSuccess; LPCWSTR pipeName = L"\\\\.\\pipe\\fpipe"; HANDLE hFile = NULL; LPWSTR pReadBuf[MESSAGE_SIZE] = { 0 }; LPDWORD pdwBytesRead = { 0 }; LPDWORD pTotalBytesAvail = { 0 }; LPDWORD pBytesLeftThisMessage = { 0 }; // connect to named pipe hFile = CreateFile(pipeName, GENERIC_READ, 0, NULL, OPEN_EXISTING, SECURITY_SQOS_PRESENT | SECURITY_ANONYMOUS, NULL); // sneak peek data bSuccess = PeekNamedPipe(hFile,pReadBuf,MESSAGE_SIZE,pdwBytesRead,pTotalBytesAvail,pBytesLeftThisMessage );
参考:
关于命名管道安全的论文:https://www.blakewatts.com/namedpipepaper.html
绕过防火墙:https://www.secpulse.com/archives/164049.html
Remote Procedure Call (RPC)
介绍
远程过程调用(Remote Procedure Calls, RPC)是一种使客户端和服务器之间能够跨进程和机器边界进行数据通信(网络通信)的技术。因此 RPC 是一种进程间通信 ( IPC ) 技术。
顾名思义,RPC用于调用远程服务器以交换/传递数据或触发远程例程。但术语“远程”在这里是有点误导人的。因为RPC技术的目标是让开发者只要按照设计好的函数原型发起调用,就既可以调用本地的函数实现,也可以调用远程的函数实现。RPC技术提供了一种透明调用机制让使用者不必显式的区分本地调用和远程调用。
所以RPC 服务器不必非要位于远程机器上,理论上甚至不必位于不同的进程中(可以在dll中实现RPC服务器和客户端,将它们加载到相同的进程中互相通信)。
历史:https://kganugapati.wordpress.com/tag/msrpc/
RPC工作模型
RPC 是一种客户端-服务器技术,其消息传递体系结构类似于 COM,宏观上由以下三个组件组成:
-
负责注册 RPC 接口和相关绑定信息的服务器和客户端进程
-
负责转换传入和传出数据的服务器和客户端存根
-
服务器和客户端的 RPC 运行时库 (rpcrt4.dll),它获取存根数据并使用指定的协议通过网络发送它们
RPC 协议序列
RPC 协议序列是一个常量字符串,它定义了 RPC 运行时应该使用哪种RPC协议、传输协议来传输消息。
RPC的传输层有很多种选择,比如命名管道、TCP、UDP、IPX、SPX、LPC等。
在传输层上面,RPC层存在多种协议,目前,Microsoft 支持以下三种 RPC 协议:
-
面向连接的协议,全称为面向连接的网络计算架构 (NCACN)
-
数据报文协议, 全称为数据报文网络计算架构 (NCADG)
-
本地远程过程调用,全称为本地远程过程调用网络计算架构 (NCALRPC)
在大多数跨系统边界进行连接的场景中,你会发现NCACN,相比而言,本地RPC通信一般用NCALRPC。。
在使用RPC时,RPC双方必须明确RPC协议和传输层协议。所以协议序列就是用来标识不同的协议组合,例如ncacn_ip_tcp,用于基于TCP 数据包的面向连接的通信。
可以在以下位置找到 RPC 协议序列常量的完整列表:https://docs.microsoft.com/en-us/windows/win32/rpc/protocol-sequence-constants
最常用的协议序列如下所示:
常数/值 | 描述 |
---|---|
ncacn_ip_tcp | 面向连接的传输控制协议/互联网协议 (TCP/IP) |
ncacn_http | 使用 Microsoft Internet Information Server 作为 HTTP 代理的面向连接的 TCP/IP |
ncacn_np | 面向连接的命名管道(通过 SMB。) |
ncadg_ip_udp | 数据报(无连接) 用户数据报协议/互联网协议 (UDP/IP) |
ncalrpc | 本地过程调用(ALPC) |
RPC 接口
为了建立通信通道,RPC 运行时需要知道您的服务器提供了哪些方法(也称为“函数”)和参数,以及您的客户端正在发送哪些数据。这些信息在所谓的“接口”中定义。
接口是在接口定义语言 (IDL) 文件中定义的。然后由 Microsoft IDL 编译器 (midl.exe) 将其中的定义编译成服务器和客户端使用的头文件和源代码文件。
定义 RPC 接口的 IDL 文件示例:
[// UUID: 每个接口都与一个 128 位或 16 字节的通用唯一标识符 (UUID) 相关联。uuid(9510b60a-2eac-43fc-8077-aaefbdf3752b),// 这是该接口的1.0版本version(1.0),// 使用一个名为sec的隐式句柄implicit_handle(handle_t sec) ] interface Test //接口命名为Test {//接受以零结尾的字符串的函数void fn1([in, string] const char* szString);void fn2(); }
上面显示了正在公开的接口的 UUID、接口名称 (Test),与该接口交互时可以调用的方法以及交互的参数。
该接口可以被认为是 RPC 客户端和服务器之间的桥梁。RPC 客户端必须实现该接口,而 RCP 服务器必须公开完全相同的接口,否则将无法进行通信。
注:fn1函数的参数定义中的[in, string]语句不是强制性的,只是有助于理解该参数的用途。
RPC 绑定
一旦客户端连接到一个 RPC 服务器(我们稍后会介绍如何完成),就创建了 Microsoft 所谓的“绑定句柄”。或者用微软的话来说:
绑定是在客户端程序和服务器程序之间创建逻辑连接的过程。构成客户端和服务器之间绑定的信息由称为绑定句柄的结构表示。
存在三种类型的绑定句柄:
-
隐式
-
显式
-
自动
隐式绑定句柄允许您的客户端连接到特定的 RPC 服务器并与之通信(由 IDL 文件中的 UUID 指定)。缺点是隐式绑定不是线程安全的,因此多线程应用程序应该使用显式绑定。隐式绑定句柄在 IDL 文件中定义,如上面的示例 IDL 代码中所示。
显式绑定句柄允许您的客户端连接到多个 RPC 服务器并与之通信。一般建议使用显式绑定句柄,因为它是线程安全的,并且允许多个连接。
对于懒惰的开发人员来说,自动绑定是介于两者之间的一种解决方案,让RPC在运行时确定需要什么也不失了一种好的方案。
到这你可能会问,为什么我需要绑定句柄?
把绑定句柄想象成客户端和服务器之间通信通道的表示,就像罐头电话中的电线一样,你手里有一个通信通道(“线”),那么你可以给这个通信通道添加属性,比如给你的线外面套上一层胶管使他不容易被别人扯断,从而保证一定的安全。
与此类似,绑定句柄允许你保护客户端和服务器之间的连接(你可以给绑定句柄添加安全性的东西),从而形成Microsoft术语中的“经过身份验证的”绑定。
匿名和认证绑定
假设您正在运行一个简单普通的 RPC 服务器,现在一个客户端连接到您的服务器。如果你没有指定任何东西(稍后会说),那么客户端和服务器之间的这种连接被称为匿名绑定或未验证绑定,因为你的服务器根本不知道是谁连接的。
所以为了避免任何客户端连接,并提高服务器的安全性,你可以采取三种措施:
-
您可以在注册服务器接口时设置注册标志
-
您可以使用自定义例程设置安全回调,以检查请求客户端是否应该被允许或拒绝
-
您可以设置与绑定句柄相关联的身份验证信息,以指定安全服务提供者和表示RPC服务器的SPN
让我们一步一步来看看这三种办法:
注册标志
首先,当您创建服务器时,您需要注册您的接口,例如调用RpcServerRegisterIf2。在RpcServerRegisterIf2的第四个参数,您可以指定接口注册标志,例如 RPC_IF_ALLOW_LOCAL_ONLY 以仅允许本地连接。
RPC_STATUS rpcStatus = RpcServerRegisterIf2(Example1_v1_0_s_ifspec, // Interface to register.NULL, // NULL type UUIDNULL, // Use the MIDL generated entry-point vector.RPC_IF_ALLOW_LOCAL_ONLY, // Only allow local connectionsRPC_C_LISTEN_MAX_CALLS_DEFAULT, // Use default number of concurrent calls.(unsigned)-1, // Infinite max size of incoming data blocks.NULL // No security callback. );
安全回调
可以以你喜欢的任何方式自行实现过滤机制
RPC_STATUS CALLBACK SecurityCallback(RPC_IF_HANDLE hInterface, void* pBindingHandle) {return RPC_S_OK; //这里表示允许任何连接 }
使用的话只需将RpcServerRegisterIf2函数的最后一个参数设置为安全回调函数的名称
RPC_STATUS rpcStatus = RpcServerRegisterIf2(Example1_v1_0_s_ifspec, // Interface to register.NULL, // Use the MIDL generated entry-point vector.NULL, // Use the MIDL generated entry-point vector.RPC_IF_ALLOW_LOCAL_ONLY, // Only allow local connectionsRPC_C_LISTEN_MAX_CALLS_DEFAULT, // Use default number of concurrent calls.(unsigned)-1, // Infinite max size of incoming data blocks.SecurityCallback // No security callback. );
认证绑定
最后一个措施是一组额外的Windows API,它可以让服务器和客户端验证您的绑定
认证绑定结合正确的注册标志 (RPC_IF_ALLOW_SECURE_ONLY) 使您的 RPC 服务器能够确保只有经过身份验证的用户才能连接;并且——如果客户端允许的话——能够使服务器通过模拟客户端来确定谁连接到它。
虽然也可以使用 SecurityCallback 拒绝任何匿名客户端连接,但自行实现过滤机制不太容易。
在服务器端验证绑定:
RPC_STATUS rpcStatus = RpcServerRegisterAuthInfo(pszSpn, // 服务器主体名称RPC_C_AUTHN_WINNT, // 使用NTLM作为身份验证服务提供者NULL, NULL );
在客户端验证绑定:
RPC_STATUS status = RpcBindingSetAuthInfoEx(hExplicitBinding, // 客户端的绑定句柄pszHostSPN, // 服务器的服务主体名称(SPN)RPC_C_AUTHN_LEVEL_PKT, // 身份验证级别PKTRPC_C_AUTHN_WINNT, // 使用NTLM作为身份验证服务提供者NULL, // 使用当前线程凭据RPC_C_AUTHZ_NAME, // 基于提供的SPN授权&secQos // QOS结构体 );
客户端的有趣之处在于,您可以使用经过身份验证的绑定句柄设置安全服务质量 (QOS)结构。例如,可以在客户端使用此 QOS 结构来确定模拟级别
这里需要注意:在服务器端设置验证绑定,不会强制客户端进行身份验证。如果在服务器端没有设置标志或者只设置了RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH,未经身份验证的客户端仍然可以连接到RPC服务器。但是,设置RPC_IF_ALLOW_SECURE_ONLY标志可以防止未经身份验证的客户端绑定,因为客户端不能在不创建验证绑定的情况下设置身份验证级别
知名端点 & 动态端点
当你启动 RPC 服务器时,服务器会注册一个接口(RpcServerRegisterIf2),它还需要定义它想要侦听的协议序列(例如' ncacn_ip_tcp ', ' ncacn_np ',…),好像就完了是不是?但此时在服务器中指定的协议序列字符串不足以打开 RPC 端口连接。
假设你指定了“ncacn_ip_tcp”作为你的协议序列,这意味着你指示你的服务器打开一个通过TCP/IP接受连接的RPC连接…但是…服务器应该在哪个TCP端口上打开连接呢?所以为了解决这个问题,与ncacn_ip_tcp类似,其他协议序列也需要更多关于在何处打开连接对象的信息:
-
ncacn_ip_tcp 需要一个 TCP 端口号,例如 9999
-
ncacn_np 需要一个命名管道名称,例如 “\pipe\FRPC-NP”
-
ncalrpc 需要一个 ALPC 端口名称,例如“\RPC Control\FRPC-LRPC”
假设将ncacn_np指定为协议序列并设置命名管道名称为“\pipe\FRPC-NP”。
RPC 服务器将愉快地启动并等待客户端连接。另外,客户端需要知道它应该连接到哪里。你告诉客户端你的服务器的名称,指定协议序列为ncacn_np并将命名管道名称设置为你在服务器中定义的相同名称(“\pipe\FRPC-NP”)。
然后客户端成功连接,正如你已经建立了一个RPC客户端和服务器基于一个众所周知的端点\pipe\FRPC-NP
使用知名的RPC 端点只意味着你预先知道所有绑定信息(协议序列和端点地址),并且如果您愿意的话,还可以在客户端和服务器中对这些信息进行硬编码。使用知名端点是建立你的第一个 RPC 客户端/服务器连接的最简单方法。
那么什么是动态端点,为什么要使用它们?如果我们现在选择ncacn_ip_tcp作为协议序列,我们如何知道哪些TCP端口仍然是可用的? 好吧,我们可以指定我们的程序需要9999端口才能正常工作,并且要确保这个端口没有被使用,但我们也可以要求 Windows 系统为我们分配一个空闲的端口。是的,这就是动态端点。十分简单的一个概念。
那么我们动态地分配了一个端口,客户端如何知道连接到哪里?...这是动态端点的另一个特点:如果你选择动态端点,你需要有人告诉你的客户端你使用的是什么端口,这个家伙便是RPC Endpoint Mapper服务(在Windows系统上默认是运行的)。如果服务器使用动态端点,它需要调用 RPC 端点映射器来告诉它注册的接口和函数(在 IDL 文件中指定)。一旦客户端尝试创建绑定,它将查询服务器的RPC端点映射器来匹配接口,端点映射器将填充缺失的信息(例如TCP端口)来创建绑定
动态端点的主要优点是在端点地址空间有限时自动找到可用的端点地址。
知名端点实现
RPC_STATUS rpcStatus; // 创建绑定信息 rpcStatus = RpcServerUseProtseqEp((RPC_WSTR)L"ncacn_np", // 使用命名管道协议序列RPC_C_PROTSEQ_MAX_REQS_DEFAULT, // 等待队列长度,使用 RPC_C_PROTSEQ_MAX_REQS_DEFAULT 指定默认值(RPC_WSTR)L"\\pipe\\FRPC-NP", // 命名管道名称NULL // 没有Secuirty描述符 ); // 注册接口 rpcStatus = RpcServerRegisterIf2(...) // 可选:注册认证信息 rpcStatus = RpcServerRegisterAuthInfo(...) // 监听收到的客户端连接 rpcStatus = RpcServerListen(1, //建议的最小线程数RPC_C_LISTEN_MAX_CALLS_DEFAULT, //建议的最大线程数。FALSE //立刻开始监听 );
动态端点实现
RPC_STATUS rpcStatus; RPC_BINDING_VECTOR* pbindingVector = 0; // 创建绑定信息 rpcStatus = RpcServerUseProtseq((RPC_WSTR)L"ncacn_ip_tcp", // tcp/ipRPC_C_PROTSEQ_MAX_REQS_DEFAULT, // 等待队列长度,使用 RPC_C_PROTSEQ_MAX_REQS_DEFAULT 指定默认值NULL // 没有Secuirty描述符 ); // 注册接口 rpcStatus = RpcServerRegisterIf2(...) // 可选:注册认证信息 rpcStatus = RpcServerRegisterAuthInfo(...) // 获取服务器绑定句柄的向量 rpcStatus = RpcServerInqBindings(&pbindingVector); // 添加本地端点映射数据库中的服务器地址信息 rpcStatus = RpcEpRegister(Example1_v1_0_s_ifspec, //通过IDL定义的接口pbindingVector, //绑定句柄的向量0, //不用uuid(RPC_WSTR)L"MyDyamicEndpointServer" //注释 ); // 监听收到的客户端连接 rpcStatus = RpcServerListen(1, //建议的最小线程数RPC_C_LISTEN_MAX_CALLS_DEFAULT, //建议的最大线程数。FALSE //立刻开始监听 );
RPC 通信流程
综上所述,通信流程可以总结如下:
-
服务器注册接口,例如使用RpcServerRegisterIf2
-
服务器使用RpcServerUseProtseq和RpcServerInqBindings创建绑定信息(RpcServerInqBindings对于知名端点是可选的)
-
服务器使用RpcEpRegister注册 Endpoints (对于知名端点是可选的)
-
服务器 可以使用RpcServerRegisterAuthInfo注册身份验证信息(可选)
-
服务器使用RpcServerListen监听客户端连接
-
客户端创建一个绑定句柄,使用RpcStringBindingCompose & RpcBindingFromStringBinding
-
客户端RPC 运行时库通过查询服务器主机系统上的 Endpoint Mapper 找到服务器进程(仅动态端点需要)
-
客户端 可以使用RpcBindingSetAuthInfo验证绑定句柄(可选)
-
客户端通过调用使用的接口中定义的函数之一进行 RPC 调用
-
客户端RPC 运行时库在 NDR 运行时的帮助下以NDR格式编组参数并将它们发送到服务器,
-
服务器的RPC 运行时库将编组的参数提供给存根,存根将它们解组,然后将它们传递给服务器例程。
-
当服务器例程返回时,存根获取 [out] 和 [in, out] 参数(在接口 IDL 文件中定义)和返回值,对它们进行编组,并将编组后的数据发送到服务器的 RPC 运行时库,它将它们传输回客户端。
实现
.idl
//uuid可以使用VS工具生成 [uuid("00000001-EAF3-4A7A-A0F2-BCE4C30DA77E"),version(1.0) ]interface HelloWorld {int intAdd(int x, int y); }
.acf(对RPC接口进行配置)
[implicit_handle(handle_t test_Binding) ]interface HelloWorld { }
server.cpp
#include <windows.h> #include <stdlib.h> #include <stdio.h>#include "HelloWorld_h.h"int intAdd(int x, int y) {printf("%d + %d = %d\n", x, y, x + y);return x + y; }int main(int argc, wchar_t* argv[]) {// 采用tcp协议,13521端口RpcServerUseProtseqEp((RPC_WSTR)L"ncacn_ip_tcp", RPC_C_PROTSEQ_MAX_REQS_DEFAULT,(RPC_WSTR)L"13521", NULL);// 注册,HelloWorld_v1_0_s_ifspec定义域头文件test.h// 注意:从Windows XP SP2开始,增强了安全性的要求,如果用RpcServerRegisterIf()注册接口,客户端调用时会出现// RpcExceptionCode() == 5,即Access Denied的错误,因此,必须用RpcServerRegisterIfEx带RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH标志// 允许客户端直接调用RpcServerRegisterIfEx(HelloWorld_v1_0_s_ifspec, NULL, NULL, RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH, 0, NULL);// 开始监听,本函数将一直阻塞RPC_STATUS result = RpcServerListen(1, 20, FALSE);printf("end-------------RPC_STATUS: %d\n", result);return 0; }// 下面的函数是为了满足链接需要而写的,没有的话会出现链接错误 void __RPC_FAR* __RPC_USER midl_user_allocate(size_t len) {return(malloc(len)); } void __RPC_USER midl_user_free(void __RPC_FAR* ptr) {free(ptr); }
client.cpp
#include <windows.h> #include <stdlib.h> #include <stdio.h> #include <locale.h>#include "HelloWorld_h.h"int wmain(int argc, wchar_t* argv[]) {if (argc < 2) {_wsetlocale(LC_ALL, L"chs");wprintf_s(L"请输入ip地址,格式为 testClient.exe xxx.xxx.xxx.xxx\n");Sleep(2000);return 0;}wprintf_s(L"server ip: %s\n", argv[1]);RPC_WSTR pszStringBinding = NULL;int x, y, rval;RpcStringBindingCompose(NULL,(RPC_WSTR)L"ncacn_ip_tcp",(RPC_WSTR)argv[1] /*NULL*/,(RPC_WSTR)L"13521",NULL,&pszStringBinding);// 绑定接口,这里要和 test.acf 的配置一致,那么就是test_BindingRpcBindingFromStringBinding(pszStringBinding, &test_Binding);// 下面是调用服务端的函数了RpcTryExcept{while (1){printf("Input two integer: ");scanf_s("%d %d", &x, &y);rval = intAdd(x, y);printf("%d\n", rval);Sleep(2000);}}RpcExcept(1){printf("RPC Exception %d\n", RpcExceptionCode());Sleep(2000);}RpcEndExcept// 释放资源RpcStringFree(&pszStringBinding);RpcBindingFree(&test_Binding);return 0; }// 下面的函数是为了满足链接需要而写的,没有的话会出现链接错误 void __RPC_FAR* __RPC_USER midl_user_allocate(size_t len) {return(malloc(len)); } void __RPC_USER midl_user_free(void __RPC_FAR* ptr) {free(ptr); }
Attack
寻找目标
在我们探讨在RPC有哪些漏洞和攻击方法之前,我们首先需要先研究一下如何在系统上找到我们的攻击目标:RPC服务器和客户端
RPC 服务器
一般来说,构建服务器需要通过指定所需信息(协议序列和端点地址)并调用特定的Windows API。所以,逆向思维,在本地系统上查找 RPC 服务器的方法便是查找导入这些 RPC Windows API 的程序。那么一种简单的方法是使用现在随 Visual Studio 一起提供的DumpBin实用程序。
在C:\Windows\System32\
下搜索RPC服务器如下,此代码段将可执行文件的名称打印到控制台,并将整个 DumpBin 输出打印到文件RpcServerListen.txt
Get-ChildItem -Path "C:\Windows\System32\" -Filter "*.exe" -Recurse -ErrorAction SilentlyContinue | % { $out=$(C:\"Program Files (x86)"\"Microsoft Visual Studio 14.0"\VC\bin\dumpbin.exe /IMPORTS:rpcrt4.dll $_.VersionInfo.FileName); If($out -like "*RpcServerListen*"){ Write-Host "[+] Exe starting RPC Server: $($_.VersionInfo.FileName)"; Write-Output "[+] $($_.VersionInfo.FileName)`n`n $($out|%{"$_`n"})" | Out-File -FilePath RpcServerListen.txt -Append } }
另一种查找感兴趣的 RPC 服务器的方法是在本地或任何远程系统上查询 RPC Endpoint Mapper,Microsoft 有一个叫为PortQry的工具可以用来执行此操作(注意,只有RPC接口注册到目标的Endpoint Mapper才能查询出来,而知名端点是不必通知Endpoint Mapper有关其接口的信息的)
PortQry.exe -n <HostName> -e 135
RpcView枚举
还可以通过调用RpcMgmtEpEltInqBegin并通过RpcMgmtEpEltInqNext遍历接口来直接查询
/** Copyright (c) BindView Development Corporation, 2001* See LICENSE file.* Author: Todd Sabin <tsabin@razor.bindview.com>*/#include <windows.h> #include <winnt.h>#include <stdio.h>#include <rpc.h> #include <rpcdce.h>static int verbosity;int try_protocol (char *protocol, char *server) {unsigned char *pStringBinding = NULL;RPC_BINDING_HANDLE hRpc;RPC_EP_INQ_HANDLE hInq;RPC_STATUS rpcErr;RPC_STATUS rpcErr2;int numFound = 0;//// Compose the string binding//rpcErr = RpcStringBindingCompose (NULL, protocol, server,NULL, NULL, &pStringBinding);if (rpcErr != RPC_S_OK) {fprintf (stderr, "RpcStringBindingCompose failed: %d\n", rpcErr);return numFound;}//// Convert to real binding//rpcErr = RpcBindingFromStringBinding (pStringBinding, &hRpc);if (rpcErr != RPC_S_OK) {fprintf (stderr, "RpcBindingFromStringBinding failed: %d\n", rpcErr);RpcStringFree (&pStringBinding);return numFound;}//// Begin Ep enum//rpcErr = RpcMgmtEpEltInqBegin (hRpc, RPC_C_EP_ALL_ELTS, NULL, 0,NULL, &hInq);if (rpcErr != RPC_S_OK) {fprintf (stderr, "RpcMgmtEpEltInqBegin failed: %d\n", rpcErr);RpcStringFree (&pStringBinding);RpcBindingFree (hRpc);return numFound;}//// While Next succeeds//do {RPC_IF_ID IfId;RPC_IF_ID_VECTOR *pVector;RPC_STATS_VECTOR *pStats;RPC_BINDING_HANDLE hEnumBind;UUID uuid;unsigned char *pAnnot;rpcErr = RpcMgmtEpEltInqNext (hInq, &IfId, &hEnumBind, &uuid, &pAnnot);if (rpcErr == RPC_S_OK) {unsigned char *str = NULL;unsigned char *princName = NULL;numFound++;//// Print IfId//if (UuidToString (&IfId.Uuid, &str) == RPC_S_OK) {printf ("IfId: %s version %d.%d\n", str, IfId.VersMajor,IfId.VersMinor);RpcStringFree (&str);}//// Print Annot//if (pAnnot) {printf ("Annotation: %s\n", pAnnot);RpcStringFree (&pAnnot);}//// Print object ID//if (UuidToString (&uuid, &str) == RPC_S_OK) {printf ("UUID: %s\n", str);RpcStringFree (&str);}//// Print Binding//if (RpcBindingToStringBinding (hEnumBind, &str) == RPC_S_OK) {printf ("Binding: %s\n", str);RpcStringFree (&str);}if (verbosity >= 1) {unsigned char *strBinding = NULL;unsigned char *strObj = NULL;unsigned char *strProtseq = NULL;unsigned char *strNetaddr = NULL;unsigned char *strEndpoint = NULL;unsigned char *strNetoptions = NULL;RPC_BINDING_HANDLE hIfidsBind;//// Ask the RPC server for its supported interfaces////// Because some of the binding handles may refer to// the machine name, or a NAT'd address that we may// not be able to resolve/reach, parse the binding and// replace the network address with the one specified// from the command line. Unfortunately, this won't// work for ncacn_nb_tcp bindings because the actual// NetBIOS name is required. So special case those.//// Also, skip ncalrpc bindings, as they are not// reachable from a remote machine.//rpcErr2 = RpcBindingToStringBinding (hEnumBind, &strBinding);RpcBindingFree (hEnumBind);if (rpcErr2 != RPC_S_OK) {fprintf (stderr, ("RpcBindingToStringBinding failed\n"));printf ("\n");continue;}if (strstr (strBinding, "ncalrpc") != NULL) {RpcStringFree (&strBinding);printf ("\n");continue;}rpcErr2 = RpcStringBindingParse (strBinding, &strObj, &strProtseq,&strNetaddr, &strEndpoint, &strNetoptions);RpcStringFree (&strBinding);strBinding = NULL;if (rpcErr2 != RPC_S_OK) {fprintf (stderr, ("RpcStringBindingParse failed\n"));printf ("\n");continue;}rpcErr2 = RpcStringBindingCompose (strObj, strProtseq,strcmp ("ncacn_nb_tcp", strProtseq) == 0 ? strNetaddr : server,strEndpoint, strNetoptions,&strBinding);RpcStringFree (&strObj);RpcStringFree (&strProtseq);RpcStringFree (&strNetaddr);RpcStringFree (&strEndpoint);RpcStringFree (&strNetoptions);if (rpcErr2 != RPC_S_OK) {fprintf (stderr, ("RpcStringBindingCompose failed\n"));printf ("\n");continue;}rpcErr2 = RpcBindingFromStringBinding (strBinding, &hIfidsBind);RpcStringFree (&strBinding);if (rpcErr2 != RPC_S_OK) {fprintf (stderr, ("RpcBindingFromStringBinding failed\n"));printf ("\n");continue;}if ((rpcErr2 = RpcMgmtInqIfIds (hIfidsBind, &pVector)) == RPC_S_OK) {unsigned int i;printf ("RpcMgmtInqIfIds succeeded\n");printf ("Interfaces: %d\n", pVector->Count);for (i=0; i<pVector->Count; i++) {unsigned char *str = NULL;UuidToString (&pVector->IfId[i]->Uuid, &str);printf (" %s v%d.%d\n", str ? str : "(null)",pVector->IfId[i]->VersMajor,pVector->IfId[i]->VersMinor);if (str) RpcStringFree (&str);}RpcIfIdVectorFree (&pVector);} else {printf ("RpcMgmtInqIfIds failed: 0x%x\n", rpcErr2);}if (verbosity >= 2) {if ((rpcErr2 = RpcMgmtInqServerPrincName (hEnumBind,RPC_C_AUTHN_WINNT,&princName)) == RPC_S_OK) {printf ("RpcMgmtInqServerPrincName succeeded\n");printf ("Name: %s\n", princName);RpcStringFree (&princName);} else {printf ("RpcMgmtInqServerPrincName failed: 0x%x\n", rpcErr2);}if ((rpcErr2 = RpcMgmtInqStats (hEnumBind,&pStats)) == RPC_S_OK) {unsigned int i;printf ("RpcMgmtInqStats succeeded\n");for (i=0; i<pStats->Count; i++) {printf (" Stats[%d]: %d\n", i, pStats->Stats[i]);}RpcMgmtStatsVectorFree (&pStats);} else {printf ("RpcMgmtInqStats failed: 0x%x\n", rpcErr2);}}RpcBindingFree (hIfidsBind);}printf ("\n");}} while (rpcErr != RPC_X_NO_MORE_ENTRIES);//// Done//RpcStringFree (&pStringBinding);RpcBindingFree (hRpc);return numFound; }char *protocols[] = {"ncacn_ip_tcp","ncadg_ip_udp","ncacn_np","ncacn_nb_tcp","ncacn_http", }; #define NUM_PROTOCOLS (sizeof (protocols) / sizeof (protocols[0]))void Usage (char *app) {printf ("Usage: %s [options] <target>\n", app);printf (" options:\n");printf (" -p protseq -- use protocol sequence\n", app);printf (" -v -- increase verbosity\n", app);exit (1); }int main (int argc, char *argv[1]) {int i, j;char *target = NULL;char *protseq = NULL;for (j=1; j<argc; j++) {if (argv[j][0] == '-') {switch (argv[j][1]) {case 'v':verbosity++;break;case 'p':protseq = argv[++j];break;default:Usage (argv[0]);break;}} else {target = argv[j];}}if (!target) {fprintf (stderr, "Usage: %s <server>\n", argv[0]);exit (1);}if (protseq) {try_protocol (protseq, target);} else {for (i=0; i<NUM_PROTOCOLS; i++) {if (try_protocol (protocols[i], target) > 0) {break;}}}return 0; }
RPC 客户端
相比较于服务器可以查询Endpoint Mapper找到它而言,Windows上是没有一个管理程序知道当前正在运行哪些 RPC 客户端的,因此只有两个选择来寻找客户端:
-
查找使用客户端RPC api的可执行文件/进程
-
通过客户端的特定行为
查找导入客户端 RPC API 的本地可执行文件类似于我们使用DumpBin查找服务器。一个特点什么鲜明的 Windows API 是RpcStringBindingCompose:
Get-ChildItem -Path "C:\Windows\System32\" -Filter "*.exe" -Recurse -ErrorAction SilentlyContinue | % { $out=$(C:\"Program Files (x86)"\"Microsoft Visual Studio 14.0"\VC\bin\dumpbin.exe /IMPORTS:rpcrt4.dll $_.VersionInfo.FileName); If($out -like "*RpcStringBindingCompose*"){ Write-Host "[+] Exe creates RPC Binding (potential RPC Client) : $($_.VersionInfo.FileName)"; Write-Output "[+] $($_.VersionInfo.FileName)`n`n $($out|%{"$_`n"})" | Out-File -FilePath RpcClients.txt -Append } }
查找 RPC 客户端的另一种选择是在它们连接到目标时发现它们。Wireshark 有一个“DCERPC”过滤器,可用于发现连接。
绑定请求是我们可以查找的用于标识客户端的东西之一。选中包,我们可以看到客户端尝试绑定到 UUID 为“d6b1ad2b-b550-4729-b6c2-1651f58480c3”的服务器接口。
未授权访问
你可以实现自己的客户端来尝试是否可以未经授权连接到服务器。
通过前面的学习,我们已经知道服务器通过调用RpcServerRegisterAuthInfo及其 SPN 和指定的服务提供者来设置身份验证信息,请注意,经过身份验证的服务器绑定并不会强制客户端使用经过身份验证的绑定。换句话说:仅仅因为服务器设置了身份验证信息,并不意味着客户端需要通过经过身份验证的绑定进行连接
请记住,默认情况下,安全性是可选的来源:https ://docs.microsoft.com/en-us/windows/win32/api/rpcdce/nf-rpcdce-rpcserverregisterifex
连接到服务器后,“下一步要做什么?”的问题出现了......好吧,可以调用接口函数,但坏消息是:需要首先识别函数名称和参数,这归结为对目标服务器进行逆向工程。如果你不是在搞一个纯粹的 RPC 服务器,而是一个 COM 服务器(COM,尤其是 DCOM,在后台使用 RPC),该服务器可能带有一个类型库 (.tlb),你可以使用它来查找接口函数。其外的使用Rpcview就好了(这个工具需要自己编译)。
客户端模拟
模拟客户端的方法如下:
-
需要一个 RPC 客户端连接到服务器
-
客户端必须使用经过身份验证的绑定(否则就没有可以模拟的安全信息)
-
客户端不得在SecurityImpersonation下设置 Impersonation Level 身份验证绑定----下面会解释
模拟的过程很简单:
-
从服务器接口函数中调用RpcImpersonateClient
-
如果该调用成功,服务器的线程上下文将更改为客户端的安全上下文,您可以调用GetCurrentThread和OpenThreadToken来接收客户端的模拟令牌。
-
调用DuplicateTokenEx将 Impersonation 令牌转换为主令牌后,您可以通过调用RpcRevertToSelfEx愉快地返回到原始服务器线程上下文
-
最后,您可以调用CreateProcessWithTokenW使用客户端的令牌创建一个新进程。
如上面的步骤中所述,您只需要一个连接到您的服务器的客户端,并且该客户端必须使用经过身份验证的绑定。如果客户端未对其绑定进行身份验证,则对RpcImpersonateClient的调用将导致错误 1764 (RPC_S_BINDING_HAS_NO_AUTH)。
找到可以连接到服务器的合适客户端是这个漏洞利用链中的棘手部分,我不能在这里就如何找到这些连接给出一般性建议。原因之一是它取决于客户端使用的协议序列,像ncacn_ip_tcp就去找未应答的 TCP 调用,ncacn_np就去找未应答的命名管道连接尝试。
好了,为啥客户端不得在*SecurityImpersonation下设置 Impersonation Level ?
还记得在创建经过身份验证的绑定时可以在客户端设置服务质量(QOS)结构吗? 正如在验证绑定一节中所说的,在连接到服务器时,可以使用该结构来确定模拟级别。有趣的是,如果您不设置任何 QOS 结构,则默认值为 SecurityImpersonation,它允许任何服务器模拟RPC客户端,只要客户端不显式设置低于SecurityImpersonation的模拟级别
利用服务器模拟失败
前面我们介绍了模拟客户端时涉及的步骤,这些步骤同样适用于 RPC 模拟(以及所有其他类似技术),其中以下两个步骤特别有趣:
>> 第 8 步:然后将服务器的线程上下文更改为客户端的安全上下文。>> 第 9 步:服务器在客户端的安全上下文中执行的任何操作和服务器调用的任何函数都是使用客户端的身份进行的,从而模拟客户端。
根据章节标题,你现在可能已经猜到了……如果模拟失败并且服务器不检查怎么办?
对RpcImpersonateClient的调用返回模拟操作的状态,服务器检查该状态至关重要。如果模拟成功,那么之后您将位于客户端的安全上下文中,但如果失败,您将处于调用RpcImpersonateClient的相同旧安全上下文中。现在,RPC 服务器可能是在一个高安全性的上下文中,而请求的客户端在较低的安全性上下文,所以它可能会尝试模拟其客户端以级别更低的客户端安全性上下文中运行客户端操作。那么作为攻击者,可以通过强制服务器端进行失败的模拟尝试,从而导致服务器在更高安全性的服务器上下文中执行客户端操作,从而实现权限提升。
这种攻击场景的方法很简单:
-
您需要一个模拟其客户端的服务器,并且在执行进一步操作之前不仔细检查RpcImpersonateClient的返回状态。
-
从您的客户端的角度来看,在模拟尝试后服务器所采取的操作必须是可利用的。
-
需要强制模拟尝试失败。
如果您阅读了前面的部分并记下了如何使用DumpBin ,那么查找试图模拟客户端的本地服务器是一项简单的任务。
找到可利用的服务器也什么简单,通过procmon、processhacker、wireshark等一系列工具监测一些事件动作网络连接从而筛选出目标。一个相当简单但功能强大的示例可能是服务器执行的文件操作;也许你可以使用连接在一个写保护权限的系统路径中创建一个文件
清单上的最后一项是导致服务器的模拟尝试失败,这是工作中最简单的部分。有两种方法可以实现这一点:
-
可以从未经身份验证的绑定进行连接
-
可以从经过身份验证的绑定连接并将 QOS 结构的模拟级别设置为SecurityAnonymous
这些操作中的任何一个都将安全地导致模拟尝试失败。
其实这个安全问题,微软也RpcImpersonateClient函数的备注部分特别提醒了这一点 :
如果对 RpcImpersonateClient 的调用由于任何原因失败,则不会模拟客户端连接,并且客户端请求是在进程的安全上下文中发出的。如果该进程作为高权限帐户(例如 LocalSystem)或作为管理组的成员运行,则用户可能能够执行否则会被禁止的操作。因此,始终检查调用的返回值很重要,如果失败,则引发错误;不要继续执行客户端请求。来源:RpcImpersonateClient:安全备注
参考:
What is RPC?https://www.youtube.com/watch?v=MdaGuP6-bKs
Microsoft 的 RPC 文档:https ://docs.microsoft.com/en-us/windows/win32/rpc/overviews
xpn:https ://blog.xpnsec.com/analysing-rpc-with-ghidra-neo4j/
rpc编写https://www.codeproject.com/Articles/4837/Introduction-to-RPC-Part-1#IDLRPCandyou1
从防御的角度看RPChttps://ipc-research.readthedocs.io/en/latest/subpages/RPC.html#identifying-the-rpc-servers
ALPC
介绍
在本地使用RPC就是使用ALPC
在讨论了可以在远程和本地使用的两种进程间通信 ( IPC ) 协议之后,即命名管道和RPC,我们再来研究一种只能在本地使用的技术。关于ALPC名称有两种说法:一种是Advanced Local Procedure Call(增强) ,另一种是Asynchronous Local Procedure Call(异步)。
LPC(Local Inter-Process Communication)
在深入ALPC之前,首先让我们了解一下什么是LPC。本地过程调用机制(严格来说,更像是一种消息通信机制)是在 1993-94 年与原始 Windows NT 内核一起引入的,作为同步进程间通信工具。它的同步特性意味着客户端/服务器必须等待消息发送并采取行动,然后才能继续执行。这是 ALPC 旨在取代的主要缺陷之一,也是一些人将 ALPC 称为异步LPC 的原因。ALPC 是在 Windows Vista 中出现的,至少从 Windows 7 开始,LPC 已从 NT 内核中完全删除。为了不破坏遗留应用程序并允许向后兼容,微软保留了用于创建 LPC 端口的函数,但函数调用被重定向为不创建 LPC,而是创建 ALPC 端口。
所以我们不会讨论已经被历史扫进垃圾堆里的LPC,只关注ALPC。
回到 ALPC。
ALPC 是一种快速、非常强大并且在 Windows 操作系统(内部)内非常广泛使用的进程间通信工具,但它没有公开出来,不打算供外界开发人员使用。因为对微软来说,ALPC是一个内部IPC工具,这意味着ALPC是没有文档化的,只被用作其他文档化的、旨在为开发人员使用的消息传输协议的底层传输技术,例如RPC。所以一种间接的方式是通过微软公开的RPC编程接口来使用ALPC
然而,ALPC没有文档记录的这一事实并不意味着 ALPC 是一个完全的黑匣子,因为许多聪明人已经对它的工作原理和它具有哪些组件进行了逆向工程。但是如果你作为开发者显然你不应该在生产开发中使用,不应该直接使用 ALPC 来构建软件。因为这个东西完全是逆向出来的,有很多可能导致安全性或稳定性问题的非显而易见的隐患。
此外,这篇文章中的所有信息也不一定100% 准确,因为 ALPC 没有记录在案。
ALPC 内部结构
ALPC通信的主要组件是ALPC端口对象。ALPC端口对象是一个内核对象,它的使用类似于网络套接字的使用,服务器打开一个套接字,客户机可以连接到它来交换消息。
启动WinObj,会发现在每个Windows操作系统上都有很多ALPC端口,一小部分可以在根路径下找到:
大多数 ALPC 端口都位于“RPC Control”路径下(记住,RPC在底层使用ALPC):
要开始进行ALPC通信,服务器需要打开一个客户端可以连接的ALPC端口,该端口称为ALPC 连接端口。然而,这并不是在ALPC通信流期间创建的唯一ALPC端口,另外还要创建了两个ALPC端口,分别用于客户端和服务器,以便向其传递消息(后面解释)所以,首先要记住的是:
-
总共有 3 个 ALPC 端口(2个在服务器端,1个在客户端)参与 ALPC 通信。
-
在上面的WinObj截图中看到的端口是ALPC连接端口,这是客户端可以连接到的端口。
尽管在一次 ALPC 通信中总共使用了 3 个 ALPC 端口,并且它们都以不同的名称引用(例如“ALPC 连接端口”),但只有一个ALPC端口内核对象,在ALPC通信中使用的所有三个端口都实例化它。这个 ALPC 内核对象的结架如下所示:
//0x1d8 bytes (sizeof) struct _ALPC_PORT {struct _LIST_ENTRY PortListEntry; //0x0struct _ALPC_COMMUNICATION_INFO* CommunicationInfo; //0x10struct _EPROCESS* OwnerProcess; //0x18VOID* CompletionPort; //0x20VOID* CompletionKey; //0x28struct _ALPC_COMPLETION_PACKET_LOOKASIDE* CompletionPacketLookaside; //0x30VOID* PortContext; //0x38struct _SECURITY_CLIENT_CONTEXT StaticSecurity; //0x40struct _EX_PUSH_LOCK IncomingQueueLock; //0x88struct _LIST_ENTRY MainQueue; //0x90struct _LIST_ENTRY LargeMessageQueue; //0xa0struct _EX_PUSH_LOCK PendingQueueLock; //0xb0struct _LIST_ENTRY PendingQueue; //0xb8struct _EX_PUSH_LOCK DirectQueueLock; //0xc8struct _LIST_ENTRY DirectQueue; //0xd0struct _EX_PUSH_LOCK WaitQueueLock; //0xe0struct _LIST_ENTRY WaitQueue; //0xe8union{struct _KSEMAPHORE* Semaphore; //0xf8struct _KEVENT* DummyEvent; //0xf8};struct _ALPC_PORT_ATTRIBUTES PortAttributes; //0x100struct _EX_PUSH_LOCK ResourceListLock; //0x148struct _LIST_ENTRY ResourceListHead; //0x150struct _EX_PUSH_LOCK PortObjectLock; //0x160struct _ALPC_COMPLETION_LIST* CompletionList; //0x168struct _CALLBACK_OBJECT* CallbackObject; //0x170VOID* CallbackContext; //0x178struct _LIST_ENTRY CanceledQueue; //0x180LONG SequenceNo; //0x190LONG ReferenceNo; //0x194struct _PALPC_PORT_REFERENCE_WAIT_BLOCK* ReferenceNoWait; //0x198union{struct{ULONG Initialized:1; //0x1a0ULONG Type:2; //0x1a0ULONG ConnectionPending:1; //0x1a0ULONG ConnectionRefused:1; //0x1a0ULONG Disconnected:1; //0x1a0ULONG Closed:1; //0x1a0ULONG NoFlushOnClose:1; //0x1a0ULONG ReturnExtendedInfo:1; //0x1a0ULONG Waitable:1; //0x1a0ULONG DynamicSecurity:1; //0x1a0ULONG Wow64CompletionList:1; //0x1a0ULONG Lpc:1; //0x1a0ULONG LpcToLpc:1; //0x1a0ULONG HasCompletionList:1; //0x1a0ULONG HadCompletionList:1; //0x1a0ULONG EnableCompletionList:1; //0x1a0} s1; //0x1a0ULONG State; //0x1a0} u1; //0x1a0struct _ALPC_PORT* TargetQueuePort; //0x1a8struct _ALPC_PORT* TargetSequencePort; //0x1b0struct _KALPC_MESSAGE* CachedMessage; //0x1b8ULONG MainQueueLength; //0x1c0ULONG LargeMessageQueueLength; //0x1c4ULONG PendingQueueLength; //0x1c8ULONG DirectQueueLength; //0x1ccULONG CanceledQueueLength; //0x1d0ULONG WaitQueueLength; //0x1d4 };
正如上面所看到的,ALPC内核对象是一个相当复杂的内核对象,它引用各种其他的对象类型。
ALPC 消息传递
为了更深入地研究ALPC,我们将研究ALPC消息流,以理解消息是如何发送的以及它们是什么样子的。首先,我们已经了解到在ALPC通信场景中涉及3个ALPC端口对象,第一个是ALPC 连接端口,由服务器进程创建,客户机可以连接到它(类似于网络套接字)。一旦客户端连接到服务器的ALPC连接端口,内核将创建两个新端口,称为ALPC 服务器通信端口和ALPC 客户端通信端口。
一旦建立了服务器和客户端通信端口,双方就可以使用ntdll.dll公开的函数NtAlpcSendWaitReceivePort
相互发送消息。这个函数的名字听起来像是同时做三件事—发送、等待和接收—而这正是它的真正含义。服务器和客户端使用这个单独的函数在它们的ALPC端口上等待消息、发送消息和接收消息。
在这个单独的函数中,你还可以指定你想要发送什么样的消息(有不同的类型,有不同的含义),以及你想要与你的消息一起发送哪些属性,这些我们都将在后面讨论。
到目前为止,这听起来相当简单:服务器打开一个端口,客户端连接到它,两者都接收到一个通信端口的句柄,并通过单个函数NtAlpcSendWaitReceivePort
发送消息……嗯,这很简单。
我们站在高处看,这一切都很容易理解的,但站得高虽然可以望得远却看不清,所以让我们怀着一颗谦卑的心从高处慢慢走下来,拉近距离去看清“细节”:
-
服务器进程使用选定的 ALPC 端口名称(例如“ *CSALPCPort ”)调用
NtAlpcCreatePort
,并可选地使用安全描述符来指定谁可以连接到它。然后内核创建一个ALPC端口对象,并将这个对象的句柄返回给服务器,这个端口被称为ALPC连接端口 -
服务器调用
NtAlpcSendWaitReceivePort
,将句柄传递给其先前创建的连接端口,以等待客户端连接 -
客户端调用
NtAlpcConnectPort
:-
服务器ALPC端口的名称(CSALPCPort)
-
(可选)发送给服务器的消息
-
(可选)服务器的SID,以确保客户机连接到预期的服务器
-
(可选)与客户端连接请求一起发送的消息属性
-
-
然后将此连接请求传递给服务器,服务器调用
NtAlpcAcceptConnectPort
来接受或拒绝客户端的连接请求。(没看错,虽然该函数叫作NtAlpcAccept…但这个函数也可以用来拒绝客户端连接。这个函数的最后一个参数是一个布尔值,它指定连接是被接受(如果设置为true)还是被拒绝(如果设置为false))。服务器可以选择:
-
(可选)向客户端返回一条消息,接受或拒绝连接请求
-
(可选)向消息添加消息属性
-
(可选)分配一个自定义结构,例如一个唯一的ID,附加到服务器的通信端口,以便识别客户端--如果服务器接受连接请求,服务器和客户端分别接收到一个通信端口的句柄
-
-
客户端和服务器现在可以通过
NtAlpcSendWaitReceivePort
相互发送和接收消息,其中:-
客户端监听新消息并将其发送到其通信端口
-
服务器监听新消息并将其发送到其连接端口
-
客户端和服务器都可以指定在监听新消息时要接收哪些消息属性
-
这里有一些奇怪?为什么服务器是在连接端口而不是通信端口上发送/接收数据呢?
因为服务器在其通信端口上监听和发送消息是LPC (ALPC的前身)的工作方式。但是,这种设计将迫使您在服务器接受的每个新客户端上监听越来越多的通信端口。假设一个服务器有100个客户端与它通信,那么服务器需要监听100个通信端口来获取客户机消息,这通常会导致创建100个线程,其中每个线程将与不同的客户端通信。这是非常低效的,一个更有效的解决方案是让一个线程在服务器的连接端口上监听(和发送),所有消息都被发送到这个连接端口。
这意味着:服务器接收客户端连接,接收到客户端通信端口的句柄,但仍然在调用NtAlpcSendWaitReceivePort
时使用服务器的连接端口句柄,以便从所有连接的客户端发送和接收消息。
这是否意味着服务器的通信端口过时了?也不是。
服务器的每个客户端通信端口由操作系统在内部使用,将由特定客户端发送的消息绑定到该客户端的特定通信端口。操作系统会将一个特殊的上下文结构绑定到每个客户端通信端口,用于标识客户端。这个特殊的上下文结构是PortContext,它在接受连接时分配给这个客户端。
这意味着:当服务器监听它的连接端口时,它接收来自所有客户端的消息。如果它想知道哪个客户端发送消息,服务器可以获取它分配给的端口上下文结构来判断。
我们可以得出结论,服务器的每个客户端通信端口对于操作系统仍然很重要,并且在 ALPC 通信结构中仍然具有其位置和作用。但是,这并不能回答为什么服务器实际上需要每个客户端通信端口的句柄的问题(因为可以从使用连接端口句柄接收的消息中提取客户端的PortContext )。
答案便是模拟。当服务器想要模拟客户端时,它需要将客户端的通信端口传递给NtAlpcImpersonateClientOfPort. 这样做的原因是执行模拟所需的安全上下文信息被绑定(如果客户端允许)到客户端的通信端口。将这些信息附加到连接端口是没有意义的,因为所有客户端都使用此连接端口,而每个客户端都有自己的每个服务器的唯一通信端口。因此:如果您想模拟您的客户端,您需要保留每个客户的通信端口句柄。
.......好了,OK。越说越远了,让我们回过头来。
回头看看上面的消息流,我们可以看出客户端和服务器正在使用各种函数调用来创建ALPC端口,然后通过单个函数NtAlpcSendWaitReceivePort
发送和接收消息。虽然这包含了大量关于消息流的信息,但重要的是要始终注意服务器和客户端没有直接的点对点连接,而是通过内核来转发所有消息,内核负责将消息放置在消息队列中,通知接收到的消息的每一方,以及验证消息和消息属性等其他事情
ALPC 消息传递细节
一条 ALPC 消息总是由一个所谓的PORT_HEADER或PORT_MESSAGE 组成,后面跟着你想要发送的实际消息,例如一些文本、二进制内容或任何其他内容。
发送消息代码示例:
使用以下两个结构定义 ALPC 消息:
typedef struct _ALPC_MESSAGE {PORT_MESSAGE PortHeader;BYTE PortMessage[100]; // 使用大小为100的字节数组来存储实际的消息 } ALPC_MESSAGE, * PALPC_MESSAGE;typedef struct _PORT_MESSAGE {union {struct {USHORT DataLength;USHORT TotalLength;} s1;ULONG Length;} u1;union {struct {USHORT Type;USHORT DataInfoOffset;} s2;ULONG ZeroInit;} u2;union {CLIENT_ID ClientId;double DoNotUseThisField;};ULONG MessageId;union {SIZE_T ClientViewSize;ULONG CallbackId;}; } PORT_MESSAGE, * PPORT_MESSAGE;
发送消息:
// 指定消息结构并清空它 ALPC_MESSAGE pmSend, pmReceived; RtlSecureZeroMemory(&pmSend, sizeof(pmSend)); RtlSecureZeroMemory(&pmReceived, sizeof(pmReceived)); // 获取指向消息数组的指针 LPVOID lpPortMessage = pmSend->PortMessage; LPCSTR lpMessage = "Hello World!"; int lMsgLen = strlen(lpMessage); // 将消息复制到消息字节数组中 memmove(lpPortMessage, messageContent, lMsgLen); // 指定消息的长度 pMessage->PortHeader.u1.s1.DataLength = lMsgLen; // 指定ALPC消息的总长度 pMessage->PortHeader.u1.s1.TotalLength = sizeof(PORT_MESSAGE) + lMsgLen; // 发送ALPC消息 NTSTATUS lSuccess = NtAlpcSendWaitReceivePort(hCommunicationPort, // 客户端通信端口句柄ALPC_MSGFLG_SYNC_REQUEST, // 消息标志:同步消息(发送和接收消息)(PPORT_MESSAGE)&pmSend, // ALPC消息NULL, // 发送消息属性(PPORT_MESSAGE)&pmReceived, // ALPC消息缓冲区接收消息&ulReceivedSize, // 接收消息的大小NULL, // 接收消息属性0 // Timeout参数,0表示不想超时 );
此代码段将发送一条“Hello World!”的 ALPC 消息到我们连接的服务器。我们将消息指定为带有ALPC_MSGFLG_SYNC_REQUEST
标志的同步消息,这意味着该调用将等待(阻塞),直到在客户端的通信端口上接收到消息。
当然,我们不必等到有新消息进来,而是将在那之前的时间用于其他任务(记住 ALPC 被设计为异步、快速和高效的)。为了实现这一点,ALPC提供了三种不同的消息类型:
-
同步请求:如上所述,同步消息阻塞,直到有新消息进入(所以在使用同步消息调用
NtAlpcSendWaitReceivePort
时必须指定一个接收ALPC消息缓冲区) -
异步请求:异步发送消息,不用等待或处理任何收到的消息
-
数据报请求:数据报请求类似于UDP包,它们不期待应答,因此在发送数据报请求时,内核不会阻塞等待接收到的消息
因此,基本上你可以选择发送一条期待回复的消息或不期待回复的消息,当你选择前者时,你可以进一步选择等待,直到回复到来,或者不等待,并在此期间利用宝贵的CPU时间做其他事情。与此同时。如果选择了最后一个选项而不是在NtAlpcSendWaitReceivePort
函数调用中等待(异步请求),那么将面临如何接收回复的问题?
同样也有3个选择:
-
你可以使用ALPC完成列表,在这种情况下,内核不会通知你(作为接收方)收到了新数据,而是简单地将数据复制到进程内存中。由你(作为接收方)来意识到这个新数据的存在。例如,这可以通过使用在你和ALPC服务器之间共享的通知事件来实现,一旦服务器发出事件信号,你就知道新数据已经到达
-
你可以使用 I/O 完成端口
-
你可以接收一个内核回调来获取回复 - 但仅当你的进程位于内核领域时才能这样做
由于你可以选择不直接接收消息,因此不太可能有多个消息传入并等待获取。为了处理不同状态下的多个消息,ALPC使用队列来处理和管理堆积在服务器上的大量消息。有五个不同的消息队列:
-
主队列:消息已发送,客户端正在处理它。
-
待处理队列:消息已发送,调用者正在等待回复,但尚未发送回复。
-
大消息队列:消息已发送,但调用者的缓冲区太小而无法接收。调用者有另一个机会分配更大的缓冲区并再次请求消息。
-
已取消队列:已发送到端口但此后已被取消的消息。
-
直接队列:发送时附带事件的消息。
最后,关于 ALPC 的消息传递细节,还有最后一件事还没有详细说明,那就是消息如何从客户端传输到服务器的问题。前面我们已经提到可以发送什么样的消息,消息的结构是什么样的,存在什么机制来同步和停止消息,但到目前为止还没有详细说明消息是如何从一个进程到另一个进程的。你有两个选择:
-
双缓冲机制:这种方法在发送方和接收方的(虚拟)内存空间中分配一个消息缓冲区,然后将消息从发送方的(虚拟)内存复制到内核的(虚拟)内存中,再从内核的(虚拟)内存复制到接收方的(虚拟)内存中。它被称为双缓冲区,因为包含消息的缓冲区被分配和复制两次(发送者 -> 内核 & 内核 -> 接收者)。
-
内存映射机制:除了分配缓冲区来存储消息,客户端和服务器也可以分配一个共享内存段,它可以被双方访问,映射该段的一个视图,复制消息到映射视图,并最终将该视图作为消息属性发送给接收者。接收方可以通过消息属性提取一个指向发送方使用的同一视图的指针,并从该视图读取数据。
使用'内存映射机制'的主要原因是为了发送较大的消息,因为通过' 双缓冲机制'发送的消息长度有一个硬编码的大小限制,即65535字节。如果在消息缓冲区中超过此限制,则会引发错误。函数AlpcMaxAllowedMessageLength()
可用于获取最大消息缓冲区大小,这在未来的Windows版本中可能会改变。
ALPC 消息属性
在发送和接收消息时,通过NtAlpcSendWaitReceivePort
,客户端和服务器都可以指定一组他们想要发送和/或接收的属性。想要发送的这些属性集和想要接收的属性集在NtAlpcSendWaitReceivePort
两个额外参数中指定
NTSTATUS NTAPI NtAlpcSendWaitReceivePort( _In_ HANDLE PortHandle, _In_ ULONG Flags, _In_reads_bytes_opt_(SendMessage->u1.s1.TotalLength) PPORT_MESSAGE SendMessage, _Inout_opt_ PALPC_MESSAGE_ATTRIBUTES SendMessageAttributes, //发送消息属性 _Out_writes_bytes_to_opt_ *,*BufferLength PPORT_MESSAGE ReceiveMessage, _Inout_opt_ PSIZE_T BufferLength, _Inout_opt_ PALPC_MESSAGE_ATTRIBUTES ReceiveMessageAttributes, // 接收消息属性 _In_opt_ PLARGE_INTEGER Timeout )
可以发送或接收以下消息属性:
安全属性:安全属性包含安全上下文信息,例如可以用来模拟消息的发送者。此信息由内核控制和验证。该属性的结构如下:
typedef struct _ALPC_SECURITY_ATTR {ULONG Flags;PSECURITY_QUALITY_OF_SERVICE pQOS;HANDLE ContextHandle; } ALPC_SECURITY_ATTR, * PALPC_SECURITY_ATTR;
视图属性:此属性用于传递指向共享内存部分的指针,接收方可以使用该指针从该内存部分读取数据。该属性的结构如下:
typedef struct _ALPC_DATA_VIEW_ATTR {ULONG Flags;HANDLE SectionHandle;PVOID ViewBase;SIZE_T ViewSize; } ALPC_DATA_VIEW_ATTR, * PALPC_DATA_VIEW_ATTR;
上下文属性:上下文属性存储指向已分配给特定客户端(通信端口)或特定消息的用户指定上下文结构的指针。上下文结构可以是任何任意结构,例如唯一编号,用于标识客户端。服务器可以提取和引用端口结构,以唯一地标识发送消息的客户端。此消息属性总是可以由消息的接收方提取,发送方不必指定此属性,也不能阻止接收方访问此属性。该属性的结构如下:
typedef struct _ALPC_CONTEXT_ATTR {PVOID PortContext;PVOID MessageContext;ULONG Sequence;ULONG MessageId;ULONG CallbackId; } ALPC_CONTEXT_ATTR, * PALPC_CONTEXT_ATTR;
句柄属性:句柄属性可用于将句柄传递给特定对象,例如文件。接收者可以使用这个句柄来引用对象,例如在一个ReadFile调用中。内核将验证传递的句柄是否有效,否则将引发错误。该属性的结构如下:
typedef struct _ALPC_MESSAGE_HANDLE_INFORMATION {ULONG Index;ULONG Flags;ULONG Handle;ULONG ObjectType;ACCESS_MASK GrantedAccess; } ALPC_MESSAGE_HANDLE_INFORMATION, * PALPC_MESSAGE_HANDLE_INFORMATION;
令牌属性:令牌属性可用于传递有关发送方的令牌信息。该属性的结构如下:
typedef struct _ALPC_TOKEN_ATTR {ULONGLONG TokenId;ULONGLONG AuthenticationId;ULONGLONG ModifiedId; } ALPC_TOKEN_ATTR, * PALPC_TOKEN_ATTR;
直接属性:直接属性可用于将创建的事件与消息关联起来。接收方可以检索发送方创建的事件并发出信号,让发送方知道已接收到发送消息(这对数据报请求特别有用)。该属性的结构如下:
typedef struct _ALPC_DIRECT_ATTR {HANDLE Event; } ALPC_DIRECT_ATTR, * PALPC_DIRECT_ATTR;
Attack
确定目标
关于如何识别,通常有三种途径:
-
识别ALPC端口对象,然后通过端口找到所属的进程
-
检查进程是否使用了ALPC
-
使用Windows事件跟踪(ETW)来列出ALPC事件
查找 ALPC 端口对象
识别ALPC端口对象的最直接的方法,那就是启动WinObj并通过“类型”列找到ALPC对象。
WinObj不能给我们更多的细节,所以我们转向WinDbg内核调试器来检查这个ALPC端口对象。如果在看文章跟着做的您不是学员,这是你第一次使用 WinDbg的话(或者你像我一样容易忘记某些命令的含义),你可以随时使用 WinDbg 的帮助菜单kd:> .hh
在上述命令中,我们使用 Windbg 的!object命令在对象管理器中查询指定路径中的命名对象。这已经隐含地告诉我们只能使用WinObj来查找ALPC 连接端口的ALPC 服务器进程。
说到服务器进程:如上所示,你可以使用WinDbg没有记录的!alpc命令来显示我们刚刚标识的alpc端口的信息。输出包括非常多的有用信息,例如端口的所属服务器进程,在本例中为svchost.exe。
alpc用法:
现在我们知道了ALPC Port对象的地址,我们可以再次使用! ALPC命令来显示这个ALPC连接端口的活动连接:
此外,还可以愉快的通过googleprojectzero提供的脚本来搜索 ALPC 端口对象
查找使用ALPC的进程
与之前方法类似,可以使用dumpbin.exe实用程序列出可执行文件的导入函数,并在其中搜索特定于ALPC的函数调用。
以下两个 PowerShell 可用于查找创建或连接到 ALPC 端口的.exe和.dll文件:
## Get ALPC Server processes (those that create an ALPC port) Get-ChildItem -Path "C:\Windows\System32\" -Include ('*.exe', '*.dll') -Recurse -ErrorAction SilentlyContinue | % { $out=$(C:\"Program Files (x86)"\"Microsoft Visual Studio 14.0"\VC\bin\dumpbin.exe /IMPORTS:ntdll.dll $_.VersionInfo.FileName); If($out -like "*NtAlpcCreatePort*"){ Write-Host "[+] Executable creating ALPC Port: $($_.VersionInfo.FileName)"; Write-Output "[+] $($_.VersionInfo.FileName)`n`n $($out|%{"$_`n"})" | Out-File -FilePath NtAlpcCreatePort.txt -Append } }## Get ALPC client processes (those that connect to an ALPC port) Get-ChildItem -Path "C:\Windows\System32\" -Include ('*.exe', '*.dll') -Recurse -ErrorAction SilentlyContinue | % { $out=$(C:\"Program Files (x86)"\"Microsoft Visual Studio 14.0"\VC\bin\dumpbin.exe /IMPORTS:ntdll.dll $_.VersionInfo.FileName); If($out -like "*NtAlpcConnectPor*"){ Write-Host "[+] Executable connecting to ALPC Port: $($_.VersionInfo.FileName)"; Write-Output "[+] $($_.VersionInfo.FileName)`n`n $($out|%{"$_`n"})" | Out-File -FilePath NtAlpcConnectPort.txt -Append } }
可以附加到正在运行的进程,查询该进程的打开句柄并过滤指向 ALPC 端口的句柄。
#windbg !handle 0 2 0 ALPC Port
使用processhacker也可以
使用 Windows 事件跟踪
虽然ALPC没有文档记录,但有一些ALPC事件被Windows公开了,可以通过Windows事件跟踪(ETW)捕获这些事件。帮助处理ALPC事件的好工具是由zodiacon开发的ProcMonXv2
模拟
这个不用多讲了,和之前一样意思
未释放的消息对象
如ALPC 消息属性部分所述,客户端或服务器可以随消息一起发送多个消息属性。其中一个属性是ALPC_DATA_VIEW_ATTR,可用于向通信的另一方发送关于映射视图的信息。这可以用于在共享视图中存储较大的消息或数据,并将该共享视图的句柄发送给另一方,而不是使用双缓冲区消息传递机制将数据从一个内存空间复制到另一个内存空间。
这里有趣的一点是,当在ALPC_DATA_VIEW_ATTR属性中被引用时,共享视图(或称为节)被映射到接收方的进程空间。然后接收者就可以对这个部分做一些事情,但是最后,消息的接收方必须确保映射视图从它自己的内存空间中释放出来,这需要一定数量的步骤,而这些步骤有可能无法正确地执行。那么如果接收者未能释放一个映射视图,更或者是它从一开始就没有期望接收到一个视图,那么发送者可以发送越来越多的带有任意数据的视图,以用任意数据的视图填充接收者的内存空间,这就变成了堆喷射攻击。
最后
ALPC 未记录且相当复杂,但作为一种好处是:ALPC 内部的漏洞利用可能变得非常强大,因为 ALPC 在 Windows 操作系统中无处不在,所有内置的高特权进程都使用 ALPC。我们在未来还会在很多地方接触到它
参考:
LPC、RPC 和ALPC演讲https://youtu.be/UNpL5csYC1E
深入解析Windows操作系统卷2https://item.jd.com/13094235.html
processhackerhttps://processhacker.sourceforge.io/doc/ntlpcapi_8h.html
大家伙,如果想学习更多的知识和联系我们,可以看我们的论坛:
哔哩哔哩有免杀基础课程,搜索账号:老鑫安全培训,老鑫安全二进制
相关文章:
Windows IPC
进程间通信 (IPC,Inter-Process Communication) 进程间通信 (IPC) 是一种在进程之间建立连接的机制,在两台计算机或一台多任务计算机上运行,以允许数据在这些进程之间流动。进程间通信 (IPC) 机制通常用于客户端/服务器环境,并在…...
BMS存储模块的设计
目的 电池管理系统中存在着数据本地存储的要求,保证控制器重新上电后能够根据存储器中的一些参数恢复控制状态,和信息的下电存储1.继电器故障信息的存储。2. 系统性故障的存储。3.SOC、SOH相关信息的存储。4.均衡参数的存储。5.系统时间信息。6.出厂信息…...
2024-12-29-sklearn学习(25)无监督学习-神经网络模型(无监督) 烟笼寒水月笼沙,夜泊秦淮近酒家。
文章目录 sklearn学习(25) 无监督学习-神经网络模型(无监督)25.1 限制波尔兹曼机25.1.1 图形模型和参数化25.1.2 伯努利限制玻尔兹曼机25.1.3 随机最大似然学习 sklearn学习(25) 无监督学习-神经网络模型(无监督) 文章参考网站&a…...
【动态规划篇】穿越算法迷雾:约瑟夫环问题的奇幻密码
欢迎拜访:羑悻的小杀马特.-CSDN博客 本篇主题:带你众人皆知的约瑟夫环问题 制作日期:2024.12.29 隶属专栏:C/C题海汇总 目录 引言: 一约瑟夫环问题介绍: 11问题介绍: 1.2起源与历史背景&…...
【Elasticsearch】DSL查询文档
目录 1.DSL查询文档 1.1.DSL查询分类 1.2.全文检索查询 1.2.1.使用场景 1.2.2.基本语法 1.2.3.示例 1.2.4.总结 1.3.精准查询 1.3.1.term查询 1.3.2.range查询 1.3.3.总结 1.4.地理坐标查询 1.4.1.矩形范围查询 1.4.2.附近查询 1.5.复合查询 1.5.1.相关性算分 …...
MySQL第三弹----函数
笔上得来终觉浅,绝知此事要躬行 🔥 个人主页:星云爱编程 🔥 所属专栏:MySQL 🌷追光的人,终会万丈光芒 🎉欢迎大家点赞👍评论📝收藏⭐文章 一、合计/统计函数 1.1count…...
路由器刷机TP-Link tp-link-WDR5660 路由器升级宽带速度
何在路由器上设置代理服务器? 如何在路由器上设置代理服务器? 让所有连接到该路由器的设备都能够享受代理服务器的好处是一个不错的选择,特别是当需要访问特定的网站或加速网络连接的时候。下面是一些您可以跟随的步骤,使用路由器…...
Qml 中实现水印工具
【写在前面】 在 Qt 的 Quick 模块中,QQuickPaintedItem 是一个非常有用的类,它允许我们在 Qml 中自定义绘制逻辑。 我们可以通过这种方式实现水印工具,包括在文本、图片或整个窗口上添加水印。 本文将介绍如何在 Qml 中实现一个简单但功能…...
2024年数字政府服务能力优秀创新案例汇编(附下载)
12月19日,由中国电子信息产业发展研究院指导、中国软件评测中心主办的“2024数字政府评估大会”在北京召开,大会主题是:为公众带来更好服务体验。 会上,中国软件评测中心副主任吴志刚发布了2024年数字政府服务能力评估结果&#…...
数据链路层知识要点
这里写目录标题 数据链路层的功能1.封装成帧2.差错控制2.1循环冗余校验(CRC)2.2奇偶校验法 3.可靠传输3.1停止等待协议(SW)3.2后退N帧协议(GBN)3.3选择重传协议(SR) 4.使用广播信道的数据链路层5.以太网(局域网)5.1以太网与网卡5.2以太网的MAC地址 6.VLA…...
Linux实验报告6-用户管理
目录 一:实验目的 二:实验内容 (1)查看 Linux 系统的相关文件,回答以下问题 ①root 用户的 UID为多少?他的主目录在哪里? ②请举出一个普通用户,指出他的主目录及其所使用的 shell 是什么? (2)新建用户abc1(abc代表你的姓名拼音字母,下同),为其…...
微信小程序打印生产环境日志
微信小程序打印生产环境日志 新建一个log.js文件,写入以下代码: let log wx.getRealtimeLogManager ? wx.getRealtimeLogManager() : nullmodule.exports {debug() {if (!log) returnlog.debug.apply(log, arguments)},info() {if (!log) returnlog.i…...
Edge如何获得纯净的启动界面
启动Edge会出现快速链接,推广链接,网站导航,显示小组件,显示信息提要,背景 ●复杂页面 ●精简页面 点击页面设置按钮 关闭快速链接 关闭网站导航 关闭小组件 关闭信息提要 关闭背景 关闭天气提示 精简页面看起来十分舒…...
探索开源项目 kernel:技术的基石与无限可能
在开源的广袤世界中,有一颗璀璨的明星——kernel(https://gitee.com/openeuler/kernel),它宛如一座技术的宝藏,蕴含着无数的智慧与创新,为众多开发者所瞩目和敬仰。 一、初窥 kernel 项目 当我第一次接触…...
使用PHP函数 “setcookie“ 设置cookie
在网站开发中,cookie是一种非常常用的技术,它用于在用户的浏览器中存储少量的数据,以便在不同页面之间传递信息。PHP提供了一个名为 "setcookie" 的函数,用于设置cookie的值和属性。在本文中,我们将学习如何…...
LUA基础语法
目录 变量篇 算数运算符 条件分支语句与循环语句 函数 表 Table 全局变量与本地变量 协程 元表 面向对象(封装,继承,多态) 常用自带库 垃圾回收 变量篇 print("hello") print("lua") --注释 --[[…...
链路聚合
链路聚合 目的:备份链路以及提高链路带宽。 链路聚合技术(Eth-Trunk):将多个物理接口捆绑成一个逻辑接口,将N条物理链路逻辑上聚合为一条逻辑链路。 正常情况下,想要配置链路聚合 1、A设备通过多条链路连接…...
OpenCV-Python实战(4)——图像处理基础知识
一、坐标 在 OpenCV 中图像左上角坐标为(0,0),竖直向下为 Y(height) ;水平向右为 X(width)。 二、生成图像 2.1 灰度图像 img np.zeros((h,w), dtype np.uint8) i…...
爬虫案例-爬取网页图片
爬虫案例-爬取网页图片 1、安装依赖库2、爬取图片的代码3、效果图 1、安装依赖库 #以下是安装http请求的第三方库 pip install requests urllib3 #以下是安装处理图片的第三方库 pip install image pillow #以下是安装python解析html的第三方库 pip install beautifulsoup4 …...
KAN网络最新优化改进——基于小波变换的KAN网络
KAN网络概念 KAN网络(Kolmogorov-Arnold Networks)是一种革命性的神经网络架构,源于Kolmogorov-Arnold表示定理。 该定理表明,多变量连续函数可通过有限数量的单变量连续函数的嵌套加法表示 。KAN的核心创新在于将传统神经网络中的固定激活函数替换为可学习的单变量函数,…...
【潜意识Java】深入理解Java中的方法重写,理解重写的意义,知道其使用场景,以及重写的访问权限限制等的完整笔记详细总结。
目录 一、方法重写是啥玩意儿 (一)定义和概念 (二)为啥要方法重写 二、方法重写的规则 (一)方法签名必须相同 (二)返回类型的要求 (三)访问权限的限制…...
Android Thread优先级和调度算法
Thread优先级设置方式: java: Process.setThreadPriority: android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_LOWEST、Process.THREAD_PRIORITY_URGENT_AUDIO、-20) c: #include <sched.h> setpriority( https://blo…...
OpenCV-Python实战(6)——图相运算
一、加法运算 1.1 cv2.add() res cv2.add(img1,img2,dstNone,maskNone,dtypeNone) img1、img2:要 add 的图像对象。(shape必须相同) mask:图像掩膜。灰度图(维度为2)。 dtype:图像数据类型…...
2、C#基于.net framework的应用开发实战编程 - 设计(二、四) - 编程手把手系列文章...
二、设计; 二.四、制定设计规范; 编码规范在软件编程里起到了非常重要的作用,主要是让代码更加的规范化,更加的简洁,更加的漂亮,更加的能够面向对象显示。 以前那个系列就有发布C#的编码规范的文…...
DVWA靶场Brute Force (暴力破解) 漏洞low(低),medium(中等),high(高),impossible(不可能的)所有级别通关教程
目录 暴力破解low方法1方法2 mediumhighimpossible 暴力破解 暴力破解是一种尝试通过穷尽所有可能的选项来获取密码、密钥或其他安全凭证的攻击方法。它是一种简单但通常无效率的破解技术,适用于密码强度较弱的环境或当攻击者没有其他信息可供利用时。暴力破解的基…...
sql字段值转字段
表alertlabel中记录变字段 如何用alertlabel表得到下面数据 实现的sql语句 select a.AlertID, (select Value from alertlabel where AlertIDa.AlertID and Labelhost) as host, (select Value from alertlabel where AlertIDa.AlertID and Labeljob) as job from (select …...
lua和C API库一些记录
相关头文件解释 lua.h:声明lua提供的基础函数,所有内容都有个前缀lua_; luaxlib.h:声明辅助库提供的函数,所有内容都有个前缀luaL_; lualib.h:声明了打开标准库的函数; 辅助库对…...
游戏引擎学习第68天
关于碰撞和交互的进展回顾 在进行引擎架构设计时,我们决定开始探讨如何处理游戏中的碰撞问题。举个例子,比如一把被投掷的剑碰到了敌人。我们希望能够响应这些事件,开始构建游戏中的互动机制。这些互动是游戏设计的核心部分,游戏…...
LeetCode430周赛T3
题目描述 给定一个只包含正整数的数组 nums,我们需要找到其中的特殊子序列。特殊子序列是一个长度为4的子序列,用下标 (p, q, r, s) 表示,它们满足以下条件: 索引顺序:p < q < r < s,且相邻坐标…...
网络:常用的以太网PHY芯片
常用的以太网PHY芯片(物理层芯片)主要负责将数字信号转换为适合在物理介质上传输的模拟信号。它们是网络设备(如交换机、路由器、网卡等)中的关键组件,通常工作在OSI模型中的物理层和数据链路层之间。 以下是一些常见…...
前端项目 node_modules依赖报错解决记录
1.首先尝试解决思路 npm报错就切换yarn , yarn报错就先切换npm删除 node_modules 跟 package-lock.json文件重新下载依 2. 报错信息: Module build failed: Error: Missing binding D:\vue-element-admin\node_modules\node-sass\vendor\win32-x64-8…...
小猫可以吃面包吗?
在宠物饲养日益普及的当下,小猫的饮食健康成为众多铲屎官关注的焦点。其中,小猫是否可以吃面包这一问题引发了不少讨论。 从面包的成分来看,其主要原料是面粉、水、酵母和盐,部分还会添加糖、油脂、鸡蛋、牛奶等。面粉富含碳水化…...
ACPI PM Timer
ACPI PM Timer 概述: ACPI PM Timer是一个非常简单的计时器,它以 3.579545 MHz 运行,在计数器溢出时生成系统控制中断(SCI)。它精度较低,建议使用其他定时器,如HPET或APIC定时器。 检测ACPI P…...
算法学习(19)—— 队列与 BFS
关于bfs bfs又称宽搜,全称是“宽度优先遍历”,然后就是关于bfs的三个说法:“宽度优先搜索”,“宽度优先遍历”,“层序遍历”,这三个都是同一个东西,前面我们介绍了大量的深度优先遍历的题目已经…...
python|利用ffmpeg按顺序合并指定目录内的ts文件
前言: 有的时候我们利用爬虫爬取到的ts文件很多,但ts文件只是视频片段,并且这些视频片段是需要按照一定的顺序合并的,通常ts文件合并输出格式为mp4格式 因此,本文介绍利用python,调用ffmpeg来批量的按自己…...
腾讯音乐:说说Redis脑裂问题?
Redis 脑裂问题是指,在 Redis 哨兵模式或集群模式中,由于网络原因,导致主节点(Master)与哨兵(Sentinel)和从节点(Slave)的通讯中断,此时哨兵就会误以为主节点…...
jmeter并发用户逐步递增压测找性能拐点
jmeter并发用户逐步递增压测找性能拐点 目的: 使用逐层递增的并发压力进行测试,找到单功能的性能拐点(一般需要包含四组测试结果,拐点前一组,拐点一组,拐点后两组),统计响应时间、…...
跟着问题学3.2——Fast R-CNN详解及代码实战
R-CNN的不足 2014年,Ross Girshick提出RCNN,成为目标检测领域的开山之作。一年后,借鉴空间金字塔池化思想,Ross Girshick推出设计更为巧妙的Fast RCNN(https://github.com/rbgirshick/fast-rcnn)ÿ…...
【yolov5】实现FPS游戏人物检测,并定位到矩形框上中部分,实现自瞄
介绍 本人机器学习小白,通过语言大模型百度进行搜索,磕磕绊绊的实现了初步效果,能有一些锁头效果,但识别速度不是非常快,且没有做敌友区分,效果不是非常的理想,但在4399小游戏中爽一下还是可以…...
软考高级:磁盘阵列(RAID)
** 概念讲解 ** 磁盘阵列是由多个磁盘组合成的一个大容量存储设备。它主要有以下几个作用: 提高存储容量:通过将多个磁盘组合在一起,可以获得比单个磁盘更大的存储容量。比如,一个磁盘的容量是 1TB,使用四个磁盘组成…...
梳理你的思路(从OOP到架构设计)_介绍Android的Java层应用框架05
1、认识ContentProvider...
torch.nn.LSTM介绍
torch.nn.LSTM 是 PyTorch 提供的一个高级封装,用于构建长短时记忆网络(LSTM)。相比手动实现,torch.nn.LSTM 更高效且支持批量处理、双向 LSTM、多层 LSTM 等功能,适合大多数实际应用。 LSTM基本原理 门控机制(Gating Mechanism)是深度学习中常见的一种设计,用于控制信…...
React 组件的通信方式
在 React 应用开发中,组件之间的通信是构建复杂用户界面和交互逻辑的关键。正确地实现组件通信能够让我们的应用更加灵活和易于维护。以下是几种常见的 React组件通信方式。 一、父子组件通信 1. 通过 props 传递数据(父组件向子组件传递数据࿰…...
关于 覆铜与导线之间间距较小需要增加间距 的解决方法
若该文为原创文章,转载请注明原文出处 本文章博客地址:https://hpzwl.blog.csdn.net/article/details/144776995 长沙红胖子Qt(长沙创微智科)博文大全:开发技术集合(包含Qt实用技术、树莓派、三维、OpenCV…...
使用seata实现分布式事务管理
配置 版本说明:springCloud Alibaba组件版本关系 我用的是spring cloud Alibaba 2.2.1.RELEASE 、springboot 2.2.1.RELEASE、nacos 2.0.1、seata1.2.0,jdk1.8 seata 主要用于在分布式系统中对数据库进行事务回滚,保证全局事务的一致性。 seata的使用…...
【机器学习】深度学习(DNN)
文章目录 1. 神经网络结构2. 训练步骤3. 反向传播4. 为什么深,而不是宽(模块化)5. 初始化参数能否全为0? 1. 神经网络结构 输入层隐藏层:用于特征转换输出层:用于分类技巧:将网络中的参数写成矩…...
C++ 设计模式:门面模式(Facade Pattern)
链接:C 设计模式 链接:C 设计模式 - 代理模式 链接:C 设计模式 - 中介者 链接:C 设计模式 - 适配器 门面模式(Facade Pattern)是一种结构型设计模式,它为子系统中的一组接口提供一个一致&#…...
自动化测试之Pytest框架(万字详解)
Pytest测试框架 一、前言二、安装2.1 命令行安装2.2 验证安装 三、pytest设计测试用例注意点3.1 命名规范3.2 断言清晰3.3 fixture3.4 参数化设置3.5 测试隔离3.6 异常处理3.7 跳过或者预期失败3.8 mocking3.9 标记测试 四、以案例初入pytest4.1 第一个pytest测试4.2 多个测试分…...
YOLOv10-1.1部分代码阅读笔记-conv.py
conv.py ultralytics\nn\modules\conv.py 目录 conv.py 1.所需的库和模块 2.def autopad(k, pNone, d1): 3.class Conv(nn.Module): 4.class Conv2(Conv): 5.class LightConv(nn.Module): 6.class DWConv(Conv): 7.class DWConvTranspose2d(nn.ConvTranspose2d)…...
大模型-Ollama使用相关的笔记
大模型-Ollama使用相关的笔记 解决Ollama外网访问问题(配置ollama跨域访问)Postman请求样例 解决Ollama外网访问问题(配置ollama跨域访问) 安装Ollama完毕后, /etc/systemd/system/ollama.service进行如下修改&#…...