Tools to support VanMoof S3/X3 reverse engineering efforts
- mainware.bin (MCU: ST STM32F413VGT6)
- bleware.bin (MCU: TI CC2642R1F)
- motorware.bin (MCU: TI TMS320F28054F)
- shifterware.bin (MCU: MindMotion MM32F031F6U6)
- batteryware.bin (MCU: ST STM32L072CZT6)
- bmsboot.bin (MCU: see batteryware.bin)
- muco-boot.bin (MCU: see mainware.bin)
- bleboot.bin (MCU: see bleware.bin)
- shifterboot.bin (MCU: see shifterware.bin)
Most components of the bike contain a split boot loader and firmware image, the Bluetooth firmware might differ from this scheme.
The boot loader starts with the typical ARM vector table and is 20Kb (0x5000 bytes) long. The last 8 bytes of the boot loader binary contain:
- 4 bytes version encoded as ASCII characters
- 4 bytes CRC32.
The CRC is calculated using the STM32 CRC algorithm over the whole boot loader data without the last 4 bytes
The firmware image starts with a magic 0xaa55aa55, followed by these header items:
- 4 bytes version info
- 4 bytes CRC32
- 4 bytes length
- 12 bytes date in ASCII
- 12 bytes time in ASCII
All 4 byte values are encoded little endian
The CRC is calculated using the STM32 CRC algorithm over the whole image data with the header CRC and length fields both set to 0xffffffff.
The S5/A5 generation uses a different container for its Cortex-M ECU images
(user_ecu, imx8_bridge, elock, eshifter, frontlight, rearlight,
motor_sensor, power_control, power_pedal). Each .bin is the whole
flashable image (the updater streams it page by page); instead of a magic at
offset 0, a VMFW header sits at a fixed offset 0x134, right after the
Cortex-M vector table. The header items are:
- 4 bytes magic "VMFW"
- 4 bytes version info
- 4 bytes CRC32
- 4 bytes length
- 12 bytes date in ASCII (
__DATE__, e.g. "Jan 29 2024") - 12 bytes time in ASCII (
__TIME__, e.g. "14:50:32", 9 bytes used)
All 4 byte values are encoded little endian. The length is authoritative - it is
the size of the image that is CRC'd and flashed; for the shipped device files it
equals the exact file size. crc32 enforces it: a length that runs past the file
is treated as a truncated/corrupt image and fails, while any bytes past length
(e.g. an appended signature) are excluded from the CRC. The version word is packed
exactly like manifest.txt:
major << 24 | minor << 16 | (variant & 7) << 13 | (patch & 0x1fff), where the
variant is 1 dev, 2 rc, 3 main, 0 untagged (e.g. 0x01056000 reads as
1.5.0 main).
Unlike the S3/X3 images, the CRC here is the standard CRC-32 (zlib /
Ethernet poly 0x04c11db7, reflected, init and final-xor 0xffffffff - what
crc32(3) from zlib computes), again over the image (its length bytes) with
the header CRC and length fields both set to 0xffffffff.
The S6 generation reuses the same VMFW magic/CRC/length but a different
header tail: instead of __DATE__/__TIME__ it carries a 4-byte build number
followed by a "vMAJOR.MINOR.PATCH.BUILD" version string, and the version word
is plain bytes major.minor.patch (e.g. 0x00080801 with build 4974 reads as
1.8.8 build 4974, "v1.8.8.4974"). crc32 detects the dialect from that version
string; the CRC algorithm is identical.
The Nordic parts (ble, modem) instead carry an MCUboot header at offset 0,
and motor_control uses its own non-standard header; these are not VMFW images.
You can use a Raspberry Pi, your dusty old Linux Machine (Ubuntu or Debian is fine), WSL (Windows Subsystem for Linux) and macOS.
apt update
apt install build-essential gcc-arm-none-eabi binutils-arm-none-eabi libssl-dev
make
You need Homebrew
brew install openssl@3 # host tools: pack, unpack, crc32, patch
brew install --cask gcc-arm-embedded # firmware targets: patch-dump, ble-patch, backupcode
make
The ARM cross-compiler is only required for the firmware targets (patch-dump, ble-patch, backupcode).
usage: unpack [-l] [-h] [-d <dir>] <packfile>
This tool extracts the contents of a VanMoof update file, also known as PACK file. A PACK file starts with a header containing the magic "PACK", an offset to a directory structure and the length of the directory structure. The directory structure (at the end of the file) contains one or more entries containing a filename, an offset, and the length of the data. See pack.h for details of these structures.
If the PACK file (or its enclosing HEAD wrapper) has a TLV signature trailer (same format as parsed by crc32), the SHA256, KEYHASH and ECDSA_SIG entries are printed and the SHA256 is verified.
By default the files are extracted into the current directory, overwriting any file already present there with the same name. Run this in a separate directory, or use -d <dir> to extract elsewhere, to be shure not to loose any data.
-l: List the PACK file contents only, do not extract any files.-h: Show file sizes as human readable (KiB / MiB) instead of hex.-d <dir>: Extract the files into<dir>instead of the current directory. The directory is created if it does not exist (its parent must already exist). Ignored together with-l, since nothing is written.
usage: pack <packfile> <warefile> [<warefile> ...]
This tool packs one or more firmware files into a VanMoof update file, also known as PACK file. This is the reverse operation of the unpack command.
usage: crc32 [-w] <warefile>
This tool calculates and verifies the CRC of both boot loader and firmware images. It auto-detects the container and recurses into wrappers: S3/X3 vanmoof_ware_t images (magic 0xaa55aa55), the HEAD signature wrapper (TLV trailer with SHA256/KEYHASH/ECDSA_SIG), PACK bundles (each contained ware is listed and CRC-checked individually; a bundled animations.pak is summarised rather than descended into), BLE OAD images, plain ARM bootloaders, and the S5/A5 and S6 VMFW images described above. A signed S6/S3 update .pak is a HEAD-wrapped PACK, so running crc32 on it verifies the wrapper signature and then every firmware inside. Formats it cannot verify (e.g. the raw battery payload or the nRF .cbor modem image) are reported as "cannot verify" rather than failing.
With -w the tool finalises an application ware (magic 0xaa55aa55) in place instead of only checking it: it sets the length field to the file size and writes the correct CRC-32, computed with the same ware_crc it verifies with, into the header, then re-verifies. This is the post-build stamp step for self-built images: a ware's Makefile runs crc32 -w on the objcopy output so the boot loader accepts it (e.g. backupcode uses it as its STAMP). Inputs without the ware magic are left untouched.
usage: patch [-v] [-f <fake_version>] [-m <model>] <mainware>
This tool patches a modern VanMoof mainware file, so the region OFFROAD is not reset to region US during boot. The power assistance level can be configured to 5 again. When cycling through power assistance levels using the handle bar, the bike will cycle through power level 5 as well if region is OFFROAD.
The file given on the command line is overwritten with the patched version of the file, so please make a backup of your mainware before using the tool.
-v: Be verbose-f <fake-version>: Specify a different version, format:<major>.<minor>.<patch>-m <model>: Change default bike model, format:<model (3|4)>,<shifter (0|1)>,<display (0|1)>
Currently the tool only works for mainware version 1.9.3.
usage: ble-patch <bleware>
This tool patches the VanMoof bleware file, so the command rtos-statistics is replaced with a command dump. Using dump keys inside the bledebug console will show the stored keys. These keys are two factory default keys used during bringup of the bike in factory, your API key, and the VanMoof manufacturer key. The latter is used to encrypt firmware images (when sending updates to the APP).
You can update the patched bleware on the bike by creating a pack containing this bleware and using the command pack-upload inside the bledebug console. You need to send the created pack using ymodem.
Output looks like:
> dump keys
Key 0x00: UKEY 52XXXXXXXXXXXXXXXXXXXXXXXXXXXX2f 00000001 000003fe CRC 81XXXX92
Key 0x01: UKEY 98XXXXXXXXXXXXXXXXXXXXXXXXXXXX02 00000002 000001f4 CRC 72XXXX94
Key 0x7c: M-ID 00000000000000000000000000000000 00000008 00000000 CRC 7062da7e
Key 0x7d: UKEY 5f5f5f5f5f4f574e45525f5045524d53 00000000 ffffffff CRC 4f25ee68
Key 0x7e: MKEY 710b2ea0dc8568b7b5e5ec0b8a39dae9 00000000 00000000 CRC a69429b6
Key 0x7f: MKEY 46383841XXXXXXXXXXXXXXXX4d4f4f46 00000000 ffffffff CRC 4aXXXX7e
The keys 0x7d and 0x7f seem to be the factory bring-up keys, translated to ASCII they read:
Key 0x7d: UKEY _____OWNER_PERMS 00000000 ffffffff CRC 4f25ee68
Key 0x7f: MKEY F88AXXXXXXXXMOOF 00000000 ffffffff CRC 4aXXXX7e
^- Bike MAC Address
This can also dump memory (i.e. ROM, internal FLASH, or external FLASH):
> dump mem 10000000 40000
10000000 00 20 00 11 b1 19 00 10 bf 20 00 10 c1 20 00 10 . ...... . ... ..
10000010 c3 20 00 10 c3 20 00 10 c3 20 00 10 00 00 00 00 . ... .. . ......
...
> dump extflash 5afa0 20
0005afa0 5f 5f 5f 5f 5f 4f 57 4e 45 52 5f 50 45 52 4d 53 _____OWN ER_PERMS
0005afb0 00 00 00 00 ff ff ff ff 55 4b 45 59 68 ee 25 4f ........ UKEYh.%O
There is a special command which shows the values of CCFG_TI_OPTIONS and CCFG_TAP_DAP_*, this command will patch to boot loader of the BLE chip to enable the JTAG debug port of the BLE chip for further debugging. The first time the command is called, it will output values like:
> dump ccfg
CCFG_TI_OPTIONS: 0xffffff00
CCFG_TAP_DAP_0: 0xff000000
CCFG_TAP_DAP_1: 0xff000000
JTAGCFG: 0x00000000
When these values are found, the last sector of flash (including the CCFG) is copied down from 0x56000 to 0x46000 while patching the CCFG values and the version string, and then copying the sector back to 0x56000. After a reset these new CCFG values take effect and the JTAG port is enabled:
> dump ccfg
CCFG_TI_OPTIONS: 0xffffffc5
CCFG_TAP_DAP_0: 0xffc5c5c5
CCFG_TAP_DAP_1: 0xffc5c5c5
JTAGCFG: 0x00000003
The boot loader is not touched again once these CCFG values are present.
-v: Be verbose, list every patch as it is verified and applied.-m: Experimental. Take the BLE MAC address from CCFG instead of FCFG1, see below.
When a BLE chip is replaced, the bike stops matching the MAC address stored for
it in the VanMoof backend. The CC2642R1F cannot change its primary address,
it is factory programmed read-only into FCFG1 at 0x500012e8, but it does have
a customer programmable secondary address in CCFG at 0x50004fd0
(IEEE_BLE_0/IEEE_BLE_1), and CCFG lives in the last flash page, so it can be
written together with the boot loader.
-m rewrites every literal reference to the FCFG1 register into a reference to
the CCFG one, so the replacement chip reports the address of the chip it
replaces. In bleware 2.4.01 there are eight such references; the firmware
already carries one reference to the CCFG register (next to the FCFG1 one, in
what looks like TI's own primary/secondary address selection), which is left
alone. The OAD CRC-32 is recomputed afterwards, so the boot loader accepts the
result - bleboot verifies a CRC-32 and never a signature, on either its
quick-scan or its full-scan path, so the stale ECDSA signature in the image
header does not need renewing for a JTAG-flashed image.
Because the patch keys off a register literal rather than hardcoded offsets, it
works on any bleware version. If the file is not one of the two versions the
dump patchset knows, -m applies the MAC patch alone and leaves everything else
untouched:
$ ble-patch -m bleware_2.4.01.bin
ble-patch: EXPERIMENTAL: taking the BLE MAC address from CCFG instead of FCFG1
bleware_2.4.01.bin: mac source: 8 reference(s) 0x500012e8 -> 0x50004fd0 in [0x00000090..0x00035b2c)
Note that on a version the dump patchset does know, -m is applied in
addition to it - the file gets the debug console patch as well, which is
ble-patch's normal job.
This is only half the job. The address itself still has to be programmed
into the CCFG field, or the radio comes up as ff:ff:ff:ff:ff:ff.
cp bleboot_1.0.1.bin bleboot_f88a5e4f1337.bin
MAC=f8:8a:5e:4f:13:37
printf "$(echo $MAC | awk -F: '{for (i=6;i>=1;i--) printf "\\x%s", $i}')\xff\xff" |
dd of=bleboot_f88a5e4f1337.bin bs=1 seek=$((0x1fd0)) conv=notrunc
use ble-merge afterwards with the patched bootloader and flash the file onto the Chip.
Flash the Image but afterwards change the secondary mac manually in SmartFR Flash Programmer 2.
usage: ble-merge [-k] [-v] [-x] <blewarefile> <blebootfile> [<outfile>]
The BLE MCU keeps its two images in two places, bleware at 0x00000000 and
bleboot (the TI BIM) in the last 8 KB flash page at 0x00056000, so there is
no single file to hand to a flash programmer. This tool merges the pair into one
352 KB image covering the whole CC2642R1F flash (0xff in the gap between them)
and removes the debug lock from the CCFG on the way. That is what you need to
bring a chip back to life after a forced mass erase, using SmartRF Flash
Programmer 2, UniFlash or any other XDS110 host.
Both images are copied byte for byte. The only bytes that change are three CCFG words in the last page:
| Address | Field | VanMoof | ble-merge |
|---|---|---|---|
| 0x57fe0 | CCFG_TI_OPTIONS |
0xffffff00 | 0xffffffc5 |
| 0x57fe4 | CCFG_TAP_DAP_0 |
0xff000000 | 0xffc5c5c5 |
| 0x57fe8 | CCFG_TAP_DAP_1 |
0xff000000 | 0xffc5c5c5 |
These are the same values the dump ccfg command of ble-patch writes at
runtime, so a patched bleware recognises its own marker and never rewrites the
boot loader page.
A TAP is enabled only by the exact value 0xc5. VanMoof ships 0x00 in every
TAP field, which turns off the CPU DAP (and with it JTAG/cJTAG debugging), the
power profiler and test TAPs, the PBIST and AON TAPs, and TI's failure-analysis
unlock. CCFG_BL_CONFIG is 0x00ffffff, so BOOTLOADER_ENABLE is 0x00 and
the ROM serial boot loader is disabled as well - there is no serial backdoor
either. An XDS110 still sees the ICEPick router on a locked chip, but the CPU TAP
never comes up and CCS/UniFlash stops at:
IcePick_C: Error connecting to the target: (Error -241 @ 0x0) A router subpath
could not be accessed. A security error has probably occurred.
CCFG_ERASE_CONF stays 0xffffffff, so chip erase and bank erase remain
enabled. That is why a forced mass erase still gets through on a locked chip,
and it is the only way back in. Programming the factory CCFG unchanged locks the
chip again on the first boot; that is what -k does, and the tool warns when it
is about to hand you such an image.
-k: Keep the CCFG exactly as it is in<blebootfile>, i.e. reproduce the factory flash including the debug lock.-v: Also print all 22 CCFG fields by name.-x: Write Intel HEX instead of a raw binary (implied by a.hexoutput file name). The HEX covers only the two occupied ranges, so the erased gap between them is not programmed.
Both input files are recognised by content, so their order on the command line
does not matter. <blebootfile> may also be a full 352 KB flash dump, in which
case its last page is used. With no <outfile> nothing is written and the tool
only reports what it found - a quick way to check whether some bleboot image
locks the debug port.
The raw binary carries no addresses and is meant to be programmed from
0x00000000, which is where SmartRF Flash Programmer 2 puts a .bin by default.
In its Program page pick the merged file, Program: "Entire source file", Verify: "Readback".
The CCFG is part of the image, so the programmer writes the unlocked values with everything else; there is no separate CCFG step and no need to keep the old page.
- The BIM boots the freshly programmed image through its quick scan, which
only checks the OAD header fields (
bimVer3,metaVer1, image type 1/3/7, crc status other than0xfc) and then jumps toprgEntry. It does not re-verify the CRC32 on that path, so a self-built bleware boots even if its CRC is stale.ble-mergereports the CRC anyway, because an OTA update of the same image would be rejected. - Before the quick scan the BIM runs a full scan over the external SPI NOR flash. A staged OAD image that is still marked pending there is promoted into internal flash and launched, overwriting what you just programmed. If the module comes back with a locked chip that might be the reason.
blebootverifies a CRC32, not a signature, so nothing here needs re-signing.- Neither the bleware nor the boot loader code is modified; if you want the
factory-identical flash, use
-k.
This tool patches a modern VanMoof mainware as patch above, but adds a function to dump FLASH or memory to the console. This function is patched into the help command and will output FLASH or memory as hexdump. Use as help <addr> <count>.
The hexdump can be converted to binary using the dump2bin.sh script.
An older version would output the whole FLASH as S-Records, the source is still provided in the repo, edit the Makefile if you want to use this function.
Capture the terminal output to a logfile and clip out the S-Records to a file vanmoof.srec. To convert this dump to the different binaries used inside the bike, use these shell commands:
objcopy -I srec -O binary vanmoof.srec vanmoof.bin
dd if=vanmoof.bin of=muco-boot.bin bs=4096 count=8
dd if=vanmoof.bin of=vanmoof-config-a.bin bs=4096 skip=8 count=4
dd if=vanmoof.bin of=vanmoof-config-b.bin bs=4096 skip=12 count=4
dd if=vanmoof.bin of=shifterware.bin bs=4096 skip=16 count=16
dd if=vanmoof.bin of=mainware.bin bs=4096 skip=32 count=64
dd if=vanmoof.bin of=shadowware.bin bs=4096 skip=96 count=64
dd if=vanmoof.bin of=motorware.bin bs=4096 skip=160 count=32
dd if=vanmoof.bin of=batteryware.bin bs=4096 skip=192 count=32
dd if=vanmoof.bin of=bmsboot.bin bs=4096 skip=224 count=32
0x08000000: stm32 boot loader
0x08008000: bike config A
0x0800c000: bike config B
0x08010000: shifterware image
0x08020000: mainware image
0x08060000: shadow image
0x080a0000: motorware image
0x080c0000: batteryware image
0x080e0000: bmsboot image
0x20000a00: Bike state/config
from FLASH at 0x8008000 or 0x800c000, 0xc0 bytes
+ 0x0f4: Sound bitmask [1] low
+ 0x0f8: Sound bitmask [1] medium
+ 0x0fc: Sound bitmask [1] high
+ 0x100: Backup code
+ 0x102: Lux low
+ 0x105: Volume low
+ 0x106: Volume medium
+ 0x107: Volume high
+ 0x108: Shift mode: AUTO/MANUAL
+ 0x109: Region
+ 0x10a: Unit system
+ 0x10b: Wheel motor type
+ 0x10c: Light mode
+ 0x10e: Shift up EU (3 x uint16_t)
+ 0x114: Shift up US (3 x uint16_t)
+ 0x11a: Shift up JP (3 x uint16_t)
+ 0x120: Shift up OFFROAD (3 x uint16_t)
+ 0x126: Shift down EU (3 x uint16_t)
+ 0x12c: Shift down US (3 x uint16_t)
+ 0x132: Shift down JP (3 x uint16_t)
+ 0x138: Shift down OFFROAD (3 x uint16_t)
+ 0x140: Mainware version
+ 0x144: Region lock
+ 0x145: Model: Bit0: 0=ES3, 1=ES4; Bit1: 1=E-Shifter, Bit2: 1=Display
+ 0x146: Custom soc
+ 0x147: HW revision
+ 0x1c0: CRC32 over FLASH data
Motor support settings (from mainware):
+ 0x2c6: Motor percent power level 0
+ 0x2c8: Motor speed limit power level 0
+ 0x2ca: Motor percent power level 1
+ 0x2cc: Motor speed limit power level 1
+ 0x2ce: Motor percent power level 2
+ 0x2d0: Motor speed limit power level 2
+ 0x2d2: Motor percent power level 3
+ 0x2d4: Motor speed limit power level 3
+ 0x2d6: Motor percent power level 4
+ 0x2d8: Motor speed limit power level 4
+ 0x2d6: Motor percent power level 5
+ 0x2d8: Motor speed limit power level 5
from EEPROM, 0x3c bytes
+ 0x310: Alarm state
+ 0x311: Play lock sound
+ 0x312: Remote locked
+ 0x313: Logging APP/Serial
+ 0x314: Shipping
+ 0x315: Cached BMS soc
+ 0x316: Power level + Boost
+ 0x317: Alarm enable/disable
+ 0x318: Horn file index
+ 0x31c: Odometer: km * 10
+ 0x320: Timestamp bell button
+ 0x324: Timestamp boost button
+ 0x328: Timestamp GSM check
+ 0x32c: Firmware update order (6 bytes)
+ 0x332: BMS soc override
+ 0x333: BLE sleep request
+ 0x334: Shifter retries
+ 0x336: Shifter firmware version
+ 0x338: Shifter total shifts
+ 0x33c: GSM tracking heartbeat
+ 0x340: Kicklock state
+ 0x341: Battery state
+ 0x342: BMS firmware version
+ 0x344: Wake counter
+ 0x348: CRC32 over EEPROM data
Bike state
+ 0x34c: BLE debug flag
+ 0x34d: GSM debug flag
+ 0x34e: Shift debug flag
+ 0x34f: BMS debug flag
+ 0x358: Loop count actual
+ 0x35c: Loop count min
+ 0x360: Loop count max
+ 0x364: Motor error
+ 0x366: Motor speed
+ 0x368: Motor I
+ 0x36a: Motor m_tmp
+ 0x36c: Motor d_tmp
+ 0x36e: Motor wlsp
+ 0x370: Motor Ubat
+ 0x372: Motor Pdsp
+ 0x374: Motor Pdtrg
+ 0x376: Motor io
+ 0x388: Motor firmware version
+ 0x38c: BLE firmware version
+ 0x390: BLE MAC address (6 bytes)
+ 0x396: BMS ESN (18 bytes)
+ 0x3a8: Debug password (16 bytes)
+ 0x3ba: Lipo version
+ 0x3c0: Error flags (2 x uint32_t)
+ 0x3cc: Speed 1
+ 0x3ce: Speed 2
+ 0x3d1: Power level (init from 0x316 & 0x7f)
+ 0x3d2: Power level (copy, init from 0x316 & 0x7f)
+ 0x3d3: Ride change
+ 0x3e0: Lipo state
+ 0x3e4: Powerbank soc
+ 0x3e6: Powerbank serial number (4 bytes)
+ 0x3ea: Powerbank serial version (3 bytes)
+ 0x3ed: Powerbank soh
+ 0x3ee: Powerbank noc
+ 0x3f1: Powerbank present
+ 0x3f8: GSM type pointer
Battery state
+ 0x402: Type
+ 0x406: Error flags
+ 0x408: Temperature (°C/100)
+ 0x40a: Voltage (mV)
+ 0x40c: State of charge (%)
+ 0x40e: Current (mA)
+ 0x412: Discharging flag
+ 0x414: Testmode flag
+ 0x416: HW version
+ 0x418: SW version
+ 0x41a: Serial number (14 bytes)
+ 0x428: Manufacture date (3 bytes)
+ 0x42c: cap_nominal
+ 0x42e: cap_full
+ 0x430: cap_remain
+ 0x432: health
+ 0x434: cycle_count
+ 0x438: cell_voltage (10 x mV)
+ 0x44c: tp1
+ 0x44e: tp2
+ 0x450: mos_tmp
+ 0x454: u_max
+ 0x456: u_min
+ 0x45a: Boot loader version
+ 0x462: fsr
+ 0x490: dotp
+ 0x492: dutp
+ 0x494: cotp
+ 0x496: cutp
+ 0x498: docp1
+ 0x49a: docp2
+ 0x49c: cocp1
+ 0x49e: cocp2
+ 0x4a0: ovp1
+ 0x4a2: ovp2
+ 0x4a4: uvp1
+ 0x4a6: uvp2
+ 0x4a8: pdocp
+ 0x4aa: pdscp
+ 0x4ac: motp
+ 0xac scp
[1] bitmask
10000000: TI ROM boot loader
10007000: TI ROM ble5stack
1002b400: TI ROM tirtos7
50001000: FCFG1
500012e8: FCFG1::MAC_BLE_0
500012ec: FCFG1::MAC_BLE_1
50003000: CCFG (mirrored from FLASH 56000)
00000000: bleware.bin
00056000: boot loader
00057f38: boot loader version: "BVERApr 23 2020"
00057f48: boot loader version: "14:10:12"
00057fa8: CCFG (Customer configuration) Mirrored at CCFG 50004fa8
00057fd8: CCFG::BL_CONFIG (ROM serial boot loader, 0xc5 in bits 31:24 enables it)
00057fdc: CCFG::ERASE_CONF (chip erase / bank erase)
00057fe0: CCFG::CCFG_TI_OPTIONS (TI failure analysis)
00057fe4: CCFG::CCFG_TAP_DAP_0 (CPU DAP, PWRPROF TAP, TEST TAP)
00057fe8: CCFG::CCFG_TAP_DAP_1 (PBIST2 TAP, PBIST1 TAP, AON TAP)
00057fec: CCFG::IMAGE_VALID_CONF -> 56000 (boot loader entry)
2000a3dc: Memory location of UKEY, when used in BLE protocol (1.4.1)
2000a3fc: Memory location of MKEY, when used in BLE protocol (1.4.1)
2000cec8: Memory location of UKEY, when used in BLE protocol (2.4.1)
2000cee8: Memory location of MKEY, when used in BLE protocol (2.4.1)
A simple cheasy update tool to send firmware packed with pack to the bike. This needs pymoof to run.
You need to insert your bikes API key and manufacturer key before using the tool.
Use as a reference for the firmware update over BLE.
A simple cheasy tool to read the internal debug logs from the bike using BLE. This needs pymoof to run.
You need to insert your bikes API key before using the tool.
Use as a reference howto read logs over BLE.
Main MCU communicates with the other MCUs via:
| UART | TX | RX | Function | Protocol | Comments |
|---|---|---|---|---|---|
| USART1 | PA9 | PA10 | Alternative debug | console | maybe accessed through debug header TC1? |
| USART2 | PA2 | PA3 | GSM uBlox G350 | AT commands | passthrough in gsmdebug mode |
| USART3 | PD8 | PD9 | Shifter MCU | Modbus | |
| UART4 | PA0 | PA1 | Battery MCU | Modbus | |
| UART5 | PB13 | PB12 | BLE MCU control | SSP | SLIP encoded packets |
| USART6 | PC6 | PC7 | Motor MCU | SSP | SLIP encoded packets |
| UART7 | PE8 | PE7 | Main MCU debug | console | port behind rear light |
| UART8 | PE1 | PE0 | BLE MCU debug | console | passthrough in bledebug mode |
The SLIP encoded packets contain one byte sender address, a command byte, a sequence byte, a little endian 16 bit word which is an offset or a function code, a 16 bit data length word, data, and a 16 bit CRC, which is the same as the Modbus CRC.
The command byte is 06 for READ, 07 for WRITE, and 05 for ACK packets. The CRC is calculated over the packet without the C0 framing characters.
C0 01 06 56 1A 01 33 F8 C0 MCU -> BLE READ req 56: 0x011a
C0 02 05 56 53 6E C0 BLE -> MCU ACK 56
C0 02 07 79 1A 01 06 00 F8 8A 5E XX XX XX YY YY C0 BLE -> MCU WRITE req 79: 0x011a: 0x0006 bytes: 0xF8 8A 5E XX XX XX
C0 01 05 79 E2 B2 C0 MCU -> BLE ACK 79
The MCU handles both packet streams from the BLE and the Motor MCU inside the same packet handler, so the offsets/function codes of the BLE and the Motor need to be disjunct.
The bluetooth service @5505 contains backoffice messages, these are used to configure UKEYs or MKEYs on the bike, for example for bike sharing or workshop access. These messages are encrypted using the bikes MKEY. The messages are prefixed with a two byte nonce, the byte 0x01 and an offset byte. These 4 bytes are not encrypted. The message continues with n * 16 bytes MKEY encrypted data.
| Nonce | Const 1 | Offset | Encrypted backoffice message |
|---|---|---|---|
| a12d | 01 | 00 | acc3d0f327c70f5a4755185bcb27c40df508b19df62e7551127abe79c9c822326adef001d97d51b45f8c58a7d2cc0cc0 |
The decrypted message is build up like this (format 1):
| M-ID | Cmd | Len | UKEY data | Index | Perms | Modbus CRC | Padding |
|---|---|---|---|---|---|---|---|
| 00000008 | 0001 | 18 | 98d29703b832207ed7c67b34edfadc02 | 00000002 | 000001f4 | 6845 | 0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f |
Message format 2:
| M-ID | Cmd | Len | Index | Modbus CRC | Padding |
|---|---|---|---|---|---|
| 00000004 | 0003 | 04 | 00000002 | 2577 | 0505050505 |
Message format 3:
| M-ID | Cmd | Len | State | Modbus CRC | Padding |
|---|---|---|---|---|---|
| 00000007 | 0005 | 01 | 00 | 5ae4 | 0808080808080808 |
Message format 4:
| M-ID | Cmd | Len | Modbus CRC | Padding |
|---|---|---|---|---|
| 00000005 | 0006 | 00 | 6c18 | 090909090909090909 |
Message format 5:
| M-ID | Cmd | Len | UKEY data | Index | Perms | ... | UKEY data | Index | Perms | Modbus CRC | Padding |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 00000008 | 0001 | 48 | 98d29703b832207ed7c67b34edfadc02 | 00000002 | 000001f4 | ... | cb27c40df508b19df62e7551127abe79 | 00000004 | 000001f4 | xxxx | 07070707070707 |
Message format 6:
| M-ID | Cmd | Len | Unknown1 | Unknown2 | Modbus CRC | Padding |
|---|---|---|---|---|---|---|
| 00000004 | 0008 | 02+n | xxxx | yy .. yy (n bytes) | zzzz | .. |
The possible Cmd values are:
| Cmd | Message (format) |
|---|---|
| 0001 | Update UKEY (1) |
| 0002 | Update MKEY (1) |
| 0003 | Erase key by index (2) |
| 0004 | Nothing (always succeed) |
| 0005 | Set Module State @5562 (3) |
| 0006 | Erase all keys (4) |
| 0007 | Update multiple keys (5) |
| 0008 | Unknown, read keys? (6) |
| 0009 | Unknown, FMNA related |
| 000a | Unknown, FMNA related |
The Login: prompt on the debug console knows two passwords, one fixed password hardcoded in the firmware vEVjGF!paYsM2EBV8SoDT8*T0eB&#T6xevaoxCaO and one password containing the last three bytes of the bikes MAC address followed by the word "DeBug", as output by printf("%02X%02X%02XDeBug", MAC[3], MAC[4], MAC[5]).
The debug console has a help command. The BLE chip and the GSM modem can also be accessed from the debug port, by using the commands bledebug and gsmdebug respectively.
Login: ***********
Welcome to ES3
help
Available commands:
help This tekst
reboot reboot CPU
login Login shell
logout Logout shell
ver Software version
distance Manual set dst
gear set gear
region Region 0..3
model model
blereset hard reset BLE
bledebug redirect uart8
show Parameters
motorupdate Update F2806 CPU
vollow Audio volume
volmid Audio volume
volhigh Audio volume
speed override speed
loop main loop time
shipping Shipping mode
factory-shipping Factory shipping mode (ignores BMS)
logprn Print log
logclr Clear log 6
logapp 1/ 0
powerchange 1/ 0
factory Load factory defaults
battery Show battery
batware Battery update
batboot BatteryBL update
batreset Battery reset
shiftware Battery update
shifterstatus Show shifter
shiftdebug Show Modbus
shiftresetcounter Reset shift counter
motorstatus
gsminfo Info from Ublox
gsmstart start GSM function
gsmdebug redirect uart2
bmsdebug Show Modbus
sound sample,volume,times
adc read adc
bwritereg Modbus Bat write register
bwritedata Modbus Bat write data
breadreg Modbus Bat read register
swritereg Modbus Shift write register
swritedata Modbus Shift write data
sreadreg Modbus Shift read register
stc read lipo monitor
stcreset
setoad test
setgear save muco shifter
soc overrule soc
customsoc sound soc
hwrev hardware revision
error set errorcode
ver
ES3.0 Main 1.09.03 (10:30:52 Apr 30 2025)
ES3 boot 1.9
Motorware S.0.00.22
BMSWare BL:007 FW:1.17 RSOC:100 Cycles:42 HW:3.10 ESN:XXXXXXXXXXXXXX
Shifterware 0.237 stored: 0.237
BLEWare 1.4.01
GSMWare 08.90
CMD_BLE_MAC F8:8A:5E:XX:XX:XX
The BLE console also has a help command and one can return to the MCU console with the command exit.
Login: ***********
Welcome to ES3
bledebug
Connect to UART8
> help
The following commands are available:
firmware-update - update a new image of firmware to the external flash
extflash-verify - verify the current flashchip
log-count - get log-count statistic
log-dump <start-index> <n> - print <n> blocks starting at address <start-index>
log-flush - flush all log-entries
log-inject <n> - Create <n> fake-logs
audio-play <index> - play audio bound to the specified index
audio-stop - stop playing the current audio file
audio-dump - dump all audio files in external memory
audio-upload <index> - upload audio binary using Y-Modem at the address linked to the specified index
audio-volume-set-all <level> - set audio level of all audio-clips (0-3)
pack-upload - upload a PACK file by Y-Modem
pack-list - list the contents of a PACK file
pack-delete - delete a PACK file
pack-process - process pack files in external flash memory
ble-info - dump current BLE connection info / statistics
ble-disconnect - force a disconnect of all connected devices
ble-erase-all-bonds - erase all bonds
shutdown - shutdown the system
rtos-statistics - dump memory stats every 500ms
rtos-nvm-compact - Compact the non-volatile storage
dump - dump keys/memory/extflash
info/ver - show basic firmware info
exit - exit from shell
help - show all monitor commands
> info
BLE MAC Address: "f8:8a:5e:xx:xx:xx"
Device name ................ : ES3-F88A5EXXXXXX
Firmware version ........... : 1.04.01
Compile date / time ........ : May 12 2025 / 09:03:35
BIM firmware version ....... : 1.00.00
BIM compile date / time .... : Apr 23 2020 / 14:10:12
reset type ................. : pin reset
systick .................... : -117259002
> exit
The GSM console talks extended ublox AT commands. It can be exited with the sequence <ESC>[14~.
Login: ***********
Welcome to ES3
gsmdebug
Modem powering on..
ATI
SARA-G350-02S-01
OK
AT+UGSRV?
+UGSRV: "ublox1.vanmoof.com","ublox1.vanmoof.com","PBNjh0V46Eev8CcfS4LPJg",14,4,1,65,0,15
OK
AT+UPSDA=0,3
OK
AT+UPSND=0,8
+UPSND: 0,8,1
OK
AT+ULOCIND=1
OK
AT+ULOC=2,2,1,180,1,10
OK
+UULOCIND: 0,0
+UULOCIND: 1,0
+UULOCIND: 2,0
+UULOCIND: 3,0
+UULOC: 16/05/2025,08:27:54.000,<latitude>,<longitude>,0,601,0,0,0,2,0,0,0
I see a crash of the controller when sending firmware update packets > 256 bytes, so there might be the chance to use this as an exploit to read out the MKEY over bluetooth. We need the MKEY to be able to:
- decode the update packages received from Vanmoof
- send our own update package (patched with features like offroad) to the bike
The magic numbers used by VanMoof have been used previously by others, this gives some funny output when using the unix file command:
$ file packfile.bin
packfile.bin: Quake I or II world or extension, 340 entries
$ file mainware.bin
mainware.bin: BIOS (ia32) ROM Ext. (85*512)