TrendNet switch
I was doom-scrolling vendor websites when I noticed the Trendnet TEG switches. They let you download the firmware. So I grabbed one, and the first thing I saw:
Web Smart L2+ Firmware Release Notes
Model: TEG-204WS, TEG-284WS, TEG-524WS, TPE-082WS, TPE-204US, TPE-5028WS, TPE-5048WS, TPE-5240WS, TPE-1620WSF, TPE-1021WS
H/W Version: v1.0R
Model: TEG-082WS, TPE-1620WS, TPE-2840WS
H/W Version: v2.0R
But, the files are encrypted.

A bit of Googling suggests something called aescrypt. From what I can tell, it’s a commercially licensed library. That would neatly explain why nothing extracts with unblob and why the entropy is at 1. A closer look shows the .hex file is really just two AESCRYPT blobs concatenated together, with some headers and metadata glued on for good measure.
So, with that avenue closed, I go back to the upgrade text file—mostly out of routine. And there it is. A detail worth paying attention to:
Release Date: 05/2025
If upgrading from firmware version 1.00.010, firmware version
1.10.026 must be loaded first, then firmware version 2.10.024
afterwards. (Additional firmware files are included in the
firmware v3.01.012 download.)
That’s interesting. If there’s a mandatory upgrade chain, then somewhere along the line there has to be material that allows later firmware to be decrypted. Keys don’t usually appear by magic.
So I pull down firmware 2.10.024. As expected, it’s completely unencrypted. No obfuscation, no theatrics. Once unpacked, it’s the usual lineup: a Linux kernel, busybox utilities, a standard filesystem. MIPS MSB, 32-bit. Nothing exotic.
The /etc/rc script is almost offensively minimal:
# start application
#
if test -f "/usr/bin/ISS.exe"; then
cd /usr/bin
./ISS.exe
fi
Everything funnels straight into ISS.exe, so let’s open it and look for something—anything—related to aescrypt. And yes, buried in there, we find a function named decrypt_stream, complete with sanity checks on the aescrypt header:

And who calls that function? CmFWImgDecryptHandler. Yes. Literally “firmware
image decrypt handler.” Not exactly hiding the ball. This is encouraging. This
is progress. And, against expectations, the key actually turns up. Plain as
day.

At this point, there are options. None of them are especially appealing.
-
Option one: grab aescrypt and try to decrypt the file directly. That goes nowhere. Trendnet has clearly made some small but decisive modifications to the algorithm.
-
Option two: reimplement the logic in Python. In a moment of questionable judgment, I ask ChatGPT to translate the C code. It doesn’t work. I try Claude. Then Mistral. Three translations, three different interpretations, zero working results. Impressive, in its own way.
-
Which leaves option three: let ISS.exe do the job itself.
The plan: use an LD_PRELOAD library to directly call CmFWImgDecryptHandler
and dump the decrypted output. This is the easiest option.
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <string.h>
/* README
* mips-linux-gcc -shared -fPIC lib.c -o preload.so -ldl
* go into the rootfs
* qemu-mips -strace -L . -E LD_PRELOAD=./preload.so ISS.exe ./encrypted.hex
* An encrypted.hex_clear file is created
*/
typedef int (*depack_fn)(void *, int *);
/* We need to get the first arg passed to ISS.exe */
static char *get_filename_from_cmdline(void)
{
int fd = open("/proc/self/cmdline", O_RDONLY);
if (fd < 0)
return NULL;
static char buf[4096];
ssize_t n = read(fd, buf, sizeof(buf) - 1);
close(fd);
if (n <= 0) return NULL;
buf[n] = '\0';
/* cmdline = argv[0]\0argv[1]\0... */
char *p = buf;
p += strlen(p) + 1; // skip argv[0]
if (*p == '\0') return NULL;
return p; // argv[1]
}
__attribute__((constructor))
static void preload_init(void)
{
printf("[preload] Calling constructor\n");
/* symbol depack to solve */
depack_fn depack = (depack_fn)dlsym(RTLD_DEFAULT, "CmFWImgDecryptHandler");
if (!depack) {
fprintf(stderr, "[preload] CmFWImgDecryptHandler not found: %s\n", dlerror());
return;
}
char *filename = get_filename_from_cmdline();
printf("[preload] file: %s\n", filename);
int fd = open(filename, O_RDONLY);
if (fd < 0) {
perror("[preload] open");
return;
}
/* ===== get size with lseek ===== */
int len = lseek(fd, 0, SEEK_END);
if (len <= 0) {
perror("[preload] lseek SEEK_END");
close(fd);
return;
}
lseek(fd, 0, SEEK_SET);
fprintf(stderr, "[preload] malloc(%d)\n", len);
void *buf_in = malloc(len);
if (!buf_in ) {
fprintf(stderr, "[preload] malloc failed\n");
close(fd);
return;
}
if (read(fd, buf_in, len) != (ssize_t)len) {
fprintf(stderr, "[preload] read failed\n");
close(fd);
return;
}
close(fd);
printf("[preload] call depack(%p, %zu)\n",
buf_in, len);
int ret = depack(buf_in, &len);
printf("[preload] CmFWImgDecryptHandler return %d\n", ret);
if (ret == 1) {
/* ===== writing output ===== */
char outname[4096];
snprintf(outname, sizeof(outname), "%s_clear", filename);
int fd_out = open(outname, O_CREAT | O_WRONLY | O_TRUNC, 0644);
if (fd_out < 0) {
perror("[preload] open output");
goto cleanup;
}
if (write(fd_out, buf_in, len) != len) {
perror("[preload] write output");
} else {
printf("[preload] file: %s (%d bytes)\n",
outname, len);
}
close(fd_out);
}
cleanup:
free(buf_in);
/* Better to force exit now instead of running ISS.exe */
_exit(0);
}
Running the code over the two aescrypt blobs gets us what we expected: a kernel and a squashfs. With that in place, the rc script brings up exactly three programs:
-
/usr/bin/cdd — unclear what it does, and not in a hurry to explain itself
-
/usr/bin/system — essentially RCE-as-a-service on localhost (it happily system()s whatever you send to 127.0.0.1:2001)
-
/usr/bin/ISS.exe — the large, all-purpose binary that handles everything else: DHCP, SSH, telnet, web admin, and the rest of the circus
If I had a Trendnet TEG switch on the bench right now, I’d start with something simple. The web interface allows URIs up to 1024 characters, which eventually get copied into a 256-byte stack buffer. No ASLR, no stack cookies. Classic conditions. Odds are good that a sufficiently long URI will bring ISS.exe down—and with a little care, possibly do more than that.
I don’t have a Trendnet switch on hand. At this point, that settles it. I remove the working directory and call it a night.
To die, to sleep. To sleep, perhaps to dream.
Tomorrow can deal with it.