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

iOS Console App — пишем программу для iOS без GUI

Время на прочтение7 мин
Количество просмотров18K
Доброго дня всем хабравчанам!

Сегодня я расскажу вам о том, как можно легко, быстро и просто написать консольную программку для запуска на iOS-девайсе. Разумеется, нам потребуется для этого jailbreak-нутый девайс, без него, увы, никак: iOS AppStore (он же iTunesStore) не позволяет распространять консольные утилиты.



Писать HelloWorld — дело не особо интересное. Поэтому, мы будем писать полезную утилиту, позволяющую просмотреть некоторую информацию о системе, полученную через приватные API.

К примеру, информацию об установленных программах и их версиях.

В принципе, можно ещё поворовать пароли и прочие персональные данные, но это оставлю как факультативное задание.

Итак, под катом — описание процесса создания консольной программки прямо в Xcode.



Создаём проект в Xcode: выбираем темплейт «Empty Application», придумываем имя программе (в моём случае — hackup). Что ж, у нас получилось пустое GUI-приложение, но ведь мы не этого хотели, не так ли? Смело удаляем из проекта лишние файлы!


Что ещё? Нам не нужен GUI, так что лишние фреймворки тоже убираем.


Теперь идём в свойства проекта, точнее — в свойства таргета. Там убираем подписывание кода.


Далее, нам не нужен Info.plist, смело сносим и его упоминание.


Бандл нам тоже не нужен: поэтому пусть итоговая программа будет лежать в фейковом бандле с расширением ".console"


Что ж, теперь немного подправим код в main.m: уберём импорт UIKit и AppDelegate. В .pch тоже уберём лишнее, оставив лишь Foundation.h. Из функции main() так же убираем UIApplication, поставим просто return 0.

Ну вот, можем теперь попробовать собрать проект для симулятора! Да, всё именно так просто. Но как же запустить теперь нашу замечательную ничегонеделающую программу? Ведь в симуляторе терминала нет? Да всё просто: это же симулятор, а не эмулятор, так что наша программа суть обычная программа для макоси. Но запустить её даже из терминала — штука непростая, ведь она требует фреймворки, собранные для симулятора!

Начинаем вспоминать теорию. Все либы и фреймворки ищутся и загружаются программой dyld, которая опирается на некоторые флаги, полное описание которых содержится в мане. Нас интересует параметр DYLD_ROOT_PATH, то есть, путь, к которому dyld «приделывает» все пути к фреймворкам и либам в файле.

Ну что ж, заходим в терминале в папку с собранной для симулятора программой. Для этого выбираем «Show In Finder» для нашего таргета.


Затем можно просто перетащить hackup.console в терминал с набранным заранее cd.

$ pwd
/Users/silvansky/Library/Developer/Xcode/DerivedData/hackup-cbpgmjwwgsfnqlgjnrlcnbjcxnmu/Build/Products/Debug-iphonesimulator/hackup.console
$ ./hackupdyld: Symbol not found: _OBJC_CLASS_$_NSString
  Referenced from: /Users/silvansky/Library/Developer/Xcode/DerivedData/hackup-cbpgmjwwgsfnqlgjnrlcnbjcxnmu/Build/Products/Debug-iphonesimulator/hackup.console/./hackup
  Expected in: /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation
 in /Users/silvansky/Library/Developer/Xcode/DerivedData/hackup-cbpgmjwwgsfnqlgjnrlcnbjcxnmu/Build/Products/Debug-iphonesimulator/hackup.console/./hackup
[1]    61835 trace trap  ./hackup
$ otool -L hackup
hackup:
     /System/Library/Frameworks/Foundation.framework/Foundation (compatibility version 300.0.0, current version 992.0.0)
     /usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 227.0.0)
     /usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 65.0.0)
     /usr/lib/libSystem.dylib (compatibility version 1.0.0, current version 125.0.0)

Мы видим, что сейчас наша программа ищет фреймворк Foundation в стандартной системной папке. Разумеется, он отличается он нужного нам фреймворка, собранного для симулятора.

Лечим: находим наш iOS SDK в бандле Xcode.app, прописываем:

$ DYLD_ROOT_PATH="/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator6.0.sdk" ./hackup

Ошибок нет! Программа запустилась, но по прежнему ничего не делает. Исправим это!

