Записки гиканутого

@channel_n5 Нравится 0
Это ваш канал? Подтвердите владение для дополнительных возможностей

Android: хакинг, кодинг и прочая жесть @ezobnin
Гео и язык канала
Россия, Русский
Категория
Технологии


Гео канала
Россия
Язык канала
Русский
Категория
Технологии
Добавлен в индекс
28.10.2017 23:53
реклама
TGStat Bot
Бот для получения статистики каналов не выходя из Telegram
SearcheeBot
Ваш гид в мире Telegram-каналов
Telegram Analytics
Подписывайся, чтобы быть в курсе новостей TGStat.
235
подписчиков
~72
охват 1 публикации
~33
дневной охват
~3
постов / нед.
30.6%
ERR %
0.45
индекс цитирования
Репосты и упоминания канала
3 упоминаний канала
0 упоминаний публикаций
1 репостов
Сисодминиум
Новые каналы
Groks
Android Tools
Каналы, которые цитирует @channel_n5
trgt
Android Tools
Последние публикации
Удалённые
С упоминаниями
Репосты
Code expressivity++ with operator overloading - статья о перегрузке операторов на примере, как бы странно это не звучало, хора певцов.

Допустим у нас есть класс Choir (хор), в который можно добавлять певцов (Singer):

class Choir {
private val singers = mutableListOf()

fun addSinger(singer: Singer) {
singers.add(singer)
}

...
}

Теперь, чтобы добавить певца, необходимо сделать так:

choir.addSinger(singer)

Все понятно и логично, но было бы более логичным делать это так:

choir += singer

Именно для этого нужна перегрузка операторов. Например, чтобы добавить оператор += достаточно сделать так:

class Choir {
private val singers = mutableListOf()

operator fun plusAssign(singer: Singer) {
singers.add(singer)
}
}

