scriptod911/R_WEIRD

Abusing ld.so

C

0

4 commits

updated Sep 17, 2026

See the code

README

How much can you push the idea of computation without code? It is an interesting question, it really is. And it's also the foundation for a really interesting concept in software obfuscation that is also quite old, which is weird machines.

Infact, I think this was first mentioned in 2013 in the paper “Weird Machines” in ELF: A Spotlight on the Underappreciated Metadata (Dartmouth link). But do note that the original general concept of weird machines came from LangSec in 2009-2010, the 2013 paper was specfically about applying it to ELF metadata.

Now, the 2013 paper mostly relied on corrupting the loader's internal stack frames and link_map structures, which is an interesting approach but I also thought you can take an ABI-compliant approach to this.

In non-PIE binaries, the base address is fixed at 0x400000, which means the dynamic load bias is literally 0. And according to the AMD64 ABI spec, the calculation for R_X86_64_RELATIVE is just B + A (Base + Addend).

If B is 0, ld.so would evaluate B + A = A, and writes the addend directly to *r_offset, its not even a bug or an exploit, by the ABI rules, ld.so is required to write whatever 8 bytes you put in the addend directly into whatever memory address you target.

So... you pretty much have an in-memory stager before the process starts.

To build a binary that does something while having zero code on disk, you only need three veyr nice ingredients.

Naturally you need a place to write the code, I made a buffer marked RWX named .stage

Second, you need a place to write the data, which I made a buffer for our string (.msgbuf)

And finally, and most importantly, you need to overwrite an entry in .init_array so ld.so runs our stage buffer before going to main

When the dynamic linker loads the binary, it will process our relocations, and then write the shellcode and string into memory, and execute the constructo without touching main.

If you open the binary in binja, you can see that main looks completly incorrect.

main

As you can see, main does nothing. An amatuer might think it's a dummy program at first, but if you do run it then:

~/Documents/R_WEIRD$ ./weird
R_WEIRD: ld.so ran me!
~/Documents/R_WEIRD$ echo $?
42

If you look at the memory view in Binja, you can see what ld.so did before main "fired".

stage

Right at 0x4040a0, the string appears. And at '0x4040c0 (stage), the raw syscall shellcode was written driectl there.

Now let's take a final look:

~/Documents/R_WEIRD$ readelf -r -W weird
Relocation section '.rela.dyn' at offset 0x4b8 contains 18 entries:
    Offset             Info             Type               Symbol's Value  Symbol's Name + Addend
0000000000403fd8  0000000100000006 R_X86_64_GLOB_DAT      0000000000000000 __libc_start_main@GLIBC_2.34 + 0
0000000000403fe0  0000000300000006 R_X86_64_GLOB_DAT      0000000000000000 __gmon_start__ + 0
00000000004040c0  0000000000000008 R_X86_64_RELATIVE                         1bf00000001b8
00000000004040c8  0000000000000008 R_X86_64_RELATIVE                         -30ca72b80000
00000000004040d0  0000000000000008 R_X86_64_RELATIVE                         50f00000017baff
00000000004040d8  0000000000000008 R_X86_64_RELATIVE                         2abf0000003cb8
00000000004040e0  0000000000000008 R_X86_64_RELATIVE                         c3050f0000
00000000004040a0  0000000000000008 R_X86_64_RELATIVE                         3a44524945575f52
00000000004040a8  0000000000000008 R_X86_64_RELATIVE                         72206f732e646c20
00000000004040b0  0000000000000008 R_X86_64_RELATIVE                         a21656d206e61
0000000000403e38  0000000000000008 R_X86_64_RELATIVE                         4040c0
0000000000404068  0000000200000001 R_X86_64_64            0000000000000000 printf@GLIBC_2.2.5 + 9
0000000000404070  0000000200000001 R_X86_64_64            0000000000000000 printf@GLIBC_2.2.5 + a
0000000000404078  0000000200000001 R_X86_64_64            0000000000000000 printf@GLIBC_2.2.5 + b
0000000000404080  0000000200000001 R_X86_64_64            0000000000000000 printf@GLIBC_2.2.5 + c
0000000000404088  0000000200000001 R_X86_64_64            0000000000000000 printf@GLIBC_2.2.5 + d
0000000000404090  0000000200000001 R_X86_64_64            0000000000000000 printf@GLIBC_2.2.5 + e
0000000000404098  0000000200000001 R_X86_64_64            0000000000000000 printf@GLIBC_2.2.5 + f

You can see our data sitting in the addend column, as for example, because x86-64 is little endian, the bytes are stored in reverse order, meaning the integer 0x3a44524945575f52 is written into memory as "R_WEIRD:". Oh and do note that the printf entries at the bottom were just empty slots I padded to make room for the relocations.

Please also note that this is nothing more than a parlor trick and shouldn't be really used. You can build with make.

Contributors

scriptod911

4 commits

scriptod911/R_WEIRD

