Search code examples
linuxassemblyx86-64system-callsshellcode

Ubuntu 16.04 assembly code for shell


.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


Solution

  • 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.