Radare2
Вскрываем и изменяем исполняемые файлы при помощи опенсорсного фреймворка
Упражняться мы будем на crackme, которую я написал специально для демонстрации. Скачать файл ты можешь с моего GitHub. Все действия мы будем проводить в Debian Linux.
В статье «Radare2 с самого начала. Учимся использовать опенсорсный фреймворк для анализа приложений в Linux» мы уже начали исследовать этот исполняемый файл. При запуске он открывает сокет и начинает слушать порт 14884. Если подключиться к этому сокету при помощи netcat, то можно увидеть приглашение для ввода имени пользователя или пароля. Если ввести неверный пароль, сеанс завершится.
Используя основные возможности Radare2, мы выявили в программе функции main
, authenticate
, check_username
, check_password
, start_server
и несколько других. В authenticate
есть пять локальных переменных, а также вызов функции с говорящим названием check_username
, которой в качестве единственного аргумента передается значение переменной fd
.
Это краеугольная функция в защитном механизме, поскольку из нее вызываются некоторые другие весьма важные и не всегда относящиеся к процедуре логина функции. На очереди — извлечение пароля. Но даже когда мы раскусим защитный механизм, у нас будет возможность копать этот крякмис в глубину. Мы изменим его код, чтобы выполнение программы шло по той ветви, результаты выполнения которой нам нужны.
Распознание имени пользователя