Обрати внимание на ключевое слово operator и имя функции (plusAssign). У каждого оператора есть свое имя ([полный список](https://kotlinlang.org/docs/reference/operator-overloading.html)), а перегрузка одного оператора никогда не приводит перегрузке его "родственников". К примеру, перезагрузка оператора + не приведет к перегрузке оператора ++.

Перегрузка не всех операторов полезна во всех случаях. В данном случае может стать полезным оператор вхождения (contains):

operator fun contains(s: Singer) : Boolean {
return singers.contains(s)
}

Благодаря ему можно сделать так:

if (singerMeghan in choir) {
println("Meghan is a part of the choir!")
}

Перегрузку операторов можно использовать в функциях-расширениях:

operator fun ViewGroup.plusAssign(other: View) = addView(other)

Теперь добавить View к ViewGroup можно с помощью оператора:

viewGroup += view

Как и в других языках при перегрузке операторов в Kotlin следует руководствоваться простым правилом: краткость не всегда повышает читаемость кода. Стоит несколько раз подумать перед тем как применять перегрузку.
Читать полностью
Functional Programming in Kotlin - серия статей о функциональном программировании на Kotlin.

В функциональном программировании функции языка программирования рассматриваются с точки зрения математических функций, которые представляют собой зависимость одной переменной величины от другой. Такие функции называются чистыми (pure function). Они имеют ряд ограничений в сравнении с функциями, к которым привыкли программисты на традиционных языках:

- чистая функция всегда возвращает значение;
- она не выбрасывает исключений;
- не изменяет данных за пределами своей области видимости;
- не изменяет свои аргументы;
- всегда возвращает одинаковое значение для одних и тех же аргументов.

Пример чистой функции на языке Kotlin:

fun division(x: double, y: Double): Double = x / y

Пример не чистой функции:

fun addItems(value: Int, list: MutableList): List {
list.add(value)
return list
}

Функция из второго примера изменяет один из своих аргументов (list) и поэтому не может считаться чистой.

Сторонники функционального программирования утверждают, что приложения, написанные с использованием исключительно или преимущественно чистых функций, всегда более надежны, так как приложение состоит из набора понятных и простых строительных блоков, каждый из которых легко протестировать.

Для объединения функций применяется композиция, когда несколько функций используются для создания новой функции. Интуитивно ты можешь попробовать сделать это следующим образом:

fun f(x: Int) = x + 1
fun g(x: Int) = 2 * x
println(f(g(2))

Но это не верно. Настоящая композиция должна выполняться как операция над функциями:

fun f(x: Int) = x + 1
fun g(x: Int) = 2 * x
fun compose(f: (Int) -> Int, g: (Int) -> Int): (Int) -> Int = { x -> f(g(x)) }

val fog = compose(::f, ::g)
println(fog(2))

Предыдущий пример композиции подходит только для типов Int, но его можно расширить для поддержки любых типов данных:

fun r1(x: Boolean): Int = if(x) 1 else 0
fun r2(x: Int): Boolean = if(x == 0) false else true

fun compose(f: (U) -> V, g: (T) -> U): (T) -> V = { f(g(it)) }

val r1of2 = compose(::r1, ::r2)
println(r1of2(2))
Читать полностью
Why Kotlin’s Elvis Operator is Better Than Swift’s Guard Statement - заметка о том, почему языковые конструкции для защиты от разыменования null-переменной в Kotlin лучше, чем в Apple Swift.

Все, кто программировал на Swift, должны знать о ключевом слове guard:

func process(couldBeNullMesage: String?) {
guard let notNullMessage = couldBeNullMesage else { return }
printMessage(notNullMessage)
}

func printMessage(message: String) {
print(message)
}

Guard защищает от возникновения ситуаций, когда значение null может быть присвоено переменной, которая не может быть null. В данном случае функция process() будет завершена если аргумент couldBeNullMesage будет равен null.

Теперь посмотрим как сделать то же самое в Kotlin с помощью четырех разных способов (от худшего к лучшему).

1. Ключевое слово let.

fun process(couldBeNullMesage: String?) {
couldBeNullMesage?.let {
printMessage(it)
}
}

fun printMessage(message: String) {
println(message)
}

Это выбор новичков. Способ хорошо работает, но создает дополнительные отступы.

2. Оператор elvis:

fun process(couldBeNullMesage: String?) {
val notNullMessage = couldBeNullMesage ?: return
printMessage(notNullMessage)
}

fun printMessage(message: String) {
println(message)
}

Результат идентичен работе оператора guard, но сама запись короче.

3. Старый добрый if-else:

fun process(couldBeNullMesage: String?) {
if (couldBeNullMesage == null) return
printMessage(couldBeNullMesage)
}

fun printMessage(message: String) {
println(message)
}

Компилятор Kotlin достаточно умный чтобы понять, что в функции printMessage используется переменная, которая уже не может быть null.

4. И снова оператор elvis:

fun process(couldBeNullMesage: String?) {
couldBeNullMesage ?: return
printMessage(couldBeNullMesage)
}

fun printMessage(message: String) {
println(message)
}

Здесь мы совместили локаничность оператора elvis и автоматического приведения типов из предыдущего примера.

Итого:

// Swift
guard let notNullMessage = couldBeNullMesage else { return }
// Kotlin
couldBeNullMesage ?: return
Читать полностью
Android Logging on Steroids: Clickable Logs With Location Info - заметка о том, как сделать логи более информативными и приятными для чтения.

Все знают как выглядят стектрейсы в логах Android при падении приложения. Ты сразу получаешь ссылку на файл, а после клика попадаешь на нужную строку в этом файле. Почему бы не сделать то же самое с обычными отладочными лог-сообщениями?

Нам понадобится библиотека логирования Timber. Подключаем ее к проекту:

implementation 'com.jakewharton.timber:timber:4.7.1'

Далее добавляем в метод onCreate() приложения следующие строки:

class HyperlinkedDebugTree : Timber.DebugTree() {
override fun createStackElementTag(element: StackTraceElement): String? {
with(element) {
return "($fileName:$lineNumber) $methodName()"
}
}
}

Timber.plant(HyperlinkedDebugTree())

Это все, теперь все вызовы Timber.d() будут формировать логи со ссылкой на исходный код.

Чтобы сделать жизнь еще проще создадим следующую функцию-расширение:

inline fun Any?.log(prefix: String = "object:") = Timber.d("$prefix ${toString()}")

Теперь содержимое любой переменной (например, id) можно вывести в лог таким образом:

id.log()
Читать полностью
Playing Around With The Fuchsia Operating System - исследование безопасности операционной системы Fuchsia, которая, по слухам, должна придти на смену Android. Исследователи нашли в ОС несколько стандартных багов, которые, тем не менее, не дают каких-либо полномочий в системе в силу самой архитектуры ОС. И именно описание архитектуры - наиболее интересная часть статьи.

Fuchsia - это микроядерная операционная система на базе ядра Zircon, написанного на язык C++. Все остальные компоненты ОС, обычно реализованные внутри ядра, вынесены в сервисы пространства пользователя и общаются между собой с помощью независимого от языка механизма IPC. Часть этих компонентов, как и само ядро, реализованы на языке C++ (драйверы USB, например), другая часть - на других языках. Например, TCP/IP-стек написан на языке Rust, также поддерживается язык Go.

В отличие от других микроядерных ОС, драйверы Fuchsia могут быть объединены в один процесс, именуемый devhost. Например, драйверы AHCI, SATA и файловой системы могут быть объединены в один devhost.

Такая архитектура позволяет сократить количество переключений контекста и сделать ОС более эффективной. С другой стороны, надежность компонентов снижается, но не катастрофически - devhost-процессы обычно объединяют в себе драйверы одного стека, поэтому уязвимость в одном процессе Devhost приведет к уязвимости в драйверах одного стека и не заденет другие (например, драйверы USB). Fuchsia активно использует модуль IOMMU для защиты памяти устройств. Каждый процесс Devhost имеет право на обращение только к своим адресам ввода-вывода.

Как и Unix, Fuchsia следует концепции "все есть файл", когда файлы могут представлять собой как данные на диске, так и устройства или каналы коммуникации. Однако, в отличие от Unix, Fuchsia не открывает каждому процессу доступ ко всей файловой иерархии, а создает для него собственное пространство имен (также как это делает Plan 9, неудавшаяся преемница Unix). Так реализуется идея песочницы, когда каждое приложение имеет доступ только к ограниченному набору ресурсов.

На самом низком уровне (уровне микроядра Zircon) все файлы представлены хендлами (handle), представляющими собой нечто вроде токена для доступ к файловому объекту. Каждый хендл должен иметь свой вид (kind) и права, которые контролируют доступ к системным вызовам.

В репозитории Fuchsia все компоненты имеют unit-тесты и фуззеры. Код собирается с активированными технологиями защиты: ASLR, DEP, SafeStack, ShadowCallStack и AutoVarInit. В случае кода на C++ используются дополнительные меры для повышения надежности. Например, часто используется версия оператора [] с проверками границ массива.
Читать полностью
Cancellation in coroutines - статья о работе с Kotlin Coroutines, а точнее о том, как правильно их завершать.

Существует два основных способа завершения короутин. Первый: через завершение CoroutineScope, которому принадлежат короутины:

val job1 = scope.launch { … }
val job2 = scope.launch { … }

scope.cancel()

Второй: прямое завершение отдельно взятых короутин:

val job1 = scope.launch { … }
val job2 = scope.launch { … }

job1.cancel()

Вроде бы все просто. Но есть несколько неочевидных моментов:

1. Завершение короутины должно быть кооперативным. Сам по себе метод cancel() не завершает короутину, а лишь посылает ей сигнал завершения. Короутина должна сама проверить получила ли она сигнал завершения используя поле Job.isActive и метод ensureActive(). Первое будет содержать false если короутина получила сигнал завершения, второй выбросит CancellationException.
2. Все стандартные suspend-функции пакета kotlinx.coroutines (withContext(), delay() и так далее) умеют реагировать на сигнал завершения, поэтому при их использовании программисту не обязательно делать это самостоятельно.
3. При завершении короутины ее родитель получает исключение CancellationException.
4. Завершенный CoroutineScope больше нельзя использовать для запуска короутин.
Читать полностью
The Top 20 Android Studio Plugins - обзор 20 плагинов Android Studio на все случаи жизни. Наиболее интересные экземпляры:

- Rainbow Brackets - раскрашивает парные скобки в различные цвета, упрощая поиск закрывающей/открывающей скобки;
- ADB Idea - плагин, позволяющий быстро выполнить различные команды ADB: убить приложение, отозвать полномочия;
- ADB Wifi - плагин для управления/отладки смартфона без по Wifi;
- Gradle Killer - убивает процесс Gradle одной кнопкой (полезно для быстрой остановки сборки или освобождения памяти);
- Kotlin Fill Class - позволяет быстро создать класс с дефолтовыми свойствами;
- TabNine - плагин автодополнения на основе нейросети, обученной на коде GitHub;
- SQLScout - плагин для управления базами SQLite в режиме реального времени;
- Material Design Icon Generator - плагин для генерации иконок в стиле MD.
Читать полностью
In-App Updates: Getting Started - большой туториал по реализации In-App Updates - функции, с помощью которой приложение может принудительно обновить себя или вывести уведомление об обновлении.

Функция реализована в библиотеке Google Play Core и предлагает два способа обновления приложения: immediate и flexible. Первый принудительный: приложение автоматически запускает процесс обновления, а пользователю остается только наблюдать за процессом. Второй позволяет проверить наличие обновление и вывести сообщение/уведомление/кнопку, нажав на которое пользователь может самостоятельно запустить процесс обновления.

И в том, и в другом случае проверить наличие наличие новой версии и запустить процесс обновления очень просто. Например, принудительное обновление запускается всего в несколько строчек:

private const val REQUEST_UPDATE = 100

fun checkForUpdate(activity: Activity) {
val appUpdateManager = AppUpdateManagerFactory.create(getAppContext())
val appUpdateInfo = appUpdateManager.appUpdateInfo
appUpdateInfo.addOnSuccessListener {
handleImmediateUpdate(activity, appUpdateManager, appUpdateInfo)
}
}

private fun handleImmediateUpdate(activity: Activity, manager: AppUpdateManager, info: Task) {
if ((info.result.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE ||
info.result.updateAvailability() == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS) &&
info.result.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)) {

manager.startUpdateFlowForResult(info.result, AppUpdateType.IMMEDIATE, activity, REQUEST_UPDATE)
}
}

Все, что делает этот код - это проверяет есть ли обновление (UpdateAvailability.UPDATE_AVAILABLE), подходит ли оно для принудительной установки (AppUpdateType.IMMEDIATE) и, если да, устанавливает его.

Flixible-обновления работают примерно также с тем исключением, что процесс обновления можно запустить в фоне, а затем попросить пользователя перезапустить приложение.
Читать полностью
Showing the Android Keyboard Reliably - статья разработчиков Square о том как показать клавиатуру и не поиметь проблем.

Суть проблемы: в Android есть способ форсировать показ клавиатуры без необходимости дожидаться пока пользователь кликнет на поле ввода:

val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT)

Он работает, но только в том случае, если поле ввода (в данном случае - editText) будет иметь фокус в момент вызова метода showSoftInput(). Это ограничение можно обойти передав методу showSoftInput() флаг InputMethodManager.SHOW_FORCED. Но тогда клавиатура не будет спрятана автоматически и останется на экране, например, если пользователь свернет приложение.

Статья описывает способ обхода этих проблем. Тебе необходимо повесить на поле ввода листенер, который сработает когда поле ввода получит фокус и в этот момент покажет клавиатуру.

Код функции-расширения, которая корректно показывает клавиатуру во всех ситуациях:

fun View.focusAndShowKeyboard() {
fun View.showTheKeyboardNow() {
if (isFocused) {
post {
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT)
}
}
}

requestFocus()
if (hasWindowFocus()) {
showTheKeyboardNow()
} else {
viewTreeObserver.addOnWindowFocusChangeListener(
object : ViewTreeObserver.OnWindowFocusChangeListener {
override fun onWindowFocusChanged(hasFocus: Boolean) {
if (hasFocus) {
this@focusAndShowKeyboard.showTheKeyboardNow()
viewTreeObserver.removeOnWindowFocusChangeListener(this)
}
}
})
}
}
Читать полностью
Android Battery Testing at Microsoft YourPhone - еще одна статья разработчиков из Microsoft. На этот раз авторство принадлежит команде приложения YourPhone (того, что позволяет управлять телефоном из Windows), а статья посвящена измерению потребления батареи.

