June 3, 2021

Android: прайваси в Android 12 и анализ трояна FluBot

Се­год­ня в выпус­ке: изме­нения в защите при­ват­ных дан­ных поль­зовате­лей в Android 12, иссле­дова­ние проб­лем неп­равиль­ной нас­трой­ки облачных сер­висов в при­ложе­ниях, статья о пра­вилах раз­бора и ана­лиза кода мал­вари на при­мере FluBot, а так­же смеш­ная уяз­вимость в при­ложе­нии Medium. Как бонус: опти­миза­ция запус­ка при­ложе­ния и оче­ред­ные советы раз­работ­чикам.

ПОЧИТАТЬ

Android 12 и прайваси

What’s new in Android Privacy — офи­циаль­ный анонс изме­нений в защите поль­зователь­ских дан­ных в Android 12.

  • Privacy-экран. В Android 12 будет спе­циаль­ный экран нас­тро­ек, показы­вающий, как час­то уста­нов­ленные при­ложе­ния получа­ли дос­туп к мик­рофону, камере, мес­тополо­жению и дру­гим сен­сорам и дан­ным.
  • Ин­дикато­ры камеры и мик­рофона. Вслед за Apple инже­неры Google добави­ли в Android 12 инди­като­ры дос­тупа к камере и мик­рофону. Если какое‑то при­ложе­ние в дан­ный момент исполь­зует то или дру­гое, в вер­хней час­ти экра­на появит­ся соот­ветс­тву­ющая икон­ка. Более того, пос­ле откры­тия штор­ки мож­но будет уви­деть, какие кон­крет­но при­ложе­ния име­ют дос­туп к камере и мик­рофону, и немед­ленно зап­ретить дос­туп.
  • Приб­лизитель­ное мес­тополо­жение. Диалог зап­роса мес­тополо­жения будет давать воз­можность выб­рать, каким типом информа­ции о мес­тополо­жении делить­ся с при­ложе­нием: точ­ным мес­тополо­жени­ем либо приб­лизитель­ным. Вто­рое хорошо под­ходит, нап­ример, для погод­ных сер­висов.
  • Уве­дом­ление о чте­нии буфера обме­на. Android 12 будет показы­вать сооб­щение каж­дый раз, ког­да какое‑либо при­ложе­ние про­чита­ет содер­жимое буфера обме­на. Единс­твен­ное исклю­чение, ког­да это­го не про­изой­дет, — если буфер обме­на чита­ет при­ложе­ние, находя­щееся в текущий момент на экра­не.
  • Пол­номочие на обна­руже­ние устрой­ств. Обна­руже­ние Bluetooth-устрой­ств в текущей Android 11 и ниже тре­бует раз­решение на дос­туп к мес­тополо­жению (да, имен­но так). В Android 12 у этой опе­рации появит­ся собс­твен­ное спе­циаль­ное раз­решение.
  • Усып­ление при­ложе­ний. Android 11 уме­ет отзы­вать раз­решения у дав­но не исполь­зуемых при­ложе­ний. Android 12 идет еще даль­ше и пол­ностью «усып­ляет» такие при­ложе­ния, забирая у них занима­емое дис­ковое прос­транс­тво. Для вывода при­ложе­ния из сна дос­таточ­но запус­тить его.

Проблемы неправильной настройки облачных сервисов

Mobile app developers’ misconfiguration of third party services leave personal data of over 100 million exposed — статья о таких уяз­вимос­тях при­ложе­ний, которые выз­ваны неп­равиль­ной нас­трой­кой облачных сер­висов. Никаких прак­тичес­ких при­меров взло­ма в статье нет, но общая ста­тис­тика весь­ма инте­рес­на.

  1. Неп­равиль­ное кон­фигури­рова­ние облачной БД. Здесь все прос­то, откры­тые на все­общее обоз­рение базы дан­ных до сих пор час­то встре­чают­ся в том чис­ле в при­ложе­ниях. Как при­мер: при­ложе­ние‑горос­коп Astro Guru сох­раня­ет в базу дан­ных email поль­зовате­ля, его имя, пол, мес­тополо­жение и дату рож­дения. А при­ложе­ние для заказа так­си T’Leva хра­нит в откры­той БД пол­ную перепис­ку меж­ду пас­сажира­ми и водите­лями.
  2. Пуш‑уве­дом­ления. Боль­шинс­тво сер­висов push-уве­дом­лений для отправ­ки уве­дом­ления от име­ни кон­крет­ного при­ложе­ния тре­буют исполь­зовать при­вязан­ный к нему крип­тогра­фичес­кий ключ. Но что, если этот ключ будет вшит в код самого при­ложе­ния в откры­том виде? В этом слу­чае ключ мож­но будет извлечь и исполь­зовать для отправ­ки уве­дом­лений от име­ни это­го при­ложе­ния.
  3. Се­тевые хра­нили­ща. С сетевы­ми хра­нили­щами проб­лема обыч­но та же, что и с облачны­ми БД. Если не нас­тро­ить аутен­тифика­цию или вшить клю­чи дос­тупа пря­мо в код при­ложе­ния — дан­ные будут в опас­ности. От этой проб­лемы, нап­ример, стра­дает при­ложе­ние Screen Recorder с 10 мил­лиона­ми уста­новок.

