Exploring the iOS screen framebuffer– a kernel reversing experiment

Billy Ellis
11 min readJan 18, 2020

It’s been over two years since I last published a blog, so I thought I’d give this another go in 2020 and kick it off by writing about an iOS-related project I’ve been working on over the last couple weeks – a reverse engineering task involving the iOS screen frame-buffer.

First of all, my inspiration to start looking at this came shortly after the release of the checkra1n jailbreak for the iPhone 5S – iPhone X. If you haven’t yet played with checkra1n, check it out here – https://checkra.in/.

One of the things checkra1n does during the jailbreaking process is display a series of debug messages on the phone’s screen to let the user know about the progress of the jailbreak, and allow them to see what went wrong in the event of the jailbreak failing.

These debug messages are written to the display over the top of the standard boot screen. I was intrigued by this and started to wonder how the checkra1n team had implemented it. I heard some phrases thrown around online mentioning the use of a “framebuffer” and that the jailbreak tool was somehow “writing” to it.

So I set myself the challenge of figuring out how this worked and how I could replicate this in order render some characters on the screen by directly manipulating the pixels.

Locating the iOS frame-buffer in kernel memory

The first step towards reaching my goal of writing custom characters to the screen was to find where in memory the screen pixel data was stored.

The checkra1n tool manipulates pixels in the framebuffer during the iBoot stage of the boot process, which is why we see the debug messages printed over the Apple boot-logo. I decided to take a different approach and focus on an already-booted iPhone.

The only difference meant dealing with kernel memory as opposed to iBoot memory, and dealing with the kernel memory seemed like the simpler approach as I could get started right away without having to construct a fully patched boot-chain etc.

I started digging around to find where the iOS kernel holds the screen pixel data in memory. Knowing this memory location would allow me to write to it (assuming I’m using a jailbroken device with a TFP0 patch) resulting in the colour of the pixels changing.

My first thought was to see if there was some kind of framebuffer symbol within the iOS kernel.

