Pull to refresh

Comments 3

В целом получается, что уровень сложности взлома приложения на Dart такой же как если бы приложение было написано на C++ и NDK. И такой же как уровень сложности нативных приложений под Windows/Linux/MacOS (т. е. которые не написаны на C#/Java/Python/etc). Это как «возврат к истокам» во времена, когда управляемые языки не были так широко распространены.

Взлом, безусловно, усложняется, но не становится невозможным.

Например, для данного приложения можно было попробовать найти в бинарнике строку «wrong password» (если она в ресурсах, то определить id ресурса и искать в бинарнике уже его, но в данном случае, мне кажется, в бинарнике будет сама строка), затем найти какая ассемблерная команда грузит её адрес в ОЗУ и начать идти по программе задом-наперёд начиная с этой инструкции. Рано или поздно мы бы дошли к тому, что на текущую инструкцию ссылается какой-нибудь условный переход (либо наоборот мы увидим условный переход, который может обскочить обращение к строке «wrong password»). Меняем его на противоположный, тестируем результат. Если не помогло, то ищём какой условный переход привёл к этому условному переходу и т. д. Разумеется, для упрощения работы не помешает продвинутый диссассемблер, который способен построить граф переходов, чтобы можно было быстро отвечать на вопрос «откуда можно попасть на эту инструкцию».

Грубо говоря, программа после компиляции выглядит как-то так (пример для x86 assembler, потому что я его лучше знаю, для ARM ничего принципиально не меняется):

call get_user_input
push eax
push right_password
call strcmp
test eax, eax
jz .password_correct
push password_invalid_msg
call show_msg
jmp .exit
.password_correct:
push password_correct_msg
call show_msg
.exit:
...
right_password db "secret",0
password_invalid_msg db "wrong password",0
password_correct_msg db "ok",0


Простым поиском «wrong password» (текст сообщения мы узнаём из интерфейса приложения, собственно, попробовав ввести неверный пароль, разумеется, при поиске надо учитывать, что строка может быть не в ASCII, а в каком-нибудь UTF-16, но перебрать несколько популярных кодировок не проблема), мы узнаём его адрес. Затем мы можем поиском адреса найти инструкцию push password_invalid_msg (опять же может потребоваться перебрать несколько вариантов адресации и т. д.). Наконец, идя от этой инструкции мы натыкаемся на jz .password_correct (либо мы бы могли наткнуться, что на текущую инструкцию ссылается переход откуда-то), которую можно попытаться заменить на противоположную jnz и протестировать результат.

Ещё может случиться, что код в библиотеке зашифрован/упакован. Тогда все эти операции выполняются не над файлом на диске, а в отладчике, поймав момент, когда код дешифруется. Разумеется, и от этого есть защиты, для которых в свою очередь есть контр-атаки. Это уже соревнование брони и снаряда и может продолжаться вечно, но я полагаю, что в примере из статьи случай максимально примитивный.

Чтобы получить больше опыта в этой области, можно загуглить такое явление как crackme — специальные учебные программы, предназначенные для того, чтобы их взломали (многие crackme сопровождаются эталонным «решением» как надо было догадаться, какую инструкцию патчить). Потренировавшись на них, можно вернуться к Flutter с новыми силами.
Нативный код сгенерированный компилятором Dart отличается от кода сгенерированного C/C++, в том числе отличается работа с кучей и объектами в памяти, но что касается C/C++, то по нему относительно давно существуют неплохой опыт и инструменты — тот же HexRays или IDA Pro. Т.е. привести к более читабельному для исследования виду нативный код полученный из сишного вполне можно, правда конечно без «красивых» имен переменных и функций.

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

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

В реальном Flutter приложении очень "далеко" от константы строки (wrong pass например) до логики условия, а именно:


  1. строки ассетятся через делегат локализации, который в свою очередь инициируется также в рантайме по данным о локалях устройства, которые в свою очередь получаются из нативного канала (ещё одна неприятная для дизассемблирования фича)
  2. Flutter сильно реактивный фреймворк, проброс этой строки в вёрстку будет выполнен через стрим pub/sub, что усложняет анализ дерева переходов.
  3. Как правило продовые приложения имеют несколько дополнительных логических проверок (наличие соединения, валидация, санитайзинг, спам) которые тоже вполне себе неприятные для анализа стримы.

Предположу, что в реальном приложении найти машинное слово в бинарнике для != можно будет только на отладчике с брекпойнтами, который ещё не разработан(?).

Sign up to leave a comment.