Ubuntu 16.04 assembly code for shell

2019-09-11 12:14发布

问题:

.global main
main:
    call func
    .string "/bin/sh"
func:
    push %rsp
    pop %rsi
    pop %rdi
    mov $0x00, %edx
    mov $0x3b, %eax
    syscall

I wrote assembly lagunage like above for execute /bin/sh I compiled it but when I try to execute program, /bin/sh: 0: Can't open ???? this error is occur. It doesn't execute /bin/sh. I want to know why I can't execute /bin/sh

I'm using Ubuntu 16.04 and x64 Architectures

回答1:

Your code is needlessly hard to follow because of using push/pop in a weird way, but running your program under strace -f ./a.out to trace the system calls shows:

... dynamic linker and libc init stuff before main() is called ...
execve("/bin/sh", ["/bin/sh", "\211\307\350\t\222\1", "\367", "\367", 0x100000000, "\350\10"], [/* 0 vars */]) = -1 EFAULT (Bad address)
exit_group(0)                           = ?
+++ exited with 0 +++

So on my system, execve returns with an error, but the program exits successfully. IDK how you're getting /bin/sh: 0: Can't open ????. Your question doesn't contain enough information to reproduce your results. But maybe when you tried, the stack happened to contain different garbage. I built it with gcc -g foo.S.

After main fails to return, execution falls through into whatever CRT function follows main, which does end with a RET instruction. It must also zero eax, since it will be -EFAULT after the SYSCALL.


Anyway, your asm is equivalent to this non-useful code:

int main(void) {
    const char *p = "/bin/sh";
    execve(p, &p, NULL);
}

note that push %rsp; pop %rsi is equivalent to mov %rsp, %rsi. So RSI holds a pointer to the stack memory where CALL wrote the "return address".

One more POP after that dereferences the stack pointer and loads the pointer to the string into RDI.

Using CALL to push the address of a string is really nasty. IDK why you'd do that. Just use MOV like a normal person.


How to do this correctly

# build with gcc -no-pie -g shell.S

#include <asm/unistd.h>         // for __NR_execve                                                                                                                
// #include <sys/syscall.h>     // or include this glibc header for SYS_execve

main:          # main(argc, argv, envp)
    mov   $shell, %edi     # filename = shell
                           # argv     = main's argv, already in rsi (not on the stack like in _start)

    # leave RDX = envp
    # xor   %edx, %edx       # envp     = NULL

    mov   $__NR_execve, %eax    # execve
    syscall                     # execve(shell, argv, envp)
    ret                    # in case the syscall fails

.section .rodata
shell:
.string "/bin/sh"

This works, and doesn't do anything weird.


Or, in a way that works as a position-independent flat binary, and doesn't use main()'s argv, since you added that request:

#include <asm/unistd.h>         // for __NR_execve                                                                                                                
// #include <sys/syscall.h>     // or include this glibc header for SYS_execve

.globl main
main:
    lea   shell(%rip), %rdi     # filename = shell
    xor   %edx, %edx            # envp     = NULL

    push  %rdx                  # or push $0
    push  %rdi
    mov   %rsp, %rsi            # argv     = { $shell, NULL } that we just pushed

    mov   $__NR_execve, %eax    # execve(
    syscall
    ret                    # in case the syscall fails
shell:                     # still part of the .text section, and we don't use the absolute address of the label, only for a RIP-relative LEA.
.string "/bin/sh"

Your RSI=RSP idea is not bad, but you forgot to add a terminating NULL pointer to the end of the argv[] array.

Outside of main which gets envp[] as an arg, we don't have an already-constructed envp[] accessible anywhere convenient, so just pass NULL. On Linux, that's equivalent to passing a valid pointer to an empty NULL-terminated array, i.e. a pointer to an 8-byte 0 in memory.



回答2:

C code:

#define _GNU_SOURCE             /* See feature_test_macros(7) */
#include <stdio.h>
#include <unistd.h>

int main(int argc, char **argv, char **env)
{
  argv[0] = "/bin/bash";
  return execve(argv[0], argv, env);
}

64 bit X86 assembly code:

// gcc shell.S -Wl,--build-id=none -static -nostdlib -fPIC -o shell
// strip -s shell

#include <asm/unistd.h>

        .global _start

        .text
_start:
        mov (%rsp), %rbx        # rbx = argc

        mov     $__NR_execve, %rax             # system call 3b is execve()
        lea     sh(%rip), %rdi     # command to execute
        lea 8(%rsp), %rsi       # argv
#        xor     %rsi, %rsi              # no args
        lea 16(%rsp, %rbx, 8), %rdx # envp !!!
#        xor     %rdx, %rdx              # no envp
        syscall                         # invoke system call

        # exit(0)
        mov     $__NR_exit, %rax               # system call 60 is exit
        xor     %rdi, %rdi              # we want return code 0
        syscall                         # invoke operating system to exit
sh:
        .string  "/bin/bash"

you can invoke the shell as:

./shell

or

./shell -c "echo hello;echo world"

With this code all parameters of the command "shell" will be passed to /bin/bash unchanged.

Also the process name will change and become "bash" (in ps -ef)

And what is more important (which I didn't see anywhere else)

is that with this code the environment is preserved. (no pun intended)

If you don't need the args and you don't want the env preserved, uncomment the comments and comment the previous lines.