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.

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".

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.
4 commits
C
95.7%
Makefile
4.3%
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.

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".

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.
4 commits
C
95.7%
Makefile
4.3%