В спорах в интернете и в реальной жизни наибольший вес имеют аргументы, которые подкрепляются доказательствами — скриншоты, фотографии, записи сообщений/звонков, видеозаписи экрана и т.д.
Статичные картинки себя уже дискредитировали — их слишком просто отредактировать. К аудиозаписям доверие тоже невысокое. Как же дело обстоит с видео или живой демонстрацией на экране устройства?
Введение
В телешоу, видеоблогах и на стримах часто можно увидеть, как кто-то обвиняет кого-то или, наоборот, оправдывается, демонстрируя свои переписки.
В киноиндустрии благодаря усилиям Тимура Бекмамбетова появился новый формат сторителлинга — screenlife, где сюжет развивается на экране устройства. Это и неудивительно: наши мобильные устройства могут рассказать о нас больше, чем мы сами.
Скриншотам уже никто не верит (правда, не всегда). Наблюдается другая тенденнция: показывать / снимать видео либо сразу с экрана устройства, либо второй камерой. Все это чтобы доказать, что это не фотошоп, а реальные данные в реальном приложении. Аргументы понятны: видео подделать куда сложнее и дороже, а лого приложения / имя разработчика внушают доверие.
Но спешим вас огорчить: чтобы подделать screenlife, не нужно ни больших бюджетов, ни спецэффектов, ни навыков видеомонтажа.
Все, что нужно, — это фреймворк инструментации Frida, небольшой скрипт на JavaScript и один реверсер. Да, не нужно машинного обучения, нейронных сетей и тяжелой артиллерии вроде DeepFace.
Техническая сторона вопроса, или пара слов о Frida
К сожалению, на Хабре Frida упоминается всего лишь 18 раз.
Frida — это фреймворк динамической бинарной инструментации (Dynamic Binary Instrumentation, DBI), позволяющий в runtime добавлять пользовательский JavaScript код внутрь исполняемого blackbox процесса / приложения с целью его дальнейшего изучения — hooking и tracing кода приложения, подмена аргументов и возвращаемых значений функций, изменение их поведения и многое другое. Подробнее с возможностями Frida можно ознакомиться на официальном сайте. Frida доступна для многих платформ, в том числе и для iOS.
Пример/proof
Устройство: iPhone 6s с версией iOS 13.2.3
Целевое приложение: Instagram (v126.0.0.13.120)
Цель: Продемонстрировать переписку с очень известной личностью, в нашем случае с Мадонной.
Сразу оговоримся, что можно взять абсолютно любое устройство под любой ОС. Целевое приложение тоже может быть любым — от новостных до мобильного банкинга.
TL;DR
- Скачиваем приложение
- Распаковываем
- Реверсим и находим нужные участки кода
- Пишем и тестируем скрипт для Frida
- Запаковываем приложение и демонстрируем
Более подробно об этом можно почитать в нашей статье “Динамический анализ iOS-приложений без Jailbreak”.
Что мы хотим получить
Ниже продемонстрирован конечный результат описанной задачи:
Если вам интересно, как это реализовано технически, то читайте дальше. В противном случае можете сразу перейти к выводам.
Для написания и отладки скрипта мы используем устройство с jailbreak, а финальный результат можно будет запускать и на устройстве без него.
Прежде всего, нужно понять, как должно выглядеть приложение после запуска скрипта. Какие UI элементы должны быть изменены, чтобы создать видимость правдивой переписки? Для этого взглянем на стандартный UI интерфейс Instagram:
Так выглядит профиль пользователя Madonna (на момент написания статьи):
Если открыть личные сообщения с Мадонной, то увидим пустой диалог (что ожидаемо):
Также пример непустого диалога с пользователем (состоящий просто из рандомных предложений):
Таким образом, открывая сообщения с Мадонной (назовём её origUser
— original User), мы должны попасть в диалог с другим пользователем (назовём его replUser
— replicated User), где все UI элементы будут заменены на те, что находятся в диалоге с origUser
(верхний бар, аватар у сообщений). При этом сам диалог должен вестись с replUser
, который контролируется нами, например, через второй аккаунт. Кроме того, хочется сделать скрипт универсальным, чтобы на месте Мадонны мог быть любой пользователь. Как мы увидим далее, для этого будет достаточно ID origUser
и replUser
, которые публично известны, и могут быть легко получены.
Получаем дешифрованный IPA файл
Итак, чтобы начать разработку Frida-скрипта, нам нужно дешифрованное целевое приложение (в данном случае IPA файл приложения Instagram). Получить его можно несколькими способами:
- Вариант для Mac: установить приложение
Apple configurator 2
из AppStore, подключить целевое устройство и дождаться обновления приложения. После этого нужно скопировать дешифрованные файлы приложения из определённой директории. Подробнее про этот способ можно почитать тут - Поставить на iPhone jailbreak для нужной версии iOS, затем с помощью скрипта frida-ios-dump дешифровать и выкачать нужное приложение. Как видите, мы уже используем Frida. Пока можете просто следовать инструкциям в репозитории, чтобы дешифровать и скачать IPA файл Instagram. Работа с Frida будет описана далее.
Получив файлы приложения, можно приступать к ревёрсу. Про иерархию директорий и файлов iOS bundle можно почитать на официальном сайте разработчиков. В нашем случае можно найти три интересных исполняемых Mach-O 64-bit arm64 файла:
Instagram.App/Instagram
— само запускаемое приложениеInstagram.app/Frameworks/InstagramAppCoreFramework.framework/InstagramAppCoreFramework
— фреймворк InstagramInstagram.app/Frameworks/FBSharedFramework.framework/FBSharedFramework
— shared фреймворк Facebook, который используется внутри InstagramAppCoreFramework и Instagram.
Ревёрсим исполняемые файлы (Instagram)
Сперва посмотрим на код самого приложения — Insagram.App/Instagram
. Использовать для этого можно различные инструменты для дизассемблирования, в нашем случае использовалась IDA Pro.
Просмотрев функции бинарного файла Instagram, можно заметить, что их сравнительно мало (всего 364), хотя Instagram — довольно обширное приложение:
Большая часть этих функций — импорты из фреймворков. Поэтому далее имеет смысл смотреть функционал фреймворков, упомянутых выше.
Ревёрсим исполняемые файлы (InstagramAppCoreFramework и FBSharedFramework)
Далее мы будем работать с InstagramAppCoreFramework
, содержащим основную логику приложения Instagram.
Поверхностный анализ фреймворков показал, чтоFBSharedFramework
является перемычкой междуInstagramAppCoreFramework
, содержащей классы (User, Direct Messages, Message итд), controller и view к которым реализуются вInstagramAppCoreFramework
.
Попробуем для начала найти код, ответственный за direct сообщения. Нам это понадобится, чтобы по нажатию на кнопку “Message” в профиле origUser
, попасть в direct с replUser
.
Doxygen документация по хедерам
Открыв InstagramAppCoreFramework
в IDA Pro, мы видим, что в нём по меньшей мере 250000 функций, а значит, искать нужную логику вслепую будет довольно долго. Чтобы ускорить процесс ревёрс-инжиринга, можно сгенерировать doxygen-документацию на основе header-файлов Objective-C классов этого фреймворка. Для генерации header-файлов можно использовать утилиту classdumpios, поддерживающую архитектуру ARM64. Имея iPhone с jailbreak, можем зайти на него через SSH по USB и в директории с уже распакованным IPA файлом Instagram сделать следующее:
iPhone:~/insta $ mkdir headers
iPhone:~/insta $ cp Payload/Instagram.app/Frameworks/InstagramAppCoreFramework.framework/InstagramAppCoreFramework ./headers
iPhone:~/insta $ cp Payload/Instagram.app/Frameworks/FBSharedFramework.framework/FBSharedFramework ./headers
iPhone:~/insta $ cd headers
iPhone:~/insta/headers $ classdumpios -H InstagramAppCoreFramework -o IG_headers
iPhone:~/insta/headers $ classdumpios -H FBSharedFramework -o FB_headers
iPhone:~/insta/headers $ tar -czvf IG_headers.tar.gz IG_headers
iPhone:~/insta/headers $ tar -czvf FB_headers.tar.gz FB_headers
Теперь осталось скачать архивы с header-файлами из iPhone на хостовую машину, разархивировать их в одну директорию, создать соответствующий конфигурационный файл для doxygen и запустить генерацию документации:
Host@Host:~/insta$ mkdir docs && cd docs
Host@Host:~/insta/docs $ scp -P <PORT> root@<IP>:</path/to/instagram>/insta/headers/IG_headers.tar.gz .
Host@Host:~/insta/docs $ scp -P <PORT> root@<IP>:</path/to/instagram>/insta/headers/FB_headers.tar.gz .
Host@Host:~/insta/docs $ tar xf IG_headers.tar.gz
Host@Host:~/insta/docs $ tar xf FB_headers.tar.gz
Host@Host:~/insta/docs $ cp </path/to/dox.template> .
Host@Host:~/insta/docs $ doxygen dox.template
Когда doxygen-документация сгенерирована, можно смотреть на отношения между классами и их иерархию. При анализе больших файлов это упрощает жизнь.
Вот небольшой кусочек списка классов, участвующих в InstagramAppCoreFramework
и FBSharedFramework
:
И пример того, как выглядит описание рандомно взятого класса IGAuthUserParser
:
С помощью такой документации перемещаться по классам, их методам и полям становится в разы проще.
Первый Frida-скрипт
В процессе анализа исполняемого файла вы наверняка найдёте интересную функцию, которая может участвовать в необходимой логике. Чтобы проверить это предположение, но при этом не ревёрсить статически все внутренние и внешние вызовы функции, можно с помощью простого Frida-скрипта проверить, действительно ли функция срабатывает, с какими аргументами была вызвана, какое возвращаемое значение, какой backtrace, состояние стека на момент вызова и так далее. При желании вы даже можете подменять возвращаемое значение или полностью заменить логику функции. Всё зависит от ваших нужд. Далее мы опишем только trace-скрипт, который следит за вызовами функции и показывает интересующую нас отладочную информацию.
Прежде всего необходимо установить Python-биндинг для Frida, а также вспомогательные утилиты:
~ $ pip install --upgrade frida frida-tools
Подразумевается, что к этому моменту на тестируемом устройстве уже поднят frida-server. О том, как это сделать, можно прочитать тут (для устройств с JB) или тут (для устройств без JB).
В качестве примера мы будем использовать метод - inputView:didTapStickerButton:
класса IGDirectThreadViewController
. Почему именно он? Забежим немного вперёд и представим, что уже провели некоторый анализ исполняемого файла, и нас заинтересовала эта функция. Хочется понять, действительно ли она вызывается при нажатии на кнопку стикеров внутри direct сообщений (на самом деле, этот класс взят не случайно, он используется далее в работе). Сейчас можно считать, что это просто рандомная функция, которая для нас ничего не значит, ведь мы просто хотим показать, как работать с функционалом Frida. Для начала получим NativeFunction
этого метода:
var func = ObjC.classes.IGDirectThreadViewController["inputView:didTapStickerButton:"].implementation;
Затем поставим hook, срабатывающий на вход и выход из функции:
Interceptor.attach(func, {
onEnter: function(args) {
console.log("-[IGDirectThreadViewController inputView:didTapStickerButton:] => onEnter");
},
onLeave: function(retval) {
console.log("-[IGDirectThreadViewController inputView:didTapStickerButton:] <= onLeave");
}
}
В итоге получился максимально простой скрипт, который выводит сообщения на вход и выход из функции.
Уже этого скрипта будет достаточно, чтобы понять, выполняется ли та или иная функция. Но, разумеется, нам нужен более детальный анализ, поэтому внесём некоторые улучшения.
В hook, срабатывающий на вход, добавим вывод названий аргументов, их значений и бектрейс. А в hook на выход — возвращаемое значение. Большая часть кода позаимствована отсюда. В итоге получим такую функцию:
function addTracer(className, methodName) {
// Получаем NativeFunction по методу класса
var impl = ObjC.classes[className][methodName].implementation;
Interceptor.attach(impl, {
onEnter: function(a) {
this.log = []; // Логи, которые будут выводиться на выходе из функции
this.log.push('(' + a[0] + ',' + Memory.readUtf8String(a[1]) + ') ' + className + ' ' + methodName); // Логируем адрес функции, её название и класс, к которому она относится
if (methodName.indexOf(':') !== -1) { // Далее парсим аргументы, разделённые ":"
var params = methodName.split(':');
params[0] = params[0].split(' ')[1];
// Складываем названия аргуметов с соответствующими значениями
for (var i = 0; i < params.length - 1; i++) {
try {
this.log.push(params[i] + ': ' + new ObjC.Object(a[2 + i]).toString());
} catch (e) {
this.log.push(params[i] + ': ' + a[2 + i].toString());
}
}
}
// Добавляем бектрейс до текущей функции
this.log.push(
Thread.backtrace(this.context, Backtracer.ACCURATE)
.map(DebugSymbol.fromAddress)
.join('\n')
);
},
onLeave: function(r) {
// И, наконец, возвращаемое значение
try {
this.log.push('RET: ' + new ObjC.Object(r).toString());
} catch (e) {
this.log.push('RET: ' + r.toString());
}
console.log(this.log.join('\n') + '\n');
}
});
}
Использовать полученную функцию можно таким образом:
addTracer("IGDirectThreadViewController", "- inputView:didTapStickerButton:");
Чтобы запустить полученный скрипт, нужно выполнить следующую команду (с запущенным Frida-сервером на тестируемом устройстве, подключенном по USB):
$ frida -U -n Instagram -l </path/to/script> --no-pause
После запуска скрипта заходим в direct сообщения с пользователем и нажимаем на кнопку со стикерами (расположены справа от поля для ввода сообщения). Мы получем исчерпывающий вывод:
(0x1269aea00,inputView:didTapStickerButton:) IGDirectThreadViewController - inputView:didTapStickerButton:
inputView: <IGDirectComposer: 0x125d241b0; frame = (0 607; 375 60); text = ''; autoresize = H; layer = <CALayer: 0x281ef5fc0>>
didTapStickerButton: <IGDirectComposerButton: 0x127a5e210; baseClass = UIButton; frame = (319 10; 24 24); opaque = NO; tintColor = <UIDynamicProviderColor: 0x281e29dc0; provider = <__NSMallocBlock__: 0x2810cfdb0>>; layer = <CALayer: 0x281e37900>>
0x1072df1dc InstagramAppCoreFramework!IGHandleURLForIGInstagramToast
0x18743bab0 UIKitCore!-[UIApplication sendAction:to:from:forEvent:]
0x186e738ac UIKitCore!-[UIControl sendAction:to:forEvent:]
0x186e73c10 UIKitCore!-[UIControl _sendActionsForEvents:withEvent:]
0x186e72c2c UIKitCore!-[UIControl touchesEnded:withEvent:]
0x1019378ac FBSharedFramework!IGCrashReportProcessorLatestUploadError
0x187475288 UIKitCore!-[UIWindow _sendTouchesForEvent:]
0x1874765c8 UIKitCore!-[UIWindow sendEvent:]
0x187452b78 UIKitCore!-[UIApplication sendEvent:]
0x1012e0784 FBSharedFramework!IGALLoggerSetTrackingNodeForView
0x1874caef8 UIKitCore!__dispatchPreprocessedEventFromEventQueue
0x1874cd454 UIKitCore!__handleEventQueueInternal
0x1874c62c8 UIKitCore!__handleHIDEventFetcherDrain
0x18334f7c4 CoreFoundation!__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
0x18334f71c CoreFoundation!__CFRunLoopDoSource0
0x18334eeb4 CoreFoundation!__CFRunLoopDoSources0
RET: nil
Поиск и анализ direct чата
Сразу оговоримся, что дальнейшее описание относится к указанной версии Instagram. Мы не гарантируем работу скрипта для других версий.
В Instagram
за direct сообщения (как личные сообщения, так и чаты) отвечает класс IGDirectThread
(далее "direct сообщения", "direct thread", "диалог" можно считать одной и той же сущностью).
Если посмотреть на поля этого класса, то можно найти строку, идентифицирующую конкретный direct thread — _threadId
. У каждого диалога есть свой уникальный ID. В дальнейшем мы воспользуемся этими данными. Если вы всё же сгенерировали документацию, то можете самостоятельно поизучать методы и поля, относящиеся к direct сообщениям, это затягивает.
Также стоит отметить поле _metadata
. Внутри объекта этого класса хранится список пользователей (поле _users
), относящихся к этому direct thread.
За объекты пользователей отвечает класс IGUser
. Он довольно большой, поэтому освещать его целиком мы не будем. Отметим лишь поле _pk
, которое является уникальным идентификатором пользователя. Найти _pk
можно на публичных ресурсах, например, тут
Класс, соединяющий UI и IGDirectThread
, — IGDirectThreadViewController
будет опорной точкой дальнейшего анализа, так как в нём содержатся все вспомогательные элементы, необходимые для достижения результата. Это большой класс со множеством методов и полей — как своих, так и наследованных.
По логике вещей, при открытии direct сообщений создается и инициализируется объект класса IGDirectThreadViewController
. Попробуем сперва посмотреть на функции, отвечающие за инициализацию объекта. Наше внимание привлёк метод -[IGDirectThreadViewController initWithUserSession:thread:entryPoint:perfComponents:]
. Если поставить указанный выше вспомогательный Frida-hook на эту функцию, то можно увидеть, как она вызывается при открытии нового диалога. Проанализировав логику функции, можно заметить создание вспомогательных объектов для контроллера и дальнейшую инициализацию созданного объекта IGDirectThreadViewController
:
Одним из ключевых объектов для инициализации является передаваемый аргумент функции IGDirectThread
. Как мы уже знаем, IGDirectThread
относится, в частности, к direct сообщениям с пользователем, поэтому контролирование этого аргумента может сыграть нам на руку.
Подмена direct чата
Таким образом, перенося вышесказанное на поставленную задачу, получаем следующий алгоритм: если открывается диалог с origUser
, то заменяем объект IGDirectThread
на тот, что относится к replUser
. Но перед этим необходимо найти в памяти процесса Instagram
диалоги, относящиеся к origUser
и replUser
соответственно. Способов сделать это несколько. Один из них — зная _pk
пользователя, найти IGDirectThread
, содержащий в поле _users
(которое находится в _metadata
) пользователя с указанным _pk
. Другой способ — в ещё одном hook находим _threadId
для определённого пользователя (например, при открытии нового диалога), а затем находим direct thread по этому ID.
В коде ниже реализован первый вариант, так как он требует меньше времени на поиск direct thread — не нужно проходить все объекты IGDirectThread
в памяти, достаточно лишь сравнить открываемые direct сообщения с теми, что нам нужны (наличие пользователя в поле _users
):
var IGDirectThreadViewControllerInitWithUserSession = ObjC.classes.IGDirectThreadViewController["- initWithUserSession:thread:entryPoint:perfComponents:"].implementation;
Interceptor.attach(IGDirectThreadViewControllerInitWithUserSession, {
onEnter: function(args) {
this.thread = new ObjC.Object(args[3]);
/**
* origUser и replUser - глобальные переменные, в которых хранится
* необходимая информация о пользователе
*
* Если текущий открываемый direct thread предназначается для одного из
* наших пользователей (origUser или replUser), то
* копируем его и сохраняем
*/
[origUser, replUser].forEach(function (user) {
var users = this.thread.$ivars["_metadata"].$ivars["_users"];
if (users.count() == 1 && users.objectAtIndex_(0).$ivars["_pk"] == user.pk && user.thread == undefined) {
user.thread = this.thread.retain();
}
}, this);
/**
* По крайней мере мы должны знать threadId обоих пользователей, чтобы
* производить замену диалогов
*/
if (origUser.threadId == undefined || replUser.threadId == undefined) {
return;
}
/* Проверяем, открывается ли thread с `origUser` */
if (this.thread.$ivars["_threadId"].equals(origUser.threadId)) {
/**
* Если да, то подменяем передаваемый IGDirectThread на тот,
* что соответствует `replUser`
*/
args[3] = replUser.thread.retain();
/**
* Эта переменная нужна для дополнительной логики, про которую будет
* сказано далее
*/
replaceState = true;
console.log(
FgGreen + "[+]" + Reset +
" Thread " +
origUser.threadId +
" spoofed with " +
replUser.threadId +
"!"
);
}
}
});
Загрузив полученный Frida-скрипт (необходимо лишь собрать недостающую информацию, упомянутую выше) и открыв direct сообщения с origUser
, мы откроем диалог с replUser
! А это именно то, чего мы хотели. Теперь нужно разобраться с верхним баром и аватаркой пользователя.
Изменяем верхний бар
Посмотрев на объект IGDirectThreadViewController
в runtime, можно найти интересное поле — _viewModel
. Чем же оно примечательно? Посмотрев на структуру класса IGDirectThreadViewControllerViewModel
, можно найти внутреннее поле _leftAlignedTitleViewModel
и, пройдя вглубь _leftAlignedTitleViewModel->_titleButton->_titleView->text
, увидеть название текущего диалога, расположенного в баре (т.е. имя пользователя, с которым открывались direct сообщения). Что произойдёт, если взять _viewModel
, создаваемую для origUser
(без подмены диалогов), создать копию этого объекта и заменить после инициализации direct сообщений с origUser
(учитывая, что подмена диалогов уже работает)?
Такой подход сработает, но только частично. При нажатии на бар будет открываться профиль origUser
— то, чего мы и добивались. Но при новом сообщении от replUser
верхний бар обновится и вернётся в состояние для replUser
. Таким образом, происходит обновление верхнего бара — функция -[IGDirectViewController _updateThreadNavigationBar]
. Можем производить замену бара в вызове этой функции. Теперь при открытии direct сообщений с origUser
открывается диалог с replUser
, верхний бар которого принадлежит origUser
, и новые сообщения не влияют на его обновление.
var IGDirectThreadViewControllerUpdateNavigationBar = ObjC.classes.IGDirectThreadViewController["- _updateThreadNavigationBar"].implementation;
Interceptor.attach(IGDirectThreadViewControllerUpdateNavigationBar, {
onLeave: function(retval) {
/**
* Если мы сейчас находимся в состоянии подмены пользователя, и при этом
* текущий контроллер от replUser, то заменяем viewModel. Тут нужно
* понимать, что подмена диалогов уже работает, и после открытия direct сообщений
* с origUser мы работаем с direct сообщениями replUser.
*/
if (replaceState && this.controller.$ivars["_threadId"].equals(replUser.threadId)) {
this.controller.$ivars["_viewModel"] = origUser.viewModel.retain();
}
if (DEBUG) console.log("IGDirectThreadViewController[onLeave]: -_updateNavigationBar");
}
});
Теперь осталась только аватарка.
Замена аватарки
Это наиболее простая операция — в объекте класса IGUser
есть поле под названием _profilePicURL
. При входе в direct сообщения с origUser
будем заменять аватарку replUser
на origUser
, а при выходе из диалога производить обратную замену. Обратная замена нужна только для того, чтобы не испортить аватарку подменяемому пользователю.
Код для этого функционала оставляем читателю в качестве домашнего задания.
Небольшие проблемы
Эта реализация имеет ряд проблем, которые могут привести к падению приложения и некоторым другим глюкам. Мы не ставили задачи написать production-ready скрипт, а этой реализации достаточно, чтобы записать небольшую демку, показывающую изначальную задумку.
Весь скрипт занимает около 350 строк на JavaScript с учётом всех возникших проблем. Для более сложной логики он, конечно, будет больше — тут уже дело в вашей фантазии ;)
Взгляд в будущее
Можно с ужасом представить, что будет, когда до широких масс дойдет дополненная реальность (AR), которая для телефонов сейчас в основном представлена в виде развлекательных масок и небольших игрушек.
Заключение
Когда вас в следующий раз будут в чем-то убеждать и показывать переписку со звездой в популярном мессенджере или круглые цифры в мобильном банкинге — не спешите этому верить ;)
Спасибо @d1g1 за идею статьи и помощь в подготовке! :)
P.S. Предупрежден значит вооружен