neme666
neme666

Reputation: 53

Create an arg array for execve on the stack

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

Answers (3)

Peter Cordes
Peter Cordes

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

fpmurphy
fpmurphy

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

prl
prl

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

Related Questions