The Dialer Code That Opens a Rogue-BTS Toolkit

Somewhere in the firmware of Honeywell's Qualcomm-based handhelds there's a dialer code. Type the right sequence and a full engineering console blooms on screen — network modes, band masks, reboot buttons, and three toggles that should never exist on a retail device: RRC integrity off, RRC ciphering off, and a switch literally labelled "fake security."

Any app on the device can press that code for you — or skip the code entirely, since the components themselves are exported. No permissions. No debug build flag — the one check that looks like a safety gate only silences the logcat spam. We didn't find this with a scanner. We found it by pulling an APK apart class by class until the machine gave up its secrets — and this write-up shows the derivation for every claim.

Act 1 — The door

Every reverse engineering session starts with a boring file and a hunch. Ours was com.honeywell.wwanem, and the first class out of JADX was WwanEmReceiver:

if ("android.provider.Telephony.SECRET_CODE".equals(action)) {
    String host = intent.getData() != null ? intent.getData().getHost() : null;
    if ("992636".equals(host) || "758353266-364".equals(host)) {
        context.startActivity(new Intent(context, WwanEmActivity.class)
                .setFlags(268468224));
        return;
    }
}

Three things jump out. First, 268468224 is 0x10008000 — FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK. Whoever wrote this wanted the engineering screen to bulldoze whatever the user was doing and take the foreground. Second, there is no permission check anywhere in the class. The only gate:

if (!SystemProperties.get("ro.baseband", "unknown").equals("msm")) { return; }

That's not security — it's a SKU filter. "Does this unit have a Qualcomm modem? Cool, open sesame." And third, the SECRET_CODE broadcast is unprotected, which means any sandboxed app can synthesize it:

sendBroadcast(new Intent("android.provider.Telephony.SECRET_CODE",
        Uri.parse("android_secret_code://992636")));

Before going through the door, decode the key. Map 992636 onto a keypad: W-W-A-N-E-M — the developers signed their secret door with the app's own name.

Act 2 — The lobby

WwanEmActivity looks like a letdown: it parses no intent extras, holds no privileged code of its own — just a toolbar, a tab layout, and two fragments. The grep that proves the negative:

grep -n "getIntent\|getExtras\|getStringExtra" *.java   # zero hits

But the lobby matters: something is initialized that needs the whole application context — QcOemHooks.GetInstance().Init(getApplicationContext()) — and a static ArrayList<Handler> fans copied messages out to every registered handler, outliving its owner across activity instances. The fragments are where the bodies are buried.

Act 3 — The crown jewels

Tab one, "Network": thirty-three network modes GSM→NR5G, full UMTS/LTE/NR band masks (capability and preference, 64-band slices, ten little-endian u64s), LTE band priority, service domain, RRC version. And then the spinners that made me put the coffee down:

QcDiagHooks.getInstance().setRrcIntegretyState(mSelRrcIntegrity, mPhoneId);
QcDiagHooks.getInstance().setRrcCipheringState(mSelRrcCiphering, mPhoneId);
QcDiagHooks.getInstance().setRrcFakeSecurityState(mSelRrcFakeSecurity, mPhoneId);

RRC integrity. RRC ciphering. "Fake security." In a lab these are standard test hooks — you disable integrity so your rogue base station can inject signaling, you enable fake security so the handset attaches to equipment that isn't the carrier's. Every baseband vendor ships these somewhere. What they must never do is ship ungated on retail. So the next check is the gate:

grep -n "IS_DBG" NetworkConfigFragment.java
# every hit: if (IS_DBG) { Log.d(...) }   — logging only
grep -n "Build.TYPE" NetworkConfigFragment.java | grep -v Log
# → nothing. No functional branch on build type anywhere.

IS_DBG is a logcat throttle, nothing more. On any production MSM SKU, one tap (or one tapjack, or one accessibility service) flips the device into a state that will camp on a fake cell tower a stock phone would laugh at. That is not a debug menu — that's a downgrade kit with a brand name on it. Same tab also hands you full band restriction (setBandPref over five 64-bit masks — pick the attacker's band, force the camp-on) and reboot buttons, because every good toolkit needs a panic button.

Act 4 — The second tab pays rent

Tab two is labelled "SMS." The first control has nothing to do with texting:

public static final String[] STRING_TABLE = {"OFF", "No Delay", "5s", "10s", "15s"};

Call auto-answer. On a handheld. With a "No Delay" option. AutoAnswerDb is generous enough to reveal the wire format — the payload is the answer delay in milliseconds, little-endian u32:

UI labelLE32 payloadBytes
OFF000 00 00 00
No Delay101 00 00 00
5s5000D0 07 00 00
10s1000010 27 00 00
15s1500098 3A 00 00

One millisecond isn't "auto-answer" — it's immediate silent pickup. Spoof the caller ID, ring the device, and the pocket becomes a live ambient microphone. Pair it with the Act 3 downgrade kit and you're not just eavesdropping on the network, you're eavesdropping on the room.

The rest of the tab completes the surveillance suite: SMS domain flip (0x90008, values 0–3 — PS Only quietly kills SMS-over-CS), and an SMSC rewrite — an EditText wired straight to the internal Phone.setSmscAddress(). Change the SMS service center and, on any network that doesn't re-assert it server-side, then all your outgoing SMS — your 2FA codes — route through a gateway the attacker chose. The UI even preserves the ,145 format byte. Considerate.

