12.7. APK sweep: bulk decompile of the remaining 123 system/product apps¶
Preinstalled apps (Boox, GMS, AOSP) catalogues the 124 preinstalled APKs by partition
without decompiling them; Userspace boot — init, SELinux, A/B updates decompiled exactly one,
OnyxOtaService.apk, and found a hardcoded AES key protecting (weakly)
its OTA-payload decryption step. This page covers the other 123: every
remaining APK under system, product and system_ext was bulk
decompiled with jadx and grepped for anything bootloader/security
relevant. The result mostly confirms stock AOSP/GMS/Qualcomm code and one
large shared Onyx SDK copy-pasted into every Onyx-branded app, plus a
small number of concrete, Onyx-custom findings detailed below — none of
them change the trust picture in Userspace boot — init, SELinux, A/B updates or Security and DRM userspace,
but two are genuine exported-unguarded-component bugs of the same class
as the OnyxOtaService finding, just lower-impact.
12.7.1. Method and coverage¶
All 123 non-OnyxOtaService APKs found under artifacts/system_a,
artifacts/product_a and artifacts/system_ext_a were decompiled with
jadx 1.5.1 (--show-bad-code, 90 s timeout per APK) into
artifacts/re_apk/<AppName>/:
75 decompiled cleanly (exit 0).
47 exited non-zero (jadx’s own “finished with errors, count: N” — it still emits sources for every class it could decompile; only a minority of individual methods/classes inside these APKs came out as bad-code stubs). Spot-checked several of these (
NetworkStack,knote2-release) and confirmed full, usable source trees underneath — this is normal jadx behaviour on large, ProGuard/R8-shrunk, Kotlin-heavy real-world APKs, not a sign the APK is unusual.1 (
GmsCore.apk, 119 K decompiled files) hit the 90 s timeout before finishing but had already written the bulk of its sources; treated as partial-but-usable, not retried.20 of the 124 produced only
R.java(no real code): these are resource-only APKs — fiveDisplayCutoutEmulation*RROs, three more RRO overlays (NavigationBarMode3Button,NavigationBarModeGestural,TransparentNavigationBar), the two*__tablet_nosdcard__auto_ generated_characteristics_rroframework RROs,FontNotoSerifSource Overlay,GmsConfigOverlayMdm,FrameworksResCommon_Sys,WifiResCommon_Sys,WigigTetheringRRO,framework-res.apkitself,ModuleMetadata(GMS module-version metadata),dcf(Qualcomm DRM-content-format resource shim),NotesRoleEnabledOverlay(packagecom.android.role.notes.enabled, a role-grant RRO) andchromium_TrichromeLibrary(the shared native WebView/Chrome library APK — its code lives in the native.so, not in classes.dex). None of these had anything to grep.
Total: 124/124 attempted APKs produced a usable decompiled source tree
under artifacts/re_apk/ (left in place, not deleted). 711,698 .java
files across all trees, most of it duplicated third-party/AOSP/GMS/framework
boilerplate — see below for why a raw grep count is meaningless without
filtering.
12.7.3. Grep sweep: false positives worth naming (so they don’t get re-flagged)¶
“BOOTLOADER” in Apache Commons Logging (
org/apache/commons/logging/ LogFactory.java, present in ~15 apps via a bundled logging dep): this is the library’s own Java-ClassLoader-hierarchy diagnostic string ("BOOTLOADER"/"SYSTEM"/"CONTEXT"describing which classloader tier loaded a class) — nothing to do with the Android bootloader.``Build.BOOTLOADER`` reads (
igetshop-release’s bundledEasyDeviceModdevice-info library, and an iFlytek SDK property-name table): standard AndroidBuild.BOOTLOADERdevice-info string reporting, used the same way in thousands of apps for crash-report device fingerprints.``avb``/``avc``/``avg`` etc. as bare identifiers: these are R8/ProGuard short obfuscated class names inside
GmsCore,Phonesky,AndroidPlatformServicesandGoogleRestore(packagedefpackage, classes namedava.java,avb.java,avc.java…) — coincidental 3-letter collisions with “AVB”, unrelated to Android Verified Boot.``ShellUtils.COMMAND_SH`` inside ``Caverphone1``/``Caverphone2`` (phonetic-matching algorithms in Apache Commons Codec, bundled in most Onyx apps): an R8 constant-pooling artifact — the literal string
"sh"happens to be shared between the phonetic algorithm’s own.replaceAll("sh", ...)call and the unrelatedShellUtils.COMMAND_SHconstant, so jadx’s static-import resolution prints the SDK’s constant name in a file that has nothing to do with shelling out.``—–BEGIN … PRIVATE KEY—–`` blocks: every hit resolves to a well-known public library test fixture — Google API Client’s
TestCertificates.java(a published, documented test key shipped withgoogle-api-java-clientupstream) and Netty’sPemPrivateKey/SelfSignedCertificateself-signed-cert-generation utility code (Netty generates these at runtime for TLS testing; the “key” in source is generation code, not a fixed secret). None are device- or Onyx-specific.No
com/onyx/*source file anywhere in the sweep contains a-----BEGINblock of any kind.
12.7.4. Real findings¶
12.7.4.1. Exported, unguarded content provider in the Boox shell app (kcb-release)¶
kcb-release (package com.onyx, application class
com.onyx.ContentBrowserApplication, main activity
com.onyx.tablet.main.ui.TabletMainActivity) is not the minor component
Preinstalled apps (Boox, GMS, AOSP) guesses at (“a Boox component”, name-inferred only) —
it is the home-screen / content-browser shell app. Its manifest declares:
<provider
android:name="com.onyx.android.sdk.data.db.OnyxSystemContentProvider_Provider"
android:exported="true"
android:authorities="com.onyx.system.database.ContentProvider"/>
No android:permission, and no other guard. This is a DBFlow-generated ContentProvider
(OnyxSystemContentProvider_Provider.java) that implements full
query/insert/update/delete/bulkInsert directly against a
SQLite table (SystemConfigDatabase → SystemKeyValueItem, a generic
(key, value) store) with the caller-supplied selection string passed
straight through to SQLiteDatabase.query/update/delete. Since the
provider is exported with no permission, any app installed on the device
can read, insert, update or delete rows in this table via
`content://com.onyx.system.database.ContentProvider/SystemKeyValueItem`
without any permission grant. One confirmed key stored there,
"sys.app_preference" (JSON blob, written/read by
AppPreferenceSaveRequest/AppPreferenceLoadRequest in the same SDK),
holds the shell app’s own preferences — so the concrete impact found is
local tamper/denial-of-service against the Boox shell’s preferences, not
secret disclosure: no credentials or tokens were found stored under this
authority in static analysis, though only this one class’s reads/writes were
traced, not the full key/value space the provider exposes. This is the same
shape of bug as OnyxOtaService
(exported + unguarded + Onyx-custom) but lower severity, since the reachable
data is app-preference key/value pairs rather than a flashable OTA payload.
Only kcb-release registers this provider in its manifest — the SDK class
is compiled into the other ~19 Onyx apps too, but the <provider> entry
itself only appears in kcb-release’s merged manifest, so only that app’s
process actually exposes the authority.
The same manifest declares a second exported, unguarded provider,
com.onyx.android.sdk.scribble.data.contentprovider.NoteDataStatisticsDatabaseContentProvider
(authority com.onyx.android.sdk.note.statistics.ContentProvider.cb, no
android:permission). Its implementation is the same shape as the one
above — full query/insert/update/delete/bulkInsert against
a single table, here a Room database (NoteStatisticsRoomDatabaseManager)
holding a NoteStatisticsModel table — so any local app can also read or
write the device’s note-taking usage-statistics rows with no permission grant.
Lower severity again (usage-stats data, not credentials or a flashable
payload), but it is a second, independent instance of the exact same
exported-unguarded-DBFlow/Room-provider pattern in the same APK, which makes
this look like a house convention in the Onyx SDK rather than a one-off
oversight.
The same provider class turns up a third time, in knote2-release
(package com.onyx.android.note, Onyx’s flagship note-taking app — not a
companion/shell app). Its manifest declares
com.onyx.android.sdk.scribble.data.contentprovider.NoteDataStatisticsDatabaseContentProvider
exported, with no android:permission, under a different authority,
com.onyx.android.sdk.note.statistics.ContentProvider (no .cb suffix).
Same bug, same class, same lack of a guard — but reachable via the main note
app rather than the shell, which arguably makes this the more impactful of
the two instances.
12.7.4.2. Exported, unguarded download-trigger receiver/service in the App Market¶
app-market-release (package com.onyx.appmarket, the Boox app store)
declares two exported, unguarded components:
<receiver android:name="com.onyx.appmarket.receiver.PreInstallAppReceiver"
android:exported="true">
<intent-filter>
<action android:name="com.onyx.intent.action.DOWNLOAD_APP"/>
</intent-filter>
</receiver>
<service android:name="com.onyx.appmarket.service.AppCheckUpdateService"
android:exported="true">
<intent-filter>
<action android:name="com.onyx.appmarket_ACTION_APP_CHECK_UPDATE"/>
</intent-filter>
</service>
PreInstallAppReceiver.onReceive() reads a caller-supplied string extra
(args_app_package_name) straight off the intent, uses it to fetch that
package’s listing from the Onyx App Market backend
(MarketAppFetchAction), and if found, immediately kicks off
DownloadAppManagerAction — which acquires a wake lock, downloads the APK
over the network to local storage, MD5-checks it against the store’s
metadata, and marks it AppStatus.INSTALL on completion. In other words:
any app on the device, with no permission, can make the Boox App Market
silently fetch and stage for install any package that exists in Onyx’s
store catalogue, by broadcasting one unguarded intent. Whether that final
step then installs silently or just prompts the user is an open question
(see Honest limits).
AppCheckUpdateService is lower-impact: its Messenger-bound handler
only starts the store’s own update-check routine
(gated by AppMarketMMKVHelper.isUpdateRemainderEnable()), which any app
could already trigger implicitly just by being present.
12.7.4.4. Factory test app (ProductionTest-release) — broad exported-activity surface, no privileged exec found¶
This is a genuinely Onyx-custom, non-generic app (package
com.onyx.android.production.test) with an unusually large permission
set (REBOOT, MASTER_CLEAR, WRITE_SECURE_SETTINGS,
MOUNT_UNMOUNT_FILESYSTEMS, …) and several activities marked
android:exported="true" with no intent-filter and no permission —
DramTestActivity, SensorTestActivity, SIMTestActivity,
EpdcTestActivity, WaveFormActivity, OTGCopyTestActivity, plus
DeviceBindingInfoActivity (exported with an intent-filter,
com.onyx.action.device.binding.INFO). Any local app could launch these
by component name — but they are all factory hardware-test UI screens
(DRAM stress test, sensor readout, SIM diagnostics, e-ink waveform test),
not shell/root primitives. The app does call Runtime.getRuntime().exec()
and ShellUtils.execCommand() in a few places
(CTPCalibration: setprop ctl.start <service>; StorageUtils:
df/cat /proc/partitions), but every argument traced back to a
hardcoded string or a local file path, never to intent/network input — no
command-injection or unguarded-root path was found. ResetFactoryActivity
/ ResetFactoryMainActivity (the MASTER_CLEAR-adjacent screens) are
not exported (no android:exported and no intent-filter → defaults to
non-exported even at this app’s targetSdkVersion=29), so they are not
reachable from other apps. Net assessment: broad attack surface on paper
(permissions, exported test activities) but nothing that resolves to a
usable privilege-escalation or bootloader-relevant primitive was found.
12.7.4.5. A dedicated touch-calibration app that shells out to sysfs (tscalibration-release)¶
CalibrationManager.doExec() wraps a bare Runtime.getRuntime().exec(str)
(no shell, so no metacharacter injection is possible — Runtime.exec(String)
tokenizes on whitespace and never invokes /bin/sh), called only as
doExec("touch " + str) and doExec("rm " + CALIBRATE_FILE), where
str/CALIBRATE_FILE are hardcoded device calibration-node paths (e.g.
Wacom touch-raw-data nodes), not attacker- or network-supplied. This is a
system app creating/removing files it has permission to touch on sysfs/procfs
calibration nodes — normal factory-calibration behaviour, not a finding.
12.7.5. Confirmed present, unmodified: the stock AOSP OEM-unlock toggle¶
artifacts/re_apk/Settings (system_ext’s Settings.apk,
com.android.settings) contains OemUnlockPreferenceController.java,
byte-for-byte consistent with upstream AOSP: it gates the Developer-Options
“OEM unlocking” switch on SystemProperties.get("ro.oem_unlock_supported"),
talks to OemLockManager (isOemUnlockAllowed /
setOemUnlockAllowedByUser / isDeviceOemUnlocked), and additionally
checks carrier-lock (getAllowedCarriers) and the no_factory_reset
user restriction before allowing the toggle. No Onyx or vendor customization
was found in this controller. This doesn’t change anything in
Userspace boot — init, SELinux, A/B updates (which already establishes the device is AVB 2.0,
test-key signed, secure-boot-fuse-unfused) — it just confirms the standard
Android-side UI/permission path for OEM unlock is present and unaltered, so
the fastboot-side unlock story is governed entirely by
AVB enforcement code path (LoadImageAndAuth / libavb), not by
anything in this app layer. Also present and equally stock:
Enable16KOemUnlockDialog and EnableOemUnlockSettingWarningDialog
(both standard AOSP 16 KB-page-size / OEM-unlock warning dialogs) and
com.android.dynsystem.InstallationAsyncTask in
DynamicSystemInstallationService.apk — the stock AOSP Dynamic System
Updates (DSU/GSI) installer, which references AvbPublicKey and refuses
to DSU-install over vbmeta/boot/dtbo/super_empty/
system_other/scratch/userdata partitions
(UNSUPPORTED_PARTITIONS) — again unmodified upstream AOSP code, not an
Onyx addition.
12.7.6. Qualcomm vendor apps: names checked, nothing custom found¶
A batch of oddly-named system_ext_a/priv-app entries — Xpan,
xrcbservice, xrvdservice, xrwifiservice,
com.qualcomm.qti.services.systemhelper, DynamicDDSService,
NtnSatApp, WigigTetheringRRO, dcf, aptxui — all resolve to
stock Qualcomm reference-platform packages by their manifest package=
attribute (vendor.qti.bluetooth.xpan, com.qualcomm.qti.xrcb,
com.qualcomm.qti.xrvd.service, com.qualcomm.qti.xrwifi,
com.qualcomm.qti.dynamicddsservice, vendor.qti.data.ntnsatapp,
com.qualcomm.qti.server.wigig.tethering.rro, com.qti.dcf,
com.qualcomm.qtil.aptxui). These are carried over unmodified from the
Qualcomm SM6350 BSP for hardware this device mostly doesn’t use (XR
controller-bridge, wigig tethering, satellite non-terrestrial-network data,
aptX UI) — normal for a QCOM reference-platform-derived build, not
Onyx-specific, and none showed anything relevant in the grep sweep beyond
what the platform BSP apps generically contain.
12.7.7. What was checked and came up empty¶
No other exported, unguarded
<service>/<receiver>/<provider>withota/update/recovery/factory/debug/admin/root/backdoorin its name was found across all 124 manifests beyondOnyxOtaService(already documented) and the two above (kcb-release’s content provider,app-market-release’s receiver/ service). The many other exported-unguarded hits from the grep sweep (ConfigUpdater’s dozen receivers, GMS’s Chimera services, Phonesky’s install services, SystemUI’s standard framework services) are stock AOSP/ GMS components whose components rely on protected-broadcast actions or Chimera’s own internal caller checks — normal for these subsystems, not flagged further.No
vbmeta/avb/fastboot/dm-verity/slot_suffixstring outside of the two legitimate AOSP hits already covered (OemUnlockPreferenceController,DynamicSystemInstallationService).No embedded private key, certificate, or high-entropy secret specific to this device or to Onyx was found beyond the one dead AES key constant above and the already-documented
OnyxOtaServicekey (Userspace boot — init, SELinux, A/B updates).No
su/root-escalation path reachable from network or IPC input; the onlysu-capable code (ShellUtils.checkRootPermission()/execCommand(..., true, ...)) is the shared SDK’s generic root-detection helper — grepping every call site across all 20 Onyx apps found no caller that actually passessu=true; every real call in this sweep uses the non-sushpath with hardcoded arguments.
12.7.8. Honest limits¶
The final step of
PreInstallAppReceiver’s download flow — whetherAppStatus.INSTALLleads to an automaticPackageInstallersession or only a user-facing “tap to install” prompt — was not traced pastDownloadAppManagerAction. This matters for severity (silent local install-of-attacker’s-choice vs. silent download + still requires a user tap). Similarly, whether anything actually callsAESEncryptUtils.encrypt/decryptwith the statickeyfield reflectively, dynamically, or from a class this static sweep’s call-site grep missed, was not exhaustively proven — “no static call site found” is not the same as “provably unreachable.”47 of the 123 APKs decompiled with some internal jadx errors (partial bad-code on a minority of classes within each, above). The grep sweep still covers whatever jadx did emit for those classes; anything jadx failed to render at all (rare, and typically synthetic/obfuscated inner classes in heavily R8-shrunk apps like GMS/Phonesky) is not covered, including
GmsCore.apk’s un-decompiled tail (above).Given the volume (124 apps, ~712K decompiled files), the grep sweep is necessarily keyword-driven rather than an exhaustive line-by-line read; the “what was checked and came up empty” section lists what was specifically searched for, not a claim that every method in every app was read.
The manifest regex pass covers every exported, unguarded service/receiver/provider by definition — that part of the sweep is complete. What it doesn’t cover is anything that isn’t an exported component: unsafe deserialization, path traversal, SSRF-shaped backend calls, logic bugs in non-exported code reachable only through a chain of intents, and so on. Those bug classes were never searched for here, so “came up empty” above means “nothing of the searched-for shape was found,” not “these 124 apps have no other issues.”
12.7.9. Provenance¶
- Source:
all APKs under
artifacts/system_a,artifacts/product_aandartifacts/system_ext_a(find ... -iname '*.apk', 123 files plus the already-decompiledOnyxOtaService.apk), decompiled withtools/jadx/bin/jadx1.5.1 (--show-bad-code, 90 s timeout per APK via a manual bash watchdog — thetimeout/gtimeoutcoreutils binary is not present on a stock macOS host) intoartifacts/re_apk/<AppName>/{sources,resources}. Per-APK decompile logs kept atartifacts/re_apk/<AppName>/jadx.log.- Method:
bulk decompile via a one-off shell loop (not checked in); grep sweep across every
artifacts/re_apk/*/sourcestree for the term groups listed above (bootloader/AVB, crypto/secrets, exec/privilege, exported-component name patterns), each hit manually read in context to separate real findings from bundled-library/obfuscation-name coincidence noise (documented above); manifest analysis via a small Python/regex pass over each APK’sresources/AndroidManifest.xmlto enumerateexported="true"services/receivers/providers lackingandroid:permission. No code was executed; no device was touched.- Cross-refs:
Preinstalled apps (Boox, GMS, AOSP) (the un-decompiled full app catalogue this page follows up on — note the
kcb-releasepurpose correction above), Userspace boot — init, SELinux, A/B updates (the one APK decompiled and audited before this sweep,OnyxOtaService, and the AVB/A/B chain this sweep’s OEM-unlock finding relates to), Security and DRM userspace (keystore2/FBE context for the device’s overall trust model, unaffected by anything in this sweep).