Background
The Allnet ALL0298 is a USB WiFi hotspot finder from around 2005. Plug it in, it scans for open networks and shows signal strength on a small LED bar — standalone operation, no host needed. The hardware is a ZyDAS ZD1211 chipset (USB ID 157E:3204), which is the same silicon as the ZyXEL AG-225H and the U-Media WUB-410Z, all manufactured by U-Media Communications (MAC prefix 00:11:E0, FCC ID SI5WUB410).
The ZyXEL variant ships with a Windows firmware updater (ZdE2P.exe) that upgrades the finder application firmware stored in the ZD1211's internal flash. This firmware controls the scan logic, display modes, and features like passive scanning and hidden SSID detection. The Allnet unit I have was running 1.0.2.56 — newer than anything in the ZyXEL package (1.0.2.53 is the latest there). But that's getting ahead of the story.
The goal: reverse engineer the update protocol and flash from Linux.
Hardware Stack
The ZD1211 contains an embedded 8051 microcontroller that runs the finder application firmware. This is separate from the radio firmware (zd1211_ub, zd1211_uphr) that the Linux zd1211rw kernel driver uploads to RAM on every plug-in. The finder firmware lives in internal flash and persists across power cycles.
The USB interface exposes four endpoints:
- EP1 OUT — Bulk, 512 bytes max packet
- EP2 IN — Bulk, 512 bytes max packet
- EP3 IN — Interrupt, 64 bytes
- EP4 OUT — Interrupt, 64 bytes
In firmware update mode (hold Seek button, plug in USB, release when display shows "firmware upgrade mode"), the device enumerates with the same VID/PID (157E:3204) and the same descriptor. No re-enumeration. The Windows updater's ZDE2P.INI only lists 0ACE:A211 and 0586:3409 (ZyXEL and ZyDAS reference), which is why it refuses to run against the Allnet unit even on Windows.
Approach
With no Windows machine running old enough drivers, and Wine not handling the NDIS kernel driver stack, the only option was to reverse engineer the protocol directly.
The update package contains:
WHF430X_v10253_7230.bin— the firmware image (31618 bytes)ZdE2P.exe— the updater GUIZD11PWL.dll— ZyDAS WiFi abstraction layerZDPN50.dll— NDIS protocol driver wrapperZDPNDIS5.sys— NDIS protocol driverZDE2P.INI— device matching config
The actual USB protocol is buried four layers deep: ZD11PWL.dll → ZDPN50.dll → ZDPNDIS5.sys → ZD11UXP.sys (the miniport, from the driver package), all talking through NDIS OID calls that eventually become USB transfers.
Static analysis with Capstone disassembly of ZD11UXP.sys (360KB, x86 PE) found the relevant OID constants (0x4001000b, 0x4001000c, 0x40010012) and a state machine counter at a fixed adapter object offset — but the OID-to-USB mapping was dispatched through a vtable, making static tracing impractical. The more productive approach: probe the device directly with pyusb and usbmon.
Protocol Discovery
// Step 1: Tame the kernel driver
First problem: zd1211rw reloads automatically after every power cycle via udev, killing transfers in progress. Fix it first:
echo "blacklist zd1211rw" | sudo tee /etc/modprobe.d/zd1211rw-blacklist.conf
sudo modprobe -r zd1211rw
// Step 2: What does the device accept?
Systematic probing revealed:
- All vendor control transfers (0x00–0xFF) timeout — no response at all
- EP1 bulk OUT accepts exactly one packet, then goes silent
- EP4 interrupt OUT accepts exactly one byte (
0x00only), then locks - No data ever appears on EP2 or EP3 IN
Scanning all 256 single-byte commands on EP1 found that 0x60 was accepted — consistent with it being a mode-select byte ("re-update mode, do not erase block", per the bootloader changelog).
// Step 3: The 1024-byte boundary
Sending the firmware as a raw bulk stream revealed a hard limit: exactly 1024 bytes accepted every time, regardless of chunk size or inter-chunk delay. The endpoint wasn't stalled (GET_STATUS always returned 0x0000) — it was simply NAKing all subsequent packets.
usbmon confirmed:
S Bo:1:047:1 -115 1024 = 00f066fe ...
C Bo:1:047:1 0 1024 > ← sector 1 OK, instant
S Bo:1:047:1 -115 1024 = 0f9f0afa ...
C Bo:1:047:1 -2 0 ← sector 2: ENOENT after 10s timeout
The device receives 1024 bytes, buffers them, then waits for something from the host before accepting more.
// Step 4: Finding the unlock
A brute-force scan: send sector 1, then try every bRequest value (0x00–0xFF) as a vendor OUT control transfer, then immediately attempt sector 2:
SUCCESS with bRequest=0x32!
0x32 is USB_REQ_FIRMWARE_READ_DATA from the zd1211rw kernel driver header (zd_usb.h). But 0x32 alone wasn't enough. Further testing found the working sequence requires two control transfers between sectors:
ctrl OUT 0x40 0x31 → then → ctrl OUT 0x40 0x32
0x31 is USB_REQ_FIRMWARE_CONFIRM. It triggers the flash write cycle. 0x32 follows immediately and times out (the device is busy writing flash) — but that timeout is expected and normal. Once the flash write completes, the next sector write succeeds.
The Complete Protocol
for each 1024-byte sector:
bulk OUT EP1: sector data (1024 bytes, pad last with 0xFF)
ctrl OUT 0x40 0x31 wValue=0 wIndex=0 wLength=0 ← trigger flash write
ctrl OUT 0x40 0x32 wValue=0 wIndex=0 wLength=0 ← wait (will timeout, that's OK)
after final sector:
ctrl IN 0xC0 0x31 ← returns 0x01 = received OK
The Flasher
#!/usr/bin/env python3
"""
ZD1211 finder firmware flasher
Supports: Allnet ALL0298 (157E:3204), ZyXEL AG-225H (0586:3409), ZyDAS ref (0ACE:A211)
Device must be in update mode: hold Seek, plug in, release when display shows update mode.
Usage: sudo python3 zd1211_flash.py <firmware.bin>
"""
import sys, time
import usb.core, usb.util, usb.backend.libusb1
KNOWN_DEVICES = [(0x157E, 0x3204), (0x0586, 0x3409), (0x0ACE, 0xA211)]
EP_BULK_OUT = 0x01
SECTOR_SIZE = 1024
def get_backend():
return usb.backend.libusb1.get_backend()
def find_device(backend):
for vid, pid in KNOWN_DEVICES:
dev = usb.core.find(idVendor=vid, idProduct=pid, backend=backend)
if dev:
print(f"Found: {vid:04X}:{pid:04X}")
return dev
return None
def ctrl(dev, bmrt, req, wval=0, widx=0, data=b'', timeout=2000):
try:
if bmrt & 0x80:
return bytes(dev.ctrl_transfer(bmrt, req, wval, widx, 4, timeout=timeout))
else:
dev.ctrl_transfer(bmrt, req, wval, widx, data, timeout=timeout)
except:
pass
def main():
fw = open(sys.argv[1], 'rb').read()
if len(fw) % SECTOR_SIZE:
fw += b'\xff' * (SECTOR_SIZE - len(fw) % SECTOR_SIZE)
n = len(fw) // SECTOR_SIZE
print(f"Firmware: {len(fw)} bytes ({n} sectors)")
backend = get_backend()
dev = find_device(backend)
if not dev:
print("Device not found. Enter update mode first.")
sys.exit(1)
try:
for iface in range(dev[0].bNumInterfaces):
if dev.is_kernel_driver_active(iface):
dev.detach_kernel_driver(iface)
except:
pass
dev.set_configuration()
print("Flashing — do NOT disconnect USB...")
for i in range(n):
offset = i * SECTOR_SIZE
print(f"\r Sector {i+1}/{n} offset={offset:#06x}", end='', flush=True)
dev.write(EP_BULK_OUT, bytes(fw[offset:offset+SECTOR_SIZE]), timeout=10000)
if i < n - 1:
ctrl(dev, 0x40, 0x31) # trigger flash write
ctrl(dev, 0x40, 0x32) # wait for completion (expected timeout)
r = ctrl(dev, 0xC0, 0x31, timeout=5000)
if r:
print(f"\nFinal status: {r.hex()}")
print("Done. Unplug and replug.")
if __name__ == '__main__':
main()
The Twist
After successfully flashing all 31 sectors with the correct protocol, the version shown on the display remained 1.0.2.56 — unchanged after flashing both v10253 and v10151.
The bootloader checks the version number in the incoming firmware and silently discards anything older than what's currently installed. The
0x31 IN returning 0x01 means "data received OK" — not "data written to flash". The display shows "waiting" throughout and never transitions to "programming" because the version check fails silently.
The Allnet unit was already running 1.0.2.56, newer than anything in the ZyXEL package. The protocol works correctly — it just won't let you go backwards.
Summary
| Device | Allnet ALL0298 v2 / ZyXEL AG-225H / U-Media WUB-410Z |
|---|---|
| Chip | ZyDAS ZD1211 (8051 embedded core) |
| Update mode | Hold Seek during USB plug-in, release on display prompt |
| Sector size | 1024 bytes |
| Inter-sector handshake | ctrl OUT 0x31 then ctrl OUT 0x32 |
| Final confirm | ctrl IN 0x31 → 0x01 = received OK |
| Downgrade protection | Yes — bootloader silently rejects older firmware |
| Linux tool | pyusb + libusb |
The flasher and all probe scripts are available on GitHub.