Большая часть статьи - вода, но в конце есть мякотка - скрипт для запуска тестирования батареи и часть кода для парзинга результатов работы скрипта. Код скрипта:

# Эмулируем отключение смартфона от источника питания
adb shell dumpsys unplug
# Сбрасываем статистику использования батареи
adb shell dumpsys batterystats --reset

# Запускаем тесты
...

# Останавливаем тесты

# Получаем статистику работы батареи (вывод этой команды надо сохранить)
adb shell dumpsys batterystats

# Отключаем эмуляцию отключения от источника питания
adb shell dumpsys batterystats reset

Далее результат работы команды dumpsys batterystats reset можно пропарзить чтобы получить сводные данные об использовании батареи. В статье приведен фрагмент приложения на C#, которое выводит такой результат:

Total Usage: 62.1mAh
Cpu Usage: 1.21mAh
Wifi Usage: 60.9mAh
Wakelock Usage: 0mAh
Bluetooth Usage: 0mAh

Разумеется, его можно переписать на любом другом языке.
Читать полностью
App size reduction at Microsoft SwiftKey - сборник заметок разработчиков клавиатуры SwiftKey (Microsoft) о том как сократить размер скачиваемого и установленного приложения.

1. Установленное приложение весит намного больше скачиваемого потому, что во время установки Android сохраняет в памяти устройства не только сам APK-файл, но и некоторые извлеченные из него компоненты: верифицированный файл DEX (VDEX - Verified DEX) и нативные библиотеки. Также, через некоторое время виртуальная машина создает файл ODEX - оптимизированную версию файла DEX, пропущенную через AOT-компилятор (некоторые части байткода заменяются на машинные инструкции). Причем начиная с Android 9 - этот файл может быть получен сразу из Google Play.

