Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2026-43499-pmg110-root | Kitploit
Tools/GitHubGitHub/soralis0912/cve-2026-43499-pmg110-root
Android SecurityPrivilege EscalationVulnerability AnalysisExploitationPost-ExploitationPenetration TestingMobile SecurityRed TeamingPayload DevelopmentBinary Exploitation
GitHubsoralis0912/cve-2026-43499-pmg110-root
124 days agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

CVE-2026-43499-pmg110-root

View Repository

pmg110-root

CVE-2026-43499 (futex PI rt_mutex_waiter use-after-free) local privilege escalation, ported to the OPPO PMG110 / K15 Pro+ — MediaTek MT6991, ColorOS 16.

One file pushed, run over LD_PRELOAD:

root@kitploit:~
adb push out/preload-pmg110-16.0.9.400.so /data/local/tmp/preload.so
adb shell chmod 644 /data/local/tmp/preload.so
adb shell LD_PRELOAD=/data/local/tmp/preload.so /system/bin/true

On success a persistent su is left behind:

root@kitploit:~
adb shell /data/local/tmp/su -c id      # uid=0(root)

Verified on device (2026-07-27): uid=0 in about 35 seconds from a bare run with no environment overrides, and su answering afterwards from an ordinary unprivileged adb shell:

root@kitploit:~
$ adb shell "/data/local/tmp/su -c 'echo 0 > /proc/sys/kernel/kptr_restrict'"
$ adb shell "/data/local/tmp/su -c 'grep -w init_task /proc/kallsyms'"
ffffffe89033e780 D init_task

The exploit and the su install are both verified on this device. What that rooted shell then read back also confirms P0_KERNEL_PHYS_LOAD, the symbol offsets and KS_MTE_TAGGED=0 independently of the exploit — see targets/pmg110-16.0.9.400/NOTES.md.

What it does, and what it does not

It runs Write 1 (SELinux permissive) and Write 2 (cred → init_cred), gets a child process to uid=0, and from there installs the embedded su daemon.

  • still one file pushed. su is not a second artifact: su_daemon.c is built as a standalone aarch64 PIE and .incbin'd into the library's .rodata, so it rides inside preload.so and is written back out at run time. warhol-root's route, unchanged.
  • no root script, no ksud, no KernelSU
  • the calling process stays unprivileged — it gets root by asking the daemon, which is the same thing you will do from the shell afterwards
  • SELinux is left permissive, as warhol-root leaves it: the daemon has to serve unprivileged clients over its socket. Reboot to restore enforcing.

su is installed in three places, because one of them is the one you will actually reach:

The daemon listens on /data/local/tmp/temp_su.sock and logs to /data/local/tmp/su_daemon.log. Root is not persistent across reboot — re-run the LD_PRELOAD line after each boot.

For KernelSU installation, use the /data/local/tmp/a/e route in ghostlock-oneplus instead.

Relationship to warhol-root

Everything except the exploit core is warhol-root's, taken rather than reinvented:

  • the layout — per-device headers under targets/<device>/, staged into source/src/ at build time, so switching DEVICE can never leave the previous device's headers behind
  • the build — source/Makefile's toolchain selection (NDK if there is one, host clang against the NDK sysroot if there is not) and the two-stage embed rule that produces build/embed/su_daemon_aarch64_pie before linking the .so
  • the su route — su_daemon.c and su_blob.S are byte-identical to warhol-root's, and su_install.c is its preload.c installer

The exploit core is not warhol-root's. warhol-root is popsicle, which is pinned to GKI 6.12 / android16 and whose generate_target.py refuses any other banner. PMG110 is 6.6 / android15, so the core here is the ghostlock 6.6 tree — itself a descendant of the same code (kernelsnitch/utils.h and timeutils.h are byte-identical between the two repos), further developed.

Every line of the exploit proper — Write 1, Write 2, KernelSnitch, the pselect route — is the same code in both trees.

Where the su install is called from

This is the one structural difference, and it is forced by the two trees getting root in different shapes.

warhol-root roots the exploit process itself and so calls install_embedded_su() straight from run_direct_root(). Here Write 2 swaps the cred pointer of a forked child and the parent stays the unprivileged caller, so the child in child_main() is the only context that can do the install — that is where it runs.

Both trees carry the same weak install_embedded_su() stub in util.c that returns ENOSYS; providing the strong definition is what turns the route on. That is worth knowing because a build that somehow drops su_install.c still links and still runs — it just reports su=0/38 and installs nothing.

Build

root@kitploit:~
make                      # = make preload -> out/preload-<DEVICE>.so
make DEVICE=<name>        # use targets/<name>/
make devices              # list available DEVICE values
make info                 # show the selected target and the resolved toolchain

The toolchain is found on its own: ANDROID_NDK_HOME / ANDROID_NDK_ROOT first, then the usual NDK install locations for Linux and macOS, and failing all of those, host clang targeting the NDK sysroot. Set ANDROID_NDK_HOME only to override the search. make info prints what it picked.

The build is two stages, which is the part worth knowing:

  1. su_daemon.c → build/embed/su_daemon_aarch64_pie, a standalone aarch64 PIE
  2. su_blob.S .incbins that binary into .rodata, and the whole thing links into the one preload.so

So make clean and a rebuild are the only way to change the embedded su — editing su_daemon.c alone is enough, the dependency is declared, but the blob is a build artifact and is not tracked.

targets/<device>/{target.h,device_offsets.h} are re-staged into source/src/ on every build, so a stale header from another device cannot be picked up silently.

