Хакер - Android: реверс-инжиниринг Flutter-приложения

Хакер - Android: реверс-инжиниринг Flutter-приложения

hacker_frei

https://t.me/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-при­ложе­ний — дело неб­лагодар­ное и дос­таточ­но слож­ное. До появ­ления пол­ноцен­ных инс­тру­мен­тов еще год‑дру­гой.

При­мер печати клас­сов с помощью Doldrums

РАЗРАБОТЧИКУ

Четыре ошибки при использовании корутин и Flow

Misnomers, Mistakes and Misunderstandings to watch for when learning Kotlin Coroutines and Flow — статья о типич­ных ошиб­ках, которые допус­кают прог­раммис­ты при работе с корути­нами.

  1. Ис­поль­зование Flow вмес­то обыч­ной suspend-фун­кции. Мно­гие прог­раммис­ты исполь­зуют Flow как уни­вер­саль­ный инс­тру­мент для переда­чи зна­чений даже в тех слу­чаях, ког­да в нем нет никако­го смыс­ла.
  2. Пред­ста­вим себе такую фун­кцию:
  3. fun getAccountInfo(): Flow<Response<Account>>
  4. По фак­ту она воз­вра­щает толь­ко одно зна­чение и Flow здесь не нужен. Логич­нее было бы прев­ратить эту фун­кцию в обыч­ную suspend-фун­кцию:
  5. suspend fun getAccountInfo(): Response<Account>
  6. Suspend-фун­кции с парамет­рами‑кол­бэками. Suspend-фун­кции были задума­ны, что­бы заменить кол­бэки, поэто­му соз­давать фун­кции с кол­бэками в качес­тве парамет­ров — боль­шая ошиб­ка.
  7. Возь­мем, к при­меру, сле­дующую фун­кцию:
  8. suspend fun download(
  9. url: String,
  10. onSuccess: (String) -> Unit,
  11. onError: (Throwable) -> Unit
  12. )
  13. Как в дан­ном слу­чае изба­вить­ся от кол­бэков, но оста­вить воз­можность воз­вра­тить два раз­ных типа зна­чений? Для это­го мож­но исполь­зовать sealed-клас­сы:
  14. suspend fun download(url:String): Result
  15. sealed class Result {
  16. data class Success(val data:String): Result()
  17. data class Error(val error:Throwable): Result()
  18. }
  19. Ис­поль­зование GlobalScope. Активнос­ти, фраг­менты, ViewModel, View и дру­гие стан­дар­тные клас­сы име­ют фун­кции‑рас­ширения, которые мож­но исполь­зовать для запус­ка корутин. Не сто­ит исполь­зовать GlobalScope, который может при­вес­ти к утеч­кам корутин.
  20. Не­нуж­ное перек­лючение потоков. Корути­ны устро­ены так, что их очень лег­ко и прос­то мож­но перек­лючить на дру­гой поток с помощью сме­ны дис­петче­ра:
  21. with(Dispatchers.IO){
  22. ...
  23. }
  24. Од­нако сто­ит нес­коль­ко раз подумать перед тем, как исполь­зовать эту воз­можность. Во‑пер­вых, это усложня­ет юнит‑тес­тирова­ние. Во‑вто­рых, мно­гие фрей­мвор­ки и биб­лиоте­ки уме­ют самос­тоятель­но перек­лючать исполне­ние меж­ду потока­ми, так что руч­ное перек­лючение, кро­ме овер­хеда, ничего не даст.

Советы по использованию корутин