В целях тестирования файл ODEX можно создать принудительно:

adb shell cmd package compile -m speed-profile -f имя.пакета

2. В случае с SwiftKey помогли следующие флаги ProGuard: repackageclasses, renamesourcefileattribute и allowaccessmodification. Но появился побочный эффект: еще большее запутывание стектрейсов в системе анализа сбоев.

3. Компилятор R8 (в новых версиях Android Studio включен по умолчанию) сократил размер установленного приложения на 1.3 Мб, но замедлил время его старта.

4. По умолчанию при сборке приложения среда разработки сжимает нативные библиотеки. При установке приложения на устройство Android распаковывает библиотеки в специальный каталог с данными приложение. Это приводит к дополнительному расходу пространства устройства. Чтобы поменять такое поведение и заставить Android использовать библиотеки прямо из пакета следует внести два изменения в проект.

В файл build.gradle добавить такие строки:

android {
aaptOptions {
noCompress 'so'
...
}
...
}

В AndroidManifest.xml - такие:



Имей ввиду, что некоторые библиотеки, например, SoLoader, будут распаковывать библиотеки принудительно, несмотря на используемые опции.

5. По умолчанию при использовании обфускации/минимизации приложения, среда разработки удалит все неиспользуемые ресурсы. Также ты можешь удалить их интерактивно используя следующую опцию Android Studio: "Refactor -> Remove Unused Resources".

