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) thatwrite
does get invoked but with the wrong address.