Поиск callback-ов кнопок в рантайме iOS

от автора

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

В данной статье будет рассказано как узнать какой callback будет вызван при нажатие кнопки в интерфейсе iOS приложения с использованием фреймворка frida.

Также я думаю эта статья будет полезна тем разработчикам на iOS кто хочет знать как работает внурянка cllaback-ов графических элементов.

Для нетерпеливых конечный скрипт тут.

В чем собственно проблема

Если мы имеем на входе простое одноэкранное приложение то найти нужную кнопку не составит особого труда — достаточно выполнить команду:

ObjC.classes.UIWindow.keyWindow().recursiveDescription().toString()

И получить список всех имеющихся графических элементов:

"<UIWindow: 0x10110b250; frame = (0 0; 667 375); gestureRecognizers = <NSArray: 0x2832b58c0>; layer = <UIWindowLayer: 0x283c83dc0>>   | <UITransitionView: 0x10110d150; frame = (0 0; 667 375); autoresize = W+H; layer = <CALayer: 0x283c83220>>   |    | <UIDropShadowView: 0x10110e780; frame = (0 0; 667 375); autoresize = W+H; layer = <CALayer: 0x283c812a0>>   |    |    | <UIView: 0x10110b050; frame = (0 0; 667 375); autoresize = W+H; layer = <CALayer: 0x283c816e0>>   |    |    |    | <UIButton: 0x10110b540; frame = (250 171; 62 33); opaque = NO; autoresize = RM+BM; layer = <CALayer: 0x283c83fa0>>   |    |    |    |    | <UIButtonLabel: 0x101117e00; frame = (0 6; 62 21); text = 'Test'; opaque = NO; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x281f8c0a0>>"

Откуда посредствам ручного анализа достать адрес нужной кнопки (0x10110b540) и посмотреть зарегистрированные обработчики:

var button = new ObjC.Object(new NativePointer("0x10110b540")) button._allTargetActions().toString()

В результате получим что-то вроде:

"(    \"<UIControlTargetAction: 0x2832b6fd0> actionHandler=<_UIImmutableAction: 0x281891860; title = > events=0x40\",    \"<UIControlTargetAction: 0x2832b6f70> target=0x10110a9d0 action=click1 events=0x40\",    \"<UIControlTargetAction: 0x2832b6f40> target=0x0 action=click2 events=0x40\" )"

Как минимум у нас есть имена двух селекторов — click1 и click2, а для одного из них есть даже объект который его реализует — поле target — из которого можно достать имя класса, но об этом позже. В любом случае есть что поискать в IDA для понимания дальнейшей логики работы.

Однако ситуация меняется кардинальным образом когда мы переходим к реальным приложениям где может быть больше одной перекрывающейся сцены и в выводе keyWindow().recursiveDescription() появляется больше 30 кнопок. Разбираться со всем этим богатством руками нет никакого желания, а для автоматизации придется немного разобраться в том как происходит обработка нажатий в iOS.

UIEvent

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

Обработка событий генерируемых пользователем происходит следующим образом:

  1. При нажатии на экран или другой активности пользователя UIKit, на основании данных от ОС генерирует UIEvent и кладет его в очередь событий UIApplication. События для нашего случая можно разделить на касания экрана и все остальные (пользователь трясет устройство/пользователь нажал аппаратную кнопку/пользователь сгенерил команду от наушников). Далее будем разбирать алгоритм для касания экрана.
  2. UIApplication достает событие из очереди и если это касание экрана — отправляет его в текущее окно (UIWindow)
  3. UIWindow с помощью метода hitTest(_:with:) определяет самое верхнее UIView в пределах которого находиться касание
  4. После чего данная вьюшка выбирается первым обработчиком события в цепочке и у нее вызывается один из четырех методов в зависимости от жизненного цикла касания.

Тут надо сделать небольшое лирическое отступление и сказать что все UIView равно как и UIWindow и UIApplication реализуют интерфейс UIResponder что позволяет им обрабатывать различные UIEvent — ты.

Так вот в случай касания экрана будет вызван один из четырех методов UIResponder-а:

func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?)

Далее у первого UIResponder есть три варианта действия:

1) Вызвать реализацию по-умолчанию и передать событие следующему обработчику в цепочке
2) Провести обработку события и передать его следующему обработчику в цепочке
3) Провести обработку события и не передавать его дальше.

Для полноты картины осталось разобраться с цепочкой обработчиков:

