Arbitrary code execution in objdump -g

Lobsters Hottest News

Summary

A security vulnerability in objdump -g allows arbitrary code execution via a crafted FR30 object file due to a missing bounds check in the FR30 relocation handler, with a single-shot exploit that defeats ASLR and other mitigations.

<p><a href="https://lobste.rs/s/sbcasc/arbitrary_code_execution_objdump_g">Comments</a></p>
Original Article
View Cached Full Text

Cached at: 06/09/26, 01:24 AM

# OOBdump: Relocation Oriented Programming Source: [https://blog.calif.io/p/oobdump-relocation-oriented-programming](https://blog.calif.io/p/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 were saying we should try objdump\. So here we go\. `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\. 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\. 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: That changes when the corresponding relocation section \(`\.rela\.debug\_addr`\) is processed: 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\. 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): 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\. 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](https://substackcdn.com/image/fetch/$s_!0TOW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6678e2c1-1c7e-4cad-a58c-f4200ce562ab_932x884.png)](https://substackcdn.com/image/fetch/$s_!0TOW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6678e2c1-1c7e-4cad-a58c-f4200ce562ab_932x884.png) The exploit clears both obstacles in order\. The on\-disk FR30 relocation offset is 32 bits, but BFD expands it into a 64\-bit`arelent\.address`: 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: [![Wrapping a 64-bit arelent address to reach memory before the buffer](https://substackcdn.com/image/fetch/$s_!6O1t!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6e75d94-a8d2-4609-b877-123e5d9f2755_480x452.gif)](https://substackcdn.com/image/fetch/$s_!6O1t!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6e75d94-a8d2-4609-b877-123e5d9f2755_480x452.gif) 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](https://substackcdn.com/image/fetch/$s_!LkbF!,w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febe3ceba-639f-4643-b3b3-04bc555401c8_760x340.gif)](https://substackcdn.com/image/fetch/$s_!LkbF!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Febe3ceba-639f-4643-b3b3-04bc555401c8_760x340.gif) 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: 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\. 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: 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](https://substackcdn.com/image/fetch/$s_!hlnd!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09e08168-722e-49b0-aeda-a76cd02460f5_1760x680.png)](https://substackcdn.com/image/fetch/$s_!hlnd!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F09e08168-722e-49b0-aeda-a76cd02460f5_1760x680.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](https://substackcdn.com/image/fetch/$s_!EAOg!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F476c14cb-36a8-488f-86d5-4b777d138b9e_1440x460.png)](https://substackcdn.com/image/fetch/$s_!EAOg!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F476c14cb-36a8-488f-86d5-4b777d138b9e_1440x460.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 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: 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\. 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](https://github.com/califio/publications/tree/main/MADBugs/oobdump)\. #### Discussion about this post ### Ready for more?

Similar Articles

Bytecode VMs in surprising places (2024)

Hacker News Top

This article explores surprising uses of bytecode virtual machines, specifically eBPF in the Linux kernel and DWARF expressions for debug information in compiled binaries.

stack unwinding can lead to leakless code execution

Lobsters Hottest

The article discusses how stack unwinding using DWARF bytecode in glibc can be exploited for code execution, illustrated through CTF challenges and analysis of thread cancellation in POSIX systems.

schrodingers-toctou: The binary you run is not the program you wrote

Lobsters Hottest

This research describes compiler-invented loads, where compiler optimizations create additional memory reads not present in source code, turning seemingly secure code into vulnerable binaries with TOCTOU races. It includes audits across kernels, hypervisors, enclaves, and firmware.