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
How to Reset Ipython/Jupyter Profile After Upgrade to 'Factory' Defaults
How to Quickly Check If Linux 'Unzip' Is Installed Using Perl
Setting Color Brightness on Linux/Xorg
Is There a Linker Flag to Force It to Load All Shared Libraries at Start Time
Linux - Limit Usb Device Bandwidth
Replace Key:Value from One File in Another File in Shellscript
How to Determine The Ip of The Computer That Connects to Me
Is There Any Reference Implementation of Linux File System Filter Driver
Mod_Perl Can't See Files in /Tmp
How to Specify a Local Bond Interface to Multicast Socket in Haskell
Ssl and Tkinter Not Present on Source Build of Python 3.5.2, Debian Linux
How to Setup Virtualhosts to Point Two Ports on Same Ip to Different Servernames
Converting Linux Svn Repos to Windows Svn Repos