Search code examples
csocketssegmentation-faultshellcodeexperimental-design

Closed system shellcode experimentation (segfault)


I am taking an online software security course. I am attempting to experiment with shellcode. I have written a vulnerable server, an injection program, a (probably broken) shellcode I convert to assembly, that I then strip with a python script. I then compile and run everything with a shell script. I am including all of my files, even though I am pretty sure I am not making the shellcode binary properly.

vulnerable.c

#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>


int main(void) {
    int sd, sdc, addrsize, ret, i;
    struct sockaddr_in addr;
    char exec[1024], buf[128];
    void (*fn)(void);
    const short family = AF_INET;

    fn = (void (*)(void))exec;
    addrsize = sizeof(struct sockaddr_in);
    sd = socket(family, SOCK_STREAM, IPPROTO_TCP);
    if (sd < 0) {
        perror("socket");
        return 1;
    }
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = INADDR_ANY;
    addr.sin_port = htons(8000);
    if (bind(sd, (struct sockaddr *) &addr, 
            sizeof(addr)) < 0) {
        perror("bind");
        return 2;
    }
    listen(sd, 1);
    sdc = accept(sd, (struct sockaddr *)&addr, &addrsize);
    i = 0;
    do {
        i += ret = read(sdc, buf, 128);
        memcpy(exec + i, buf, ret);
    } while (ret > 0);
    close(sd);
    fn();
    return 0;
}

inject.c

#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <errno.h>
#include <stdlib.h>
#include <stdio.h>

int main(void) {
    int sd, fd, ret, buflen = 128;
    struct sockaddr_in addr;
    FILE *f;
    const char fname[] = "shellcode";
    char buf[buflen];
    const short family = AF_INET;
    const char host[] = "127.0.0.1";

    f = fopen(fname, "r");
    fd = fileno(f);

    sd = socket(family, SOCK_STREAM, IPPROTO_TCP);
    if (sd < 0) {
        perror("socket");
        return 1;
    }
    memset(&addr, 0, sizeof(struct sockaddr_in));
    addr.sin_family = family;
    addr.sin_port = htons(8000);
    inet_pton(family, host, &(addr.sin_addr.s_addr));
    ret = connect(sd, (struct sockaddr*)&addr, sizeof(struct sockaddr));
    if (ret < 0) {
        perror("connect");
        return 2;
    }
    do {
        ret = read(fd, buf, buflen);
        if (write(sd, buf, ret) != ret)
            perror("write");
    } while (ret > 0);
    close(fd);
    close(sd);
    return 0;
}

shellcode.c

void shellcode(void) {
    char sz[6] = { 'h', 'e', 'l', 'l', 'o', '\0' };
    write(1, sz, 6);
}

stripshellcode.py

copy = False
outf = open("shellcode-stripped.s", "w")
for line in open("shellcode.s").read().split("\n"):
    if copy:
        outf.write(line + "\n")
    if "shellcode:" in line:
        copy = True
    elif "ret" in line and copy:
        copy = False
outf.close()

make.sh

gcc -S shellcode.c && \
python stripshellcode.py && \
gcc -c shellcode-stripped.s -o shellcode && \
gcc inject.c -o inject && \
gcc vulnerable.c -o vulnerable && \
./vulnerable & ./inject

output

$ sh make.sh
make.sh: line 6: 13905 Segmentation fault      ./vulnerable

Where am I going wrong in this experimental process?

EDIT:

I should note by "closed system" I mean this is being executed in a virtual machine (check the bind address), and also it means some of the variables are hard-coded in a dependent way for brevity and ease.


Solution

  • This doesn't work because shellcode file is an object file. It is an intermediate file format meant for linker consumption. Treating it as a sequence of instructions is plain wrong. Object file contains code, among other things. However this code is incomplete. When a code references a symbol, like write, a placeholder is inserted instead of the address. It is the job of a linker to stitch several object files plus libraries together, to lay out code in memory effectively assigning addresses to symbols and finally patching placeholders.

    Even if an executable file was produced it still wouldn't work because the modern executable formats require a loader to run. Basically a loader reads metadata stored in executable, maps several sections from the file into memory, adjusts things and finally transfers control to an entry point defined by metadata.

    I think there are several working approaches to make a shellcode.

    1. Encode the assembly sequence manually.

    2. Use a tool to do the encoding for you.

    3. Research object file format and extract the code, I assume objdump can do it for you.

    4. Ask a linker to produce executable file in a format that doesn't require a loader to run.

    Reading other answers I realized that the failure actually happens earlier - exec buffer is allocated on stack and stack memory protection attributes explicitly denies execution. This protection could be turned off with say execstack utility on Linux.