红蓝对抗course1-Windows重要系统组件
用户进程,有6个类型:windows32位,windows64位,windows3.1 16位,ms-dos 16位,posix32位或者OS/2 32位
服务进程,服务进程承载了windows服务,如任务计划 和 打印机服务。通常来说,服务需要能在用户不登录的情况下运行
系统进程,用于完成操作系统的各种功能
环境子系统服务进程,实现了操作系统环境的支持部分,这里的环境指操作系统展示给用户或程序员的个性化部分。windows nt最初有3个环境子系统,windows、posix、os/2,windows xp以后基本的产品中只有windows子系统随产品发布。
windows执行体:包含基本的操作系统服务,如内存管理、进程线程管理、安全性、I/O、网络和跨进程通信。
windows内核:由一组低层次的操作系统功能构成,如线程调度,中断和异常分发,以及多处理器同步。他也提供了一组例程和基本对象,执行体的其余部分利用这些例程和对象实现更高层次的功能。
设备驱动程序:既包括硬件设备驱动程序,也包括文件系统和网络驱动程序。其中,硬件设备驱动程序将用户的I/O函数调用转换为特定的硬件设备I/O请求。
硬件抽象层:是一层特殊的代码,把内核、设备驱动程序、windows执行体的其余部分,跟与平台相关的硬件差异隔离开来。
窗口和图形系统:实现了图形用户界面函数,比如对窗口的处理,用户界面控件,以及绘制等。
https://zh.wikipedia.org/wiki/Windows组件列表
图中上半部分描述的是用户空间,下半部分描述的是内核空间
在用户空间中,除了用户安装的或系统预装的普通应用程序而外,还运行着操作系统的一些进程,一般将它们称为常规进程,常规进程的角色与内核空间的执行体类似,负责某一方面的职能,维护系统的秩序
常见的:
1.会话管理器进程(SMSS.EXE),它是内核启动的第一个用户模式进程,是在系统启动后期由执行体的初始化函数创建的。运行后会加载和初始化win32子系统的内核模块Win32K.sys,创建win32子系统服务器进程(CSRSS.EXE),以及登录进程(WinLogon.EXE)
2.Windows子系统服务器进程( CSRSS.EXE), 负责维护Windows子系统的“日常事务”,为子系统中的各个进程提供服务。例如登记进程和线程,管理控制台窗口,管理DOS程序虚拟机( VDM )进程等。CSRSS是Client/Server Runtime Server Subsystem的缩写,即客户端/服务器运行时子系统。
3.登录进程( WinLogon.EXE),负责用户登录和安全有关的事务。它启动后,会创建LSASS进程和系统服务管理进程( Services.EXE )。
4.本地安全和认证进程( LSASS.EXE),负责用户身份验证,LSASS 是Local Security Authority Subsystem Service的缩写。
5.服务管理进程( SERVICES.EXE),负责启动和管理系统服务程序。系统服务程序是按照NT系统服务规范编写的EXE程序,通常没有用户界面,只在后台运行。例如SpoolSv.exe 是打印机脱机服务,WmiPrvSE.exe 是WMI提供器管理服务,SvcHost.exe是一-个通用的服务宿主程序。
6.外壳( Shell)程序,默认为Explorer.exe, 负责显示“开始”菜单、任务栏和桌面图标等。
空闲进程(idle process) 系统进程(System)
普通的windows进程都是通过CreateProcess或类似的API并指定一个PE文件而创建,但是这2个进程不是,也就是说,在windows目录下并不存在它们对应的磁盘PE文件,而是系统启动的时候内核造出来的。
普通的进程都有用户空间跟内核空间两个部分,但是这2个进程都只在内核空间运行,没有用户空间,并且具有固定的进程ID,空闲进程的ID总是0,系统进程的ID总是4
空闲进程
空闲进程是空闲线程的载体,用来负责占用CPU的闲置时间,因为CPU一上电工作,就要取指令执行,没什么事做也要给它几条指令,组成一个循环让它在那里空转。在今天的OS中,都会设计空闲线程,当CPU没有其他线程执行时,让它执行空闲线程。
因为系统中每个CPU都需要空闲线程,所以线程的个数 = 系统中逻辑处理器的数量
在执行体的阶段0初始化时,进程管理器的初始化函数便会创建空闲进程和第一个空闲线程。也就是说,空闲进程是系统启动时创建的第一个进程,而且它一直存在,所以可以通过观察它的运行时间来间接推测系统的运行时间,方法是把空闲时间的CPU时间除以CPU个数,向上取整,因为CPU还用一定时间执行其他进程。
!prcb 观察peb跟usertime
系统进程
系统进程是操作系统内核和所有系统线程的宿主,其作用是为操作系统提供独立的进程空间和进程对象。简单来说就是专门用来运行内核模式下的线程,执行内核代码,这些代码可能来自Ntoskrnl.exe或其他加载的驱动程序(驱动程序默认是在system中,但是可以在任何进程中创建线程) 系统进程里的系统线程都是由PsCreateSystemThread或IoCreateSystemThread函数创建
!process 4 1
NTDLL.DLL
用户空间中运行着不同来源的各种代码,是不可信赖的,让内核直接与五花八门的用户代码交互的风险很大,设计也会复杂。但是内核存在的意义就是要为用户代码服务,所以NTDLL就是沟通用户和内核空间的桥梁,相当于是内核在用户空间中的“代理”
这里面有些API是公开的,有些是未公开的,未公开的使用符号+windbg调试查看即可
映像文件加载器
编译好的程序是存放在磁盘等外部存储器上的,运行时需要先加载到内存中,负责执行这个加载任务的操作系统部件一般称为映像加载器(image loader)。在linux系统中,担任这个角色的是ld.so;在windows中,它是NTDLL.DLL中的一部分,一般简称为LDR
NTDLL中函数以Ldr或_Ldr开头的都是属于LDR,又可分为两类,其中第4个字符为小写p的代表内部函数,大写形式的代表接口函数
环境子系统
对于大多数计算机用户来说,购买或者安装操作系统的目的是运行应用程序。这意味着,能够支持的应用程序类型越多,市场机会便越多。为了能够在Windows操作系统上运行多种类型的应用软件,Windows设计者在定义Windows架构时便设计了环境子系统( environmet Subystem )的概念。不同类型的应用程序运行在不同的环境子系统中。这样,一个Windows系统中可以同时有多个不同的环境子系统,因此可以同时运行不同类型的应用程序。
windows nt最初有3个环境子系统,win2000支持windows子系统、posix子系统(unix)、os/2子系统(ibm),windows xp以后基本的产品中只有windows子系统,win10引入了linux环境子系统,支持Ubuntu、Fedora、Kali Linux等多种发行版本 wsl
虽然windows在设计时即可支持多种独立的环境子系统,但实际上让每个子系统实现所有的代码以处理窗口和显示I/O会产生大量重复的系统函数,最终会对系统的体积和性能产生不利影响。为了避免多个子系统都重复实现类似的功能,Windows的设计者决定将基本函数放在windows子系统中,让Windows 子系统有着与其他子系统不同的地位,Windows子系统可以独立存在而且是系统中不可缺少的部分,其他子系统以Windows子系统为基础,必须依赖于Windows子系统,而且是按需要运行的
在这种设计决策的影响下,windows子系统成了任何windows系统的必需组件,即使在不提供交互式用户登录的服务器系统上也是如此
windows子系统包含下列重要组件:
1.CSRSS.EXE windows子系统服务进程的主程序
2.对于每个会话,环境子系统进程将加载4个DLL(Basesrv.dll、Winsrv.dll、Sxssrv.dll、Csrsrv.dll)用来支持一下操作
进程和线程的创建删除等多种管理任务
windows应用程序的关闭(ExitWindowsEx API)
包含有关注册表位置映射的.ini文件,用来维持兼容
以windows消息的形式将某些内核通知消息发送给windows应用程序(WM_DEVICECHANGE)
为16位DOS虚拟机(VDM)进程提供部分支持
.............
3.一个内核模式的设备驱动程序(win32k.sys),windows子系统的内核模式部分(GUI)
4.核心子系统DLL
当应用程序调用子系统 DLL 中的函数时,会发生下列 3 种情况之一。
1.函数完全在子系统 DLL 内部以用户模式实现。换句话说,不会向环境子系统进程发送任何消息,也不会调用任何 Windows 执行体系统服务。函数将在用户模式下执行,并将执行结果返回给调用方,如 GetCurrentProcess(该函数总会返回“-1”,在所有与进程有关的函数中,定义这样的一个值是为了代表当前进程)和
GetCurrentProcessId 函数。(运行中的进程的 ID 不会改变,因此该 ID 可以从某个缓存的位置获取,以避免对内核进行调用)
2.函数需要对 Windows 执行体进行一个或多个调用。例如,Windows 的 ReadFile和 WriteFile 函数就分别需要调用底层内部的 Windows I/O 系统服务NtReadFile 和 NtWriteFile。
3.函数要在环境子系统进程中执行一些工作。(运行于用户模式的环境子系统进程负责维持在其控制下运行的客户端应用程序的状态。)这种情况下,需要通过 ALPC以消息的形式向环境子系统发出客户端/服务器请求,借此让子系统执行所需操作。随后子系统 DLL 将等待响应,并将响应返回给调用方。
原生进程
在NT系统中,普通的应用程序都是属于某个环境子系统的,比如记事本、画笔、计算器都属于windows子系统,它们通过所谓的WindowsAPI与子系统交互。类似的,运行在NT系统中的Linux应用程序通过LinuxAPI与Linux子系统交互
那么有没有不依赖子系统的进程呢?答案是肯定的,有一类的特殊的进程它们不依赖任何子系统,通过特殊的私有接口直接与内核交互,通常把这类进程叫作原生(native)进程
特点:原生程序的入口不是main或者WinMain,而是叫作NtProcessStartup的入口函数,原生进程结束时,不能像main函数那样返回运行时,而是要调用NtTerminateProcess把自己结束掉。另外不能像开发普通程序那样开发原生程序,而需要使用开发驱动程序的DDK或者WDK环境来构建原生程序,原生程序也是标准的PE格式,但是头信息中的Subsystem字段的值为0001,代表IMAGE_SUBSYSTEM_NATIVE
(Session Manager Sub-System) 会话管理器子系统,一般简称为SMSS,其可执行文件为SMSS.EXE,NT系统启动时,当内核空间准备就绪后,内核会着手创建SMSS,让其带领用户空间的建设
SMSS启动后,会执行一系列任务来开创用户世界,步骤非常多,这里挑几个比较重要的做介绍
1.执行注册表中登记的程序,例如启动 HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\BootExecute 下列出的程序。(默认情况下,Autochk.exe 会列在那里,它会检查磁盘。)
2.执行登记在PendingFileRenameOperations表键下的延迟文件操作,比如删除杀毒软件发现但又无法立即删除的病毒程序
3.初始化虚拟内存文件(paging file)
4.加载windows子系统的内核模块Win32K.SYS
5.创建Windows子系统服务进程CSRSS
6.创建显示登录桌面和执行登录过程的WinLogon进程
系统启动后,SMSS内部一般运行着2个线程:一个是主线程,它调用NtWaitForMultipleObjects永久等待CSRSS进程WinLogon进程,监视其是否意外退出,一旦退出,即触发蓝屏崩溃(CSRSS被创建的时候会被标记为关键进程,同时也是受保护进程),另一个线程是SMSS的工作线程,它调用NtWaitForWorkViaWorkerFactory等待登录会话有关的任务
CSRSS是Windows子系统的服务进程,全名为Client-Server Sub-system,这个名字代表了CSRSS内部的工作模式--经典的C/S(客户端/程序发起请求,服务器程序响应请求)模式
CSRSS进程内会加载多个DLL形式的服务模块,向windows子系统的程序提供服务。服务模块的数量和名字是可以通过注册表配置的,所以某些病毒程序会冒充服务模块,目的是能混进可以长时间栖身的CSRSS进程空间(开启PPL主动注入不了)
CSRSS主要负责管理窗口和GDI对象,分发窗口消息,打印消息和调试服务等。
最小进程和Pico进程
最小进程就是在创建进程时指定一个特殊的标志,告诉NT内核只创建进程的空间,不要向进程空间中添加内容,因此不存在PEB结构也不会有映射的NTDLL,相当于一个空壳子
以注册表进程(Registry)为例,注册表进程的主要作用是缓存注册表数据,以便提高访问注册表数据的效率,降低与注册表有关的内存开销,相当于一个内存仓库
!process 0 0 Registry
Registry进程一般有3个工作线程,用来把修改过的注册表数据成批写回磁盘
pico进程是“最小进程”的子类,pico代表10的-12次方,有微小之意,跟最小进程相比,Pico进程的特点是它通过所谓的Pico提供器与NT内核协作,主要用来支持WSL Linux子系统
安全性
实现windows安全性的安全组件和数据库有:
1 安全引用监视器Security reference monitor (简称SRM) 如图,这是位于核心态windows执行体中的一个组件。它负责:
2 定义访问令牌数据结构来表示一个安全环境。
3 执行对象的安全访问检查
4 管理特权(用户权限)
5 生成所有的结果安全审计日志。
本地安全授权子系统 (Local Security Authority Subsystem, LSASS) 包括用户态程序lsass(windows/system32/Lsass.exe)和策略数据库
其中lsass程序它负责:
1)本地系统安全策略(比如:允许哪些用户登录到本地机器上,口令策略,授予用户和用户组的特权,以及系统安全审计设置
2)用户认证
3)发送安全审计消息到事件日志。
在此进程加载了4个策略相关的dll模块,他们是:
netlogon.dll
ntdsapi.dll
samsrv.dll
lsasrv.dll
这四个模块对应于上图中的Netlogon ,Active Directory , SAM server,LSA server
Netlogon (网络登陆服务),这是一个windows服务,它建立起与域控制器之间的安全通道,对用户和服务进行身份验证。它将用户的凭然后返回用户据传递给域控制器,的域安全标识符和用户权 限。这通常称为 pass-through 身份验证。
Active Directory (活动目录),包括两个方面:目录和与目录相关的服务。目录是存储各种对象的一个物理上的容器,从静态的角度来理解这活动目录与我们以前所结识的“目录”和“文 件夹”没有本质区别,仅仅是一个对象,是一实体;而目录服务是使目录中所有信息和资源发挥作用的服务,活动目录是一个分布式的目录服务,信息可以分散在多 台不同的计算机上,保证用户能够快速访问,因为多台机上有相同的信息,所以在信息容氏方面具有很强的控制能力,正因如此,不管用户从何处访问或信息处在何 处,都对用户提供统一的视图。域是由一组计算机和与他们相关联的安全组构成的,每个安全组被当作单个实体来管理。活动目录存储了有关该域中的对象的信息,这样的对象包括用户、组和计算机。域用户和组的口令信息和特权也被存储在活动目录中。关于活动目录的介绍祥见 http://windows.chinaitlab.com/domain/37480.html
SAM (安全帐户管理器服务),它负责管理一个数据库,SAM 数据库包含已定义的本地用户和组,以及他们的密码和其他属性的数据库。在域控制器上,SAM 不存储域定义的用户,而是存储系统的管理员恢复帐户定义和密码。此数据库存储在 HKLM\SAM 下的注册表中。
LSA (本地安全授权服务),它实现了Lsass子系统的绝大部分功能。
LSASS策略数据库 LSASS policy database 包含本地系统安全策略设置的数据库,数据库存储在注册表HKLMSecurity子键下面,包含:
-
哪些域是可信任的,从而可以认证用户的登录请求
-
谁允许访问系统,以及如何访问(交互式登录、网络登录,或者服务登录)
-
分配给谁哪些特权
-
执行哪一种安全审计
-
域登录在本地缓存的信息
-
Windows服务的用户-账户登录信息
交互式登录管理器WinLogon(SMSS建立Windows子系统后,会创建这个关键的进程),主要负责安全登录工作,掌握着登录、重启和关机等重要的系统行为
登录用户界面 (LogonUI)运行 %SystemRoot%\System32\LogonUI.exe 的用户模式进程,它为用户提供用户界面,他们可以用来在系统上进行身份验证。LogonUI 使用凭据提供程序通过各种方法查询用户凭据。
winlogon需要依赖系统中安装的凭据提供程序来获取用户的账户名和密码
凭据提供程序是一种位于DLL内部的COM对象,默认的提供程序位authui.dll、SmartcardCredentialProvider.dll、FaceCredentialProvider,分别用于支持密码、智能卡PIN码以及面不识别身份验证,还可以提供其他的凭据提供程序以支持其他的验证身份的方法
在操作系统中,当我们提到安全的时候,意味着有一些资源需要被保护,在Windows操作系统中,这些被保护的资源大多以对象(Object)的形式存在,对象是对资源的一种抽象。
windows的大部分内核代码都是C写的,C是不能直接面向对象的构造,例如多态函数或类的继承,所以windows中基于语言的对象实现只是借助面向对象语言的一些特性
windows中可保护的对象包括文件、设备、邮件槽、管道、作业、进程、线程、事件、互斥体、信号量、服务、注册表、访问令牌、共享内存节、打印机、存储卷、窗口站、桌面、网络共享、活动目录等等。为了控制谁能操作某个对象,安全系统必须首先确定每个用户的身份。
对象拥有统一的头部,保存着抽象出来的信息
lkd> dt _OBJECT_HEADER
nt!_OBJECT_HEADER
+0x000 PointerCount : Int8B
+0x008 HandleCount : Int8B
+0x008 NextToFree : Ptr64 Void
+0x010 Lock : _EX_PUSH_LOCK
+0x018 TypeIndex : UChar
+0x019 TraceFlags : UChar
+0x019 DbgRefTrace : Pos 0, 1 Bit
+0x019 DbgTracePermanent : Pos 1, 1 Bit
+0x01a InfoMask : UChar
+0x01b Flags : UChar
+0x01b NewObject : Pos 0, 1 Bit
+0x01b KernelObject : Pos 1, 1 Bit
+0x01b KernelOnlyAccess : Pos 2, 1 Bit
+0x01b ExclusiveObject : Pos 3, 1 Bit
+0x01b PermanentObject : Pos 4, 1 Bit
+0x01b DefaultSecurityQuota : Pos 5, 1 Bit
+0x01b SingleHandleEntry : Pos 6, 1 Bit
+0x01b DeletedInline : Pos 7, 1 Bit
+0x01c Reserved : Uint4B
+0x020 ObjectCreateInfo : Ptr64 _OBJECT_CREATE_INFORMATION
+0x020 QuotaBlockCharged : Ptr64 Void
+0x028 SecurityDescriptor : Ptr64 Void
+0x030 Body : _QUAD
在+0x28的位置包含着指向安全描述符的指针
每个对象都可以拥有自己的安全描述符(Security Descriptor),用来描述它能够被谁、以何种方式而访问。这些对象是客体,那么访问这些对象的主体是什么呢?这些主体就是操作系统中的各个进程,更准确地说是这些进程中的每个线程。每个进程都有一个基本令牌 (Primary Token),可以被进程中的每个线程所共享,而有些线程比较特殊,它们正在模拟(Impersonating)某个客户端的身份,因此拥有一个模拟令牌(Impersonation Token),对于这些线程来说,有效令牌就是模拟令牌,而其他线程的有效令牌则是其所属进程的基本令牌。当主体尝试去访问客体时,操作系统会执行访问权限检查(Access Check),具体来说,是用主体的有效令牌与客体的安全描述符进行比对,从而确定该次访问是否合法。为了提高效率,访问权限检查(Access Check)提供了一种缓存机制,即只有当一个对象被一个进程创建或者打开时,才会进行访问权限检查,访问权限检查的结果会被缓存到进程的句柄表(Handle Table)中(一个进程中的所有线程共享了同一个句柄表,当一个线程打开对象时,其他线程都可以访问该对象),该进程对该对象的后续操作只需要查询句柄表中内容即可确定访问的合法性,而不必每次都执行开销更大的访问权限检查(Access Check)。因为对象和进程都有各自的继承层次,所以对象的安全描述符和进程的令牌从逻辑上讲也是可以继承的,比如文件系统中的文件可以继承其所在目录的安全描述符,子进程可以继承父进程的令牌,这种继承机制使让对象和进程拥有了默认的安全属性,因此,在一个系统中的安全描述符和令牌的实例数目非常有限,大多数情况下是从父辈们继承过来的默认值。
安全描述符(Security Descriptors,SD)
在Windows中,每一个安全对象实体都拥有一个安全描述符,安全描述符包含了被保护对象相关联的安全信息的数据结构,它的作用主要是为了给操作系统提供判断来访对象的权限。
一个文件的权限描述符主要表现为:
上图中的安全选项卡主要是表述了sethc.exe这个文件能够被哪些用户访问,并且这些用户拥有sethc.exe的哪些权限,如:读取、读取和执行、写入、完全控制等。
dt nt!_security_descriptor
+0x000 Revision : UChar # 版本
+0x001 Sbz1 : UChar # 大小
+0x002 Control : Uint2B # 一组标志,用于限定安全描述符或安全描述符的各个字段的含义
+0x008 Owner : Ptr64 Void # 指定对象的所有者(SID)
+0x010 Group : Ptr64 Void # 指定对象的主组(SID)
+0x018 Sacl : Ptr64 _ACL # 系统访问控制列表
+0x020 Dacl : Ptr64 _ACL # 自定义访问控制列表
在安全描述符中存在着一个DACL表,里面描述了拥有何种身份的主体申请的何种访问权限会被允许或者拒绝。DACL表中的每个表项(ACE)拥有如下的结构:
https://docs.microsoft.com/en-us/windows/win32/secauthz/access-control-entries
+---------------+-------+-------+---------------+---------------+
| AceFlags | Resd |Inherit| AceSize | AceType |header
+---------------+-------+-------+---------------+---------------+
| Mask |
+---------------------------------------------------------------+
| |
+ +
| |
+ Sid +
| |
+ +
| |
+---------------------------------------------------------------+
对于DACL来说,AceType可以有2种可能
#define ACCESS_ALLOWED_ACE_TYPE (0x0)
#define ACCESS_DENIED_ACE_TYPE (0x1)
Mask是一个代表相关权限集合的位图;主体的身份使用Sid(Security Identifier)来表示
什么是SID(Security Identifier,SID)
windows不是使用名称标识在系统中执行操作的实体(因为名称可能唯一,也可能不唯一),而是使用安全标识符来保证角色的唯一性。用户、本地和域组、本地计算机、域、域成员以及服务都有SID。SID是一种可变长度的数值,包含SID结构版本号、一个48位标识符结构值,以及可变数量的32位子机构或相对标识符 (RID) 值。
dt nt!_SID
+0x000 Revision : UChar # SID结构版本号
+0x001 SubAuthorityCount : UChar # 指定SubAuthority 数组中的元素数
+0x002 IdentifierAuthority : _SID_IDENTIFIER_AUTHORITY # 一个48位标志符颁发机构值
+0x008 SubAuthority : [1] Uint4B # 可变数量的32位子机构或相对标识符 (RID) 值,相对标识符(RID) 是一个可变长度的数字,它在创建时分配给对象,并成为唯一标识帐户或组的对象的安全标识符(SID) 的一部分域内,因为某些ID在所有系统上都是通用的,或者以相同的前缀开头,所以就靠这个随机值来实现真正唯一
C:\Users\MR robot>whoami /user
用户信息
----------------who
用户名 SID
============= ==============================================
zhou\mr robot S-1-5-21-1998032399-4149069537-3699205571-1001
版本号为1,标识符机构值为5(windows安全机构),4个子结构值加上一个RID(1001)这就组成了一个SID
指派给用户、计算机和组的RID从1000开始。500-999的RID被专门保留起来、表示在每个Windows计算机和域中通用的账户和组,它们称为“已知RID”,有些已知RID会附加到一个域SID上,从而构成一个惟一的标识符
例如:
- S-1-5-21-xxxx-xxx-500 (Administrator) 本地管理员
- S-1-5-21-xxxx-xxx-501 (Guest) 本地来宾用户
- S-1-5-21-xxxx-xxx-1004 (Workstaion) 本地工作站
其中RID为500的用户代表管理员用户,账户的RID值是固定的,通常渗透中常说的RID劫持、克隆用户就是修改其他用户的RID值来实现让系统认为当前用户是管理员。
DACL检查
https://docs.microsoft.com/en-us/windows/win32/secauthz/how-dacls-control-access-to-an-object
每一个Windows进程都拥有一个线程,当程序想要访问某个安全对象时,对象管理器会调用安全引用监视器Security reference monitor (简称SRM) 。他会提取当前线程的访问令牌,然后将访问令牌的权限和被访问的安全对象DACL进行比较,具体是ACE中的安全标识符 ( SID ) 与访问令牌中的 SID 进行逐个比较,以查看应授予进程的访问级别。检查的条目就是访问控制条目(Access control entries,ACE),最先检查的ACE优先级越高。
-
Thread A拥有Adrew的访问令牌,当它访问对象Object的时候,系统会从DACL第一个条目开始向下比对,由于第一个ACE是Access denied,并且用户名恰巧是Adrew,因此系统会拒绝访问,返回错误代码5。
-
Thread B拥有Jane的访问令牌,当它访问对象Object的时候,系统会从DACL第一个条目开始向下比对,由于第一个ACE不是针对Jane设置的,因此会继续向下比对,当到达第二个ACE时,Jane属于Group A,满足比对条件,因此拥有Write的权限。到达第三个时,如果Jane想要的是读取,那么也会在到达第三个ACE后,获得读取的权限。
DACL检查过程是这样的,如果对象的安全描述符为空,代表着这个对象不受任何保护,即可以被任何令牌代表的主体访问;如果对象拥有一个非空的安全描述符,但是安全描述符中的Dacl成员为空,代表该对象没有显式地赋予任何主体任何访问权限,即不允许被访问;如果Dacl成员非空,就按照表中的顺序逐一与当前主体的身份Sid进行比对,直到该主体所请求的权限被全部显式允许,或者某一请求的权限被显式地拒绝,检查结束;如果直到Dacl表检查结束,主体申请的权限仍未被全部显式允许,那么仍然代表拒绝。
根据以上规则可知,DACL中各个表项的顺序对访问权限检查的结果至关重要,比如说有一条有关某主体的显式拒绝的表项位于表的最后,如果位于其之前的表项已经显式允许了这一主体申请的权限,那么这条显式拒绝的表项将起不到任何作用。基于此种原因,如果通过Windows右键安全选项菜单添加的DACL表项,显式拒绝的表项永远被置于显式允许表项前面,以保证其有效性。直接通过API添加则不受此种限制。
系统访问控制列表(System access control list,SACL)
包含当前对象的审计(Audit)、完整性级别(Integrity Level)以及可信赖级别(Trust Level)等信息
系统访问控制列表主要是配置审核对象的ACE,当这些ACE被允许或拒绝的时候,系统将自动产生“安全”日志。
图中设置了Service.log的SACL,当它的DACL被改变成功后,操作系统会自动帮助我们产生一条安全日志,我们可以提取其中的关键信息
修改访问控制列表(Access control list,ACL)
在Windows中,修改内核对象的方法只有调用API,因此,可以将安全描述符也理解为一个内核对象的属性。
以下是修改ACL的简要过程:
- 使用 GetSecurityInfo 或者 GetNamedSecurityInfo 函数从对象的安全描述符中获取DACL。
- 对于每个新的ACE,请调用BuildExplicitAccessWithName函数以使用描述ACE的信息填充EXPLICIT_ACCESS结构。
- 调用SetEntriesInAcl,为新ACE指定现有的ACL和EXPLICIT_ACCESS结构的数组。SetEntriesInAcl函数分配和初始化的ACL和的ACE。
- 调用SetSecurityInfo或SetNamedSecurityInfo函数,将新的ACL附加到对象的安全描述符。
https://docs.microsoft.com/zh-cn/windows/win32/secauthz/creating-or-modifying-an-acl
令牌(Token)
SRM使用一种名为令牌(访问令牌)的对象来标识进程或线程的安全上下文。令牌所包含的信息是与该用户账户相关的进程或线程的身份和权限信息。当用户登录时,系统通过将用户输入的密码与储存在安全数据库中的密码进行对比。若密码正确,系统将生成一个访问令牌。之后,该用户执行的每个进程都会拥有一个该访问令牌的副本。
目前访问令牌分为两种令牌:
- 主令牌(每一个进程都具有一个唯一的主令牌,进程通过主令牌被开启)
- 模拟令牌(在默认的情况下,当线程被开启的时候,所在进程的主令牌会自动附加到当前线程上,作为线程的安全上下文。而线程可以运行在另一个非主令牌的访问令牌下执行,而这个令牌被称为模拟令牌。而指定线程的模拟令牌的过程被称为模拟)
主令牌是与进程相关的;模拟的令牌是与模拟令牌的线程相关的。主令牌和模拟令牌,都会在系统重启或者关机后全部清除。
访问令牌包含以下信息:
- 用户帐户的安全标识符 (SID)
- 用户帐户所属的用户组的 SIDs
- 一个 logon SID,标识当前登录会话
- 用户或用户群的特权清单
- 所有者的 SID
- 基本群的 SID
- 当用户创建可安全对象(securable object)且没有给出安全描述符时,系统使用的缺省的自主访问控制列表(DACL)
- 访问令牌资源
- 是否为 primary 或 impersonation token
- 限制性 SIDs 的可选列表
- 当前 impersonation 级别
- 其他统计
dt nt!_TOKEN
+0x000 TokenSource : _TOKEN_SOURCE
+0x010 TokenId : _LUID
+0x018 AuthenticationId : _LUID
+0x020 ParentTokenId : _LUID
+0x028 ExpirationTime : _LARGE_INTEGER
+0x030 TokenLock : Ptr64 _ERESOURCE
+0x038 ModifiedId : _LUID
+0x040 Privileges : _SEP_TOKEN_PRIVILEGES
+0x058 AuditPolicy : _SEP_AUDIT_POLICY
+0x078 SessionId : Uint4B
+0x07c UserAndGroupCount : Uint4B
+0x080 RestrictedSidCount : Uint4B
+0x084 VariableLength : Uint4B
+0x088 DynamicCharged : Uint4B
+0x08c DynamicAvailable : Uint4B
+0x090 DefaultOwnerIndex : Uint4B
+0x098 UserAndGroups : Ptr64 _SID_AND_ATTRIBUTES
+0x0a0 RestrictedSids : Ptr64 _SID_AND_ATTRIBUTES
+0x0a8 PrimaryGroup : Ptr64 Void
+0x0b0 DynamicPart : Ptr64 Uint4B
+0x0b8 DefaultDacl : Ptr64 _ACL
+0x0c0 TokenType : _TOKEN_TYPE
+0x0c4 ImpersonationLevel : _SECURITY_IMPERSONATION_LEVEL
+0x0c8 TokenFlags : Uint4B
+0x0cc TokenInUse : UChar
+0x0d0 IntegrityLevelIndex : Uint4B
+0x0d4 MandatoryPolicy : Uint4B
+0x0d8 LogonSession : Ptr64 _SEP_LOGON_SESSION_REFERENCES
+0x0e0 OriginatingLogonSession : _LUID
+0x0e8 SidHash : _SID_AND_ATTRIBUTES_HASH
+0x1f8 RestrictedSidHash : _SID_AND_ATTRIBUTES_HASH
+0x308 pSecurityAttributes : Ptr64 _AUTHZBASEP_SECURITY_ATTRIBUTES_INFORMATION
+0x310 Package : Ptr64 Void
+0x318 Capabilities : Ptr64 _SID_AND_ATTRIBUTES
+0x320 CapabilityCount : Uint4B
+0x328 CapabilitiesHash : _SID_AND_ATTRIBUTES_HASH
+0x438 LowboxNumberEntry : Ptr64 _SEP_LOWBOX_NUMBER_ENTRY
+0x440 LowboxHandlesEntry : Ptr64 _SEP_CACHED_HANDLES_ENTRY
+0x448 pClaimAttributes : Ptr64 _AUTHZBASEP_CLAIM_ATTRIBUTES_COLLECTION
+0x450 TrustLevelSid : Ptr64 Void
+0x458 TrustLinkedToken : Ptr64 _TOKEN
+0x460 IntegrityLevelSidValue : Ptr64 Void
+0x468 TokenSidValues : Ptr64 _SEP_SID_VALUES_BLOCK
+0x470 IndexEntry : Ptr64 _SEP_LUID_TO_INDEX_MAP_ENTRY
+0x478 DiagnosticInfo : Ptr64 _SEP_TOKEN_DIAG_TRACK_ENTRY
+0x480 BnoIsolationHandlesEntry : Ptr64 _SEP_CACHED_HANDLES_ENTRY
+0x488 SessionObject : Ptr64 Void
+0x490 VariablePart : Uint8B
我们主要关注其中的特权位图和三个代表主体身份的Sid数组:UserAndGroups,RestrictedSids,Capabilities
Privileges位图
令牌中的Privileges决定该令牌的线程或进程可以做哪些事情。 如果想让一个进程有更大的权限去做一些事情,我们就可以通过给进程提权的方式来做到
用户权限和特权
在系统还有其他很多操作是无法通过对象访问保护机制来控制,因为并未涉及与特定对象的交互。
windows会使用特权(privilege)和用户权限(account right)让系统管理员控制哪些用户可以执行与安全有关的操作
所谓特权,是指那些不与某一具体对象相关的,用户执行的某些与系统相关,影响整个系统的操作的访问权限,例如关闭计算机或更改系统时间。
特权
而用户权限可以允许或拒绝用户获得针对计算机执行某种类型登录操作的能力,例如本地登录或远程交互
可以使用本地安全策略(secpol.msc)将特权分配给组或账户,该工具会列出Windows中所有特权和用户权限,其中不包含“登录”字眼的都是特权
用户权限不由SRM负责实施这种策略,肯定也不存放在令牌里面。那这种策略怎么实施的呢?
当用户登录到计算机时,Winlogon调用LogonUser API,然后LogonUser会调用LsaLogonUser。LogonUser接受的参数代表了所要执行的登录类型,其中包括:
Windows系统的安全日志中可以获得更多有价值的信息,它细分了很多种登录类型,可以方便让你区分登录者到底是从本地登录、网络登录,以及其它更多的登录方式来访问系统。通过了解这些登录方式,将有助于你从系统事件日志中发现可疑的登录行为
登录类型2:交互式登录(Interactive)
这是大家最常用的登录方式,交互式登录就是指用户在计算机的控制台上进行的登录,也就是在本地键盘上进行的登录,但不要忘记通过虚拟机控制台登录仍然属于交互式登录,虽然它是基于网络的。
登录类型3:网络(Network)
通过网络的上访问一台计算机时在大多数情况下Windows会记为类型3,最常见的情况就是连接到共享文件夹或者共享打印机。另外大多数情况下通过网络登录IIS时也被记为这种类型,但基本验证方式的IIS登录是个例外,它将被记为类型8。
登录类型4:批处理(Batch)
当Windows运行一个计划任务时,“计划任务服务”将为这个任务首先创建一个新的登录会话以便它能在此计划任务所配置的用户账户下运行,当这种登录出现时,Windows在日志中记为类型4,对于其它类型的工作任务系统,依赖于它的设计,也可以在开始工作时产生类型4的登录事件,类型4登录通常表明某计划任务启动,但也可能是一个恶意用户通过计划任务来猜测用户密码,这种尝试将产生一个类型4的登录失败事件,但是这种失败登录也可能是由于计划任务的用户密码没能同步更改造成的,比如用户密码更改了,而忘记了在计划任务中进行更改。
登录类型5:服务(Service)
与计划任务类似,每个服务都被配置在某个特定的用户账户下运行,当一个服务开始时,Windows首先为这个特定的用户创建一个登录会话,这将被记为类型5,失败的类型5通常表明用户的密码已变而这里没得到更新,当然这也可能是由恶意用户的密码猜测引起的,但是这种可能性比较小,因为创建一个新的服务或编辑一个已存在的服务默认情况下都要求是管理员或服务器管理员身份,而这种身份的恶意用户,已经有足够的能力来干他的坏事了,已经用不着费力来猜测服务密码了。
登录类型7:解锁(Unlock)
你可能希望当一个用户离开他的计算机时相应的工作站自动开始一个密码保护的屏保,当一个用户回来解锁时,Windows就把这种解锁操作认为是一个类型7的登录,失败的类型7登录表明有人输入了错误的密码或者有人在尝试解锁计算机。
登录类型8:网络明文(NetworkCleartext)
这种登录表明这是一个像类型3一样的网络登录,但是这种登录的密码在网络上是通过明文传输的,WindowsServer服务是不允许通过明文验证连接到共享文件夹或打印机的,据我所知只有当从一个使用Advapi的ASP脚本登录或者一个用户使用基本验证方式登录IIS才会是这种登录类型。“登录过程”栏都将列出Advapi。
登录类型9:新凭证(NewCredentials)
当你使用带/Netonly参数的RUNAS命令运行一个程序时,RUNAS以本地当前登录用户运行它,但如果这个程序需要连接到网络上的其它计算机时,这时就将以RUNAS命令中指定的用户进行连接,同时Windows将把这种登录记为类型9,如果RUNAS命令没带/Netonly参数,那么这个程序就将以指定的用户运行,但日志中的登录类型是2。
登录类型10:远程交互(RemoteInteractive)
当你通过终端服务、远程桌面或远程协助访问计算机时,Windows将记为类型10,以便与真正的控制台登录相区别,注意XP之前的版本不支持这种登录类型,比如Windows2000仍然会把终端服务登录记为类型2。
登录类型11:缓存交互(CachedInteractive)
Windows支持一种称为缓存登录的功能,这种功能对移动用户尤其有利,比如你在自己网络之外以域用户登录而无法登录域控制器时就将使用这种功能,默认情况下,Windows缓存了最近10次交互式域登录的凭证HASH,如果以后当你以一个域用户登录而又没有域控制器可用时,Windows将使用这些HASH来验证你的身份。
为了响应登录请求, LSA (本地安全权限服务)会做一个权限检查。在用户试图登录时,通过LSA策略数据库获得分配给该用户的权限,然后针对分配给用户的用户权限检查登录类型,如果用户不具备所要进行的登录类型必需的权限,则会被拒绝
Windows应用程序可以使用LsaAddAccountRights和LsaRemoveAccountRights函数来为用户添加或移除用户权限,还可以使用LsaEnumerateAccountRights枚举分配给用户的权限
特权
特权(privilege)是Windows操作系统的帐户权限,如用户、用户群、系统服务、计算机等执行不同的系统操作,如关机、装入设备驱动、改编系统时间等。特权不同于访问权限(access right)特权控制访问系统资源或系统相关任务,访问权限控制访问可安全对象(securable object)
特权策略由不同的组件定义并实施,例如,调试特权(SeDebugPrivilege)可以让进程在使用WindowsAPI中的OpenProcess函数打开到另一个进程的句柄时跳过安全检查,该特权的检查工作就由进程管理器负责
当组件需要检查令牌以确定是否具备某个特权时,如果组件运行在用户模式下,会使用PrivilegeCheck或LsaEnumerateAccountRights;内核模式下,则会使用SeSinglePrivilegeCheck或SePrivilegeCheck
超级特权
一些特权极为强大,以至于一旦用户获得了这些特权,基本上已经等同于能完整控制计算机的“超级用户”,某用户拥有以下几种权限中的任意一个,那我们就能进一步进行利用,使用这些特权执行代码,进而为用户提供本不应获得的特权
SeDebugPrivilege
用来调试指定进程,包括读写内存,常用作实现注入。
利用思路:
- 找到system权限的进程
- 代码注入
- 获得权限
SeTcbPrivilege
拥有它就等同于获得了系统的最高权限。
利用思路:
- 调用LsaLogonUser获得Token
- 将该Token添加至Local System account组
- 该Token具有System权限
SeRestorePrivilege
对当前系统任意文件具有写的权限,可以用自己的文件替换系统中的任何文件。
SeCreateTokenPrivilege
可以创建令牌,win10中一般用户没有这个权限。
利用思路:
通过WinAPI ZwCreateToken创建Primary Token
将Token添加至local administrator组
该Token具有System权限
SeLoadDriverPrivilege
加载驱动文件。
利用思路:
msf中的capcom_sys_exec模块即可
原理:
加载存在漏洞的驱动文件Capcom.sys,驱动文件不仅想要什么权限就有什么权限,而且还可以重新定义权限,因为权限本来就是在内核被定义(强制执行)的
https://github.com/tandasat/ExploitCapcom
SeTakeOwnershipPrivilege
特权持有者能够接管任何一个被保护对象的所有权。做法是,将他自己的SID写到该对象的安全描述符的所有者域中。由于所有者总是被授予“读取和修改该安全描述符的DACL”许可,所以,具有此特权的进程可以修改此DACL,以授予他自己对于该对象的完全访问权。
SeImpersonatePrivilege
拥有此权限的用户:
管理员或者本地的服务账户
由服务控制管理器启动的服务
由组件对象模型 (COM) 基础结构启动的并配置为在特定帐户下运行的COM服务器
IIS与SqlServer用户
当我们拥有这个权限的时候,代表了我们可以将当前计算机上的其他令牌盗取过来自己使用,具体操作可见通过token窃取实现降权或者提权。
利用场景:
拿到iis或者sqlserver用户的权限,使用juicypotato工具进行提权到system。
SeAssignPrimaryPrivilege
拥有这个权限的进程可以向其他进程分配token。
拥有此权限的用户:
iis与sqlserver
利用原理跟SeImpersonatePrivilege权限的利用原理类似,都是通过NTLM Relay to Local Negotiation来获取system权限的token,区别是获取token后利用system权限的token创建新的进程,如果开启SeImpersonate权限,调用CreateProcessWithToken,传入System权限的Token,创建的进程为System权限,或者如果开启SeAssignPrimaryToken权限,调用CreateProcessAsUser,传入System权限的Token,创建的进程为System权限。
利用场景:
拿到iis或者sqlserver用户的权限,使用juicypotato工具进行提权到system。
注意:特权在用户态是通过LUID来表示
dt _LUID_AND_ATTRIBUTES
LUID就是指locally unique identifier,GUID大家是比较熟悉的,和GUID的要求保证全局唯一不同,LUID只要保证局部唯一,就是指在系统的每一次运行期间保证 是唯一的就可以了。另外和GUID相同的一点,LUID也是一个64位的值,相信大家都看过GUID那一大串的值,我们要怎么样才能知道一个权限对应的 LUID值是多少呢?答案是使用API函数LookupPrivilegevalue
BOOL LookupPrivilegevalue(
LPCTSTR lpSystemName, // system name
LPCTSTR lpName, // privilege name
PLUID lpLuid // locally unique identifier
);
第一个参数是系统的名称,如果是本地系统只要指明为NULL就可以了,第三个参数就是返回LUID的指针,第二个参数就是指明了权限的名称,如“SeDebugPrivilege”。
在内核结构体_TOKEN中是使用三个位图来表示
lkd> dt _SEP_TOKEN_PRIVILEGES
nt!_SEP_TOKEN_PRIVILEGES
+0x000 Present : Uint8B
+0x008 Enabled : Uint8B
+0x010 EnabledByDefault : Uint8B
分别代表当前的主体可以选用的特权集合(Present)、已经打开的特权集合(Enabled)和默认打开的特权集合(EnabledByDefault),后两个集合应该是Present集合的子集。
与代表主体身份的Sid数组不同,特权集合的表示简单,而且没有任何保护。从用户态通过API只能打开或者关闭某一项已经存在的特权,而不能增加可选的特权,换句话说,用户态只能修改Enabled特权集合,而不能修改Present特权集合;从内核态可以直接修改Present特权集合,比如给普通进程增加SeAssignPrimaryTokenPrivilege特权,以便为子进程显式指定令牌,而不是继承当前进程的令牌,可以达到扩大子进程权限的效果。
经典提权应用:
要对一个任意进程(包括系统安全进程和服务进程)进行指定了写相关的访问权的OpenProcess操作,只要当前进程具有SeDeDebug权限就可以了。
组要一个用户是Administrator或是被给予了相应的权限,就可以具有该权限。
可是,有时候就算我们用Administrator帐号对一个系统安全进程执行OpenProcess(PROCESS_ALL_ACCESS,FALSE, dwProcessID)还是会遇到“访问拒绝”的错误。
什么原因呢?这是因为在默认的情况下进程的一些访问权限是没有被开启(Enabled)的,所以我们要做的首先是开启这些权限。例如:我们要给当前进程具有SeDebug权限,只要找到Privileges 数组中对应的SeDebug权限的那个元素,然后让它开启就可以了
在SDK中,令牌对象中关于权限的两个成员PrivilegeCount和 Privileges合并到了一个结构体TOKEN_PRIVILEGES中
typedef struct _TOKEN_PRIVILEGES {
DWORD PrivilegeCount; //表示了权限数组元素的个数
LUID_AND_ATTRIBUTES Privileges[ANYSIZE_ARRAY]; //表示一个权限数组
} TOKEN_PRIVILEGES, *PTOKEN_PRIVILEGES;
我们使用下面的三个函数来打开令牌,修改权限。
OpenProcessToken,LookupPrivilegeValue,AdjustTokenPrivileges。
1)首先要获得进程访问令牌的句柄,这可以通过OpenProcessToken得到
BOOL OpenProcessToken(
HANDLE ProcessHandle, //要修改访问权限的进程句柄
DWORD DesiredAccess, //指定你要进行的操作类型
PHANDLE TokenHandle //返回的访问令牌指针
);
通过这个函数我们就可以得到当前进程的访问令牌的句柄(指定函数的第一个参数为GetCurrentProcess()就可以了)。
2)获取权限对应的LUID LookupPrivilegevalue
3) 修改权限,即:将2步骤找到的权限项开启。调用AdjustTokenPrivileges对这个访问令牌进行修改
BOOL AdjustTokenPrivileges(
HANDLE TokenHandle, // handle to token
BOOL DisableAllPrivileges, // disabling option
PTOKEN_PRIVILEGES NewState, // privilege information
DWORD BufferLength, // size of buffer
PTOKEN_PRIVILEGES PreviousState, // original state buffer
PDWORD ReturnLength // required buffer size
);
第一个参数是访问令牌的句柄;第二个参数决定是进行权限修改还是Disable所有权限;第三个参数指明要修改的权限,是一个指向 TOKEN_PRIVILEGES结构的指针,该结构包含一个数组,数据组的每个项指明了权限的类型和要进行的操作; 第四个参数是结构PreviousState的长度,如果PreviousState为空,该参数应为NULL;第五个参数也是一个指向 TOKEN_PRIVILEGES结构的指针,存放修改前的访问权限的信息,可空;最后一个参数为实际PreviousState结构返回的大小。
完整实现:
#include <Windows.h>
#include <stdio.h>
BOOL SetPrivilege(LPCTSTR lpszPrivilege, BOOL bEnablePrivilege)
{
TOKEN_PRIVILEGES tp;
HANDLE hToken;
LUID luid;
OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken);
LookupPrivilegeValueA(NULL, lpszPrivilege, &luid);
tp.PrivilegeCount = 1;
tp.Privileges[0].Luid = luid;
if (bEnablePrivilege)
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
else
tp.Privileges[0].Attributes = 0;
AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), (PTOKEN_PRIVILEGES)NULL, (PDWORD)NULL);
if (GetLastError() == ERROR_NOT_ALL_ASSIGNED)
return FALSE;
else
return TRUE;
}
int main()
{
SetPrivilege(SE_DEBUG_NAME, TRUE);
return 0;
}
1)OpenProcessToken -〉NtOpenProcessToken ->NtOpenProcessTokenEx->ObOpenObjectByPointer 得到进程的Token对象
2)LookupPrivilegeValueA-〉LookupPrivilegeValueW-〉LsaOpenPolicy-〉LsaLookupPrivilegeValue-〉NdrClientCall2 -〉通过LPC与SRM通讯,返回LUID。。。。
SRM运行在内核模式下,而Lsass运行在用户模式下,他们利用LPC进行通讯。在系统初始化过程中,SRM创建了一个名为SeRmCommandPort的端口,Lsass连接到此端口上。当Lsass进程启动的时候,它创建一个名为SeLsaCommandPort的LPC端口。SRM连接到此端口上,从而建立起两者之间的私有通讯。
SRM创建了一个共享内存区,用于传递超过256字节长的消息,他在连接调用中把该共享内存区的句柄传递过去。在系统初始化过程中,一旦SRM与Lsass相互连接起来,他们就不再监听各自的连接端口,因此,后来的用户进程无法连接到这些端口,从而避免一些恶意的破坏。
3)AdjustTokenPrivileges-〉NtAdjustPrivilegesToken-〉修改Taken对象中相关的值。。。
枚举
#include <stdio.h>
#include <Windows.h>
#include <TlHelp32.h>
// 通过进程Token获取进程用户类型
void __stdcall EnumOwner(HANDLE htoken)
{
DWORD dwLen;
PSID pSid = 0;
TOKEN_USER* pWork;
SID_NAME_USE use;
TCHAR User[256], Domain[256];
GetTokenInformation(htoken, TokenUser, NULL, 0, &dwLen);
pWork = (TOKEN_USER*)LocalAlloc(LMEM_ZEROINIT, dwLen);
if (GetTokenInformation(htoken, TokenUser, pWork, dwLen, &dwLen))//TokenUser代表用户帐户的令牌信息
{
dwLen = GetLengthSid(pWork->User.Sid);
pSid = (PSID)LocalAlloc(LMEM_ZEROINIT, dwLen);
CopySid(dwLen, pSid, pWork->User.Sid);
dwLen = 256;
//LookupAccountSid函数接受安全标识符(SID) 作为输入。它检索此 SID 的帐户名称和找到此 SID 的第一个域的名称
LookupAccountSid(NULL, pSid, &User[0], &dwLen, &Domain[0], &dwLen, &use);
printf("\t 用户类型 => %s : %s ", Domain, User);
}
}
// 枚举系统中进程的令牌权限信息
int enumprocess()
{
HANDLE SnapShot, ProcessHandle, hToken;
PROCESSENTRY32 pe32;
// 拍摄快照
SnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
pe32.dwSize = sizeof(PROCESSENTRY32);
if (Process32First(SnapShot, &pe32) == FALSE)
return 0;
while (1)
{
if (Process32Next(SnapShot, &pe32) == FALSE)
return 0;
printf("PID => %6i \t 进程名 => %-20s \t 线程数 => %3i", pe32.th32ProcessID, pe32.szExeFile, pe32.cntThreads);
// 获取特定进程权限等
ProcessHandle = OpenProcess(PROCESS_QUERY_INFORMATION, TRUE, pe32.th32ProcessID);
if (ProcessHandle != NULL)
{
if (OpenProcessToken(ProcessHandle, TOKEN_QUERY, &hToken))
{
EnumOwner(hToken);
CloseHandle(hToken);
CloseHandle(ProcessHandle);
}
}
printf("\n");
}
return 1;
}
int main(int argc, char* argv[])
{
enumprocess();
system("pause");
return 0;
}
代表主体身份的Sid数组
UserAndGroups:
代表着主体的普通用户身份和组身份,是不可或缺的成员;
RestrictedSids:
可选成员,如果不为空,则代表着当前的令牌是受限令牌,受限令牌通过从普通令牌中过滤掉一些比较敏感的身份转化而来,受限令牌中的UserAndGroups成员与普通令牌相同,但是RestriectedSids成员不为空,里面保存着过滤后的身份子集;由于在访问权限检查时,三个身份Sid数组要同时进行检查,只有结果都通过才允许该次访问,因此通过增加代表着受限制的权限集合的RestrictedSids成员,既达到了限制令牌权限的目的,又在UserAndGroups成员中保留了原有令牌的完整身份信息。
Capabilities:
可选成员,仅用于AppContainer,其中的Sid代表着与App相关的身份,比如拥有连接网络、访问当前位置等权限的身份。
这三个Sid数组都关联了哈希信息,以保护其完整性,因此,即使从内核态直接修改,也会因为无法通过完整性验证而失败。不过好在哈希的算法非常简单,可以轻易纂改
UAC与关联令牌
UAC是Vista版本引入的比较重要的安全机制,很多用户抱怨它带来的不便性,然而它就像Linux操作系统中的sudo一样,是保护系统安全的一道重要屏障。当用户登录Windows时,操作系统会为用户生成一对初始令牌,分别是代表着用户所拥有的全部权限的完整版本令牌(即管理员权限令牌),以及被限制管理员权限后的普通令牌,二者互为关联令牌;此后,代表用户的进程所使用的令牌都是由普通令牌继承而来,用来进行常规的、非敏感的操作;当用户需要进行一些需要管理员权限的操作时,比如安装软件、修改重要的系统设置时,都会通过弹出提权对话框的形式提示用户面临的风险,征求用户的同意,一旦用户同意,将会切换到当前普通令牌关联的管理员权限令牌,来进行敏感操作。通过这种与用户交互的方式,避免一些恶意程序在后台稍稍执行敏感操作。
关联令牌是通过Logon Session来实现的,下图展示了其大致原理:
不是所有的令牌都有关联令牌,例如一直运行在较高权限下不需要进行提权操作的令牌。
访问令牌模拟(Access Token Impersonation)
为什么需要用到令牌模拟?
举例来说,如果远程计算机上的用户希望删除NTFS共享中的文件,提供该共享的服务器就必须查询用户的账户和组SID,并检查文件的安全属性,对于程序来说这种方法非常繁琐且容易出错,因此Windows为了简化服务器的工作,提供了模拟服务。服务器即可去模拟客户端的令牌,从而代表客户端访问所请求的资源,由SRM去进行访问验证。
进程令牌模拟的流程:
- 调用OpenProcess获取进程句柄
- 调用OpenProcessToken,传入进程句柄获取访问令牌句柄
- 调用DuplicateTokenEx,设置访问令牌模拟级别并复制一个令牌句柄
- 调用CreateProcessWithToken,传入模拟令牌,创建一个新的进程达到代码执行的目的
注意,要达到令牌窃取创建进程的效果需要有一些前提:
-
当前用户必须拥有SeImpersonatePrivilege或SeAssignPrimaryTokenPrivilege特权
-
拥有目标进程的PROCESS_QUERY_INFORMATION权限
-
拥有目标进程访问令牌的
TOKEN_DUPLICATE | TOKEN_IMPERSONATE
权限
渗透中常说的令牌假冒、令牌窃取都是利用Windows的令牌模拟功能获取其他用户的令牌来创建进程。
在域渗透的过程中,我们常常发现域管理员登录了某些不安全的机器,会针对这些机器进行定向的攻击,获取机器权限后,寻找域管理员创建的进程,窃取访问令牌进而获取整个域的权限。
令牌窃取提权的防御措施
- 为了防止域管理员的令牌被窃取,应该禁止域管理员登录其它主机。如果登录了,使用完后应该及时重启电脑,从而把令牌清除。
- 及时安装微软推送的补丁
- 对于来路不明的或者危险的软件,既不要在系统中使用,也不要在虚拟机中使用
- 对令牌的时效性进行限制,以防止散列值被破解后泄露有效的令牌信息
- 对于令牌,应采取加密存储及多长验证保护
- 使用加密链路 SSL/TLS 传输令牌,以防止被中间人窃听
模拟级别
-
为了防止滥用模拟机制,Windows不允许服务器在没有得到用户同意的情况下执行模拟。用户进程在连接到服务器的时候可以指定一个安全服务质量,以此来限制服务器进程可以执行的模拟级别。
-
SecurityAnonymous级别:最为限制,服务器不能模仿或者识别出用户
-
SecurityIdentification级别:允许服务器得到用户的SID和特权 ,但不能模拟该用户
-
SecurityImpersonation级别:默认级别,允许服务器在本地系统上识别和模拟该用户
-
SecurityDelegation:最为随意,允许服务器在本地系统或远程系统上模拟该用户
-
烂土豆(Potato)提权的本质
烂土豆的提权原理主要是诱导高权限访问低权限的系统对象,导致低权限的对象可以模拟高权限对象的访问令牌(Access Token),进而可以用访问令牌创建进程,达到代码执行。
- 烂土豆(Rotten Potato)提权MS16-075
https://docs.microsoft.com/zh-cn/security-updates/securitybulletins/2016/ms16-075
- CVE-2020-0668
- CVE-2020-0683 MSI Packages Symbolic Links Processing - Windows 10 Privilege Escalation
- CVE-2020-8950
- CVE-2020-0683
- CVE-2019-1002101
- CVE-2019-0986
- CVE-2018-1088
- ….
这些漏洞要么是利用巧妙的手法获取令牌、要么是用高权限移动文件,因此用户层的提权漏洞大多都需要系统“主动”起来。
完整性级别(Integrity Level) 扩展
作用:将同一个账户的进程分为不同的完整性级别,从而保证低级别的进程无法影响高级别的进程。
-
完整性级别表示正在运行的应用程序进程和对象(例如应用程序创建的文件)的可信度,由一组特殊的 SID 和 ACL 条目实现的,它们代表五个不断增加的特权级别:untrusted, low, medium, high, system
-
如果一个对象处于比请求者更高级的信用等级,访问该对象就会受限。
-
Integrity 级别还实现了用户界面权限隔离,Integrity 性级别的规则应用于在同一桌面下的不同进程之间交换窗口消息
-
token 可以向更高 level 的 object 读数据,但是不能写数据。
-
大多数桌面应用程序以 medium integrity (MI)信任等级运行,信任度较低的程序(保护模式下的 IE 和 GPU 的 Sandbox)以 low integrity (LI) 信任等级运行,而 renderer 则以最低的信任等级运行。
-
Windows 完整性机制的目的是限制同一账户下运行的可信度较低的应用程序的访问权限。主要解决的安全问题是未授权篡改用户数据,间接篡改系统状态,信息泄露。
-
系统首先进行完整性级别的检查,然后再进行DACL的检查。
受保护进程
保护进程(Protected Process)是Windows操作系统为了保护某些关键进程,防止其被普通进程调试、注入、甚至是读取内存信息而建立起来的一种安全机制。
保护进程与其他普通进程的区别在于,保护进程的Protection成员不为0。
kd> dt nt!_EPROCESS Protection
+0x6b2 Protection : _PS_PROTECTION
kd> dt nt!_PS_PROTECTION
+0x000 Level : UChar
+0x000 Type : Pos 0, 3 Bits
+0x000 Audit : Pos 3, 1 Bit
+0x000 Signer : Pos 4, 4 Bits
保护进程的Type成员可以代表两种保护类型:Protected Process(PP),Protected Process Lite(PPL),两者的区别在于PP保护进程拥有更高的权限。保护进程的Signer成员代表该进程的签名发布者的类别。对于Signer为PsProtectedSignerWindows(5)和PsProtectedSignerTcb(6)的保护进程,其Type和Signer信息会被抽取出来,组装成一个Sid,代表着该进程的可信赖级别,保存到基本令牌中的TrustLevelSid成员中。当一个令牌中的TrustLevelSid被使用时,需要保证与当前进程的Protection信息保持同步,这主要是为了应对令牌在不同进程间传递时(比如子进程继承父进程的令牌)导致的TrustLevelSid成员过时的情形。
当调试或者创建一个进程时,会调用内核函数PspCheckForInvalidAccessByProtection进行权限检查,该函数根据当前进程以及目标进程的Protection来判定当前操作是否需要遵守保护进程规定的权限限制,具体判定规则如下:
- 如果操作来自于内核态代码,不需要遵守限制;
- 如果目标进程的Protection成员为空,表示目标进程不是保护进程,不需要遵守限制;
- 如果当前进程是PP类型保护进程,该类型保护进程拥有最高权限,不需要遵守限制;
- 如果当前进程与目标进程都是PPL类型保护进程,需要根据RtlProtectedAccess表来判断当前进程的Signer是否优先于(dominate)目标进程的Signer,如果是,不需要遵守限制;
- 其他情况,需要遵守限制。
http://47.117.131.13/index.php/archives/488/
https://lzeroyuee.cn/2021/03/16/Windows-internals-PPL进程/
用户帐户控制UAC
UAC 是微软在 Windows Vista 以后版本引入的一种安全机制, UAC全称是User Account Control直译为“用户帐户控制”,是微软为提高系统安全而在Windows Vista中引入的新技术,UAC需要用户在执行一些可能会影响计算机运行的操作或执行更改影响其他用户的设置的操作之前,提供权限或管理员密码
实现原理:Windows操作系统中所有资源都有一个DACL(Access Control List)标识了拥有什么权限的用户/进程能够访问这个资源。 在开启了 UAC 之后,如果用户以管理员权限登陆,会生成两份访问令牌,一份是完整的管理员访问令牌(Full Access Token),一份是标准用户令牌(Access Token)。一般情况下会以标准用户权限启动 Explorer.exe 进程。在需要使用高完整性令牌时,会提示询问用户,如果用户同意,则继续进行操作。
在Windows 7下打开注册表
在Windows 7上管理计算机
在Windows 10上管理计算机
有的需要授权、有的不需要,是因为UAC是分授权等级的:
首先请按Win+R,输入gpedit.msc,打开组策略。
然后我们在左侧窗口找到“计算机配置–Windows设置–安全设置–本地策略–安全选项”,再在右侧窗口找到“用户帐户控制: 管理员批准模式中管理员的提升权限提示的行为”,双击该条目,打开设置窗口,如下图:
- 不提示直接提升:关闭UAC,需要权限时直接提升权限。
- 在安全桌面上提示凭据:需要权限时在安全桌面上输入管理员密码提升权限。
- 在安全桌面上同意提示:需要权限时在安全桌面上选择“允许”提升权限。
- 提示凭据:需要权限时在普通窗口中输入管理员密码提升权限。
- 同意提示:需要权限时在普通窗口中选择“允许”提升权限。
- 非 Windows 二进制文件的同意提示:(默认设置)当非 Microsoft 应用程序的某个操作需要提升权限时,选择“允许”提升权限。
为什么有的应用程序不需要提示UAC?
因为普通应用执行权限有限,某些操作必然会要求更高的管理员权限。此时,通常就需要一个权限提升的操作。程序可以向系统请求提权,系统会将此请求通过提一个提示框,请用户确认。
如果当前用户的用户组权限不是管理员,提权操作是要求输入管理员密码的,这点和在Linux中的相应操作类似。
- 程序只能在运行前要求提权。如果已经在运行了,那么将失去申请提权的能力
- 权限提升仅对此次进程有效
提升权限的操作大致有两个:
- 自动提权请求
- 手动提权请求
手动提权就是“以管理员身份运行”,自动提权请求就是程序本身就一运行就开始申请权限,如:注册表编辑器
在开发的过程中,程序员若要开发一个程序,可以在编译器配置,写入一个配置文件,用于向系统标识该应用程序是必须要管理员权限运行的。
manifest文件
这个文件本质上是一个XML文件,用于标识当前应用程序的配置属性。
- aslnvoker 默认权限
- highestAvailable 最高权限
- requireAdministrator 必须是管理员权限
编译选项调整为requireAdministrator,当用户运行程序后,将获得管理员权限会话,不需要绕过UAC了。
manifest中其实还有其他属性,如:autoElevate(自动提升)
拥有自动权限提升属性的文件,当默认以管理员权限运行,不需要经过用户的授权。
BypassUAC
通常通过UAC的权限提升需要由用户确认,在不被用户发现的情况下静默的将程序的普通权限提升为管理员权限,从而使程序可以实现一些需要权限的操作被称之为BypassUac。