I opened a decrypted kernelcache file (for this, I’m using an iPhone3,1 iOS 7.1.2 kernel) in Hopper disassembler and searched for “framebuffer” but no luck :(

Despite not finding any symbols, Hopper did find a string containing “framebuffer”:

This string appeared to be some kind of debug message coming from the function initialize_screen. I went to the XNU source code to confirm this function actually existed and found it in the file https://github.com/apple/darwin-xnu/blob/a449c6a3b8014d9406c2ddbdc81795da24aa7443/osfmk/console/video_console.c.

Looking at the first few lines of code in this function it is clear that this function is responsible for setting up the device’s screen and assigning values for the width, height, colour depth etc.

The first argument to this function is a pointer to a PE_Video structure. I went to where this struct was defined (in https://github.com/apple/darwin-xnu/blob/master/pexpert/pexpert/pexpert.h) and found that it holds some information about the host’s screen, including the width, height, colour depth and a base address of where in memory the pixel data actually lies.

struct PE_Video {        
unsigned long v_baseAddr; /* Base address of video memory */
unsigned long v_rowBytes; /* Number of bytes per pixel row */
unsigned long v_width; /* Width */
unsigned long v_height; /* Height */
unsigned long v_depth; /* Pixel Depth */
unsigned long v_display; /* Text or Graphics */
char v_pixelFormat[64];
unsigned long v_offset; /* offset into video memory to start at */
unsigned long v_length; /* length of video memory (0 for v_rowBytes * v_height) */
unsigned char v_rotate; /* Rotation: 0:normal, 1:right 90, 2:left 180, 3:left 90 */
unsigned char v_scale; /* Scale Factor for both X & Y */
char reserved1[2];
#ifdef __LP64__
long reserved2;
#else
long v_baseAddrHigh;
#endif
};

If I could find this structure sitting in the kernel memory I’d be able to follow the v_baseAddr pointer and find the raw pixel data.

I followed the first XREF from the debug string I had found in the iOS kernelcache and landed in the following code:

If this code was part of initialize_screen then I’d have a chance at finding the address of this PE_Video structure in here.

There was no symbol for initialize_screen in the binary so I couldn’t tell for sure if the code I had found myself in was actually initialize_screen but a couple of observations between the disassembly and the source code left me pretty confident that it probably was.

I did a bit of digging and found out that the PE_Video structure is only used when setting up the screen initially, and that the screen information was actually held somewhere else after the kernel had properly started.

The values passed in through the PE_Video structure are copied over to a global vc_info structure named vinfo further down in the code.

Looking at the definition for vc_info we can see that it is similar to the PE_Video structure in terms of the information it holds about the screen:

struct vc_info{ 
unsigned int v_height; /* pixels */
unsigned int v_width; /* pixels */
unsigned int v_depth;
unsigned int v_rowbytes;
unsigned long v_baseaddr;
unsigned int v_type;
char v_name[32];
uint64_t v_physaddr;
unsigned int v_rows; /* characters */
unsigned int v_columns; /* characters */
unsigned int v_rowscanbytes; /* Actualy number of bytes used for display per row*/
unsigned int v_scale;
unsigned int v_rotate;
unsigned int v_reserved[3];
};

The vinfo structure is assigned its values between calls to simple_lock and simple_unlock in initialize_screen.

if (vc_progress) { 
simple_lock(&vc_progress_lock);
vinfo = new_vinfo;
simple_unlock(&vc_progress_lock);
}

Locating this code in the disassembly was relatively easy as we had some pretty clear ‘markers’ to look out for — the lock and unlock functions.

Here is the disassembly of the above code in the iOS kernel (note: in the kernel version I was using for this, the lock and unlock functions used were actually _lck_spin_lock and _lck_spin_unlock instead of simple_lock and simple_unlock):

8011c688         blx        _lck_spin_lock                                      
8011c68c ldr r0, [sp, #-0x8 + 56]
8011c68e add.w r1, fp, #0x8
8011c692 str.w r0, [fp]
8011c696 ldr r0, [sp, #-0x8 + 20]
8011c698 str.w r0, [fp, #0x4]
8011c69c ldr r0, [sp, #-0x8 + 52]
8011c69e stm.w r1, {r0, r8, sl}
8011c6a2 ldr r0, [sp, #-0x8 + 24]
8011c6a4 str.w r0, [fp, #0x14]
8011c6a8 movs r0, #0x0
8011c6aa strb.w r0, [fp, #0x18]
8011c6ae ldr r0, [sp, #-0x8 + 28]
8011c6b0 vld1.8 {d16, d17}, [r0]
8011c6b4 add r0, sp, #0x54
8011c6b6 vld1.32 {d18, d19}, [r0]
8011c6ba ldr r0, [sp, #-0x8 + 36]
8011c6bc vst1.8 {d16, d17}, [r0]
8011c6c0 ldr r0, [sp, #-0x8 + 40]
8011c6c2 vst1.8 {d18, d19}, [r0]
8011c6c6 ldr r0, [sp, #-0x8 + 60]
8011c6c8 str.w r0, [fp, #0x3c]
8011c6cc str.w r5, [fp, #0x38]
8011c6d0 ldr r0, [sp, #-0x8 + 88]
8011c6d2 vldr d16, [sp, #-0x8 + 80]
8011c6d6 str.w r0, [fp, #0x48]
8011c6da ldr r0, [sp, #-0x8 + 32]
8011c6dc vstr d16, [fp, #0x40]
8011c6e0 str.w r0, [fp, #0x4c]
8011c6e4 add r0, sp, #0x38
8011c6e6 vld1.32 {d16, d17}, [r0]
8011c6ea ldr r0, [sp, #-0x8 + 44]
8011c6ec vst1.64 {d16, d17}, [r0, #0x80]
8011c6f0 mov r0, r6
8011c6f2 blx _lck_spin_unlock

The series of STR and LDR instructions in the above assembly code are copying each of the values out of the PE_Video struct over to the new vc_info struct.

I decided to go to the same code in Ghidra, in pseudo code view, as it helped display the memory addresses that were being written to a little clearer.

_lck_spin_lock(&DAT_8037d714);
DAT_8037f260 = uStack104;
DAT_8037f268 = uStack108;
uRam8037f278 = 0;
uRam8037f29c = uStack100;
uRam8037f2b0 = CONCAT44(uStack96,uStack96);
uRam8037f2b8 = CONCAT44(uStack92,uStack92);
DAT_8037f264 = uVar12;
DAT_8037f26c = uVar14;
DAT_8037f270 = iVar15;
uRam8037f274 = uVar11;
uRam8037f298 = uVar7;
_DAT_8037f2a0 = uVar4;
DAT_8037f2a8 = iVar5;
uRam8037f2ac = uVar10;
_lck_spin_unlock(&DAT_8037d714);

The address 0x8037f260 is written to first, on the line DAT_8037f260 = uStack104;. This is actually the base address of the vc_info structure in memory. All other addresses written to in the above code are relative to this address.

Taking this address, adding the KASLR slide and reading some bytes from it produced the following output:

Billys-N90AP:/var/mobile root# ./kernread 0x9B17F260 0x50
0x9b17f260: 000003c0 00000280 00000020 00000a00 | � �
0x9b17f270: 92e31000 00000000 00000000 ffff0000 | �� ��

Nice! This definitely looks like it is the vc_info structure.

We can confirm that this is the case by comparing the values against the struct definition — the 3c0 and 280 (in hex) should represent the screen height and width in pixels. This checks out as I’m using an iPhone with a 3.5 inch display, so the decimal conversions of these values (960 and 640) are accurate.

The 5th value in the memory dump is what I was looking for — the v_baseaddr pointer to the start of the raw pixel data. Reading from this memory, we see a bunch of ffffffff values.

Billys-N90AP:/var/mobile root# ./kernread 0x92e31000 0x100
0x92e31000: ffffffff ffffffff ffffffff ffffffff | ���� ���� ����
0x92e31010: ffffffff ffffffff ffffffff ffffffff | ���� ���� ����
0x92e31020: ffffffff ffffffff ffffffff ffffffff | ���� ���� ����
0x92e31030: ffffffff ffffffff ffffffff ffffffff | ���� ���� ����
0x92e31040: ffffffff ffffffff ffffffff ffffffff | ���� ���� ����

This is because at the time of reading the memory, I had an app open on the iPhone’s screen with a white background, so these ffffffff values are representing those white pixels.

However, when I read from the same memory when on the home screen, we get a different result:

Billys-N90AP:/var/mobile root# ./kernread 0x92e31000 0x100
0x92e31000: ff162341 ff162342 ff172442 ff152241 | �#A �#B �$B �"A
0x92e31010: ff152342 ff162342 ff162341 ff162241 | �#B �#B �#A �"A
0x92e31020: ff172341 ff162241 ff172341 ff162241 | �#A �"A �#A �"A
0x92e31030: ff172240 ff172341 ff172241 ff172140 | �"@ �#A �"A �!@
0x92e31040: ff172141 ff172341 ff16203e ff152041 | �!A �#A � > � A

These values are representing the blues and greens found on the default iOS 7 wallpaper on the SpringBoard.

Manipulating the pixels

Now that we know exactly where the screen pixel values are held in kernel memory, we can move on to actually manipulating them.

I started by writing a small program that writes some black and white pixels to the screen, just to test that any changes I made to the memory actually affected what was being displayed on the screen.

The result looked something like this:

So now I had confirmed that this was indeed the framebuffer and changes to the memory resulted in changes to what we see on the device’s screen almost immediately.

Cool! Now we can look at actually rendering something potentially useful.

Rendering characters

The next step was to figure out how to actually render alpha-numeric characters so we can print our own text strings over the display.

I did some more digging around the XNU source and found some functions that appeared to be responsible for rendering characters on the screen. One of these functions was vc_render_char.

This function follows an algorithm that renders a specific character on the screen. The font used here is the iso_font which is the same font that is used during the verbose boot mode on iOS and macOS. It is defined as a 256*16 byte array in the file https://github.com/apple/darwin-xnu/blob/master/osfmk/console/iso_font.c.

The algorithm to render the characters works by taking a desired character — say ‘A’ — and using its ASCII code (65) multiplied by the desired character height in pixels and using it as an index into the iso_font array.

A series of bit-shifts and bitwise ANDs are then performed on the values to produce an output value. Depending on this output value, either a black or white pixel is written to the framebuffer.

I wrote some code to test if I could generate a character based on this algorithm, using the iso_font. The output was as follows:

~/Documents/dev ./print_char
1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1
1 0 0 0 0 0 1 1
0 0 1 1 1 0 0 1
0 0 1 1 1 0 0 1
0 0 1 1 1 0 0 1
0 0 0 0 0 0 0 1
0 0 1 1 1 0 0 1
0 0 1 1 1 0 0 1
0 0 1 1 1 0 0 1
0 0 1 1 1 0 0 1
0 0 1 1 1 0 0 1
1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1
~/Documents/dev

Looking closely at the above output, you can see that the 0 characters create the shape of a capital A and the 1 characters act as a background.

To translate this to screen pixel data all we have to do is render a white pixel when the value returned is a 0 and a black pixel when the value returned is a 1.

Final outcome

As a result of the above research, I managed write a program that allows me to render arbitrary text strings to the iPhone’s screen by directly modifying the framebuffer pixels!

I guess this research doesn’t have any real practical use, but it was a fun project to work on and the end result looks pretty cool ;)

If you want to try running this code on your own jailbroken device, you can find the code for my fb_write tool here https://github.com/Billy-Ellis/framebuffer_write.

Right now the offsets are hard-coded for the iPhone3,1 running iOS 7.1.2, but you can add your own for other devices you want to try this on.

Hopefully you enjoyed this post and got something out of it! Feedback is welcome — you can contact me @bellis1000 on Twitter or billy@zygosec.com via e-mail.

Thanks for reading!

--

--