-
-
[原创]从mimikatz学习Windows安全之访问控制模型(一)
-
发表于: 2021-8-24 15:14 12719
-
作者:Loong716@Amulab
Mimikatz是法国安全研究员Benjamin Delpy开发的一款安全工具。渗透测试人员对mimikatz印象最深的肯定就是抓取Windows凭证,但作者对它的描述是“a tool I've made to learn C and make somes experiments with Windows security.”,其实它的功能不仅仅是抓取凭证,还包含了很多Windows安全相关的技术和知识
这里借用@daiker师傅的思维导图,mimikatz的模块大致可分为几个部分:
因此文章也会大致分为windows 访问控制模型,windows 凭据以及加解密,windows AD 安全,windows 进程以及服务,mimikatz 其他模块五个小系列。之前自己一直想分析mimikatz的相关功能,主要是出于以下原因:
mimikatz中与Windows访问控制模型相关的有privilege、token、sid三个模块,其分别对应特权、访问令牌、安全标识符三个知识,本文主要分析token模块,并简要介绍Windows访问控制模型
由于mimikatz代码逻辑较为复杂,涉及大量回调,因此文中代码都是经过简化的。文章可能也会有一些技术上或者逻辑上的错误,还请师傅们指正
Windows访问控制模型有两个基本组成部分:
访问令牌(Access Token)被用来描述一个进程或线程的安全上下文,用户每次登录成功后,系统会为其创建访问令牌,该用户的所有进程也将拥有此访问令牌的副本
当线程与安全对象进行交互或尝试执行需要特权的系统任务时,系统使用访问令牌来标识用户。使用windbg查看进程的token,其包含信息如下图所示:
安全描述符(Security Descriptor)包含与安全对象有关的安全信息,这些信息规定了哪些用户/组可以对这个对象执行哪些操作,安全描述符主要由以下部分构成:
在windbg中查看一个安全对象的安全描述符,可以清晰的看到安全描述符的组成:
可以看到该安全描述符的DACL中有三条ACE,ACE的类型都是ACCESS_ALLOWED_ACE_TYPE
,Mask
是权限掩码,用来指定对应的权限。以第一条ACE为例,其表示允许SID为S-1-5-32-544的对象能够对该安全对象做0x001fffff对应的操作
当某个线程尝试访问一个安全对象时,系统根据安全对象的ACE对照线程的访问令牌来判断该线程是否能够对该安全对象进行访问。通常,系统使用请求访问的线程的主访问令牌。但是,如果线程正在模拟其他用户,则系统会使用线程的模拟令牌
此时将在该安全对象的DACL中按顺序检查ACE,直到发生以下事件:
我们以微软文档中的图片为例,描述一下整个过程:
Mimikatz的token模块共有5个功能:
该功能用于列出当前进程/线程的token信息
只有一个可选参数/full
,当指定该参数时会打印出当前token的组信息和特权信息:
该功能的原理大致如下:
其核心为调用GetTokenInformation()
来获取token的各种信息,我们先来看这个API定义
其中第二个参数是一个TOKEN_INFORMATION_CLASS
枚举类型,我们可以通过指定它的值来获取token指定的信息
例如获取token的SessionID并输出,可以使用以下代码:
该功能是获取当前系统中所有的token,注意使用前需要先获取SeDebugPrivilege
,否则列出的token不全
该功能原理大致如下:
NtQuerySystemInformation()
用来检索指定的系统信息:
其第一个参数是一个SYSTEM_INFORMATION_CLASS
枚举类型,我们同样可以指定不同参数来获取不同的系统信息
以获取系统进程名和PID为例,代码如下:
PS:按照该思路,理论上利用CreateToolhelp32Snapshot()
+ Process32First()
遍历进程PID也可以实现该功能
该模块用于窃取指定用户的token,共有7个可选参数,这些参数主要用来指定要窃取的token,如果不指定参数则默认窃取NT AUTHORITY\SYSTEM
的token
假设我们现在在目标机器上发现的域管权限的token
我们可以指定目标TokenID,或者使用/domainadmin
来窃取域管的token,执行成功后可以看到当前线程已经拥有域管的模拟令牌:
然后我们就可以在当前mimikatz上下文中使用域管身份执行操作了,如DCSync
该功能大致过程如下:
由于窃取token是Access Token利用的重点,该过程放在本文后面分析
该功能是使用指定的token来运行程序,也可以使用token::elevate
中的几个参数来指定运行程序的token,除此之外还有一个参数:
其原理前三步与token::elevate
大致相同,区别在于使用DuplicateTokenEx()
窃取token后,该功能使用CreateProcessAsUser()
来使用新的primary token创建一个进程
创建进程后,利用匿名管道做进程间通信,将新创建进程的标准输出写入到匿名管道的write端,从管道read端读取数据进行回显(在webshell等非交互场景下很有用)
该模块用来清除线程的模拟令牌:
原理很简单,直接使用SetThreadToken(NULL, NULL)
即可将当前线程的token清除
在渗透测试中,窃取token是administrator -> system的常见手法之一,还经常被用于降权等用户切换操作
窃取token主要涉及以下几个API:
该函数打开指定PID的进程的句柄,需要注意的是第一个参数dwDesiredAccess,主要会用到的是以下三个权限
我在编写窃取Token的代码时,发现对部分进程(如smss.exe、csrss.exe等)调用OpenProcess会出现拒绝访问的情况,查阅网上资料后发现这些进程存在保护,需要使用PROCESS_QUERY_LIMITED_INFORMATION
权限打开句柄,详情请参考这篇文章
该函数打开与进程相关联的令牌的句柄,其中第二个参数DesiredAccess同样用来指定令牌的访问权限,需要以下几个:
如果要调用DuplicateTokenEx
需要指定TOKEN_DUPLICATE,如果调用ImpersonatedLoggedOnUser
则需要指定TOKEN_DUPLICATE和TOKEN_QUERY
DuplicateTokenEx
用来复制现有的令牌来生成一张新令牌,该函数可以选择生成主令牌还是模拟令牌
复制完一张新令牌后,我们就可以利用这张新令牌来运行我们指定的进程了
该函数创建一个新进程及其主线程,新进程在指定令牌的安全上下文中运行。我们直接指定前面复制出来的新令牌,使用该令牌创建我们指定的进程即可
根据mimikatz的token模块的原理,简单实现了一个demo,也有许多token相关的工具如incognito等
当获取管理员权限后,我们可以列出系统中进程对应的token:
然后窃取指定进程的token来运行我们的程序,如直接运行上线马
如果想要拿回程序输出的话,则可以通过管道等进程间通信的方法来回显输出
如果拿到一台机器有域管的进程,那么我们可以直接窃取域管进程的token来进行DCSync攻击
https://docs.microsoft.com/
https://github.com/gentilkiwi/mimikatz/
https://www.ired.team/offensive-security/privilege-escalation/t1134-access-token-manipulation
https://www.slideshare.net/JustinBui5/understanding-windows-access-token-manipulation
https://posts.specterops.io/understanding-and-defending-against-access-token-theft-finding-alternatives-to-winlogon-exe-80696c8a73b
BOOL
GetTokenInformation(
HANDLE TokenHandle,
TOKEN_INFORMATION_CLASS TokenInformationClass,
LPVOID TokenInformation,
DWORD TokenInformationLength,
PDWORD ReturnLength
);
BOOL
GetTokenInformation(
HANDLE TokenHandle,
TOKEN_INFORMATION_CLASS TokenInformationClass,
LPVOID TokenInformation,
DWORD TokenInformationLength,
PDWORD ReturnLength
);
typedef enum _TOKEN_INFORMATION_CLASS {
TokenUser,
TokenGroups,
TokenPrivileges,
TokenOwner,
TokenPrimaryGroup,
TokenDefaultDacl,
TokenSource,
...
} TOKEN_INFORMATION_CLASS,
*
PTOKEN_INFORMATION_CLASS;
typedef enum _TOKEN_INFORMATION_CLASS {
TokenUser,
TokenGroups,
TokenPrivileges,
TokenOwner,
TokenPrimaryGroup,
TokenDefaultDacl,
TokenSource,
...
} TOKEN_INFORMATION_CLASS,
*
PTOKEN_INFORMATION_CLASS;
if
(!GetTokenInformation(hToken, TokenSessionId, &sessionId, sizeof(TokenSessionId), &dwSize))
{
wprintf(L
"[!] GetTokenInformation error: %u\n"
, GetLastError());
}
wprintf(L
"\t%-21s: %u\n"
, L
"Session ID"
, sessionId);
if
(!GetTokenInformation(hToken, TokenSessionId, &sessionId, sizeof(TokenSessionId), &dwSize))
{
wprintf(L
"[!] GetTokenInformation error: %u\n"
, GetLastError());
}
wprintf(L
"\t%-21s: %u\n"
, L
"Session ID"
, sessionId);
__kernel_entry NTSTATUS NtQuerySystemInformation(
SYSTEM_INFORMATION_CLASS SystemInformationClass,
PVOID SystemInformation,
ULONG SystemInformationLength,
PULONG ReturnLength
);
__kernel_entry NTSTATUS NtQuerySystemInformation(
SYSTEM_INFORMATION_CLASS SystemInformationClass,
PVOID SystemInformation,
ULONG SystemInformationLength,
PULONG ReturnLength
);
PSYSTEM_PROCESS_INFORMATION pProcessInfo
=
NULL;
DWORD flag
=
TRUE;
pProcessInfo
=
(PSYSTEM_PROCESS_INFORMATION)malloc(dwSize);
ntReturn
=
NtQuerySystemInformation(SystemProcessInformation, pProcessInfo, dwSize, &dwSize);
while
(ntReturn
=
=
STATUS_INFO_LENGTH_MISMATCH) {
free(pProcessInfo);
pProcessInfo
=
(PSYSTEM_PROCESS_INFORMATION)malloc(dwSize);
ntReturn
=
NtQuerySystemInformation(SystemProcessInformation, pProcessInfo, dwSize, &dwSize);
}
while
(flag)
{
if
(pProcessInfo
-
>NextEntryOffset
=
=
0
)
flag
=
FALSE;
wprintf(L
"%-15d"
, (DWORD)pProcessInfo
-
>UniqueProcessId);
wprintf(L
"%-50s"
, (wchar_t
*
)pProcessInfo
-
>ImageName.
Buffer
);
pProcessInfo
=
(PSYSTEM_PROCESS_INFORMATION)((BYTE
*
)pProcessInfo
+
pProcessInfo
-
>NextEntryOffset);
}
PSYSTEM_PROCESS_INFORMATION pProcessInfo
=
NULL;
DWORD flag
=
TRUE;
pProcessInfo
=
(PSYSTEM_PROCESS_INFORMATION)malloc(dwSize);
ntReturn
=
NtQuerySystemInformation(SystemProcessInformation, pProcessInfo, dwSize, &dwSize);
while
(ntReturn
=
=
STATUS_INFO_LENGTH_MISMATCH) {
free(pProcessInfo);
pProcessInfo
=
(PSYSTEM_PROCESS_INFORMATION)malloc(dwSize);
ntReturn
=
NtQuerySystemInformation(SystemProcessInformation, pProcessInfo, dwSize, &dwSize);
}
while
(flag)
{
if
(pProcessInfo
-
>NextEntryOffset
=
=
0
)
flag
=
FALSE;
wprintf(L
"%-15d"
, (DWORD)pProcessInfo
-
>UniqueProcessId);
wprintf(L
"%-50s"
, (wchar_t
*
)pProcessInfo
-
>ImageName.
Buffer
);
pProcessInfo
=
(PSYSTEM_PROCESS_INFORMATION)((BYTE
*
)pProcessInfo
+
pProcessInfo
-
>NextEntryOffset);
}
BOOL
CreateProcessAsUserA(
HANDLE hToken,
LPCSTR lpApplicationName,
LPSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL
bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCSTR lpCurrentDirectory,
LPSTARTUPINFOA lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
);
BOOL
CreateProcessAsUserA(
HANDLE hToken,
LPCSTR lpApplicationName,
LPSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL
bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCSTR lpCurrentDirectory,
LPSTARTUPINFOA lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
);
if
(CreatePipe(&hStdoutR, &hStdoutW, &saAttr,
0
))
{
SetHandleInformation(hStdoutR, HANDLE_FLAG_INHERIT,
0
);
si.cb
=
sizeof(STARTUPINFO);
si.hStdOutput
=
hStdoutW;
si.hStdError
=
si.hStdOutput;
si.dwFlags |
=
STARTF_USESTDHANDLES;
if
(CreateProcessWithTokenW(hDupToken, LOGON_WITH_PROFILE, NULL, cmd, CREATE_NO_WINDOW | CREATE_UNICODE_ENVIRONMENT, NULL, NULL, &si, &pi))
{
CloseHandle(si.hStdOutput);
si.hStdOutput
=
si.hStdError
=
NULL;
while
(ReadFile(hStdoutR, resultBuf, sizeof(resultBuf), &dwRead, NULL) && dwRead)
{
for
(i
=
0
; i < dwRead; i
+
+
)
wprintf(L
"%c"
, resultBuf[i]);
}
WaitForSingleObject(pi.hProcess, INFINITE);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
}
else
wprintf(L
"CreateProcessWithTokenW error 0x%08X\n"
, GetLastError());
}
else
wprintf(L
"CreatePipe error! 0x%08X\n"
, GetLastError());
if
(CreatePipe(&hStdoutR, &hStdoutW, &saAttr,
0
))
{
SetHandleInformation(hStdoutR, HANDLE_FLAG_INHERIT,
0
);
si.cb
=
sizeof(STARTUPINFO);
si.hStdOutput
=
hStdoutW;
si.hStdError
=
si.hStdOutput;
si.dwFlags |
=
STARTF_USESTDHANDLES;
if
(CreateProcessWithTokenW(hDupToken, LOGON_WITH_PROFILE, NULL, cmd, CREATE_NO_WINDOW | CREATE_UNICODE_ENVIRONMENT, NULL, NULL, &si, &pi))
{
CloseHandle(si.hStdOutput);
si.hStdOutput
=
si.hStdError
=
NULL;
while
(ReadFile(hStdoutR, resultBuf, sizeof(resultBuf), &dwRead, NULL) && dwRead)
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课