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 label | LE32 payload | Bytes |
|---|---|---|
| OFF | 0 | 00 00 00 00 |
| No Delay | 1 | 01 00 00 00 |
| 5s | 5000 | D0 07 00 00 |
| 10s | 10000 | 10 27 00 00 |
| 15s | 15000 | 98 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) | Call | Payload / response |
|---|---|---|
| 589825 / 0x90001 | getBandPref | empty → 10×u64 |
| 589826 / 0x90002 | setBandPref | 40B LE, 5×u64 masks |
| 589827 / 0x90003 | getSrvDomain | empty → u8 |
| 589828 / 0x90004 | setSrvDomain | LE32 int |
| 589829 / 0x90005 | getAutoAnswer | empty → int ms |
| 589830 / 0x90006 | setAutoAnswer | LE32 ms |
| 589831 / 0x90007 | getSmsDomain | empty → int |
| 589832 / 0x90008 | setSmsDomain | LE32 int 0–3 |
| 589833 / 0x90009 | getLteBandPriority | count-prefixed list |
| 589834 / 0x9000A | setLteBandPriority | count-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:
| Code | Target | Note |
|---|---|---|
| 123456, 992646 | com.honeywell.wwaninfo/.WwanInfoActivity | WWAN info |
| 758353266-364, 992636 | com.honeywell.wwanem/.WwanEmActivity | this write-up |
| 746735 | com.honeywell.simselection/.SimSelectionActivity | SIM selection |
| 899786 | com.honeywell.omadmservice/.DataDeleteConfirmUI | data-delete confirm |
| 682769737 | com.honeywell.nvbrowser/.MainActivity | NVBROWSER — 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
• *#*#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
Post a Comment