October 31, 2026
I Finally Hacked My ISP Router and Turned It Into a Mini PC — Part 1
It happened again. This is indeed a failed project, but I thought it interesting enough that we need to talk about it…

By hiruthicSha
11 min read
It happened again. This is indeed a failed project, but I thought it interesting enough that we need to talk about it…
After publishing the first article, I kept working on the router.
For days, I tried different approaches. I read firmware dumps, examined binaries, tested commands, rebuilt things, and went back to the serial console. The original goal was still the same: get enough control over this ISP-provided router to run something useful on it, preferably custom firmware.
I didn't get there.
psst… You didn't hear this from me, but here's the secret door 👉 Trust me, bro, no virus here…
At some point, I was ready to write another article explaining everything I had tried and why it hadn't worked. But that didn't feel like a particularly interesting follow-up. It would have been a long list of failed attempts, with no satisfying conclusion.
So, I kept the project around and occasionally returned to it.
Then I stopped returning to it altogether.
I had burned out. There was only so much time I wanted to spend staring at a serial terminal, trying to understand why a command that appeared to succeed had changed absolutely nothing.
Last week, I opened the project again.
That is what happened.
And, as usual, getting there involved considerably more mistakes than the final result suggests. So, you are about to read a lot of unnecessary things just to see me host my portfolio on the router and run some custom C code on it.
Since there was a lot of things I had to go through to do what I did, I'll be making a video content explaining each one in the future: (69) KreativeCows — YouTube. Stay tuned.
1. The original plan was becoming unrealistic
The first thing I had to reconsider was the idea of installing OpenWrt.
Even though I had already established that the board was heavily locked down and offered no straightforward way to install OpenWrt or other custom firmware, I still wasn't entirely convinced. I assumed my earlier conclusion might have been a result of my limited experience rather than an actual limitation of the hardware. So, I decided to revisit the whole process and give it another shot.
The router was running a Realtek Luna SDK build with Linux kernel 2.6.30.9. The SoC was an RTL9602C, with a MIPS-derived CPU, 128 MB of RAM, and 128 MB of SPI NAND flash.
It was a GPON ONT, not a conventional consumer router. What those difference means? IDK. I just know, "GPON. NO GOOD"…
The wireless, Ethernet switching, GPON, and management components were tightly coupled to Realtek's proprietary SDK. Replacing the kernel would mean more than compiling a newer Linux version. I would need CPU support, SoC drivers, switch support, and a working GPON stack.
I looked into the available OpenWrt and mainline work. The newer RTL9607C work was interesting, but it was a different SoC and explicitly did not cover the Lexra-derived RTL9602C.
I couldn't find a public source drop for the exact Luna SDK and kernel combination on this device either.
The sensible option was to stop trying to replace everything and instead, I would keep the vendor kernel and replace the userland.
In other words, I wanted to use the router's existing Linux kernel while building my own minimal Linux environment on top of it.
It wasn't the original plan. But the original plan was anyways a long shot.
If you want to see what I eventually built without following every late-night crying, feel free to jump ahead:
Medium Edit description
I experimented with hosting a website, building a device-monitoring dashboard, setting up DNS-based ad blocking, bringing up Wi-Fi, and even attempting per-device bandwidth control.
Some worked surprisingly well. Others reminded me why embedded hardware can be so frustrating.
If you're interested in how I got there, though, stick around. The details are where most of the interesting discoveries happened.
2. Getting my own Linux binaries to run
The first obstacle was the CPU instruction set.
Buildroot is a tool that builds minimal Linux systems for embedded devices by compiling the required programs and libraries for the target processor.
I had Buildroot producing BusyBox and Dropbear binaries. The filenames even suggested that they were MIPS-I builds.
They weren't.
Buildroot 2024.02 didn't have the MIPS-I configuration option I initially expected. My configuration was silently using MIPS32. The binaries contained instructions that the router's CPU didn't support.
I need to probe the CPU to find the right configuration…
I wrote a small Python script that assembled individual MIPS instructions into tiny ELF executables. Each executable would run one instruction and exit. If the instruction wasn't supported, the process would terminate with an illegal-instruction error.
Here's what the Python script does:
- Lists the instructions to test. The
TESTSdictionary contains MIPS instructions such asnop,mul, andclz. Each is represented by a hexadecimal machine-code value. - Builds tiny executables. For each instruction, the
build()function creates a minimal Linux executable (ELF). It contains the instruction being tested, followed by commands to exit. - Transfers them to the router. The script converts each executable into bytes and sends it through the USB-TTL serial connection. The files are stored in
/home, a temporary RAM filesystem. - Executes them on the router. Each binary runs directly on the RTL9602C processor. If the instruction is supported, it completes normally. If not, Linux terminates it with an illegal-instruction signal.
- Records the results. The script prints each program's exit code. An exit code of
132indicatesSIGILL, an illegal instruction.
The mul and clz instructions caused an illegal-instruction exception. The other tested instructions executed.
The fix wasn't simply adding -march=mips1 to the optimization flags. I had already tried that, and the resulting binaries still contained unsupported instructions because parts of the toolchain, including libgcc, were built for a newer architecture.
I had to modify Buildroot's generated configuration after make defconfig, explicitly set the GCC target architecture to MIPS-I, and rebuild the toolchain itself.
BTW, do I know what it all means? a lil bit. Do I know if this is the right path? IDK. It's just googling and lot of overnight LLM consultation.
As usual, you readers know far more about this than I do, so, please help guide this poor soul in the right direction.🙏
The relevant configuration change was:
sed -i \
's/BR2_GCC_TARGET_ARCH="mips32"/BR2_GCC_TARGET_ARCH="mips1"/' \
.configsed -i \
's/BR2_GCC_TARGET_ARCH="mips32"/BR2_GCC_TARGET_ARCH="mips1"/' \
.configAfter rebuilding, I checked the resulting binaries with the cross-compiler's objdump.
The architecture was reported as: mips:3000 . Which seems to be the right thing. The rebuilt BusyBox was approximately 1.27 MB. Dropbear was around 633 KB.
3. Serial was not going to be a practical file-transfer protocol
The next problem was getting those binaries onto the router.
At this point, I was still using the USB-TTL connection for most of the work. The serial console was running at 115200 baud.
That is fine for commands and logs. It is not particularly fast when you need to transfer a 1.2 MB executable.
To put that in perspective,
At 115200 baud, the theoretical maximum transfer rate is lower than 115,200 bytes per second. The UART was configured for 8N1: each byte requires 10 transmitted bits (1 start bit, 8 data bits, 1 stop bit).
So, the maximum data rate is:
For a 1.2 MB executable:
That's approximately 1 minute and 44 seconds, assuming a perfect, uninterrupted transfer with no overhead.
The actual BusyBox binary we transferred was 1,275,160 bytes:
So even under ideal conditions, it would take around 1 minute and 51 seconds.
And that was the best-case estimate.
My first approach used shell commands that wrote escaped bytes into files:
echo -ne '\000\177\003...' >> /home/busyboxecho -ne '\000\177\003...' >> /home/busyboxIt worked for small files. For larger files, it was painfully slow. Long command lines could overflow the terminal, and an incomplete quote could leave the shell waiting at a continuation prompt.
I tried a raw serial upload using cat, but my upload script had another problem: it matched its own echoed end marker and mistook that for the router's response. It then sent more commands while cat was still receiving the binary.
The resulting file was corrupted.
I suspected the router was hanging or that its watchdog was resetting it. I ran a monitoring script that checked /proc/uptime every ten seconds.
The shell stayed alive for 21 minutes.
The watchdog theory was wrong. The upload automation was the problem.
It would take hours to transfer a 1.2 MB file. Just trust and don't ask me how I know.😑
Ethernet to Clutch
Eventually, I used the Ethernet connection instead.
The router's U-Boot had a TFTP client. Once that was sorted, the router could fetch files over the network.
The transfer was substantially faster (285x faster) than the serial approach, in just 1.2 seconds. It also gave me a way to test images without writing them to flash.
That was an important change in the workflow where I could just stage an image in RAM, verify it, and decide what to do next without permanently commit to flashing and potentially breaking the router.
4. My own BusyBox finally ran on the router
Once the toolchain was fixed, I uploaded the rebuilt BusyBox and Dropbear binaries.
I also verified their checksums against the original PC files.
The router was now running my own BusyBox build, with 278 applets, including the commands I needed for basic administration.
There was still a limitation: the root filesystem was read-only, and the stock kernel didn't support ext2, ext3, or ext4.
Fortunately, the kernel supported RAM filesystems.
I could mount a writable directory: mount -t ramfs ramfs /home. And put my own binaries there.
The router could now run my own programs without replacing the kernel.
5. The flash experiment: a custom root filesystem in slot 0
At this point, I had a working custom userland, but it existed only in RAM.
I wanted to make it survive a reboot.
The router used an A/B firmware layout. Slot 1 was the stock firmware and was the default boot target. Slot 0 was the inactive side.
I decided to use only the inactive slot's root filesystem partition.
Before touching it, I read the existing partition back through U-Boot and compared its CRC32 against my earlier dump and the original data matched.
I also backed up the bootloader using a raw SPI NAND read. There was a useful lesson here: the first bootloader read used the normal ECC-aware command and produced a matching transfer CRC, but the contents were garbage.
A second read using spi_nand read.raw produced recognisable MIPS code and Realtek strings.
This produced a different dump. Its CRC32 matched on both the router and PC (3d3e086d), and inspecting the binary revealed recognisable MIPS instructions and Realtek strings, including RTL9602C and rsdk-1.5.6–5281.
The first custom root filesystem booted far enough to mount, but then PID 1 died.
The kernel printed:
I initially suspected the init script.
After testing the individual commands manually, I found that the problem was more subtle. The kernel could run our BusyBox binary as PID 1, but it wasn't successfully starting the shell script I had supplied as /init.
I changed the approach and moved the initial setup into the normal init sequence.
That still wasn't the end of it.
The next cold boots intermittently produced: Bus error
The strange part was that reading the BusyBox file with cat worked, and its MD5 matched. The binary itself wasn't corrupted.
The problem appeared when the kernel loaded executable pages on demand from the SquashFS image.
My first image used 128 KiB SquashFS blocks. I rebuilt it with 4 KiB blocks, matching the kernel's page size.
The new image was slightly larger, but the cold-boot behaviour changed.
I tested the new image across four cold boots: one manual boot followed by three automated tests. All four completed without Bus errors, kernel panics, or mount/mkdir errors.
The change that fixed the instability was reducing the SquashFS block size from 128 KiB to 4 KiB, matching the kernel's page size. The earlier image intermittently produced Bus errors when executable pages were loaded from flash.
I then verified that the stock slot-1 kernel, stock root filesystem, environment partitions, and raw bootloader read still matched their original CRCs.
Only the slot-0 root filesystem had been changed.
The saved boot environment still pointed to stock slot 1. That meant the router would continue to boot its original firmware normally. To boot my custom userland, I had to interrupt U-Boot and issue a temporary command sequence:
setenv bootargs console=ttyS0,115200 root=/dev/mtdblock5 rootfstype=squashfs mtdparts=physmap-flash.0 panic=1
spi_nand read 0x83000000 3b00000 800000
bootm 0x83000000setenv bootargs console=ttyS0,115200 root=/dev/mtdblock5 rootfstype=squashfs mtdparts=physmap-flash.0 panic=1
spi_nand read 0x83000000 3b00000 800000
bootm 0x83000000- Sets the kernel boot arguments to boot from slot 0 and restart 1 second after a kernel panic
- Reads the stock kernel from slot 1, copying
0x800000bytes from flash offset0x3b00000into RAM address0x83000000, without modifying flash. - Instructs Uboot to execute the kernel image loaded at the address
0x83000000
6. The LAN port was up, but it wasn't actually working
Getting the custom root filesystem to boot reliably was one problem. Getting its network interface to work was another.
The kernel created interfaces such as:
eth0
eth0.2
eth0.3
wlan0eth0
eth0.2
eth0.3
wlan0But from the custom shell, the LAN interface didn't have a working link.
I could bring the Linux interface using ifconfig,
ifconfig eth0 up
ifconfig eth0.2 upifconfig eth0 up
ifconfig eth0.2 upBut the interface still reported that the link wasn't ready.
Then I say vendor's network-related kernel modules 😀. I loaded that, but it didn't work😑.
I also investigated the stock boot sequence. The vendor's scripts did a lot of things: start configuration daemons, initialize the Realtek gateway stack, configure bridges, initialize GPON components, and bring up the physical ports.
I didn't want to reproduce the entire stock startup process just to get a basic LAN connection.
The useful clue was in the stock init scripts.
One of them, rc35, ran this command: /bin/diag port set phy-force-power-down port all state disable
It looked like the command was related to enabling the physical Ethernet ports.
I tried it from the custom environment. But diag was a dynamically linked vendor binary, so it needed the original libraries and filesystem layout.
The solution was to mount the stock root filesystem read-only and run the command inside a chroot.
There was another quirk: diag would spin indefinitely if its standard input was left attached to the terminal. It needed its input closed.
The working invocation looked like this:
chroot /tmp/stock /bin/diag \
port set phy-force-power-down port all state disable \
< /dev/null > /tmp/diag.out 2>&1 &chroot /tmp/stock /bin/diag \
port set phy-force-power-down port all state disable \
< /dev/null > /tmp/diag.out 2>&1 &After that, I brought up the interface and requested a DHCP lease:
ifconfig eth0 up
ifconfig eth0.2 down
ifconfig eth0.2 hw ether 02:96:02:c0:ff:ee
ifconfig eth0.2 up
udhcpc -i eth0.2 -n -q -s /tmp/udhcpc.scriptifconfig eth0 up
ifconfig eth0.2 down
ifconfig eth0.2 hw ether 02:96:02:c0:ff:ee
ifconfig eth0.2 up
udhcpc -i eth0.2 -n -q -s /tmp/udhcpc.scriptThe result was what I had been trying to get:
lease of 192.168.31.173 obtained from 192.168.31.1lease of 192.168.31.173 obtained from 192.168.31.1The router could ping the gateway. The laptop could SSH into the custom userland.
No configd, no full Realtek gateway startup chain, and no replacement kernel. The missing piece for basic LAN connectivity had been the PHY power command.
I added the working sequence to /etc/rc.net, tested it live, and then included it in the next slot-0 image.
On the next boot, the custom userland brought up LAN1 and SSH by itself.
And that's where I'll leave Part 1. The original draft was creeping past a 20-minute read, and I wasn't about to make you sit through an entire documentary about one router. So, I've split the adventure in two. The fun stuff starts in Part 2: Continue reading here.
Stay Curious. Adios 👋