Screenhax: The obvious bug that led to the creation of Phi

March 29, 2022

This article is a followup to that one. If you’ve been following the Numworks community lately, you might know that I released a jailbreak for Epsilon 16 two days ago. Since the exploit has been patched yesterday evening, here is a write-up on that.

What is phi ?

Phi is the combination of three things:

  • A kernel exploit
  • A payload that allows to unlock, erase and re-flash the internal flash
  • A custom bootloader

This article will only talk about the kernel exploit, as the custom bootloader is already public and open-source, and as there is almost no point talking about the payload.

Why is phi ?

I want to make this really clear: this is not about messing with Numworks. I love the Numworks, but I love it the way it was before Epsilon 16. I love it open, programmable, customizable, hackable. I paid for the hardware, I want to use the hardware however I want. Phi is there to give freedom to the user, annoying Numworks is only an unfortunate side effect.

Now that this is out of the way, let’s see how the exploit works.

How does it work ?

Phi uses an exploit that I named screenhax. As the name suggests, it has something to do with the screen driver. Screenhax uses SVC 18 (Ion::Device::Display::pullRectSecure) to overwrite the kernel stack.

Here is the code of that method:

void Ion::Device::Display::pullRectSecure(KDRect r, KDColor * pixels) {
  if (Board::addressInUserlandRAM(pixels)) {
    pullRect(r, pixels);
  }
}

The method Board::addressInUserlandRAM checks is the address is in userland RAM, but they never check if the end of the buffer is in userland RAM, which is perfect, because the RAM layout of the OS looks like this:

            | USERLAND DATA/BSS | HEAP | USERLAND STACK | KERNEL STACK | KERNEL DATA/BSS - canary |
 Kernel:    | xxxxxxxxxxxxxxxxxxxxxxxx | USERLAND STACK | KERNEL STACK | KERNEL DATA/BSS - canary |
 Userland:  | USERLAND DATA/BSS | HEAP | USERLAND STACK | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx |

As we can see, the kernel stack is located right after the end of userland RAM. This means we can overwrite the kernel stack, and so overwrite the return address of the running function.

How did they patch it ?

As usual, Numworks made an update without releasing the sources, meaning it’s Ghidra time! Here is what the new decompiled code looks like:

void pullRectSecure(uint xy,uint wh,int *color) {
    short width = wh & 0xFF;
    short height = wh >> 0x10;

    if ((color + 0xe0000000 < 0x3f000) &&
        ((uint)((int)color + (int)width * (int)height * 2 + 0xe0000000) < 0x3f000)) {
        pullRect(xy,wh,color);
    }
}

We can see that they now check for the end of the color buffer too. Nice job patching user freedom, Numworks!

What can we learn ?

They, for sure, used dynamic analysis. They can’t have fixed that bug so quickly (remember, Epsilon 18.2.3 came out 23 hours after the release of Phi) by using only static analysis, due to the multiple levels of obfuscation I used. Measures will need to be taken against that if someday a new version of Phi comes out.

Legal stuff about this article

Numworks being a French company, French laws apply. This article is published for educational purposes. The code snippets quoted in this article are cited under article L211-3 of the code of intellectual property, specifically under paragraph 3°a. Please contact me by email in case of complaint.