out/*.so is not tracked (same convention as warhol-root) — clone and make.

The .so is built -fvisibility=hidden and exports zero symbols. An LD_PRELOAD library wins symbol lookup for the entire process, so anything it exported could shadow a same-named symbol in the host binary or in libc. That flag governs C codegen only, so su_blob.S marks its two symbols .hidden by hand — without those lines the blob bounds would be the only thing the library still exported.

Environment variables

None of these were needed on the verified run.

Reading the log

root@kitploit:~
[*] futex_hashsize 2048 (8 possible CPUs)
[*] ks collisions=3/3 baseline=8 threshold=10x (80) accepted=[1244..1597] slowest_rejected=N
[+] child uid = 0
[+] embedded su wrote 15304 bytes to /apex/com.android.virt/bin/su
[+] embedded su daemon ready pid=NNNN socket=/data/local/tmp/temp_su.sock daemon=/apex/com.android.virt/bin/su
[+] embedded su install ok=1 errno=0 daemon=NNNN
[+] su ready: /data/local/tmp/su and /apex/com.android.virt/bin/su
[+] ghostlock preload verdict: EXPLOIT OK

child uid = 0 is the exploit succeeding; everything after it is the install. The two are reported separately on purpose, and so is the verdict:

The middle one is the distinction worth having: it says the offsets in target.h are right for this build and the problem is somewhere in the install, which is a completely different thing to debug. su=0/38 (ENOSYS) inside it specifically means the weak stub is what got linked.

mm_struct leak failed followed by prepare_kernel_page retry N/24 is not a failure. It is loop progress, and the successful run shows it too. Nothing has failed until 24 attempts are exhausted and prepare_kernel_page timeout appears. Likewise probing cfi ... expected=9 with child uid = 2000 is one round missing, out of ten.

Do not judge a run from a truncated log — that mistake cost a full round of misdiagnosis here.

A whole failed run is normal too. The pselect race is not 100%: a run can lose it five times over and end in Write 1 failed, and the next run takes it first try with ret=9. Observed on this device. ret=4 expected=9 is what losing the race looks like, not a wrong target.h — one failure is not a reason to go re-derive offsets. Re-run it.

Files

License

For authorized security research and educational purposes only.

Download Tool
DeviceOPPO PMG110 / K15 Pro+ / OP61E5L1
SoCMediaTek MT6991 (Dimensity 9500s)
Kernel6.6.118-android15-8-g93e223c276e7-abogki500782043-4k (GKI, 4K pages)
BuildColorOS 16 / PMG110_16.0.9.400(CN01) — same kernel bytes as 16.0.8.300
BugCVE-2026-43499, unfixed in this image (shown by disassembly, not by version)
PathWhy
/apex/com.android.virt/bin/suon a tmpfs mounted over that directory; on PATH for a root shell
/data/local/tmp/sureachable from a plain adb shell with no PATH games
/apex/com.android.virt/bin/su in adbd's mount namespaceinstalled via setns, so a new adb shell sees it
FileRelationship
util.c slide.c fops.c pipe.c root.c miniadb.c common.h offset.h kernelsnitch/*ghostlock's, byte-identical
su_daemon.c su_blob.Swarhol-root's, byte-identical (su_blob.S adds two .hidden lines — see Build)
su_install.cwarhol-root's preload.c installer, moved to its own file because this tree's preload.c already has a different job
main.cghostlock's, plus the su call in the rooted child and the result reporting
preload.conly here — the constructor and the dual-sink log
offsets.hstruct definition only; the entry is staged from targets/<device>/device_offsets.h
VariableEffect
GHOSTLOCK_LOGlog destination (default /data/local/tmp/.ghostlock.log); output goes to stdout and the file
GHOSTLOCK_KS_VERBOSE=1print KernelSnitch's collision addresses and sweep ranges
GHOSTLOCK_KS_THRESHOLD=<n>override the collision threshold multiplier
GHOSTLOCK_MTE=1sweep kernel pointer tags as well (15x slower)
GHOSTLOCK_PHYS_LOAD=0x...override the kernel physical load address
PSELECT_SHIFT=<n>override the stack overlay shift (replaces, does not add)
VerdictMeaning
EXPLOIT OKroot, and su answers
EXPLOIT OK, SU INSTALL FAILEDWrite 1 and Write 2 landed; only the install went wrong
EXPLOIT FAILEDthe writes did not land
ABORTEDthe run died before it could report — read the last [!] line
PathContents
source/src/preload.cconstructor: runs the exploit, reports, stops
source/src/main.cthe exploit itself (Write 1 / Write 2)
source/src/su_daemon.cthe su binary — built standalone as an aarch64 PIE, not linked into the .so
source/src/su_blob.S.incbins that PIE into the .so's .rodata
source/src/su_install.cwrites the blob back out, starts the daemon, probes it
source/src/target.hstaging destination (gitignored)
targets/<device>/target.hcompile-time layout: struct offsets, physmap constants, slab and futex shapes
targets/<device>/device_offsets.hglobal symbol offsets from kallsyms
tools/extract_device.pyboot.img → offsets, BTF struct fields, pselect overlay result
tools/preloader_memlayout.pyMediaTek preloader → P0_KERNEL_PHYS_LOAD
tools/qemu_verify.pyboots the kernel under QEMU: measures the stack overlay, checks linear-map stability
tools/device_probe.shpre-flight check from an unprivileged adb shell