Home > Software design >  elf aarch64 golfed with sys_write
elf aarch64 golfed with sys_write

Time:11-23

To better understand the ELF format and the ARM aarch64, I'm trying to create my elf binary without compilers, just echoing bytes with bash.

Will can see my effort here: http://www.github.com/glaudiston/elf

I have succeeded in achieving a fully working elf with sys_write and sys_exit syscalls for x64.

But for aarch64, it's not working as I expect it to:

# cat make-elf.sh 
#!/bin/bash
#
# depends on:
# - elf_fn.sh (github.com/glaudiston/elf)
# - base64 (gnu-coreutils)
#

. elf_fn.sh

instructions="";
instructions="${instructions}\nwrite $(echo -en "hello world\n" | base64 -w0)";
instructions="${instructions}\nexit 3";
write_elf elf "${instructions}";

It generates:

$ xxd elf
00000000: 7f45 4c46 0201 0100 0000 0000 0000 0000  .ELF............
00000010: 0200 b700 0100 0000 7800 0100 0000 0000  ........x.......
00000020: 4000 0000 0000 0000 0000 0000 0000 0000  @...............
00000030: 0000 0000 4000 3800 0100 0000 0000 0000  [email protected].........
00000040: 0100 0000 0500 0000 0000 0000 0000 0000  ................
00000050: 0000 0100 0000 0000 0000 0000 0000 0000  ................
00000060: 7800 0000 0000 0000 7800 0000 0000 0000  x.......x.......
00000070: 0000 0000 0000 0000 2000 80d2 010c 0058  ........ ......X
00000080: 8201 80d2 0808 80d2 0100 00d4 6000 80d2  ............`...
00000090: a80b 80d2 0100 00d4 6865 6c6c 6f20 776f  ........hello wo
000000a0: 726c 640a           
$ ./make-elf.sh 0 && ./elf; echo $?
3
$ cat elf | base64 -w0; echo
f0VMRgIBAQAAAAAAAAAAAAIAtwABAAAAeAABAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAEAAOAABAAAAAAAAAAEAAAAFAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAeAAAAAAAAAB4AAAAAAAAAAAAAAAAAAAAIACA0gEMAFiCAYDSCAiA0gEAANRgAIDSqAuA0gEAANRoZWxsbyB3b3JsZAo=

It returns the expected exit code, with no illegal exceptions, but the sys_write call is not printing anything.

Hiding all ELF overhead, we have this:

00000078:                     2000 80d2 010c 0058           ......X
00000080: 8201 80d2 0808 80d2 0100 00d4 6000 80d2  ............`...
00000090: a80b 80d2 0100 00d4 6865 6c6c 6f20 776f  ........hello wo
000000a0: 726c 640a                                rld.

The exit call is working as expected, so I can hide it too:

00000078:                     2000 80d2 010c 0058           ......X
00000080: 8201 80d2 0808 80d2 0100 00d4            ............    
00000090:                     6865 6c6c 6f20 776f          hello wo
000000a0: 726c 640a                                rld.

So we have the data hello world.\n starting at position 98. I am very confused about how to do the sys_write call here. In x64 I can set to the next data address that in this case should be 65688(composed of PH_VADDR_V(65536) ELF_HEADER_SIZE(64) ELF_BODY_SIZE(32) (without DATA_SECTION)")

To the output fd I do set in r0 the value 1 with 2000 80d2

To the data address I am using 010c that is little endian representation of 0c01 this bits: 00001100000 00001 The last 5 bits are the r1 register, used to data address.

Given I only have 11 bits Here I've used the LDR (0058) but I've tried MOV (here 80d2) too. With no success

I've tried any value from 0 to 2048 where it starts to reports Illegal instruction and exit code 132.

I think maybe aarch64 does not allow the same trick I've used in x64 to print data without a labeled data section. I'll work on creating it, but this is just a guess and I really want to understand why this is not printing nothing.

CodePudding user response:

So, your string is at absolute address 0x10098 and you need to get this address into the x1 register.

First of all, LDR is not what you want. It is, as the name suggests, a load (read) from memory. You don't want your instruction to access memory at all, it just wants to put the value 0x10098 into the register.

MOV is closer, which writes an immediate value into the register, but the problem is that the immediate is limited to 16 bits, and you need 17. Because instructions are 32 bits, there are only so many bits available for an immediate. My guess is that you overflowed this and ended up changing opcode bits instead, so you encoded a totally different instruction. (Don't guess at encodings! Look them up. This would have shown you the 16-bit limit.)

For getting arbitrary immediate values into a register, the intended approach is a sequence of MOV/MOVK instructions to write 16 bits at a time. Here you would just need two of them:

   0:   d2801301    mov x1, #0x98                   // #152
   4:   f2a00021    movk    x1, #0x1, lsl #16

Though since we are using a extra word, the address of the string will also shift, so you'd have to adjust accordingly.

However, for addresses in particular, AArch64 provides pc-relative address generation instructions, ADR/ADRP. These let you add an immediate value to the current value of the program counter (i.e. the address of the currently executing instruction) and write the result to a register. As a bonus, they allot more bits for the immediate (though you will no longer need them).

Here we can use ADR. Its opcode is 0 at bit 31, and 10000 at bits 24-28. The destination register is bits 0-4, we want 00001. The immediate gets its low two bits at bits 29-30, and the higher bits at 5-23. The ADR instruction will be at absolute address 0x1007c and we want 0x10098, so the displacement is 0x1c = 0b11100. Thus the encoding we want is

0 00 10000 0000000000000000111 00001 = 0x100000e1

Some general tips:

  • Try writing code with an assembler first, so that you can learn the instruction set and be able to focus on experimenting with what the instructions do, instead of also getting bogged down in how they are encoded. If you want to come back and do the encoding by hand later, fine, but with an assembler you'll also have a way to check your work.

  • Use a debugger to single-step your program. That would have showed you that your LDR was giving you a totally bogus value and might have been a hint that it didn't do what you think it did.

  • Use strace to see what system calls your program makes. That would show you (I think, I didn't test) that write does get invoked but with the wrong address.

  • Related