Making a New Native App for a Jailbroken iPhone 6
Disclaimer
This project was performed on my own jailbroken iPhone 6 for experimentation and learning. Jailbreaking, installing packages, building software directly on-device, and changing system configuration can cause instability, security issues, data loss, or require restoring the phone. Commands and package availability may vary depending on your jailbreak, repositories, and iOS version. Proceed only on hardware you own and understand that you do so at your own risk.
For this experiment, I used OpenAI Codex as a hands-on development assistant. Codex connected to the iPhone over SSH with my authorization, inspected the available tools, installed required packages, created and compiled the sample app, packaged and installed it, and verified that it launched. I supervised the process and permitted access to the device.
This blog post was also written with Codex: it organized the technical steps, command examples, source snippets, and explanations based on the work carried out during the session.
The iPhone 6 is old hardware now, but it is still a pleasant little computer:
a good screen, touch input, Wi-Fi, battery power, speakers, sensors, and a
compact body. With an iPhone 6 running iOS 12.5.8 and a jailbreak, I wanted to
find out whether it could still be a practical target for new software.
The answer is yes. In this project I connected to the phone over SSH, explored
low-level display access, installed an on-device iOS application toolchain, and
built a genuine UIKit Hello World app packaged as a Cydia-compatible
Debian package.
The finished app runs directly from the Home Screen and displays a fullscreen
native interface with a working button:
Hello, iPhone 6!
Native UIKit app installed through a .deb
[ Tap Me ]
This is the path from an old jailbroken phone to a new native app.
The Device
The phone was already configured as an SSH host named iphone6:
ssh iphone6
The first inspection confirmed the exact target:
Device: iPhone7,2 (iPhone 6)
Architecture: arm64
iOS: 12.5.8
Darwin: 18.7.0
Shell: /bin/sh
Jailbreak layout: rootful
The rootful detail matters. On this generation of jailbreak, native
applications are installed in the traditional location:
/Applications/YourApp.app
Newer rootless jailbreak environments generally package applications beneath
/var/jb, but that does not apply to this iOS 12 device.
What Was Already Installed
The jailbroken phone already contained a surprisingly useful command-line
environment:
apt / dpkg / dpkg-deb
clang / LLVM
git
curl / wget / ssh
ldid
uicache / uiopen
Python 3.7.3
MobileSubstrate
NewTerm
Those tools are enough to investigate the system and compile simple native
code. A new application still needs more structure: SDK framework stubs,
packaging support, and standard project templates. That is what Theos
provides.
An Early Detour: Direct Display Access
Before writing a UIKit app, I tried the most direct experiment possible: a C
program that writes pixels into the phone's framebuffer.
Unlike Linux devices with /dev/fb0, iOS exposes display plumbing through
private frameworks:
/System/Library/PrivateFrameworks/IOMobileFramebuffer.framework
/System/Library/PrivateFrameworks/IOSurface.framework
The C test loaded those frameworks, requested the main display, and attempted
to obtain the writable IOSurface behind the current screen:
result = get_main_display(&display);
result = get_default_surface(display, 0, &surface);
The executable required ldid signing and private framebuffer-related
entitlements. Once those were supplied, the call reached the real display
service:
iomfb_paint_test: starting
loaded IOMobileFramebuffer and IOSurface; requesting main display
IOMobileFramebufferGetMainDisplay: 0x00000000 display=0x101000600
requesting the display default IOSurface
IOMobileFramebufferGetLayerDefaultSurface: 0xe00002c1 surface=0x0
No writable display IOSurface was exposed. This is expected on iOS 9 and later.
In other words, the native code ran and obtained the main display object, but
iOS 12 would not expose the live pixel buffer for modification. That boundary
is sensible for a phone operating system, and it pointed toward the better
solution: build a regular UIKit application and let iOS render it normally.
Preparing an On-Device App Toolchain
The conventional jailbreak development system is
Theos. It handles:
- UIKit application project structure.
- Compilation against an iPhone SDK.
- Fake-signing application executables.
- Staging an app under
/Applications. - Building a
.debpackage that Cydia ordpkgcan install.
Theos' official iOS setup documentation recommends using a normal user account
rather than building everything as root. On this phone, the development
workspace lives under the mobile account:
/var/mobile/theos
Installing Build Prerequisites
The phone had clang, ldid, and dpkg-deb, but not make or perl. Those
were installed from the configured jailbreak package repositories:
apt-get install -y make perl rsync com.bingner.plutil
One historical-package wrinkle appeared here: installing Bingner's working
plutil implementation removed older conflicting packages that had installed
an incompatible plutil binary. That was acceptable for this build machine
because the replacement is the tool needed by current packaging workflows.
Installing Theos
Theos itself was cloned under the normal mobile account:
su - mobile -c \
'cd /var/mobile && git clone --recursive https://github.com/theos/theos.git /var/mobile/theos'
The official installer script normally automates this, but on this particular
device its dependency-elevation step waited for interactive sudo input. With
the prerequisite packages already installed through root SSH, cloning Theos as
mobile achieved the intended ownership and directory layout cleanly.
Installing an SDK That Matches the Phone
UIKit application builds require SDK headers and .tbd linker stubs. The
device had some legacy headers under /var/include, but it did not initially
have normal SDK stubs for linking a UIKit application.
Theos can retrieve SDK archives. Rather than downloading its newest SDK, I
installed the release matching this iOS 12 phone:
su - mobile -c \
'export THEOS=/var/mobile/theos; bash /var/mobile/theos/bin/install-sdk iPhoneOS12.4'
After extraction, the necessary files existed:
/var/mobile/theos/sdks/iPhoneOS12.4.sdk/System/Library/Frameworks/UIKit.framework/UIKit.tbd
/var/mobile/theos/sdks/iPhoneOS12.4.sdk/System/Library/Frameworks/Foundation.framework/Foundation.tbd
/var/mobile/theos/sdks/iPhoneOS12.4.sdk/usr/lib/libSystem.tbd
At that point, the phone had a real on-device UIKit build environment.
The Hello World Application
The first app was deliberately small: one view controller, two labels, and a
button. It is enough to confirm compilation, signing, packaging, installation,
launching, touch handling, and native layout all work on the device.
The source tree looks like this:
HelloWorld/
Makefile
control
main.m
HWRAppDelegate.h
HWRAppDelegate.m
HWRViewController.h
HWRViewController.m
Resources/
Info.plist
Targeting iOS 12 and arm64
The Theos Makefile targets the installed iPhoneOS 12.4 SDK, with an iOS 12
deployment target and the arm64 architecture used by the iPhone 6:
TARGET := iphone:clang:12.4:12.0
ARCHS = arm64
INSTALL_TARGET_PROCESSES = HelloWorld
include $(THEOS)/makefiles/common.mk
APPLICATION_NAME = HelloWorld
HelloWorld_FILES = main.m HWRAppDelegate.m HWRViewController.m
HelloWorld_FRAMEWORKS = UIKit Foundation
HelloWorld_CFLAGS = -fobjc-arc
include $(THEOS_MAKE_PATH)/application.mk
Starting UIKit
The entry point is standard Objective-C UIKit:
#import <UIKit/UIKit.h>
#import "HWRAppDelegate.h"
int main(int argc, char *argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil,
NSStringFromClass(HWRAppDelegate.class));
}
}
The application delegate creates a window and gives it a root view controller:
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
self.window.rootViewController = [[HWRViewController alloc] init];
[self.window makeKeyAndVisible];
return YES;
}
Drawing the First Screen
The view controller creates a dark fullscreen view, a greeting, a subtitle,
and a touchable system button:
UILabel *title = [[UILabel alloc] initWithFrame:CGRectZero];
title.translatesAutoresizingMaskIntoConstraints = NO;
title.text = @"Hello, iPhone 6!";
title.textColor = [UIColor whiteColor];
title.font = [UIFont boldSystemFontOfSize:30.0];
title.textAlignment = NSTextAlignmentCenter;
[self.view addSubview:title];
UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
button.translatesAutoresizingMaskIntoConstraints = NO;
[button setTitle:@"Tap Me" forState:UIControlStateNormal];
[button addTarget:self
action:@selector(buttonTapped:)
forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:button];
Button taps update the secondary label:
- (void)buttonTapped:(UIButton *)sender {
self.tapCount += 1;
self.messageLabel.text = [NSString stringWithFormat:@"Button tapped %ld time%@",
(long)self.tapCount,
self.tapCount == 1 ? @"" : @"s"];
}
The app also hides the status bar to use the full iPhone display:
- (BOOL)prefersStatusBarHidden {
return YES;
}
Packaging for Cydia
Cydia-installed software is packaged as Debian archives. The app's control
file declares package metadata:
Package: com.reynafamily.helloworld
Name: Hello World
Version: 0.0.1
Architecture: iphoneos-arm
Description: A native UIKit Hello World test for the jailbroken iPhone 6.
Maintainer: Adolfo Reyna
Author: Adolfo Reyna
Section: Utilities
On this rootful jailbreak, Theos stages the built bundle into:
Applications/HelloWorld.app/
with the application executable and its Info.plist inside.
An iOS 12 Theos Quirk
The app compiled and linked correctly, but the normal Theos package step failed
when it attempted to execute its fakeroot.sh helper directly:
Killed: 9 /var/mobile/theos/bin/fakeroot.sh
make: *** [before-stage] Error 137
The shell script itself was fine; this jailbreak was terminating its direct
execution. Calling the same script explicitly through bash fixed packaging.
I also selected the already-installed dpkg-deb backend:
su - mobile -c '
export THEOS=/var/mobile/theos
export PATH=/usr/bin:/bin:/usr/sbin:/sbin
cd /var/mobile/HelloWorld
make package \
FAKEROOT="bash /var/mobile/theos/bin/fakeroot.sh -p /var/mobile/HelloWorld/.theos/fakeroot" \
_THEOS_PLATFORM_DPKG_DEB=dpkg-deb \
THEOS_PLATFORM_DEB_COMPRESSION_TYPE=gzip
'
That generated the installable application package:
/var/mobile/HelloWorld/packages/com.reynafamily.helloworld_0.0.1-+debug_iphoneos-arm.deb
The package was only about 12 KB, a nice reminder that a native UIKit hello
world app does not need a large runtime or a browser framework.
Installing and Launching the App
Installation used dpkg, just as Cydia ultimately would:
dpkg -i /var/mobile/HelloWorld/packages/com.reynafamily.helloworld_0.0.1-+debug_iphoneos-arm.deb
The installed app bundle appeared at:
/Applications/HelloWorld.app
SpringBoard was told to register the new app:
uicache -p /Applications/HelloWorld.app
Then the app was launched directly over SSH:
uiopen com.reynafamily.helloworld
The final verification showed a normal running UIKit process:
mobile ... /Applications/HelloWorld.app/HelloWorld
Package: com.reynafamily.helloworld
Status: install ok installed
Architecture: iphoneos-arm
Version: 0.0.1-+debug
The app was now present on the iPhone's Home Screen and interactive like any
other installed application.
What This Unlocks
A functioning Hello World app establishes much more than a label on a screen.
This iPhone can now be a native target for small personal applications:
- A bedside clock or information display.
- A household dashboard.
- A music remote.
- A status monitor for self-hosted services.
- A local photo-frame application.
- A touch controller for custom hardware or home automation.
UIKit on iOS 12 is old, but it is mature, lightweight, and completely adequate
for focused applications. Keeping the design intentionally simple is an
advantage on this hardware.
Files from the Project
Display-access experiment:
iomfb_paint_test.c
iomfb_entitlements.plist
libSystem.B.tbd
Native Hello World application:
HelloWorld/Makefile
HelloWorld/control
HelloWorld/main.m
HelloWorld/HWRAppDelegate.h
HelloWorld/HWRAppDelegate.m
HelloWorld/HWRViewController.h
HelloWorld/HWRViewController.m
HelloWorld/Resources/Info.plist
Conclusion
An iPhone 6 running iOS 12.5.8 is no longer a reasonable target for arbitrary
modern software, but a jailbroken one can still be a delightful native
development device.
The key lesson was to work with the platform instead of underneath it. Direct
framebuffer access reached an iOS security barrier. A regular UIKit app,
compiled with Theos, fake-signed, packaged as a .deb, and installed through
the jailbreak package system worked exactly as hoped.
The phone now has a native application built specifically for it. More
importantly, it has a repeatable path for whatever small app comes next.
References
- Theos, installation on iOS: https://theos.dev/docs/installation-ios
- Theos, New Instance Creator and application templates: https://theos.dev/docs/NIC.html
- Theos, rootful and rootless packaging: https://theos.dev/docs/rootless
- The Apple Wiki, IOMobileFramebuffer: https://theapplewiki.com/wiki/Dev:IOMobileFramebuffer
Top comments (0)