Сто­ит ска­зать, что мно­гие раз­работ­чики все‑таки пыта­ются скрыть клю­чи дос­тупа к облачным сер­висам, но обыч­но дела­ют это столь неуме­ло, что клю­чи извле­кают­ся три­виаль­но. Нап­ример, при­ложе­ние iFax пыта­ется скрыть клю­чи, исполь­зуя Base64, поэто­му все, что нуж­но сде­лать, — это прос­то ско­пиро­вать закоди­рован­ную в Base64 стро­ку и рас­кодиро­вать ее с помощью любой ути­литы, уме­ющей работать с этим фор­матом. Раз­бивка клю­чей на час­ти и исполь­зование XOR так­же популяр­ные решения. Забав­но, что имен­но так дела­ет мал­варь CopyCat.

При­мер сооб­щения, извле­чен­ного из БД при­ложе­ния T’Leva

Еще одна уязвимость в приложении Medium

Exploiting Activity in medium android app — корот­кая замет­ка об оче­ред­ной дурац­кой уяз­вимос­ти в мобиль­ном при­ложе­нии блог‑плат­формы Medium.

Суть статьи сво­дит­ся к сле­дующе­му. В при­ложе­нии есть активность SaveToMediumActivity. Все, что она дела­ет, — это сох­раня­ет статью в спи­сок избран­ного. Ока­залось, что активность не толь­ко дос­тупна извне любому дру­гому при­ложе­нию (экспор­тирова­на), но и поз­воля­ет сох­ранять в спи­сок какой угод­но URL, а не толь­ко статьи, опуб­ликован­ные на самом Medium.

Как итог: в спи­сок избран­ного мож­но лег­ко добавить что угод­но с помощью прос­тей­шего при­ложе­ния или такой коман­ды:

$ adb shell am start -n com.medium.reader/com.medium.android.donkey.save.SaveToMediumActivity -e android.intent.extra.TEXT "https://attacker.com"

Ах да. За все это Medium зап­латил баг‑баун­ти.

Разборка и анализ малвари

How to analyze mobile malware: a Cabassous/FluBot Case study — статья об эта­пах ана­лиза злов­редных при­ложе­ний на при­мере тро­яна Cabassous/FluBot.

  1. Для начала рас­паковы­ваем APK с помощью apktool или любого дру­гого ана­логич­ного инс­тру­мен­та.
  2. От­кры­ваем файл AndroidManifest.xml и находим основную активность при­ложе­ния. Она име­ет сле­дующий интент‑филь­тр:
  3. <intent-filter>
  4. <action android:name="android.intent.action.MAIN"/>
  5. <category android:name="android.intent.category.LAUNCHER"/>
  6. </intent-filter>
  7. В слу­чае с FluBot активность име­ет имя com.tencent.mobileqq.MainActivity. Но в пакете такой активнос­ти нет.
  8. Смот­рим, какие еще инте­рес­ные фай­лы и клас­сы есть в пакете. В слу­чае с FluBot пакет так­же содер­жал сле­дующие фай­лы: classes-v1.bin в катало­ге dex (воз­можно, зашиф­рован­ный код мал­вари), пакет com.whatsapp в деком­пилиро­ван­ном коде при­ложе­ния, пакеты n, np и obfuse (явно обфусци­рован­ные) и биб­лиоте­ку libreactnativeblob.so (ее так­же мож­но най­ти в при­ложе­нии WhatsApp).
  9. Оче­вид­но, что авто­ры мал­вари прос­то перепа­кова­ли WhatsApp, вклю­чив в него злов­редную фун­кци­ональ­ность. Поэто­му сле­дующее, что нуж­но сде­лать, — узнать, чем отли­чают­ся ори­гиналь­ный пакет WhatsApp и ока­зав­ший­ся у нас обра­зец. Сде­лать это мож­но с помощью ути­литы apkdiff:
  10. $ python3 apkdiff.py ../com.whatsapp_2.21.3.19-210319006_minAPI16\(x86\)\(nodpi\)_apkmirror.com.apk ../Cabassous.apk
  11. В дан­ном слу­чае автор срав­нива­ет мал­варь с WhatsApp вер­сии 2.21.3.19 потому, что в дизас­сем­бли­рован­ных лис­тингах мал­вари (в фай­ле AbstractAppShell) уда­лось най­ти имя вер­сии: 2.21.3.19-play-release.
  12. Ока­залось, что авто­ры мал­вари модифи­циро­вали все­го четыре фай­ла ори­гиналь­ного WhatsApp и в общем в них нет ничего инте­рес­ного. Поэто­му сле­дующий шаг — попыт­ка выяс­нить, где же находит­ся основная активность при­ложе­ния. Оче­вид­но, что она в фай­ле classes-v1.bin, но чем он запако­ван? Для начала поп­робу­ем исполь­зовать ути­литу APKiD.
