Доброго дня всем!
Сегодня я расскажу вам о том, как можно легко, быстро и просто написать консольную программку для запуска на 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
Автор: silvansky