Для начала напишем простенькую функцию для вывода чего-либо в stdout, замену NSLog. Я назвал свою NSPrintf, вот её код:

void NSPrintf(NSString *format, ...)
{
     va_list args;
     va_start(args, format);
     NSString *message = [[[NSString alloc] initWithFormat:format arguments:args] autorelease];
     va_end(args);
     std::cout << [message cStringUsingEncoding:NSUTF8StringEncoding];
}

Внимательный читатель заметит, что реальный вывод осуществляется через std::cout, собственно, поэтому не забываем переименовать main.m в main.mm и подключить iostream.

Что ж, теперь мы умеем печатать в консоль, но что мы будем печатать?

Здесь начинается самое интересное. Допустим, мы хотим получить список установленных программ. Публичные API не позволяют нам этого сделать. А как насчёт приватных? А где их взять? А как же отсутствие документации?

Ох, сложные это вопросы. Документации к приватным API, разумеется, нет. Хоть какого-то разумного описания — тоже. Но зато в нашем распоряжении есть замечательная штука — реверс-инженеринг! С его помощью были получены недостающие хедеры для публичных и приватных фреймворков. Собственно, есть такой познавательный репозиторий: iOS-Runtime-Headers (и есть инструмент, с помощью которого эти хедеры и были получены: RuntimeBrowser, спасибо доброму человеку Nicolas Seriot). Немного почитаем хедеры и поищем в них что-либо, связанное с программами. Рано или поздно, но наткнёмся мы на метод - (id)applications у класса ISSoftwareMap, включённого в приватный фреймворк iTunesStore. Что ж, зададимся целью вызвать этот метод и распечатать что бы он нам ни вернул!


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

BOOL loadPrivateFramework(NSString *framework)
{
     NSString *path = [NSString stringWithFormat:@"/System/Library/PrivateFrameworks/%@.framework", framework];
     NSBundle *b = [NSBundle bundleWithPath:path];
     BOOL success = [[[b retain] autorelease] load];
     if (!success)
     {
          NSPrintf(@"Failed to load private framework %@!\n", framework);
     }
     return success;
}

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

Class ISSoftwareMap = NSClassFromString(@"ISSoftwareMap");

Как мы уже поняли из хедеров, нам нужно вызвать + (id)currentMap или + (id)loadedMap для получения экземпляра класса.

id isSoftwareMap = [ISSoftwareMap performSelector:@selector(currentMap)];
if (!isSoftwareMap)
{
     isSoftwareMap = [ISSoftwareMap performSelector:@selector(loadedMap)];
}

Ну вот мы и научились работать с приватными фреймворками! Мои поздравления! =)

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

id *applications = [isSoftwareMap performSelector:@selector(applications)];
NSPrintf(@"applications:\n%@\n", applications);

Итак, теперь протестируем нашу программу в симуляторе! Что-то? Не работает? Ну да, не работает, у нас ведь пути для загрузки фреймворков абсолютные прописаны. Так что давайте лучше запустим на устройстве. В моём случае это iPad первой версии.

Что-что? Не собирается? Ругается на неподписанность? Ну да, я ведь забыл сказать: iOS SDK не позволяет собирать бинарники для девайса без подписи. Но ведь мы твёрдо решили добиться своего! Будем править настройки SDK. Находим файлик SDKSettings.plist в /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS6.0.sdk/ и правим его. Что, не вышло? Вы не владелец файла? Ну да, политики безопасности и всё такое… Но у нас есть sudo:

$ sudo plutil -convert xml1 SDKSettings.plist
$ sudo nano SDKSettings.plist
$ sudo plutil -convert binary1 SDKSettings.plist

Вместо nano, разумеется, можно использовать vim/mcedit/emacs и даже Sublime Text 2. В редакторе надо найти в XML-ке тег CODE_SIGNING_REQUIRED и установить его значение в NO.


Теперь перезапускаем Xcode и радуемся — наша проблема решена! Теперь проект собирается. Не терпится уже закинуть его на девайс! Для этого будем пользоваться OpenSSH (ставим через Cydia):

