mimikatz是内网渗透中的一大利器,本文是分析学习mimikatz源码的第三篇,主要讨论学习mimikatz中涉及权限提升部分的代码,即token模块和privilege模块
Privilege
这个模块很简单,实际上就使用了一个函数:RtlAdjustPrivilege
,这个函数是MSDN上未公开的一个API,可以一步提权,还是很方便的。正常情况下,获取SE_DEBUG
权限为例,需要经历四步才能完成提权:
- 1、打开目标进程:
OpenProcess()
或GetCurrentProcess()
- 2、获取目标进程Token:
OpenProcessToken()
- 3、获取目标权限的LUID:
LookupPrivilegesValue()
- 4、提升目标进程的权限:
AdjustTokenPrivileges()
大体的代码如下:
HANDLE token;
TOKEN_PRIVILEGES state;
LUID luid;
OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &token);
LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid);
state.PrivilegeCount = 1;
state.Privileges[0].Luid = luid;
state.Privileges[0].Attibutes = SE_PRIVILEGE_ENABLED;
AdjustTokenPrivileges(token, FALSE, &state, NULL, NULL, NULL);
相比之下RtlAdjustPrivilege
只需要一行代码:RtlAdjustPrivilege(20, TRUE, FALSE, NULL);
,只不过这个函数在使用前需要先从ntdll中获取函数地址。
在mimikatz的代码中,还结合了LookupPrivilegeValue()
用于以权限名来提升对应权限,避免所需权限是mimikatz代码中所定义的权限之外的权限,mimikatz代码中所定义的权限如下:
当然,mimikatz将现有的特权id以宏定义的形式定义在kuhl_m_privilege.h
中,只是将常用的权限封装成函数直接调用。其余情况均以特权id或特权名的形式处理。
Token
根据这个模块名大抵可以知道这部分主要是对令牌操作的,常用的可能是token::elevate
这一功能,本质上是复制具有system权限进程的令牌来达到目的,这时候就需要上一节的内容,因为要先开启SE_DEBUG权限才能打开具有system权限的进程(获取进程句柄)。 另外两个功能点token::list
和token::run
大体流程一样,使用同一个函数kuhl_m_token_list_or_elevate
。
kuhl_m_token_whoami
这是token模块中一个查询Token信息的功能点,原理也很简单,就是在打开进程、线程令牌的的时候DesiredAccess
字段传入宏TOKEN_QUERY
,随后只需要解析返回的句柄即可,解析主要用到一个函数,即GetTokenInformation
,在MSDN上的函数声明如下:
BOOL GetTokenInformation(
[in] HANDLE TokenHandle,
[in] TOKEN_INFORMATION_CLASS TokenInformationClass,
[out, optional] LPVOID TokenInformation,
[in] DWORD TokenInformationLength,
[out] PDWORD ReturnLength
);
这里主要关注两个字段,即TokenInformationClass
和TokenInformation
,前者是指定要查询的内容,后者是查询结果。可查询的类型是定义在winnt.h
中的一个枚举,每一个枚举值都会有一个对应的结构体用于保存查询到的结果,以TokenStatistics
为例,对应的结构体为:
typedef struct _TOKEN_STATISTICS {
LUID TokenId;
LUID AuthenticationId;
LARGE_INTEGER ExpirationTime;
TOKEN_TYPE TokenType;
SECURITY_IMPERSONATION_LEVEL ImpersonationLevel;
DWORD DynamicCharged;
DWORD DynamicAvailable;
DWORD GroupCount;
DWORD PrivilegeCount;
LUID ModifiedId;
} TOKEN_STATISTICS, *PTOKEN_STATISTICS;
具体字段的含义可参考MSDN。当然,mimikatz的代码中还用到了其他的查询类型,比如TokenSessionId
, TokenElevationType
等,这里不再一一列举。所以token::whoami
这一功能项其实就是调用GetTokenInformation
这一API完成对各种Token信息的查询。
kuhl_m_token_revert
这一功能点十分简单,仅仅只是调用一个API:
BOOL SetThreadToken(
[in, optional] PHANDLE Thread,
[in, optional] HANDLE Token
);
MSDN的描述大致为这个函数既可以给线程分配模拟令牌也可以让线程停止使用模拟令牌,显然这里是用的后者。从函数参数命名可以看出,Thread
指向目标线程,Token
则是要分配的令牌,mimikatz在实现这一模块功能的时候,将两个参数都设置为NULL
,将Thread
设置为NULL表示将Token分配给调用线程,而Token
设置为NULL则表示停止线程使用模拟令牌。
kuhl_m_token_list_or_elevate
前面也提到,主要的功能点都用的是这个函数,他也算是token模块的主体部分了,关于函数的执行流程粗略画个流程图:
这个函数有一点不太明白,函数后两个参数elevate
,runIt
会影响函数体的执行流程,主要有两个地方:
if(!elevate || !runIt || pData.tokenId || type || pData.pUsername)
{
kprintf(L"Token Id : %u\nUser name : %s\nSID name : ", pData.tokenId, pData.pUsername ? pData.pUsername : L"");
...
if(!elevate || !runIt || pData.tokenId || pData.pSid || pData.pUsername)
kull_m_token_getTokensUnique(kuhl_m_token_list_or_elevate_callback, &pData);
...
上面两个判断只要elevate
,runIt
其中有一个为False就满足条件,但是三处调用这两个参数都至少有一个为False:
换句话说这两个条件必定为真,所以这里的两个判断条件看上去是可有可无的,也或许是作者为后续其他功能所预留,菜鸡属实琢磨不透。
0x1 kull_m_net_getCurrentDomainInfo
回到正题,从流程图出发,在解析完命令行参数之后,第一个调用的函数是kull_m_net_getCurrentDomainInfo
,不过这个调用是有条件的即是否传入了domainadmin
或enterpriseadmin
,显然这里是需要域管理权限的情况。从函数名可以猜到主要是获取域的一些信息,在函数的实现中用到的两个API在MSDN中声明为:
NTSTATUS LsaOpenPolicy(
[in] PLSA_UNICODE_STRING SystemName,
[in] PLSA_OBJECT_ATTRIBUTES ObjectAttributes,
[in] ACCESS_MASK DesiredAccess,
[in, out] PLSA_HANDLE PolicyHandle
);
NTSTATUS LsaQueryInformationPolicy(
[in] LSA_HANDLE PolicyHandle,
[in] POLICY_INFORMATION_CLASS InformationClass,
[out] PVOID *Buffer
);
第一个函数用于打开一个Lsa策略的句柄,需要进程以Administrator的身份调用,返回的句柄为最后一个参数。第三个参数用于指定要访问的权限,也即是打开句柄准备要做的操作,这个操作应当是系统的DACL所允许的操作。
第二个函数就是对打开的句柄操作了,即查询Lsa策略信息,结果存放在第三个参数中,第二个参数用于指定查询的类型,mimikatz此处的代码是PolicyDnsDomainInformation
,关于这一字段,是用于检索Lsa策略的句柄对应的主域的DNS,MSDN也说明了前一个函数LsaOpenPolicy
的DesiredAccess需设置为POLICY_VIEW_LOCAL_INFORMATION
,并且查询的结果由结构体POLICY_DNS_DOMAIN_INFO
描述:
typedef struct _POLICY_DNS_DOMAIN_INFO {
LSA_UNICODE_STRING Name;
LSA_UNICODE_STRING DnsDomainName;
LSA_UNICODE_STRING DnsForestName;
GUID DomainGuid;
PSID Sid;
} POLICY_DNS_DOMAIN_INFO, *PPOLICY_DNS_DOMAIN_INFO;
这里主要是获取域的Sid用于后续复制token。
0x2 kull_m_net_CreateWellKnownSid
这一个函数只用到一个API来创建一个SID:
BOOL CreateWellKnownSid(
[in] WELL_KNOWN_SID_TYPE WellKnownSidType,
[in, optional] PSID DomainSid,
[out, optional] PSID pSid,
[in, out] DWORD *cbSid
);
第一个参数由传入的命令行参数决定:
- 1、
domainadmin
-> WinAccountDomainAdminsSid - 2、
enterpriseadmin
-> WinAccountEnterpriseAdminsSid - 3、
admin
-> WinBuiltinAdministratorsSid - 4、
networkservice
-> WinNetworkServiceSid - 5、
system
-> WinLocalSystemSid
其实就是要创建的sid属于什么样的类型,每种类型的具体解释参见MSDN
函数会根据第二个参数是指向的SID创建SID,参数设置为NULL时会使用本机的SID。
这个API总共调用了两次,第一次用于获取pSid的大小,第二次则是获取pSid。这里获取pSid大小的方式有些巧妙,利用了函数的错误信息来获取大小:
0x3 kull_m_token_getNameDomainFromSID
从函数名可以看出,这个函数用于获取sid对应的用户名以及域名,只用到一个API:
BOOL LookupAccountSidA(
[in, optional] LPCSTR lpSystemName,
[in] PSID Sid,
[out, optional] LPSTR Name,
[in, out] LPDWORD cchName,
[out, optional] LPSTR ReferencedDomainName,
[in, out] LPDWORD cchReferencedDomainName,
[out] PSID_NAME_USE peUse
);
总共调用了两次,第一次Name和ReferencedDomainName均传入NULL,用于获取二者对应的缓冲区大小分别写入cchName和cchReferencedDomainName,第二次调用则获取用户名和域名。前面的三个函数的流程都是很简单的调用相关API获取一些基本数据,用于提权的是函数kull_m_token_getTokensUnique
, 函数主体就是两个回调函数kull_m_token_getTokensUnique_callback
和kuhl_m_token_list_or_elevate_callback
。第一个回调函数是在kull_m_token_getTokensUnique
的函数体中以函数指针的形式通过参数传递给kull_m_token_getTokens
, 而第二个回调函数则是是以函数指针cans的形式通过参数传递给函数kull_m_token_getTokensUnique
,按照调用时间顺序来看,首先调用的是kull_m_token_getTokensUnique_callback
然后循环调用kuhl_m_token_list_or_elevate_callback
。
BOOL kull_m_token_getTokensUnique(PKULL_M_TOKEN_ENUM_CALLBACK callBack, PVOID pvArg)
{
BOOL status = FALSE, mustContinue = TRUE;
KULL_M_TOKEN_LIST list = {0}, *cur, *tmp;
if(status = kull_m_token_getTokens(kull_m_token_getTokensUnique_callback, &list))
{
for(cur = &list; cur && mustContinue; cur = cur->next)
mustContinue = callBack(cur->hToken, cur->ptid, pvArg);
0x4 kull_m_token_getTokensUnique_callback
这个函数被结构体PKULL_M_TOKEN_ENUM_CALLBACK
所描述,函数声明以及结构体定义如下:
typedef BOOL (CALLBACK * PKULL_M_TOKEN_ENUM_CALLBACK) (HANDLE hToken, DWORD ptid, PVOID pvArg);
typedef struct _KULL_M_TOKEN_ENUM_DATA {
PKULL_M_TOKEN_ENUM_CALLBACK callback;
PVOID pvArg;
BOOL mustContinue;
} KULL_M_TOKEN_ENUM_DATA, *PKULL_M_TOKEN_ENUM_DATA;
这个结构体会被传递给另外两个回调函数kull_m_token_getTokens_process_callback
和kull_m_token_getTokens_handles_callback
,新的回调函数分别在kull_m_process_getProcessInformation
和kull_m_handle_getHandlesOfType
中被调用。有点套娃的感觉了,先画个图以免被绕进去:
从流程图看kull_m_process_getProcessInformation
和kull_m_handle_getHandlesOfType
最终都会调用回调函数kull_m_token_getTokensUnique_callback
,这个回调函数本身只是调用了DuplicateHandle
,顾名思义这个API用于复制句柄,复制的句柄和原来的句柄一样,任何对对象的操作都会反映到两个句柄中,在MSDN中的申明为:
BOOL DuplicateHandle(
HANDLE hSourceProcessHandle,
HANDLE hSourceHandle,
HANDLE hTargetProcessHandle,
LPHANDLE lpTargetHandle,
DWORD dwDesiredAccess,
BOOL bInheritHandle,
DWORD dwOptions
);
关于各个参数的含义参见MSDN),这个函数配合NtQuerySystemInformation
可以做反调试,大致原理是检测系统中所有的进程是否包含有活动的调试句柄,详细的代码实现参见System-wide anti-debug technique using NtQuerySystemInformation and DuplicateHandle,此外还可以通过如下方式针对进程本身反调试:
HANDLE hTarget, hNewTarget;
DuplicateHandle((HANDLE)-1, (HANDLE)-1, (HANDLE)-1, &hTarget, 0, 0, DUPLICATE_SAME_ACCESS);
SetHandleInformation(hTarget, HANDLE_FLAG_PROTECT_FROM_CLOSE, HANDLE_FLAG_PROTECT_FROM_CLOSE);
DuplicateHandle((HANDLE)-1, (HANDLE)hTarget, (HANDLE)-1, &hNewTarget, 0, 0, DUPLICATE_CLOSE_SOURCE);
MessageBoxA(NULL, "hello!", "hello", NULL);
正常运行没问题,调试时会抛出异常:
但是此种方式似乎并不会影响正常的调试,在vs中继续调试依旧能够正常执行到后面的MessageBox。此外偶然发现的一篇文章中提到的关于这个函数的另外一种用法:《复制对象句柄DuplicateHandle(文件占坑) 》
文章中的大致思路是复制一个打开文件的句柄到其他进程(explore.exe),这样一来只要explore.exe
没有被关闭,那么这个文件就不能被编辑、删除等。
回到正题,其实归根结底回调函数kull_m_token_getTokensUnique_callback
就是复制具有特定令牌的句柄为后续的提权做准备,可以看到该回调函数的流程中还涉及一个函数kull_m_token_equal
,其功能简单即比较两个令牌句柄,涉及两个API:
- 1、
NtCompareTokens
: 比较两个访问令牌,判断调用AccessCheck时两个令牌是否等效(MSDN) - 2、
GetTokenInformation
: 查询令牌对应的TokenSessionId
所以无论是kull_m_process_getProcessInformation
还是kull_m_handle_getHandlesOfType
最终的操作都是复制令牌句柄,只是二者的功能不一样罢了,而这两者又分别涉及两个回调函数并且这两个回调函数都是被循环调用的,循环调用的目的是维护PKULL_M_TOKEN_LIST
描述的单链表,结构体的定义如下:
typedef struct _KULL_M_TOKEN_LIST {
HANDLE hToken;
DWORD ptid;
struct _KULL_M_TOKEN_LIST *next;
} KULL_M_TOKEN_LIST, *PKULL_M_TOKEN_LIST;
先看看kull_m_process_getProcessInformation
做了什么,这个函数的函数体就涉及两个函数,一是kull_m_process_NtQuerySystemInformation
,根据传入的参数buffer
值为NULL,函数会循环调用NtQuerySystemInformation
,循环调用的原因是不确定查询信息所占缓冲区的大小,每趟循环通过比较函数的返回状态码是否是STATUS_INFO_LENGTH_MISMATCH
来判断申请的缓冲区大小是否足够,不满足就将申请的缓冲区大小翻倍。直至获取到查询的SystemProcessInformation
信息:
这里查询的SystemProcessInformation
,会返回一个SYSTEM_PROCESS_INFORMATION
描述的数组,这个数组中的结构体包含了每个进程的资源使用情况,比如这些进程使用的线程数、句柄数以及分配的内存页数等信息。这个函数还能查询其他五十多个系统信息,具体参见MSDN,关于结构体SYSTEM_PROCESS_INFORMATION
在MSDN中似乎并没有相应的说明,不过mimikatz的代码中给出了其定义:
typedef struct _SYSTEM_PROCESS_INFORMATION {
ULONG NextEntryOffset;
ULONG NumberOfThreads;
LARGE_INTEGER Reserved[3];
LARGE_INTEGER CreateTime;
LARGE_INTEGER UserTime;
LARGE_INTEGER KernelTime;
UNICODE_STRING ImageName;
KPRIORITY BasePriority;
HANDLE UniqueProcessId;
HANDLE ParentProcessId;
ULONG HandleCount;
LPCWSTR Reserved2[2];
ULONG PrivatePageCount;
VM_COUNTERS VirtualMemoryCounters;
IO_COUNTERS IoCounters;
SYSTEM_THREAD Threads[ANYSIZE_ARRAY];
} SYSTEM_PROCESS_INFORMATION, *PSYSTEM_PROCESS_INFORMATION;
到这里其实不难发现,kull_m_process_getProcessInformation
的操作其实就是枚举进程,不过不同于使用CreateToolhelp32Snapshot
枚举进程,mimikatz采用的方法更底层一点。
二是回调函数kull_m_token_getTokens_process_callback
,这个回调函数被循环调用,其实就是处理前面枚举进程的结果,每趟循环处理一个SYSTEM_PROCESS_INFORMATION结构体数组成员:
这里的callBack即回调函数kull_m_token_getTokens_process_callback
,函数所做的操作其实就是尝试从枚举得到的进程的令牌复制到KULL_M_TOKEN_LIST
结构体描述的单链表中(上图中pvArg指向的单链表),此时复制令牌操作进入到kull_m_token_getTokensUnique_callback
函数流程中不比较令牌的分支,即直接复制令牌:
所以归根结底函数kull_m_process_getProcessInformation
的作用就是完成对维护链表PKULL_M_TOKEN_LIST
工作中的初始化工作。
接下来就是函数kull_m_handle_getHandlesOfType
了,这个函数最终也会调用回调函数kull_m_token_getTokensUnique_callback
,不过这次是进入到比较令牌的流程,函数kull_m_token_equal
在前面提到过这里就不再重复,然后接着就是将复制得到的令牌句柄添加到KULL_M_TOKEN_LIST
链尾:
对两个分支中的DuplicateHandle
而言,传入的参数都是一样的,差别就是hToken
的来源不同,kull_m_process_getProcessInformation
是通过查询SYSTEM_PROCESS_INFORMATION
得到的进程信息打开一个可复制的令牌句柄:
而对于kull_m_handle_getHandlesOfType
则是查询SystemHandleInformation
,关于这一查询结果的结构体定义如下:
typedef struct _SYSTEM_HANDLE_INFORMATION
{
DWORD HandleCount;
SYSTEM_HANDLE Handles[ANYSIZE_ARRAY];
} SYSTEM_HANDLE_INFORMATION, *PSYSTEM_HANDLE_INFORMATION;
获取到系统句柄信息之后,由kull_m_handle_getHandlesOfType_callback
处理查询的系统句柄信息,并将得到的令牌句柄传递给kull_m_token_getTokensUnique_callback
,所以两个分支中传递给DuplicateHandle
的hToken
一个是通过进程句柄查询得到的令牌句柄,一个是根据查询得到的系统句柄得到的一个令牌句柄,具体的过程在回调函数kull_m_handle_getHandlesOfType_callback
中体现。
先回到kull_m_handle_getHandlesOfType
函数的执行流程,首先传递给该函数一个回调kull_m_token_getTokens_handles_callback
,然后还需要注意的是传入的KULL_M_TOKEN_ENUM_DATA
,该结构体包含了回调函数kull_m_token_getTokensUnique_callback
以及前面查询系统进程信息后复制得到的句柄链表。函数的函数体流程很简单,就做了两件事情:
- 1、使用结构体
HANDLE_ENUM_DATA
描述传递进来的参数 - 2、调用函数
kull_m_handle_getHandles
并传入回调函数kull_m_handle_getHandlesOfType_callback
和HANDLE_ENUM_DATA
结构体变量
HANDLE_ENUM_DATA
定义如下:
typedef struct _HANDLE_ENUM_DATA
{
PCUNICODE_STRING type;
DWORD dwDesiredAccess;
DWORD dwOptions;
PKULL_M_HANDLE_ENUM_CALLBACK callBack;
PVOID pvArg;
} HANDLE_ENUM_DATA, *PHANDLE_ENUM_DATA;
其中成员callBack指向的是回调函数kull_m_token_getTokens_handles_callback
,pvArg
指向结构体KULL_M_TOKEN_ENUM_DATA
,对于函数kull_m_handle_getHandles
来讲,就是调用kull_m_process_NtQuerySystemInformation
查询系统句柄信息(SystemHandleInformation),并将_HANDLE_ENUM_DATA
描述的数据以及查询得到的系统句柄信息交由回调函数kull_m_handle_getHandlesOfType_callback
处理。
前面说到这个回调函数会根据查询得到最终传递给kull_m_token_getTokensUnique_callback
的令牌句柄,函数的流程也在前面的流程图中有所体现,首先是OpenProcess
,我们知道这个函数是用于获取进程句柄,这里打开的是查询得到的系统令牌句柄对应的进程句柄,并且句柄的访问权限是PROCESS_DUP_HANDLE
,MSDN中描述其为:请求一个用于DuplicateHandle
的复制句柄。这里和前面不同,不是通过打开的进程句柄去查询获取一个可以复制的令牌句柄而是通过查询的系统句柄信息获直接获取一个可以复制的进程句柄。得到复制的句柄之后利用这个句柄通过函数NtQueryObject
查询ObjectTypeInformation
,随后将查询结果中的类型名与Token
比较,确定相同之后进入回调函数kull_m_token_getTokens_handles_callback
的流程,并传入复制得到的句柄和对应的查询得到的系统句柄,传入的系统句柄作用是获取对应进程的PID进而在kull_m_token_getTokensUnique_callback
中复制令牌句柄。
函数kull_m_token_getTokens_handles_callback
所做的事情就是调用函数kull_m_token_getTokensUnique_callback
:
BOOL CALLBACK kull_m_token_getTokens_handles_callback(HANDLE handle, PSYSTEM_HANDLE pSystemHandle, PVOID pvArg)
{
return (((PKULL_M_TOKEN_ENUM_DATA) pvArg)->mustContinue = ((PKULL_M_TOKEN_ENUM_DATA) pvArg)->callback(handle, pSystemHandle->ProcessId, ((PKULL_M_TOKEN_ENUM_DATA) pvArg)->pvArg));
}
上面的流程图整体的流程差不多就是这样,其实函数功能就如其名字一样,是用来获取系统令牌的句柄,得到这些令牌句柄之后的工作就是利用这些句柄进行提权了。
0x5 kuhl_m_token_list_or_elevate_callback
前面也说到,这个函数有两个功能,一是提权即token::elevate
,二是以指定权限执行程序(命令)即token::run
。关于提权,实际上就两个API:
- 1、
DuplicateTokenEx
,复制一个新的访问令牌,可以是主令牌也可以是模拟令牌 - 2、
SetThreadToken
,将复制得到的令牌设置到当前进程
所以提权本身就很简单了,这里列出DuplicateTokenEx
提权时传入的相关参数:
- ·
dwDesiredAccess
-> TOKEN_QUERY | TOKEN_IMPERSONATE - ·
ImpersonationLevel
-> SecurityDelegation或其他(取决于复制的令牌) - ·
TokenType
-> TokenImpersonation
第二个功能即以指定权限执行程序,涉及的API要多一点,不过同样也会先使用DuplicateTokenEx
复制一个令牌,不过此时参数dwDesiredAccess
变成了TOKEN_QUERY | TOKEN_IMPERSONATE | TOKEN_ASSIGN_PRIMARY | TOKEN_DUPLICATE | TOKEN_ADJUST_DEFAULT | TOKEN_ADJUST_SESSIONID
。接下来的流程由函数kull_m_process_run_data
封装,其流程中主要的API也是两个:
- 1、
CreateEnvironmentBlock
,根据传入的令牌句柄即前面DuplicateTokenEx
复制得到的句柄创建一个进程环境块 - 2、
CreateProcessAsUser
, 根据创建的进程环境块以模拟的令牌权限创建新的进程,这个函数也用于session穿透,原理和这里提权大相径庭
其余的API主要用于和以指定权限创建的进程进行交互。先完整表述一下函数的执行流程:
- 1、创建匿名管道用于获取程序输出信息,
CreatePipe
- 2、设置匿名管道属性,
SetHandleInformation
- 3、利用复制得到的令牌创建进程环境块,
CreateEnvironmentBlock
- 4、以指定权限创建新的进程,
CreateProcessAsUser
- 5、获取执行结果(程序输出信息),
ReadFile
- 6、等待进程结束,销毁相关环境,
WaitForSingleObject
…
核心是在函数CreateProcessAsUser
和CreateEnvironmentBlock
,相关的代码讲解网上也有不少。这里关注一下创建匿名管道的函数,它在MSDN上的声明如下:
BOOL CreatePipe(
[out] PHANDLE hReadPipe,
[out] PHANDLE hWritePipe,
[in, optional] LPSECURITY_ATTRIBUTES lpPipeAttributes,
[in] DWORD nSize
);
前两个参数分别是管道的读写句柄指针,第三个参数是指向结构体SECURITY_ATTRIBUTES
的指针。mimikatz的代码中设置新进程的启动参数(STARTUPINFO->hStdOutput
)将新进程的标准输出绑定到写管道句柄,然后通过读管道句柄从管道中读取新进程的输出。这里从管道读数据用的是ReadFile
,在MSDN上也说明了ReadFile的第一个参数是一个指向设备(驱动)的句柄,这个设备(驱动)可以是文件、文件流、物理磁盘、卷、控制台缓冲区、套接字、通信资源、邮件槽或者管道。
然后第二个关注点是WaitForSingleObject
,作用是等带指定的对象(句柄)状态,在mimikatz这里调用该函数就是等待新进程结束,这里第二个参数传入的是INFINITE
,表示无限制等待。
函数kuhl_m_token_list_or_elevate_callback
核心的部分差不多就这些,剩下的主要操作就是从复制得到的令牌中提取一些关键信息,首先是函数GetTokenInformation
,此处查询的是TokenStatistics
,即获取令牌的信息,比如前面提到的DuplicateTokenEx
的ImpersonationLevel
参数,在提权时就需要判断令牌的类型从而确定传入的枚举SECURITY_IMPERSONATION_LEVEL
值。然后就是几个mimikatz封装的函数:
- 1、
kull_m_token_getNameDomainFromToken
- 2、
kull_m_token_getUserFromToken
- 3、
kull_m_token_CheckTokenMembership
- 4、
kuhl_m_token_displayAccount
这几个函数都是使用GetTokenInformation
查询相应的内容并解析获取想要的内容。第一个函数和kull_m_token_getNameDomainFromSID
类似,通过令牌获取SID之后使用LookupAccountSid
查询用户名、域名等信息,第二个函数则是直接通过GetTokenInformation
查询TokenUser
获取用户信息,第三个函数用到了一个新的API,在MSDN中的声明如下:
BOOL CheckTokenMembership(
[in, optional] HANDLE TokenHandle,
[in] PSID SidToCheck,
[out] PBOOL IsMember
);
函数用于查询指定的令牌中是否启用了指定的SID,即检查SidToCheck
是不是在TokenHandle
对应的令牌中。此处传入的令牌如果属于主令牌,则需要通过DuplicateTokenEx
复制得到一个模拟令牌。最后一函数kuhl_m_token_displayAccount
则是‘充分’利用GetTokenInformation
,涉及到的查询类型有:
- 1、
TokenStatistics
- 2、
TokenElevationType
- 3、
TokenGroupsAndPrivileges
- 4、
TokenUser
每种类型都对应有相应的结构体,具体的可参考MSDN
这里查询TokenGroupsAndPrivileges
时还涉及一个API:
BOOL LookupPrivilegeNameA(
[in, optional] LPCSTR lpSystemName,
[in] PLUID lpLuid,
[out, optional] LPSTR lpName,
[in, out] LPDWORD cchName
);
函数通过结构体PTOKEN_GROUPS_AND_PRIVILEGES
的Luid
成员查询其对应的特权名。关于mimikatz提权部分的代码大体就是这些内容,总的来说提权的原理并不复杂,但作者的使用了不少的回调函数,这让代码阅读起来有些绕。
发表评论
您还未登录,请先登录。
登录