6. Используемое приложением место можно сократить преобразовав изображения в формат WebP (клик правой клавишей на папке drawable, затем пункт "Convert to WebP"). Экономия составит примерно 25%. Еще большего выигрыша можно достигнуть заменив растровые изображения на векторные. Однако в этом случае автоматически преобразовать их не получится.

7. При сборке среда разработки помещает все используемые приложением строки в файл resources.arsc. Туда попадают строки на всех языках включая строки из используемых в проекте библиотек. Проблема здесь в том, что если библиотека переведена на большее количество языков, чем само приложение, строки на этих языках все равно попадут в пакет. Избавиться от них можно перечислив используемые приложением языки в конфиге Gradle:

android {
defaultConfig {
resConfigs "en", "es"
}
}

Теперь в приложение попадут строки только на английском и и испанском.

8. Хороший выигрыш в размере загружаемого приложения даст использование App Bundle. Новые версии Android Studio по умолчанию предлагают собирать приложение в App Bundle вместо классического APK. Bundle затем можно залить в Google Play и он автоматически будет разбит на несколько APK для разных платформ, включая отдельные дополнительные APK для разных языков и регионов. В этом случае итоговый размер пакета, который загружает на смартфон пользователь, обычно становится намного меньше.

Все эти техники позволили сократить размер пакета SwiftKey на 50% (с 27.6 Мб до 14.3 Мб), а занимаемое место после установки - на 40% (с 81.5 Мб до 48 Мб).
Читать полностью
A cautionary tale on Android: do not call System.exit() - небольшая статья о том, почему не стоит использовать System.exit() в своем приложении.

Документации Android сообщает нам, что метод System.exit() (и его эквивалент в Kotlin: exitProcess()) делает следующее: завершает текущую виртуальную машину и сообщает системе было ли это корректное завершение или нет (0 - все нормально, больше нуля - произошло что-то плохое).

Так как в Android каждое приложение исполняется в собственной виртуальной машине, можно предположить, что System.exit() по сути приводит к полному уничтожению приложения. Это действительно так, но есть один нюанс: если в момент вызова у приложения были другие активности в состоянии "paused", то они останутся в стеке активностей. Это, в свою очередь, приведет к тому, что после вызова System.exit() произойдет уничтожение текущей активности и самого приложения, а затем система вернет управление к предыдущей активности приложения. Но так как приложение уже мертво, система перезапустит его чтобы показать активность.

Решить эту проблему в большинстве случаев можно используя метод finishAffinity() который приводит к корректному завершению текущей активности приложения, а также всех активностей с тем же значением affinity. Так как по умолчанию Android назначает всем активностям приложения одинаковый affinity, равный имени пакета приложения, это приведет к закрытию всех активностей.

От себя также добавлю, что System.exit() приводит к проблемам на смартфонах Huawei. Прошивка EMUI считает, что завершение виртуальной машины - это ошибочная ситуация, независимо от кода возврата. Поэтому пользователь видит на экране предупреждение с рекомендацией удалить приложение.
Читать полностью
Таблица ограничений
App Standby Buckets In Android - небольшая заметка о функции App Standby Buckets, появившейся в Android 9.

App Standby Buckets - новая энергосберегающая функция Android 9, расширяющая и дополняющая механизм Doze, появившиеся в Android 6. Идея в этот раз состоит в том, чтобы разделить все установленные на смартфоне приложения на категории в зависимости от того, насколько часто они используются.

Всего существует пять основных категорий:

- Active - приложение используется в данный момент или использовалось совсем недавно;
- Working set - часто используемые приложения;
- Frequent - регулярно используемые приложения, но не обязательно каждый день;
- Rare - редко используемые приложения;
- Never - приложение установлено, но ни разу не запускалось.

В простейшем случае Android группирует приложения по категориям основываясь на времени последнего запуска, но производитель смартфона может использовать другие способы группировки. Например, смартфоны Google Pixel используют для этих целей нейронную сеть.

В зависимости от группы, система применяет к приложениям различные ограничения, включая ограничения на запуск фоновых задач (Jobs), срабатывание таймеров (Alarm), доступность сетевых функций и push-уведомлений (Firebase Cloud Messaging - FCM).

Интересный побочный эффект этой системы в том, что если все взаимодействие пользователя с приложением будет сводиться только к прочтению и смахиванию уведомлений, то через несколько дней приложение перейдет в группу Rare и будет серьезно урезано в возможностях.

