ТУТОРИАЛ ДЛЯ ЗАДАНИЯ NICO ДЛЯ EKOPARTY 2018 - ЧАСТЬ 1
Давайте отреверсим шаг за шагом задание NICO для EKOPARTY 2018. Это сервер скомпилированный 64-битным компилятором и работающий конечно на WINDOWS.
Для начала я посмотрю на него в WINDOWS 7. В любом случае, часть статического реверсинга будет похожей.
При запуске мы видим следующее.
![]()
Описание находится здесь.
https://labs.bluefrostsecurity.de/blog/2018/09/11/bfs-ekoparty-2018-exploitation-challenge/
![]()
Резервные запасы уменьшаются и вы должны остановить это, затем, в качестве второй цели, вы должны запустить калькулятор. Мы открываем исполняемый файл в IDA64, чтобы проанализировать его.
В окне строк мы ищем TOTAL RESERVES и получаем два результата.
![]()
Давайте посмотрим, где они используются.
![]()
И нажав клавишу X мы можем увидеть ссылки.
![]()
![]()
Здесь мы видим цикл со счетчиком. Когда он достигает нуля, программа переходит к TOTAL RESERVES : U$0, а если он больше нуля, программа переходит налево, чтобы вывести сумму, иначе программа идет туда, где находится строка THE CAPITAL FLIGHT HAS STOPPED.
Здесь мы видим десятичное значение 50000, которым инициализируется переменная CONTADOR_GUITA.
![]()
Здесь программа копирует переменную CONTADOR_GUITA в другую переменную.
![]()
Я переименую её.
![]()
Мы видим также, что после функций SPRINTF, которые создают строку для печати в памяти, программа переходит к CALL, который наверняка будет тем вызовом, который печатает строку.
![]()
Есть две переменные 130 и 134, которые передаются в качестве аргумента, и третья переменная, которая передается через регистр R8, которая является указателем на строку, которую я создал для печати.
![]()
Здесь мы видим начало функции и как переменная инициализируется константой 0x10 и остальные строки читают переменную, значение больше не изменяется внутри функции.
![]()
С переменной 134 происходит то же самое, поэтому мы переименуем сейчас их в CONST_0x10 и CONST_0x18.
![]()
В 64х битных приложениях, если мы хотим, чтобы имена аргументов распространялись в родительскую функцию, мы должны установить тип с помощью SET TYPE в адресе функции.
Делаем правый щелчок и выбираем SET TYPE или клавишу Y. Мы можем определить функцию как USERCALL, так как вызов FASTCALL позволяет нам только устанавливать регистры в качестве аргументов функции.
![]()
__INT64 __USERCALL A_IMPRIMIR@<RAX>(INT CONST_0X10@<ECX>, INT CONST_0X18@<EDX>, CHAR *DEST@<R8>);
Мы видим, что программа изменила функцию, которая была __FASTCALL на USERCALL. Тип возвращаемого значения, я оставляю равным __INT64. Я добавляю после нового имени A_IMPRIMIR @<EAX>, что является регистром, в которой будет возвращать возвращаемое значение. Оно должно быть равно @<RAX>, но я уже сделал это, и это не влияет на анализ, так как программа не возвращает полезные значения только для печати, а затем три аргумента:
INT CONST_0X10@<ECX>
INT CONST_0X18@<EDX>
CHAR *DEST@<R8>
Два целых числа и указатель на строку DEST.
Если в родительской функции все в порядке, должны появиться имена аргументов.
![]()
Мы видим, что в некоторых вызовах функции A_IMPRIMIR программа добавляет к постоянным переменным 0x10 и 0x18 значения перед вызовом, как в случае зеленого блока, который увеличивает регистр ECX и вычитает 4 из регистра EAX перед вызовом. Также, пока не станет ясно, что это значение мы не будем его переименовывать.
Мы также видим, что резервирование пространства для локальных переменных выполняется с помощью инструкции SUB RSP, 48.
![]()
Программа сохраняет в стеке значения аргументов через регистры ECX, EDX и R8 в пространство, зарезервированное для родительской функцией, поверх ее локальных переменных. Я резервирую еще 4 QWORDS для передачи аргументов, и, поскольку они находятся ниже адреса возврата, они ведут себя как если аргументы были бы переданы через стек.
![]()
Здесь есть адрес возврата. Ниже как всегда находятся аргументы, а выше переменные. Все пространство под адресом возврата, где я сохраняю аргументы, было зарезервировано родительской функцией через её инструкцию SUB RSP, XXX Для этого я добавил больше места, чем нужно для локальных переменных.
Если мы добавим опцию указателя стека.
![]()
Мы видим, что стек не изменяется. Нет ни PUSHа ни POPа, и вход и выход из функции не были изменены.
![]()
Мы видим, что это функции, относящиеся к RSP, не сохраняется регистр RBP в любое время, и все отсчитывается относительно RSP + XXX вместо RBP + XXX.
Мы видим, что щелкнув правой кнопкой мыши по одному из этих трех аргументов, который расположен ниже адреса возврата, мы подтверждаем, что это RSP+18h. (они находятся ниже адреса возврата).
![]()
Таким образом, отсюда это похоже на известную функцию. Аргументы ниже R и переменные выше. Регистр RBP не сохраняется, потому что это всё относительно RSP.
![]()
Мы видим, что аргументы CONST_0x10 и 0x18 являются частью структуры, которую обнаружила IDA.
![]()
Структура имеет тип COORD, а переменная этого типа называется DWWRITECOORD.
В статическом представлении стека.
![]()
Мы можем дважды щелкнуть на COORD. Это приведет нас к определению.
![]()
Размер структуры равен 4 байта, и у неё есть два поля: WORD X и Y.
И в LOCAL TYPES также есть определение.
![]()
Т.е. теперь мы можем правильно переименовать аргументы.
Теперь смотрится красивее.
![]()
Мы знаем, что это не будет важной частью упражнения, но мы собираемся сделать всё это подробно.
Затем передаётся указатель на строку в функцию STRLEN, чтобы найти ее длину и сохранить ее в NLENGTH.
![]()
Затем вызывается функция GETSTDHANDLE, чтобы получить дескриптор стандартного устройства, которое может быть одним из трех в списке. (-10, -11 или -12 в зависимости от того, является ли оно вводом, выводом или ошибкой)
![]()
Также в IDA при правом щелчке и выборе пункта - USE STANDARD SYMBOLIC CONSTANT показывает в возможном списке значения, поэтому мы выбрали его оттуда.
![]()
В регистре RAX программа возвращает дескриптор HCONSOLEOUTPUT.
![]()
Это первый аргумент функции WRITECONSOLEOUTPUTCHARACTER. Справка поясняет, что функция копирует символы из буфера в выходные данные консоли.
![]()
Как мы уже видели, первый аргумент передается через регистр RCX и является дескриптором HCONSOLEOUTPUT.
Второй находится в регистре RDX и является указателем на буфер для печати.
![]()
Третий через регистр R8D - это количество символов для печати NLENGTH.
Четвертый аргумент это структура COORD. Здесь программа показывает, что это поле X, но поскольку оно является первым полем, оно совпадает с началом того же поля, и при чтении DWORD читает 4 байта одного и того же поля, т.е. оба поля.
И последний аргумент передается стеком, поскольку он является пятым и является указателем на переменную, которая получит количество напечатанных байтов.
После выхода программа восстанавливает стек, который создала инструкция SUB RSP, 0x48 в начале. Теперь программа возвращает его в ноль с помощью инструкции ADD RSP, 0x48.
![]()
Хорошо. Эта функция уже завершена. Мы видим, что то, что вы добавляете в некоторых вызовах переменных X и Y начальных значений 0x10 и 0x18, - это запись в другую позицию.
Возвращаясь к основной функции, мы видим, что есть глобальная переменная, которая, если мы наведем курсор мыши, мы увидим, что она инициализирована 1. Если бы она была равна нулю, программа перенесла бы нас в зеленые блоки, где она не уменьшит значение счетчика, и выведет THE CAPITAL FLIGHT HAS STOPPED.
![]()
Существует значение 1, которое изначально имеет глобальная переменную.
![]()
Мы переименуем переменную в FLAG_FUGA, потому что, если она равна 1 т.е. если она истинна, то запасы уменьшаются, а если она равна нулю, то запасы восстанавливаются.
![]()
Мы также видим, что, если мы нажимаем X в указанной глобальной переменной, нет никакой ссылки на LEGAL, где она должна быть установлена в ноль. Проблема состоит в том, чтобы увидеть, как это сделать.
![]()
Здесь мы видим, что переменная находится в секции данных, что делает её доступным для записи, и мы увидим, как это сделать.
![]()
А сейчас давайте реверсить функцию STARTADDRESS. Мы видим, что она не использует аргументы, так как первое, что она делает, это SUB RSP, 158. Мы помним, что если у нее есть аргументы, она сохраняет их в стеке, прежде чем резервировать место для переменных.
![]()
Также, если мы нажимаем X на имени, чтобы увидеть ссылки.
![]()
Мы видим, что это поток, созданный в основной функции. Мы закончим его полный анализ до этой функции STARTADDRESS, а затем продолжим здесь.
![]()
Затем есть переменная, которая, сохраняется здесь. Я называю ее CONST_1, также переименуем CONST_0x10 и 0x18 в имя COORD_X и Y.
![]()
Мы видим, что эта переменная CONST_1 lo que hace es que una vez que ya se detuvo запасы уменьшаются так как это цикл, который будет продолжать исполняться, измениться на нуль и не будет бесконечно повторять печать THE CAPITAL FLIGTH HAS STOPPED.
![]()
Таким образом, мы можем изменить имя на FLAG_IMPRIMIR_STOP.
![]()
Мы помним что в IDA есть префиксы.
https://www.hex-rays.com/products/ida/support/idadoc/609.shtml
![]()
Эти префиксы, за которыми следуют подчеркивание (как OFF_) и затем адрес, эквивалентны скобкам [], а OFF указывает мне тип значения который находится в скобках.
Это было бы эквивалентно.
MOV RCX, [0x14000D088]
За исключением того, что программа добавляет, что содержимым является смещение.
![]()
Таким образом, вы должны увидеть содержимое, которое будет помещено в регистр RCX.
![]()
Здесь мы это видим. По адресу 0x14000D088 добавляется префикс OFF_, поскольку его содержимое является смещением или указателем. В этом случае его значение равно 0x14000D000, содержимое которого является строкой ASC, поэтому этот адрес имеет префикс ASC_ впереди.
Т.е., проще говоря, у нас есть строка, и этот другой адрес хранит смещение или её адрес.
![]()
Теперь смотрится лучше. Переименуйте глобальную переменную в строку со звездочками как STRING_EN_DATA, а другая сохраняет ее смещение или адрес.
Здесь также, прежде чем резервировать место в стеке для переменных, сохраните 4 аргумента ниже адреса возврата.
![]()
Регистр RCX был адресом STRING_EN_DATA.
Другие три аргумента являются константами.
![]()
Я изменил эти имена.
![]()
С этим я могу продолжить реверсинг, но если я захочу распространить переменные.
__INT64 __USERCALL SUB_140001580@<RAX>(CHAR *STRING_EN_DATA@<RCX>, INT CONST_0XA@<EDX>, INT CONST_0X18@<R8D>, INT CONST_0X90@<R9D>);
И у меня получается ссылка.
![]()
Я переименовываю функцию, хотя до сих пор не знаю, что она делает, чтобы она была более заметной, когда к ней обращаются из другого места.
Затем у нас есть файл COOKIE. Программа читает их из глобальной переменной в секции данных, которую я переименую.
![]()
И также есть локальная переменная COOKIE.
Перед входом в цикл скопируется адрес строки STRING_EN_DATA в другую переменную и инициализируется счетчик в ноль.
![]()
Затем программа вызывает функцию STRCHR. Она ищет байт 0xA. Функция возвращает указатель на первое вхождение этого символа в строке или ноль, если она не находит его.
![]()
![]()
Мы видим, что строка имеет несколько символов 0xA, другими словами это строка с несколькими строками.
![]()
Поэтому я переименовываю переменную, в которой сохраняется указатель как P_NEXT_LINE, и вижу, что, когда больше не находится 0xA, программа выходит из цикла.
Также мы видим, что STRING_EN_DATA всегда указывает на начало строки и никогда не меняется, поскольку она читает только после инициализации указанной переменной.
![]()
Тем не менее, STRING_EN_DATA_2 в начале аналогична STRING_EN_DATA, но есть доступ на запись к указанной переменной, поэтому она изменит свое значение.
![]()
Мы видим, что перед выходом повторяется цикл.
![]()
Читается указателя на следующую строку увеличивает его, поскольку он указывает на 0xA, чтобы пропустить этот символ и сохранить его в STRING_EN_DATA_2, так что последний в каждом цикле будет увеличиваться, сохраняя указатель, который увеличивается построчно.
Поэтому я переименую его в P_LINEA_STRING_EN_DATA, а другой изменю на P_SIGUIENTE_0XA, поскольку он всегда будет указывать на 0xA, как это выглядит в STRCHR.
![]()
Итак, мы видим, что цикл будет повторять строку за строкой, а поскольку P_LINEA_STRING_EN_DATA всегда указывает на следующую строку, когда строки заканчиваются и больше нет 0xA в строке программа выходит из цикла. Теперь давайте посмотрим, что программа делает в цикле.
![]()
Мы видим, что есть вызов функции STRNCPY. COUNT или количество копируемых байтов происходит из вычитания двух адресов. Из P_LINEA_STRING_EN_DATA и из следующего адреса 0xA. Т.е программа скопировала строку. Поскольку источник - это то же самое P_LINEA_STRING_EN_DATA и назначение это DEST, который является буфером назначения.
![]()
Если мы сделаем правой кнопкой мыши и выберем - ARRAY в DEST в представлении стека.
![]()
Здесь мы видим целевой буфер длиной 256 байт.
![]()
Снова пересчитывается размер строки, вычитая адрес 0xA из следующей строки от её начала и перемещая результат в регистр R9D.
![]()
Мы видим, что счетчик увеличивается при каждом цикле.
![]()
Но также счетчик добавляется к тому, что он читает из переменной CONST_0xA, а затем передает это значение в качестве второго аргумента, поэтому в регистре EDX будет CONST_0XA_MAS_COUNTER.
![]()
Здесь есть 4 аргумента внутри функции.
![]()
Если я хочу распространить переменные с помощью с SET TYPE.
__INT64 __USERCALL SUB_140001580@<RAX>(CHAR * P_DEST@<RCX>, INT CONST_0XA_MAS_CONTADOR@<EDX>, INT CONST_0X18@<R8D>, INT NLENGHT@<R9D>);
И у меня получается ссылка.
![]()
Мы видим, что программа собирается войти в цикл, она инициализирует счетчик в ноль, чтобы отличить его от родительской функции. Я переименую его в COUNTER_LOOP_ACTUAL.
![]()
Этот блок увеличивает счетчик на один внутри цикла.
![]()
И поскольку переменная считается от нуля и увеличивается на один каждый раз, выходной результат равен NLENGHT, т. е. длине строки. (JNB, если результат не ниже, т.е. если он равен или больше)
![]()
Внутри цикла программа берет начальный адрес строки и прибавляет счетчик, т.е. берет первый байт строки и сравнивает его со значением 0x20, чтобы увидеть, является ли символ пробелом.
![]()
Мы помним, что каждая строка состоит из пробелов и звездочек.
В HEX_DUMP я вижу строку. Это 0x20 (пробелы) и 0X2A (звездочки)
![]()
![]()
Если символ является пробелом, программа переходит к зеленому блоку, иначе к другому.
Мы помним, что когда ищем 0xA и сохраняем указатель INC EAX, чтобы пропустить 0xA, поэтому, если символ не является пробелом, он будет звездочкой, поскольку 0xA от начала пропускается путем увеличения указателя.
![]()
Таким образом, мы можем думать, что если это не пробел, то это звездочка.
![]()
Осталось также увидеть, что такое ARG_20, поскольку существует только 4 аргумента, а у дочерней функции только 5, 5тый - это ARG_20.
![]()
Напомним, что в этом компиляторе, родительская функция исполняет SUB RSP, 168 чтобы освободить место для собственных переменных, а также освободить место для аргументов, которым необходимо передать регистры в дочернюю функцию (4QWORDS),
![]()
Если в родительской функции я определяю 4 QWORDS выше зарезервированного пространства, чтобы использовать в качестве аргументов, а следующая функция читает 5 аргументов, 5-й будет локальной переменной родительской функции, которая выше, в этом случае это CONST_0x90_B.
На следующем рисунке я определил больше пространства, чем создала отцовская функция при выполнении SUB RSP, XXX, поверх той, которая ему нужна для локальных переменных, например, 4 QWORDS (VAR_168, VAR_160, VAR_158 и VAR_150)
![]()
Следовательно, мы передали еще один аргумент, который будет переменной родительской функции CONST_0X90.
![]()
Мы также видим, что существует массив слов с именем ATTRIBUTE. Я преобразую его в массив длиной 0x256 слов.
![]()
Мы также видим, что когда это пробел, программа записывает ноль в массив ATTRIBUTE, а когда это звездочка, программа записывает 0x90.
![]()
Другими словами для каждой строки запишется слово 0x00 в пробелах и слово 0x90, где были звездочки.
Мы видим, что программа собирается снова записать в консоль. Программа возвращается, чтобы найти дескриптор OUTPUT. Она передает координаты X и Y и в виде строки для печати передает указатель на ATTRIBUTE,
![]()
Очевидно, что в выходных данных, если я запускаю программа, я вижу, что каждый раз, когда она проходит через функцию WRITECONSOLEOUTPUTATTRIBUTE в каждой строке атрибута, она создает синий рисунок.
![]()
И создают цветную строку.
![]()
![]()
Так что я могу переименовать функцию в DIBUJAR_STRING.
![]()
Хотя я могу уточнить, что в этом вызове подтягивается BCRA.
![]()
Мы также видим, что если вы запустите программа, она поменяет цвет на желтый.
![]()
А чуть ниже на красный.
![]()
Мы видим, что среди аргументов функции WRITECONSOLEOUTPUTATTRIBUTE, атрибуты символов указывают в другое место.
![]()
![]()
![]()
![]()
Хорошо, 0x90 как мы писали в начало, это сумма этих двух констант
#define BACKGROUND_BLUE 0x0010
#define BACKGROUND_INTENSITY 0x0080
Вот почему это дает синий цвет.
Чтобы получить желтый цвет, нужно сочетание красного и зеленого.
#define BACKGROUND_GREEN 0x0020
#define BACKGROUND_RED 0x0040
#define BACKGROUND_INTENSITY 0x0080
![]()
![]()
Здесь мы видим, что программа вызывает все те же аргументы, кроме 0xE0 от Ox90, чтобы изменить цвет на желтый.
И красный цвет получается так
#define BACKGROUND_RED 0x0040
#define BACKGROUND_INTENSITY 0x0080
![]()
Помните, что CONTADOR_GUITA и CONTADOR_GUITA_2 равны в начале цикла
![]()
Далее вызывается GETTICKCOUNT.
Введение в реверсинг с нуля, используя IDA PRO. Часть 65. Часть 1.
Дата публикации 18 апр 2019
| Редактировалось 4 июл 2019