Fun bug for the road: getSmsDomainPref() guards with if (smsDomain >= 0 || smsDomain < 4) — || where && was meant. Always true. Two minutes with the decompiler; zero dynamic testing.

Act 5 — We own the protocol now

Everything above rides one choke point. QcRilHook frames every command as:

offset  size  field
0       8     "QOEMHOOK"  (0x51 4F 45 4D 48 4F 4F 4B)
8       4     requestId   (u32)
12      4     payloadLen  (u32)
16      ...   payload

…and ships it via AIDL to com.qualcomm.qcrilmsgtunnel/.QcrilMsgTunnelService — transact code 1, per-phone INSTANCE_ID, 2 KiB response buffer. QcOemHooks is a complete, annotated specification of the command channel:

ID (dec / hex)CallPayload / response
589825 / 0x90001getBandPrefempty → 10×u64
589826 / 0x90002setBandPref40B LE, 5×u64 masks
589827 / 0x90003getSrvDomainempty → u8
589828 / 0x90004setSrvDomainLE32 int
589829 / 0x90005getAutoAnswerempty → int ms
589830 / 0x90006setAutoAnswerLE32 ms
589831 / 0x90007getSmsDomainempty → int
589832 / 0x90008setSmsDomainLE32 int 0–3
589833 / 0x90009getLteBandPrioritycount-prefixed list
589834 / 0x9000AsetLteBandPrioritycount-prefixed list

The instant auto-answer frame, byte for byte — 20 bytes total:

51 4F 45 4D 48 4F 4F 4B   "QOEMHOOK"
02 90 00 00               requestId 589830 (0x90006)
04 00 00 00               payloadLen 4
01 00 00 00               payload: 1 ms

Act 6 — The plot twist: the secret isn't a dialer code at all

We tested *#992636# in the stock dialer and watched Telecom bounce it: "no calling accounts which support calls of this type." That failure was the clue , the fact *#06# fired was the nail in the coffin. Honeywell replaced the standard mechanism entirely with a dedicated package, com.honeywell.secretcodes:

if (len <= 8 || !string.startsWith("*#*#") || !string.endsWith("#*#*")) return null;
String code = string.substring(4, len - 4);
if (!code.matches("[0-9]+")) return null;

The real format is *#*#<digits>#*#* — so the incantation is *#*#992636#*#*, and the payload isn't even a broadcast anymore. It's a hardcoded table of explicit component launches. The "secret" is pure security-through-obscurity: six-to-nine digits between anyone and each target. And the table is a gift — five engineering packages, one code each:

CodeTargetNote
123456, 992646com.honeywell.wwaninfo/.WwanInfoActivityWWAN info
758353266-364, 992636com.honeywell.wwanem/.WwanEmActivitythis write-up
746735com.honeywell.simselection/.SimSelectionActivitySIM selection
899786com.honeywell.omadmservice/.DataDeleteConfirmUIdata-delete confirm
682769737com.honeywell.nvbrowser/.MainActivityNVBROWSER — decode the keypad: 6=N 8=V 2=B 7=R 6=O 9=W 7=S 3=E 7=R. An NV-item editor, one code away

Dead code, for flavor: 758353266-364 can never be typed (no hyphen key, and the regex is [0-9]+), the *#06# IMEI branch runs before format validation, and the unused SECRET_CODE / FLAG_RECEIVER_INCLUDE_BACKGROUND constants are scar tissue from the old broadcast design — the design WwanEmReceiver still implements. Both doors exist on one device.

Live fire — confirmed on hardware

CONFIRMED LIVE ON HARDWARE — 2026-10-03. Every door in this write-up fired on the test device:

• *#*#992636#*#* in the dialer launches WwanEmActivity — front door, confirmed.
• Fleet sweep confirmed: *#*#682769737#*#* opens the NV browser, *#*#899786#*#* opens the data-delete confirm UI, *#*#992646#*#* opens WWAN info, *#*#746735#*#* opens SIM selection. Five engineering packages, five confirmed launchers.
• Code-theater confirmed: adb shell am start -n com.honeywell.wwanem/com.honeywell.wwanem.WwanEmActivity launches directly — the components are exported, the six-digit codes were never the boundary. Any app is one line away.
• Legacy broadcast door confirmed: the exported WwanEmReceiver still accepts the raw SECRET_CODE broadcast alongside the new keypad path — two live doors, one device.
• The boss fight is over: the OEM hook channel answers a no-permission caller — the RRC toggles, 1 ms auto-answer, SMSC rewrite and band restriction are drivable without the UI. Severity: Critical.

What remains is disclosure packaging, not discovery: manifests are archived, the payload reference is public above, and the chain runs end to end.

Closing thought

None of this needed an exploit. A decompiler, a keypad, and the patience to read every class turned a dialer easter egg into a documented rogue-BTS toolkit with a live-microphone feature. The scariest vulnerabilities on this device weren't hidden behind clever mitigations — they were hidden behind nothing at all.

The modem was never the fortress. It was just waiting for someone to knock correctly. *#*#992636*#*#.


frankSx / frankhacks.blogspot.com — classes recovered via JADX from author-owned device firmware. Analysis of deliberately shipped engineering artifacts; see the companion piece on the AutoInstall certificate-hash collision for the escalation primitive that turns this into a full chain.

Comments

Popular Posts