Хакер - Android: реверс-инжиниринг Flutter-приложения
hacker_frei
Евгений Зобнин
Содержание статьи
- Почитать
- Реверс-инжиниринг Flutter-приложения
- Разработчику
- Четыре ошибки при использовании корутин и Flow
- Советы по использованию корутин
- Автоматически устаревающие комментарии
- Пять функций-расширений
- Toп 11 Koltin-библиотек
- Инструменты
- Библиотеки
Сегодня в выпуске: реверс‑инжиниринг Flutter-приложения, подборка полезных функций‑расширений на Kotlin, две статьи об ошибках использования корутин и Flow в Kotlin, заметка об автоматически устаревающих комментариях, а также подборка из одиннадцати must have библиотек и десяток новых библиотек.
ПОЧИТАТЬ
Реверс-инжиниринг Flutter-приложения
Reverse Engineering a Flutter app by recompiling Flutter Engine — статья о реверс‑инжиниринге приложений, написанных с использованием фреймворка Flutter.
Flutter — это кросс‑платформенный инструмент, предназначенный для создания быстрых приложений на языке Dart с использованием реактивного UI-фреймворка. Написанные с помощью Flutter приложения могут работать на Android, iOS, десктопе и вебе. При этом интерфейс будет полностью идентичен на всех платформах.
Главная особенность, отличающая Flutter от фреймворка, предоставляемого Android, в том, что код всего приложения, вместо набора из байт‑кода и ресурсов, компилируется в единую нативную библиотеку, разобраться в структуре которой достаточно сложно. К тому же формат данных в этой библиотеке постоянно меняется, что еще сильнее запутывает реверсера.
Библиотека, содержащая код приложения, называется libapp.so. Причем это не просто код и данные приложения, а так называемый snapshot, представляющий собой снимок состояния виртуальной машины Dart перед передачей управления на точку входа приложения (функция main), плюс скомпилированный с помощью AOT-компилятора код всех классов приложения.
Разбирать код библиотеки libapp.so классическим способом (запускаем IDA Pro и начинаем исследовать) бесполезно. Да, это нативный код, но формат самого файла в корне отличается от обычных библиотек.
Один из методов анализа состоит в том, чтобы пропарсить заголовок снапшота, найти в нем ссылки на все объекты типа Code (они как раз и хранят нативный код методов), а затем дизассемблировать находящиеся по этим адресам инструкции. В этом поможет инструмент Doldrums. Он выведет на экран все имеющиеся в коде классы и укажет, по каким адресам располагается код методов.
Проблема этого подхода в том, что формат снапшота меняется от версии к версии. Тот же Doldrums отлично работает для приложений, собранных с помощью Flutter 2.5, но не работает для более поздних версий.
Универсальный подход к анализу заключается в том, чтобы модифицировать сам фреймворк Flutter, располагающийся в библиотеке libflutter.so рядом с libapp.so. Для этого необходимо взять исходники фреймворка той же версии, добавить в них код для печати всех нужных нам данных (имена классов, методов и адреса их кода), а затем собрать его и заменить им оригинальный фреймворк в пакете приложения.
В частности, можно внести исправления в метод Deserializer::ReadProgramSnapshot(ObjectStore* object_store) в файле runtime/vm/clustered_snapshot.cc, чтобы заставить его распечатать таблицу классов. Также можно изменить метод void ClassTable::Print() в файле runtime/vm/class_table.cc для печати более подробной информации.
В статье приведено еще несколько деталей, как это сделать правильно, но нет готовых файлов. Так что в данный момент реверс‑инжиниринг Flutter-приложений — дело неблагодарное и достаточно сложное. До появления полноценных инструментов еще год‑другой.