Abusing ld.so

C

0

4 commits

updated Sep 17, 2026

See the code

README

How much can you push the idea of computation without code? It is an interesting question, it really is. And it's also the foundation for a really interesting concept in software obfuscation that is also quite old, which is weird machines.

Infact, I think this was first mentioned in 2013 in the paper “Weird Machines” in ELF: A Spotlight on the Underappreciated Metadata (Dartmouth link). But do note that the original general concept of weird machines came from LangSec in 2009-2010, the 2013 paper was specfically about applying it to ELF metadata.

Now, the 2013 paper mostly relied on corrupting the loader's internal stack frames and link_map structures, which is an interesting approach but I also thought you can take an ABI-compliant approach to this.

In non-PIE binaries, the base address is fixed at 0x400000, which means the dynamic load bias is literally 0. And according to the AMD64 ABI spec, the calculation for R_X86_64_RELATIVE is just B + A (Base + Addend).

If B is 0, ld.so would evaluate B + A = A, and writes the addend directly to *r_offset, its not even a bug or an exploit, by the ABI rules, ld.so is required to write whatever 8 bytes you put in the addend directly into whatever memory address you target.

So... you pretty much have an in-memory stager before the process starts.

To build a binary that does something while having zero code on disk, you only need three veyr nice ingredients.

Naturally you need a place to write the code, I made a buffer marked RWX named .stage

Second, you need a place to write the data, which I made a buffer for our string (.msgbuf)

And finally, and most importantly, you need to overwrite an entry in .init_array so ld.so runs our stage buffer before going to main

When the dynamic linker loads the binary, it will process our relocations, and then write the shellcode and string into memory, and execute the constructo without touching main.

If you open the binary in binja, you can see that main looks completly incorrect.

main

As you can see, main does nothing. An amatuer might think it's a dummy program at first, but if you do run it then:

~/Documents/R_WEIRD$ ./weird
R_WEIRD: ld.so ran me!
~/Documents/R_WEIRD$ echo $?
42

If you look at the memory view in Binja, you can see what ld.so did before main "fired".

stage

Right at 0x4040a0, the string appears. And at '0x4040c0 (stage), the raw syscall shellcode was written driectl there.

Now let's take a final look:

~/Documents/R_WEIRD$ readelf -r -W weird
Relocation section '.rela.dyn' at offset 0x4b8 contains 18 entries:
    Offset             Info             Type               Symbol's Value  Symbol's Name + Addend
0000000000403fd8  0000000100000006 R_X86_64_GLOB_DAT      0000000000000000 __libc_start_main@GLIBC_2.34 + 0
0000000000403fe0  0000000300000006 R_X86_64_GLOB_DAT      0000000000000000 __gmon_start__ + 0
00000000004040c0  0000000000000008 R_X86_64_RELATIVE                         1bf00000001b8
00000000004040c8  0000000000000008 R_X86_64_RELATIVE                         -30ca72b80000
00000000004040d0  0000000000000008 R_X86_64_RELATIVE                         50f00000017baff
00000000004040d8  0000000000000008 R_X86_64_RELATIVE                         2abf0000003cb8
00000000004040e0  0000000000000008 R_X86_64_RELATIVE                         c3050f0000
00000000004040a0  0000000000000008 R_X86_64_RELATIVE                         3a44524945575f52
00000000004040a8  0000000000000008 R_X86_64_RELATIVE                         72206f732e646c20
00000000004040b0  0000000000000008 R_X86_64_RELATIVE                         a21656d206e61
0000000000403e38  0000000000000008 R_X86_64_RELATIVE                         4040c0
0000000000404068  0000000200000001 R_X86_64_64            0000000000000000 printf@GLIBC_2.2.5 + 9
0000000000404070  0000000200000001 R_X86_64_64            0000000000000000 printf@GLIBC_2.2.5 + a
0000000000404078  0000000200000001 R_X86_64_64            0000000000000000 printf@GLIBC_2.2.5 + b
0000000000404080  0000000200000001 R_X86_64_64            0000000000000000 printf@GLIBC_2.2.5 + c
0000000000404088  0000000200000001 R_X86_64_64            0000000000000000 printf@GLIBC_2.2.5 + d
0000000000404090  0000000200000001 R_X86_64_64            0000000000000000 printf@GLIBC_2.2.5 + e
0000000000404098  0000000200000001 R_X86_64_64            0000000000000000 printf@GLIBC_2.2.5 + f

You can see our data sitting in the addend column, as for example, because x86-64 is little endian, the bytes are stored in reverse order, meaning the integer 0x3a44524945575f52 is written into memory as "R_WEIRD:". Oh and do note that the printf entries at the bottom were just empty slots I padded to make room for the relocations.

Please also note that this is nothing more than a parlor trick and shouldn't be really used. You can build with make.

Contributors

scriptod911

4 commits

Languages

C

95.7%

Makefile

4.3%