● RE ◆ agentic Project №03 10 min read

Reverse Engineering the Alientek HP20 Reflow Hot Plate

Cracking XOR encryption, decompiling .NET updaters, and reverse engineering BLE protocols on a $100 soldering hot plate.

Ghidraradare2ILSpyBlutterPythonWireshark

The Alientek HP20 is a USB-C PD-powered reflow hot plate. It’s a 240mm heated platform meant for SMD soldering, component rework, and BGA reballing. You can find one for about $100 to $150. It has a nice round GC9A01 LCD, pairs with a companion Android app over Bluetooth, and takes firmware updates through a desktop tool over USB-HID.

What’s less standard is how locked down everything is. The firmware ships as encrypted .atk files. The desktop updater is an obfuscated .NET binary. The BLE protocol has zero public documentation. No schematics, no SDK, nothing. The kind of setup that makes you go, “alright, let’s see what’s actually in here.”

So I pulled apart every piece of software that touches this thing: the firmware, the updater, both Android apps. Here’s what I found.

The Hardware

The MCU is an HC32F460, an ARM Cortex-M4F from HDSC. 512KB flash, 192KB SRAM, runs up to 200MHz. I confirmed the chip identity through several independent signals in the firmware binary:

  • 14 embedded driver source paths (hc32_ll_driver/src/hc32_ll_*.c), which are HDSC’s official HAL driver filenames
  • Exactly 144 external IRQ entries in the vector table, a count unique to the HC32F460 among HDSC’s lineup
  • Fixed USART IRQ mappings at positions 128 to 143
  • Peripheral register addresses matching the HC32F460 reference manual

The rest of the BOM I pieced together from firmware strings, I2C addresses, and AT commands:

ComponentRoleEvidence
HC32F460Main MCUDriver paths, IRQ count, peripheral addresses
GC9A01240×240 round LCDInit sequence in firmware, driver strings
JDY-seriesBLE moduleAT command set (AT+SBNHP20 sets device name)
CH224QUSB-PD sink controllerNegotiates 5 to 32V from USB-C
INA219Current/power sensorI2C address 0x40, power measurement strings
SI2302Auxiliary MOSFETNamed in self-test, but NOT the heater (more on this later)
NTC thermistorTemperature sensing130-entry ADC lookup table in firmware

The Encryption That Wasn’t

This is the part that had me going in circles for longer than I’d like to admit.

The HP20 firmware ships as .atk files: encrypted blobs with a 13-byte plaintext header containing version info, device type, and sizes. The desktop updater (ATK_HP20.exe, 547KB) is a .NET WinForms application protected with .NET Reactor, a commercial obfuscator that renames all symbols to random strings, injects fake metadata streams, and encrypts method bodies at rest (decrypting them at runtime via a native stub).

When I ran it through ILSpy, the crypto classes jumped out immediately: AesCryptoServiceProvider, CreateDecryptor, proper key and IV setup.! The entropy analysis of the firmware file backed this up: 7.12 bits per byte, near-random distribution. And the ciphertext had clear 16-byte block alignment. Everything screamed AES.

Except the numbers didn’t add up.

I wrote a quick script to split the encrypted firmware into 16-byte blocks and count duplicates. Out of 26,618 total blocks, 2,003 were repeated. That’s a 7.5% collision rate. With AES in any proper mode (CBC, CTR, GCM) that’s mathematically impossible. Even AES-ECB wouldn’t produce this many collisions unless the underlying plaintext was extraordinarily repetitive.

Then I looked at which blocks repeated most:

Block A  (533 hits): 46 2F 71 9F  46 2F 71 9F  46 2F 71 9F  46 2F 71 9F
Block B  (173 hits): B9 D0 8E 60  B9 D0 8E 60  B9 D0 8E 60  B9 D0 8E 60

See it? Each 16-byte block is a 4-byte value repeated four times. That’s not a block cipher. That’s a 4-byte repeating XOR key producing a 16-byte pattern when it lines up with the analysis window.

2,003 duplicate blocks out of 26,618. AES doesn’t do that. A 4-byte XOR key does.

The known-plaintext attack writes itself from there. Firmware binaries always contain large regions of 0x00 bytes (zeroed initialization data, BSS) and 0xFF bytes (erased flash padding). XOR anything with zero and you get the key back directly:

0x00000000 XOR key = key         →  B9 D0 8E 60
0xFFFFFFFF XOR key = ~key        →  46 2F 71 9F  ✓ matches Block A

Key: B9 D0 8E 60, applied as a 4-byte repeating XOR starting after the 13-byte plaintext header.

I wrote a Python decryption tool, ran it, and out came a clean ARM Cortex-M4 binary. Valid vector table, readable strings, sensible code at the entry point. The whole “encryption” was a speed bump.

So what about the AES classes in the updater? Hard to say definitively, since .NET Reactor encrypted the actual method bodies. All the interesting code was replaced with nop; nop; nop; ret stubs that get patched at runtime. The AES infrastructure might be there for a different product line, or it could be a future migration that never shipped. Either way, the wire format is plain XOR.

The .NET Reactor situation in more detail

The obfuscation was thorough but not impenetrable. .NET Reactor renames around 1,286 types and methods to strings like vCVrtesXKJYnEZTyxM, injects decoy metadata streams (#GUlD, #Blop) to trip up disassemblers, and adds SuppressIldasmAttribute to block Microsoft’s own tool. Method bodies are encrypted and only decrypted at runtime by a native DLL stub.

But the .NET type system metadata survived intact. ILSpy recovered 76 C# source files with full class and method signatures. The class hierarchy was readable: ManageDevATKUpdateFwATKFile (firmware parser) plus ManageProtocal (protocol layer). String literals were unencrypted. Struct layouts for the firmware header and device info packets came through clean.

What I couldn’t recover: the actual logic inside encrypted methods. The XOR decryption routine, the USB-HID packet construction, the exact VID/PID values passed to Hid.GetDevices(). Those are locked behind the runtime decryption layer. A proper de4dot run or a runtime memory dump would crack it, but I’d already reconstructed the protocol from other angles by that point.

Two Protocols, One Device

The HP20 speaks two completely different wire protocols depending on how you talk to it. This tripped me up at first. I kept trying to unify them before realizing they’re intentionally separate.

BLE Protocol (Mobile App)

The Bluetooth protocol is straightforward framed messaging, used by the companion Android app for real-time control:

BLE protocol frame structure: Flag, DevType, Func, DataLen, Data, Checksum
fig. 2 BLE wire format. Same frame structure for commands and responses.

The flag byte is always 0xAF. Device type is 0x0209 for the HP20 (little-endian, so 09 02 on the wire). The function byte selects one of 13 commands:

FuncCommandDirectionPurpose
0x00DevRegisterApp → DeviceInitial handshake
0x01GetVerApp → DeviceFirmware version query
0x02SetThermostatApp → DeviceSet target temperature
0x03GetRealTimeDataApp → DevicePoll telemetry (every 500ms)
0x04SetReflowApp → DeviceUpload reflow profile
0x05GetReflowApp → DeviceDownload current reflow profile
0x06StartReflowApp → DeviceBegin reflow cycle
0x07StopReflowApp → DeviceAbort reflow cycle
0x08SetOptionsApp → DeviceConfigure device settings
0x09GetOptionsApp → DeviceRead device settings
0x0ASetAlarmApp → DeviceConfigure alarm thresholds
0x0BSetRepairApp → DeviceUpload repair profile (HP20 only)
0x0CGetRepairApp → DeviceDownload repair profile (HP20 only)

Checksum is (256 - sum_of_all_preceding_bytes) & 0xFF. The device echoes the same function byte in its response, so you always know which command a reply belongs to.

USB-HID Protocol (Firmware Update)

The bootloader protocol is a different beast, designed for reliable bulk transfer of firmware images:

USB-HID bootloader frame structure: DevAddr, Func, Seq, DataLen, Data, CRC16, Padding
fig. 3 USB-HID bootloader frame. CRC16-MODBUS, sequence numbers, padded to 64 B.

Different framing, different command set (0x10 to 0x15), CRC16-MODBUS instead of a byte sum, sequence numbers for ordering chunks, and packets padded to exactly 64 bytes (USB-HID requirement). The firmware update handshake looks like this:

  1. GetDevInfo (0x10): query the bootloader for device type and 12-byte serial number
  2. SetFwInfo (0x11): send the 13-byte firmware header so the bootloader knows what to expect
  3. StartSendFW (0x12): signal that data transfer is beginning
  4. SendData (0x13): stream firmware chunks with incrementing sequence numbers
  5. SendDataEnd (0x14): signal transfer complete
  6. StartAPP (0x15): reboot into the new firmware (or just let the watchdog do it)

The split between BLE and USB makes sense once you think about it. BLE is a chatty real-time channel: short messages, frequent polls, simple checksums where overhead needs to be minimal. The USB bootloader transfers 400KB+ firmware images where you need proper integrity checking and sequencing. Two different problems, two different solutions.

HC32F460 flash memory layout: 40KB bootloader, 416KB application, 56KB settings
fig. 4 512 KB flash partitioned into three regions. The bootloader is never overwritten by updates.

The flash layout is clean: 40KB bootloader at the base (factory-flashed, never updated), the application image starting at 0x0A000, and a 56KB settings region at the top of the 512KB flash. The bootloader stays untouched across firmware updates. Sensible design: a bad update can’t brick the device.

The 11-Byte Heartbeat

Every 500ms during operation, the HP20 pushes an 11-byte telemetry packet over BLE notify. It’s packed tight:

BytesFieldEncodingNotes
0–1Platform temperatureu16 LEDegrees Celsius
2Power consumptionu8Current watts from supply
3Input voltageu8Tenths of volts (÷ 10)
4Unknownu8Always 0x00 in thermostat mode
5Heater resistanceu8Constant 0x02 ohms
6Heating poweru8PID-regulated watts to heater
7–8State infou16 LE bitfieldSee below
9–10State info 2u16 LE bitfieldSee below

The bitfields are where it gets dense. stateinfo packs three things into 16 bits:

  • Bits [9:0]: target temperature (0 to 1023 range, plenty for a 387°C max device)
  • Bits [11:10]: reflow stage index (which phase of a reflow profile is active)
  • Bits [14:12]: current UI mode. 0=idle, 1=reflow, 2=heating, 3=settings, 4=repair

stateinfo2 does similar packing for timer state:

  • Bits [5:0]: timer seconds (0 to 59)
  • Bits [12:6]: timer minutes (0 to 127)
  • Bit [14]: temperature unit flag (0=Celsius, 1=Fahrenheit)

The app never has to ask “what mode are you in?” It just reads the telemetry stream. 11 bytes, 120 times per minute, and you’ve got full observability of the device state.

The Apps: Two Frameworks, Same Protocol

Alientek ships two companion Android apps. The newer V2.0.8 is a Flutter app (Dart 3.10.3, compiled to ARM AOT, 61MB). The older HP15 app is a Xamarin/.NET MAUI app (C#, .NET 8, 66MB). They speak the exact same BLE protocol: same GATT UUIDs, same frame format, same command bytes.

The V2 Flutter app is actually universal across all Alientek BLE devices. It supports HP15, HP20, C2, DM40, EL15, and ND1. So if you’re working with any device in their lineup, this one APK covers them all. Hash is in the sources section below.

A few things stood out during app analysis.

The package name is com.example.atk_xtool. That’s the default Android Studio template. They never renamed it for production. It shipped to the Play Store like that.

The build path leaks the dev environment. Metadata in the Dart snapshot reveals D:\1_git\atk_xtool_flutter_android\atk_xtool. A Windows machine, personal Git directory structure.

Dart AOT snapshots are a pain to reverse. Standard tools like jadx don’t touch compiled Dart. I used blutter to recover the object pool: 21,475 entries totalling 943KB of strings, class names, constants, and metadata. That’s where the BLE GATT UUIDs came from (service 0xFFF0, write 0xFFF1, notify 0xFFF2), the device type enum (HP20 = 0x0209), and all the command method signatures.

The HP15 Xamarin app was more conventional to reverse. Extract the XALZ-compressed assembly blob from the APK, decompress with LZ4, and feed ATKMobileBLE.dll (708KB) into ILSpy. Out comes readable C# with full namespace and class hierarchy. Cross-referencing between the two apps resolved several ambiguous fields. Where one was unclear, the other usually spelled it out.

The Small Stuff

A few findings that didn’t fit neatly into sections above but were too good to skip.

The Boot Self-Test

Every power-on runs a 10-step diagnostic sequence, displayed on the round LCD with color-coded pass/fail indicators:

  1. Checking Voltage: ADC reads input voltage (0 to 32V range)
  2. Checking Pd: CH224Q USB Power Delivery negotiation
  3. Checking SI2302: auxiliary MOSFET test
  4. Checking NTC: thermistor ADC read (0 to 387°C range)
  5. Checking Heating: heater element continuity and current draw
  6. Checking Temp=300°C: thermal response test (actually heats up to verify)
  7. Checking FAN: fan spin-up with 20ms PWM loop
  8. Checking Bluetooth: BLE module AT-command ping
  9. Left key OK.: left button GPIO check
  10. Right key OK.: right button GPIO check

The firmware detects and reports specific fault conditions: open/short heater circuits, fan failure, NTC sensor errors, undervoltage, and thermal runaway. For a $100 hot plate, this is genuinely thorough.

The SI2302 Mystery

I initially assumed the SI2302 was the main heater MOSFET, since it’s the only MOSFET named in firmware strings. But the ratings don’t work. SI2302 is spec’d for 20V/2.6A max, while the HP20 supports 32V input (V1.2 changelog explicitly added a “32V power meter”) and needs 5 to 7A for 100 to 140W heating. The boot sequence tests “Checking SI2302” and “Checking Heating” as separate steps, confirming they’re different components. The actual heater MOSFET is some unnamed TO-220 package. You’d need board photos to identify it.

USB Vendor ID

The USB VID is 0x413D, registered to PCSensor (RDing Tech), not Alientek themselves. This was confirmed on Alientek’s T80 soldering iron. They appear to share or license this VID across their product line rather than registering their own.

The NTC Lookup Table

The firmware embeds a 130-entry 16-bit ADC-to-temperature lookup table at flash offset 0x0662DC. It covers 0 to 387°C with 3-degree resolution. The midpoint (ADC value ~2048, which is half of a 12-bit ADC’s range) maps to roughly 165°C. That’s consistent with an NTC thermistor on the low side of a voltage divider with a well-matched fixed resistor. Simple, reliable, no floating-point math needed on the MCU.

What’s Still Unknown

Some open threads for anyone who wants to pick them up:

  • Byte 4 of the real-time telemetry. Always zero in thermostat mode. Possibly activates during reflow or repair cycles.
  • The main heater MOSFET. Not named anywhere in firmware. Needs physical board inspection to identify.
  • Alarm delivery mechanism. 11 alarm types defined in app code, but the BLE notification path isn’t fully traced.
  • HP20 USB PID. Almost certainly 0x413D:0x2107 based on T80 cross-reference, but unconfirmed with lsusb in bootloader mode.
  • Firmware header date encoding. The year/month/day fields (0xB6/0x0F/0x00) don’t map to any obvious release date.

Sources

The V2 Flutter app (atk-xtool-V2.0.8_C2.apk) is universal across all Alientek BLE devices: HP15, HP20, C2, DM40, EL15, and ND1. If you’re poking at any of these, you only need this one APK. Despite the _C2 in the filename, it’s the same app for all devices.

FileDescriptionSHA256
atk-xtool-V2.0.8_C2.apkUniversal Android app (V2, Flutter)a5ba4ece...cdfaac
hp20_app_V1.5.atkEncrypted firmware image5c3b02cf...7156d0
ATK_HP20.exeWindows desktop updater (.NET)8aa02a72...400462
Full SHA256 hashes
a5ba4ece470be7e1cd3bc0b9141fa16f8b20db7bd998bc2f06e0c12675cdfaac  atk-xtool-V2.0.8_C2.apk
5c3b02cf242fe11932e482af87cbad7fb98c9c319eacae3d11aba8c9b17156d0  hp20_app_V1.5.atk
8aa02a72fe0726f49b1cb333dc1c77646f32671e6d75e34c9011132271400462  ATK_HP20.exe

The firmware .atk file and desktop updater ship together from Alientek’s official download page for the HP20. The APK was pulled from the Play Store listing. A full writeup with tools, scripts, and detailed protocol documentation is available in the project repository.