Create and Test X86-64 Elf Executable Shellcode on a Linux Machine

ELF - Entry point patching with x86 zero-extended address

IDA isn't decoding it wrong, your hex string version of your machine code is wrong; one \x41 byte short so mov r64, imm64 consumes the following FF byte as part of its immediate, instead of the opcode for jmp. That's why it decodes at 0e e8 loopne`.

I noticed this by copy/pasting your C array into a .c and compiling that into a .o. Then I disassembled it with objdump -D -rwC -Mintel foo.o to get objdump to disassemble the .data section. It agrees with IDA, proving IDA was right and you did make a mistake in whatever you did to translate your NASM output into a hex string. (IDK why you're bothering to do that, instead of just linking with the NASM .o output to test it the normal way first, or what it has to do with modifying an ELF binary.)

 // write syscall + jmp OEP (mov rax, addr, jmp rax). patch at 23
unsigned char shellcode[] = "\x48\x31\xc0\x48\x31\xff\x48\x31\xf6\xeb"
"\x16\x5e\xb0\x01\x40\xb7\x01\xb2\x09\x0f"
"\x05\x48\xb8\x41\x41\x41\x41\x41\x41\x41" // this is only 7 x41 bytes
"\xff\xe0\xe8\xe5\xff\xff\xff\x68\x69\x6a"
"\x61\x63\x6b\x65\x64\x0a";

objdump -D shows 48 b8 41 41 41 41 41 41 41 ff movabs rax,0xff41414141414141 - the most significant byte of your mov imm64 is the FF that's supposed to be the jmp opcode. Your C string only has 7 \x41 bytes.

You should also see the same thing if you disassemble within GDB on the instruction that faulted; it's probably the in instruction which is privileged.


Creating values that contain 0 in registers with shellcode

This part is easy. XOR or ADD some constant like -1 or 0x80 that makes every byte non-zero, then NOT, xor-immediate, or sub-immediate. Or pad with low garbage and right shift.

e.g. to create 3-byte 0x47fe8d in a register, you can do

   mov eax, 0x47fe8d61       ; (0x47fe8d << 8) + 'a'
shr eax, 8

Writing a 32-bit register implicitly zero-extends to 64 bits, so this leaves

RAX = 0 0 0 0 0 47 fe 8d = 0x47fe8d.

Or

    mov eax, ~0x47fe8d          ; none of the bytes are FF -> none of ~x are 0
not eax ; still leaving the upper 32 bits zeroed

How to assemble an ASM file into an x86 shellcode on a Linux 64 bits?

As indicated by @fuz in his comment, the ASM file must contains the [BITS 32] directive to specify the target processor mode. It gives:

; Call to sys_chmod
; eax = 15 (0xf)
; ebx = filepath "/tmp/before"
; ecx = mode: 0777 (0x1ff)
[BITS 32]
xor eax, eax
mov al, 0xf

xor ebx, ebx
push ebx
push dword 0x65726f66
push dword 0x65622f70
push dword 0x6d742f2f
lea ebx, [esp]

mov cx, 0x1ff

int 0x80

nop

By default, the nasm command generates a binary, it is not necessary to provide the -f bin option:

nasm chmod.asm

Then, hexdump can be used to generate the shellcode in the correct format:

$ hexdump -v -e '"\\""x" 1/1 "%02x" ""' chmod
\x31\xc0\xb0\x0f\x31\xdb\x53\x68\x66\x6f\x72\x65\x68\x70\x2f\x62\x65\x68\x2f\x2f\x74\x6d\x8d\x1c\x24\x66\xb9\xff\x01\xcd\x80\x90

What is the smallest x86_64 Hello World ELF binary?

I tried to write the smallest possible x86_64 ELF hello world program by hand

You should provide a source for your program, so we can fix it.

gdb says: During startup program terminated with signal SIGSEGV

This is GDB telling you that it called fork/execve to create the target program, and expected the kernel to notify GDB that the program is now ready to be debugged. Instead, the kernel notified GDB that the program has died with SIGSEGV, without ever reaching its first instruction.

GDB didn't expect that. Why would this happen?

This happens when the kernel looks at your executable, and says "I can't create a running program out of that".

Why is that the case here? Because this LOAD segment:

Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000031 0x0000000000000031 R E 0x2

is asking the kernel to map 0x31 bytes from offset 0 in the file to virtual address 0. But the kernel (rightfully) refuses such nonsense request, and terminates the program with SIGSEGV before returning from execve.

You could probably avoid this by making the file ET_DYN instead of ET_EXEC -- that would change the meaning of your program header from "map this segment at 0" to "map this segment anywhere".

You could definitely avoid this by keeping the ET_EXEC, but changing the .p_vaddr and .p_paddr of the segment to something like 0x10000.

TL;DR: Your program and file headers must make sense to the kernel, or you'll never get off the ground.

How to execute 32-bit shellcode on a 64-bit Linux system?

You have a typo in your push immediate instructions, and the command you are actually trying to execute is //in//sh. As no such file exists, the execve system call fails, which means that it returns. So your program continues executing past the last int 0x80, after which there is only garbage that crashes your program when executed as instructions.

Linux default behavior of executable .data section changed between 5.4 and 5.9?

This is only a guess: I think the culprit is the READ_IMPLIES_EXEC personality that was being set automatically in the absence of a PT_GNU_STACK segment.

In the 5.4 kernel source we can find this piece of code:

SET_PERSONALITY2(loc->elf_ex, &arch_state);
if (elf_read_implies_exec(loc->elf_ex, executable_stack))
current->personality |= READ_IMPLIES_EXEC;

That's the only thing that can transform an RW section into an RWX one. Any other use of PROC_EXEC didn't seem to be changed or relevant to this question, to me.

The executable_stack is set here:

for (i = 0; i < loc->elf_ex.e_phnum; i++, elf_ppnt++)
switch (elf_ppnt->p_type) {
case PT_GNU_STACK:
if (elf_ppnt->p_flags & PF_X)
executable_stack = EXSTACK_ENABLE_X;
else
executable_stack = EXSTACK_DISABLE_X;
break;

But if the PT_GNU_STACK segment is not present, that variable retains its default value:

int executable_stack = EXSTACK_DEFAULT;

Now this workflow is identical in both 5.4 and the latest kernel source, what changed is the definition of elf_read_implies_exec:

Linux 5.4:

/*
* An executable for which elf_read_implies_exec() returns TRUE will
* have the READ_IMPLIES_EXEC personality flag set automatically.
*/
#define elf_read_implies_exec(ex, executable_stack) \
(executable_stack != EXSTACK_DISABLE_X)

Latest Linux:

/*
* An executable for which elf_read_implies_exec() returns TRUE will
* have the READ_IMPLIES_EXEC personality flag set automatically.
*
* The decision process for determining the results are:
*
* CPU: | lacks NX* | has NX, ia32 | has NX, x86_64 |
* ELF: | | | |
* ---------------------|------------|------------------|----------------|
* missing PT_GNU_STACK | exec-all | exec-all | exec-none |
* PT_GNU_STACK == RWX | exec-stack | exec-stack | exec-stack |
* PT_GNU_STACK == RW | exec-none | exec-none | exec-none |
*
* exec-all : all PROT_READ user mappings are executable, except when
* backed by files on a noexec-filesystem.
* exec-none : only PROT_EXEC user mappings are executable.
* exec-stack: only the stack and PROT_EXEC user mappings are executable.
*
* *this column has no architectural effect: NX markings are ignored by
* hardware, but may have behavioral effects when "wants X" collides with
* "cannot be X" constraints in memory permission flags, as in
* https://lkml.kernel.org/r/20190418055759.GA3155@mellanox.com
*
*/
#define elf_read_implies_exec(ex, executable_stack) \
(mmap_is_ia32() && executable_stack == EXSTACK_DEFAULT)

Note how in the 5.4 version the elf_read_implies_exec returned a true value if the stack was not explicitly marked as not executable (via the PT_GNU_STACK segment).

In the latest source, the check is now more defensive: the elf_read_implies_exec is true only on 32-bit executable, in the case where no PT_GNU_STACK segment was found in the ELF binary.

I assembled your program, linked it, and found no PT_GNU_STACK segment, so this may be the reason.

If this is indeed the issue and if I followed the code correctly, if you set the stack as not executable in the binary, its data section should not be mapped executable anymore (not even on Linux 5.4).



Related Topics



Leave a reply



Submit