APKiD бес­силен
  1. Она не дает резуль­тата. В таком слу­чае сто­ит поп­робовать разоб­рать­ся в коде клас­сов тех самых пакетов n, np и obfuse. Одна­ко они силь­но обфусци­рова­ны с исполь­зовани­ем имен клас­сов в юни­коде и реф­лексии. В этом мож­но разоб­рать­ся, но про­ще исполь­зовать динами­чес­кий ана­лиз.
  2. Что­бы выпол­нить динами­чес­кий ана­лиз, мы дол­жны понять, как мал­варь вооб­ще запус­кает свою злов­редную фун­кци­ональ­ность. Как мы уже видели, активнос­ти, ука­зан­ной в манифес­те, вооб­ще не сущес­тву­ет в коде. Это зна­чит, что она находит­ся где‑то в запако­ван­ном (и зашиф­рован­ном) коде и поэто­му где‑то дол­жен сущес­тво­вать код рас­паков­ки. Час­то авто­ры мал­вари засовы­вают такой код в бло­ки ста­тичес­кой ини­циали­зации Java, которые получа­ют управле­ние сра­зу пос­ле заг­рузки клас­са и до запус­ка основной активнос­ти при­ложе­ния.
Ста­тичес­кие ини­циали­зато­ры лег­ко най­ти с помощью grep
  1. Dexcalibur — один из самых удоб­ных инс­тру­мен­тов динами­чес­кого ана­лиза. По сути, это гра­фичес­кая обер­тка для Frida с пред­нас­тро­енны­ми хуками и воз­можностью быс­трой уста­нов­ки сво­их хуков. В слу­чае с FluBot Dexcalibur показы­вает все исполь­зования реф­лексии, одна­ко фун­кции рас­паков­ки сре­ди них нет. Это про­исхо­дит потому, что бло­ки ста­тичес­кой ини­циали­зации выпол­няют­ся еще до начала работы Frida.
  2. Те­перь у нас есть три пути: поп­робовать все‑таки разоб­рать­ся в обфусци­рован­ном коде заг­рузчи­ка и уда­лить из него код уда­ления рас­пакован­ного DEX-фай­ла (обыч­но мал­варь уда­ляет его сра­зу пос­ле заг­рузки в память); най­ти все ста­тичес­кие ини­циали­зато­ры, прев­ратить их в обыч­ные ста­тичес­кие фун­кции и написать код их вызова; ско­пиро­вать дизас­сем­бли­рован­ный код при­ложе­ния в Android Studio и запус­тить его под управле­нием отладчи­ка.
  3. Ока­залось, одна­ко, что все это не нуж­но, так как мал­варь по какой‑то при­чине забыва­ет уда­лить рас­пакован­ный код пос­ле его рас­паков­ки. Поэто­му дос­таточ­но вытащить уже рас­пакован­ный DEX-файл из при­ват­ного катало­га при­ложе­ния:
  4. $ adb shell
  5. hammerhead:/ $ su
  6. hammerhead:/ # cp /data/data/com.tencent.mobileqq/app_apkprotector_dex /data/local/tmp/classes-v1.bin
  7. hammerhead:/ # chmod 666 /data/local/tmp/classes-v1.bin
  8. hammerhead:/ # exit
  9. hammerhead:/ $ exit
  10. $ adb pull /data/local/tmp/classes-v1.bin payload.dex
  11. /data/local/tmp/classes-v1.bin: 1 file pulled. 18.0 MB/s (3229988 bytes in 0.171s)
  12. Те­перь мож­но спо­кой­но занять­ся ана­лизом кода мал­вари. Здесь все дос­таточ­но стан­дар­тно, и об этом мож­но про­читать в ори­гина­ле статьи.

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