РАЗРАБОТЧИКУ
Четыре ошибки при использовании корутин и Flow
Misnomers, Mistakes and Misunderstandings to watch for when learning Kotlin Coroutines and Flow — статья о типичных ошибках, которые допускают программисты при работе с корутинами.
- Использование Flow вместо обычной suspend-функции. Многие программисты используют Flow как универсальный инструмент для передачи значений даже в тех случаях, когда в нем нет никакого смысла.
- Представим себе такую функцию:
fun getAccountInfo(): Flow<Response<Account>>- По факту она возвращает только одно значение и Flow здесь не нужен. Логичнее было бы превратить эту функцию в обычную suspend-функцию:
suspend fun getAccountInfo(): Response<Account>- Suspend-функции с параметрами‑колбэками. Suspend-функции были задуманы, чтобы заменить колбэки, поэтому создавать функции с колбэками в качестве параметров — большая ошибка.
- Возьмем, к примеру, следующую функцию:
suspend fun download(url: String,onSuccess: (String) -> Unit,onError: (Throwable) -> Unit)- Как в данном случае избавиться от колбэков, но оставить возможность возвратить два разных типа значений? Для этого можно использовать sealed-классы:
suspend fun download(url:String): Resultsealed class Result {data class Success(val data:String): Result()data class Error(val error:Throwable): Result()}- Использование GlobalScope. Активности, фрагменты, ViewModel, View и другие стандартные классы имеют функции‑расширения, которые можно использовать для запуска корутин. Не стоит использовать GlobalScope, который может привести к утечкам корутин.
- Ненужное переключение потоков. Корутины устроены так, что их очень легко и просто можно переключить на другой поток с помощью смены диспетчера:
with(Dispatchers.IO){...}- Однако стоит несколько раз подумать перед тем, как использовать эту возможность. Во‑первых, это усложняет юнит‑тестирование. Во‑вторых, многие фреймворки и библиотеки умеют самостоятельно переключать исполнение между потоками, так что ручное переключение, кроме оверхеда, ничего не даст.
Советы по использованию корутин
Best practices for coroutines in Android — советы Google, как использовать корутины.
- Внедряй диспетчеры как зависимости. Благодаря этому юнит‑тестирование станет намного более удобным.
class NewsRepository(private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default) {suspend fun loadNews() = withContext(defaultDispatcher) { /* ... */ }}- Suspend-функции должны быть безопасными для вызова из UI-потока приложения. Если suspend-функция делает сложную ресурсоемкую работу, она должна сама позаботиться о переключении диспетчера. Другими словами, за перемещение работы в фоновый поток должна отвечать сама suspend-функция, а не код, который ее вызывает.
- ViewModel должна создавать корутины сама. Вместо того чтобы выставлять наружу suspend-функции, ViewModel должна сама порождать корутины из обычных функций. Такой подход упрощает тестирование и не создает проблем при пересоздании активности.
class LatestNewsViewModel(private val getLatestNewsWithAuthors: GetLatestNewsWithAuthorsUseCase) : ViewModel() {private val _uiState = MutableStateFlow<LatestNewsUiState>(LatestNewsUiState.Loading)val uiState: StateFlow<LatestNewsUiState> = _uiStatefun loadNews() {viewModelScope.launch {val latestNewsWithAuthors = getLatestNewsWithAuthors()_uiState.value = LatestNewsUiState.Success(latestNewsWithAuthors)}}}- Не следует выставлять наружу изменяемые типы данных. Это стандартный пример грамотного ООП‑проектирования, который позволит сделать сопровождение кода более простым.
class LatestNewsViewModel : ViewModel() {val _uiState = MutableStateFlow(LatestNewsUiState.Loading)val uiState: StateFlow<LatestNewsUiState> = _uiState/* ... */}- Уровни данных и бизнес‑логики должны быть доступны через suspend-функции и Flow. Следование этому принципу позволит правильно управлять жизненным циклом приложения, когда жизнью корутин управляет ViewModel, а не классы уровня бизнес‑логики.
class ExampleRepository {suspend fun makeNetworkRequest() { /* ... */ }fun getExamples(): Flow<Example> { /* ... */ }}- Используй TestCoroutineDispatcher. Следование первому правилу позволит использовать в тестах диспетчер TestCoroutineDispatcher, который выполняет работу сразу, позволяя лучше контролировать исполнение кода.
- Избегай использования GlobalScope. GlobalScope приводит к утечкам корутин, усложняет тестирование и отладку кода.
- Корутины должны легко убиваться. Корутины построены на идее кооперативной многозадачности. Это значит, что завершение корутины через
cancel()не происходит сразу. Корутина должна сама проверить свой статус и завершить работу в случае необходимости. В большинстве случаев делать для этого ничего не нужно, так как все suspend-функции из пакетаkotlinx.coroutines(withContext,delay) умеют сами проверять свой статус и реагировать на сигнал завершения. Но иногда все‑таки приходится делать эту работу самому: someScope.launch {for(file in files) {ensureActive() // Проверка флага завершенияreadFile(file)}}- Не забывай об исключениях. Исключения лучше перехватывать в теле корутины:
class LoginViewModel(private val loginRepository: LoginRepository) : ViewModel() {fun login(username: String, token: String) {viewModelScope.launch {try {loginRepository.login(username, token)// Notify view user logged in successfully} catch (error: Throwable) {// Notify view login attempt failed}}}}
Автоматически устаревающие комментарии
Write self-deprecating comments — короткая, но полезная заметка о том, как писать комментарии, по которым будет сразу понятно, устарел комментарий или нет.
Трюк состоит в том, чтобы поместить значение, которое описывает комментарий, в сам комментарий:
PaymentAPI.call(
mode: "X", # "X": Disable 3D Secure verification
timeout: 12, # 12 secs is the smallest value that avoids errors
)
Теперь, если значение изменится, комментарий автоматически станет неактуальным:
PaymentAPI.call(
mode: "Y", # "X": Disable 3D Secure verification
timeout: 15, # 12 secs is the smallest value that avoids errors
)
Разумеется, трюк сработает далеко не во всех случаях, но помнить о нем стоит.
Пять функций-расширений
5 More Kotlin Extensions for Android Developers — несколько полезных функций‑расширений для разработчиков на Kotlin:
- Функция для проверки подключения к интернету.
fun Context?.isOnline(): Boolean {this?.apply {val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManagerval netInfo = cm.activeNetworkInforeturn netInfo != null && netInfo.isConnected}return false}- Функции для показа и скрытия клавиатуры.
fun View.hideKeyboard(): Boolean {try {val inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManagerreturn inputMethodManager.hideSoftInputFromWindow(windowToken, 0)} catch (ignored: RuntimeException) { }return false}fun View.showKeyboard() {val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManagerthis.requestFocus()imm.showSoftInput(this, 0)}- Стоит отметить, что это наивный метод показа клавиатуры. Из‑за особенностей работы Android он может просто не сработать. Более удачную реализацию подобной функции можно найти в одном из предыдущих выпусков дайджеста.
- Функции запроса и проверки полномочий:
fun Fragment.isGranted(permission: AppPermission) = run {context?.let {(PermissionChecker.checkSelfPermission(it, permission.permissionName) == PermissionChecker.PERMISSION_GRANTED)} ?: false}fun Fragment.shouldShowRationale(permission: AppPermission) = run {shouldShowRequestPermissionRationale(permission.permissionName)}fun Fragment.requestPermission(permission: AppPermission) {requestPermissions(arrayOf(permission.permissionName), permission.requestCode)}fun AppCompatActivity.checkPermission(permission: AppPermission) = run {context?.let {(ActivityCompat.checkSelfPermission(it, permission.permissionName) == PermissionChecker.PERMISSION_GRANTED)} ?: false}fun AppCompatActivity.shouldRequestPermissionRationale(permission: AppPermission) =ActivityCompat.shouldShowRequestPermissionRationale(this, permission.permissionName)fun AppCompatActivity.requestAllPermissions(permission: AppPermission) {ActivityCompat.requestPermissions(this, arrayOf(permission.permissionName), permission.requestCode))}- Работа с цветами:
fun String.hextoRGB() : Triple<String, String, String>{var name = thisif (!name.startsWith("#")){name = "#$this"}var color = Color.parseColor(name)var red = Color.red(color)var green = Color.green(color)var blue = Color.blue(color)return Triple(red.toString(), green.toString(), blue.toString())}fun Int.colorToHexString(): String? {var data = String.format("#%06X", -0x1 and this).replace("#FF","#")return data}
Toп 11 Koltin-библиотек
The Top 11 Trending Kotlin Libraries for 2021 — статья о новых и старых библиотеках с поддержкой Kotlin. Автор ведет рассказ с точки зрения бэкенд‑разработчика, но многие библиотеки применимы и для мобильной разработки.
- Kotless — так называемый Kotlin serverless framework. Позволяет сгенерировать готовый к запуску на AWS сервер из кода приложения.
- Kotest — современный фреймворк для unit-тестирования. Включает в себя также mocking-фреймворк и assert-фреймворк.
- Exposed — ORM-фреймворк с синтаксисом, построенным на DSL. Минималистичный и удобный в использовании.
- Ktor — простой и быстрый фреймворк для создания клиентских и серверных асинхронных сетевых приложений.
- Kotlinx Serialization — удобная и простая в использовании библиотека сериализации и десериализации JSON и других типов данных.
- Koin — DI-фреймворк, написанный на Kotlin и не использующий рефлексию.
- Netflix DGS framework — фреймворк для создания GraphQL-серверов.
- KMongo — удобная в использовании обертка для MongoDB.
- JetBarains Xodus — встраиваемая неблокируемая база данных без схемы.
- Dokka — аналог JavaDoc для Kotlin.
- Vaadin — веб‑фреймворк, базирующийся на DSL.
Читайте ещё больше платных статей бесплатно: https://t.me/hacker_frei