Skip to content

Commit

Permalink
zycast: new tool for ZyXEL bootloader flashing
Browse files Browse the repository at this point in the history
The bootloader of many ZyXEL routers support a proprietary
feature allowing the devices to be flashed over the network
using a multicast stream.

This tool is an attempt to document and implement the client
side of this protocol

The supported devices listen for magic packets a few seconds
on every boot.  The console will print something like

  Multiboot Listening...

with a countdown indicating when the listen window closes.
Syncronizing the client with the listening window is not
required.  The protocol is designed to allow the client to
continuously repeat its image stream. Just start the
client before rebooting the router and let it run till the
download is finished.

This means that it is possible to do blind upgrades too. But
any error will be hard to catch without console.

Signed-off-by: Bjørn Mork <bjorn@mork.no>
  • Loading branch information
bmork committed Aug 6, 2023
1 parent 9e2de85 commit dd4ce54
Show file tree
Hide file tree
Showing 2 changed files with 337 additions and 0 deletions.
1 change: 1 addition & 0 deletions CMakeLists.txt
Expand Up @@ -109,6 +109,7 @@ FW_UTIL(uimage_sgehdr "" "" "${ZLIB_LIBRARIES}")
FW_UTIL(wrt400n src/cyg_crc32.c "" "")
FW_UTIL(xiaomifw "" "" "")
FW_UTIL(xorimage "" "" "")
FW_UTIL(zycast "" "" "")
FW_UTIL(zyimage "" "" "")
FW_UTIL(zytrx "" "" "")
FW_UTIL(zyxbcm "" "" "")
336 changes: 336 additions & 0 deletions src/zycast.c
@@ -0,0 +1,336 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* zycast - push images via multicast to a ZyXEL bootloader
*
* Many ZyXEL devices supports image manipulation using a multicast
* based protocol. The protocol is not documented publically, and
* both the bootloader embedded part and the official clients are
* closed source.
*
* This client is based on the following decsription of the protocol.
* which is reverse engineered from bootloader binaries. It is likely
* to be both incomplete and inaaccurate, as it only covers the
* observed implementation on a limited set of devices. No client
* implementation or network packets were available for the protocol
* reverse engineering.
*
* Protocol description:
*
* UDP to multicast destination address 225.0.0.0 port 6531. Source
* address and port is arbitrary.
*
* Payload is split in packets prepended with a 30 byte header:
*
* 4 byte signature: 'z', 'y', 'x', 0x0 [1]
* 16 bit checksum [2][3]
* 32 bit packet id [2][4]
* 32 bit packet length [2][5]
* 32 bit file length [2][6]
* 32 bit image bitmap [2][7]
* 2 byte ascii country code [8]
* 8 bit flags [9]
* 5 byte reserved [10]
*
* [1] the terminating null is not actually checked by the observed
* implemtations, but is assumed to be safest in case the
* signature is treated as a string
*
* [2] all integers are in network byte order, i.e. big endian
*
* [3] checksum = sum >> 16 + sum, where sum is the sum of all
* payload bytes
*
* [4] starts at 0 and is incremented by 1 for each packet. Used both
* to ensure sequential, loss free, unidirectional transport, and to
* allow the transfer to start at any point. The sequence must be
* repeated until the transfer is complete
*
* [5] Testing indicates that some implementations expect 1024 byte
* packets. Smaller size results in a corrupt download, and larger
* size causes the download to hang - waiting for packet ids which
* does not exist.
*
* [6] the length of each file in case of a multi file transfer.
*
*
* [7] the lower 8 bits is a bitmap of all image types included in the
* transfer. Bits 8 - 16 contains the image type for this packet.
* The purpose of the upper 16 bits is unknown.
*
* The known image types are
*
* 0x01 - "bootbase" (often "Bootloader" partition)
* 0x02 - "rom" (often "data" partition)
* 0x04 - "ras" (often "Kernel" partition)
* 0x08 - "romd" (often "rom-d" partition)
* 0x10 - "backup" (often "Kernel2" partition)
*
* The supported set of images vary among implementations.
* The protocol may support other image types.
*
* WARNING: The location of each supported image type is hard
* coded in the bootloader server implementation. There is no
* relation to the bootloader configuration, and no way to verify
* that those values are correct without decompiling that
* implementations. Device specific bugs are likely, and may
* result in a brick.
*
* [8] two upper case ascii characters, like 'D','E'. The purpose
* is unknown, but ZyXEL devices are often configured with this
* as one of their device specific variables
* [9] bitmap controlling actions taken after a complete transfer:
*
* 0x01 - set DebugFlag
* 0x02 - erase "rom"
* 0x04 - erase "rom-d"
*
* Other, unknown, values may exist in the protocol. Device
* support may vary.
*
* [10] these bytes are not used by the observed implementations.
* The purpose is therefore unknown. There is a risk
* they are interpreted by other devices, resulting in
* unexpected and potentionally harmful behaviour.
*
* Copyright (C) 2023 Bjørn Mork <bjorn@mork.no>
*/

