// 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 publicly, and * both the bootloader embedded part and the official clients are * closed source. * * This client is based on the following description of the protocol. * which is reverse engineered from bootloader binaries. It is likely * to be both incomplete and inaccurate, 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 5631. 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 * implementations, 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 flash offset 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 potentially harmful behaviour. * * Copyright (C) 2024 Bjørn Mork */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* defaulting to 10 ms interpacket delay */ static int pktdelay = 10000; static int sockfd = -1; static bool 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)((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 = true; } 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 firmware 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 firmware 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) \ do { \ hdr.images |= BIT(nr); \ file[nr] = map_input(optarg, &len[nr]); \ if (!file[nr]) \ errexit(optarg); \ } while (0) 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; }