Best practices for coroutines in Android — советы Google, как исполь­зовать корути­ны.

  1. Внед­ряй дис­петче­ры как зависи­мос­ти. Бла­года­ря это­му юнит‑тес­тирова­ние ста­нет нам­ного более удоб­ным.
  2. class NewsRepository(
  3. private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
  4. ) {
  5. suspend fun loadNews() = withContext(defaultDispatcher) { /* ... */ }
  6. }
  7. Suspend-фун­кции дол­жны быть безопас­ными для вызова из UI-потока при­ложе­ния. Если suspend-фун­кция дела­ет слож­ную ресур­соем­кую работу, она дол­жна сама позабо­тить­ся о перек­лючении дис­петче­ра. Дру­гими сло­вами, за переме­щение работы в фоновый поток дол­жна отве­чать сама suspend-фун­кция, а не код, который ее вызыва­ет.
  8. ViewModel дол­жна соз­давать корути­ны сама. Вмес­то того что­бы выс­тавлять наружу suspend-фун­кции, ViewModel дол­жна сама порож­дать корути­ны из обыч­ных фун­кций. Такой под­ход упро­щает тес­тирова­ние и не соз­дает проб­лем при пересоз­дании активнос­ти.

  9. class LatestNewsViewModel(
  10. private val getLatestNewsWithAuthors: GetLatestNewsWithAuthorsUseCase
  11. ) : ViewModel() {
  12. private val _uiState = MutableStateFlow<LatestNewsUiState>(LatestNewsUiState.Loading)
  13. val uiState: StateFlow<LatestNewsUiState> = _uiState
  14. fun loadNews() {
  15. viewModelScope.launch {
  16. val latestNewsWithAuthors = getLatestNewsWithAuthors()
  17. _uiState.value = LatestNewsUiState.Success(latestNewsWithAuthors)
  18. }
  19. }
  20. }
  21. Не сле­дует выс­тавлять наружу изме­няемые типы дан­ных. Это стан­дар­тный при­мер гра­мот­ного ООП‑про­екти­рова­ния, который поз­волит сде­лать соп­ровож­дение кода более прос­тым.

  22. class LatestNewsViewModel : ViewModel() {
  23. val _uiState = MutableStateFlow(LatestNewsUiState.Loading)
  24. val uiState: StateFlow<LatestNewsUiState> = _uiState
  25. /* ... */
  26. }
  27. Уров­ни дан­ных и биз­нес‑логики дол­жны быть дос­тупны через suspend-фун­кции и Flow. Сле­дова­ние это­му прин­ципу поз­волит пра­виль­но управлять жиз­ненным цик­лом при­ложе­ния, ког­да жизнью корутин управля­ет ViewModel, а не клас­сы уров­ня биз­нес‑логики.

  28. class ExampleRepository {
  29. suspend fun makeNetworkRequest() { /* ... */ }
  30. fun getExamples(): Flow<Example> { /* ... */ }
  31. }
  32. Ис­поль­зуй TestCoroutineDispatcher. Сле­дова­ние пер­вому пра­вилу поз­волит исполь­зовать в тес­тах дис­петчер TestCoroutineDispatcher, который выпол­няет работу сра­зу, поз­воляя луч­ше кон­тро­лиро­вать исполне­ние кода.
  33. Из­бегай исполь­зования GlobalScope. GlobalScope при­водит к утеч­кам корутин, усложня­ет тес­тирова­ние и отладку кода.
  34. Ко­рути­ны дол­жны лег­ко уби­вать­ся. Корути­ны пос­тро­ены на идее коопе­ратив­ной мно­гоза­дач­ности. Это зна­чит, что завер­шение корути­ны через cancel() не про­исхо­дит сра­зу. Корути­на дол­жна сама про­верить свой ста­тус и завер­шить работу в слу­чае необ­ходимос­ти. В боль­шинс­тве слу­чаев делать для это­го ничего не нуж­но, так как все suspend-фун­кции из пакета kotlinx.coroutines (withContextdelay) уме­ют сами про­верять свой ста­тус и реаги­ровать на сиг­нал завер­шения. Но иног­да все‑таки при­ходит­ся делать эту работу самому:

  35. someScope.launch {
  36. for(file in files) {
  37. ensureActive() // Проверка флага завершения
  38. readFile(file)
  39. }
  40. }
  41. Не забывай об исклю­чени­ях. Исклю­чения луч­ше перех­ватывать в теле корути­ны:

  42. class LoginViewModel(
  43. private val loginRepository: LoginRepository
  44. ) : ViewModel() {
  45. fun login(username: String, token: String) {
  46. viewModelScope.launch {
  47. try {
  48. loginRepository.login(username, token)
  49. // Notify view user logged in successfully
  50. } catch (error: Throwable) {
  51. // Notify view login attempt failed
  52. }
  53. }
  54. }
  55. }

