Skip to content
KitploitKITPLOIT
ToolsExploitsBlog
Log in
Submit
ToolsExploitsBlog
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.

FeedsContactPrivacy© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
ghostlock-pfem10 — GhostLock (CVE-2026-43499) for OPPO Find X5 Pro (PFEM10) — OPlus watchdog & heap-spray detector reverse engineering | Kitploit
Tools/GitHubGitHub/imeiplus/ghostlock-pfem10
Android SecurityPrivilege EscalationMemory ForensicsVulnerability AnalysisExploitationReverse EngineeringMobile SecurityPayload DevelopmentBinary Exploitation
GitHubimeiplus/ghostlock-pfem10

ghostlock-pfem10

GhostLock (CVE-2026-43499) for OPPO Find X5 Pro (PFEM10) — OPlus watchdog & heap-spray detector reverse engineering

4823 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
View Repository

GhostLock — OPPO Find X5 Pro (PFEM10)

English · 中文

build

GhostLock (CVE-2026-43499) port for the OPPO Find X5 Pro on ColorOS 16. Reaches a uid=0 child process and a loaded kernelsu.ko; the root process is intercepted.

Vulnerability

CVE-2026-43499 — futex PI use-after-free. remove_waiter() clears current->pi_blocked_on when current is the requeuer, on the -EDEADLK rollback path of rt_mutex_start_proxy_lock().

remove_waiter @ 0xffffffc0081ed254 — pre-fix shape.

Device

DeviceOPPO Find X5 Pro (PFEM10)
SoCSM8450 / Adreno 730
OSColorOS 16.0.3.520 (CN01)
Kernel5.10.236-android12-9-o-gaf2075ad2c06
Bootloaderlocked, green
VA_BITS39 — KIMAGE_TEXT_BASE = 0xffffffc008000000

Status

Stage
Compact waiter trigger (CMP_REQUEUE_PI → EDEADLK)works
task_struct leak (perf)works
PI write (8-byte; value = 0 or a valid kernel address)works
task+0x778 or task+0x780 alone → Uid=rootworks — but a single-field landing leaves the task divergent, and that is a latent hard BUG_ON. See the divergence hazard
Both fields written with ONE value (a consistent pair)❌ never produced with a sprayed page. Only ever observed with the global init_cred alias (09-14, CONTROL=1). The runner now enforces it (SAME_VALUE=1); not run on device
Credential laundering (setresgid + setresuid)implemented behind V12_LAUNDER=1; not run on device
kernelsu.ko loadedworks
Root process survives⚠ not established — see below
Reboot mechanism❌ not established. One candidate (the divergence) is now excluded; see below
probe_state as a landing criterion❌ wrong — do not use. Three counterexamples; see the table below
pstore/ramoops panic channel⚠ instrument exists; channel never validated (no null test yet)
"The victim spins in pure userspace"⚠ no reading yet — uid.stream now records utime/stime/nvcsw so it can be checked
pi-side single-pass dual write⚠ not established; pi.pc/pi.left are hard-coded 0 in fdset_map.h
Path A (UMH / modprobe_path)STATIC_USERMODEHELPER_PATH=""

