1

I've got the following simple assembly code in hello.asm:

section .text:
    global _start

    _start:

    jmp short ender

    starter:

    ; clean registers
    xor rax, rax
    xor rbx, rbx
    xor rcx, rcx
    xor rdx, rdx

    ; writes 'hello\n' to stdout
    mov al, 0x04 ; 4 = syscall number of sys_write
    mov bl, 0x01 ; 1 = file descriptor of stdout
    pop rcx      ; points to 'hello\n'
    mov dl, 0x06 ; writes 6 characters
    int 0x80

    ; exits with status 5
    xor rax, rax
    mov al, 0x01 ; 1 = syscall number of sys_exit
    xor rbx, rbx
    mov bl, 0x05 ; 5 = exit status number
    int 0x80

    ender:

    call starter
    db 'hello',0x0a

Inspired by an article on shellcoding, the assembly code pushes hello\n on the stack, writes it to the standard outputs, then exits with code 5.

I can compile it and run it successfully:

$ nasm -f elf64 hello.asm
$ ld hello.o -o hello
$ ./hello
hello

Using objdump -D hello and a short python script, I can extract the related shellcode string and test it with this simple C code:

#include <stdio.h>
#include <string.h>
#include <unistd.h>

char* shellcode = "\xeb\x21\x48\x31\xc0\x48\x31\xdb\x48\x31\xc9\x48\x31\xd2\xb0\x04\xb3\x01\x59\xb2\x06\xcd\x80\x48\x31\xc0\xb0\x01\x48\x31\xdb\xb3\x05\xcd\x80\xe8\xda\xff\xff\xff\x68\x65\x6c\x6c\x6f\x0a";

void main() {
    (*(void(*)()) shellcode)();
}

The issue

The expected output of ./test-shellcode is hello\n with exit status 5, just like when running the compiled assembly. However, the program simply doesn't output anything, but still exits with a status of 5!

After debugging it with GDB, I observe that the program executes the shellcode as expected. Particularly, this section that is supposed to write hello\n to stdout is executed without errors and the content of rcx is the address of the hello\n string:

   0x5555555546d6:  mov    $0x4,%al
   0x5555555546d8:  mov    $0x1,%bl
   0x5555555546da:  pop    %rcx
   0x5555555546db:  mov    $0x6,%dl
   0x5555555546dd:  int    $0x80

But despite the fact that sys_write is apparently correctly called and is executed without errors, nothing is written to the standard output.

Why?

Michael Petch
  • 46,082
  • 8
  • 107
  • 198
Nino Filiu
  • 16,660
  • 11
  • 54
  • 84
  • What is your platform? – Jabberwocky May 07 '19 at 15:10
  • Linux 4.15.0-48-generic, Ubuntu 51, x86_64 – Nino Filiu May 07 '19 at 15:13
  • 2
    Do not use `int 0x80` in 64-bit code especially when you are passing addresses that are stack based. You need to use the `syscall` instruction and the 64-bit System V ABI Kernel calling convention. `Int 0x80` will only see the bottom 32-bits of the RCX register (ECX). Stack addresses in Linux can't be represented in a 32-bit register so the address `int 0x80` sees for your string isn't where you think it is. – Michael Petch May 07 '19 at 15:44
  • 1
    As for the reason the standalone version works - because in standalone executable the string address that ends up in RCX is part of the `.data` section and in your case the addresses in the `.data` section can be represented in a 32-bit register (ECX). When run from the stack the addresses aren't properly represented by just looking at ECX so `int 0x80` silently fails with a bad address. – Michael Petch May 07 '19 at 15:47
  • 1
    You can get a table of the Linux 64-bit syscall system numbers and their parameters in Ryan Chapman's table. The `syscall` instruction uses different system call numbers than `int 0x80` and parameters are passed in different registers: http://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64 – Michael Petch May 07 '19 at 15:48
  • Your stand-alone version is a static executable so it's loaded in the low 2GB of virtual address space where the addresses fit in 32-bit for `int 0x80` to work. When you inject it into a PIE executable, `sys_write` returns `-EFAULT` because `ecx` isn't a valid address (only RCX is, but you used the 32-bit ABI). – Peter Cordes May 07 '19 at 21:27
  • Also, 64-bit code doesn't need to use jmp/call/pop. You can just jump over your string and use a RIP-relative `lea rsi, [rel msg]`. (Although that's not actually shorter, with LEA being 7 bytes vs. call+pop being 6.) – Peter Cordes May 07 '19 at 21:29

0 Answers0