Оптимизация скорости запуска приложения

From zero to hero: Optimizing Android app startup time — оче­ред­ная статья об опти­миза­ции ско­рос­ти запус­ка при­ложе­ний для Android. Ничего нового она не рас­ска­зыва­ет, но автор при­водит нес­коль­ко весь­ма инте­рес­ных наб­людений.

  1. Koin не мед­леннее Dagger. При­нято счи­тать, что так называ­емый service locator, которым явля­ется Koin, мед­леннее «нас­тояще­го» DI-фрей­мвор­ка, такого как Dagger. Замеры авто­ра показы­вают, что пос­ле перево­да при­ложе­ния с Koin на Dagger ско­рость запус­ка при­ложе­ния сов­сем не меня­ется. Koin быс­трый.
  2. Соз­дание объ­ектов — дешевая опе­рация. В ста­рые вре­мена счи­талось хорошей прак­тикой исполь­зовать пулы объ­ектов вмес­то соз­дания объ­ектов с нуля. Сов­ремен­ные вер­сии Android поз­воля­ют соз­давать объ­екты нас­толь­ко быс­тро, что пулы объ­ектов перес­тали вно­сить какую‑то роль в про­изво­дитель­ность при­ложе­ния.
  3. Очис­тка клас­са Application не помога­ет. Час­то в голову раз­работ­чиков при­ходит идея убрать из клас­са Application все тяжелые опе­рации или вынес­ти их в фоновые потоки. Как ни стран­но, это ничего не дает.
  4. От­ложен­ная ини­циали­зация Firebase помога­ет. Око­ло 60 мил­лисекунд мож­но выиг­рать на отло­жен­ной ини­циали­зации Firebase с помощью androidx.startup.
  5. Оп­ция android:useEmbeddedDex=true может помочь. По умол­чанию Android запус­кает код при­ложе­ния не из самого пакета при­ложе­ния, а из спе­циаль­но под­готов­ленно­го odex-фай­ла, содер­жащего опти­мизи­рован­ный и час­тично ском­пилиро­ван­ный в машин­ные инс­трук­ции код. Такой код быс­трее работа­ет, но, как ока­залось, доль­ше запус­кает­ся. Если исполь­зовать опцию android:useEmbeddedDex=true, мож­но добить­ся уско­рения запус­ка при­ложе­ния, но про­играть в ско­рос­ти работы. Кому что важ­нее.
  6. onCreate — то мес­то, где обыч­но кро­ется проб­лема. Логич­но, что боль­шую часть вре­мени запус­ка сжи­рает метод onCreate основной активнос­ти при­ложе­ния. Это имен­но то мес­то, куда сто­ит при­ложить уси­лия.
  7. Об­фуска­ция с помощью ProGuard серь­езно повыша­ет про­изво­дитель­ность. ProGuard сок­раща­ет раз­мер кода при­ложе­ния, так что его вли­яние дос­таточ­но ожи­даемо. С дру­гой сто­роны, автор не уточ­няет, прос­то ли он вклю­чил ProGuard или про­тес­тировал релиз­ную сбор­ку при­ложе­ния. Все‑таки дебаг‑сбор­ки всег­да работа­ют мед­леннее из‑за логиро­вания и отклю­чен­ных опти­миза­ций в биб­лиоте­ках (в одном из пре­дыду­щих выпус­ков дай­джес­та было показа­но, что отла­доч­ная сбор­ка замед­ляет ини­циали­зацию биб­лиоте­ки корутин при­мер­но в десять раз — до 100 мил­лисекунд).

Очередная подборка советов

