The asm does write RDX (with 0): note the cdq
right before syscall
. The sign bit of EAX=59
is 0, so EDX = 0, and writing EDX zero-extends into RDX.
That's a standard code-golf trick for zeroing EDX/RDX with a 1-byte instruction instead of xor edx,edx
, given a known non-negative EAX.
Linux special-cases NULL argv
or envp
pointers as working like an empty list (pointer to a NULL in memory). See the man page: https://man7.org/linux/man-pages/man2/execve.2.html#NOTES
The man page discourages the practice for C programs because it's not portable to other unixes, but shellcode already isn't, and it saves bytes of machine-code size.
In a _start
in a static executable under Linux, all regs other than RSP will be 0
. (This isn't guaranteed by the x86-64 SysV ABI, it's just the convenient value the kernel chooses to avoid info leaks before entering user-space.) So it would have worked that way even if it did have the bug you thought.
But they also test by putting the machine code bytes into an array in .data
in a C program, and calling it from main()
. This would also happen to work with buggy shellcode that left RDX untouched: compiler-generated code for calling via function-pointer would likely leave RDX unmodified.
On entry to main, EDI=argc, RSI=argv, RDX=envp. So this block of machine code would start with RDX already being a valid pointer to char **envp
! Maybe a bit less of a test than they intended. :P
The 3rd arg to main
being envp
is not specified by POSIX but is widely supported: Is char *envp[] as a third argument to main() portable
x86-64 Linux's system-calling convention is quite similar to its function-calling convention, intentionally so that system-call wrapper functions only need mov r10, rcx
/ mov eax, __NR_...
/ syscall
.
And BTW, bad args to a syscall never result in a segmentation fault (SIGSEGV signal being delivered to your process). Instead, Linux system calls return a -EFAULT
error code when you pass a pointer to unmapped memory, in a case where that's not OK.
(Fun fact: write(1, buf, way_past_end)
will successfully write up to the end of the pages actually mapped starting at the buf
address, and return that length. You only get -EFAULT
if 0 bytes were written to the fd before encountering on in an unreadable page.)