1) Как было сказано ранее первый обработчик определяется с помощью метода hitTest(_:with:) и является самой верхней вьюшкой в пределах которой произошло касание
2) От первого обработчика событие будет отправлено его родительской UIViewsuperview
3) Шаг 2 повторяется пока очередь не дойдет до ViewController
4) Дальше перебираются ViewController-ы пока очередь не дойдет до root-ового контроллера
5) После него следующим обработчиком становиться UIWindow
6) И последним в данной цепочке является AppDelegate
7) После этого событие просто удаляется

UIControl

И так UIEvent это конечно хорошо и очень интересно однако callback на кнопки обычно вешаются либо через метод addTarget(_:action:for:) либо addAction(_:for:) оба из которых относяться к класу UIControl и на вход помимо функции принимают событие типа UIControl.Event а не UIEvent.

addTarget кроме UIControl.Event принимает еще собственно сам target — экземпляр класса реализующего callback который может быть еще и null и селектор.

На основании полученных данныъ addTarget создает объект типа UIControlTargetAction заполняет его поля следующим образом

  • UIControlTargetAction._target = target
  • UIControlTargetAction._action = action
  • UIControlTargetAction._eventMask = controlEvents

Затем addTarget вызывает метод UIControl._addControlTargetAction который проходит по массиву UIControl._targetActions и если находит UIControlTargetAction с такими же полями target и _action то добавляет в его поле _eventMask значения из маски нового UIControlTargetAction (чтобы один callback мог запускаться разными событиями), а если не находит то добавляет новый UIControlTargetAction в массив.

addAction(_:for:) является нововведением начиная с 14 iOS и имеет схожу логику работу с addTarget за исключением того что вместо полей _target и _action у обьекта UIControlTargetAction заполяет поле _actionHandler значением типа UIAction.

UIControl_add

Так вот от UIKit у нас приходят UIEvent-ы, а обработчики мы вешаем уже на UIControl.Event. Нестыковочка какая-то. Логика конечно подсказывает, что UIControl скорее всего тоже является UIResponder-ом и таки предпринимает некоторую обработку UIEvent-ов. Осталось выяснить какую.

Чтож открываем дизасм UIControl.touchesEnded. Почему именно ее? Да потому что обычно обработчик кнопки вешается обычно на событие UIControl.Event.touchUpInside — то есть пользователь поднял палец после нажатия, что хорошо вяжется с обработчиком окончания нажатия.

touchesEnded

Как видно из дизасма, функция, после обработки поступивших данных о нажатии вызывает UIControl._sendActionsForEvents(_:UIEvent, withEvent: UIControl.Event) со следующими возможными UIControl.Event-ми:

touchDragEnter =    1 <<  4 = 16 touchDragExit =     1 <<  5 = 32 touchUpInside =     1 <<  6 = 64 touchUpOutside =    1 <<  7 = 128

_sendActionsForEvents в свою очередь проходит по массиву сохраненных UIControlTargetAction и выбирает тот у которого маска совпадает с переданным UIControl.Event -ом, а дальше логика зависит от того установлен ли _actionHandler или поле _action.

_action

В случай если у нас установлен _action вызывается UIControl.sendAction(_:to:for:) который достает синглтон UIApplication и вызывает документированный UIApplication.sendAction(_:to:from:for:), куда в качестве sender передает себя.

UIApplication.sendAction первым делом проверяет не null ли таргет и если null запускает UIApplication._targetInChainForAction(_:Selector, sender: Any) -> Any? которая находит первого (сюрприз) UIResponder, только алгоритм обнаружения первого — обратный — первым выбирается самый нижний активный слой.

После этого UIApplication.sendAction вызывает уже задокументированный UIResponder.target(forAction:withSender:) который проходится вверх по цепочке UIResponder — ов
и с помощью метода UIResponder.canPerformAction(_:withSender:) проверяет может ли данный UIResponder выполнить переданый селектор и если может — возвращает обьект кторый отозвался. Если ни один объект в цепочке обработчиков не может выполнить данный селектор то возвращается null.

После поиска target UIApplication.sendAction вне зависимости от результата вызовет либо класический objc_msgSend или perform(_:with:with:), благо objc_msgSend успешно игнорирует null в качестве id.

И на этом цепочка поиска и вызова callback-а завершается.

UIAction aka _actionHandler

В случай с UIAction, установленным в поле UIControlTargetAction._actionHandler, UIControl._sendActionsForEvents вызывает UIControl.sendAction(_:UIAction) который в свою очередь вызывает UIAction._performActionWithSender.