Автоматически устаревающие комментарии

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:

  1. Фун­кция для про­вер­ки под­клю­чения к интерне­ту.
  2. fun Context?.isOnline(): Boolean {
  3. this?.apply {
  4. val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
  5. val netInfo = cm.activeNetworkInfo
  6. return netInfo != null && netInfo.isConnected
  7. }
  8. return false
  9. }
  10. Фун­кции для показа и скры­тия кла­виату­ры.

  11. fun View.hideKeyboard(): Boolean {
  12. try {
  13. val inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
  14. return inputMethodManager.hideSoftInputFromWindow(windowToken, 0)
  15. } catch (ignored: RuntimeException) { }
  16. return false
  17. }
  18. fun View.showKeyboard() {
  19. val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
  20. this.requestFocus()
  21. imm.showSoftInput(this, 0)
  22. }
  23. Сто­ит отме­тить, что это наив­ный метод показа кла­виату­ры. Из‑за осо­бен­ностей работы Android он может прос­то не сра­ботать. Более удач­ную реали­зацию подоб­ной фун­кции мож­но най­ти в одном из пре­дыду­щих выпус­ков дай­джес­та.
  24. Фун­кции зап­роса и про­вер­ки пол­номочий:
  25. fun Fragment.isGranted(permission: AppPermission) = run {
  26. context?.let {
  27. (PermissionChecker.checkSelfPermission(it, permission.permissionName
  28. ) == PermissionChecker.PERMISSION_GRANTED)
  29. } ?: false
  30. }
  31. fun Fragment.shouldShowRationale(permission: AppPermission) = run {
  32. shouldShowRequestPermissionRationale(permission.permissionName)
  33. }
  34. fun Fragment.requestPermission(permission: AppPermission) {
  35. requestPermissions(arrayOf(permission.permissionName), permission.requestCode
  36. )
  37. }
  38. fun AppCompatActivity.checkPermission(permission: AppPermission) = run {
  39. context?.let {
  40. (ActivityCompat.checkSelfPermission(it, permission.permissionName
  41. ) == PermissionChecker.PERMISSION_GRANTED)
  42. } ?: false
  43. }
  44. fun AppCompatActivity.shouldRequestPermissionRationale(permission: AppPermission) =
  45. ActivityCompat.shouldShowRequestPermissionRationale(this, permission.permissionName)
  46. fun AppCompatActivity.requestAllPermissions(permission: AppPermission) {
  47. ActivityCompat.requestPermissions(this, arrayOf(permission.permissionName), permission.requestCode))
  48. }
  49. Ра­бота с цве­тами:
  50. fun String.hextoRGB() : Triple<String, String, String>{
  51. var name = this
  52. if (!name.startsWith("#")){
  53. name = "#$this"
  54. }
  55. var color = Color.parseColor(name)
  56. var red = Color.red(color)
  57. var green = Color.green(color)
  58. var blue = Color.blue(color)
  59. return Triple(red.toString(), green.toString(), blue.toString())
  60. }
  61. fun Int.colorToHexString(): String? {
  62. var data = String.format("#%06X", -0x1 and this).replace("#FF","#")
  63. return data
  64. }

Toп 11 Koltin-библиотек

The Top 11 Trending Kotlin Libraries for 2021 — статья о новых и ста­рых биб­лиоте­ках с под­дер­жкой Kotlin. Автор ведет рас­сказ с точ­ки зре­ния бэкенд‑раз­работ­чика, но мно­гие биб­лиоте­ки при­мени­мы и для мобиль­ной раз­работ­ки.

  1. Kotless — так называ­емый Kotlin serverless framework. Поз­воля­ет сге­нери­ровать готовый к запус­ку на AWS сер­вер из кода при­ложе­ния.
  2. Kotest — сов­ремен­ный фрей­мворк для unit-тес­тирова­ния. Вклю­чает в себя так­же mocking-фрей­мворк и assert-фрей­мворк.
  3. Exposed — ORM-фрей­мворк с син­такси­сом, пос­тро­енным на DSL. Минима­лис­тичный и удоб­ный в исполь­зовании.
  4. Ktor — прос­той и быс­трый фрей­мворк для соз­дания кли­ент­ских и сер­верных асин­хрон­ных сетевых при­ложе­ний.
  5. Kotlinx Serialization — удоб­ная и прос­тая в исполь­зовании биб­лиоте­ка сери­али­зации и десери­али­зации JSON и дру­гих типов дан­ных.
  6. Koin — DI-фрей­мворк, написан­ный на Kotlin и не исполь­зующий реф­лексию.
  7. Netflix DGS framework — фрей­мворк для соз­дания GraphQL-сер­веров.
  8. KMongo — удоб­ная в исполь­зовании обер­тка для MongoDB.
  9. JetBarains Xodus — встра­иваемая неб­локиру­емая база дан­ных без схе­мы.
  10. Dokka — ана­лог JavaDoc для Kotlin.
  11. Vaadin — веб‑фрей­мворк, базиру­ющий­ся на DSL.

Читайте ещё больше платных статей бесплатно: https://t.me/hacker_frei



Report Page