Top Put_your_number Kotlin utils we use all over our project — пять при­емов раз­работ­ки для Android, которые могут помочь писать более понят­ный и тес­топри­год­ный код. Наибо­лее инте­рес­ные при­емы:

Прок­си для дос­тупа к ресур­сам. В Android есть класс Resources, который пред­назна­чен для дос­тупа к ресур­сам при­ложе­ния. Стан­дар­тный путь получе­ния объ­екта это­го клас­са — через Context. Одна­ко это не всег­да удоб­но. Нап­ример, при бин­динге ViewHolder’а получать дос­туп к ресур­сам при­дет­ся при­мер­но так:

viewHolder.itemView.context.resources.getDimensionPixelSize(R.dimen.my_dimen)

Очень длин­ная стро­ка. К тому же мы не смо­жем внят­но про­тес­тировать этот код (без исполь­зования гряз­ных хаков вро­де Robolectric).

Ре­шение обе­их проб­лем сос­тоит в том, что­бы соз­дать обер­тку для клас­са Resources:

interface ResourcesProvider {

val isRtl: Boolean

@ColorInt

fun getColor(@ColorRes resId: Int): Int

fun getString(@StringRes resId: Int): String

fun getString(@StringRes resId: Int, vararg args: Any): String

fun getDimen(@DimenRes resId: Int): Float

fun getDimenInt(@DimenRes resId: Int): Int

// Другие методы

}

@Suppress("TooManyFunctions")

inline class AppResourcesProvider(

private val context: Context

) : ResourcesProvider {

override val isRtl: Boolean get() = context.isRtl

@ColorInt

override fun getColor(resId: Int) = context.getColorCompat(resId)

override fun getColorStateList(resId: Int): ColorStateList? = context.getColorStateListCompat(resId)

override fun getString(resId: Int) = context.getString(resId)

override fun getString(resId: Int, vararg args: Any) = context.getString(resId, *args)

override fun getDimen(resId: Int): Float = context.resources.getDimension(resId)

override fun getDimenInt(resId: Int) = context.resources.getDimensionPixelSize(resId)

// Другие методы

}

Бла­года­ря интерфей­су класс лег­ко под­делать (mocking) для тес­тирова­ния. К тому же ему мож­но делеги­ровать все вызовы типа getColor в том же ViewHolder:

abstract class ResViewHolder(

itemView: View,

) : RecyclerView.ViewHolder(itemView), ResourcesProvider by AppResourcesProvider(itemView.context)

Прос­то и удоб­но, а бла­года­ря исполь­зованию сло­ва inline — в боль­шинс­тве слу­чаев класс даже не будет соз­дан — код методов будет встро­ен в вызыва­ющую сто­рону.

Фун­кции‑рас­ширения для изме­нения отсту­пов. Во вре­мя раз­работ­ки неред­ко воз­ника­ет необ­ходимость изме­нить отступ того или ино­го эле­мен­та интерфей­са. Популяр­ная биб­лиоте­ка рас­ширений Android KTX пред­лага­ет делать это так:

view.updateLayoutParams {

updateMarginsRelative(start = newMargin)

}

И это доволь­но стран­ный и сов­сем не опи­сатель­ный спо­соб. Вмес­то это­го мы бы хотели делать это как‑то так:

view.margin.start = newMargin

Код, поз­воля­ющий сде­лать это:

val View.margin get() = ViewMargin(this)

inline class ViewMargin(private val view: View) {

var start: Int

get() = view.marginStart

set(value) = applyMargin { marginStart = value }

var top: Int

get() = view.marginTop

set(value) = applyMargin { topMargin = value }

var end: Int

get() = view.marginEnd

set(value) = applyMargin { marginEnd = value }

var bottom: Int

get() = view.marginBottom

set(value) = applyMargin { bottomMargin = value }

var horizontal: Int

get() = start + end

set(value) {

start = value

end = value

}

var vertical: Int

get() = top + bottom

set(value) {

top = value

bottom = value

}

var total: Int

@Deprecated("No getter for that field", level = DeprecationLevel.HIDDEN)

get() = throw UnsupportedOperationException("No getter for such field")

set(value) {

horizontal = value

vertical = value

}

private inline fun applyMargin(body: ViewGroup.MarginLayoutParams.() -> Unit) {

if (view.layoutParams is ViewGroup.MarginLayoutParams) {

view.updateLayoutParams(body)

} else {

throw IllegalStateException("Parent layout doesn't support margins")

}

}

}