Как стать автором
Обновить

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

Время на прочтение 9 мин
Количество просмотров 1.4K

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


В данной статье будет рассказано как узнать какой 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.

Теги:
Хабы:
+3
Комментарии 0
Комментарии Комментировать

Публикации

Истории

Работа

iOS разработчик
23 вакансии
Swift разработчик
32 вакансии

Ближайшие события

PG Bootcamp 2024
Дата 16 апреля
Время 09:30 – 21:00
Место
Минск Онлайн
EvaConf 2024
Дата 16 апреля
Время 11:00 – 16:00
Место
Москва Онлайн
Weekend Offer в AliExpress
Дата 20 – 21 апреля
Время 10:00 – 20:00
Место
Онлайн