前言
网上这方面的资料比较少,很多都是自己总结出来的。目前使用此技术可以在一定程度上挑战现有的安全策略,如给线程添加任意特权,添加任意用户组。请大家理智使用,不要用在危害网络安全的事情上。
由于本人水平有限,文中难免会出现错误,欢迎批评指正。
一、 预备知识
1. 内核中的令牌对象
访问令牌(Access Token)是描述进程或线程安全上下文的对象。令牌中的信息包括与进程或线程关联的用户帐户SID和特权。当用户成功登录后,系统会为用户创建一个访问令牌,用户创建的所有进程都包含了这个令牌的副本(copy)。[本段内容翻译自MSDN]
当线程尝试访问一个对象或执行一个需要特定的特权的系统任务时,系统会检查线程关联的令牌中的信息。具体来说,如果是访问一个对象,内核中的服务例程会根据对象的访问控制列表和访问令牌的用户组决定是否授予访问权限;如果是执行需要特权的系统任务,服务例程则会检查令牌中是否包含并启用所需的特权。
每个进程都有一个主令牌,并且在默认情况下系统在进行访问检查时使用的就是主令牌。线程可以设置一个模拟令牌。
访问令牌包含的信息
1.1 用户安全标识符(SID)、主要组、所有者
令牌中这些结构大小是固定的。
可以分别使用TokenUser、TokenPrimaryGroup、TokenOwner信息类调用GetTokenInformation函数获取这些信息。
结构定义如下:
typedef struct _SID_AND_ATTRIBUTES {
PSID Sid;
DWORD Attributes; //TOKEN_GROUP_* 常量
} SID_AND_ATTRIBUTES, * PSID_AND_ATTRIBUTES;
//用户安全标识符
typedef struct _TOKEN_USER {
SID_AND_ATTRIBUTES User;
} TOKEN_USER, *PTOKEN_USER;
//主要组
typedef struct _TOKEN_PRIMARY_GROUP {
PSID PrimaryGroup;
} TOKEN_PRIMARY_GROUP, *PTOKEN_PRIMARY_GROUP;
//所有者
typedef struct _TOKEN_OWNER {
PSID Owner;
} TOKEN_OWNER, *PTOKEN_OWNER;
1.2 用户组
用户组是令牌的重要信息之一。调整用户组信息可以调用AdjustTokenGroups函数。用户组在创建令牌时指定,创建后不能添加或删除成员,也不能修改属性包含 SE_GROUP_USE_FOR_DENY_ONLY 位的成员。服务例程进行访问检查用的就是用户组结构。
可以使用TokenGroups信息类调用GetTokenInformation函数来获取此信息。
结构定义如下:
typedef struct _TOKEN_GROUPS {
DWORD GroupCount; //成员的个数
SID_AND_ATTRIBUTES Groups[ANYSIZE_ARRAY];
} TOKEN_GROUPS, *PTOKEN_GROUPS;
1.3 特权列表
特权列表是令牌的又一重要信息。调整特权信息可以调用AdjustTokenPrivileges函数。特权列表也是在创建令牌时指定,创建后不能添加新的特权,但是可以移除特权。服务例程就是根据特权列表判断是否具有特权的。
可以使用TokenPrivileges信息类调用GetTokenInformation函数来获取此信息。
结构定义如下:
typedef struct _LUID_AND_ATTRIBUTES {
LUID Luid;
DWORD Attributes; //SE_PRIVILEGE_* 常量
} LUID_AND_ATTRIBUTES, * PLUID_AND_ATTRIBUTES;
typedef struct _TOKEN_PRIVILEGES {
DWORD PrivilegeCount;
LUID_AND_ATTRIBUTES Privileges[ANYSIZE_ARRAY];
} TOKEN_PRIVILEGES, *PTOKEN_PRIVILEGES;
1.4 默认访问控制列表(DACL)
令牌对象的默认访问控制列表,完全可以在创建后修改,因此DACL暂时对我们意义不大。相关内容请参阅MSDN文档。
结构定义如下:
typedef struct _TOKEN_DEFAULT_DACL {
PACL DefaultDacl;
} TOKEN_DEFAULT_DACL, *PTOKEN_DEFAULT_DACL;
1.5 令牌来源
令牌来源用来表示令牌创建者,只读的,定长的。
名称成员可以为用户定义的任意8字节字符,id成员可以调用AllocateLocallyUniqueId函数分配一个全局唯一的LUID。
结构定义如下:
typedef struct _TOKEN_SOURCE {
CHAR SourceName[TOKEN_SOURCE_LENGTH]; //8字节
LUID SourceIdentifier;
} TOKEN_SOURCE, *PTOKEN_SOURCE;
1.6 令牌类型
令牌类型只有两种(目前)。
令牌类型可以使用DuplicateTokenEx函数相互转换。
令牌类型在1.8的统计信息中定义。
定义如下:
typedef enum _TOKEN_TYPE {
TokenPrimary = 1, //主令牌,创建进程时需要此类型的令牌
TokenImpersonation //模拟令牌,线程的令牌时此类型的
} TOKEN_TYPE;
1.7 模拟级别
如果线程有模拟令牌,那么系统在进行访问检查时会根据模拟令牌中的模拟级别决定线程是否允许具有令牌中的特权。值越大表示级别越高,允许的就越多。
模拟级别在1.8的统计信息中定义。
定义如下:
typedef enum _SECURITY_IMPERSONATION_LEVEL {
SecurityAnonymous,
SecurityIdentification,
SecurityImpersonation,
SecurityDelegation
} SECURITY_IMPERSONATION_LEVEL, * PSECURITY_IMPERSONATION_LEVEL;
1.8 统计信息和其他信息
统计信息包含很多令牌的多种数据。
使用TokenStatistics信息类调用GetTokenInformation函数获取此信息。
结构定义如下:
typedef struct _TOKEN_STATISTICS {
LUID TokenId; //令牌的标识符
LUID AuthenticationId; //认证ID,与登录会话有关
LARGE_INTEGER ExpirationTime; //令牌过期时间
TOKEN_TYPE TokenType; //令牌类型
SECURITY_IMPERSONATION_LEVEL ImpersonationLevel; //模拟级别
DWORD DynamicCharged;
DWORD DynamicAvailable;
DWORD GroupCount; //同_TOKEN_GROUPS::GroupCount成员
DWORD PrivilegeCount; //同_TOKEN_PRIVILEGES::PrivilegeCount成员
LUID ModifiedId;
} TOKEN_STATISTICS, *PTOKEN_STATISTICS;
令牌会话ID,类型为DWORD,与会话隔离中的会话是一个概念。
会话ID可以更改,前提是没有使用这个令牌创建进程/模拟线程,或者创建的进程或线程已经终止。
2.访问令牌的已文档化操作
2.1 令牌的打开和关闭
具有访问令牌的对象只有进程和线程。
访问令牌也是一种内核对象,因此它也有访问控制列表,限制其他对象对访问令牌的访问。
打开进程的令牌,进程句柄必须具有PROCESS_QUERY_INFORMATION权限;
打开线程的令牌,线程句柄必须具有THREAD_QUERY_INFORMATION权限。
要打开进程或线程,需使用进程或者线程的ID调用OpenProcess或OpenThread函数。
打开进程令牌的函数原型:
BOOL
WINAPI
OpenProcessToken(
_In_ HANDLE ProcessHandle, //进程句柄
_In_ DWORD DesiredAccess, //访问掩码
_Outptr_ PHANDLE TokenHandle //返回令牌句柄
);
打开线程令牌的函数原型:
BOOL
WINAPI
OpenThreadToken(
_In_ HANDLE ThreadHandle, //线程句柄
_In_ DWORD DesiredAccess, //访问掩码
_In_ BOOL OpenAsSelf, //访问检查是否使用当前进程的令牌
_Outptr_ PHANDLE TokenHandle //返回令牌句柄
);
2.2 令牌信息的检索和修改
令牌中包含的信息非常多,仅信息类就有40+,并且在MSDN上都有详尽的记录。
直接上函数原型:
//检索令牌信息:
BOOL
WINAPI
GetTokenInformation(
_In_ HANDLE TokenHandle, //令牌句柄
_In_ TOKEN_INFORMATION_CLASS TokenInformationClass, //令牌信息类
_Out_writes_bytes_to_opt_(TokenInformationLength,*ReturnLength) LPVOID TokenInformation, //接收信息的缓冲区
_In_ DWORD TokenInformationLength, //缓冲区长度
_Out_ PDWORD ReturnLength //接收需要的缓冲区长度
);
//修改令牌信息:
BOOL
WINAPI
SetTokenInformation(
_In_ HANDLE TokenHandle,
_In_ TOKEN_INFORMATION_CLASS TokenInformationClass,
_In_reads_bytes_(TokenInformationLength) LPVOID TokenInformation,
_In_ DWORD TokenInformationLength
);
对于令牌中的用户组和特权列表,需要使用AdjustTokenGroups和AdjustTokenPrivileges函数修改。
2.3 令牌的分配
2.3.1 分配给进程
一旦进程对象在内核中创建完成便无法替换关联的令牌。因此分配给进程需要在进程创建的边界上进行。即在创建进程时指定要分配的访问令牌。
可以实现这个过程的WIN32API函数:
//从win7开始可用,常用的,需要SE_IMPERSONATE_NAME特权
BOOL
WINAPI
CreateProcessWithTokenW(
_In_ HANDLE hToken,
_In_ DWORD dwLogonFlags, //LOGON_* 常量
_In_opt_ LPCWSTR lpApplicationName,
_Inout_opt_ LPWSTR lpCommandLine,
_In_ DWORD dwCreationFlags,
_In_opt_ LPVOID lpEnvironment,
_In_opt_ LPCWSTR lpCurrentDirectory,
_In_ LPSTARTUPINFOW lpStartupInfo,
_Out_ LPPROCESS_INFORMATION lpProcessInformation
);
//特权要求比较严格,需要SE_INCREASE_QUOTA_NAME特权
//如果使用的令牌不可分配(一般是令牌的用户与当前进程不同),还需要SE_ASSIGNPRIMARYTOKEN_NAME特权,这个特权仅在特定的服务进程中有。
BOOL
WINAPI
CreateProcessAsUserW(
_In_opt_ HANDLE hToken,
_In_opt_ LPCWSTR lpApplicationName,
_Inout_opt_ LPWSTR lpCommandLine,
_In_opt_ LPSECURITY_ATTRIBUTES lpProcessAttributes,
_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
_In_ BOOL bInheritHandles,
_In_ DWORD dwCreationFlags,
_In_opt_ LPVOID lpEnvironment,
_In_opt_ LPCWSTR lpCurrentDirectory,
_In_ LPSTARTUPINFOW lpStartupInfo,
_Out_ LPPROCESS_INFORMATION lpProcessInformation
);
//这是kernel32.dll中导出的一个函数,但是并没有文档化
//需要的特权未知
BOOL
WINAPI
CreateProcessInternalW(
_In_opt_ HANDLE hUserToken,
_In_opt_ LPCWSTR lpApplicationName,
_Inout_opt_ LPWSTR lpCommandLine,
_In_opt_ LPSECURITY_ATTRIBUTES lpProcessAttributes,
_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
_In_ BOOL bInheritHandles,
_In_ DWORD dwCreationFlags,
_In_opt_ LPVOID lpEnvironment,
_In_opt_ LPCWSTR lpCurrentDirectory,
_In_ LPSTARTUPINFOW lpStartupInfo,
_Out_ LPPROCESS_INFORMATION lpProcessInformation,
_Outptr_opt_ PHANDLE hRestrictedUserToken
);
除了上述方法外,还可以使用本地api,如NtCreateProcessEx、NtCreateUserProcess等。
2.3.2 分配给线程
默认情况下线程是没有模拟令牌的,但是可以手动为线程分配一个模拟令牌。
//为调用线程设置令牌可以直接使用这个函数
//如果令牌是主令牌,这个函数会自动转为模拟令牌
BOOL
WINAPI
ImpersonateLoggedOnUser(
_In_ HANDLE hToken
);
//直接调用此函数,方便快捷
//令牌必须为模拟令牌,且具有TOKEN_IMPERSONATE权限
//线程句柄必须具有THREAD_SET_THREAD_TOKEN权限,可能还需要THREAD_SET_INFORMATION权限。
BOOL
APIENTRY
SetThreadToken(
_In_opt_ PHANDLE Thread,
_In_opt_ HANDLE Token
);
//也可以使用ThreadImpersonationToken=5信息类调用这个函数。
//限制条件同上
BOOL
WINAPI
SetThreadInformation(
_In_ HANDLE hThread,
_In_ THREAD_INFORMATION_CLASS ThreadInformationClass,
_In_reads_bytes_(ThreadInformationSize) LPVOID ThreadInformation,
_In_ DWORD ThreadInformationSize
);
二、未文档化的函数: NtCreateToken
1. SE_CREATE_TOKEN_NAME 特权
SE_CREATE_TOKEN_NAME是调用NtCreateToken例程所必需的的特权。根据MSDN文档,用户不能使用“创建令牌对象”策略将此特权添加到用户帐户;也不能使用Windows API将此特权添加到拥有的进程。使用ProcessHacker工具可以确定,正常情况下,系统中具有此特权的进程只有lsass.exe,并且目前lsass.exe并没有受到轻型保护,能够在用户态访问。
获取 SE_CREATE_TOKEN_NAME 特权:
1.1 将代码注入到lsass.exe
优点:
无需处理太多关于令牌的操作,可以直接调用NtCreateToken实现目的
缺点:
调试麻烦
杀毒软件拦截
需要额外编写代码注入代码
取回令牌句柄麻烦,需要进程间通信
1.2 使用lsass.exe的令牌创建新进程
优点:
杀毒软件不会拦截
缺点:
需要处理新进程的相关判断
使用lsass.exe的令牌创建一个进程,新进程将具有 SE_CREATE_TOKEN_NAME 特权。
1.3 直接设置当前线程的模拟令牌
对于NtCreateToken例程,如果调用线程的有效令牌是主令牌,会检查是否具有SE_CREATE_TOKEN_NAME特权;
如果是模拟令牌,则会先检查令牌的模拟级别,如果小于SecurityDelegation则会认为不具有特权;然后检查模拟令牌中是否具有SE_CREATE_TOKEN_NAME特权。
调用OpenProcessToken得到的模拟令牌小于等于SecurityImpersonation,因此需要用SecurityDelegation级别调用DuplicateTokenEx复制一个令牌,然后模拟这个新令牌。
显然,这个方法是最佳选择,它可以让我们执行代码像真实获得了SE_CREATE_TOKEN_NAME特权一样。
2. 调用 NtCreateToken 例程
2.1 函数原型
NTSTATUS
NTAPI
NtCreateToken(
_Out_ PHANDLE TokenHandle, //返回创建的令牌句柄
_In_ ACCESS_MASK DesiredAccess, //一般用 TOKEN_ALL_ACCESS
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_ TOKEN_TYPE TokenType, //令牌类型,一般使用TokenPrimary,然后复制成TokenImpersonation使用,以避免STATUS_BAD_IMPERSONATION_LEVEL错误
_In_ PLUID AuthenticationId, //认证ID
_In_ PLARGE_INTEGER ExpirationTime, //过期时间
_In_ PTOKEN_USER User, //用户安全标识符
_In_ PTOKEN_GROUPS Groups, //用户组
_In_ PTOKEN_PRIVILEGES Privileges, //特权列表
_In_opt_ PTOKEN_OWNER Owner, //所有者
_In_ PTOKEN_PRIMARY_GROUP PrimaryGroup, //主要组
_In_opt_ PTOKEN_DEFAULT_DACL DefaultDacl, //DACL,一般设置nullptr
_In_ PTOKEN_SOURCE TokenSource //令牌源
);
认证ID可以通过令牌统计信息获取,也可以使用下面的常量:
如果使用AllocateLocallyUniqueId函数生成一个不存在的ID可能会发生异常情况。
#define SYSTEM_LUID { 0x3e7, 0x0 }
#define ANONYMOUS_LOGON_LUID { 0x3e6, 0x0 }
#define LOCALSERVICE_LUID { 0x3e5, 0x0 }
#define NETWORKSERVICE_LUID { 0x3e4, 0x0 }
#define IUSER_LUID { 0x3e3, 0x0 }
#define PROTECTED_TO_SYSTEM_LUID { 0x3e2, 0x0 }
过期时间可以自己定义,也可以直接用令牌的统计信息中的过期时间。
2.2 例程失败的原因
NtCreateToken例程返回值可能有以下几种:
其他情况目前还没遇到过。
User、Groups、Owner、PrimaryGroup这四个参数的关系非常复杂,创建时可以参考已存在的令牌中的信息。
另外,ObjectAttributes参数中的SecurityDescriptor成员也可能影响上述四个参数。
2.3 其他问题
特权与用户组中的完整性标签也有相互影响
三、完整代码演示
代码比较长,就不在这里贴了,可以到bb107/WinSudo查看。
代码是一年前写的,风格比较差,思路勉强能看出来,大家凑合着看看吧。
[培训]《安卓高级研修班(网课)》月薪三万计划