Также следует иметь ввиду, что приложение не будет урезано в правах если находится в списке исключений системы энергосбережения или телефон находится на зарядке.
Читать полностью
Yet Another Tamper Detection in Android - статья о том как защитить приложение с помощью проверки цифровой подписи в нативной библиотеке.

Любое приложение для Android имеет цифровую подпись, с помощью которой можно подтвердить его авторство. Проблема только в том, что сверка цифровой подписи происходит только во время обновления приложения (подписи не совпадают - обновить нельзя), но не его первой установки. Это значит, что взломщик может разобрать любое приложение, взломать его или внедрить новую функциональность, а затем собрать с использованием своего ключа. А пользователь спокойно его установит при необходимости удалив оригинальную версию приложения.

Бороться с пиратством можно внедрив код проверки цифровой подписи прямо в само приложение. При старте приложение получает свою собственную цифровую подпись, сравнивает ее с сохраненным образцом и, в случае несовпадения, завершает само себя с показом сообщения о пиратстве.

Недостаток этого подхода в том, что для сверки контрольной суммы используются стандартные API Android (packageManager.getPackageInfo()), ориентируясь по которым взломщик может найти код сверки контрольной суммы и просто вырежет его из приложения.

Автор статьи предлагает разметить данный код в написанной на языке Си библиотеке. Вместо API Android это библиотека использует собственные средства сверки цифровой подписки (в частности код из библиотек libzip и mbedtls). Также в библиотеке применен ряд средств защиты от реверса, таких как собственные реализации функций libc и позаимствованный из OpenSSL способ определения факта изменения кода библиотеки.

Последний работает так: при сборке в секции text (содержит код) и rodata (содержит константы, включая хэш сертификата) вставляются специальные маркеры, которые помечают начало и конец секции. Далее для данных между этими маркерами вычисляется HMAC и записывается в секцию данных. Во время вызова функции сверки цифровых подписей библиотека проверяет собственную целостность с помощью HMAC.

Код проекта опубликован на GitHub: https://github.com/darvincisec/DetectTamper
Читать полностью
Composition over inheritance (and Kotlin) - небольшая заметка, хорошо иллюстрирующая принцип композиции объектов и его преимущества перед наследованием.

Взгляни на следующий код:

open class Parent {
fun parentFunctionality() {}
}

open class Child(): Parent() {
fun childFunctionality() {}
}

class Grandchild constructor() : Child() {
fun grandchildFunctionality() {}
}

Это канонический пример наследования в объектно-ориентированном программировании. Объект класса Grandchild сможет вызывать методы parentFunctionality() и childFunctionality(). Код красив и замечателен. Но представь себе, что будет если сильно усложнить этот пример, добавив в каждый класс множество новых открытых методов и связав их между собой. В какой-то момент ты можешь оказаться в ситуации, когда ты переопределяешь метод, который используется другим методом, и ,таким образом, ломаешь функциональность всего объекта.

Разумеется, грамотный дизайн поможет избежать этой проблемы, но что если команда разработчиков состоит не только из тебя одного и в коде есть множество классов с незнакомым тебе кодом?

На самом деле в современном мире наследование уже не считается единственно верным способом проектирования приложения. Во многих случаях более предпочтительным будет принцип *композиции*, когда вместо класса-наследника создается новый класс, который не переопределяет методы предка, а вызывает их напрямую.

Предыдущий код, переписанный с использованием принципа композиции, будет выглядеть так:

class Parent {
fun parentFunctionality() {}
}

class Child() {
private val parent = Parent()

fun parentFunctionality() { parent.parentFunctionality() }
fun childFunctionality() {}
}

class Grandchild {
private val parent = Parent()
private val child = Child()

fun parentFunctionality() { parent.parentFunctionality() }
fun childFunctionality() { child.childFunctionality() }
fun grandchildFunctionality() {}
}

Принцип композиции позволяет не только избежать трудно уловимых багов, но и упрощает тестирование (класс-предок легко заменить на фейковую реализацию) и сопровождение приложения (код становится более очевидным и понятным).

Kotlin содержит несколько инструментов, которые могут упростить композицию классов и даже принудить тебя использовать ее вместо наследования. Например, именно по причине возможных багов, Kotlin делает классы не наследуемыми по умолчнию. Также здесь есть поддержка синглтонов на уровне языка, так что многие классы можно быстро оформить в виде синглтонов и напрямую вызывать их методы и без необходимости создавать класс и хранить на него ссылку.

Функция-делегат lazy также помогает создавать композиции, а точнее минимизировать возможный оверхед. В следующем коде объект parent будет создан только в момент первого обращения к нему, то есть не будет занимать дополнительную память если вообще не используется:

open class Parent {
fun parentFunctionality() {}
}

open class Child() {
val parent by lazy { Parent() }
...
fun childFunctionality() {}
}

Ну и последнее - функции-расширения, которые позволяют добавить новые методы к существующему классу без необходимости наследоваться от него:

class SystemClass {
...
}