#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <inttypes.h>

/* defaulting to 10 ms interpacket delay */
static int pktdelay = 10000;
static int sockfd = -1;
static int exiting;

/* All integers are stored in network order (big endian) */
struct zycast_t {
uint32_t magic;
uint16_t chksum;
uint32_t pid;
uint32_t plen;
uint32_t flen;
uint16_t unusedbits;
unsigned char type;
unsigned char images;
char cc[2];
unsigned char flags;
char reserved[5];
} __attribute__ ((packed));

#define HDRSIZE (sizeof(struct zycast_t))
#define DEST_ADDR "225.0.0.0"
#define DEST_PORT 5631
#define CHUNK 1024
#define MAGIC 0x7a797800 /* "zyx" */

#define BIT(nr) (1 << (nr))

enum imagetype {
BOOTBASE = 0,
ROM,
RAS,
ROMD,
BACKUP,
_MAX_IMAGETYPE
};

#define FLAG_SET_DEBUG BIT(0)
#define FLAG_ERASE_ROM BIT(1)
#define FLAG_ERASE_ROMD BIT(2)

static void errexit(const char *msg)
{
fprintf(stderr, "ERR: %s: %s\n", msg, errno ? strerror(errno) : "unknown");
exit(EXIT_FAILURE);
}

static void *map_input(const char *name, size_t *len)
{
struct stat stat;
void *mapped;
int fd;

fd = open(name, O_RDONLY);
if (fd < 0)
return NULL;
if (fstat(fd, &stat) < 0) {
close(fd);
return NULL;
}
*len = stat.st_size;
mapped = mmap(NULL, stat.st_size, PROT_READ, MAP_SHARED, fd, 0);
if (close(fd) < 0) {
(void) munmap(mapped, stat.st_size);
return NULL;
}
return mapped;
}

static uint16_t chksum(uint8_t *p, size_t len)
{
int i;
uint32_t sum = 0;

for (i = 0; i < len; i++)
sum += *p++;
return (uint16_t)((uint32_t)(sum >> 16) + sum);
}

static int pushimage(void *file, struct zycast_t *phdr)
{
uint32_t count = 0;
uint32_t len = ntohl(phdr->flen);
uint32_t plen = CHUNK;

while (!exiting && len > 0) {
if (len < CHUNK)
plen = len;
phdr->plen = htonl(plen);
phdr->pid = htonl(count++);
phdr->chksum = htons(chksum(file, plen));
if (send(sockfd, phdr, HDRSIZE, MSG_MORE | MSG_DONTROUTE) < 0)
errexit("send(phdr)");
if (send(sockfd, file, plen, MSG_DONTROUTE) < 0)
errexit("send(payload)");
file += plen;
len -= plen;

/* No need to kill the network. The target can't
* process packets as fast as we send them anyway.
*/
usleep(pktdelay);
}
return 0;
}

static void sig_handler(int signo)
{
if (signo == SIGINT)
exiting = 1;
}