В функции check_username
дескриптор сокета, переданный в аргументе, помещается в переменную fildes
. Далее с помощью библиотечной функции memset
готовится буфер памяти: src
заполняется нулями, затем в него с помощью функции read
читается пользовательский ввод из сокета, на который указывает fildes
. То есть читается как бы с удаленного устройства.
Дальше в консоль на стороне сервера функция printf
выводит строку [
, потом в нее же с помощью fputs
выливается содержимое буфера src
, содержащего введенное имя пользователя. Далее возвращенный функцией fputs
результат сравнивается с -1
. Если равенство верно, выводится сообщение об ошибке, если же возвращенное значение не равно -1
(ноль или положительное значение), то выполняется переход на строку 0x1605
. Здесь происходит вывод символа конца строки — \
.
После этого готовятся параметры для вызова библиотечной функции strcpy
. Она копирует имя пользователя src
в новую область памяти — dest
. Далее в строке со смещением 0x1628
в переменную var_420h
копируется значение 0x6262616a
. В комментарии рядом Radare2 оставил метку, заключенную в одинарные кавычки, — jabb
:
mov dword [var_420h], 0x6262616a ; 'jabb'
По мнению Radare2, это шестнадцатеричное число — набор букв в кодировке UTF-8, используемой в большинстве дистрибутивов Linux. Доверяй, но проверяй! В командную строку под дизассемблированным листингом функции введи
? 0x6262616a
Ниже появится список, в котором указанное значение будет конвертировано в разные типы данных.

Нас интересует тип string
. Напротив него мы видим строку jabb
, что и требовалось доказать.

Вернемся в функцию check_username
. Строкой ниже (0x1632
) мы видим, что в переменную var_21ch
помещается символ a
, пока непонятно для чего. Снова промотаем листинг к началу функции, где расположены комментарии о переменных:
...
; var int64_t var_41ch @ rbp-0x41c
; var int64_t var_420h @ rbp-0x420
...
Нас интересуют эти две переменные. Теперь посмотрим на код присвоения им значений:
mov dword [var_420h], 0x6262616a ; 'jabb'mov word [var_41ch], 0x61 ; 'a'
Истина где‑то рядом. В UTF-8 символ может занимать от одного до четырех байтов, однако латинские символы, которые мы видим в листинге, никогда не превышают одного байта. Таким образом, значение 0x6262616a
— это четыре байта, что подтверждает размер приемника — двойное слово, dword.
Также вспомним, что в стеке переменные кладутся от старших адресов к младшим (стек растет сверху вниз). Поэтому, чтобы найти размер переменной var_420h
, надо из адреса ее расположения вычесть адрес расположения следующей переменной:
0x420 – 0x41C = 4 байта
Это размер переменной var_420h
, что требовалось подтвердить. Переменные располагаются впритык друг к другу. Скорее всего, в исходном коде на языке высокого уровня дополнительной переменной не существует. Ее для удобства нарисовал Radare2 во время дизассемблирования, потому что для копирования точно пяти байтов способа не существует. Отсюда следует, что в переменной var_420h
находится строка jabba
.

Посмотрим, что творится в функции дальше, начиная со строки со смещением 0x163b
. Здесь готовятся параметры — очевидно, для передачи в функцию: берутся указатели на переменные var_420h
и dest
. Помнишь, что находится в последней? Правильно, копия значения, которое раньше ввел пользователь. И эти две строки передаются в функцию compare_username
.

Эта функция совсем короткая, мы ее разберем в два счета! Аргументы‑строки перекладываются в переменные: введенная пользователем строка — в src
, jabba
— в s2
. При этом для переменных сразу выделяется память с запасом: по восемь байтов, qword.
Далее в консоль на стороне сервера выводится строка «[–] Comparing username». Затем строка src
копируется в область памяти dest
. Этот трюк нужен для снятия со строки модификатора const
. То есть на новую копию строки он не влияет.
После этого строки подготавливаются для передачи в библиотечную функцию strcmp
, которая, как известно, служит для сравнения строк. Она возвращает 0, когда переданные строки одинаковы, 1, когда первая строка больше второй, и -1, когда вторая больше первой. Размерность строк в данном случае зависит от кодов первых отличающихся символов.
После возврата из функции strcmp
с помощью инструкции test
проверяется значение в регистре EAX, в котором возвратился результат. Если это значение не равно нулю, то есть строки не равны, выполняется переход на команду, где регистру EAX (в нем будет возвращен результат вызвавшей функции) присваивается 0. Когда строки равны (в EAX ноль), после test
в регистр EAX помещается единица.
Когда в EAX будет ноль или единица, выполнение программы возвращается в функцию check_username
. И выполнение продолжается со строки 0x1654
, где нас поджидает test
. Здесь все наоборот: если 0 — строки неравны, на серверной консоли выводится [
; если 1, то строки равны, выводится [
.
После вывода сообщения о том, что юзернейм корректный, в регистр EAX записывается единица для возврата и выполняется безусловный переход в конец функции, на инструкции leave
и ret
.
Если юзернейм неверный, после вывода строки «Wrong user» с помощью инструкции CMP
программа сравнивает значение переменной fildes
и ноль. Они никогда не будут равны! Ведь переменная fildes
всегда содержит дескриптор сокета. Если же она содержит ноль, значит, где‑то раньше произошла ошибка и досюда дело не дойдет.
Таким образом, следующее условие (jne
) всегда выполняется, регистру EAX присваивается ноль, и происходит выход из функции, при этом во время выполнения программы никогда не будет вызвана функция secret
. В нее передаются dest
— введенная пользователем строка и fildes
— дескриптор сокета. Код противоречит сам себе: если выполнение программы каким‑то образом дойдет досюда, дескриптор сокета будет нулевой!
Ладно, оставим функцию secret
на десерт, сейчас продолжим заниматься главным блюдом.
Комментируем листинг дизассемблера
Если тебе приходилось работать с IDA Pro, то наверняка при изучении листингов ты оставлял в них комментарии, чтобы потом быстро вспомнить, что происходит в коде. В Radare2 тоже можно создавать комментарии. Для этого используется команда CC
с таким синтаксисом:
CC [комментарий] [@смещение/адрес]
Еще раз обратимся к check_username
, чтобы оставить в ней след — наш комментарий.

В командной строке Radare2 введи команду
CC full username: jabba @0x163b
После этого запроси дизассемблерный листинг функции еще раз:
pdf @ sym.check_username
Ты увидишь изменения.

Мы наконец можем покинуть функцию check_username
и вернуться в authenticate
. На этом месте можешь сделать перерыв: принять душ, погулять с собакой, попить газировку из Черноголовки и, главное, дать мозгам отдых. В нашем деле это очень важно.
Распознавание пароля
Надеюсь, ты хорошо отдохнул и готов с нами выгрызать пароль. Напомню, мы сейчас находимся в функции authenticate
, сразу после вызова check_username
, в строке со смещением 0x181f
. Здесь с помощью инструкции test
проверяется возвращенное значение.
Если в регистре EAX находится ноль (что бывает, когда имя введено неверно), выполняется переход в конец функции, соответственно, authenticate
в этом случае тоже возвращает ноль. Если же в регистре EAX единица, то с помощью инструкции jne
выполняется условный переход на строку 0x182a
. В консоли на стороне клиента выводится строка «Password:», затем вызывается функция check_password
, ей передается дескриптор сокета.

Аргумент перекладывается в переменную fildes
, а прочие инструкции из этой функции похожи на рассмотренные раньше аналоги: подготавливается буфер памяти, в него читаются данные из сокета, а в серверную консоль выводится [
.
Дальше происходит все самое интересное. В переменную s1
помещается содержимое упомянутого буфера. А в переменную s2
, судя по комментариям, оставленным дизассемблером, с двух заходов помещается строка SoloIsMyB*tch
(как мы знаем, Хан Соло и Джабба Хатт в конфликте). Для удобства Radare2 создал дополнительную переменную — var_418h
.
Можно с большой долей вероятности предположить, что это и есть эталонный пароль. Нашли?! Не спеши вперед паровоза. Обращает на себя внимание способ записи данных в переменную, а именно использование инструкции movabs
, которая за один присест целиком копирует 64-битное значение в регистр без вспомогательных средств наподобие служебного слова qword
.
Стоит проверить, что же скрывается под числовыми константами. Введи в командную строку следующее:
? 0x794d73496f6c6f53
...
? 0x6863742a42794d

Проверяем найденную комбинацию
Пришло время вновь запустить server64.
и подключиться к порту 14884 с помощью netcat.

После ввода верного пароля функция authenticate
возвращает в главную функцию (main
) единицу, в результате чего происходит вызов функции send_pw_list
, которая вываливает в консоль на стороне клиента содержимое файла passwords
. Напротив, если authenticate
возвращает ноль, выполнение программы переходит на строчку 0x1a6d
, где вызывается функция reject
. Которая, как мы знаем, сообщает: «Дроиды не те, что ты ищешь».
В итоге защита взломана, пароль найден — отлично!
Динамический анализ
Между тем за нами остался должок — неразгрызенная функция secret
. В нее еще надо как‑то попасть, то есть запустить выполнение. Заодно поупражняемся в динамическом анализе с помощью Radare2.
Чтобы запустить наш сервер под надзором отладчика R2, нужно воспользоваться ключом -d
:
r2 -d server64.elf
Когда R2 запускает программу в отладчике, первая остановка происходит не на точке входа, функции main
или каких‑то других библиотеках. Поэтому, чтобы оказаться в функции main
, надо выполнить команду dcu
— то есть debug continue until main (функцию можно указать любую).
Теперь ход выполнения программы остановится в начале функции main
. Дебажить программу с помощью командной строки негуманно по отношению к самому себе. Radare2 предлагает вполне сносный визуальный режим прямо в консоли. Чтобы его открыть, набери Vpp
.

Смещение той строки в дизассемблере, на которой остановился ход выполнения программы, выделено зеленым цветом. На самом деле, чтобы открыть визуальный режим, достаточно команды V, две буквы P после нее позволяют сразу при входе в этот режим настроить содержание консоли. В противном же случае тебе бы пришлось нажимать P после входа в режим.
Попробуй попереключать содержимое консоли нажатием P, возможно, ты найдешь другой вариант более удобным. Мне же в текущем варианте нравится то, что отображается содержимое регистров процессора.
Нажатие клавиши F7 позволяет сделать шаг вперед на следующую инструкцию с заходом в функцию, если такая будет встречена. F8 же делает шаг вперед, игнорируя встретившуюся функцию. Если вдруг нужно поставить точку останова на строку, где установлен курсор, достаточно нажать F2. Чтобы получить доступ к командной строке, надо набрать двоеточие. А чтобы покинуть визуальный режим, достаточно нажать Q.
Наигравшись с перемещением со строки на строку, установим точку останова на вызов функции authenticate
:
db sym.authenticate
Продолжим выполнение вводом команды dc
. После нажатия Enter курсор перейдет на новую строку, но ничего нового мы не увидим. Ну конечно, программа застряла на брейк‑пойнте. Из другой консоли подключись к серверу, набрав nc
. Покажется приветственный экран, но приложение остановится до приглашения ввести имя пользователя.
При этом в первой консоли, где выполняется отладка сервера, содержимое обновится и будет выведена информация о достижении точки останова. Обрати внимание: первая строка функции помечена буквой b
, что говорит нам об установленном брейк‑пойнте. Затем в этой же консоли мы вновь откроем визуальный режим, нажав V.
Здесь мы тоже можем баловаться перебором строк, однако наша целевая функция — check_username
. Поставим на ее начало точку останова:
db sym.check_username
И продолжим прогон программы — dc
. Вновь Radare2 сообщит о достижении точки останова.
Откроем визуальное отображение. Далее с чувством, с толком, с расстановкой, шаг за шагом, достигнем инструкции cmp
.
Между тем, когда мы дойдем до инструкции call
, продвигаться дальше будет невозможно до тех пор, пока во второй консоли мы не введем username
. После того как произойдет проверка cmp
, неумолимо случится переход jne
, что не позволит программе исполнить функцию secret
.

Можно провести такой прогон еще бесконечное количество раз, однако результат будет тем же. Для решения проблемы нам надо этот переход изменить или вообще обнулить.
Ломаем сервер и меняем его поведение
На этот раз откроем сервер с возможностью модификации двоичного файла. Для этого запустим Radare2 с флагом -w
. Не забудь сделать копию файла на всякий случай, если операция выйдет из‑под контроля!
r2 –w server64.elf
Привычным движением руки запустим анализ: aaa
. Перейдем в функцию check_username
:
s sym.check_username
Отобразим дизассемблерный код этой функции, набрав pdf
. Теперь надо перейти к инструкции, которую мы собираемся заломить, то есть к условному переходу: s
. Смещение в начале строки изменится.
Для наглядности можно вывести только конкретные строки, с которыми идет работа. Можно добавить или убрать 2–4 строки (указывается в параметрах команды): pd
($$ — индикатор текущей строки).
На следующем шаге будем резать по живому, избавимся от перехода, заменив текущую инструкцию нопами. Для этого в R2 есть команда wao
(чтобы узнать о ней больше, загляни в справку, оно того стоит).
Вновь отобрази четыре текущие строки или функцию целиком, чтобы увидеть изменения.

Наверняка ты обратил внимание, что в оригинале была одна строка, а после модификации стало две. Это произошло по той причине, что инструкция nop
занимает в памяти один байт, тогда как условный переход jne
вместе с адресом — два байта. Следовательно, чтобы сохранить баланс в изменяемой программе, вместо перехода с адресом Radare2 вставил два нопа. Теперь можешь прогнать взломанный сервак в отладчике (как мы это делали в предыдущем разделе) и убедиться, что дело доходит до функции secret
.

Сливаем фото
Настало время открыть функцию secret
в дизассемблере и узнать, что она делает:
r2 server64-hacked.elf
aaa
s sym.secret
pdf

В начале функции нет ничего необычного, полученные аргументы раскладываются по переменным: введенная юзером строка (первый аргумент) помещается в переменную s
, дескриптор сокета (второй аргумент) — в fd
. На серверной стороне выводится строка, сигнализирующая о вызове функции secret
.
Затем библиотечная функция strlen
измеряет длину пользовательской строки, и, если она не равна пяти, ход выполнения программы перемещается в конец функции — на выход. Иначе выполнение продолжается. Происходит посимвольное сравнение с последовательностью символов WtfR2
, что можно заметить в комментариях Radare2.
Далее в серверную консоль выводится строка [
. Потом готовятся параметры для вызова функции: считывается значение глобальной переменной plan_jpg_len
(представляющее размер изображения), копируется дескриптор сокета. Мы прекрасно помним, что он был передан в функцию в качестве аргумента, последним параметром (на самом деле первым) передается указатель на массив байтов, составляющих изображение, — plan_jpg
.
После этого выполняется безусловный переход в конец функции. Ниже последний символ строки s
проверяется на равенство восьми. Ага! То есть программа предполагает одну из двух строк: WtfR2
и WtfR8
.
Выше мы рассмотрели первый вариант и вывод в клиентскую консоль массива байтов plan_jpg
, сейчас мы рассматриваем второй вариант. В нем действия похожи, только в качестве размера передаваемых данных берется значение глобальной переменной portfolio_jpg_len
, а роль передаваемых данных играет массив байтов portfolio_jpg
.
На этом дизассемблерный код сервера полностью рассмотрен, пора двигаться дальше.
Получаем фото
Откроем две консоли: в одной запустим сервер, во второй подключимся к серверу и введем одно из двух специальных имен, к примеру WtfR2
, в результате получим вывод, приведенный на скриншоте ниже.

Этот поток байтов надо записать в файл. С клиентской стороны введи
echo WtfR2 | nc localhost 14884 > Изображения/picR2.jpg
Посмотрим, что, по мнению операционной системы, этот файл собой представляет:
file Изображения /picR2.jpg
На выходе получим data
. Многозначительно! Данные ведь могут быть любого типа. Выходит, Radare2 не определил, что это за файл. Что же мы можем предпринять? Вырезать из файла ненужную часть! Открой файл в текстовом редакторе:
nano Изображения/picR2.jpg

И удали в нем всю верхнюю часть, включая слово Username
и двоеточие после него. В итоге должно получиться так, как на скриншоте.

Сохраняй файл и закрывай редактор. Посмотрим теперь, как ОС определяет файл:
file Изображения/picR2.jpg
Если на предыдущем шаге все сделано правильно, утилита выведет в консоль следующее описание файла:
Изображения/picR2.jpg: JPEG image data, JFIF standard 1.01, aspect ratio, density 1x1, segment length 16, Exif Standard: [TIFF image data, little-endian, direntries=1, datetime=2008:09:27 09:19:53], progressive, precision 8, 564x690, components 1
Последний тест, который следует провести, — это открыть файл в просмотрщике изображений:
gwenview Изображения/picR2.jpg
Результат приводить не буду, чтобы тебе было любопытнее повторить все описанное самому.
Также в качестве упражнения можешь скачать с сервера второй файл — с именем WtfR8
.
Обрати внимание: для редактирования этого файла надо использовать vim. А ты думал, мы просто так устанавливали его в первой статье? Если снова забыл, как им пользоваться, после удаления ненужной части файла набери двоеточие, команду wq
и нажми Enter.

Выводы
Можем ли мы считать нашу миссию завершенной? Теперь определенно! По ходу упражнения с дизассемблером нам удалось не только благополучно получить доступ к аккаунту Джаббы Хатта, но и утащить с сервера секретные материалы.
Но главной нашей задачей на протяжении обеих статей было научиться использовать Radare2 для анализа программ в Linux.
Конечно, функциями, которые я здесь показал, возможности Radare2 не ограничиваются. Мы рассмотрели лишь малую часть верхушки айсберга. Думаю, мы еще уделим внимание тонкостям работы с Radare2, поэтому, если тема тебе интересна, пиши об этом комментарии.