fun SystemClass.newFunctionality() {}

SystemClass().newFunctionality()
Читать полностью
Zero-cost abstractions in Kotlin - статья с подробным объяснением новой экспериментальной языковой конструкции Kotlin под названием inline-классы.

Одна из ключевых особенностей языка Kotlin - null safety, которая гарантирует, что программист не сможет по ошибке вызвать методы объекта, имеющего значение null, или передать этот объект в качестве аргумента другим методам. Null safery существенно повышает надежность кода, но он не защищает от других ошибок программиста.

Допустим, у тебя есть база котов и собак, которых ты идентифицируешь по ID. Так же у тебя есть метод getDogById(dogId: Long), который возвращает информацию о собаке с конкретным ID. Очевидно, что если в качестве ID собаки ты передашь методу ID кошки, это будет ошибкой, которая приведет к неопределенному результату. Но ни среда разработки, ни компилятор, не скажу тебе о ней.

Еще во времена Java программисты придумали метод обойти эту проблему с помощью так называемых классов-оберток. Ты просто создаешь класс DogId с единственным полем (ID собаки) и используешь его везде, где раньше использовал тип Long в качестве ID. Все остальное компилятор и среда разработки сделают за тебя: они просто не позволят предать DogId в качестве аргумента функции, которая ожидает CatId - это ошибка.

Но есть в классах-врапперах одна проблема. Создание объектов - не самая дешевая операция. Если ты будешь плодить их на каждый чих, то вскоре заметишь возросшее потребление оперативной памяти и процессорных ресурсов.

И здесь на сцену выходят инлайн-классы. По своей сути инлайн-класс - это класс-враппер с одним параметром, который при компиляции разворачивается в этот параметр, чтобы избежать накладных расходов. Например:

inline class DogId(val id: Long)
val dog = getDogById(DogId(100L))

Данный код написан с использованием враппера чтобы избежать описанной выше ошибки. Однако в процессе компиляции объект DogId будет заменен на Long, так что никаких дополнительных накладных расходов не будет.

Компилятор накладывает следующие ограничения на инлайн-классы:

- Не больше одного параметра;
- Никаких теневых полей;
- Никаких блоков инициализации;
- Никакого наследования.

Однако инлайн-классы могут:

- Реализовать интерфейс;
- Иметь свойства и функции.

Так же стоит иметь ввиду, что инлайн-классы не всегда будут развернуты в свой параметр. Главное правило здесь: *объект инлайн-класса не будет развернут если используется в качестве аргумента функции, ожидающей другой тип*.

Например, функции для работы с коллекциями (listOf(), setOf() и им подобные), обычно принимают на вход параметр типа Object или Any, так что переданный им объект инлайн-класса развернут не будет. Функция equals() также принимает в качестве аргумента тип Any, поэтому следующие два примера будут работать одинаково, но второй приведет к дополнительным накладным расходам:

val doggo1 = DogId(1L)
val doggo2 = DogId(2L)

// Оба объекта будут развернуты
doggo1 == doggo2

// doggo1 будет развернут, а doggo2 - нет
doggo1.equals(doggo2)

Также, объект не будет развернут если объект инлайн-класса передать функции, аргумент которой имеет nullable-тип:

val doggo = DogId(1L)

fun pet(doggoId: DogId?) {}

// Объект не будет развернут
pet(doggo)

Интересно также, что компилятор поддерживает переопределение функций, принимающих объект инлайн-класса и его не обернутый аналог. То есть следующий код будет успешно скомпилирован:

fun pet(doggoId: Long) {}
fun pet(doggoId: DogId) {}

Ну и последнее, что стоит иметь ввиду: инлайн-классы - это экспериментальная возможность, которая может измениться со временем или будет удалена.
Читать полностью
A first look at AndroidX Activity Result APIs - небольшая заметка о решении одной из самых раздражающих задач, возникающих при разработке приложений для Android.

Речь идет о функции startActivityForResult(), которая позволяет запустить активность другого приложения чтобы переложить на нее решение определенной задачи: получение снимка с помощью камеры, выбор файла и так далее. Данный механизм серьезно облегчает жизнь разработчика, но реализован самым неудобным из возможных способов. Разработчику необходимо запустить активность, передав ей специальный код, а затем ждать результат в колбеке, реализованном с помощью переопределения метода onActivityResult() в активности или фрагменте. И все бы ничего, но точно таким же способом реализован запрос полномочий (он обернут в другие методы, но под капотом тот же механизм), так что код приложения в конечном итоге расползается по множеству внешне никак не связанных между собой функций.

Существует масса способов решения этой проблемы с помощью сторонних библиотек, но эта статья рассказывает об официальном решении от Google. В альфа-версии библиотеки AndroidX Activity наконец появился удобный в использовании API, позволяющий работать с активностями других приложений не размазывая код по активности своего приложения.

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