static void usage(const char *name)
{
fprintf(stderr, "Usage:\n");
fprintf(stderr, " %s [options]\n", name);
fprintf(stderr, "Options:\n");
fprintf(stderr, "\t-i interface outgoing interface for multicast packets\n");
fprintf(stderr, "\t-t delay interpacket delay in milliseconds\n");
fprintf(stderr, "\t-f rasimage primary firmware image\n");
fprintf(stderr, "\t-b backupimage secondary fimrware image (if supported)\n");
fprintf(stderr, "\t-d rom data for the \"rom\" or \"data\" partition\n");
fprintf(stderr, "\t-r romd data for the \"rom-d\" partition\n");
#ifdef DO_BOOTBASE
fprintf(stderr, "\t-u bootloader flash new bootloader\n");
fprintf(stderr, "\nWARNING: bootloader upgrades are dangerous. DON'T DO IT!\n");
#endif
fprintf(stderr, "\nNOTE: some bootloaders will flash a rasimage to both primary and\n");
fprintf(stderr, "secondary firmare partitions\n");
fprintf(stderr, "\nExample:\n");
fprintf(stderr, " %s -i eth1 -t 20 -f openwrt-initramfs.bin\n\n", name);
if (sockfd >= 0)
close(sockfd);
exit(EXIT_FAILURE);
}

#define ADD_IMAGE(nr) \
hdr.images |= BIT(nr); \
file[nr] = map_input(optarg, &len[nr]); \
if (!file[nr]) \
errexit(optarg)

int main(int argc, char **argv)
{
void *file[_MAX_IMAGETYPE] = {};
size_t len[_MAX_IMAGETYPE] = {};
struct zycast_t hdr = {
.magic = htonl(MAGIC),
.cc = {'F', 'F' },
.flags = FLAG_SET_DEBUG,
};
const struct sockaddr_in dest = {
.sin_family = AF_INET,
.sin_addr.s_addr = inet_addr(DEST_ADDR),
.sin_port = htons(DEST_PORT),
};
int i, c;

if (signal(SIGINT, sig_handler) == SIG_ERR)
errexit("signal()");
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
errexit("socket()");
if (connect(sockfd, (struct sockaddr *)&dest, sizeof(dest)) < 0)
errexit("connect()");

while ((c = getopt(argc, argv, "i:t:f:b:d:r:u:")) != -1) {
switch (c) {
case 'i':
if (setsockopt(sockfd, SOL_SOCKET, SO_BINDTODEVICE, optarg, strlen(optarg)) < 0)
errexit(optarg);
break;
case 't':
i = strtoul(optarg, NULL, 0);
if (i < 1)
i = 1;
pktdelay = i * 1000;
break;
case 'f':
ADD_IMAGE(RAS);
break;
case 'b':
ADD_IMAGE(BACKUP);
break;
case 'd':
ADD_IMAGE(ROM);
break;
case 'r':
ADD_IMAGE(ROMD);
break;
case 'u':
#ifdef DO_BOOTBASE
ADD_IMAGE(BOOTBASE);
break;
#endif
default:
usage(argv[0]);
}
}

if (!hdr.images)
usage(argv[0]);

fprintf(stderr, "Press Ctrl+C to stop before rebooting target after upgrade\n");
while (!exiting) {
for (i = 0; i < _MAX_IMAGETYPE; i++) {
if (hdr.images & BIT(i)) {
hdr.type = BIT(i);
hdr.flen = htonl(len[i]);
pushimage(file[i], &hdr);
}
}
};

fprintf(stderr, "\nClosing all files\n");
if (sockfd >= 0)
close(sockfd);
for (i = 0; i < _MAX_IMAGETYPE; i++)
if (hdr.images & BIT(i))
munmap(file[i], len[i]);

return EXIT_SUCCESS;
}

2 comments on commit dd4ce54

@lynxthecat
Copy link

@lynxthecat lynxthecat commented on dd4ce54 Aug 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent! Is there an option for just reset without any flashing?

@walter-ve
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For info; below the output of a boot of Zyxel NWA55AXE; here I don't see a "Multiboot Listening..." so I guess this solution wouldn't work...
And since I easily brick this one, it would be handy ;-).

