You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
On modern distributions the `sigreturn` trampoline is still exported by the **vDSO** page but the exact offset may vary across kernel versions and build flags such as BTI (`+branch-protection`) or PAC. Automating its discovery prevents hard-coding offsets:
198
+
On Linux/**AArch64** the most convenient SROP gadget is usually the **vDSO** trampoline exported as **`__kernel_rt_sigreturn`**. In current kernels the interesting sequence is intentionally tiny:
199
+
200
+
```armasm
201
+
nop // unwinder marker
202
+
__kernel_rt_sigreturn:
203
+
mov x8, #0x8b // __NR_rt_sigreturn
204
+
svc #0
205
+
```
206
+
207
+
Do **not** hard-code an offset such as `0x7b0` unless you already dumped the exact target **vDSO**. The offset varies across kernel builds, so the reliable workflow is: recover the **vDSO** base, dump that mapping, and resolve `__kernel_rt_sigreturn` inside the dump.
Both tools understand **AArch64** encodings and will list candidate `mov x8, 0x8b ; svc #0` sequences that can be used as the *SROP gadget*.
216
+
# 3) Resolve the trampoline
217
+
readelf -Ws vdso | grep rt_sigreturn
218
+
objdump -d vdso | sed -n '/__kernel_rt_sigreturn/,+4p'
219
+
ROPgadget --binary vdso --only 'svc'
220
+
```
209
221
210
-
> Note: When binaries are compiled with **BTI**the first instruction of every valid indirect branch target is `bti c`. `sigreturn` trampolines placed by the linker already include the correct BTI landing pad so the gadget remains usable from unprivileged code.
222
+
This is much more reliable than grepping the main binary because on ARM64 the **SROP entry point usually lives in the vDSO**, not in the target ELF itself.
After sending the frame you can send a second stage containing raw shell-code at `0x400000+0x100`. Because **AArch64** uses *PC-relative* addressing this is often more convenient than building large ROP chains.
227
239
228
-
## Kernel validation, PAC & Shadow-Stacks
240
+
### Quick debugging note for `pwntools` users
241
+
242
+
If you are using the stock **AArch64**`SigreturnFrame()` from modern `pwntools`, the serialized frame is **600 bytes** long. Useful offsets when debugging corrupted payloads are:
243
+
244
+
*`x8` (syscall number): `0x178`
245
+
*`sp`: `0x230`
246
+
*`pc`: `0x238`
247
+
248
+
If your payload reaches the `svc #0` but the kernel rejects the frame, checking these offsets in a hexdump is often faster than single-stepping the whole chain.
249
+
250
+
## Modern kernel parsing and branch-protection caveats
229
251
230
-
Linux 5.16 introduced stricter validation of userspace signal frames (commit `36f5a6c73096`). The kernel now checks:
252
+
Recent **arm64** kernels are stricter than many old SROP writeups assume. When you hand-craft a frame, keep in mind that:
231
253
232
-
*`uc_flags` must contain `UC_FP_XSTATE` when `extra_context` is present.
233
-
* The reserved word in `struct rt_sigframe` must be zero.
234
-
* Every pointer in the *extra_context* record is aligned and points inside the user address space.
254
+
*`sigcontext.__reserved` parsing is **16-byte aligned**.
255
+
* Only **one**`extra_context` record is accepted.
256
+
* If `extra_context` is used, its `datap` pointer must point exactly to the extra data area and the total extra size must stay inside the accepted signal-frame limits.
257
+
*`fpsimd_context` is still **mandatory** on systems that support FPSIMD, even if you only care about controlling `x0-x30`, `sp`, and `pc`.
258
+
* If you include `sve_context`/`za_context`, the kernel validates their sizes and **current vector length**. In practice, `rt_sigreturn` is not a clean way to change VL mid-exploit.
259
+
* On newer kernels there are even more optional records such as **GCS**, **POE**, and **FPMR** contexts, which is another reason to prefer the smallest valid frame possible.
235
260
236
-
`pwntools>=4.10` crafts compliant frames automatically, but if you build them manually make sure to zero‐initialize *reserved*and omit the SVE record unless you really need it—otherwise `rt_sigreturn` will deliver `SIGSEGV` instead of returning.
261
+
Therefore, the most reliable offensive strategy is usually to keep the frame **minimal**: use the default `SigreturnFrame()` layout and avoid adding SVE/SME records unless you explicitly need them.
237
262
238
-
Starting with mainstream Android 14 and Fedora 38, userland is compiled with **PAC** (*Pointer Authentication*) and **BTI** enabled by default (`-mbranch-protection=standard`). *SROP* itself is unaffected because the kernel overwrites `PC` directly from the crafted frame, bypassing the authenticated LR saved on the stack; however, any **subsequent ROP chain** that performs indirect branches must jump to BTI-enabled instructions or PACed addresses. Keep that in mind when choosing gadgets.
263
+
For hardened userlands, also keep in mind:
239
264
240
-
Shadow-Call-Stacks introduced in ARMv8.9 (and already enabled on ChromeOS 1.27+) are a compiler-level mitigation and *do not* interfere with SROP because no return instructions are executed—the flow of control is transferred by the kernel.
265
+
* With **PAC** (`paciasp` / `autiasp`), a plain saved-`x30` overwrite may die before you ever reach the SROP trampoline if the function epilogue authenticates `LR`.
266
+
* With **BTI**, indirect `br` / `blr` targets must land on a valid landing pad. The arm64 **vDSO**`__kernel_rt_sigreturn` trampoline intentionally omits `bti c` because the kernel expects it to be reached from a `ret`. So returning to it is fine, but jumping to it with a BTI-checked branch gadget can fault.
241
267
242
268
## References
243
269
244
-
*[Linux arm64 signal handling documentation](https://docs.kernel.org/arch/arm64/signal.html)
245
-
*[LWN – "AArch64 branch protection comes to GCC and glibc" (2023)](https://lwn.net/Articles/915041/)
0 commit comments