1 引言
提权漏洞(Privilege Escalation)脱离了纯粹的 CTF 竞赛范围,在真实的红队评估与 APT 攻击的后渗透阶段(Post-Exploitation)是至关重要的核心环节。它标志着攻击者从初始访问(Initial Access)向获取系统全面控制权(Total Control)的转变。
2 提权的基本概念与分类
提权是指攻击者利用操作系统、应用程序、服务或系统配置中的安全缺陷,将自身拥有的受限低权限(如普通的 Web 服务进程用户 www-data、访客用户)提升至更高级别权限(如 Linux 的 root 权限或 Windows 的 NT AUTHORITY\SYSTEM / Administrator 权限)的攻击过程。
-
垂直提权 (Vertical Privilege Escalation): 核心在于权限层级的跨越。例如从普通用户
leviathan越权成为超级管理员root。攻击者通常利用内核漏洞(Kernel Exploits)、服务配置不当(如 SUID 错误配置、弱文件权限)或劫持计划任务(Cron Jobs)来实现。 -
水平提权 (Horizontal Privilege Escalation): 核心在于信任边界的横向移动。通常涉及越权访问控制缺陷(IDOR),攻击者在保持当前权限等级不变的情况下,获取同级别普通用户 B 的权限(通常是为了获取 B 目录下的敏感文件、窃取凭证或特定执行权限)。
在二进制漏洞利用中的应用: 在 Pwn 等二进制攻防场景中,提权往往与内存破坏漏洞(如栈溢出、堆溢出、UAF)紧密结合。攻击者通过劫持程序的执行流(控制 EIP/RIP 寄存器),结合 ROP (Return-Oriented Programming) 等技术绕过现代操作系统的内存缓解机制(如 NX/DEP, ASLR, PIE),最终执行恶意的 Payload 以提升当前进程的执行权限。
3 Shellcode 技术原理与实现
Shellcode 是一段用于利用软件漏洞而执行的机器码,通常作为漏洞利用的 Payload 注入到目标进程的内存空间中。其最经典的目的是为攻击者提供一个与操作系统交互的命令行外壳(Shell)。
3.1 Linux Shellcode
在 Linux 环境中,当我们发现具备高权限(例如设置了 SUID 位且属主为 root)的二进制程序存在缓冲区溢出漏洞时,可通过执行自定义的 Shellcode 来获取 root 权限的 Shell。
- 系统调用 (System Calls): Linux Shellcode 的核心在于直接触发内核级的系统调用(例如调用
execve函数启动/bin/sh)。 - 架构差异:
- x86 架构: 通过设置特定的寄存器(如将系统调用号
0x0b存入eax),并通过执行int 0x80软中断指令从用户态切换至内核态。 - x64 架构: 使用
syscall指令触发调用,且系统调用号和传参寄存器(如rdi,rsi,rdx)与 32 位系统完全不同。
- x86 架构: 通过设置特定的寄存器(如将系统调用号
- 字符约束: 为了防止 Shellcode 在程序的字符串处理函数(如
strcpy,strlen)中被异常截断,编写汇编代码时必须严格避免出现空字符 (\x00)。通常需要采用异或操作(XOR)或使用低位寄存器(如使用al代替eax)来进行规避。
3.1.1 X86架构下
在 32 位 Linux 环境中,系统调用的触发依赖于软中断指令 int 0x80。传参机制严格遵循特定的寄存器约定:
- EAX:存储系统调用号(
execve的调用号为11,即0x0b)。 - EBX:存储第一个参数(指向可执行文件路径字符串的指针,如
"/bin/sh")。 - ECX:存储第二个参数(指向参数数组
argv的指针,通常设为NULL)。 - EDX:存储第三个参数(指向环境变量数组
envp的指针,通常设为NULL)。 核心技术难点(消除 Null 字符): 在构造 Payload 时,如果直接使用mov eax, 0x0b或直接在内存中定义带结束符的字符串"/bin/sh\x00",会在生成的机器码中引入大量的\x00字节。这会导致strcpy等存在溢出漏洞的字符串处理函数提前截断 Payload。因此,需要通过寄存器自异或(XOR)和栈操作(Stack Push)来动态构造参数。
标准汇编实现结构:
; 清空寄存器,准备 NULL 字节,同时避免机器码中出现 \x00
xor eax, eax
push eax ; 将 NULL (0x00000000) 压入栈,作为字符串的结束符
; 将 "/bin//sh" 压入栈。使用双斜杠是为了满足 4 字节对齐,不影响路径解析
push 0x68732f2f ; 压入 "n/sh" (小端序: "//sh")
push 0x6e69622f ; 压入 "/bi" (小端序: "/bin")
mov ebx, esp ; EBX 指向栈顶,即字符串 "/bin//sh\x00" 的首地址
; 配置 ECX 和 EDX 为 NULL
xor ecx, ecx ; ECX = 0 (argv = NULL)
xor edx, edx ; EDX = 0 (envp = NULL)
; 配置系统调用号
mov al, 0x0b ; 使用 8 位寄存器 AL 赋值 11,避免 mov eax, 0x0b 产生的空字节
; 触发系统调用
int 0x80
3.1.2 X64架构下
在 64 位 Linux 环境中,系统调用的机制发生了重大变化。中断指令被更高效的 syscall 指令取代,系统调用号和传参寄存器也进行了重构:
- RAX:存储系统调用号(
execve的调用号变更为59,即0x3b)。 - RDI:存储第一个参数(文件路径指针)。
- RSI:存储第二个参数(
argv指针)。 - RDX:存储第三个参数(
envp指针)。
架构差异带来的改变: 64 位架构的寄存器宽度增加到 8 字节(64 bits)。在构造路径字符串时,可以直接将完整的 8 字节字符串放入寄存器然后压栈,极大地简化了指令序列。同时,依然需要利用 xor 等位运算技术来保证机器码的纯净(无 \x00)。
标准汇编实现结构:
; 清空 RSI 和 RDX,配置第二个和第三个参数为 NULL
xor rsi, rsi ; RSI = 0
xor rdx, rdx ; RDX = 0
push rsi ; 将 NULL 压入栈,作为字符串的截断符
; 构造 8 字节对齐的路径字符串 "//bin/sh"
mov rbx, 0x68732f6e69622f2f
push rbx ; 将 "//bin/sh" 压入栈中
mov rdi, rsp ; RDI 指向栈顶,即 "/bin/sh\x00" 的地址
; 配置系统调用号
xor rax, rax ; 清空 RAX
mov al, 0x3b ; RAX = 59 (execve)
; 触发系统调用
syscall
3.2 Windows Shellcode
相比于 Linux 的直接系统调用,Windows 平台的 Shellcode 编写复杂度显著提升。这主要归因于 Windows API 的封装机制以及 ASLR(地址空间布局随机化)的严格限制。
- 动态地址解析: Windows 系统的内核系统调用号(Syscall ID)随操作系统版本的更迭变动频繁,且库函数基址随机化。因此,Shellcode 无法硬编码 API 的内存地址,必须在内存中动态定位关键系统函数。
- PEB 解析技术 (核心步骤):
- 利用段寄存器
FS(32位)或GS(64位)动态定位进程环境块 (PEB, Process Environment Block)。 - 遍历 PEB 中的
PEB_LDR_DATA结构链表,获取当前进程已加载的模块信息。 - 通过链表遍历,精准定位到核心系统库
kernel32.dll的基址。
- 利用段寄存器
- 导出表遍历与哈希匹配: 获取到
kernel32.dll的基址后,进一步解析其 PE 格式的导出表(Export Directory)。为了缩小 Shellcode 体积并规避硬编码字符串,通常采用哈希算法匹配函数名,从而动态获取GetProcAddress和LoadLibraryA的绝对内存地址。 - 执行逻辑: 利用上述两个基础 API 函数,加载后续攻击所需的动态链接库(如
user32.dll),并调用诸如WinExec或CreateProcessA的 API 来弹出一个cmd.exe终端或建立反向连接(Reverse Shell)。