Shared Object Blog

Nothing is (not) Secure

I love my Nothing Phone (1). It has good enough hardware. It looks good. And it’s not as bloated as some other phones. But the real reason I enjoy having a Nothing Phone is that niche devices are simply more fun to reverse engineer.

Android’s application sandbox makes it quite difficult to pick apart proprietary apps, as dynamic debugging and even accessing data files is impossible. Usually, the way to solve this is by using a rooted device that allows you to bypass most of these restrictions.

Rooting an Android device should be easy enough: you just unlock the bootloader, flash some custom firmware, and hope you didn’t just brick your phone. But doing it this way is both not as fun and can be detected. I don’t want my device to be detected as rooted, it might cause applications to behave differently or just completely block me from using them.

So, the first step in making my device usable for reverse engineering is to find a privilege escalation vulnerability. Obviously.

This is the story of one such vulnerability: CVE-2024-51440.

Nothing OS

Nothing OS is an Android fork that comes pre-installed on Nothing phones. It is mostly stock Android with a few tweaks for hardware support (such as the Glyph Interface) and some user-space additions.

These user-space additions include:

From a security researcher’s perspective, these vendor specific components are extremely interesting. They usually have somewhat elevated privileges and are less vetted compared to AOSP components, leaving them ripe with bugs to exploit.

For this project, I decompiled all Nothing-specific system apps that I found on my phone and looked for obviously fishy code.

The Vulnerability

com.nothing.bpf is a pre-installed app that is part of Nothing OS. It is hidden and not meant to be user-facing, but exposes a service called NtBpfService, through which others can configure network settings.

The name comes from the BPF Linux subsystem, which is responsible for packet filtering.

I am not completely sure who is supposed to interact with this service, and the fact that it requires no special permissions to use is an issue in and of itself. Regardless, we will soon encounter an even worse issue in the implementation of this service.

Services in Android use the Binder IPC mechanism to expose callable methods to other components on the device. Each service defines an interface declaring exposed functions, usually using AIDL. To communicate with the service, I recreated its AIDL interface and used Android Studio to generate a client stub.

Although com.nothing.bpf is not a system app, it does run in a special SELinux context: ntbpf_app. This SELinux context allows the app to connect to bpfloader’s Unix socket. bpfloader is the actual highly privileged (runs as root) daemon that can carry out the network configuration changes requested from NtBpfService. In short, com.nothing.bpf is mostly interesting only as stepping stone to bpfloader.

bpfloader is also a Nothing OS specific, even though that term already exists in Android

Looking at the decompiled code of NtBpfService, something immediately stands out:

public class NtBpfService extends Service {
    private int bindCount = 0;
    private final List<IResponseCallback> callbackList = new ArrayList();
    private final IBpf.Stub mBinder = new IBpf.Stub() { // from class: com.nothing.bpf.NtBpfService.1
        @Override // com.nothing.bpf.interfaces.IBpf
        public boolean setNetWorkLimit(String str, String str2, int i) {
            return Connection.getInstance().write("tc filter del dev {iface} {direction};tc filter add dev {iface} {direction} prio 1 protocol all matchall action police rate {rate} burst 128Kb conform-exceed pipe/continue drop".replace("{iface}", str).replace("{rate}", str2).replace("{direction}", i == 0 ? "ingress" : "egress"));
        }

        @Override // com.nothing.bpf.interfaces.IBpf
        public boolean cleanTcRule(String str, int i) {
            return Connection.getInstance().write("tc filter del dev {iface} {direction}".replace("{iface}", str).replace("{direction}", i == 0 ? "ingress" : "egress"));
        }

        @Override // com.nothing.bpf.interfaces.IBpf
        public boolean setUidIpTableMark(int i) {
            return Connection.getInstance().write("iptables -t mangle -A OUTPUT -m owner --uid {uid} -j TOS --set-tos 0xb8".replace("{uid}", String.valueOf(i)));
        }

        @Override // com.nothing.bpf.interfaces.IBpf
        public boolean cleanUidIpTableMark(int i) {
            return Connection.getInstance().write("iptables -t mangle -F OUTPUT -m owner --uid {uid}".replace("{uid}", String.valueOf(i)));
        }

        @Override // com.nothing.bpf.interfaces.IBpf
        public boolean setUidTcpRtt(int i) {
            return Connection.getInstance().write("setUidTcpRtt;" + i + ";/vendor/etc/bpf/info");
        }

        @Override // com.nothing.bpf.interfaces.IBpf
        public boolean closeUidTcpRtt() {
            return Connection.getInstance().write("closeUidTcpRtt");
        }

        @Override // com.nothing.bpf.interfaces.IBpf
        public boolean update_fstb_fps_list(String str) {
            return Connection.getInstance().write("echo {commond} > /sys/kernel/fpsgo/fstb/fstb_fps_list".replace("{commond}", str));
        }

        @Override // com.nothing.bpf.interfaces.IBpf
        public boolean setIcmpMonitor(boolean z, int i) {
            return Connection.getInstance().write("setIcmpMonitor;" + z + ";" + i);
        }

        @Override // com.nothing.bpf.interfaces.IBpf
        public void regCallback(IResponseCallback iResponseCallback) {
            NtBpfService.this.callbackList.add(iResponseCallback);
            Connection.getInstance().setCallback(NtBpfService.this.callbackList);
        }
    };

