.-----------------------------------------------------------------------------.
| radare2: Working with not-so-valid x86-64 ELFs |
'-----------------------------------------------------------------------------'
updated: 2025-07-29
Learning objectives:
- How to work with not-so-valid ELF binaries in radare2.
- How to fix the loading address in radare2.
- How to fix the entry point in radare2.
- How to work with raw binaries in radare2.
__ ____ /| /| \'~'/ __---. _ .--'--. /\*/\
(_)'\ | / | / | (o o) -----. | | | | '-. | /(o o)\
| | \ | \ | \./ | '--. |__| | __| '--> (_)
_|_|_ \| \| ELF 32 |_____| |___| ELF 64
In this article, we'll look at how to work with ELF files that don't follow the
standard ELF structure but are still valid Linux executables, using radare2.
Tested on the latest release of radare2:
radare2 5.9.8 33900 @ linux-x86-64
commit: 4eb49d5ad8c99eaecc8850a2f10bad407067c898
Date: 2024-11-19 12:38:30 +0100
What's the problem with ELF loading in radare2? Let's look at an example from
[ref1] ([ref2]). The 'elf64-80_bytes' file has overlapping ELF and Program
headers. When this file is loaded into radare2, it gets confused and
incorrectly identifies it as an x86-32 binary:
$ r2 ./elf64-80_bytes
[0x00000001]> i ~^arch,bits
arch x86
bits 32
If that's the case, where does radare2 map the binary, and what is the entry
point? Since it thinks the binary is 32-bit, it treats 64-bit values as 32-bit
=> effectively shifting and truncating the loading address 'p_vaddr' from
'0x0700000000' to '0x00010000', and the entry point from '0x0700000001' to
'0x1':
[0x00010000]> iS
[Sections]
nth paddr size vaddr vsize perm type name
―――――――――――――――――――――――――――――――――――――――――――――――――――――
0 0x00000000 0x50 0x00010000 0x50 -rwx ---- uphdr
[0x00000001]> f ~entry0
0x00000001 1 entry0
Unfortunately, we can't just run radare2 with an explicit architecture, like
'r2 -a x86 -b 64', and be done with it. radare2 still reads values from the
ELF header. These options affect disassembly (which we also need), but
mismatched headers can still break symbol resolution, relocations, imports, and
so on.
Fortunately, radare2 is flexible enough to let us manually remap the file to
the correct address. But first things first: we should prevent radare2 from
performing any analysis on the file. When we run 'r2' with the '-n' flag
radare2 won't try to parse the ELF header, guess the entry point, and so on.
$ r2 -n ./elf64-80_bytes
[0x00000000]> i
fd 3
file elf64-80_bytes
size 0x50
humansz 80
mode r-x
iorw false
block 0x400
In this mode, it maps the file to address 0, so we need to change that. From
[ref1], we know the binary should be mapped at '0x0700000000', so let's map
it there:
[0x00000000]> om
* 1 fd: 3 +0x00000000 0x00000000 - 0x0000004f r-x
^ ^-- This is where the binary is mapped.
'--- We need this file descriptor to remap the file.
[0x00000000]> om-1
^--- Remove the mapping, but keep the fd open.
[0x00000000]> om 3 0x0000000700000000 $s
^ ^ ^--- and map the entire file
| '-- to this address
map fd 3
[0x00000000]> om
- 1 fd: 3 +0x00000000 0x700000000 - 0x70000004f r-x
We also know that the entry point is at '0x0700000000 + 1', so let's fix that
as well:
[0x00000000]> f entry0=0x0000000700000000 + 1
We're almost done (if we had run 'r2 -n -a x86 -b 64', we'd be done, but
let's say we didn't). However, there might still be a problem with the
assembly. (Without '-n', there's definitely a problem and even with '-n',
issues can arise if the architecture we're running on doesn't match the
binary's architecture.)
[0x00000000]> s entry0
[0x700000001]> pd 3
;-- entry0:
0x700000001 45 inc ebp
0x700000002 4c dec esp
0x700000003 46 inc esi
This is not the code we expect: it shows 32-bit x86 instructions ('4X' are
the "REX" prefixes in x86-64). We can either run radare2 with architecture
specific arguments (as shown above), or manually set the 'asm.arch' and
'asm.bits' options:
[0x700000001]> e asm.arch = x86
[0x700000001]> e asm.bits = 64
[0x700000001]> pd 3
;-- entry0:
0x700000001 454c46b20d mov dl, 0xd ; 13
0x700000006 5e pop rsi
0x700000007 5e pop rsi
Once we repair the entry point, we can even happily run radare2's auto
analysis:
[0x00000001]> aa
INFO: Analyze all flags starting with sym. and entry0 (aa)
INFO: Analyze all functions arguments/locals (afva@@@F)
'aa' is effectively an alias for 'af@@ sym.* ; af@entry0 ; afva', and it's
not that handy when there are no symbols ('af@@ sym.*') or real functions
with arguments ('afva'). So in the end, the only useful command is
'af @ entry0' and that we can just run ourselves:
[0x700000001]> af @ entry0
Now we can use the "print disassemble function" command, 'pdf', and see only
the useful code and no more garbage bytes from the ELF headers.
Here's a slightly modified 'elf64-80_bytes' that jumps between possible code
sections [ref3]:
[0x700000001]> pdf
;-- rip:
┌ 33: entry0 ();
│ 0x700000001 454c46b20d mov dl, 0xd
│ 0x700000006 5e pop rsi
│ 0x700000007 5e pop rsi
│ 0x700000008 b001 mov al, 1
│ 0x70000000a 89c7 mov edi, eax
│ 0x70000000c 0f05 syscall
│ ┌─< 0x70000000e eb04 jmp 0x700000014
..
│ │ ; CODE XREF from entry0 @ 0x70000000e(x)
│ └─> 0x700000014 90 nop
│ 0x700000015 90 nop
│ 0x700000016 eb18 jmp 0x700000030
..
│ ; CODE XREF from entry0 @ 0x700000016(x)
│ 0x700000030 90 nop
│ 0x700000031 90 nop
│ 0x700000032 90 nop
│ 0x700000033 90 nop
│ 0x700000034 eb12 jmp 0x700000048
..
│ ; CODE XREF from entry0 @ 0x700000034(x)
│ 0x700000048 31c0 xor eax, eax
│ 0x70000004a 89c7 mov edi, eax
│ 0x70000004c b03c mov al, 0x3c
└ 0x70000004e 0f05 syscall
This is pretty good, but when we're jumping out of order in the assembly above,
the r2 graph view 'VV' becomes quite useful.
Here's another modified version of 'elf64-80_bytes' where we jump to "random"
positions [ref4]:
[0x700000001]> VV
┌────────────────────┐
│ [0x700000001] │
│ ;-- rip: │
│ 33: entry0 (); │
│ ; 13 │
│ mov dl, 0xd │
│ pop rsi │
│ pop rsi │
│ mov al, 1 │
│ mov edi, eax │
│ syscall │
│ jmp 0x700000030 │
└────────────────────┘
v
│
┌──────────┘
│
┌──────────────────────────────────────────┐
│ 0x700000030 [oc] │
│ ; CODE XREF from entry0 @ 0x70000000e(x) │
│ nop │
│ nop │
│ nop │
│ nop │
│ jmp 0x700000014 │
└──────────────────────────────────────────┘
v
│
│
┌──────────────────────────────────────────┐
│ 0x700000014 [ob] │
│ ; CODE XREF from entry0 @ 0x700000034(x) │
│ nop │
│ nop │
│ jmp 0x700000048 │
└──────────────────────────────────────────┘
v
│
│
┌──────────────────────────────────────────┐
│ 0x700000048 [od] │
│ ; CODE XREF from entry0 @ 0x700000016(x) │
│ xor eax, eax │
│ mov edi, eax │
│ ; '<' │
│ ; 60 │
│ mov al, 0x3c │
│ syscall │
└──────────────────────────────────────────┘
Static analysis of the not-so-valid ELF binaries is much more straightforward
and comfy now.
For completeness' sake, here's an r2 script for quicker analysis:
----------------------------[ small_elf_remap.rr2 ]----------------------------
om-1
om 3 0x0000000700000000 $s
e asm.arch = x86
e asm.bits = 64
f entry0=0x0000000700000000 + 1
s entry0
af
pdf
-------------------------------------------------------------------------------
Here's how to run it:
$ r2 -n -i small_elf.rr2 ./elf64-80_bytes
┌ 23: entry0 ();
│ 0x700000001 454c46b20d mov dl, 0xd ; 13
│ 0x700000006 5e pop rsi
│ 0x700000007 5e pop rsi
│ 0x700000008 b001 mov al, 1
│ 0x70000000a 89c7 mov edi, eax
│ 0x70000000c 0f05 syscall
│ ┌─< 0x70000000e eb38 jmp 0x700000048
...
radare2 is a great open source command line tool. Even though it has many
quirks, I love it. It's excellent when a quick look at a binary is needed.
Unfortunately, working with radare2 is not for the faint of heart.
Have fun and hack on!
[ref1] https://research.h4x.cz/html/2025/2025-08-01--touching_small_elfs.html
[ref2] https://research.h4x.cz/data/2025/elf64-80_bytes.nasm
[ref3] https://research.h4x.cz/data/2025/elf64-80_bytes-jumps.nasm
[ref4] https://research.h4x.cz/data/2025/elf64-80_bytes-out_of_order_jumps.nasm