UIAction._performActionWithSender достает значение лежащее в поле UIAction.handler которое является класическим блоком. Однако в поле invoke у нас лежит не указатель на переданное при создании UIAction замыкание, а указатель на врапер который потом достает указатель на замыкани по оффсету 0x20 и передает управление ему.

UIControl._sendActionsForEvents

Реализация

После такой достаточно большой теоретической части можно перейти к реализации.
На входе имеем случайное приложение и frida подключенную к нему.

Как мы знаем из теоретической части все callback-и хранятся в массиве UIControl._targetActions. Собственно для начала получим все UIControl благо для этого есть специальная команда choose:

uiControls = ObjC.chooseSync(ObjC.classes.UIControl)

Теперь для каждого UIControl получим массив _targetActions и выделим основные кейсы которые следует обработать:

var targetActions = uiControl.$ivars._targetActions // да массив UIControlTargetAction имеет ленивую инициализацию if (targetActions == null) {    console.log("\tNo callbacks found")    return } var count = targetActions.count().valueOf()  for (let i = 0; i !== count; i++) {    var action = targetActions.objectAtIndex_(i)    // Тут мы разбираем три рассмотренных ранее случая    // 1. Когда в качестве обработчика установлен UIAction    if (action.$ivars._actionHandler != null) {        var uiAction = action.$ivars._actionHandler        interceptUIAction(uiAction)    // 2. Когда в качестве обработчика задан селектор и обьект у которого он реализован    } else if (action.$ivars._action != null &&                action.$ivars._action != "0x0" &&                action.$ivars._target != null) {        var actionSelector = action.$ivars._action        var actionTarget = action.$ivars._target        interceptActionWithTarget(actionSelector, actionTarget)    }    // 3. Когда задан только селектор    else if (action.$ivars._action != null &&        action.$ivars._action != "0x0"){        var actionSelector = action.$ivars._action        interceptActionWithoutTarget(actionSelector, uiControl)    }    else {        console.error("Invalid UIControlTargetAction with actionHandler and action seted to null")        continue    } }

В случай если в качестве callback задан UIAction я смог достать не так много информации — только дебаг символы если они есть:

function interceptUIAction(uiAction) {    var blockAddr = uiAction.$ivars._handler.handle    // достаем адрес замыкания которое будет вызвано ибо invoke() указывает на хелпер    var closurePtr = blockAddr.add(0x20).readPointer()    // и дебаг символы    var closureName = DebugSymbol.fromAddress(closurePtr).name    // по хорошему можно достать еще и sender - UIControl но вопрос на сколько он интересен?    console.log("\tSet hook on: " + closureName)    Interceptor.attach(closurePtr, {        onEnter: function(a) {            this.log = []            this.log.push("Called " + closureName)        },        onLeave: function(r) {            console.log(this.log.join('\n') + '\n')        }}) }

Второй случай довольно простой так как у нас есть вся нужная информация — просто вешаем хук.

function interceptActionWithTarget(actionSelector, target) {    console.log("\tSet hook on: " + target.$className + "." + actionSelector.readUtf8String() + "()")    var impl = target.methodForSelector_(actionSelector)    Interceptor.attach(impl, {        onEnter: function(a) {            this.log = []            this.log.push("(" + a[0] + ") " + target.$className + "." + actionSelector.readUtf8String() + "()")        },        onLeave: function(r) {            console.log(this.log.join('\n') + '\n')        }}) }

А вот в третьем кейсе заболела frida и метод _targetInChainForAction почему то отказался находиться в режиме скрипта, при этом в консольном режиме все работало прекрасно. Но никто не мешает нам вызвать метод через objc_msgSend:

function interceptActionWithoutTarget(actionSelector, uiControl) {    var uiApp = ObjC.classes.UIApplication.sharedApplication()    // создаем функцию из указателя на objc_msgSend    var targetInChainForActionPrototype = new NativeFunction(ObjC.api.objc_msgSend, "pointer", ["pointer","pointer","pointer", "pointer"])    // и вызываем))    var actionTargetPtr = targetInChainForActionPrototype(uiApp, ObjC.selector("_targetInChainForAction:sender:"), actionSelector, uiControl)    var actionTarget = new ObjC.Object(actionTargetPtr)    if (actionTarget != null) {        interceptActionWithTarget(actionSelector, actionTarget)    } else {        console.warn("Can't get target for selector: " + actionSelector.readUtf8String())    } }

Собственно на этом и все! Готовы скрипт лежит тут. Надеюсь эта статья поможет кому-то в осознании того как работают callback-и графических элементов в iOS.


ссылка на оригинал статьи https://habr.com/ru/post/713732/