Reputation: 53
I want to write an assembly program that executes via EXECVE (syscall #0x3C) the program /bin/ls with the switches -al.
The man page (man 2 execve) states that the call requires three values:
int execve(const char *filename, char *const argv[], char *const envp[]);
I don't quite understand how to build the three arguments. As far as I know, the first argument goes into RDI
, the second into RSI
, and the third into RDX
. I believe that to set up the first one, it suffices doing
push 0x736c2f2f ;sl//
push 0x6e69622f ;nib/
mov rdi, rsp
For the third one, the thing is quite easy:
xor r11, r11
mov rdx, r11
My problem is that I don't know how to build the second argument, which should be an array containing ['/bin//ls', '-aal']
I need to write it for x86-64, so please no int 0x80
suggestions.
Upvotes: 2
Views: 1979
Reputation: 364039
You can write push '/bin'
in NASM to get the bytes into memory in that order. (Padded with 4 bytes of zeros, for a total width of qword; dword pushes are impossible in 64-bit mode.) No need to mess around with manually encoding ASCII characters; unlike some assemblers NASM doesn't suck at multi-character literals and can make your life easier.
You could use use mov dword [rsp+4], '//ls'
to store the high half. (Or make it a qword
store to write another 4 bytes of zeroes past that, with a mov r/m64, sign_extended_imm32
.) Or just zero-terminate it with an earlier push before doing mov rsi, '/bin//ls'
/ push rsi
if you want to store exactly 8 bytes.
Or mov eax, '//ls'
; shr eax, 8
to get EAX="/ls\0"
in a register ready to store to make an 8-byte 0-terminated string.
Or use the same trick of shifting out a byte after mov r64, imm64
(like in @prl's answer) instead of separate push / mov. Or NOT your literal data so you do mov rax, imm64
/ not rax
/ push rax
, producing zeros in your register without zeros in the machine code. For example:
mov rsi, ~`/bin/ls\0` ; mov rsi, 0xff8c93d091969dd0
not rsi
push rsi ; RSP points to "/bin/ls", 0
If you want to leave the trailing byte implicit, instead of an explicit \0
, you can write mov rsi, ~'/bin/ls'
which assembles to the same mov rsi, 0xff8c93d091969dd0
. Backticks in NASM syntax process C-style escape sequences, unlike single or double quotes. I'd recommend using \0
to remind yourself why you're going to the trouble of using this NOT, and the ~
bitwise-negation assemble-time operator. (In NASM, multi-character literals work as integer constants.)
I believe that to set up the first one, it suffices doing
push 0x736c2f2f ;sl// push 0x6e69622f ;nib/ mov rdi, rsp
No, push 0x736c2f2f
is an 8-byte push, of that value sign-extended to 64-bit. So you've pushed '/bin\0\0\0\0//ls\0\0\0\0'
.
Probably you copied that from 32-bit code where push 0x736c2f2f
is a 4-byte push, but 64-bit code is different.
x86-64 can't encode a 4-byte push
, only 2 or 8 byte operand-size. The standard technique is to push 8 bytes at a time:
mov rdi, '/bin//ls' ; 10-byte mov r64, imm64
push rdi
mov rdi, rsp
If you have an odd number of 4-byte chunks, the first one can be push imm32
, then use 8-byte pairs. If it's not a multiple of 4, and you can't pad with redundant characters like /
, mov dword [mem], imm32
that partially overlaps might help, or put a value in a register and shift to introduce a zero byte.
See
Upvotes: 1
Reputation: 2537
Load the following C example (modify if needed) into the Godbolt compiler explorer and you can see how various compilers typically generate assembly for a call to execve
on the AMD64 (or other) architecture.
#include <stdio.h>
#include <unistd.h>
int
main(void) {
char* argv[] = { "/bin/ls", "-al", NULL };
// char* argv[] = { "-al", NULL };
// char* argv[] = { "/bin/lsxxx", "-al", NULL };
// char* argv[] = { "", "-al", NULL };
char* envp[] = { "PATH=/bin", NULL };
if (execve("/bin/ls", argv, envp) == -1) {
perror("Could not execve");
return 1;
}
}
Upvotes: -1
Reputation: 12435
You can put the argv
array onto the stack and load the address of it into rsi
. The first member of argv
is a pointer to the program name, so we can use the same address that we load into rdi
.
xor edx, edx ; Load NULL to be used both as the third
; parameter to execve as well as
; to push 0 onto the stack later.
push "-aal" ; Put second argument string onto the stack.
mov rax, rsp ; Load the address of the second argument.
mov rcx, "/bin//ls" ; Load the file name string
push rdx ; and place a null character
push rcx ; and the string onto the stack.
mov rdi, rsp ; Load the address of "/bin//ls". This is
; used as both the first member of argv
; and as the first parameter to execve.
; Now create argv.
push rdx ; argv must be terminated by a NULL pointer.
push rax ; Second arg is a pointer to "-aal".
push rdi ; First arg is a pointer to "/bin//ls"
mov rsi, rsp ; Load the address of argv into the second
; parameter to execve.
This also corrects a couple of other problems with the code in the question. It uses an 8-byte push for the file name, since x86-64 doesn't support 4-byte push, and it makes sure that the file name has a null terminator.
This code does use a 64-bit push with a 4-byte immediate to push "-aal" since the string fits in 4 bytes. This also makes it null terminated without needing a null byte in the code.
I used strings with doubled characters as they are in the question to avoid null bytes in the code, but my preference would be this:
mov ecx, "X-al" ; Load second argument string,
shr ecx, 8 ; shift out the dummy character,
push rcx ; and write the string to the stack.
mov rax, rsp ; Load the address of the second argument.
mov rcx, "X/bin/ls" ; Load file name string,
shr rcx, 8 ; shift out the dummy character,
push rcx ; and write the string onto the stack.
Note that the file name string gets a null terminator via the shift, avoiding the extra push. This pattern works with strings where a doubled character wouldn't work, and it can be used with shorter strings, too.
Upvotes: 5