    // ...
}

Hint: the issue is even worse than the “commond” typo

Whenever the service is invoked, it sends a message to bpfloader using the aforementioned Unix socket. This message requests the daemon to perform specific actions. What stands out is the fact the many of these messages look like shell commands. For example update_fstb_fps_list:

        @Override // com.nothing.bpf.interfaces.IBpf
        public boolean update_fstb_fps_list(String str) {
            return Connection.getInstance().write("echo {commond} > /sys/kernel/fpsgo/fstb/fstb_fps_list".replace("{commond}", str));
        }

Disassembling the native daemon bpfloader, we can confirm our suspicion: certain messages are simply handled by feeding them into a system library function — executing them as shell commands.

Since the input is not sanitized, neither by com.nothing.bpf nor bpfloader, this is vulnerable to shell injection. Basically, any malicious app can abuse the NtBpfService to send shell commands that will be executed as root by bpfloader. That’s a local privilege escalation vulnerability!

Overall, I’d say that this bug isn’t particularly clever or well hidden. It only needed someone to go searching for it.

Privilege? What’s That?

Let’s take a setup back.

We set out to find a privilege escalation vulnerability, but which privileges did we aim to escalate to?

For my purposes, I mostly wanted to read and write access to application sandboxes. Figuring out how to escalate that to actual code execution and dynamic debugging could be a fun separate project.

So although we are able run shell commands as root, did we actually accomplish what we set out to do? In short, no, at least not trivially.

The injected commands do execute as root, but in a confined SELinux context that was specially crafted for bpfloader. This context restricts access to most of the filesystem, allowing only for a set of files to be read. Even escalating this shell injection to arbitrary code execution is non-trivial, since we can’t read any files that an app or the shell user wrote.

Although very confined, we still gained some power: the bpfloader process has chown, net_admin and sys_admin capabilities. Taking this bug further is surely possible.

However much I enjoyed trying to exploit this bug, I decided to disclose this bug before finishing, since I found a couple of other bugs which seemed easier to exploit.

Disclosure

I disclosed the vulnerability directly to Nothing, through their vulnerability disclosure program. I never received a reply, even after emailing them multiple times.

It was fairly disappointing, as I didn’t get credit or my bounty.

Coincidentally, a new firmware version released a couple of days after I first published this post, fixing the bug!

Timeline

The Fix

In my bug report, I suggested to do two things to patch the bug:

  1. Add a permission to the NtBpfService, so only certain Nothing system apps could use it
  2. Sanitize the inputs received by bpfloader, before pasting them into shell commands Nothing opted to do the bare minimum and only add the permission requirement to NtBpfService. No changes were made to bpfloader.

Although this prevents malicious apps from triggering the shell injection, certain semi-privileged apps could still abuse it to further escalate their privileges.

This fix was included in the Nothing OS 2.6 hotfix 2 (Spacewar-U2.6-241031-1818). The change log did not include any mention of the the vulnerability, but did note that it included Android’s November security patch. I’m guessing they decided to just bundle it in as part of that.