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

FieldValue
Packagecom.honeywell.systemtools.autoinstall
ClassCheckSignedApp$1
MethodverifyApp()
Target hash0xE7648920 (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

FieldValue
Serial1
Suffix bytes00 00 00 00 00 01 fe f4 fe ff 07 0b
Computed hash0xE7648920 ✓
Target hash0xE7648920 ✓

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 same verifyApp().
  • Skip signature verification entirely for everything that rides on this one 32-bit comparison.
Stay precise: framework signature identity is 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

Popular Posts