Boot process

U-Boot SPL 2018.09 (Jan 22 2021 - 07:42:43 +0000)
Trying to boot from NAND

Initializing NMBM ...
Signature found at block 1023 [0x07fe0000]
First info table with writecount 0 found in block 960
Second info table with writecount 0 found in block 963
NMBM has been successfully attached

U-Boot 2018.09 (Jan 22 2021 - 07:42:43 +0000)

CPU: MediaTek MT7621AT ver 1, eco 3
Clocks: CPU: 880MHz, DDR: 600MHz (1200MT/s), Bus: 220MHz, XTAL: 40MHz
Model: MediaTek MT7621 reference board (NAND)
DRAM: 256 MiB
NAND: 128 MiB

Initializing NMBM ...
Signature found at block 1023 [0x07fe0000]
First info table with writecount 0 found in block 960
Second info table with writecount 0 found in block 963
NMBM has been successfully attached

Loading Environment from NMBM... *** Warning - bad CRC, using default environment

In: uartlite0@1e000c00
Out: uartlite0@1e000c00
Err: uartlite0@1e000c00
Net:
Warning: eth@1e100000 (eth0) using random MAC address - fa:26:d7:78:e2:6c
eth0: eth@1e100000
Reading from 0x7700000, size 0x20000
Succeeded
Zyxel version:V1.03
gpio: pin 6 (gpio 6) value is 1
gpio: pin 24 (gpio 24) value is 0
gpio: pin 24 (gpio 24) value is 1
Hit any key to stop autoboot: 1 ��� 0
Loading FIT image at offset 0x180000 to memory 0x83000000, size 0x2953cc ...
Automatic boot of image at addr 0x83000000 ...

Loading kernel from FIT Image at 83000000 ...

Using 'config-1' configuration
Trying 'kernel-1' kernel subimage
Description: MIPS OpenWrt Linux-5.10.176
Type: Kernel Image
Compression: lzma compressed
Data Start: 0x830000e4
Data Size: 2696125 Bytes = 2.6 MiB
Architecture: MIPS
OS: Linux
Load Address: 0x80001000
Entry Point: 0x80001000
Hash algo: crc32
Hash value: dbbf60b7
Hash algo: sha1
Hash value: 8c63544cbc9f573caf84ac53eef21a683cead647
Verifying Hash Integrity ... crc32+ sha1+ OK

Loading fdt from FIT Image at 83000000 ...

Using 'config-1' configuration
Trying 'fdt-1' fdt subimage
Description: MIPS OpenWrt zyxel_nwa55axe device tree blob
Type: Flat Device Tree
Compression: uncompressed
Data Start: 0x832925e4
Data Size: 10401 Bytes = 10.2 KiB
Architecture: MIPS
Hash algo: crc32
Hash value: d80405a2
Hash algo: sha1
Hash value: 9390ff85783bd6ca7e3355afb569cb2a1877918d
Verifying Hash Integrity ... crc32+ sha1+ OK
Booting using the fdt blob at 0x832925e4
Uncompressing Kernel Image ... OK
Loading Device Tree to 8fe68000, end 8fe6d8a0 ... OK
[ 0.000000] Linux version 5.10.176 (builder@buildhost) (mipsel-openwrt-linux-musl-gcc (OpenWrt GCC 11.2.0 r20134-5f15225c1e) 11.2.0, GNU ld (GNU Binutils) 2.37) #0 SMP Thu Apr 27 20:28:15 2023
[ 0.000000] SoC Type: MediaTek MT7621 ver:1 eco:3
[ 0.000000] printk: bootconsole [early0] enabled
[ 0.000000] CPU0 revision is: 0001992f (MIPS 1004Kc)
[ 0.000000] MIPS: machine is ZyXEL NWA55AXE
Press the [f] key and hit [enter] to enter failsafe mode
Press the [1], [2], [3] or [4] key and hit [enter] to select the debug level
Please press Enter to activate this console.

Please sign in to comment.