On "root process survives": the runs in evidence/kill.log reach uid=0 and load kernelsu.ko, and in the run that actually polled for it the KernelSU manager process survived 120 s with kernelsu still Live in /proc/modules. In a later run the same chain left the Android framework services unreachable (Can't find service: package/power/input/phone/wifi) while the module was still Live. No [ROOTCHECK-*] kernel line and no $$sys_call_number@@ payload has ever been captured, so the cause of the later run's state is not attributed. See evidence/notes.md §2.3, §2.4 and §7.

Offsets

task_struct

FieldOffset
real_cred / cred0x778 / 0x780
cached syscallno0xdf8
cached uid / euid / gid / egid0xe00 / 0xe08 / 0xe10 / 0xe18

thread_info

FieldOffset
flags0x0
addr_limit0x8
ttbr00x10
preempt_count0x18

cred

FieldOffsetFieldOffset
uid0x4cap_inheritable0x28
gid0x8cap_permitted0x30
suid0xccap_effective0x38
sgid0x10cap_bset0x40
euid0x14cap_ambient0x48
egid0x18
fsuid / fsgid0x1c / 0x20

Exploit Flow

LT          perf leak target task_struct → file
W7 stage 1  task+0x778 = V    (real_cred → private sprayed page; V observed)
W7 stage 2  task+0x780 = V    (cred)   ★ V12_W7_VALUE=V — THE SAME VALUE, not a new page
W7 stage 3  V+8 = 0           (LOCAL repair of the page that was installed, ZERO shape)
LT child    fexecve(memfd of loader) — no execve of a /data path
loader      ksud late-load                  → kernelsu ... Live

Stage 2 must be given stage 1's value explicitly. Stages 1 and 2 are two independent processes, each with its own spray, so "write the cred page to both slots" is a trap: read naively it produces (pageA, pageB), and because commit_creds compares pointers, that pair is divergent even when both writes land. This is not hypothetical — it is exactly what runs 3 and 9 did:

run 9   0x778 shot  write value = 0xffffff88679bade0
        0x780 shot  write value = 0xffffff8785d6ade0     <- a different page
run 3   0x778 shot  write value = 0xffffff8787b5ade0
        0x780 shot  write value = 0xffffff881bad2de0     <- a different page

run_bootA.sh therefore fires stage 2 with V12_W7_VALUE=<stage 1's observed value> and refuses to fire it at all if that value cannot be recovered. HOLD must outlive stage 2, or stage 1's page is freed and reallocated and "the same value" becomes a dangling pointer. See the same-value rule.

One page per boot gets repaired. Stage 3 zeroes V+8. With two different pages, zeroing both would erase the gid/suid stamp (below) and make a divergence look like agreement, so the runner repairs only the page that was actually installed and stops if the two values disagree.

The cred page is built by payload.c: all eight id fields zero, all five capability sets full, and user / user_ns / group_info pointed at root_user / init_user_ns / init_groups. Stage 3 exists because the write's side effect always clobbers cred+8 (gid/suid) of whatever cred it installs.

On init_cred — an explicit dichotomy

Two sections here used to contradict each other ("never the global init_cred" vs "CONTROL=1 reproduces cell 2", and cell 2 is init_cred). Both statements are true of different roles:

  • Forbidden as a target. Writing the init_cred pointer makes the side effect corrupt init_cred+8 globally — init_cred is shared by every kernel thread, and Uid: 0 0 4294967176 0 is precisely that corruption. The code refuses this path unless V12_ALLOW_INIT_CRED=1 is set deliberately.
  • Retained as the only PROVEN consistent pair. The 09-14 chain that reached ksud wrote one fixed address (0xffffff802a7e0be0) to both slots, so real_cred == cred by construction — that is why it survived to execve. CONTROL=1 reproduces it. It is a control, not a configuration to build on.

perf leak: PERF_TYPE_SOFTWARE / PERF_COUNT_SW_CPU_CLOCK, PERF_SAMPLE_REGS_INTR, exclude_user=1. Accept [0xffffff8400000000, 0xffffff90000000), votes ≥ 15%.

The write primitive, and its side effect

The UAF is driven through rb_erase_cached Case 1-left. That gives two stores, not one:

*(write_target)          = write_value      // the store you aim
*(write_value + 0x08)    = write_target     // unavoidable side effect

write_value must be 8-byte aligned with bit 0 clear — it is either 0 or a valid kernel address. This is why g_boot_state cannot be set with this primitive: the byte you need to become 1 has its low bit forced to 0 by the alignment requirement, and write_value is the same quantity as the address the side effect lands at.

The side effect writes into whatever write_value points at

Download Tool