class MyContract : ActivityResultContract() {
companion object {
const val ACTION = "com.myapp.action.MY_ACTION"
const val INPUT_INT = "input_int"
const val OUTPUT_STRING = "output_string"
}

override fun createIntent(input: Int): Intent {
return Intent(ACTION)
.apply { putExtra(INPUT_INT, input) }
}

override fun parseResult(resultCode: Int, intent: Intent?): String? {
return when (resultCode) {
Activity.RESULT_OK -> intent?.getStringExtra(OUTPUT_STRING)
else -> null
}
}
}

Затем мы используем prepareCall() чтобы получить объект класс ActivityResultsLauncher, с помощью которого запускаем активность и получаем результат в коллбеке:

class MyActivity : AppCompatActivity() {
private val myActionCall = prepareCall(MyContract()) { result ->
Log.i("MyActivity", "Obtained result: $result")
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
...
button.setOnClickListener {
myActionCall(500)
}
}
}

Выглядит немного сложно. Но такой подход не разрушает связность кода. Кроме того, уже сейчас библиотека содержит в себе несколько предопределенных контрактов, в том числе для получения снимка (TakePicture), выполнения звонка (Dial) и, конечно же, запроса полномочий (RequestPermission).
Читать полностью
An adventure 13 years in the making - история проекта Sundcastle, в рамках которого разработчики из компании Corellium сумели портировать Android на iPhone.

Сама возможность порта появилась благодаря эксплойту checkm8, который использует уязвимость в загрузчике iPhone и благодаря этому позволяет не просто выполнить Jailbreak, а получить полный контроль над устройством, включая возможность установки и загрузки альтернативных операционных систем.

Но интересно даже не это, а то, с какими сложностями столкнулись разработчики. В первую очередь это кастомный процессор Apple, который вроде бы совместим со стандартным ARM, но имеет массу мелких отличий: "почти" совместимый с Samsung UART-контроллер, "почти" совместимый с Samsung SPI-контроллер, нестандартный контроллер прерываний, собственный способ включения дополнительных ядер процессора и так далее.

Еще более интересная история произошла с портированием платформы Android поверх уже портированного ядра Linux. Оказалось, что Android в принципе не поддерживает работу со страницами памяти с отличным от 4 Кб размером (Apple использует 16 Кб). Также Android оказался не совсем 64-разрядной системой, во многих местах которой до сих пор можно найти 32-битный код, который просто не будет работать на полностью 64-битном процессоре Apple.

Парадоксально, но порт Android для iPhone стал первой полностью 64-битной сборкой Android в истории.
Читать полностью
How to hook Android Native methods with Frida (Noob Friendly) - статья перехвате функций нативных библиотек с помощью Frida.

Для начала файл APK следует развернуть. Это обычный архив ZIP, поэтому можно использовать любой архиватор. Каталог lib содержит набор нативных библиотек для различных архитектур. Находим библиотеку для архитектуры своего смартфона (обычно это arm64-v8a или armeabi-v7a) и анализируем ее содержимое с помощью утилиты nm (она доступна в Linux и macOS):

$ nm --demangle --dynamic libnative-lib.so
00002000 A __bss_start
U __cxa_atexit
U __cxa_finalize
00002000 A _edata
00002000 A _end
00000630 T Java_com_erev0s_jniapp_MainActivity_Jniint
000005d0 T Jniint
U rand
U srand
U __stack_chk_fail
U time

Как видно библиотека содержит в том числе функцию Java_com_erev0s_jniapp_MainActivity_Jniint. Судя поимени она должна быть доступна для вызова из Java (на стороне Java она будет иметь имя com.erev0s.jniapp.MainActivty.Jniint).

Допустим наша задача - перехватить вызов функции и вернуть удобное нам значение. Например, если бы это была функция, проверяющая наличие root, мы бы могли перехватить ее и вернуть false невзирая на реальный результат проверки.

Есть два способа перехватить эту функцию:

1. На стороне Java, когда приложение только попытается вызывать нативную функцию;
2. На стороне нативного кода, когда управление уже будет передано библиотеке.

Допустим мы уже установили Frida на ПК и установили frida-server на смартфон. Теперь напишем такой скрипт:

Java.perform(function () {
var Activity = Java.use('com.erev0s.jniapp.MainActivity')
Activity.Jniint.implementation = function () {
return 80085
}
})

Этот скрипт переписывает функцию Jniint класса com.erev0s.jniapp.MainActivity так, чтобы она всегда возвращала значение 80085.

Сохраняем скрипт в файл myhook.js и запускаем приложение под управлением Frida:

$ frida -U -l myhook.js com.erev0s.jniapp

Перехватить нативную функцию еще проще:

Interceptor.attach(Module.getExportByName('libnative-lib.so', 'Java_com_erev0s_jniapp_MainActivity_Jniint'), {
onEnter: function(args) {},
onLeave: function(retval) {
retval.replace(0)
}
})
Читать полностью