Cracking Honeywell's Front Door With a 32-Bit Hash
Summary
Honeywell CT47 Android devices authorize APK installations through the AutoInstall feature by comparing one hardcoded 32-bit number — 0xE7648920 — against the Java hash of the installer's signing certificate. That hash is Arrays.hashCode() over the raw DER bytes: a linear recurrence with 32-bit overflow. Linear, small, and fully invertible. We built a self-signed certificate whose Signature.hashCode() collides with Honeywell's hardcoded value, which means arbitrary APK installation — and the service's permission-grant path — without ever touching Honeywell's real signing key.
Affected component
| Field | Value |
|---|---|
| Package | com.honeywell.systemtools.autoinstall |
| Class | CheckSignedApp$1 |
| Method | verifyApp() |
| Target hash | 0xE7648920 (smali: const v5, -0x189b76e0) |
The check, in smali:
.method private verifyApp()Z
...
invoke-virtual {v4}, Landroid/content/pm/Signature;->hashCode()I
move-result v4
const v5, -0x189b76e0
if-ne v4, v5, :cond_0
const/4 p0, 0x1
return p0
One method. One comparison. Thirty-two bits of "trust."
The hash, honestly
Signature.hashCode() delegates to java.util.Arrays.hashCode(byte[]) over the certificate's DER encoding:
result = 1
for each byte b:
result = 31 * result + (b as signed byte) // 32-bit signed overflow
This is a linear recurrence over the integers, computed with wraparound at 2³². For a suffix of m bytes appended to a prefix whose hash is H_prefix:
H_full = 31^m · H_prefix + 31^(m-1)·b0 + 31^(m-2)·b1 + ... + b(m-1) (mod 2^32)
Thirty-two bits of output. A birthday collision needs ~65k guesses — and we don't even need guessing, because the recurrence is invertible.
Collision construction
Strategy. Generate a self-signed X.509 certificate with a 1024-bit RSA key. Compute the hash of the certificate prefix (every byte except the last m bytes of the signature field, which land at the tail of the DER). Solve for suffix bytes that drag the full hash to the target. Overwrite those m bytes in the DER.
Base-31 digit extraction. The suffix equation is a base-31 representation in disguise. Setting S = TARGET − 31·H_prefix, we need:
S = b0·31^(m-1) + b1·31^(m-2) + ... + b(m-1)
Since 31 is invertible modulo 2³² (31·3186588639 = 23·2³² + 1, so 31⁻¹ ≡ 3186588639), and our byte range [−128, 127] covers the digit range [0, 30] many times over, the digits fall out deterministically from the least significant end:
d_i = current mod 31 (always representable in [-128, 127]) current = (current - d_i) · 31⁻¹ (mod 2^32)
With m = 12 this terminates in 12 iterations, every time — 31¹² ≈ 7.9×10²⁹ against a 2³² target means oceans of slack. No brute force. No search. The collision is computed, not found.
Result
| Field | Value |
|---|---|
| Serial | 1 |
| Suffix bytes | 00 00 00 00 00 01 fe f4 fe ff 07 0b |
| Computed hash | 0xE7648920 ✓ |
| Target hash | 0xE7648920 ✓ |
Impact
An attacker who can deliver an APK to the CT47 — through EZConfig, ADB, or the AutoInstall service itself — can now:
- Install arbitrary APKs as "signed" by Honeywell through the AutoInstall pipeline.
- Grant Android permissions through the service's
grantPermission()path, which calls the sameverifyApp(). - Skip signature verification entirely for everything that rides on this one 32-bit comparison.
Signature.equals() over the DER bytes, not hashCode(). PackageManager.checkSignatures(), sharedUserId admission, and same-signature component launches still compare full certificates — the collision does not make us a Honeywell signature holder in the framework's eyes. What it does is fool every component that gates on hashCode() — and AutoInstall is the load-bearing one: a system-context install plus permission-grant primitive, delivered by firmware the user cannot patch.Verification
python3 verify.py collision_cert.der Certificate: collision_cert.der Size: 453 bytes Hash: 0xe7648920 (-412841696 signed) Target: 0xe7648920 (-412841696 signed) Match: YES
The verifier reimplements Arrays.hashCode() exactly — signed byte values, 32-bit wrap — because Java's overflow semantics are the whole ballgame here. A solver implementing the base-31 extraction above round-trips on 200/200 random prefix/target pairs.
Why this matters more than it looks
Thirty-two bits is not a "weakness" here — it's the entire design. The firmware takes a cryptographic identity, compresses it into an int, and compares against a constant burned into the DEX. Everything that flows from that comparison — installs, permission grants — inherits the 32-bit security level. And because the check lives in a system component, the compromise is persistent and pre-authentication: it doesn't need the user to do anything wrong, and it doesn't go away when the app is closed.
Chained with the companion write-up on the WWAN engineering mode — now fully confirmed live on hardware, every launcher and component export verified — the picture completes: install a privileged app via AutoInstall, drive the exported secret-code receiver or launch the engineering UI directly, flip the RRC toggles, enable 1 ms auto-answer, rewrite the SMSC. Each finding stands alone. Together they're a quiet handset that answers to a fake tower and a spoofed caller — built entirely from components the firmware ships on purpose.
| Timeline |
|---|
| 2026-07-24 — initial discovery via smali analysis |
| 2026-07-24 — mathematical solver implemented (base-31 digit extraction) |
| 2026-07-24 — collision verified against target hash |
frankSx · references: Honeywell CT47 AutoInstall service (com.honeywell.systemtools.autoinstall); java.util.Arrays.hashCode(byte[]); Android Signature.hashCode().
Comments
Post a Comment