$ pwd
/Users/silvansky/Library/Developer/Xcode/DerivedData/hackup-cbpgmjwwgsfnqlgjnrlcnbjcxnmu/Build/Products/Debug-iphoneos/hackup.console
$ scp hackup root@192.168.2.2:/private/var/mobile/Documents/
root@192.168.2.2's password:
hackup                                                                                                                                                                    100%   26KB  26.3KB/s   00:00
$ ssh mobile@192.168.2.2                                   
mobile@192.168.2.2's password:
iSilvansky:~ mobile$ ~/Documents/hackup
(
    "<ISSoftwareApplication: 0x18b4e0>: (ru.mail.agent, 335315530:11499676)",
    "<ISSoftwareApplication: 0x18d010>: (com.getdropbox.Dropbox, 327630330:11201748)",
    # ... some more ...
    "<ISSoftwareApplication: 0x1936f0>: (8HLDK844H7.net.litchie.idos, 377135644:2716751)"
)

Собственно, мы видим, что метод - (id)applications возвращает нам NSArray, содержащий объекты типа ISSoftwareApplication. Описание этого класса так же находим в приватных хедерах того же фреймворка. Что ж, список программ получили, давайте же посмотрим на них попристальнее:

NSArray *applications = [isSoftwareMap performSelector:@selector(applications)];
if (applications)
{
     for (id app in applications)
     {
          NSPrintf(@" *** Info for application %@\n", app);
          LOG_SELECTOR(app, bundleIdentifier)
          LOG_SELECTOR(app, bundleShortVersionString)
          LOG_SELECTOR(app, bundleVersion)
          LOG_SELECTOR(app, accountDSID)
          LOG_SELECTOR(app, accountIdentifier)
          LOG_SELECTOR(app, softwareType)
          LOG_SELECTOR(app, versionIdentifier)
          LOG_SELECTOR(app, itemIdentifier)
          LOG_SELECTOR(app, containerPath)
          LOG_SELECTOR(app, storeFrontIdentifier)
          LOG_SELECTOR(app, description)
     }
}

Макрос LOG_SELECTOR определён так:

#define LOG_SELECTOR(obj, sel)\
if ([obj respondsToSelector:@selector(sel)])\
{\
     NSPrintf(@" "#sel": %@\n", [obj performSelector:@selector(sel)]);\
}

Он немного упрощает получение и вывод информации. Тестируем!

iSilvansky:~ mobile$ ~/Documents/hackup
*** Info for application <ISSoftwareApplication: 0x16d5d0>: (ru.mail.agent, 335315530:11499676)
 bundleIdentifier: ru.mail.agent
 bundleShortVersionString: 4.0
 bundleVersion: 3815
 accountDSID: 407343733
 accountIdentifier: habrahabr.ru/users/silvansky/
 softwareType: (null)
 versionIdentifier: 11499676
 itemIdentifier: 335315530
 containerPath: /private/var/mobile/Applications/374BF6DB-8773-4063-9D84-F5858DE7AEBE
 storeFrontIdentifier: 143441
 description: <ISSoftwareApplication: 0x16d5d0>: (ru.mail.agent, 335315530:11499676)
 *** Info for application <ISSoftwareApplication: 0x16f100>: (com.getdropbox.Dropbox, 327630330:11201748)
 bundleIdentifier: com.getdropbox.Dropbox
 # ... many more ...

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

Теперь вывод нашей программы несколько упростился:

iSilvansky:~ mobile$ ~/Documents/hackup
ru.mail.agent
com.getdropbox.Dropbox
# ... more and more ...
8HLDK844H7.net.litchie.idos
iSilvansky:~ mobile$ exit
logout
Connection to 192.168.2.2 closed.

Можем теперь этот вывод грепать, можем слать себе на мыло, можем… Да всё что захотим мы можем! Но главное: мы теперь умеем пользоваться приватными API и писать косольные программы для iOS. Теперь можно написать что-нибудь полезное и выложить в Cydia.

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

Что планируется сделать дальше (разумеется, с подробным описанием в статьях):
  • создание твика на основе Theos
  • выкладывание программы в Cydia
  • создание своего репозитория для Cydia
  • теория и практика создания и распространения зловредов для iOS (проникновение в систему через скачанный с торрентов/installous .ipa файл)
  • реверс-инженеринг программ и библиотек для iOS и OS X
Теги:
Хабы:
Всего голосов 37: ↑31 и ↓6+25
Комментарии7

Публикации