DEV Community

Adolfo Reyna
Adolfo Reyna

Posted on

Making a New Native App for a Jailbroken iPhone 6

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 ]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The rootful detail matters. On this generation of jailbreak, native
applications are installed in the traditional location:

/Applications/YourApp.app
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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 .deb package that Cydia or dpkg can 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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));
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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];
Enter fullscreen mode Exit fullscreen mode

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"];
}
Enter fullscreen mode Exit fullscreen mode

The app also hides the status bar to use the full iPhone display:

- (BOOL)prefersStatusBarHidden {
    return YES;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

On this rootful jailbreak, Theos stages the built bundle into:

Applications/HelloWorld.app/
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
'
Enter fullscreen mode Exit fullscreen mode

That generated the installable application package:

/var/mobile/HelloWorld/packages/com.reynafamily.helloworld_0.0.1-+debug_iphoneos-arm.deb
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The installed app bundle appeared at:

/Applications/HelloWorld.app
Enter fullscreen mode Exit fullscreen mode

SpringBoard was told to register the new app:

uicache -p /Applications/HelloWorld.app
Enter fullscreen mode Exit fullscreen mode

Then the app was launched directly over SSH:

uiopen com.reynafamily.helloworld
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)