I recently encountered a device running an old version of Android and needed elevated privileges for debugging. I tried several rooting tools without much success, and found that the device had detection mechanisms that triggered protective measures. I therefore turned to using a CVE vulnerability to obtain temporary root access.

After some investigation, I found a still-working download link for the system’s OTA update package. From it, I recovered the kernel symbol table and symbol addresses. This provided the breakthrough: I could locate the symbols required by the privilege-escalation exploits and adapt and compile three of them — CVE-2015-1805 (Pipe Read), CVE-2015-5195 (Ping Pong), and CVE-2017-8890 (Phoenix Talon). They successfully obtained elevated privileges. This article records the process.

Extracting kernel symbols from an OTA package

If you already have root access, you can obtain the kernel symbol table and address information with:

echo 0 > /proc/sys/kernel/kptr_restrict
cat /proc/kallsyms > kallsyms.txt

The situation is different when all you have is an OTA update package.

The simplest way to find the OTA download URL is to capture the update traffic. Package contents vary between systems, but they commonly include a boot.img, which packages the kernel’s self-decompressing boot code, compressed kernel image, filesystem, device tree, and other components. The binwalk -Me command can inspect and extract its contents.

In the screenshot below, the descriptions and block sizes indicate that the content beginning at 0x800, including the later block at 0x47F7, is an ARM zImage kernel image. The former is the kernel boot header; the latter is the compressed Linux vmlinux data.

The extracted 47F7 file is the decompressed vmlinux. If it were an ELF file, nm -n would display its symbol table directly. In this case, however, the result was a raw kernel binary without an ELF header, so that approach would not work.

binwalk output for boot.img, showing an ARM zImage at offset 0x800 and gzip-compressed kernel data at 0x47F7.
Inspecting and extracting the Android boot image with binwalk.

After further analysis and searching, I used the kallsyms_find.py module from the vmlinux-to-elf project to extract a kernel symbol table from 47F7.

kallsyms-finder identifies an ARM little-endian Linux 3.10.49 kernel, locates the kallsyms tables, and lists recovered symbol addresses.
Recovering the symbol table from the raw kernel binary.

Restoring the symbols in IDA

With the symbol table recovered, I could load 47F7 directly into IDA. I selected the ARM little-endian architecture and set both ROM start address and Loading address to 0xc0008000.

I then wrote and ran an IDAPython script to restore symbol names in the kernel binary:

ksyms = open(r"D:\47F7.kallsyms")
for line in ksyms:
    addr = int(line[0:8], 16)
    name = line[11:-1]
    print(f"addr:{hex(addr)},{name}")
    if not ida_funcs.add_func(addr):
        print(f"Warning: Failed to add function at {hex(addr)}")
    if not idc.set_name(addr, name, idc.SN_NOWARN):
        print(f"Warning: Failed to set name at {hex(addr)}")

After the script finished, function symbols were visible. I could then search for the functions relevant to the CVEs and collect the information needed to adapt the exploits.

IDA displays the ARM kernel at 0xC0008000 with restored function names in the Functions panel and disassembly.
The kernel loaded into IDA with recovered function symbols.

A shorter route: rebuilding an ELF file

The whole process can actually be simpler. vmlinux-to-elf includes support for decompressing zImage files, extracting kernel symbol tables, and wrapping the result in an ELF file. Running:

vmlinux-to-elf 47F7 kernel.ELF

produces a kernel file with symbols that can be loaded directly into IDA for analysis.

vmlinux-to-elf recovers kallsyms, uses a base address of 0xC0008000, and writes kernel.ELF successfully.
Generating an ELF kernel image with its symbol table.

In the earlier example, I extracted 47F7 manually with binwalk. Some OTA packages contain a file named kernel instead of boot.img. binwalk identifies such a file as a kernel boot header followed by compressed vmlinux data. In that case, feeding the file directly to vmlinux-to-elf uses its zImage decompression support as well. In my testing, that support covered a wider range of images than some extract-vmlinux scripts.

file and binwalk identify a kernel file as an ARM little-endian zImage, with compressed data at offset 0x58C4.
An OTA package containing a standalone kernel image.

A look through the project’s code shows that it handles different versions of Linux’s evolving kallsyms system. It locates the relevant structures through signature searches, reproduces the kernel’s symbol decompression algorithm, and constructs a symbol table. This makes it possible to recover symbols directly from the kernel binary.

Adapting the privilege-escalation exploits

Once I had the kernel symbol table, the large body of existing vulnerability analysis and shared research made adaptation much easier. I will not go through the detailed reproduction, debugging, and vulnerability analysis here. Instead, these are the main adaptation steps and a few pitfalls I encountered.

CVE-2015-1805: Pipe Read

I used dosomder/iovyroot as a reference. The original article points to the offsets structure in jni/inlcude/offsets.h. The required addresses to locate in the kernel symbol table are ptmx_fops, sidtab, policydb, selinux_enabled, and selinux_enforcing. The article then describes adding them to jni/offset.c.

struct offsets {
    char* devname;   // ro.product.model
    char* kernelver; // /proc/version
    union {
        void* fsync;       // ptmx_fops -> fsync
        void* check_flags; // ptmx_fops -> check_flags
    };
#if (__LP64__)
    void* joploc; // gadget location, see getroot.c
    void* jopret; // return to setfl after check_flags() (fcntl.c),
                 // usually inlined in sys_fcntl
#endif
    void* sidtab;   // optional, for SELinux context
    void* policydb; // optional, for SELinux context
    void* selinux_enabled;
    void* selinux_enforcing;
};

Consult the linked repository for its current file layout.

Ping Pong: CVE label as printed in the source

The source labels this section CVE-2015-5195 and refers to fi01/CVE-2015-3636.

The offsets needed are the sk_prot and sk_stamp members of struct sock, and the mc_list member of struct inet_sock. These can be located by comparing the disassembly of inet_release, sock_get_timestampns, and ip_mc_drop_socket, respectively, with the source code. The corresponding offsets can then be replaced in the exploit.

CVE-2017-8890: Phoenix Talon

I used idhyt/androotzf as a reference. The required structures are defined directly in the code, so little modification was necessary. I changed the fake_iml_next_rcu value passed to mmap to suit the target system.

Build and memory-layout pitfalls

My testing showed that the NDK version matters. Binaries built with newer versions could produce errors or fail to exploit the vulnerability. NDK r11c was relatively stable in my tests; it is available from Android NDK Unsupported Downloads.

Memory usage also varies across systems. The MMAP_SIZE passed to mmap may require some experimentation and adjustment.

What I learned

This article records how I used a still-accessible OTA update download to extract and analyze an old Android device’s kernel and symbol table, then adapted three privilege-escalation exploits to obtain root for debugging.

I had not previously spent much time on Linux vulnerability discovery or exploit analysis. In hindsight, the process looks straightforward: extract the kernel symbols, find the offsets, and adapt the exploits. In practice, it took a fair amount of investigation and experimentation.

Much of that time went into reading about different CVEs and understanding their root causes and exploitation flows so I could work out what needed to change. Terms such as “heap spraying” and “reclaiming allocations,” which had previously sounded obscure, became concrete techniques I could observe and verify. They no longer felt so abstract or remote.

When time permits, I plan to reproduce, debug, and analyze some classic vulnerabilities to deepen that understanding.

END OF NOTE