Chris' Blog 2026-03-02T09:37:04+00:00 https://shadow578.github.io Chris / shadow578 Repairing a Roborock S5 Max Robot Vacuum 2026-01-25T00:00:00+00:00 https://shadow578.github.io/2026/01/25/roborock-s5-max <p>so, a few weeks ago my sister bought a used Roborock S5 Max robot vacuum cleaner. it worked perfectly - for a few days. then it started to have problems with navigation, simply turning in circles in open spaces.</p> <table> <thead> <tr> <th><img src="/assets/images/common/reactions/homer-spinning.gif" alt="Robot Activities (symbolic)" /></th> </tr> </thead> <tbody> <tr> <td>Robot Activities (symbolic)</td> </tr> </tbody> </table> <p>after some failed troubleshooting attempts (updating firmware, cleaning sensors, etc) she decided to ask me for help.</p> <h2 id="basic-troubleshooting">Basic Troubleshooting</h2> <h3 id="back-to-the-original-firmware">Back to the original Firmware</h3> <p>of curse, my first step was to reset the robot to factory settings and firmware version. after all, this is a chinese product, so who knows what kind of firmware update they may have pushed (after the warranty period, of course).</p> <p>to do this:</p> <ul> <li>press the home button for 5 seconds</li> <li>the “reset” button (located under the top cover), while keeping the home button pressed</li> <li>release the “reset” button, keeping the home button pressed for another 5 seconds</li> </ul> <p>(see <a href="https://support.roborock.com/hc/en-us/articles/360035372632-How-to-reset-Roborock-to-factory-default">this guide</a>)</p> <p>sadly, this did not help</p> <h3 id="checking-general-motion-and-lidar-mapping-ability">Checking general motion and LIDAR mapping ability</h3> <p>the robot offers a joystick mode for manual movement. in this mode, the robot will also use the LIDAR to map the environment.</p> <p>i’ve driven the robot around the room using the joystick mode, and observed the LIDAR map being built in real-time in the app. everything worked perfectly, and the map looked good.</p> <p>sadly, as soon as i commanded the robot to move autonomously, it started to spin in circles again.</p> <p>to me, this suggested like either a software issue - which is unlikely, given the firmware reset - or a hardware issue related to the movement sensor or feedback.</p> <h2 id="entering-the-bit-mode">Entering the BIT Mode</h2> <p>Roborock vacuums have a hidden BIT (Built-In Test) mode, which (on this model) can be accessed as follows:</p> <ul> <li>power off the robot</li> <li>press and hold the power button</li> <li>while holding the power button, press the home button 5 times</li> <li>release the power button</li> </ul> <p>from here, you can cycle through various tests using the home and power buttons. these tests range from simple switch tests - indicating whether e.g. the bumper inputs are pressed using the leds, to more complex tests like motor movement and LIDAR operation.</p> <p>more details on the BIT mode can be found at various places online. here’s some i found useful:</p> <ul> <li>https://www.youtube.com/watch?v=c6mxG5HYA70</li> <li>https://www.youtube.com/watch?v=Fcea7dd7DFI</li> <li>https://www.youtube.com/watch?v=Iq74XT-jIDw</li> <li>https://cleanerstalk.com/roborock-error-codes/#t-1649313234742 (not in my case, but good to know)</li> </ul> <h3 id="results">Results</h3> <p>after running all tests and - suprisingly - all passed. well, that’s unexpected.</p> <p>weirdly enough, running the tests on a different day, i’ve finally got a failed test: the robot reports an error with the left wheel movement test.</p> <p>this would make sense, an encoder on the motor failing would cause the robot to see weird feedback when moving autonomously, certainly enough to throw off its navigation algorithms. with the test only failing intermittently, i’d also suspect something simpler - a loose connector, dirty contacts, or corrosion on the encoder itself.</p> <h2 id="disassembly-and-repair">Disassembly and Repair</h2> <p>disassembling the Roborock S5 Max is relatively straightforward, although a bit tedious. i’ve used this video teardown as reference, though i didn’t follow every step exactly: https://www.youtube.com/watch?v=0vLa4-iikzM</p> <p>after removing the left wheel assembly, i could inspect the motor and encoder. and sure enough, there was something wrong with it, though not what i expected.</p> <table> <thead> <tr> <th><img src="/assets/images/repairs/roborock/cable-wheel-l.jpg" alt="Left Wheel Motor Assembly" /></th> </tr> </thead> <tbody> <tr> <td>Left Wheel Motor Assembly</td> </tr> </tbody> </table> <p>as you can see, two of the wires going to the motor assembly were broken. one wire was completely severed, while the other was only partially broken.</p> <p>taking a closer look and, sure enough, the wires were wired to the motor encoder. so the robot could move, but had no feedback from the left wheel encoder, causing the freak out during autonomous navigation.</p> <h3 id="the-repair">The Repair</h3> <p>since the wires are at a point where they flex a lot, i’ve decided to replace the wires from the motor assembly PCB to the mainboard connector. that way, no stress would be applied to the solder joints splicing the wires together.</p> <p>sadly, i didn’t take pictures of the repair, but imagine two wires soldered together. i’m sure you can picture it.</p> <p>since i had the robot disassembled, i also took the opportunity to clean all the dust and dirt from the internals. i also added some fresh grease to the motor gearboxes, since they looked a bit dry.</p> <p>that’s it, after reassembling the robot, it works perfectly again.</p> <h2 id="notes">Notes</h2> <p>since originally writing this post, the robot has been working perfectly. i also ended up getting my hands on a Roborock S7 with the exact same issue, and the same repair process applied as well. so it seems like this is a common issue with roborock vacuums using this kind of motor assembly / chassis.</p> <p>since the fault results in a very specific symptoms, it should be easy to remote-diagnose this issue by checking:</p> <ul> <li>does the robot exit the charging dock correctly? -&gt; want <strong>yes</strong> <ul> <li>the initial movement out of the dock is done without using the wheel encoders, so if this works then the motors and drivers are fine.</li> </ul> </li> <li>does mapping work? -&gt; want <strong>yes</strong> <ul> <li>if this works then the LIDAR and mainboard are fine.</li> </ul> </li> <li>does the robot stop when bumpers are actuated? -&gt; want <strong>yes</strong> <ul> <li>if this works then the bumper sensors are fine.</li> </ul> </li> <li>does the robot fail to navigate autonomously (going in circles)? -&gt; want <strong>no</strong> <ul> <li>the main symptom described in this post. if <strong>only</strong> this happens, then the issue is likely with the wheel encoder feedback.</li> </ul> </li> <li>does the robot have (about) 800-1000 hours of use? -&gt; want <strong>yes</strong> <ul> <li>this is the lifespan i’ve observed (sample-size of only 2!).</li> </ul> </li> </ul> <p>if you have (or for the more adventurous, want to buy) a roborock vacuum with these symptoms, you can be fairly confident that the issue is a fairly simple repair.</p> <p>as a side note, a complete stop in movement in one of the wheels could also be caused by broken wires, in that case the robot would fail to exit the dock as well. however, that symptom could also be caused by a failed motor driver, so it would be less conclusive than the symptoms described above.</p> On the T45 2025-10-18T00:00:00+00:00 https://shadow578.github.io/2025/10/18/on-the-t45 <p>as already rumored, the T45 is basically a beefed-up T40, with some parts substituted for (likely) cheaper alternatives. i wanted to check that myself, so i decided ask on the OpenWrt forum if anyone had a T45 they could provide flash dumps from.</p> <h1 id="acquiring-dumps">Acquiring Dumps</h1> <p>to figure out the difference, i knew i needed the following:</p> <ul> <li>at least the first 256 bytes of the spi flash, containing PBI and RCW</li> <li>the kernel and device tree blob, on watchguard firmwares usually bundled in a u-boot .itb file</li> <li>output of <code class="language-plaintext highlighter-rouge">mdio list</code> from u-boot, to double-check the PHY configuration</li> </ul> <p>assuming watchguard doesn’t do some crazy initialization in either u-boot or the linux kernel itself, that should be enough information to figure out the hardware differences.</p> <p>the very kind user @hmartin on the OpenWrt forum provided me with the first and third items. as for the kernel and device tree, well… it turns out you can just extract them from the official firmware update files. simply download the update image for use from the web-ui, and open it with a tool like 7-zip. nothing special there.</p> <p>sadly (or luckily, if you’re watchguard), there’s some checks in the firmware updater that prevent us from using a modified update image.</p> <h1 id="differences-and-findings">Differences and Findings</h1> <p>first of, i also ask for the same information from a T40, just to be sure that <em>my</em> T40 matches what others have. luckily, it does.</p> <p>now, let’s get to the differences between the T40 and T45:</p> <ul> <li>the RCW has no significant differences, only some pin muxing changes. and, of course, the PLL ratio of the cpu cores is changed.</li> <li>MDIO assignments as reported by u-boot are identical</li> <li>the LAN1-4 PHYs are likely the same, but the WAN0 PHY differs (Qualcomm AR8035 vs RealTek RTL8211F). Both are RGMII tho, so likely not significant.</li> <li>the device tree is fairly similar, with some differences here and there. i’m not fully sure, but i think that the ethernet PHY configuration should still be close enough to make the T40 dts work on the T45.</li> </ul> <p>i think that you’d just need to add the kernel driver for the Realtek PHY to my <a href="/2025/10/03/linux-on-a-watchguard-t40/">T40 Linux</a> in order to get WAN0 working on the T45.</p> <h2 id="details">Details</h2> <ul> <li>RCW: <ul> <li>CGA_PLL1_RAT: 10:1 -&gt; 16:1 (1GHz -&gt; 1.6GHz CPU)</li> <li>UART_BASE: 0x07 -&gt; 0x06: UART pinmux differs</li> <li>SDHC_BASE: 0x01 -&gt; 0x00: SDHC pinmux differs</li> <li>SPI_BASE: 0x02 -&gt; 0x01: SPI pinmux differs</li> <li>IFC_GRP_E1_EXT: 0x00 -&gt; 0x02: IFC pinmux differs</li> <li>IFC_GRP_E1_BASE: 0x01 -&gt; 0x00: IFC pinmux differs</li> <li>IIC2_EXT: 0x02 -&gt; 0x00: IIC2 Pinmux differs</li> </ul> </li> <li>Device Tree: <ul> <li><code class="language-plaintext highlighter-rouge">esdhc@1560000 is enabled</code>, <code class="language-plaintext highlighter-rouge">bus-width</code> is changed to 8 bits and <code class="language-plaintext highlighter-rouge">cap-mmc-highspeed</code> is added</li> <li><code class="language-plaintext highlighter-rouge">tpm@29</code> (atmel,at97sc3204t) node is replaced by <code class="language-plaintext highlighter-rouge">tpm@2e</code> (tcg,tpm)</li> <li>PWM controller <code class="language-plaintext highlighter-rouge">pwm0@2a30000</code> is added on T45</li> <li><code class="language-plaintext highlighter-rouge">compatible = "ethernet-phy-id001c.c984", "ethernet-phy-ieee802.3-c22"</code> and <code class="language-plaintext highlighter-rouge">compatible = "ethernet-phy-id001c.c916";</code> are added to PHY nodes on T45, absent on T40</li> </ul> </li> </ul> <details> <summary>RCW Dump Output for T40 and T45</summary> T40: <pre> -- System Info -- Flash dump: t45\t40_flashstart_mm.bin Initial QSPI endianess: QWORD_LITTLE_ENDIAN Assumed SOC Model: LS1043A -- PBI Parser Messages --- ALTCBAR set to 00002200 by PBI write ALTCBAR set to 00000300 by PBI write QSPI Endianess set to DWORD_LITTLE_ENDIAN due to QUADSPI_MCR write -- RAW PBI Frames -- WRITE @ DCFG_CCSR_RCWSR0 (0x01EE0100): 0610000a0a0000000000000000000000455800020000001240044000c10020000000000000000000000000000003fffe200045040418320a0000009600000001 (CCSR) WRITE @ SCFG_QSPI_CFG (0x0157015C): 40100000 (CCSR) WRITE @ SCFG_SCRATCHRW0 (0x01570600): 00000000 (CCSR) WRITE @ SCFG_SCRATCHRW1 (0x01570604): 40100000 (CCSR) WRITE @ LNDSSCR1 (0x01EA08DC): 00502880 (CCSR) WRITE @ CCI_UNKN_BARRIER_DISABLE (0x01570178): 0000e010 (CCSR) WRITE @ CCI_400_Control_Override (0x01180000): 00000008 (CCSR) WRITE @ SCFG_USB_REFCLK_SELCR1 (0x01570418): 0000009e (CCSR) WRITE @ SCFG_USB_REFCLK_SELCR2 (0x0157041C): 0000009e (CCSR) WRITE @ SCFG_USB_REFCLK_SELCR3 (0x01570420): 0000009e (CCSR) WRITE @ DCFG_CCSR_RSTRQMR1 (0x01EE00C0): 00004400 (CCSR) WRITE @ SCFG_ALTCBAR (0x01570158): 00002200 (CCSR) WRITE @ PEX_OUTBOUND_WRITE_HANG_ERRATUM (0x22008040): 00000001 (ACS) WRITE @ LNDSSCR1 (0x01EA08DC): 00502880 (CCSR) WRITE @ SCFG_ALTCBAR (0x01570158): 00000300 (CCSR) WRITE @ PEX1_GEN3_RElATED_OFF_ERRATUM (0x03400890): 01000100 (ACS) WRITE @ PEX2_GEN3_RElATED_OFF_ERRATUM (0x03500890): 01000100 (ACS) WRITE @ PEX3_GEN3_RElATED_OFF_ERRATUM (0x03600890): 01000100 (ACS) WRITE @ QUADSPI_MCR (0x01550000): 000f400c (CCSR) COMMAND @ PBI_CRC: 0x06DE05A9 (current) == 0x06DE05A9 (calculated) -&gt; PASS -- RCW Fields -- SYS_PLL_CFG: 0x0 | 0 | 0b0 | OK SYS_PLL_RAT: 0x3 | 3 | 0b11 | 3:1 MEM_PLL_CFG: 0x0 | 0 | 0b0 | OK MEM_PLL_RAT: 0x10 | 16 | 0b10000 | 16:1 CGA_PLL1_CFG: 0x0 | 0 | 0b0 | OK CGA_PLL1_RAT: 0xA | 10 | 0b1010 | 10:1 CGA_PLL2_CFG: 0x0 | 0 | 0b0 | OK CGA_PLL2_RAT: 0xA | 10 | 0b1010 | 10:1 C1_PLL_SEL: 0x0 | 0 | 0b0 | CGA_PLL1 /1 SRDS_PRTCL_S1: 0x4558 | 17752 | 0b100010101011000 | FM1_MAC_RAT: 0x1 | 1 | 0b1 | SRDS_PLL_REF_CLK_SEL_S1: 0x0 | 0 | 0b0 | HDLC1_MODE: 0x0 | 0 | 0b0 | HDLC2_MODE: 0x0 | 0 | 0b0 | SRDS_PLL_PD_S1: 0x0 | 0 | 0b0 | SRDS_DIV_PEX: 0x0 | 0 | 0b0 | DDR_REFCLK_SEL: 0x1 | 1 | 0b1 | LYNX_REFCLK_SEL: 0x0 | 0 | 0b0 | DDR_FDBK_MULT: 0x2 | 2 | 0b10 | PBI_SRC: 0x4 | 4 | 0b100 | QSPI BOOT_HO: 0x0 | 0 | 0b0 | All cores expect core 0 in hold off SB_EN: 0x0 | 0 | 0b0 | Secure Boot Disabled IFC_MODE: 0x44 | 68 | 0b1000100 | HWA_CGA_M1_CLK_SEL: 0x6 | 6 | 0b110 | Async mode, Cluster Group A PLL 2 /3 is clock DRAM_LAT: 0x1 | 1 | 0b1 | 8-8-8 or higher latency DRAMs DDR_RATE: 0x0 | 0 | 0b0 | DDR_RSV0: 0x0 | 0 | 0b0 | SYS_PLL_SPD: 0x1 | 1 | 0b1 | MEM_PLL_SPD: 0x0 | 0 | 0b0 | CGA_PLL1_SPD: 0x0 | 0 | 0b0 | CGA_PLL2_SPD: 0x0 | 0 | 0b0 | HOST_AGT_PEX: 0x0 | 0 | 0b0 | GP_INFO: 0x0 | 0 | 0b0 | UART_EXT: 0x0 | 0 | 0b0 | See UART_BASE IRQ_EXT: 0x0 | 0 | 0b0 | SPI_EXT: 0x0 | 0 | 0b0 | See SPI_BASE SDHC_EXT: 0x0 | 0 | 0b0 | See SDHC_BASE UART_BASE: 0x7 | 7 | 0b111 | { UART1_SOUT, UART1_SIN, UART3_SOUT,UART3_SIN, UART2_SOUT, UART2_SIN, UART4_SOUT, UART4_SIN } ASLEEP: 0x1 | 1 | 0b1 | RTC: 0x1 | 1 | 0b1 | SDHC_BASE: 0x1 | 1 | 0b1 | GPIO2[4:9] IRQ_OUT: 0x1 | 1 | 0b1 | IRQ_BASE: 0x1FF | 511 | 0b111111111 | SPI_BASE: 0x2 | 2 | 0b10 | GPIO2[0:3] IFC_GRP_A_EXT: 0x1 | 1 | 0b1 | IFC_GRP_D_EXT: 0x0 | 0 | 0b0 | IFC_GRP_E1_EXT: 0x0 | 0 | 0b0 | See IFC_GRP_E1_BASE IFC_GRP_F_EXT: 0x1 | 1 | 0b1 | IFC_GRP_G_EXT: 0x0 | 0 | 0b0 | IFC_GRP_E1_BASE: 0x1 | 1 | 0b1 | GPIO2[10:12] IFC_GRP_D_BASE: 0x1 | 1 | 0b1 | IFC_GRP_A_BASE: 0x1 | 1 | 0b1 | IFC_A_22_24: 0x0 | 0 | 0b0 | EC1: 0x0 | 0 | 0b0 | RGMII1 EC2: 0x1 | 1 | 0b1 | GPIO3, GPIO3[19:23] EM1: 0x0 | 0 | 0b0 | MDC/MDIO (EM1) EM2: 0x0 | 0 | 0b0 | MDC/MDIO (EM2) EMI2_DMODE: 0x1 | 1 | 0b1 | EMI2_CMODE: 0x1 | 1 | 0b1 | USB_DRVVBUS: 0x0 | 0 | 0b0 | USB_DRVVBUS USB_PWRFAULT: 0x0 | 0 | 0b0 | USB_PWRFAULT TVDD_VSEL: 0x1 | 1 | 0b1 | 2.5V DVDD_VSEL: 0x2 | 2 | 0b10 | 3.3V QE_CLK_OVRRIDE: 0x0 | 0 | 0b0 | EMI1_DMODE: 0x1 | 1 | 0b1 | EVDD_VSEL: 0x0 | 0 | 0b0 | 1.8V IIC2_BASE: 0x0 | 0 | 0b0 | OK EMI1_CMODE: 0x1 | 1 | 0b1 | IIC2_EXT: 0x2 | 2 | 0b10 | GPIO4_2, GPIO4_3 SYSCLK_FREQ: 0x258 | 600 | 0b1001011000 | 100.000 MHz (100000200 Hz) HWA_CGA_M2_CLK_SEL: 0x1 | 1 | 0b1 | Async mode, Cluster Group A PLL 2 /1 is clock -- Effective Clocks -- SYSCLK: 100.00 MHz System (Bus): 300.00 MHz CGA (Cores): 1000.00 MHz MEM (Memory): 1600.00 MHz HWA_CGA_M1 (FMAN): 500.00 MHz HWA_CGA_M2 (eSDHC &amp; QuadSPI): 1000.00 MHz -- Erratum Workarounds -- Erratum A-009859 workaround: Yes Erratum A-009929 workaround: Yes -- CRC Results -- CRC Frame Present: Yes CRC Offset: 0x00DC In-File CRC: 0x06DE05A9 Calculated CRC: 0x06DE05A9 CRC Valid?: Yes </pre> T45: <pre> -- System Info -- Flash dump: t45\t45_flashstart_mm.bin Initial QSPI endianess: QWORD_LITTLE_ENDIAN Assumed SOC Model: LS1043A -- PBI Parser Messages --- ALTCBAR set to 00002200 by PBI write ALTCBAR set to 00000300 by PBI write QSPI Endianess set to DWORD_LITTLE_ENDIAN due to QUADSPI_MCR write -- RAW PBI Frames -- WRITE @ DCFG_CCSR_RCWSR0 (0x01EE0100): 061000100a0000000000000000000000455800020000001240044000c100200000000000000000000000000000036ffd20044104041832080000009600000001 (CCSR) WRITE @ SCFG_QSPI_CFG (0x0157015C): 40100000 (CCSR) WRITE @ SCFG_SCRATCHRW0 (0x01570600): 00000000 (CCSR) WRITE @ SCFG_SCRATCHRW1 (0x01570604): 40100000 (CCSR) WRITE @ LNDSSCR1 (0x01EA08DC): 00502880 (CCSR) WRITE @ CCI_UNKN_BARRIER_DISABLE (0x01570178): 0000e010 (CCSR) WRITE @ CCI_400_Control_Override (0x01180000): 00000008 (CCSR) WRITE @ SCFG_USB_REFCLK_SELCR1 (0x01570418): 0000009e (CCSR) WRITE @ SCFG_USB_REFCLK_SELCR2 (0x0157041C): 0000009e (CCSR) WRITE @ SCFG_USB_REFCLK_SELCR3 (0x01570420): 0000009e (CCSR) WRITE @ DCFG_CCSR_RSTRQMR1 (0x01EE00C0): 00004400 (CCSR) WRITE @ SCFG_ALTCBAR (0x01570158): 00002200 (CCSR) WRITE @ PEX_OUTBOUND_WRITE_HANG_ERRATUM (0x22008040): 00000001 (ACS) WRITE @ LNDSSCR1 (0x01EA08DC): 00502880 (CCSR) WRITE @ SCFG_ALTCBAR (0x01570158): 00000300 (CCSR) WRITE @ PEX1_GEN3_RElATED_OFF_ERRATUM (0x03400890): 01000100 (ACS) WRITE @ PEX2_GEN3_RElATED_OFF_ERRATUM (0x03500890): 01000100 (ACS) WRITE @ PEX3_GEN3_RElATED_OFF_ERRATUM (0x03600890): 01000100 (ACS) WRITE @ QUADSPI_MCR (0x01550000): 000f400c (CCSR) COMMAND @ PBI_CRC: 0x67AAF2F5 (current) == 0x67AAF2F5 (calculated) -&gt; PASS -- RCW Fields -- SYS_PLL_CFG: 0x0 | 0 | 0b0 | OK SYS_PLL_RAT: 0x3 | 3 | 0b11 | 3:1 MEM_PLL_CFG: 0x0 | 0 | 0b0 | OK MEM_PLL_RAT: 0x10 | 16 | 0b10000 | 16:1 CGA_PLL1_CFG: 0x0 | 0 | 0b0 | OK CGA_PLL1_RAT: 0x10 | 16 | 0b10000 | 16:1 CGA_PLL2_CFG: 0x0 | 0 | 0b0 | OK CGA_PLL2_RAT: 0xA | 10 | 0b1010 | 10:1 C1_PLL_SEL: 0x0 | 0 | 0b0 | CGA_PLL1 /1 SRDS_PRTCL_S1: 0x4558 | 17752 | 0b100010101011000 | FM1_MAC_RAT: 0x1 | 1 | 0b1 | SRDS_PLL_REF_CLK_SEL_S1: 0x0 | 0 | 0b0 | HDLC1_MODE: 0x0 | 0 | 0b0 | HDLC2_MODE: 0x0 | 0 | 0b0 | SRDS_PLL_PD_S1: 0x0 | 0 | 0b0 | SRDS_DIV_PEX: 0x0 | 0 | 0b0 | DDR_REFCLK_SEL: 0x1 | 1 | 0b1 | LYNX_REFCLK_SEL: 0x0 | 0 | 0b0 | DDR_FDBK_MULT: 0x2 | 2 | 0b10 | PBI_SRC: 0x4 | 4 | 0b100 | QSPI BOOT_HO: 0x0 | 0 | 0b0 | All cores expect core 0 in hold off SB_EN: 0x0 | 0 | 0b0 | Secure Boot Disabled IFC_MODE: 0x44 | 68 | 0b1000100 | HWA_CGA_M1_CLK_SEL: 0x6 | 6 | 0b110 | Async mode, Cluster Group A PLL 2 /3 is clock DRAM_LAT: 0x1 | 1 | 0b1 | 8-8-8 or higher latency DRAMs DDR_RATE: 0x0 | 0 | 0b0 | DDR_RSV0: 0x0 | 0 | 0b0 | SYS_PLL_SPD: 0x1 | 1 | 0b1 | MEM_PLL_SPD: 0x0 | 0 | 0b0 | CGA_PLL1_SPD: 0x0 | 0 | 0b0 | CGA_PLL2_SPD: 0x0 | 0 | 0b0 | HOST_AGT_PEX: 0x0 | 0 | 0b0 | GP_INFO: 0x0 | 0 | 0b0 | UART_EXT: 0x0 | 0 | 0b0 | See UART_BASE IRQ_EXT: 0x0 | 0 | 0b0 | SPI_EXT: 0x0 | 0 | 0b0 | See SPI_BASE SDHC_EXT: 0x0 | 0 | 0b0 | See SDHC_BASE UART_BASE: 0x6 | 6 | 0b110 | { UART1_SOUT, UART1_SIN, UART1_RTS_B, UART1_CTS_B, UART2_SOUT, UART2_SIN, UART2_RTS_B, UART2_CTS_B } ASLEEP: 0x1 | 1 | 0b1 | RTC: 0x1 | 1 | 0b1 | SDHC_BASE: 0x0 | 0 | 0b0 | { SDHC_CMD, SDHC_DAT[0:3], SDHC_CLK } IRQ_OUT: 0x1 | 1 | 0b1 | IRQ_BASE: 0x1FF | 511 | 0b111111111 | SPI_BASE: 0x1 | 1 | 0b1 | SDHC_DAT[4:7] for 8-bit MMC card support IFC_GRP_A_EXT: 0x1 | 1 | 0b1 | IFC_GRP_D_EXT: 0x0 | 0 | 0b0 | IFC_GRP_E1_EXT: 0x2 | 2 | 0b10 | { FTM7_CH0, FTM7_CH1, FTM7_EXTCLK } IFC_GRP_F_EXT: 0x1 | 1 | 0b1 | IFC_GRP_G_EXT: 0x0 | 0 | 0b0 | IFC_GRP_E1_BASE: 0x0 | 0 | 0b0 | IFC_CS_B[1:3] IFC_GRP_D_BASE: 0x1 | 1 | 0b1 | IFC_GRP_A_BASE: 0x1 | 1 | 0b1 | IFC_A_22_24: 0x0 | 0 | 0b0 | EC1: 0x0 | 0 | 0b0 | RGMII1 EC2: 0x1 | 1 | 0b1 | GPIO3, GPIO3[19:23] EM1: 0x0 | 0 | 0b0 | MDC/MDIO (EM1) EM2: 0x0 | 0 | 0b0 | MDC/MDIO (EM2) EMI2_DMODE: 0x1 | 1 | 0b1 | EMI2_CMODE: 0x1 | 1 | 0b1 | USB_DRVVBUS: 0x0 | 0 | 0b0 | USB_DRVVBUS USB_PWRFAULT: 0x0 | 0 | 0b0 | USB_PWRFAULT TVDD_VSEL: 0x1 | 1 | 0b1 | 2.5V DVDD_VSEL: 0x2 | 2 | 0b10 | 3.3V QE_CLK_OVRRIDE: 0x0 | 0 | 0b0 | EMI1_DMODE: 0x1 | 1 | 0b1 | EVDD_VSEL: 0x0 | 0 | 0b0 | 1.8V IIC2_BASE: 0x0 | 0 | 0b0 | OK EMI1_CMODE: 0x1 | 1 | 0b1 | IIC2_EXT: 0x0 | 0 | 0b0 | IIC2_SCL, IIC2_SDA SYSCLK_FREQ: 0x258 | 600 | 0b1001011000 | 100.000 MHz (100000200 Hz) HWA_CGA_M2_CLK_SEL: 0x1 | 1 | 0b1 | Async mode, Cluster Group A PLL 2 /1 is clock -- Effective Clocks -- SYSCLK: 100.00 MHz System (Bus): 300.00 MHz CGA (Cores): 1600.00 MHz MEM (Memory): 1600.00 MHz HWA_CGA_M1 (FMAN): 500.00 MHz HWA_CGA_M2 (eSDHC &amp; QuadSPI): 1000.00 MHz -- Erratum Workarounds -- Erratum A-009859 workaround: Yes Erratum A-009929 workaround: Yes -- CRC Results -- CRC Frame Present: Yes CRC Offset: 0x00DC In-File CRC: 0x67AAF2F5 Calculated CRC: 0x67AAF2F5 CRC Valid?: Yes </pre> </details> Overclocking the WatchGuard T40 2025-10-12T00:00:00+00:00 https://shadow578.github.io/2025/10/12/t40-overclocking <p>with linux mostly running (and me still procrastinating continuing further work on it), i figured i’d try and solve my last hang-up with the T40. the T40’s SOC is a NXP QorIQ LS1043A, and that supports up to 1.6GHz. however, the T40 is clocked at only 1.0GHz.</p> <p>only when springing for the T45, which is mostly the same device but clocked higher, do you get the 1.6GHz clockspeed. that’s unfair! i paid for the full LS1043A, so i want to use all of it.</p> <h1 id="how-the-cpu-speed-is-set">How The CPU Speed Is Set</h1> <p>linux has a cpu frequency driver for the LS1043A (specifically, all QorIQ SoCs). however, that can only scale <strong>down</strong> from the maximum frequency. setting that maximum frequency is done even before the bootloader is run, by something appropriately named the “Pre-Boot Loader” (PBL), which loads something called a “Reset Configuration Word” (RCW) from memory on boot.</p> <h2 id="something-familiar">Something Familiar</h2> <p>i was already familiar with the concept of an RCW, as the HC32F460 microcontroller (that i ported <a href="https://github.com/shadow578/framework-arduino-hc32f46x/">Arduino</a> and Marlin to) uses something similar. it just so happens that NXP made their boot ROM much more flexible, allowing not just a fixed register to be loaded, but instead allowing writes to <em>any</em> register. these writes are called “Pree-Boot Initialization” (PBI) commands, and are stored in a binary data structure at the start of the memory device configured by some GPIOs.</p> <p>the structure of these commands is super simple, it’s just a bite count field (up to 64 bytes), a target address (at least 24 bits of it), and the data itself. everything the PBL does, from the RCW to doing a CRC on the PBI data, is done through these commands.</p> <p>as for why NXP choose to do it this way, has a fairly clever reason. simply by changing the PBI data, you can do all sorts of stuff, but importantly: you can fix errata simply by changing the SDK source code that generates the PBI data. this isn’t just in theory either: NXP has fixed two PCIe-related errata (<a href="https://github.com/nxp-qoriq/rcw/blob/devel/ls1043aqds/a009859.rcw">A-009859</a> and <a href="https://github.com/nxp-qoriq/rcw/blob/devel/ls1043aqds/a009929.rcw">A-009929</a>) in the LS1043A this way, simply by writing magic values to otherwise undocumented registers. all-in-all, pretty cool.</p> <h1 id="understanding-and-parsing-the-pbi-data">Understanding and Parsing the PBI Data</h1> <p>to change the CPU clock speed, we need to change part of the PBI (specifically, the RCW) that sets up the PLLs for the CPU core cluster. however, the PBI includes a CRC-32 checksum at the end, so we can’t just change the data and expect it to work.</p> <p>to make this whole process easier, and to understand how everything works, i decided to write a small tool to parse and print the PBI data, as well as calculating the CRC-32 checksum needed. that shouldn’t be too hard, especially since NXP provides a comprehensive <a href="https://www.nxp.com/products/LS1043A">reference manual</a> for the LS1043A, which includes everything we need to know about.</p> <p>as for why i decided to go this overboard, well… let’s just say that it’ll help us for what i plan to do later.</p> <table> <thead> <tr> <th><img src="/assets/images/common/reactions/its-a-suprise-tool-later.png" alt="it's a suprise tool that'll help us later" /></th> </tr> </thead> <tbody> <tr> <td><em>it’s a suprise tool that’ll help us later</em></td> </tr> </tbody> </table> <p>if you’re interested, i published the code of the tool on <a href="https://github.com/shadow578/QorIQ_PBI_dump">GitHub</a>. just note that it’s far from perfect, and very much not meant for production use.</p> <p>for that, you should probably use <a href="https://github.com/nxp-qoriq/rcw">NXP’s rcw tool</a> instead. i only found this after writing my own tool, so… oh well. luckily (at least so i can tell myself that i didn’t waste my time), NXP’s tool isn’t able to handle the weird stuff that WatchGuard does, so mine is still useful.</p> <h2 id="the-weird-stuff-watchguard-does">The Weird Stuff WatchGuard Does</h2> <p>WatchGuard does something really weird - and frankly incredibly stupid - in their PBI. to cut an incredibly long, and frustrating, story short: they for some reason decided to use the PBI to switch the QuadSPI peripheral’s endianness from 64-bit little-endian to 32-bit big-endian. now, you could excuse this if this was some default setting of the SoC, but no: you can freely choose the on-reset configuration for this using strapping pins. the engineers at WatchGuard just… decided to do this for some reason.</p> <table> <thead> <tr> <th><img src="/assets/images/common/reactions/the-office-parkour.gif" alt="WatchGuard deciding on data endianness" /></th> </tr> </thead> <tbody> <tr> <td><em>WatchGuard deciding on data endianness</em></td> </tr> </tbody> </table> <p>what that meant for me is that i had to handle the behaviour of the QuadSPI peripheral in my PBI parser, <strong>and</strong> handle when the PBI write switches the endianness. figuring all that out was a huge pain, and took way too long. but i eventually did it, and now my PBI parser can handle the T40’s PBI data. take a look:</p> <details> <summary>The Output is pretty long, so click here to expand it</summary> <pre> -- System Info -- Flash dump: ../rcwmod/spiflash.20251012.1000mhz.bin Initial QSPI endianess: QWORD_LITTLE_ENDIAN Assumed SOC Model: LS1043A -- PBI Parser Messages --- ALTCBAR set to 00002200 by PBI write ALTCBAR set to 00000300 by PBI write QSPI Endianess set to DWORD_LITTLE_ENDIAN due to QUADSPI_MCR write -- RAW PBI Frames -- WRITE @ DCFG_CCSR_RCWSR0 (0x01EE0100): 0610000a0a0000000000000000000000455800020000001240044000c10020000000000000000000000000000003fffe200045040418320a0000009600000001 (CCSR) WRITE @ SCFG_QSPI_CFG (0x0157015C): 40100000 (CCSR) WRITE @ SCFG_SCRATCHRW0 (0x01570600): 00000000 (CCSR) WRITE @ SCFG_SCRATCHRW1 (0x01570604): 40100000 (CCSR) WRITE @ LNDSSCR1 (0x01EA08DC): 00502880 (CCSR) WRITE @ CCI_UNKN_BARRIER_DISABLE (0x01570178): 0000e010 (CCSR) WRITE @ CCI_400_Control_Override (0x01180000): 00000008 (CCSR) WRITE @ SCFG_USB_REFCLK_SELCR1 (0x01570418): 0000009e (CCSR) WRITE @ SCFG_USB_REFCLK_SELCR2 (0x0157041C): 0000009e (CCSR) WRITE @ SCFG_USB_REFCLK_SELCR3 (0x01570420): 0000009e (CCSR) WRITE @ DCFG_CCSR_RSTRQMR1 (0x01EE00C0): 00004400 (CCSR) WRITE @ SCFG_ALTCBAR (0x01570158): 00002200 (CCSR) WRITE @ PEX_OUTBOUND_WRITE_HANG_ERRATUM (0x22008040): 00000001 (ACS) WRITE @ LNDSSCR1 (0x01EA08DC): 00502880 (CCSR) WRITE @ SCFG_ALTCBAR (0x01570158): 00000300 (CCSR) WRITE @ PEX1_GEN3_RElATED_OFF_ERRATUM (0x03400890): 01000100 (ACS) WRITE @ PEX2_GEN3_RElATED_OFF_ERRATUM (0x03500890): 01000100 (ACS) WRITE @ PEX3_GEN3_RElATED_OFF_ERRATUM (0x03600890): 01000100 (ACS) WRITE @ QUADSPI_MCR (0x01550000): 000f400c (CCSR) COMMAND @ PBI_CRC: 0x06DE05A9 (current) == 0x06DE05A9 (calculated) -&gt; PASS -- RCW Fields -- SYS_PLL_CFG: 0x0 | 0 | 0b0 | OK SYS_PLL_RAT: 0x3 | 3 | 0b11 | 3:1 MEM_PLL_CFG: 0x0 | 0 | 0b0 | OK MEM_PLL_RAT: 0x10 | 16 | 0b10000 | 16:1 CGA_PLL1_CFG: 0x0 | 0 | 0b0 | OK CGA_PLL1_RAT: 0xA | 10 | 0b1010 | 10:1 CGA_PLL2_CFG: 0x0 | 0 | 0b0 | OK CGA_PLL2_RAT: 0xA | 10 | 0b1010 | 10:1 C1_PLL_SEL: 0x0 | 0 | 0b0 | CGA_PLL1 /1 SRDS_PRTCL_S1: 0x4558 | 17752 | 0b100010101011000 | FM1_MAC_RAT: 0x1 | 1 | 0b1 | SRDS_PLL_REF_CLK_SEL_S1: 0x0 | 0 | 0b0 | HDLC1_MODE: 0x0 | 0 | 0b0 | HDLC2_MODE: 0x0 | 0 | 0b0 | SRDS_PLL_PD_S1: 0x0 | 0 | 0b0 | SRDS_DIV_PEX: 0x0 | 0 | 0b0 | DDR_REFCLK_SEL: 0x1 | 1 | 0b1 | LYNX_REFCLK_SEL: 0x0 | 0 | 0b0 | DDR_FDBK_MULT: 0x2 | 2 | 0b10 | PBI_SRC: 0x4 | 4 | 0b100 | QSPI BOOT_HO: 0x0 | 0 | 0b0 | All cores expect core 0 in hold off SB_EN: 0x0 | 0 | 0b0 | Secure Boot Disabled IFC_MODE: 0x44 | 68 | 0b1000100 | HWA_CGA_M1_CLK_SEL: 0x6 | 6 | 0b110 | Async mode, Cluster Group A PLL 2 /3 is clock DRAM_LAT: 0x1 | 1 | 0b1 | 8-8-8 or higher latency DRAMs DDR_RATE: 0x0 | 0 | 0b0 | DDR_RSV0: 0x0 | 0 | 0b0 | SYS_PLL_SPD: 0x1 | 1 | 0b1 | MEM_PLL_SPD: 0x0 | 0 | 0b0 | CGA_PLL1_SPD: 0x0 | 0 | 0b0 | CGA_PLL2_SPD: 0x0 | 0 | 0b0 | HOST_AGT_PEX: 0x0 | 0 | 0b0 | GP_INFO: 0x0 | 0 | 0b0 | UART_EXT: 0x0 | 0 | 0b0 | See UART_BASE IRQ_EXT: 0x0 | 0 | 0b0 | SPI_EXT: 0x0 | 0 | 0b0 | See SPI_BASE SDHC_EXT: 0x0 | 0 | 0b0 | See SDHC_BASE UART_BASE: 0x7 | 7 | 0b111 | { UART1_SOUT, UART1_SIN, UART3_SOUT,UART3_SIN, UART2_SOUT, UART2_SIN, UART4_SOUT, UART4_SIN } ASLEEP: 0x1 | 1 | 0b1 | RTC: 0x1 | 1 | 0b1 | SDHC_BASE: 0x1 | 1 | 0b1 | GPIO2[4:9] IRQ_OUT: 0x1 | 1 | 0b1 | IRQ_BASE: 0x1FF | 511 | 0b111111111 | SPI_BASE: 0x2 | 2 | 0b10 | GPIO2[0:3] IFC_GRP_A_EXT: 0x1 | 1 | 0b1 | IFC_GRP_D_EXT: 0x0 | 0 | 0b0 | IFC_GRP_E1_EXT: 0x0 | 0 | 0b0 | See IFC_GRP_E1_BASE IFC_GRP_F_EXT: 0x1 | 1 | 0b1 | IFC_GRP_G_EXT: 0x0 | 0 | 0b0 | IFC_GRP_E1_BASE: 0x1 | 1 | 0b1 | GPIO2[10:12] IFC_GRP_D_BASE: 0x1 | 1 | 0b1 | IFC_GRP_A_BASE: 0x1 | 1 | 0b1 | IFC_A_22_24: 0x0 | 0 | 0b0 | EC1: 0x0 | 0 | 0b0 | RGMII1 EC2: 0x1 | 1 | 0b1 | GPIO3, GPIO3[19:23] EM1: 0x0 | 0 | 0b0 | MDC/MDIO (EM1) EM2: 0x0 | 0 | 0b0 | MDC/MDIO (EM2) EMI2_DMODE: 0x1 | 1 | 0b1 | EMI2_CMODE: 0x1 | 1 | 0b1 | USB_DRVVBUS: 0x0 | 0 | 0b0 | USB_DRVVBUS USB_PWRFAULT: 0x0 | 0 | 0b0 | USB_PWRFAULT TVDD_VSEL: 0x1 | 1 | 0b1 | 2.5V DVDD_VSEL: 0x2 | 2 | 0b10 | 3.3V QE_CLK_OVRRIDE: 0x0 | 0 | 0b0 | EMI1_DMODE: 0x1 | 1 | 0b1 | EVDD_VSEL: 0x0 | 0 | 0b0 | 1.8V IIC2_BASE: 0x0 | 0 | 0b0 | OK EMI1_CMODE: 0x1 | 1 | 0b1 | IIC2_EXT: 0x2 | 2 | 0b10 | GPIO4_2, GPIO4_3 SYSCLK_FREQ: 0x258 | 600 | 0b1001011000 | 100.000 MHz (100000200 Hz) HWA_CGA_M2_CLK_SEL: 0x1 | 1 | 0b1 | Async mode, Cluster Group A PLL 2 /1 is clock -- Effective Clocks -- SYSCLK: 100.00 MHz System (Bus): 300.00 MHz CGA (Cores): 1000.00 MHz MEM (Memory): 1600.00 MHz HWA_CGA_M1 (FMAN): 500.00 MHz HWA_CGA_M2 (eSDHC &amp; QuadSPI): 1000.00 MHz -- Erratum Workarounds -- Erratum A-009859 workaround: Yes Erratum A-009929 workaround: Yes -- CRC Results -- CRC Frame Present: Yes CRC Offset: 0x00DC In-File CRC: 0x06DE05A9 Calculated CRC: 0x06DE05A9 CRC Valid?: Yes </pre> </details> <h1 id="going-to-16ghz">Going to 1.6GHz</h1> <p>changing the CPU clock is fairly simple, once you have a way to actually create a valid PBI. the SoC runs on a single 100MHz reference clock (SYSCLK), and the CPU clock is derived from that using a PLL. the PLL for the CPU (and actually everything else) is configured using the <code class="language-plaintext highlighter-rouge">CGA_PLL1_RAT</code> (Cluster Group A PLL 1 Ratio) field in the RCW. well, actually there is a second PLL (<code class="language-plaintext highlighter-rouge">CGA_PLL2_RAT</code>), and you can freely choose which one to use for the CPU cores, but that’s not important right now.</p> <p>there’s some other things to keep in mind, mainly since the <code class="language-plaintext highlighter-rouge">CGA_PLLn</code> clock is also used for the hardware accceleration engines (FMAN = Frame Manager) and the eSDHC and QuadSPI peripherals. the clocks of those can be derived from either of the <code class="language-plaintext highlighter-rouge">CGA_PLLn</code> PLLs. luckily, in the T40’s RCW, they use PLL2 while the CPU cores use PLL1, so we can change the CPU clock without affecting those peripherals.</p> <p>other than that, there’s also the option to divide the PLL clock by either 1 or 2 depending on the <code class="language-plaintext highlighter-rouge">C1_PLL_SEL</code> field, but we’ll simply leave it at PLL1 /1.</p> <p>with that, we can start overlocking the CPU as follows:</p> <ul> <li>set <code class="language-plaintext highlighter-rouge">CGA_PLL1_RAT</code> to the desired multiplier (5 - 40). the field is at offset 0x0C in the SPI flash dump. For 1.0GHz, it’s set to 0xA (10:1), for 1.6GHz, set it to 0x10 (16:1).</li> <li>run <code class="language-plaintext highlighter-rouge">pbidump</code> to check the changes, and to calculate the new CRC-32 checksum.</li> <li>write the new CRC-32 checksum at offset 0xDC in the SPI flash dump.</li> <li>run <code class="language-plaintext highlighter-rouge">pbidump</code> again to verify that everything is correct.</li> <li>flash the modified image to the T40’s SPI flash.</li> <li>cross fingers and boot.</li> </ul> <p>do all that, and you should be greeted by u-boot reporting 1600MHz CPU clock:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>U-Boot 2018.09 (Dec 05 2019 - 10:13:47 -0800) SoC: LS1043AE Rev1.1 (0x87920011) Clock Configuration: CPU0(A53):1600 MHz CPU1(A53):1600 MHz CPU2(A53):1600 MHz CPU3(A53):1600 MHz Bus: 300 MHz DDR: 1600 MT/s FMAN: 500 MHz Reset Configuration Word (RCW): 00000000: 06100010 0a000000 00000000 00000000 00000010: 45580002 00000012 40044000 c1002000 00000020: 00000000 00000000 00000000 0003fffe 00000030: 20004504 0418320a 00000096 00000001 Model: LS1043A QDS Board - T40/T20 Board: LS1043AQDS, boot from vBank: 0 </code></pre></div></div> <p>success!</p> <h1 id="but-what-if-we-went-faster">But What If We… Went faster?</h1> <p>technically, we haven’t yet overclocked the SoC (1.6GHz is still supported, after all), so why not see how far we can push it? as you’ll see, this doesn’t go quite as smoothly.</p> <h2 id="20-ghz">2.0 GHz</h2> <p>well, nothing happens. doesn’t even boot.</p> <h2 id="18-ghz-">1.8 GHz ?</h2> <p>let’s take it back a notch and try 1.8GHz. aand… it crashes. while u-boot does come up briefly, it quickly crashes with a “synchronous abort” error.</p> <h2 id="maybe-just-17-ghz">Maybe just 1.7 GHz</h2> <p>ok, i really was hoping to reach 2 GHz here, but let’s go back to 1.7GHz.</p> <p>this actually looked kinda promising at first, but nope, not stable. it does get to linux, and even runs the first coremark run, but then the kernel just panics.</p> <h2 id="what-about-ram">What About RAM?</h2> <p>sadly, the T40 uses pretty basic DDR4-2666 memory (Nanya NT5AD1024M8A3-HR), with incredible 19-19-19 timings - at 2666 MT/s. At the 3200 MT/s we’re running, that decreases to 22-22-22 timings. ouch. no wonder Nanya doesn’t even list it on their website anymore. that means there isn’t really much room for improvement here, as a clock any higher than 1600 MHz will not work stable (i tried).</p> <h1 id="benchmark-results">Benchmark Results</h1> <p>to see how much of a difference this makes, i threw together a quick test using both <a href="https://github.com/eembc/coremark">coremark</a> and <a href="https://github.com/eembc/coremark-pro">coremark-pro</a>. the results are really promising, showing almost linear scaling with the increased clock speed.</p> <table> <thead> <tr> <th>Run</th> <th>CoreMark</th> <th>CoreMark-Pro (SingleCore)</th> <th>CoreMark-Pro (MultiCore)</th> </tr> </thead> <tbody> <tr> <td>T40 @ 1.0 GHz</td> <td>3276 (+0%)</td> <td>686 (+0%)</td> <td>2178 (+0%)</td> </tr> <tr> <td>T40 @ 1.6 GHz</td> <td>5268 (+61%)</td> <td>1097 (+60%)</td> <td>3326 (+53%)</td> </tr> </tbody> </table> <details> <summary>Click for benchmark details</summary> `benchmark.sh`: <pre> #!/bin/sh # standard benchmark script for T40 # assumes to be run as root, in a directory where coremark and coremark-pro are cloned (and built) already set -e echo "-- CPU Freq Config --" cpupower frequency-set -g performance cpupower frequency-info echo "-- CoreMark --" cd coremark/ ./coremark.exe cd .. echo "-- CoreMark-Pro --" cd coremark-pro/ make -s XCMD='-c4' certify-all cd .. echo "-- Done --" </pre> T40 @ 1.0GHz: <pre> -- CPU Freq Config -- Setting cpu: 0 Setting cpu: 1 Setting cpu: 2 Setting cpu: 3 analyzing CPU 0: driver: qoriq_cpufreq CPUs which run at the same hardware frequency: 0 1 2 3 CPUs which need to have their frequency coordinated by software: 0 1 2 3 maximum transition latency: 41 ns hardware limits: 500 MHz - 1000 MHz available frequency steps: 1000 MHz, 500 MHz available cpufreq governors: conservative ondemand userspace powersave performance schedutil current policy: frequency should be within 500 MHz and 1000 MHz. The governor "performance" may decide which speed to use within this range. current CPU frequency: 1000 MHz (asserted by call to hardware) -- CoreMark -- 2K performance run parameters for coremark. CoreMark Size : 666 Total ticks : 12209 Total time (secs): 12.209000 Iterations/Sec : 3276.271603 Iterations : 40000 Compiler version : GCC14.2.1 20250207 Compiler flags : -O2 -DPERFORMANCE_RUN=1 -lrt Memory location : Please put data memory location here (e.g. code in flash, data on heap etc) seedcrc : 0xe9f5 [0]crclist : 0xe714 [0]crcmatrix : 0x1fd7 [0]crcstate : 0x8e3a [0]crcfinal : 0x25b5 Correct operation validated. See README.md for run and reporting rules. CoreMark 1.0 : 3276.271603 / GCC14.2.1 20250207 -O2 -DPERFORMANCE_RUN=1 -lrt / Heap -- CoreMark-Pro -- WORKLOAD RESULTS TABLE MultiCore SingleCore Workload Name (iter/s) (iter/s) Scaling ----------------------------------------------- ---------- ---------- ---------- cjpeg-rose7-preset 113.64 29.33 3.87 core 0.81 0.20 4.05 linear_alg-mid-100x100-sp 39.18 10.19 3.84 loops-all-mid-10k-sp 1.27 0.45 2.82 nnet_test 2.68 0.80 3.35 parser-125k 18.78 6.90 2.72 radix2-big-64k 123.11 72.61 1.70 sha-test 192.31 57.47 3.35 zip-test 57.14 15.38 3.72 MARK RESULTS TABLE Mark Name MultiCore SingleCore Scaling ----------------------------------------------- ---------- ---------- ---------- CoreMark-PRO 2177.64 686.01 3.17 </pre> T40 @ 1.6GHz: <pre> -- CPU Freq Config -- Setting cpu: 0 Setting cpu: 1 Setting cpu: 2 Setting cpu: 3 analyzing CPU 0: driver: qoriq_cpufreq CPUs which run at the same hardware frequency: 0 1 2 3 CPUs which need to have their frequency coordinated by software: 0 1 2 3 maximum transition latency: 41 ns hardware limits: 500 MHz - 1.60 GHz available frequency steps: 1.60 GHz, 1000 MHz, 800 MHz, 500 MHz available cpufreq governors: conservative ondemand userspace powersave performance schedutil current policy: frequency should be within 500 MHz and 1.60 GHz. The governor "performance" may decide which speed to use within this range. current CPU frequency: 1.60 GHz (asserted by call to hardware) -- CoreMark -- 2K performance run parameters for coremark. CoreMark Size : 666 Total ticks : 18981 Total time (secs): 18.981000 Iterations/Sec : 5268.426321 Iterations : 100000 Compiler version : GCC14.2.1 20250207 Compiler flags : -O2 -DPERFORMANCE_RUN=1 -lrt Memory location : Please put data memory location here (e.g. code in flash, data on heap etc) seedcrc : 0xe9f5 [0]crclist : 0xe714 [0]crcmatrix : 0x1fd7 [0]crcstate : 0x8e3a [0]crcfinal : 0xd340 Correct operation validated. See README.md for run and reporting rules. CoreMark 1.0 : 5268.426321 / GCC14.2.1 20250207 -O2 -DPERFORMANCE_RUN=1 -lrt / Heap -- CoreMark-Pro -- WORKLOAD RESULTS TABLE MultiCore SingleCore Workload Name (iter/s) (iter/s) Scaling ----------------------------------------------- ---------- ---------- ---------- cjpeg-rose7-preset 181.82 47.17 3.85 core 1.31 0.33 3.97 linear_alg-mid-100x100-sp 62.97 16.40 3.84 loops-all-mid-10k-sp 1.84 0.70 2.63 nnet_test 4.30 1.29 3.33 parser-125k 28.99 10.99 2.64 radix2-big-64k 147.43 113.48 1.30 sha-test 312.50 92.59 3.38 zip-test 88.89 24.39 3.64 MARK RESULTS TABLE Mark Name MultiCore SingleCore Scaling ----------------------------------------------- ---------- ---------- ---------- CoreMark-PRO 3325.58 1096.57 3.03 </pre> </details> <h1 id="conclusion">Conclusion</h1> <p>this was really fun to do, and i’m super happy that it worked out. although i’m a bit bummed that i couldn’t really push the SoC any further than 1.6GHz, it’s still a nice improvement. i really would’ve thought that there’d be more headroom, but oh well.</p> cracking the T40's U-Boot 2025-10-05T00:00:00+00:00 https://shadow578.github.io/2025/10/05/t40-uboot-hacking <p>with linux mostly running (and me procrastinating continuing work on the last ethernet PHY), there still was a little hangup for me: on the T40, it’s easy enough to get into U-Boot by simply removing the SSD. but wouldn’t it be nice to have a way to get into U-Boot without having to open the case?</p> <h1 id="the-hidden-password-prompt">The Hidden Password Prompt</h1> <p>that’s exactly what the engineers at WatchGuard thought too, so they added a hidden password prompt to the U-Boot build. you can access it by pressing <code class="language-plaintext highlighter-rouge">CTRL-C</code> at the SysA / SysB prompt.</p> <table> <thead> <tr> <th><img src="/assets/images/hw-hacking/watchguard/t40/uboot/hidden-password-prompt.png" alt="Hidden Password Prompt" /></th> </tr> </thead> <tbody> <tr> <td>Hidden Password Prompt</td> </tr> </tbody> </table> <p>sadly, the password isn’t “WatchGuard!” this time around. would’ve been funny though.</p> <h1 id="the-elusive-u-boot-patch">The Elusive U-Boot Patch</h1> <p>on the <a href="https://forum.openwrt.org/t/installing-openwrt-on-watchguard-t40w/230048">OpenWRT forum thread about the T40</a>, there was some <a href="https://forum.openwrt.org/t/installing-openwrt-on-watchguard-t40w/230048/12">discussion</a> about a possible patch to bypass the password. this was posted by <a href="https://oftc.catirclogs.org/openwrt-devel/2022-04-26#30875010">neggles on the OpenWRT IRC</a>:</p> <blockquote> <p>12:02 <neggles> stintel: I binpatched the u-boot on my T20 to get into it 12:02 <neggles> changed one cbz to cbnz</neggles></neggles></p> </blockquote> <p>sadly, no further details were given on where to patch the binary. so I set out to figure it out myself. how hard could it be?</p> <h2 id="loading-the-u-boot-binary-into-ghidra">Loading The U-Boot Binary Into Ghidra</h2> <p>this first step is where i hit my first issues, really making me reconsider if this was worth the effort. dumping the U-Boot binary was easy enough using the working linux system, as the MTB partition is simply exposed as <code class="language-plaintext highlighter-rouge">/dev/mtd1</code> (even with a nice label “U-Boot”).</p> <p>that is where the easy part ends, though.</p> <p>i spend forever trying to find the base address to load the binary at. all this was made 100x harder by me being an idiot and choosing the wrong ARM architecture in Ghidra.</p> <p>eventually, i stumbled upon <a href="https://github.com/quarkslab/binbloom">binbloom</a>, which is a really neat tool for analysing raw firmware binaries. i had never heard of it before, but it worked really well for this purpose:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>binbloom <span class="nt">-e</span> be mtd1_uboot.bin <span class="o">[</span>i] Selected big-endian architecture. <span class="o">[</span>i] File <span class="nb">read</span> <span class="o">(</span>1048576 bytes<span class="o">)</span> <span class="o">[</span>i] Endianness is BE <span class="o">[</span>i] 3318 strings indexed <span class="o">[</span>i] Found 31351 base addresses to <span class="nb">test</span> <span class="o">[</span>i] Base address seems to be 0x00f8b000 <span class="o">(</span>not sure<span class="o">)</span><span class="nb">.</span> </code></pre></div></div> <p>loading the binary at <code class="language-plaintext highlighter-rouge">0x00f8b000</code> in Ghidra looked really promising, so i think it’s correct.</p> <h2 id="figuring-out-the-password-check">Figuring Out The Password Check</h2> <p>before we can patch anything, we need to understand what is happening. as a quick start, i decided to search for references to the header string we saw in the password prompt, and got to a function that likely handles drawing the prompt. single caller is a function that looks suspiciously like it handles the bootdelay count down, as well as having a direct reference to the string “password&gt;”. suspicious indeed.</p> <p>cleaning stuff up (a lot!), here’s some snippets of the relevant code (function’s really long, so just the relevant parts):</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// (...)</span> <span class="n">password_entered_ok_maybe</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span> <span class="n">scrambled_correct_password</span> <span class="o">=</span> <span class="n">SCRAMBLED_CORRECT_PASSWORD</span><span class="p">;</span> <span class="c1">// (...)</span> <span class="n">printf_ish</span><span class="p">(</span><span class="s">"Hit any key to stop autoboot: %2d"</span><span class="p">,</span><span class="n">bootdelay</span><span class="p">);</span> <span class="n">redraw_bootdelay_countdown_maybe</span><span class="o">:</span> <span class="k">if</span> <span class="p">(</span><span class="cm">/* (...) */</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// (...)</span> <span class="k">do</span> <span class="p">{</span> <span class="n">iVar3</span> <span class="o">=</span> <span class="n">ControlCPressed_likely</span><span class="p">();</span> <span class="k">if</span> <span class="p">(</span><span class="n">iVar3</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">)</span> <span class="k">goto</span> <span class="n">do_password_check</span><span class="p">;</span> <span class="c1">// (...)</span> <span class="p">}</span> <span class="k">while</span> <span class="p">(</span><span class="n">ms_delay_counter</span> <span class="o">&lt;</span> <span class="mi">1000</span><span class="p">);</span> <span class="c1">// (...)</span> <span class="p">}</span> <span class="c1">// (...)</span> <span class="k">if</span> <span class="p">(</span><span class="n">password_entered_ok_maybe</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span><span class="p">;</span> <span class="p">}</span> <span class="c1">// (...)</span> <span class="n">do_password_check</span><span class="o">:</span> <span class="k">while</span> <span class="p">(</span><span class="n">read_char</span> <span class="o">!=</span> <span class="err">`\</span><span class="n">r</span><span class="err">`</span><span class="p">)</span> <span class="p">{</span> <span class="k">while</span><span class="p">(</span> <span class="nb">true</span> <span class="p">)</span> <span class="p">{</span> <span class="n">read_char</span> <span class="o">=</span> <span class="n">maybe_read_serial_char</span><span class="p">();</span> <span class="c1">// (...)</span> <span class="c1">// get_password_text_with_prompt displays the string as a prompt,</span> <span class="c1">// then reads input from serial input until ENTER is pressed.</span> <span class="c1">// input is stored in PASSWORD_INPUT global.</span> <span class="n">pwdlen</span> <span class="o">=</span> <span class="n">get_password_text_with_prompt</span><span class="p">(</span><span class="s">"password&gt;"</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="mi">0</span> <span class="o">&lt;</span> <span class="n">pwdlen</span><span class="p">)</span> <span class="p">{</span> <span class="n">scramble_password</span><span class="p">(</span><span class="o">&amp;</span><span class="n">PASSWORD_INPUT</span><span class="p">,</span><span class="n">pwdlen</span><span class="p">,</span><span class="n">scrambled_password</span><span class="p">);</span> <span class="n">pwdlen</span> <span class="o">=</span> <span class="n">memcmp</span><span class="p">(</span><span class="o">&amp;</span><span class="n">scrambled_correct_password</span><span class="p">,</span><span class="n">scrambled_password</span><span class="p">,</span><span class="mh">0x14</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="n">pwdlen</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span> <span class="n">password_entered_ok_maybe</span> <span class="o">=</span> <span class="nb">true</span><span class="p">;</span> <span class="k">goto</span> <span class="n">boot_or_redraw</span><span class="p">;</span> <span class="p">}</span> <span class="p">}</span> <span class="c1">// (...)</span> <span class="p">}</span> <span class="p">}</span> <span class="n">password_entered_ok_maybe</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span> <span class="c1">// (...)</span> </code></pre></div></div> <p>i think we have a winner here. best part: the <code class="language-plaintext highlighter-rouge">if (pwdlen == 0)</code> check is implemented using a <code class="language-plaintext highlighter-rouge">CBZ</code> instruction, which is exactly what neggles mentioned in the IRC. patching this to a <code class="language-plaintext highlighter-rouge">CBNZ</code> should make it so that any entered password (well, except for the correct one) is accepted.</p> <p>to patch your binary the same way that neggles likely did, simply open the binary in a hex editor, search for the hex pattern <code class="language-plaintext highlighter-rouge">e32f0194c0000034</code>, and replace the last byte <code class="language-plaintext highlighter-rouge">34</code> with <code class="language-plaintext highlighter-rouge">35</code>. the byte should be at offset <code class="language-plaintext highlighter-rouge">0x199C3</code> in /dev/mtd1, or <code class="language-plaintext highlighter-rouge">0x1199C3</code> in the whole flash dump. note that these offsets are specific to the T40’s U-Boot binary, and may differ in other devices or U-Boot versions.</p> <p>after flashing the patched binary back, simply press <code class="language-plaintext highlighter-rouge">CTRL-C</code> at the boot selection, enter any password (cannot be empty), and you’re dropped into U-Boot.</p> <h2 id="cracking-the-password">Cracking the Password</h2> <p>nope, didn’t do that. though i’m curious, <code class="language-plaintext highlighter-rouge">scamble_password</code> is a rather long and annoying function to reverse. maybe someone else wants to give it a try?</p> Reverse-Engineering the Front Panel of a WatchGuard T40 2025-10-04T00:00:00+00:00 https://shadow578.github.io/2025/10/04/t40-frontpanel-reverse-engineering <p>with linux mostly working on the T40, i decided to take a look at getting the front panel LEDs and buttons working.</p> <h1 id="preparation">Preparation</h1> <p>to prepare for this, we first need to dump the file system and figure out what controls the front panel. the ssd is easily dumpable from the - now - working linux system.</p> <p>as for what controls the front panel, the boot log mentions a <code class="language-plaintext highlighter-rouge">S51armled</code> script, which probably does something with the front panel (just a hunch). looking at that script, it just runs <code class="language-plaintext highlighter-rouge">/usr/bin/armled -arm</code>, so let’s look at that binary.</p> <h1 id="the-armled-binary">The armled Binary</h1> <p>i didn’t actually look much at this binary, as i suspected it would work similar to how the T70’s front panel works, meaning theres a library doing the actual work. and i was right, the <code class="language-plaintext highlighter-rouge">armled</code> binary links agains <code class="language-plaintext highlighter-rouge">libwgpanel.so</code>. that seems promising, so let’s look at that library.</p> <h1 id="libwgpanelso">libwgpanel.so</h1> <p>after loading and analyzing the library in ghidra, there’s a bunch of functions exported. that’s really nice.</p> <table> <thead> <tr> <th><img src="/assets/images/hw-hacking/watchguard/t40/libwgpanel/exported-symbols.png" alt="exports of libwgpanel.so" /></th> </tr> </thead> <tbody> <tr> <td>exports of libwgpanel.so</td> </tr> </tbody> </table> <p>while <code class="language-plaintext highlighter-rouge">write_led</code>, <code class="language-plaintext highlighter-rouge">getResetButton</code>, and <code class="language-plaintext highlighter-rouge">read_reset</code> would be obvious candidates to look at, i decided to start with <code class="language-plaintext highlighter-rouge">gpio_initialize</code>. for now, i care more about understanding how things work generally, not specifically how to do stuff. the rest i can hopefully figure out from that.</p> <p>so, let’s look at <code class="language-plaintext highlighter-rouge">gpio_initialize</code>.</p> <h2 id="gpio_initialize-and-friends">gpio_initialize and friends</h2> <p><code class="language-plaintext highlighter-rouge">gpio_initialize</code> calls two functions, which i don’t care too much what they do right now. digging a bit in the first one tho, we see some familiar looking code (post clean-up here):</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">bool</span> <span class="nf">export_gpio_pin</span><span class="p">(</span><span class="kt">char</span> <span class="o">*</span><span class="n">gpio_pin</span><span class="p">)</span> <span class="p">{</span> <span class="n">bool</span> <span class="n">fn_rc</span><span class="p">;</span> <span class="kt">int</span> <span class="n">rc</span><span class="p">;</span> <span class="kt">size_t</span> <span class="n">len</span><span class="p">;</span> <span class="n">stat</span> <span class="o">*</span><span class="n">stat_buf</span><span class="p">;</span> <span class="kt">char</span> <span class="n">gpio_path</span> <span class="p">[</span><span class="mi">84</span><span class="p">];</span> <span class="n">undefined4</span> <span class="n">local_c</span><span class="p">;</span> <span class="kt">FILE</span> <span class="o">*</span><span class="n">fhandle</span><span class="p">;</span> <span class="n">sprintf</span><span class="p">(</span><span class="n">gpio_path</span><span class="p">,</span><span class="n">s_</span><span class="o">/</span><span class="n">sys</span><span class="o">/</span><span class="n">class</span><span class="o">/</span><span class="n">gpio</span><span class="o">/</span><span class="n">gpio</span><span class="o">%</span><span class="n">s</span><span class="o">/</span><span class="n">value_00114240</span><span class="p">,</span><span class="n">gpio_pin</span><span class="p">);</span> <span class="n">rc</span> <span class="o">=</span> <span class="n">xstat</span><span class="p">(</span><span class="n">gpio_path</span><span class="p">,</span><span class="o">&amp;</span><span class="n">stat_buf</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="n">rc</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span> <span class="n">fn_rc</span> <span class="o">=</span> <span class="nb">true</span><span class="p">;</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="n">fhandle</span> <span class="o">=</span> <span class="n">fopen</span><span class="p">(</span><span class="n">s_</span><span class="o">/</span><span class="n">sys</span><span class="o">/</span><span class="n">class</span><span class="o">/</span><span class="n">gpio</span><span class="o">/</span><span class="n">export_00114198</span><span class="p">,</span><span class="s">"wb"</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="n">fhandle</span> <span class="o">==</span> <span class="p">(</span><span class="kt">FILE</span> <span class="o">*</span><span class="p">)</span><span class="mh">0x0</span><span class="p">)</span> <span class="p">{</span> <span class="n">fn_rc</span> <span class="o">=</span> <span class="nb">true</span><span class="p">;</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="n">len</span> <span class="o">=</span> <span class="n">strlen</span><span class="p">(</span><span class="n">gpio_pin</span><span class="p">);</span> <span class="n">len</span> <span class="o">=</span> <span class="n">fwrite</span><span class="p">(</span><span class="n">gpio_pin</span><span class="p">,</span><span class="n">len</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="n">fhandle</span><span class="p">);</span> <span class="n">local_c</span> <span class="o">=</span> <span class="p">(</span><span class="n">undefined4</span><span class="p">)</span><span class="n">len</span><span class="p">;</span> <span class="n">fclose</span><span class="p">(</span><span class="n">fhandle</span><span class="p">);</span> <span class="n">fn_rc</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span> <span class="p">}</span> <span class="p">}</span> <span class="k">return</span> <span class="n">fn_rc</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>this function seems to handle exporting a gpio pin, using the standard sysfs gpio interface.</p> <p>the parent function, the only one calling <code class="language-plaintext highlighter-rouge">export_gpio_pin</code>, seems to take a list of gpio pins (as a flat string of all things!) to export. i suspect the parameter type is wrong tho, probably it should be an array of some struct. this is also true for <code class="language-plaintext highlighter-rouge">export_gpio_pin</code>.</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">bool</span> <span class="nf">export_gpio_pin_list</span><span class="p">(</span><span class="kt">char</span> <span class="o">*</span><span class="n">gpio_list_maybe</span><span class="p">)</span> <span class="p">{</span> <span class="n">bool</span> <span class="n">bVar1</span><span class="p">;</span> <span class="kt">char</span> <span class="o">*</span><span class="n">gpio_name_i</span><span class="p">;</span> <span class="n">gpio_name_i</span> <span class="o">=</span> <span class="n">gpio_list_maybe</span><span class="p">;</span> <span class="k">while</span><span class="p">(</span> <span class="nb">true</span> <span class="p">)</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="o">*</span><span class="n">gpio_name_i</span> <span class="o">==</span> <span class="sc">'\0'</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="nb">false</span><span class="p">;</span> <span class="p">}</span> <span class="k">if</span> <span class="p">(((</span><span class="o">*</span><span class="n">gpio_name_i</span> <span class="o">!=</span> <span class="sc">'-'</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="p">(</span><span class="o">*</span><span class="p">(</span><span class="kt">int</span> <span class="o">*</span><span class="p">)(</span><span class="n">gpio_name_i</span> <span class="o">+</span> <span class="mh">0x14</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">))</span> <span class="o">&amp;&amp;</span> <span class="p">(</span><span class="n">bVar1</span> <span class="o">=</span> <span class="n">export_gpio_pin</span><span class="p">(</span><span class="n">gpio_name_i</span><span class="p">),</span> <span class="n">bVar1</span><span class="p">))</span> <span class="k">break</span><span class="p">;</span> <span class="n">gpio_name_i</span> <span class="o">=</span> <span class="n">gpio_name_i</span> <span class="o">+</span> <span class="mh">0x20</span><span class="p">;</span> <span class="p">}</span> <span class="k">return</span> <span class="nb">true</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>doing the same for the other function called by <code class="language-plaintext highlighter-rouge">gpio_initialize</code>, we see it also uses the sysfs gpio interface, this time to initialize the direction (and pull-ups?) of the pins. from the code, we also get a lot of clues as to what the misterious structure actually is:</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">bool</span> <span class="nf">set_gpio_mode</span><span class="p">(</span><span class="n">s_gpio_pin_entry</span> <span class="o">*</span><span class="n">gpio_pin</span><span class="p">)</span> <span class="p">{</span> <span class="kt">int</span> <span class="n">strncmp_result</span><span class="p">;</span> <span class="kt">size_t</span> <span class="n">len</span><span class="p">;</span> <span class="kt">char</span> <span class="n">acStack_70</span> <span class="p">[</span><span class="mi">8</span><span class="p">];</span> <span class="kt">char</span> <span class="n">s_none</span> <span class="p">[</span><span class="mi">8</span><span class="p">];</span> <span class="kt">char</span> <span class="n">file_path</span> <span class="p">[</span><span class="mi">84</span><span class="p">];</span> <span class="n">undefined4</span> <span class="n">local_c</span><span class="p">;</span> <span class="kt">FILE</span> <span class="o">*</span><span class="n">fhandle</span><span class="p">;</span> <span class="n">builtin_strncpy</span><span class="p">(</span><span class="n">s_none</span><span class="p">,</span><span class="s">"none"</span><span class="p">,</span><span class="mi">5</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="n">gpio_pin</span><span class="o">-&gt;</span><span class="n">pin_name</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">==</span> <span class="sc">'-'</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="nb">false</span><span class="p">;</span> <span class="p">}</span> <span class="k">if</span> <span class="p">(</span><span class="n">gpio_pin</span><span class="o">-&gt;</span><span class="n">unkn1</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span> <span class="n">sprintf</span><span class="p">(</span><span class="n">file_path</span><span class="p">,</span><span class="n">s_</span><span class="o">/</span><span class="n">sys</span><span class="o">/</span><span class="n">class</span><span class="o">/</span><span class="n">gpio</span><span class="o">/</span><span class="n">gpio</span><span class="o">%</span><span class="n">s</span><span class="o">/</span><span class="n">direction_001141d0</span><span class="p">,</span><span class="n">gpio_pin</span><span class="p">);</span> <span class="n">fhandle</span> <span class="o">=</span> <span class="n">fopen</span><span class="p">(</span><span class="n">file_path</span><span class="p">,</span><span class="s">"wb"</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="n">fhandle</span> <span class="o">==</span> <span class="p">(</span><span class="kt">FILE</span> <span class="o">*</span><span class="p">)</span><span class="mh">0x0</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="nb">true</span><span class="p">;</span> <span class="p">}</span> <span class="n">len</span> <span class="o">=</span> <span class="n">strlen</span><span class="p">(</span><span class="n">gpio_pin</span><span class="o">-&gt;</span><span class="n">direction</span><span class="p">);</span> <span class="n">len</span> <span class="o">=</span> <span class="n">fwrite</span><span class="p">(</span><span class="n">gpio_pin</span><span class="o">-&gt;</span><span class="n">direction</span><span class="p">,</span><span class="n">len</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="n">fhandle</span><span class="p">);</span> <span class="n">local_c</span> <span class="o">=</span> <span class="p">(</span><span class="n">undefined4</span><span class="p">)</span><span class="n">len</span><span class="p">;</span> <span class="n">fclose</span><span class="p">(</span><span class="n">fhandle</span><span class="p">);</span> <span class="c1">// gpio_pin-&gt;direction == "in"</span> <span class="n">strncmp_result</span> <span class="o">=</span> <span class="n">strcmp</span><span class="p">(</span><span class="n">gpio_pin</span><span class="o">-&gt;</span><span class="n">direction</span><span class="p">,</span><span class="s">"in"</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="n">strncmp_result</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span> <span class="n">sprintf</span><span class="p">(</span><span class="n">file_path</span><span class="p">,</span><span class="n">s_</span><span class="o">/</span><span class="n">sys</span><span class="o">/</span><span class="n">class</span><span class="o">/</span><span class="n">gpio</span><span class="o">/</span><span class="n">gpio</span><span class="o">%</span><span class="n">s</span><span class="o">/</span><span class="n">active_lo_00114218</span><span class="p">,</span><span class="n">gpio_pin</span><span class="p">);</span> <span class="n">fhandle</span> <span class="o">=</span> <span class="n">fopen</span><span class="p">(</span><span class="n">file_path</span><span class="p">,</span><span class="s">"wb"</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="n">fhandle</span> <span class="o">==</span> <span class="p">(</span><span class="kt">FILE</span> <span class="o">*</span><span class="p">)</span><span class="mh">0x0</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="nb">true</span><span class="p">;</span> <span class="p">}</span> <span class="n">sprintf</span><span class="p">(</span><span class="n">acStack_70</span><span class="p">,</span><span class="s">"%d"</span><span class="p">,(</span><span class="n">ulong</span><span class="p">)(</span><span class="n">uint</span><span class="p">)</span><span class="n">gpio_pin</span><span class="o">-&gt;</span><span class="n">active_lo</span><span class="p">);</span> <span class="n">len</span> <span class="o">=</span> <span class="n">fwrite</span><span class="p">(</span><span class="n">acStack_70</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="n">fhandle</span><span class="p">);</span> <span class="n">local_c</span> <span class="o">=</span> <span class="p">(</span><span class="n">undefined4</span><span class="p">)</span><span class="n">len</span><span class="p">;</span> <span class="n">fclose</span><span class="p">(</span><span class="n">fhandle</span><span class="p">);</span> <span class="n">sprintf</span><span class="p">(</span><span class="n">file_path</span><span class="p">,</span><span class="n">s_</span><span class="o">/</span><span class="n">sys</span><span class="o">/</span><span class="n">class</span><span class="o">/</span><span class="n">gpio</span><span class="o">/</span><span class="n">gpio</span><span class="o">%</span><span class="n">s</span><span class="o">/</span><span class="n">edge_001141f8</span><span class="p">,</span><span class="n">gpio_pin</span><span class="p">);</span> <span class="n">fhandle</span> <span class="o">=</span> <span class="n">fopen</span><span class="p">(</span><span class="n">file_path</span><span class="p">,</span><span class="s">"wb"</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="n">fhandle</span> <span class="o">==</span> <span class="p">(</span><span class="kt">FILE</span> <span class="o">*</span><span class="p">)</span><span class="mh">0x0</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="nb">true</span><span class="p">;</span> <span class="p">}</span> <span class="n">len</span> <span class="o">=</span> <span class="n">strlen</span><span class="p">(</span><span class="n">s_none</span><span class="p">);</span> <span class="n">len</span> <span class="o">=</span> <span class="n">fwrite</span><span class="p">(</span><span class="n">s_none</span><span class="p">,</span><span class="n">len</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="n">fhandle</span><span class="p">);</span> <span class="n">local_c</span> <span class="o">=</span> <span class="p">(</span><span class="n">undefined4</span><span class="p">)</span><span class="n">len</span><span class="p">;</span> <span class="n">fclose</span><span class="p">(</span><span class="n">fhandle</span><span class="p">);</span> <span class="p">}</span> <span class="c1">// gpio_pin-&gt;direction == "out"</span> <span class="n">strncmp_result</span> <span class="o">=</span> <span class="n">strcmp</span><span class="p">(</span><span class="n">gpio_pin</span><span class="o">-&gt;</span><span class="n">direction</span><span class="p">,</span><span class="s">"out"</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="n">strncmp_result</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span> <span class="n">write_led</span><span class="p">(</span><span class="n">gpio_pin</span><span class="p">,</span><span class="n">gpio_pin</span><span class="o">-&gt;</span><span class="n">initial_level</span><span class="p">);</span> <span class="p">}</span> <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">gpio_pin</span><span class="o">-&gt;</span><span class="n">unkn1</span> <span class="o">==</span> <span class="mi">1</span><span class="p">)</span> <span class="p">{</span> <span class="n">write_led</span><span class="p">(</span><span class="n">gpio_pin</span><span class="p">,</span><span class="n">gpio_pin</span><span class="o">-&gt;</span><span class="n">initial_level</span><span class="p">);</span> <span class="p">}</span> <span class="k">return</span> <span class="nb">false</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>from the clues, i think the struct looks something like this:</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">struct</span> <span class="n">s_gpio_pin_entry</span> <span class="p">{</span> <span class="kt">char</span> <span class="n">pin_name</span><span class="p">[</span><span class="mi">16</span><span class="p">];</span> <span class="c1">// as you'd write to /sys/class/gpio/export</span> <span class="kt">char</span> <span class="n">direction</span><span class="p">[</span><span class="mi">4</span><span class="p">];</span> <span class="c1">// "in" or "out"</span> <span class="kt">int</span> <span class="n">unkn1</span><span class="p">;</span> <span class="c1">// 1 or 0, idk what it does</span> <span class="kt">int</span> <span class="n">active_lo</span><span class="p">;</span> <span class="c1">// 1 or 0, if the pin is active low. "in" direction only</span> <span class="p">}</span> </code></pre></div></div> <p>the calling function also makes a lot of sense with that struct in mind:</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">bool</span> <span class="nf">set_gpio_mode_list</span><span class="p">(</span><span class="n">s_gpio_pin_entry</span> <span class="o">*</span><span class="n">gpio_pins</span><span class="p">)</span> <span class="p">{</span> <span class="n">bool</span> <span class="n">fail</span><span class="p">;</span> <span class="n">s_gpio_pin_entry</span> <span class="o">*</span><span class="n">pin</span><span class="p">;</span> <span class="n">pin</span> <span class="o">=</span> <span class="n">gpio_pins</span><span class="p">;</span> <span class="k">while</span><span class="p">(</span> <span class="nb">true</span> <span class="p">)</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="n">pin</span><span class="o">-&gt;</span><span class="n">pin_name</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">==</span> <span class="sc">'\0'</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="nb">false</span><span class="p">;</span> <span class="p">}</span> <span class="n">fail</span> <span class="o">=</span> <span class="n">set_gpio_mode</span><span class="p">(</span><span class="n">pin</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="n">fail</span><span class="p">)</span> <span class="k">break</span><span class="p">;</span> <span class="n">pin</span> <span class="o">=</span> <span class="n">pin</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span> <span class="p">}</span> <span class="k">return</span> <span class="nb">true</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>those findings also check out with the previous <code class="language-plaintext highlighter-rouge">export_gpio_pin</code> and <code class="language-plaintext highlighter-rouge">export_gpio_pin_list</code> functions:</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">bool</span> <span class="nf">export_gpio_pin</span><span class="p">(</span><span class="n">s_gpio_pin_entry</span> <span class="o">*</span><span class="n">gpio_pin</span><span class="p">)</span> <span class="p">{</span> <span class="n">bool</span> <span class="n">fn_rc</span><span class="p">;</span> <span class="kt">int</span> <span class="n">rc</span><span class="p">;</span> <span class="kt">size_t</span> <span class="n">len</span><span class="p">;</span> <span class="n">stat</span> <span class="o">*</span><span class="n">stat_buf</span><span class="p">;</span> <span class="kt">char</span> <span class="n">gpio_path</span> <span class="p">[</span><span class="mi">84</span><span class="p">];</span> <span class="n">undefined4</span> <span class="n">local_c</span><span class="p">;</span> <span class="kt">FILE</span> <span class="o">*</span><span class="n">fhandle</span><span class="p">;</span> <span class="c1">// exports a gpio pin using /sys/class/gpio/export</span> <span class="n">sprintf</span><span class="p">(</span><span class="n">gpio_path</span><span class="p">,</span><span class="n">s_</span><span class="o">/</span><span class="n">sys</span><span class="o">/</span><span class="n">class</span><span class="o">/</span><span class="n">gpio</span><span class="o">/</span><span class="n">gpio</span><span class="o">%</span><span class="n">s</span><span class="o">/</span><span class="n">value_00114240</span><span class="p">,</span><span class="n">gpio_pin</span><span class="p">);</span> <span class="n">rc</span> <span class="o">=</span> <span class="n">xstat</span><span class="p">(</span><span class="n">gpio_path</span><span class="p">,</span><span class="o">&amp;</span><span class="n">stat_buf</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="n">rc</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span> <span class="n">fn_rc</span> <span class="o">=</span> <span class="nb">true</span><span class="p">;</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="n">fhandle</span> <span class="o">=</span> <span class="n">fopen</span><span class="p">(</span><span class="n">s_</span><span class="o">/</span><span class="n">sys</span><span class="o">/</span><span class="n">class</span><span class="o">/</span><span class="n">gpio</span><span class="o">/</span><span class="n">export_00114198</span><span class="p">,</span><span class="s">"wb"</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="n">fhandle</span> <span class="o">==</span> <span class="p">(</span><span class="kt">FILE</span> <span class="o">*</span><span class="p">)</span><span class="mh">0x0</span><span class="p">)</span> <span class="p">{</span> <span class="n">fn_rc</span> <span class="o">=</span> <span class="nb">true</span><span class="p">;</span> <span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="n">len</span> <span class="o">=</span> <span class="n">strlen</span><span class="p">(</span><span class="n">gpio_pin</span><span class="o">-&gt;</span><span class="n">pin_name</span><span class="p">);</span> <span class="n">len</span> <span class="o">=</span> <span class="n">fwrite</span><span class="p">(</span><span class="n">gpio_pin</span><span class="p">,</span><span class="n">len</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="n">fhandle</span><span class="p">);</span> <span class="n">local_c</span> <span class="o">=</span> <span class="p">(</span><span class="n">undefined4</span><span class="p">)</span><span class="n">len</span><span class="p">;</span> <span class="n">fclose</span><span class="p">(</span><span class="n">fhandle</span><span class="p">);</span> <span class="n">fn_rc</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span> <span class="p">}</span> <span class="p">}</span> <span class="k">return</span> <span class="n">fn_rc</span><span class="p">;</span> <span class="p">}</span> <span class="n">bool</span> <span class="nf">export_gpio_pin_list</span><span class="p">(</span><span class="n">s_gpio_pin_entry</span> <span class="o">*</span><span class="n">gpio_pins</span><span class="p">)</span> <span class="p">{</span> <span class="n">bool</span> <span class="n">fail</span><span class="p">;</span> <span class="n">s_gpio_pin_entry</span> <span class="o">*</span><span class="n">pin</span><span class="p">;</span> <span class="n">pin</span> <span class="o">=</span> <span class="n">gpio_pins</span><span class="p">;</span> <span class="k">while</span><span class="p">(</span> <span class="nb">true</span> <span class="p">)</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="n">pin</span><span class="o">-&gt;</span><span class="n">pin_name</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">==</span> <span class="sc">'\0'</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="nb">false</span><span class="p">;</span> <span class="p">}</span> <span class="k">if</span> <span class="p">(((</span><span class="n">pin</span><span class="o">-&gt;</span><span class="n">pin_name</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">!=</span> <span class="sc">'-'</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="p">(</span><span class="n">pin</span><span class="o">-&gt;</span><span class="n">unkn1</span> <span class="o">==</span> <span class="mi">0</span><span class="p">))</span> <span class="o">&amp;&amp;</span> <span class="p">(</span><span class="n">fail</span> <span class="o">=</span> <span class="n">export_gpio_pin</span><span class="p">(</span><span class="n">pin</span><span class="p">),</span> <span class="n">fail</span><span class="p">))</span> <span class="k">break</span><span class="p">;</span> <span class="n">pin</span> <span class="o">=</span> <span class="n">pin</span> <span class="o">+</span> <span class="mi">1</span><span class="p">;</span> <span class="p">}</span> <span class="k">return</span> <span class="nb">true</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>with that, we arrive at a pretty good understanding of what <code class="language-plaintext highlighter-rouge">gpio_initialize</code> does:</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">bool</span> <span class="nf">gpio_initialize</span><span class="p">(</span><span class="n">s_gpio_pin_entry</span> <span class="o">*</span><span class="n">gpio_list</span><span class="p">)</span> <span class="p">{</span> <span class="n">bool</span> <span class="n">fail</span><span class="p">;</span> <span class="n">fail</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span> <span class="k">if</span> <span class="p">(</span><span class="n">gpio_initialize_ran</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span> <span class="n">gpio_initialize_ran</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span> <span class="n">fail</span> <span class="o">=</span> <span class="n">export_gpio_pin_list</span><span class="p">(</span><span class="n">gpio_list</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">fail</span><span class="p">)</span> <span class="p">{</span> <span class="n">fail</span> <span class="o">=</span> <span class="n">set_gpio_mode_list</span><span class="p">(</span><span class="n">gpio_list</span><span class="p">);</span> <span class="p">}</span> <span class="p">}</span> <span class="k">return</span> <span class="n">fail</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <h2 id="a-thunk-to-remember">A THUNK to remember</h2> <p>all that we need now is to find who calls <code class="language-plaintext highlighter-rouge">gpio_initialize</code>, and what parameter it uses. if we find that parameter, we have everything we need.</p> <p>at first this seems bad, <code class="language-plaintext highlighter-rouge">gpio_initialize</code> only has one caller, and that’s all messed up:</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">gpio_initialize</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span> <span class="n">gpio_initialize</span><span class="p">();</span> <span class="k">return</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>that matches nothing of what we expect.</p> <p>but fear not, this is just some weirdness with how ghidra decompiled the code. looking at the disassemly, there’s a big label calling this thing a “THUNK FUNCTION”. now, what is that?!</p> <table> <thead> <tr> <th><img src="/assets/images/common/reactions/b99-what-does-that-even-mean.gif" alt="me when thunk function" /></th> </tr> </thead> <tbody> <tr> <td>me when thunk function</td> </tr> </tbody> </table> <p>a quick search reveals that, simply put, a thunk function is just a intermediate function that does nothing but call another function. it’s a bit special tho, as it only branches to the target, without messing with the stack or registers. thus, ghidra doesn’t show any parameters or return value, as there technically aren’t any. instead, they are just passed through.</p> <p><code class="language-plaintext highlighter-rouge">gpio_initialize</code> was likely thunked because it’s an export symbol, but that’s just speculation on my part.</p> <h2 id="discovering-the-button-gpios">Discovering The Button GPIOs</h2> <p>anyway, going to the single caller of the thunked <code class="language-plaintext highlighter-rouge">gpio_initialize</code>, we luckily find something that makes more sense:</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">FUN_00101d44</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span> <span class="n">gpio_initialize</span><span class="p">(</span><span class="o">&amp;</span><span class="n">DAT_00114128</span><span class="p">);</span> <span class="n">read_reset</span><span class="p">(</span><span class="o">&amp;</span><span class="n">DAT_00114128</span><span class="p">);</span> <span class="k">return</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>looks like we found our parameter!</p> <p>cleaning it up (and adding the global as a dummy), we get:</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">something_gpio_initialize_and_read_reset</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span> <span class="n">gpio_initialize</span><span class="p">(</span><span class="o">&amp;</span><span class="n">G_GPIO_LIST</span><span class="p">);</span> <span class="n">read_reset</span><span class="p">(</span><span class="n">G_GPIO_LIST</span><span class="p">);</span> <span class="k">return</span><span class="p">;</span> <span class="p">}</span> <span class="n">s_gpio_pin_entry</span> <span class="n">G_GPIO_LIST</span><span class="p">[</span><span class="cm">/*?*/</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span> <span class="c1">// ...</span> <span class="p">};</span> </code></pre></div></div> <p>from the <code class="language-plaintext highlighter-rouge">read_reset</code> call, we can also guess that the first entry will be the reset button, and the rest will be leds. now we just have to type the global, and we’re done.</p> <table> <thead> <tr> <th><img src="/assets/images/hw-hacking/watchguard/t40/libwgpanel/G_GPIO_LIST_but-its-only-one.png" alt="wait, there's only one?" /></th> </tr> </thead> <tbody> <tr> <td>wait, there’s only one?</td> </tr> </tbody> </table> <p>of course, it would’ve been to easy if it actually contains all entries. there must be another global where the leds are defined, and this is just the buttons.</p> <p>but first, let’s note what we’ve found here:</p> <table> <thead> <tr> <th>Pin #</th> <th>Direction</th> <th>Active Low?</th> <th>Initial Level</th> <th>Description</th> </tr> </thead> <tbody> <tr> <td>396</td> <td>in</td> <td>yes</td> <td>-</td> <td>Reset Button</td> </tr> </tbody> </table> <h2 id="a-small-problem">A Small Problem</h2> <p>So, GPIO pin 396 is the reset button, active low, input only. That makes sense, only one problem: the first GPIO number in linux is 512. writing to 396 will just error out.</p> <p>something must have changed to the GPIO numbering scheme from Linux 4.6 (Stock OS) to 6.16 (what I’m using)…</p> <p>another issue is that i cannot find any other globals that look like a list of <code class="language-plaintext highlighter-rouge">s_gpio_pin_entry</code> structs, or another call to <code class="language-plaintext highlighter-rouge">gpio_initialize</code>. i think with a bit more time, i could figure this out, but for now i’ll try something else.</p> <h1 id="poking-around">Poking Around</h1> <p>because the LS1043a only has 128 GPIO lines, i guessed that the gpio numbers in the older kernel would have some offset, and that my reset button was on the lower end. i also took a guess that the leds would be nearby, because why would you route the signals all over the place?</p> <p>with that, i started poking around the GPIOs, randomly writing stuff to them, in the hopes that i don’t kill something important. and suddenly, one of the leds changed!</p> <p>weirdly, when i toggled the same GPIO (539) again, a different led changed. what the heck?</p> <h2 id="figuring-out-the-leds-and-their-shift-register">Figuring Out the LEDs and their Shift Register</h2> <p>a look at the board revealed that near the front panel leds, there’s a LV164A shift register. and we’ve just found the clock pin!</p> <p>let’s just guess and assume that the data pin is either one up or one down from the clock pin. trying both, we find that indeed, GPIO 540 is the data pin!</p> <p>with that, we can now control the front panel leds, by bit-banging the shift register:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span> <span class="nv">SHIFT_CLOCK_PIN</span><span class="o">=</span>539 <span class="nv">DATA_OUT_PIN</span><span class="o">=</span>540 <span class="nv">PATTERN</span><span class="o">=</span><span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span> <span class="k">if</span> <span class="o">[</span> <span class="nt">-z</span> <span class="s2">"</span><span class="nv">$PATTERN</span><span class="s2">"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then </span><span class="nb">echo</span> <span class="s2">"Usage: </span><span class="nv">$0</span><span class="s2"> &lt;bitpattern&gt;"</span> <span class="nb">exit </span>1 <span class="k">fi</span> <span class="o">[</span> <span class="o">!</span> <span class="nt">-d</span> /sys/class/gpio/gpio<span class="nv">$SHIFT_CLOCK_PIN</span> <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s2">"</span><span class="nv">$SHIFT_CLOCK_PIN</span><span class="s2">"</span> <span class="o">&gt;</span> /sys/class/gpio/export <span class="o">[</span> <span class="o">!</span> <span class="nt">-d</span> /sys/class/gpio/gpio<span class="nv">$DATA_OUT_PIN</span> <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s2">"</span><span class="nv">$DATA_OUT_PIN</span><span class="s2">"</span> <span class="o">&gt;</span> /sys/class/gpio/export <span class="nb">echo </span>out <span class="o">&gt;</span> /sys/class/gpio/gpio<span class="nv">$SHIFT_CLOCK_PIN</span>/direction <span class="nb">echo </span>out <span class="o">&gt;</span> /sys/class/gpio/gpio<span class="nv">$DATA_OUT_PIN</span>/direction <span class="k">for</span> <span class="o">((</span><span class="nv">i</span><span class="o">=</span>0<span class="p">;</span> i&lt;<span class="k">${#</span><span class="nv">PATTERN</span><span class="k">}</span><span class="p">;</span> i++<span class="o">))</span><span class="p">;</span> <span class="k">do </span><span class="nv">bit</span><span class="o">=</span><span class="k">${</span><span class="nv">PATTERN</span>:i:1<span class="k">}</span> <span class="k">if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$bit</span><span class="s2">"</span> <span class="o">!=</span> <span class="s2">"0"</span> <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$bit</span><span class="s2">"</span> <span class="o">!=</span> <span class="s2">"1"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then </span><span class="nb">echo</span> <span class="s2">"Invalid bit '</span><span class="nv">$bit</span><span class="s2">' in pattern"</span> <span class="nb">exit </span>2 <span class="k">fi </span><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$bit</span><span class="s2">"</span> <span class="o">&gt;</span> /sys/class/gpio/gpio<span class="nv">$DATA_OUT_PIN</span>/value <span class="nb">echo </span>0 <span class="o">&gt;</span> /sys/class/gpio/gpio<span class="nv">$SHIFT_CLOCK_PIN</span>/value <span class="nb">echo </span>1 <span class="o">&gt;</span> /sys/class/gpio/gpio<span class="nv">$SHIFT_CLOCK_PIN</span>/value <span class="k">done</span> </code></pre></div></div> <p>here’s the patterns for the different leds. note however that the pattern is shifted MSB first, so the first value in the pattern ends up on the last led. secondly, the leds are active low, so a 0 lights up the led.</p> <table> <thead> <tr> <th>Pattern</th> <th>LED</th> </tr> </thead> <tbody> <tr> <td>x000</td> <td>Mode</td> </tr> <tr> <td>0x00</td> <td>Status</td> </tr> <tr> <td>00x0</td> <td>Attention</td> </tr> <tr> <td>000x</td> <td>Failover</td> </tr> </tbody> </table> <p>why the hardware is setup with a shift register, when there’s plenty of GPIOs available, is beyond me. must’ve been cheaper than a bunch of transistors…</p> <h2 id="finding-the-reset-button">Finding the Reset Button</h2> <p>for the reset button, i simply dumped the current GPIO state for all GPIOs not set to output, once with and once without pressing the button. i then passed the outputs into diff, and voila, GPIO 620 is the reset button!</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span> <span class="k">for </span>pin <span class="k">in</span> <span class="o">{</span>512..640<span class="o">}</span><span class="p">;</span> <span class="k">do</span> <span class="o">[</span> <span class="o">!</span> <span class="nt">-d</span> /sys/class/gpio/gpio<span class="nv">$pin</span> <span class="o">]</span> <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s2">"</span><span class="nv">$pin</span><span class="s2">"</span> <span class="o">&gt;</span> /sys/class/gpio/export <span class="nb">dir</span><span class="o">=</span><span class="si">$(</span><span class="nb">cat</span> /sys/class/gpio/gpio<span class="nv">$pin</span>/direction<span class="si">)</span> <span class="k">if</span> <span class="o">[</span> <span class="s2">"</span><span class="nv">$dir</span><span class="s2">"</span> <span class="o">!=</span> <span class="s2">"in"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then continue fi </span><span class="nv">val</span><span class="o">=</span><span class="si">$(</span><span class="nb">cat</span> /sys/class/gpio/gpio<span class="nv">$pin</span>/value<span class="si">)</span> <span class="nb">echo</span> <span class="s2">"#</span><span class="nv">$pin</span><span class="s2"> = </span><span class="nv">$val</span><span class="s2">"</span> <span class="k">done</span> </code></pre></div></div> <h1 id="tldr-pin-mapping">TL;DR Pin Mapping</h1> <p>the pin mapping is:</p> <table> <thead> <tr> <th>Pin #</th> <th>Direction</th> <th>Description</th> </tr> </thead> <tbody> <tr> <td>539</td> <td>out</td> <td>LED Shift Register Clock</td> </tr> <tr> <td>540</td> <td>out</td> <td>LED Shift Register Data</td> </tr> <tr> <td>620</td> <td>in</td> <td>Reset Button, low when pressed</td> </tr> </tbody> </table> <p>the leds are controlled by a LV164A shift register, the first four outputs connect to the front panel leds. outputs of the leds are inverted, so a low level lights up the led.</p> <table> <thead> <tr> <th>LV164A Pin</th> <th>LED</th> </tr> </thead> <tbody> <tr> <td>$Q_A$</td> <td>Failover</td> </tr> <tr> <td>$Q_B$</td> <td>Attention</td> </tr> <tr> <td>$Q_C$</td> <td>Status</td> </tr> <tr> <td>$Q_D$</td> <td>Mode</td> </tr> </tbody> </table> <h1 id="controlling-the-front-panel-from-u-boot">Controlling the Front Panel from U-Boot</h1> <p>after doing all that, i noticed that the U-Boot of the T40 has a convenient <code class="language-plaintext highlighter-rouge">74lv164</code> command for controlling the shift register, and with that, the front panel leds. would’ve been nice to know that earlier, but oh well.</p> <p>command usage is simple:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">=&gt;</span> 74lv164 low <span class="c">#-&gt; all leds on</span> <span class="o">=&gt;</span> 74lv164 high <span class="c">#-&gt; all leds off</span> <span class="o">=&gt;</span> 74lv164 qa 0 <span class="c">#-&gt; failover</span> <span class="o">=&gt;</span> 74lv164 qb 0 <span class="c">#-&gt; attn</span> <span class="o">=&gt;</span> 74lv164 qc 0 <span class="c">#-&gt; status</span> <span class="o">=&gt;</span> 74lv164 qd 0 <span class="c">#-&gt; mode</span> </code></pre></div></div> <h1 id="conclusion">Conclusion</h1> <p>while i would’ve liked to fully reverse-engineer the <code class="language-plaintext highlighter-rouge">libwgpanel.so</code> library, but even if i found the pin mapping there, they wouldn’t map to the current kernel’s GPIO numbering anyway. while my final solution isn’t very elegant, it works, and so i think i’ll leave it at that.</p> Running Linux on a WatchGuard T40 2025-10-03T00:00:00+00:00 https://shadow578.github.io/2025/10/03/linux-on-a-watchguard-t40 <p>as if my obsession with watchguard firewalls wasn’t bad enough already, i recently got my hands on a WatchGuard T40. since i have no use for them as just an appliance, i decided it would be fun to get linux running on it. oh, how wrong i was…</p> <h2 id="hardware-overview">Hardware Overview</h2> <p>the T40 is already discussed in the <a href="https://forum.openwrt.org/t/installing-openwrt-on-watchguard-t40w/230048">OpenWRT forums</a>, with the general consensus being that it’d make a great OpenWRT device, if only it was supported. the hardware information claimed on the forums (as to whether they’re correct, we’ll see later) is as follows:</p> <ul> <li>Board: LS1043a QDS <ul> <li><a href="https://www.nxp.com/products/LS1043A">LS1043A</a> Layerscape SOC</li> </ul> </li> <li>Storage: mSATA 16GB</li> <li>Bootloader: Custom U-Boot for WatchGuard, locked down to only boot from internal storage</li> <li>Stock device tree is available in <a href="https://forum.openwrt.org/t/installing-openwrt-on-watchguard-t40w/230048/12">a post</a> <ul> <li>“it’s pretty much the <code class="language-plaintext highlighter-rouge">LS1043A-RDB</code> one from upstream linux”</li> </ul> </li> </ul> <p>now, here’s what i found after investigating my own T40:</p> <ul> <li>LS1043A CPU, as expected</li> <li>4GB RAM, also expected</li> <li>16GB M.2 SATA SSD (idk how they got mSATA…)</li> <li>1 x Atheros AR8035 GbE PHY (WAN port)</li> <li>1 x Marvell 88E1510 Quad GbE PHY (LAN1-4 ports)</li> <li>Bootloader is indeed a custom U-Boot, but easy to bypass (see below)</li> </ul> <h2 id="accessing-the-u-boot-shell">Accessing the U-Boot Shell</h2> <p>while the U-Boot is indeed locked down and only allows booting from the internal SATA SSD, there’s a little quirk: if - for any reason - U-Boot fails to boot the OS, it will drop to the U-Boot shell. if only we could make the boot fail…</p> <p>one thing to make the boot fail is to simply remove the SSD. boot it up without the SSD, and it will drop to the U-Boot shell. from there, we can simply override the boot command to something else. i chose to set it to <code class="language-plaintext highlighter-rouge">true</code>, which does nothing and drops me into a shell immediately.</p> <p>funnily enough, the boot option for SysB is called <code class="language-plaintext highlighter-rouge">Recovery / Diagnostic Mode</code>. a unprotected shell is a diagnostic mode, right?</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">env set </span>wgBootSysB <span class="s1">'true'</span> <span class="nb">env </span>save reset </code></pre></div></div> <p>from here on, the SSD can be reinserted, as we have a persistent way to drop into the U-Boot shell.</p> <p>on the T20, i believe the boot device is a NAND flash chip soldered to the board. thus, it’s not really feasible to remove it. however, maybe something similar to the technique used for hacking the Nintendo Wii (see: <a href="http://web.archive.org/web/20090505005003/www.atomicmpc.com.au/Tools/Print.aspx?CIID=102079">Team Twiizers and the tweezer attack for the Wii</a>) could work here - i.e. shorting some pins on the NAND flash to make it temporarily unreadable.</p> <h2 id="initial-linux-boot">Initial Linux Boot</h2> <p>we have a U-Boot shell, so what now? one way i previously tackled this was to build a custom buildroot distro, put it on a USB stick, and boot from that. but that is kind of annoying, and also means i have to recompile every time i want to add some new software.</p> <p>instead, i opted to boot <a href="https://archlinuxarm.org">Arch Linux ARM</a> and the generic ARM64 image they provide. this gives me a full linux distro, with a package manager and everything, without having to recompile anything myself. well, except for a few caveats:</p> <ul> <li>while the generic ARM64 kernel image kinda works, it doesn’t really have the driver support for the LS1043A. Things like the SATA controller or networking won’t work.</li> <li>there is no device tree for the T40 included. luckily, we can just use the stock one - either extracted from the original firmware, or the one uploaded on the OpenWRT forums.</li> <li>the initramfs format is not compatible with U-Boot. we can fix this by converting it to a uImage format using <code class="language-plaintext highlighter-rouge">mkimage</code>.</li> </ul> <p>to summarize, prepare a USB stick as follows:</p> <ul> <li>download the generic <a href="https://archlinuxarm.org/platforms/armv8/generic">Arch Linux ARM</a> image</li> <li>extract to a usb stick using <code class="language-plaintext highlighter-rouge">bsdtar -xpf ArchLinuxARM-aarch64-latest.tar.gz -C /mnt/my_usb_stick</code></li> <li>convert initramfs using <code class="language-plaintext highlighter-rouge">mkimage -A arm64 -O linux -T ramdisk -C gzip -d /mnt/my_usb_stick/boot/initramfs-linux.img /mnt/my_usb_stick/boot/initramfs.uImage</code>.</li> <li>(compile and) copy the T40 device tree to <code class="language-plaintext highlighter-rouge">/mnt/my_usb_stick/boot/wg_t40.dtb</code>.</li> </ul> <p>now, we can plug the usb stick into the T40, and boot from it using U-Boot:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># start and scan usb</span> usb start <span class="c"># ensure correct load addresses</span> <span class="nb">env set </span>kernel_addr_r 0x80080000 <span class="nb">env set </span>fdt_addr_r 0x90000000 <span class="nb">env set </span>ramdisk_addr_r 0x94000000 <span class="c"># load kernel, dtb and initramfs from usb</span> load usb 0:1 <span class="k">${</span><span class="nv">kernel_addr_r</span><span class="k">}</span> /boot/Image load usb 0:1 <span class="k">${</span><span class="nv">fdt_addr_r</span><span class="k">}</span> /boot/wg_t40.dtb load usb 0:1 <span class="k">${</span><span class="nv">ramdisk_addr_r</span><span class="k">}</span> /boot/initramfs.uImage <span class="c"># ensure bootargs are correct. can theoretically be omitted</span> <span class="nb">env set </span>bootargs <span class="nv">console</span><span class="o">=</span>ttyS0,115200 <span class="nv">earlycon</span><span class="o">=</span>uart8250,mmio,0x21c0500 <span class="nv">root</span><span class="o">=</span>/dev/sda1 rw rootwait <span class="c"># boot the kernel image</span> booti <span class="k">${</span><span class="nv">kernel_addr_r</span><span class="k">}</span> <span class="k">${</span><span class="nv">ramdisk_addr_r</span><span class="k">}</span> <span class="k">${</span><span class="nv">fdt_addr_r</span><span class="k">}</span> </code></pre></div></div> <p>with that, linux should boot up kinda fine. of course, without proper drivers and a outdated device tree, not much more can be done. there’s no networking, no sata, and everything is kinda limited.</p> <p>but it does run linux!</p> <h2 id="a-custom-device-tree">A Custom Device Tree</h2> <p>for the custom device tree, i started with the <a href="https://github.com/neggles/openwrt/commit/0e2ba8ef96acc7a6c7fcb57319e89563decea333#diff-73fafdd684ce62f3c48c572d7966775d7a9ef262213ecfea4bd6b75c58bc678c">work done by @neggles</a>. i still think this was the right move, even tho it caused me a lot of headaches later on. what didn’t make things better was that at this point in time, i also used a custom compiled kernel, which of course had a different configuration than the one provided by Arch Linux ARM.</p> <h3 id="a-first-hiccup-op-tee">A First Hiccup: OP-TEE</h3> <p>running a custom kernel and device tree, i thought i’d get at least as far as i did before. but nope, it simply crashes early during boot.</p> <table> <thead> <tr> <th><img src="/assets/images/common/reactions/this-is-fine.jpg" alt="this is fine?" /></th> </tr> </thead> <tbody> <tr> <td><em>this is fine?</em></td> </tr> </tbody> </table> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[ 4.220318] optee: probing for conduit method. [ 4.231807] Internal error: Oops - Undefined instruction: 0000000002000000 [#1] SMP [ 4.239559] Modules linked in: [ 4.242619] CPU: 2 UID: 0 PID: 1 Comm: swapper/0 Not tainted 6.17.0-rc6-00266-gcd89d487374c #1 PREEMPT [ 4.257512] pstate: 80000005 (Nzcv daif -PAN -UAO -TCO -DIT -SSBS BTYPE=--) [ 4.264481] pc : __arm_smccc_smc+0x4/0x30 [ 4.268504] lr : optee_smccc_smc+0x1c/0x2c [ 4.272605] sp : ffff800081aabac0 [ 4.275917] x29: ffff800081aabad0 x28: ffff8000815c9068 x27: ffff8000814f00b0 [ 4.283069] x26: ffff8000819c2000 x25: ffff000800100810 x24: 0000000000000000 [ 4.290220] x23: ffff000800100800 x22: 0000000000000000 x21: ffff800081aabb28 [ 4.297370] x20: ffff80008198f4c0 x19: ffff800080cdd550 x18: 0000000000000006 [ 4.304519] x17: ffff80008194dfc0 x16: 0000000014d92057 x15: ffff800081aab500 [ 4.311668] x14: ffffffffffffffff x13: 0000000000000038 x12: 0101010101010101 [ 4.318819] x11: 7f7f7f7f7f7f7f7f x10: 00007fff7e1fa464 x9 : 0000000000000018 [ 4.325969] x8 : ffff800081aabb28 x7 : 0000000000000000 x6 : 0000000000000000 [ 4.333119] x5 : 0000000000000000 x4 : 0000000000000000 x3 : 0000000000000000 [ 4.340267] x2 : 0000000000000000 x1 : 0000000000000000 x0 : 00000000bf00ff01 [ 4.347418] Call trace: [ 4.349862] __arm_smccc_smc+0x4/0x30 (P) [ 4.353881] optee_probe+0xd4/0x970 [ 4.357373] platform_probe+0x5c/0x98 [ 4.361044] really_probe+0xbc/0x29c [ 4.364624] __driver_probe_device+0x78/0x12c [ 4.368987] driver_probe_device+0xd8/0x15c [ 4.373176] __driver_attach+0x90/0x19c [ 4.377017] bus_for_each_dev+0x7c/0xe0 [ 4.380857] driver_attach+0x24/0x30 [ 4.384436] bus_add_driver+0xe4/0x208 [ 4.388188] driver_register+0x5c/0x124 [ 4.392028] __platform_driver_register+0x24/0x30 [ 4.396740] optee_smc_abi_register+0x1c/0x28 [ 4.401102] optee_core_init+0x28/0x68 [ 4.404858] do_one_initcall+0x80/0x1c8 [ 4.408699] kernel_init_freeable+0x204/0x2e0 [ 4.413066] kernel_init+0x20/0x1d8 [ 4.416565] ret_from_fork+0x10/0x20 [ 4.420148] Code: d53cd045 d53cd042 d53cd043 d503245f (d4000003) [ 4.426247] ---[ end trace 0000000000000000 ]--- [ 4.430910] Kernel panic - not syncing: Attempted to kill init! exitcode=0x0000000b [ 4.438576] SMP: stopping secondary CPUs [ 4.442507] Kernel Offset: disabled [ 4.445993] CPU features: 0x000000,00080000,20002000,0400421b [ 4.451742] Memory Limit: none [ 4.454797] ---[ end Kernel panic - not syncing: Attempted to kill init! exitcode=0x0000000b ]-- </code></pre></div></div> <p>what the heck is <code class="language-plaintext highlighter-rouge">optee</code>? this is were my inexperience with linux, especially on ARM, got really apparent to me. i spent forever on this, trying to figure out what the heck is going on.</p> <p>well, turns out that linux expected to be booted by a trusted execution environment (TEE), essentially secure boot for ARM. but u-boot does not do that.</p> <p>because the way that OP-TEE in linux works, it calls into the TEE using a special SMC instruction. without the TEE actually there, and the CPU not configured for it, this instruction is undefined instead, and the kernel panics.</p> <p>so, why is OP-TEE even enabled? well, the culprit is in the base device tree fragment for LS1043a, defined by the linux kernel (<code class="language-plaintext highlighter-rouge">fsl-ls1043a.dtsi</code>):</p> <pre><code class="language-dts">/ { // (...) firmware { optee { compatible = "linaro,optee-tz"; method = "smc"; }; }; }; </code></pre> <p>this basically tells the kernel that there’s a TEE available, and it should use it. removing this node makes the kernel boot just fine, and is easily done by adding the following to our custom device tree:</p> <pre><code class="language-dts">/delete-node/ &amp;{/firmware/optee}; </code></pre> <p>with that, the kernel at least boots without panicking. however, we still not back to where we were before, as now usb enumeration doesn’t work anymore.</p> <h3 id="the-pain-of-usb-enumeration-issues">The Pain of USB Enumeration Issues</h3> <p>normally, usb enumeration not working wouldn’t be that much of a dealbreaker. sure, i’d like to have usb working, but it’s not essential.</p> <p>except in this case, it is. with SATA still not working, the place i put my root filesystem was on a usb stick. and that usb stick is not being detected at all. so, no usb = no root filesystem = no linux.</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>:: mounting '/dev/sdb1' on real root mount: /new_root: fsconfig() failed: /dev/sdb1: Can't lookup blockdev. dmesg(1) may have more information after failed mount system call. ERROR: Failed to mount '/dev/sdb1' on real root You are now being dropped into an emergency shell. </code></pre></div></div> <p>so what’s going on here? the LS1043a has tree build-in usb controller, all of which are based on the Synopsys DesignWare Core 3 USB controller IP (<code class="language-plaintext highlighter-rouge">snps,dwc3</code>). this controller supports both USB2 and USB3, is configurable as host or device, and is handled by the <code class="language-plaintext highlighter-rouge">USB_DWC3</code> linux kernel driver.</p> <p>all of that is <em>completely</em> irrelevant here, as the issue was something completely unrelated to the usb controller itself. this took me forever to figure out, at least two weeks of research and trial-and-error. nothing worked, until i randomly decided to remove the <code class="language-plaintext highlighter-rouge">dma-coherent</code> property from the <code class="language-plaintext highlighter-rouge">soc</code> node in the device tree. no idea why, but that made usb enumeration work again.</p> <p>the watchguard dts also doesn’t have that property, so maybe this is a bug in the upstream dts file? i don’t know, but at least it works now.</p> <p>in the custom device tree, we add the following and continue - confused as to why it works:</p> <pre><code class="language-dts">&amp;soc { /delete-property/ dma-coherent; }; </code></pre> <p>there’s one good thing about this whole ordeal: i now am a lot more comfortable with device trees, decompiled and in source form.</p> <h2 id="custom-linux-kernel-configuration">Custom Linux Kernel Configuration</h2> <p>until now, i simply used the ARM64 defconfig of linux. with that, lots of things don’t work, things as simple as usb network adapters.</p> <p>to fix this, i extracted the kernel config from the Arch Linux ARM image, and used that as a base for my own custom kernel config. with this, i have a sensible base to start from, and can now enable things as needed. first on that list is the SATA controller, so i can access the internal SSD.</p> <h3 id="the-sata-controller">The SATA Controller</h3> <p>this is super simple. the device tree already has the correct node for the SATA controller, and it’s not even broken. we simply need to enable the QORIQ AHCI kernel driver with <code class="language-plaintext highlighter-rouge">CONFIG_AHCI_QORIQ=y</code>, and the SATA controller gets detected.</p> <p>Note: QORIQ is NXP’s marketing name for some of their SoCs, including the LS1043a.</p> <h3 id="thermals">Thermals</h3> <p>it’d be nice to have thermal monitoring, but by default the sensors are not picked up. for this, we again simply need to enable the QORIQ thermal driver with <code class="language-plaintext highlighter-rouge">CONFIG_QORIQ_THERMAL=y</code>. with this, the thermal sensors are detected and available in <code class="language-plaintext highlighter-rouge">/sys/class/thermal/</code>.</p> <h3 id="networking-i-quad-phy-lan1-4">Networking I: Quad PHY (LAN1-4)</h3> <p>this one’s easy, as the T40 borrows the quad ethernet PHY from the LS1043A-RDB reference design.</p> <p>on both the RDB as well as the T40, four of the ethernet ports are handled by a Quad Ethernet PHY - in the case of the T40, a Marvell 88E1510. simply copying over the ethernet-related parts of the RDB’s device tree over to our custom T40 device tree, replacing the mdio stuff done by neggles, makes these four ports work just fine.</p> <p>of course, we also need to enable the corresponding kernel drivers for Freescale’s FMan (<code class="language-plaintext highlighter-rouge">CONFIG_FSL_FMAN=y</code>), but that’s it. four of five ethernet ports working, not bad.</p> <h3 id="networking-ii-atheros-phy-wan0">Networking II: Atheros PHY (WAN0)</h3> <p>remember the OpenWRT forum post from earlier, the one claiming the T40 is basically just a LS1043A-RDB? well, when it comes to the WAN port, that’s not true at all.</p> <p>the LS1043A-RDB uses an (kinda impressive tbh) Marvell AQR105 10GBASE-T PHY in place of the T40’s Atheros AR8035. that means that, understandably, this part of the device tree doesn’t match at all. different PHY, protocols, driver, everything.</p> <p>luckily, the AR8035 is supported by the linux kernel out of the box, and thanks to being a pretty common PHY, there’s some other boards (even Layerscape ones) using it as well. one such board is the Kontron SMARC-sAL28. although it uses the LS1028a, that doesn’t matter much since both share the same networking infrastructure. that’s enough to give the clues that we need <code class="language-plaintext highlighter-rouge">phy-mode = "rgmii-id";</code>, as well as defining <code class="language-plaintext highlighter-rouge">eee-broken-1000t</code> and <code class="language-plaintext highlighter-rouge">eee-broken-100tx</code> to work around some <a href="https://patchwork.kernel.org/project/linux-omap/patch/[email protected]/">issues with Energy Efficient Ethernet</a>. i initally also included <code class="language-plaintext highlighter-rouge">qca,clk-out-frequency</code> and related properties, but after closer inspection of the board it turns out that the clock output of the AR8035 is not connected to anything, so those properties are not needed.</p> <p>now just a single question remains: where to put this in the device tree? here, u-boot can actually help us out. running <code class="language-plaintext highlighter-rouge">mdio list</code> in u-boot shows us how the PHYs are connected from u-boot’s POV:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">=&gt;</span> mdio list FSL_MDIO0: 4 - Generic PHY &lt;<span class="nt">--</span><span class="o">&gt;</span> FM1@DTSEC1 5 - Generic PHY &lt;<span class="nt">--</span><span class="o">&gt;</span> FM1@DTSEC2 6 - Generic PHY &lt;<span class="nt">--</span><span class="o">&gt;</span> FM1@DTSEC5 7 - Generic PHY &lt;<span class="nt">--</span><span class="o">&gt;</span> FM1@DTSEC6 FM_TGEC_MDIO: 1 - AR8035 &lt;<span class="nt">--</span><span class="o">&gt;</span> FM1@DTSEC3 </code></pre></div></div> <p>the AR8035 is connected to <code class="language-plaintext highlighter-rouge">FM1@DTSEC3</code>, which is the third ethernet port of the first FMan instance, or <code class="language-plaintext highlighter-rouge">ethernet@e4000</code> in the device tree. with a bit of guess-work (and consulting the stock T40 device tree), we can then figure out that the MDIO is <code class="language-plaintext highlighter-rouge">mdio@fd000</code> (<code class="language-plaintext highlighter-rouge">xmdio0</code>).</p> <p>adding all that, and we finally get a link working on the WAN port as well.</p> <p>as for if the link also runs at gigabit speeds, i have no idea. my current setup is so janky that i can only test with 100Mbit/s. that, however, works just fine.</p> <h3 id="overclocking-the-cpu">Overclocking the CPU</h3> <p>the LS1043a SoC of the T40 is clocked at only 1.0GHz, but would be capable of running at up to 1.6GHz. sadly, the clock speed cannot be increased directly from linux - the clock scaling drive can only go down. to change the clock speed, the RCW (Reset Configuration Word) values in flash need to be modified. see <a href="/2025/10/12/t40-overclocking/">this follow-up post</a> for my journey into that.</p> <h3 id="the-front-panel">The Front-Panel</h3> <p>the T40 has a “front” panel (technically, parts of it are at the back) consisting of the reset button, and four status LEDs (Failover, ATTN, Status, Mode). theres of course also the many leds for the ethernet link status, but they’re controlled by the PHYs themselves and are not really interesting. to get the front-panel leds to work, we need to control the correct GPIOs. see <a href="/2025/10/04/t40-frontpanel-reverse-engineering/">this follow-up post</a> for more information on that.</p> <h2 id="conclusion-and-next-steps">Conclusion and Next Steps</h2> <p>linux now works pretty well on the T40. i still need to confirm that the PHYs can actually do gigabit speeds, but other than that, everything seems to be in order.</p> <p>as for next steps, i might try to do a proper port of OpenWRT to the T40. but that will probably take a bit, as i’m not very familiar with the OpenWRT build system.</p> <p>if you want to try it out yourself, you can find my linux fork with the custom kernel config and device tree <a href="https://github.com/shadow578/linux/tree/v6.16_wg-t40">on github</a>. for the rootfs, i recommend using the generic AARCH64 Arch Linux ARM image, as described above.</p> Reverse-Engineering the Front Panel of a WatchGuard T70 2025-09-18T00:00:00+00:00 https://shadow578.github.io/2025/09/18/reverse-engineering-frontpanel <p>in the previous posts, we explored how to jailbreak a watchguard t70 and run linux on it. sadly, by default the front panel (leds, reset button) are not functional in linux. let’s change that by reverse-engineering the custom kernel module that watchguard uses in their own os.</p> <h2 id="finding-the-module">Finding the Module</h2> <blockquote> <p>Note:<br /> for whatever reason, i decided to do this on a T55 first. since it’s basically the same hardware, everything applies to the T70 as well.</p> </blockquote> <p>just by taking a quick look at the bootlog of the stock os we find a very interesting line:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>LED/Reset Button Driver for MB-UP2010W... </code></pre></div></div> <p>that sounds like exactly what we are looking for! searching a bit around the line, we can see that the module seems to be loaded by a init script <code class="language-plaintext highlighter-rouge">/etc/runlevel/1/S09sled_drv</code>, as well as some debug output from the module itself:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[ 17.023341] Running /etc/runlevel/1/S09sled_drv... [ 16.645496] sled_drv_t55: loading out-of-tree module taints kernel. [ 16.652895] &lt;chv_pinctrl_probe&gt; Invoked! [ 16.657302] &lt;chv_pinctrl_probe&gt;: probe res check, IORESOURCE_MEM: start=00000000fed80000, end=00000000fed87fff, name=INT33FF:00 [ 16.670182] &lt;chv_pinctrl_probe&gt;: probe res check pctrl-&gt;regs = ffffc90000430000 [ 16.678364] &lt;chv_pinctrl_probe&gt;: probe res check, IORESOURCE_MEM: start=00000000fed88000, end=00000000fed8ffff, name=INT33FF:01 [ 16.691219] &lt;chv_pinctrl_probe&gt;: probe res check pctrl-&gt;regs = ffffc90000440000 [ 16.699397] LED/Reset Button Driver for MB-UP2010W... </code></pre></div></div> <p>looking at the init script, it’s fairly simple and just loads a kernel module <code class="language-plaintext highlighter-rouge">/lib/drivers/sled_drv-t55.ko</code> or <code class="language-plaintext highlighter-rouge">/lib/drivers/sled_drv-t70.ko</code> depending on the model. the following is pseudo-code of the script, as i’m not quite sure about the copyright status.</p> <div class="language-lua highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">dir</span><span class="o">=</span><span class="s2">"/lib/drivers"</span> <span class="n">model</span><span class="o">=</span><span class="n">wg</span><span class="p">.</span><span class="n">get_model</span><span class="p">()</span> <span class="k">if</span> <span class="p">(</span><span class="n">model</span> <span class="o">==</span> <span class="s2">"T70"</span><span class="p">)</span> <span class="k">then</span> <span class="n">wg</span><span class="p">.</span><span class="n">insmod</span><span class="p">(</span><span class="n">dir</span> <span class="o">..</span> <span class="s2">"sled_drv-t70.ko"</span><span class="p">)</span> <span class="k">elseif</span> <span class="p">(</span><span class="n">model</span> <span class="o">==</span> <span class="s2">"T55"</span> <span class="ow">or</span> <span class="n">model</span> <span class="o">==</span> <span class="s2">"T55-W"</span><span class="p">)</span> <span class="k">then</span> <span class="n">wg</span><span class="p">.</span><span class="n">insmod</span><span class="p">(</span><span class="n">dir</span> <span class="o">..</span> <span class="s2">"sled_drv-t55.ko"</span><span class="p">)</span> <span class="k">end</span> </code></pre></div></div> <p>so, the module we are looking for is <code class="language-plaintext highlighter-rouge">/lib/drivers/sled_drv-t70.ko</code> and <code class="language-plaintext highlighter-rouge">/lib/drivers/sled_drv-t55.ko</code> (both are present on both models, as they use the same firmware image). now, let’s extract the module and load it into ghidra.</p> <h2 id="understanding-the-module">Understanding the Module</h2> <p>looking at the (what i assume to be) entry point of the module, we can see that it maps some (I/O) memory regions, then calls a function <code class="language-plaintext highlighter-rouge">sled_init</code>. this already is very useful information, as on intel braswell (the cpu), gpio is handled through memory-mapped i/o. even better, referencing <a href="https://github.com/coreboot/coreboot/blob/main/src/soc/intel/braswell/include/soc/iomap.h"><code class="language-plaintext highlighter-rouge">iomap.h</code> of the coreboot project</a>, we can see that the start addresses of the mapped regions correspond to the gpio controllers, specifically the GPIO Communities SouthWest (at 0xfed80000) and North (at 0xfed88000).</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">ulong</span> <span class="nf">likely_entry_point</span><span class="p">(</span><span class="kt">long</span> <span class="n">param_1</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// (...)</span> <span class="n">printk</span><span class="p">(</span><span class="s">"&lt;%s&gt; Invoked!</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span><span class="s">"chv_pinctrl_probe"</span><span class="p">);</span> <span class="c1">// (...)</span> <span class="c1">// for each resource</span> <span class="k">do</span> <span class="p">{</span> <span class="n">printk</span><span class="p">(</span><span class="s">"&lt;%s&gt;: probe res check, IORESOURCE_MEM: start=%p, end=%p, name=%s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="s">"chv_pinctrl_probe"</span><span class="p">,</span><span class="o">*</span><span class="n">current_res</span><span class="p">,</span><span class="n">current_res</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span><span class="n">current_res</span><span class="p">[</span><span class="mi">2</span><span class="p">]);</span> <span class="c1">// -&gt; &lt;chv_pinctrl_probe&gt;: probe res check, IORESOURCE_MEM:</span> <span class="c1">// start=00000000fed80000, end=00000000fed87fff, name=INT33FF:00</span> <span class="c1">// -&gt; &lt;chv_pinctrl_probe&gt;: probe res check, IORESOURCE_MEM:</span> <span class="c1">// start=00000000fed88000, end=00000000fed8ffff, name=INT33FF:01</span> <span class="n">rc</span> <span class="o">=</span> <span class="n">devm_ioremap_resource</span><span class="p">(</span><span class="n">dev</span><span class="p">,</span><span class="n">current_res</span><span class="p">);</span> <span class="o">*</span><span class="n">res_ptr</span> <span class="o">=</span> <span class="n">rc</span><span class="p">;</span> <span class="n">printk</span><span class="p">(</span><span class="s">"&lt;%s&gt;: probe res check pctrl-&gt;regs = %p</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span><span class="s">"chv_pinctrl_probe"</span><span class="p">,</span><span class="n">rc</span><span class="p">);</span> <span class="c1">// -&gt; &lt;chv_pinctrl_probe&gt;: probe res check pctrl-&gt;regs = ffffc90000430000</span> <span class="c1">// -&gt; &lt;chv_pinctrl_probe&gt;: probe res check pctrl-&gt;regs = ffffc90000440000</span> <span class="n">current_res</span> <span class="o">=</span> <span class="n">current_res</span> <span class="o">+</span> <span class="mi">8</span><span class="p">;</span> <span class="p">}</span> <span class="k">while</span> <span class="p">((</span><span class="n">ledmappings_entry</span> <span class="o">*</span><span class="p">)</span><span class="n">current_res</span> <span class="o">!=</span> <span class="n">ledmappings</span><span class="p">);</span> <span class="c1">// (...)</span> <span class="n">sled_init</span><span class="p">((</span><span class="n">undefined</span> <span class="o">*</span><span class="p">)</span><span class="n">likely_pctrl</span><span class="p">);</span> <span class="p">}</span> </code></pre></div></div> <p>next, looking at <code class="language-plaintext highlighter-rouge">sled_init</code>, we see a character device is registered. that must be how the front panel is controlled from userspace. (side-note: there’s actually a whole library, <code class="language-plaintext highlighter-rouge">t55-libwgpanel.so</code>, that abstracts the ioctl calls to the character device, but we won’t look at that here.)</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int</span> <span class="nf">sled_init</span><span class="p">(</span><span class="n">undefined</span> <span class="o">*</span><span class="n">likely_pctrl</span><span class="p">)</span> <span class="p">{</span> <span class="kt">int</span> <span class="n">err</span><span class="p">;</span> <span class="n">printk</span><span class="p">(</span><span class="s">"</span><span class="se">\x015</span><span class="s">LED/Reset Button Driver for MB-UP2010W...</span><span class="se">\n</span><span class="s">"</span><span class="p">);</span> <span class="n">err</span> <span class="o">=</span> <span class="n">__register_chrdev</span><span class="p">(</span><span class="mh">0xf0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mh">0x100</span><span class="p">,</span><span class="s">"sled_drv"</span><span class="p">,</span><span class="o">&amp;</span><span class="n">chrdev_fops</span><span class="p">);</span> <span class="c1">// (...)</span> <span class="p">}</span> </code></pre></div></div> <p>(partially) defining the <code class="language-plaintext highlighter-rouge">file_operations</code> struct and correctly typing the <code class="language-plaintext highlighter-rouge">chrdev_fops</code> global, we see that <code class="language-plaintext highlighter-rouge">compat_ioctl</code>, <code class="language-plaintext highlighter-rouge">flush</code> and <code class="language-plaintext highlighter-rouge">fsync</code> are implemented. the latter two aren’t really interesting, but <code class="language-plaintext highlighter-rouge">compat_ioctl</code> is where the actual functionality is implemented. basically, it boils down to calling one of two functions (<code class="language-plaintext highlighter-rouge">set_led_off</code> and <code class="language-plaintext highlighter-rouge">set_led_on</code>), depending on the command and argument passed to the ioctl.</p> <table> <thead> <tr> <th><img src="/assets/images/hw-hacking/watchguard/t70/sled_drv_chrdev_fops.png" alt="chrdev_fops global" /></th> </tr> </thead> <tbody> <tr> <td><code class="language-plaintext highlighter-rouge">chrdev_fops</code> global</td> </tr> </tbody> </table> <p>we’re almost there! <code class="language-plaintext highlighter-rouge">set_led_on</code> and <code class="language-plaintext highlighter-rouge">set_led_off</code> are quite simple, they just call a lookup function <code class="language-plaintext highlighter-rouge">findled</code> to (presumably) convert an led id to a gpio pin, then call a write function to actually write to the pin.</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">set_led_on</span><span class="p">(</span><span class="n">undefined</span> <span class="o">*</span><span class="n">pinctl</span><span class="p">,</span><span class="kt">int</span> <span class="n">led_no</span><span class="p">)</span> <span class="p">{</span> <span class="n">ledmappings_entry</span> <span class="o">*</span><span class="n">led</span> <span class="o">=</span> <span class="n">findled</span><span class="p">(</span><span class="n">led_no</span><span class="p">);</span> <span class="k">if</span> <span class="p">(</span><span class="n">led</span> <span class="o">!=</span> <span class="n">nullptr</span><span class="p">)</span> <span class="p">{</span> <span class="n">likely_write_led_gpio</span><span class="p">(</span><span class="n">pinctl</span><span class="p">,</span><span class="n">led</span><span class="o">-&gt;</span><span class="n">gp_pad</span><span class="p">,</span><span class="n">led</span><span class="o">-&gt;</span><span class="n">led_on_level</span><span class="p">);</span> <span class="p">}</span> <span class="p">}</span> </code></pre></div></div> <p>finally, let’s look at how <code class="language-plaintext highlighter-rouge">findled</code> figures out the mapping. it’s actually quite simple, it just iterates over a static array of mappings (<code class="language-plaintext highlighter-rouge">ledmappings</code>) and returns the one that matches the given id.</p> <p>the original code may have looked something like this:</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ... foreshadowing ...</span> <span class="cp">#define WG_LED_STATUS 0 #define WG_LED_ATTN 2 #define WG_LED_MODE 3 #define WG_LED_FAILOVER 4 </span> <span class="k">struct</span> <span class="n">ledmappings_entry</span> <span class="p">{</span> <span class="kt">int</span> <span class="n">led_no</span><span class="p">;</span> <span class="kt">int</span> <span class="n">gp_pad</span><span class="p">;</span> <span class="kt">int</span> <span class="n">led_on_level</span><span class="p">;</span> <span class="p">};</span> <span class="k">struct</span> <span class="n">ledmappings_entry</span> <span class="n">ledmappings</span><span class="p">[]</span> <span class="o">=</span> <span class="p">{</span> <span class="p">{</span> <span class="n">WG_LED_STATUS</span><span class="p">,</span> <span class="mi">7</span><span class="p">,</span> <span class="mi">0</span> <span class="p">},</span> <span class="p">{</span> <span class="n">WG_LED_ATTN</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">0</span> <span class="p">},</span> <span class="p">{</span> <span class="n">WG_LED_MODE</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">0</span> <span class="p">},</span> <span class="p">{</span> <span class="n">WG_LED_FAILOVER</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="mi">0</span> <span class="p">},</span> <span class="p">}</span> <span class="n">ledmappings_entry</span><span class="o">*</span> <span class="nf">findled</span><span class="p">(</span><span class="kt">int</span> <span class="n">led_no</span><span class="p">)</span> <span class="p">{</span> <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">COUNT_OF</span><span class="p">(</span><span class="n">ledmappings</span><span class="p">);</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span> <span class="k">if</span> <span class="p">(</span><span class="n">ledmappings</span><span class="p">[</span><span class="n">i</span><span class="p">].</span><span class="n">led_no</span> <span class="o">==</span> <span class="n">led_no</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="o">&amp;</span><span class="n">ledmappings</span><span class="p">[</span><span class="n">i</span><span class="p">];</span> <span class="p">}</span> <span class="p">}</span> <span class="k">return</span> <span class="n">nullptr</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>and now, we (almost) have the pins mapped out! only one problem remains: the driver maps two gpio controllers, but we don’t know which pin belongs to which controller.</p> <p>while i could probably spend a lot of time figuring that out too, i opted to simply try out both options and see what happens. what’s the worst that could happen, right?</p> <table> <thead> <tr> <th><img src="/assets/images/common/reactions/cat-gambling.gif" alt="let's gamble" /></th> </tr> </thead> <tbody> <tr> <td>let’s gambling</td> </tr> </tbody> </table> <h3 id="note-on-reverse-engineering-steps">Note on Reverse-Engineering Steps</h3> <p>the steps shown above are very high-level and skip a lot of details, and are also in a different order than i actually did them.</p> <p>for example, the chain leading to <code class="language-plaintext highlighter-rouge">ledmappings</code> was basically in reverse, as <code class="language-plaintext highlighter-rouge">ledmappings</code>, <code class="language-plaintext highlighter-rouge">findled</code>, <code class="language-plaintext highlighter-rouge">set_led_on</code> and <code class="language-plaintext highlighter-rouge">set_led_off</code> were actually exported symbols (thus i knew their names). also, there were some “side-quests”, like taking a quick look at ``t55-libwgpanel.so` and other binaries, to understand how the leds are controlled from userspace.</p> <p>still, i think the above rendering makes it easier to understand the overall flow, even if it doesn’t quite match the actual steps taken.</p> <h2 id="finalizing-the-pin-mapping-for-linux">Finalizing the Pin Mapping for Linux</h2> <p>looking at the gpio implementation in linux, we see that gpio communities SouthWest and North are mapped to gpiochip0 (GPIOs 512-609) and gpiochip1 (610-682) respectively.</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># cat /sys/kernel/debug/gpio gpiochip0: GPIOs 512-609, parent: platform/INT33FF:00, INT33FF:00: gpiochip1: GPIOs 610-682, parent: platform/INT33FF:01, INT33FF:01: gpiochip2: GPIOs 683-709, parent: platform/INT33FF:02, INT33FF:02: (...) </code></pre></div></div> <p>using the <code class="language-plaintext highlighter-rouge">gp_pad</code> values from the <code class="language-plaintext highlighter-rouge">ledmappings</code> array as an offset to the first pin of each chip, we get some potential linux GPIOs. poking those pins and observing, we can finally figure out which pin does what.</p> <table> <thead> <tr> <th><code class="language-plaintext highlighter-rouge">led_no</code></th> <th><code class="language-plaintext highlighter-rouge">gp_pad</code></th> <th># when on GP_SouthWest (@ 512)</th> <th>Reaction when Poked</th> <th># when on GP_North (@ 610)</th> <th>Reaction when Poked</th> </tr> </thead> <tbody> <tr> <td>3</td> <td>1</td> <td>513</td> <td>None</td> <td>611</td> <td>Mode LED</td> </tr> <tr> <td>2</td> <td>3</td> <td>515</td> <td>None</td> <td>613</td> <td>ATTN LED</td> </tr> <tr> <td>4</td> <td>5</td> <td>517</td> <td>None</td> <td>615</td> <td>Fail Over LED</td> </tr> <tr> <td>0</td> <td>7</td> <td>519</td> <td>None</td> <td>617</td> <td>Status LED</td> </tr> </tbody> </table> <p>and there we have it, we can now control the front panel leds from linux! to be honest, i don’t really understand why they mapped GP_SouthWest at all, but seeing how the T70 driver only maps GP_North, maybe it was just an oversight.</p> <h2 id="what-about-the-reset-button">What about the Reset Button?</h2> <p>while <code class="language-plaintext highlighter-rouge">sled_drv</code> probably also somehow implements the reset button, i decided to just keep gambling and poke nearby pins on GP_North. that should be a safe bet, as it’s generally convenient to place similar components close to each other when creating a schematic (at least that’s what i would do).</p> <p>turns out, the first pin i tried (610) was indeed the reset button! testing the other pins (612, 614, 616), the first two seem to handle some internal functions (indicated by status leds on the board), while 616 does nothing.</p> <h2 id="how-to-try-it-out">How to try it out</h2> <p>if you want to try it yourself, you can use the following commands (as root; tested on ubuntu server 24.04):</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">gpio</span><span class="o">=</span>611 <span class="c"># Mode LED</span> <span class="nb">echo</span> <span class="s2">"</span><span class="nv">$gpio</span><span class="s2">"</span> <span class="o">&gt;</span> /sys/class/gpio/export <span class="nb">echo</span> <span class="s2">"out"</span> <span class="o">&gt;</span> /sys/class/gpio/gpio<span class="nv">$gpio</span>/direction <span class="nb">echo</span> <span class="s2">"1"</span> <span class="o">&gt;</span> /sys/class/gpio/gpio<span class="nv">$gpio</span>/value <span class="c"># or</span> <span class="nb">echo</span> <span class="s2">"0"</span> <span class="o">&gt;</span> /sys/class/gpio/gpio<span class="nv">$gpio</span>/value </code></pre></div></div> <p>and for the reset button:</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">gpio</span><span class="o">=</span>610 <span class="c"># Reset Button</span> <span class="nb">echo</span> <span class="s2">"</span><span class="nv">$gpio</span><span class="s2">"</span> <span class="o">&gt;</span> /sys/class/gpio/export <span class="nb">echo</span> <span class="s2">"in"</span> <span class="o">&gt;</span> /sys/class/gpio/gpio<span class="nv">$gpio</span>/direction <span class="nv">level</span><span class="o">=</span><span class="si">$(</span><span class="nb">cat</span> /sys/class/gpio/gpio<span class="nv">$gpio</span>/value<span class="si">)</span> <span class="nb">echo</span> <span class="s2">"</span><span class="nv">$level</span><span class="s2">"</span> </code></pre></div></div> <p>a full mapping of the pin functions is as follows:</p> <table> <thead> <tr> <th>Supported Model</th> <th>GPIO Pin</th> <th>Function</th> <th>Notes</th> </tr> </thead> <tbody> <tr> <td>T55 &amp; T70</td> <td>610</td> <td>Reset Button</td> <td>0=Pressed, 1=Released</td> </tr> <tr> <td>T55 &amp; T70</td> <td>611</td> <td>Mode LED</td> <td>0=ON, 1=OFF</td> </tr> <tr> <td>T70</td> <td>612</td> <td>LED near backplane NIC</td> <td>0=OFF, 1=ON</td> </tr> <tr> <td>T55 &amp; T70</td> <td>613</td> <td>ATTN LED</td> <td>0=ON, 1=OFF</td> </tr> <tr> <td>T55</td> <td>614</td> <td>LED near switch IC</td> <td>0=OFF, 1=ON</td> </tr> <tr> <td>T70</td> <td>614</td> <td>LED near network ports / power switch</td> <td>0=OFF, 1=ON</td> </tr> <tr> <td>T55 &amp; T70</td> <td>615</td> <td>Fail Over LED</td> <td>0=ON, 1=OFF</td> </tr> <tr> <td>T55 &amp; T70</td> <td>617</td> <td>Status LED</td> <td>0=ON, 1=OFF</td> </tr> </tbody> </table> (Somewhat) Cursed WatchGuard T55 / T70 Hardware Mods 2025-09-13T00:00:00+00:00 https://shadow578.github.io/2025/09/13/t70-hardware-mods <p>with both of my watchguards fully jailbroken and behaving (mostly) like a normal linux box, but still waiting on some smaller details before actually utilizing them, i decided to poke around the hardware a bit more.</p> <p>my motivation on this was straight-forward: both of my units have some unpopulated components on the pcb, so i wanted to see if i could get them working. also, there’s the issue with the switch ports beign disabled by default, as the original os configured them in software…</p> <h2 id="t55--t70-enabling-the-switch-ports">T55 &amp; T70: Enabling the switch ports</h2> <p>this one’s an already well known mod, but i figured i’d document it here anyway.</p> <p>the switch chip on both the t55 and t70 is, by default, set to be configured by the main cpu via an mdio interface (connected to the backplane NIC’s mdio port). only issue is, you’d need a custom kernel module to do that, and there isn’t one available outside of the original firmware.</p> <p>luckily, by simply removing a single resistor (R607 on both the T55 and T70), you can force the switch into “dumb” mode, where it just acts like a regular unmanaged switch. credits to <a href="https://forum.openwrt.org/t/watchguard-t70-hw-discovery/155544/2">@konus on the OpenWRT forums</a> for figuring this out.</p> <table> <thead> <tr> <th><img src="/assets/images/hw-hacking/watchguard/t70/hw-mod/switch-unmanaged-mode.png" alt="removing R607 makes the switch dumb" /></th> </tr> </thead> <tbody> <tr> <td>removing R607 makes the switch dumb</td> </tr> </tbody> </table> <h2 id="t55-adding-a-wifi-module">T55: Adding a WiFi module</h2> <p>besides the T55, watchguard also offers a T55-W model, which has WiFi capabilities - but is otherwise identical to the regular T55. to save costs, watchguard simply removed the mini-PCIe slot and the associated components from the regular T55 model. nothing we can’t fix, though!</p> <p>to restore the mini-PCIe slot, we simply need to add the inline capacitors of the pcie lanes (C271 and C272), as well as some in-line resistors for mpcie control lines (R8547 and R8551). oh, and of course, the mini-PCIe slot itself.</p> <p>annoyingly, watchguard also omits the power supply components for the mini-PCIe slot. but we can simply steal power from the switch’s power rail (@ TP61), as both use 3.3V, and a mini-PCIe card probably won’t draw that much power anyway.</p> <table> <thead> <tr> <th><img src="/assets/images/hw-hacking/watchguard/t70/hw-mod/t55-mpcie.png" alt="Not pretty, but it works" /></th> </tr> </thead> <tbody> <tr> <td>Not pretty, but it works</td> </tr> </tbody> </table> <h2 id="t70-adding-hdmi-output">T70: Adding HDMI Output</h2> <p>both the T55 and T70 have an unpopulated HDMI port near in the bottom left corner of the PCB. however, curiously, on the T70 they did <strong>not</strong> omit the associated components to actually make use of it.</p> <p>meaning, we can simply solder a HDMI port onto the board, and get video output!</p> <table> <thead> <tr> <th><img src="/assets/images/hw-hacking/watchguard/t70/hw-mod/t70-hdmi.png" alt="HDMI port soldered onto the T70" /></th> </tr> </thead> <tbody> <tr> <td>HDMI port soldered onto the T70</td> </tr> </tbody> </table> <p>with this alone, we still don’t get any output, as the iGPU is disabled by default in the BIOS. however, since we have full BIOS access, we can simply enable it there, and voilà - we have video output!</p> <p>now, what to do with it …</p> <h3 id="windows-11-on-the-t70-">Windows 11 on the T70 ?!</h3> <p>with a video output, there’s nothing stopping us from installing Windows 11 on the T70. surely some random firewall appliance is a supported platform, right Microsoft?</p> <p>installation and usage went pretty effortlessly (albeit quite slow, as theres only 2GB of RAM and the rest of the hardware is not exactly high-end either).</p> <p>only quirk: for some reason, the OEM logo embedded into the BIOS is really messed up.</p> <table> <thead> <tr> <th><img src="/assets/images/hw-hacking/watchguard/t70/hw-mod/t70-windows-boot.png" alt="Booting Windows 11 on the T70" /></th> </tr> </thead> <tbody> <tr> <td>Booting Windows 11 on the T70</td> </tr> </tbody> </table> Hello, World! 2025-09-09T00:00:00+00:00 https://shadow578.github.io/2025/09/09/about-this-page <p>hi there!</p> <p>if you’re reading this, you’ve found my page. i’ve set this up to have a place to write about the random stuff i do, and maybe share some of my thoughts. idk how much i’ll actually write, but we’ll see.</p> <p>this page is built using <a href="https://lanyon.getpoole.com">Lanyon</a>, a theme for <a href="https://jekyllrb.com">Jekyll</a>. find out more by <a href="https://github.com/mojombo/jekyll">visiting the project on GitHub</a>.</p> Patching the BIOS of a WatchGuard T70 to Remove the Password 2025-08-31T00:00:00+00:00 https://shadow578.github.io/2025/08/31/patching-the-t70s-bios <p>in the last post, we explored how to run linux on a watchguard t70. while it’s quite useable as is, the bios password is a bit of an annoyance. so, let’s patch the bios to remove the password check entirely.</p> <h2 id="recap">Recap</h2> <p>from the previous post, we already have the bios image extracted and loaded into ghidra. we also already found the password check function, which is how we know the password in the first place.</p> <table> <thead> <tr> <th><img src="/assets/images/hw-hacking/watchguard/t70/password_function.png" alt="the password check function" /></th> </tr> </thead> <tbody> <tr> <td>the password check function</td> </tr> </tbody> </table> <h2 id="removing-the-password-check">Removing the Password Check</h2> <p>looking a bit closer at the function, we can see some convenient details that make our job easier:</p> <ol> <li>the function only has a single call site</li> <li>the function doesn’t have a return value</li> <li>the function doesn’t take any parameters</li> <li>the function doesn’t have any relevant side effects (other than showing a prompt on the screen)</li> </ol> <p>that means we can simply NOP out the entire function call, and be good to go.</p> <table> <thead> <tr> <th><img src="/assets/images/hw-hacking/watchguard/t70/ghidra_password_callsite.png" alt="password check call site" /></th> <th><img src="/assets/images/hw-hacking/watchguard/t70/ghidra_password_call_nopped.png" alt="nop it out" /></th> </tr> </thead> <tbody> <tr> <td>password check call site</td> <td>nop it out</td> </tr> </tbody> </table> <p>i found that patching and exporting with ghidra was a bit finnicky, so i just noted the pattern of bytes to patch, and used a hex editor to do the actual patching.</p> <p>the modified <code class="language-plaintext highlighter-rouge">BdsDxe</code> module is then re-inserted into the bios using H2OEZE - which correctly recalculates the checksums for us. the patched bios binary is then flashed back to the device and voila - no more password prompt!</p> <h2 id="how-you-can-jailbreak-your-t70">How You can Jailbreak Your T70</h2> <p>while i’d love to share the patched bios image, i can’t in good conscience do that.</p> <p>however, with the knowledge in these posts, you can jailbreak your own T70 - without even opening the case. Watchguard will surely still honor the warranty - right?</p> <ol> <li>prepare your hardware: <ol> <li>plug in a bootable usb stick with, for example, ubuntu server 24.04</li> <li>connect a network cable OR a wifi adapter (i used a network cable)</li> <li>plug in a serial console cable and configure your terminal to 115200 8N1</li> </ol> </li> <li>interrupt the boot process using <code class="language-plaintext highlighter-rouge">&lt;ESC&gt;</code> and enter the setup utility (something we can do now that we have the password)</li> <li>in the setup utility, set the following settings: <ol> <li><code class="language-plaintext highlighter-rouge">Boot &gt; Boot Mode</code> to <code class="language-plaintext highlighter-rouge">UEFI</code> (needed for ubuntu server)</li> <li><code class="language-plaintext highlighter-rouge">Boot &gt; USB Boot</code> to <code class="language-plaintext highlighter-rouge">Enabled</code> (boot from usb)</li> <li><code class="language-plaintext highlighter-rouge">Advanced &gt; Chipset Configuration &gt; Misc Configuration &gt; BIOS Lock</code> to <code class="language-plaintext highlighter-rouge">Disabled</code> (allow flashing the bios)</li> </ol> </li> <li>save changes and reboot, you should now see grub on the serial console</li> <li>edit the grub entry to add <code class="language-plaintext highlighter-rouge">console=ttyS0,115200</code> to the kernel command line so you can see the installer output</li> <li>continue until you see the installer prompt, then exit to the shell via the “help” menu</li> <li>install flashrom via <code class="language-plaintext highlighter-rouge">apt install flashrom</code></li> <li>dump your current bios via <code class="language-plaintext highlighter-rouge">flashrom -p internal -r bios.bin</code> (do this twice and compare hashes to be sure everything went well), then transfer to a windows computer</li> <li>open the dumped bios in <a href="https://winraid.level1techs.com/t/tool-h20eze-insyde-easy-bios-editor/33332">H20EZE</a> and extract the <code class="language-plaintext highlighter-rouge">BdsDxe</code> module as a <code class="language-plaintext highlighter-rouge">.ffs</code></li> <li>open the <code class="language-plaintext highlighter-rouge">.ffs</code> in a hex editor, find and replace the pattern to the function call with NOPs (refer to table below)</li> <li>save the modified <code class="language-plaintext highlighter-rouge">.ffs</code></li> <li>use “replace module” in H20EZE to insert the modified <code class="language-plaintext highlighter-rouge">BdsDxe</code> module back into the bios</li> <li>export the modified bios as a <code class="language-plaintext highlighter-rouge">.fd</code> and transfer back to the T70</li> <li>flash the modified bios via <code class="language-plaintext highlighter-rouge">flashrom -p internal -w bios.patched.fd</code></li> <li>reboot and enter setup utility to verify the password prompt is gone</li> <li>(optionall) re-enable <code class="language-plaintext highlighter-rouge">BIOS Lock</code> to prevent accidental flashing</li> </ol> <table> <thead> <tr> <th>Product</th> <th>BIOS Version String</th> <th>BIOS Dump SHA256 (<code class="language-plaintext highlighter-rouge">Get-FileHash bios.fd</code>)</th> <th>Password Check Call Pattern</th> <th>Replace With</th> </tr> </thead> <tbody> <tr> <td>T55</td> <td>77.05</td> <td><code class="language-plaintext highlighter-rouge">1B13A7CD8D8CBDAEC6C3158AEBF74F8CC3B2D3519202A516A152F3F1FDDBD8A2</code></td> <td><code class="language-plaintext highlighter-rouge">e8 31 d3 ff ff</code></td> <td><code class="language-plaintext highlighter-rouge">90 90 90 90 90</code> (5x NOP)</td> </tr> <tr> <td>T70</td> <td>1.16</td> <td><code class="language-plaintext highlighter-rouge">A180292ABD17466B789A437B2758BA702FADB5EA226BCADBDD2DD6266F0D7AEE</code></td> <td><code class="language-plaintext highlighter-rouge">e8 31 d3 ff ff</code></td> <td><code class="language-plaintext highlighter-rouge">90 90 90 90 90</code> (5x NOP)</td> </tr> </tbody> </table> Running Linux on a WatchGuard T70 2025-08-29T00:00:00+00:00 https://shadow578.github.io/2025/08/29/linux-on-a-watchguard-t70 <p>so, i’ve recently got my hands on an old WatchGuard T70 firewall, and i thought it would be a fun project to see if i could get Linux running on it. while it may not be the most powerful, it should still be interesting for running some basic stuff like DNS or network monitoring. since <a href="https://forum.openwrt.org/t/watchguard-t70-hw-discovery/155544">some people managed to boot OpenWRT on it</a>, i figured it couldn’t be that difficult - right?</p> <blockquote> <p>We do this not because it is easy, but because we thought it would be easy.<br /> - Programmer’s Credo</p> </blockquote> <h2 id="hardware-overview">Hardware Overview</h2> <p>the T70 is based on a <a href="https://www.intel.com/content/www/us/en/products/sku/91831/intel-celeron-processor-n3160-2m-cache-up-to-2-24-ghz/specifications.html">Intel Celeron N3160</a> from ~2016. for a small server, this is still quite usable and most importantly with only 4-6 W TDP, it should be quite power efficient and silent. the 2GB ram is a little limiting, but should still be fine</p> <p>as for software, the stock OS is some embedded linux variant, but it’s quite locked down and thus not really useful to me. the bios is an more-or-less standard Insyde H20 one, but it’s password protected. sadly, clearing the cmos doesn’t reset the password and most tools online cannot bypass it either. also, as a further annoyance, the bios only allows booting from the internal MSATA ssd (according to some sources online, the spare sata port also works, but i haven’t tested that).</p> <p>full specs:</p> <ul> <li>Intel Celeron N3160 CPU (4 cores @ 1.6 GHz)</li> <li>2 GB RAM (DDR3L-1600 soldered to the board)</li> <li>16 GB MSATA SSD</li> <li>a spare SATA port</li> <li>4x Intel 1911BFP NICs <ul> <li>3x connected directly to GBE ports</li> <li>1x connected to a internal 88E6176 switch IC, which has 5x GBE ports connected to it</li> </ul> </li> <li>2x USB 2.0 ports</li> <li>Insyde H20 BIOS, password protected, only boots from internal SSD in legacy mode</li> </ul> <table> <thead> <tr> <th><img src="/assets/images/hw-hacking/watchguard/t70/pcb_top.jpg" alt="pcb top" /></th> <th><img src="/assets/images/hw-hacking/watchguard/t70/pcb_bottom.jpg" alt="pcb bottom" /></th> </tr> </thead> <tbody> <tr> <td>pcb top</td> <td>pcb bottom</td> </tr> </tbody> </table> <h2 id="first-steps-booting-alpine-linux-from-usb">First Steps: Booting Alpine Linux from USB</h2> <p>this first attempt actually worked pretty easily. while the bios is locked down, i do happen to have a SPI flash programmer, so i simply dumped the bios. from there, a tool called <a href="https://winraid.level1techs.com/t/tool-h20eze-insyde-easy-bios-editor/33332">H20EZE</a> can be used to simply change the default settings of the bios. for a start, i simply enabled booting from usb devices. once flashed back, the clear CMOS jumper is used to load the defaults, and thus i could finally boot from a USB stick.</p> <p>now, since there’s only a serial console, i had to modify the bootloader config and kernel command line, but that’s easy enough (see <a href="https://wiki.alpinelinux.org/wiki/Enable_Serial_Console_on_Boot">this guide</a>).</p> <p>so far so good, but alpine is still quite limited, and i’d rather run something more full featured like Ubuntu Server.</p> <h2 id="booting-ubuntu-server-from-usb">Booting Ubuntu Server from USB</h2> <p>turns out the bios does support UEFI boot, but it’s simply not enabled in the default settings. using the same methods as before, i set the bios to UEFI mode, enabled usb boot, and disabled secure boot. flashed it back, cleared CMOS, and voila - the T70 boots the Ubuntu Server installer from USB just fine.</p> <p>again, serial console needs to be enabled in grub, but other than that, everything works just fine.</p> <p>still, i’d rather not have to mess with flashing the bios every time i want to modify a setting. it’s not really practical, is it? so, next step is to bypass the bios password protection or figure out the password.</p> <h2 id="accessing-the-bios-setup">Accessing the BIOS Setup</h2> <p><a href="https://en.wikipedia.org/wiki/WatchGuard">WatchGuard, being a seasoned IT security company with hundreds of millions in revenue</a>, of course did their homework here and locked down the bios quite well. they even went out of their way to implement their own password protection, instead of using the built-in Insyde H20 password protection. thus, clearing the CMOS doesn’t reset the password, and the normal backdoor passwords for Insyde H20 don’t work either.</p> <table> <thead> <tr> <th><img src="/assets/images/hw-hacking/watchguard/t70/password_prompt.png" alt="the dreaded password prompt" /></th> </tr> </thead> <tbody> <tr> <td>the dreaded password prompt</td> </tr> </tbody> </table> <p>so, how do we get around this?</p> <h3 id="first-attempt-poking-at-the-bios-modules">First Attempt: Poking at the BIOS Modules</h3> <p>yes, this first attempt was a bit naive.</p> <p>H20EZE has a function to remove bios modules, so i thought</p> <blockquote> <p>hey, maybe i can just remove the password checking module, and then the bios won’t ask for a password at all<br /> - naive past me</p> </blockquote> <p>predictably, that didn’t work out. for some modules, there seems to be no effect at all, while for others, the bios simply refuses to boot.</p> <p>so, onto the next idea.</p> <h3 id="second-attempt-brute-forcing-the-password">Second Attempt: Brute Forcing the Password</h3> <p>in hindsight, i think i was just looking for an excuse to <strong>not</strong> do anything the right way. so, instead of simply booting up ghidra, i wrote a crappy python script that attempts to brute-force the password - over a serial connection at 115200 baud.</p> <p>yeah, that was a bad idea. since each attempt takes ~0.5 seconds, i’d be sitting here for a bazillion years (estimate) until it finds the password.</p> <table> <thead> <tr> <th><img src="/assets/images/common/reactions/aint_nobody_got_time_for_that.gif" alt="Ain't nobody got time for that" /></th> </tr> </thead> <tbody> <tr> <td>Ain’t nobody got time for that</td> </tr> </tbody> </table> <p>so, no way around it, let’s do things properly now.</p> <h3 id="third-attempt-reverse-engineering-the-password-check">Third Attempt: Reverse Engineering the Password Check</h3> <p>searching for the string “Input Password” that appears at the password prompt using <a href="https://github.com/LongSoft/UEFITool">UEFITool</a> tells me the string appears in two modules: <code class="language-plaintext highlighter-rouge">BdsDxe</code> and <code class="language-plaintext highlighter-rouge">SetupUtility</code>.</p> <ul> <li><code class="language-plaintext highlighter-rouge">SetupUtility</code> is a false lead here, since the string is actually a sub-string of the help text for the stock password protection feature of Insyde H20, which is not used.</li> <li><code class="language-plaintext highlighter-rouge">BdsDxe</code> is what we’re after. it contains the actual password checking code, which is custom made by Watchguard.</li> </ul> <p>UEFITool has a feature to extract the PE image of a uefi module. So let’s load that into Ghidra for analysis.</p> <h4 id="the-password-check-function">The Password Check Function</h4> <p>after initial analysis by ghidra, searching for the “Input Password” string gives us a single match - with a single cross-reference to it. that cross reference brings us right to the function that prompts for - and checks - the password.</p> <table> <thead> <tr> <th><img src="/assets/images/hw-hacking/watchguard/t70/ghidra_password_string_xref.png" alt="cross reference in ghidra" /></th> </tr> </thead> <tbody> <tr> <td>cross reference for “Input Password” in ghidra</td> </tr> </tbody> </table> <p>this function truely is a sight to behold. take a look for yourself:</p> <table> <thead> <tr> <th><img src="/assets/images/hw-hacking/watchguard/t70/password_function.png" alt="the password check function" /></th> <th><img src="/assets/images/common/reactions/lgtm.gif" alt="the developer who reviewed it" /></th> </tr> </thead> <tbody> <tr> <td>the password check function</td> <td>the developer who reviewed it</td> </tr> </tbody> </table> <p>as you can see, it’s highly secure… well, almost. to be fair, it’s not like this is highly critical to the security of the device, since misusing it requires physical access anyway. but still, i at least expected something involving the serial number of the device or something like that.</p> <p>let’s clean it up a bit, and see what it does:</p> <div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">password_check</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span> <span class="n">longlong</span> <span class="n">n</span><span class="p">;</span> <span class="kt">short</span> <span class="o">*</span><span class="n">user_input</span> <span class="p">[</span><span class="mi">4</span><span class="p">];</span> <span class="kt">char</span> <span class="n">the_password</span> <span class="p">[</span><span class="mi">16</span><span class="p">];</span> <span class="kt">char</span> <span class="n">user_input_ascii</span> <span class="p">[</span><span class="mi">32</span><span class="p">];</span> <span class="n">builtin_strncpy</span><span class="p">(</span><span class="n">the_password</span><span class="p">,</span><span class="s">"WatchGuard!"</span><span class="p">,</span><span class="mh">0xb</span><span class="p">);</span> <span class="p">(</span><span class="o">**</span><span class="p">(</span><span class="n">code</span> <span class="o">**</span><span class="p">)(</span><span class="o">*</span><span class="p">(</span><span class="n">longlong</span> <span class="o">*</span><span class="p">)(</span><span class="n">DAT_0006b1a8</span> <span class="o">+</span> <span class="mh">0x40</span><span class="p">)</span> <span class="o">+</span> <span class="mh">0x30</span><span class="p">))(</span><span class="o">*</span><span class="p">(</span><span class="n">longlong</span> <span class="o">*</span><span class="p">)(</span><span class="n">DAT_0006b1a8</span> <span class="o">+</span> <span class="mh">0x40</span><span class="p">));</span> <span class="k">do</span> <span class="p">{</span> <span class="k">do</span> <span class="p">{</span> <span class="n">user_prompt</span><span class="p">(</span><span class="s">L"Input Password"</span><span class="p">,</span><span class="n">user_input</span><span class="p">);</span> <span class="n">likely_utf16_to_ascii</span><span class="p">(</span><span class="n">user_input</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span><span class="n">user_input_ascii</span><span class="p">);</span> <span class="n">FUN_0003c424</span><span class="p">();</span> <span class="n">n</span> <span class="o">=</span> <span class="n">strlen</span><span class="p">(</span><span class="n">user_input_ascii</span><span class="p">);</span> <span class="p">}</span> <span class="k">while</span> <span class="p">(</span><span class="n">n</span> <span class="o">!=</span> <span class="mh">0xb</span><span class="p">);</span> <span class="n">n</span> <span class="o">=</span> <span class="n">strncmp_with_identity_check</span><span class="p">(</span><span class="n">user_input_ascii</span><span class="p">,</span><span class="n">the_password</span><span class="p">,</span><span class="mh">0xb</span><span class="p">);</span> <span class="p">}</span> <span class="k">while</span> <span class="p">(</span><span class="n">n</span> <span class="o">!=</span> <span class="mi">0</span><span class="p">);</span> <span class="k">return</span><span class="p">;</span> <span class="p">}</span> </code></pre></div></div> <p>so, how does this work? to figure that out is left as an exercise to the reader. hope you can figure out the password ;)</p> <p>if you don’t like random passwords sticking around, check out <a href="/2025/08/31/patching-the-t70s-bios/">part 2</a> where i patch the bios to remove the password check entirely.</p> <h2 id="exploring-the-setup-utility">Exploring the Setup Utility</h2> <p>now we can access the bios setup, so what’s next?</p> <table> <thead> <tr> <th><img src="/assets/images/common/reactions/i-dont-know.png" alt="i didn't think i'd get this far" /></th> </tr> </thead> <tbody> <tr> <td>i didn’t think i’d get this far</td> </tr> </tbody> </table> <p>let’s take a quick look around to see what options are available. turns out, quite a lot of options are available - this is a fairly unlocked InsydeH20 bios. it seems like noone at WatchGuard expected anyone to get this far, so they didn’t bother limiting the options.</p> <table> <thead> <tr> <th><img src="/assets/images/hw-hacking/watchguard/t70/bios_main_screen.png" alt="the bios setup main screen" /></th> <th><img src="/assets/images/hw-hacking/watchguard/t70/bios_pdm-dfx.png" alt="what the hell is a PDM/Dfx?" /></th> </tr> </thead> <tbody> <tr> <td>the bios setup main screen</td> <td>what the hell is a PDM/Dfx?</td> </tr> </tbody> </table> <p>also fun: the two OS choices are “Windows” and “Android”. not quite what i’d expect on a firewall appliance (imagine the horror of a <em>Windows</em> firewall).</p> <table> <thead> <tr> <th><img src="/assets/images/hw-hacking/watchguard/t70/bios_os_selection.png" alt="ah yes, the two osses you'd expect on a firewall" /></th> <th><img src="/assets/images/common/reactions/long-neck-reaction.jpg" alt="my reaction to the thought of a windows-based firewall" /></th> </tr> </thead> <tbody> <tr> <td>ah yes, the two OSses you’d expect on a firewall</td> <td>my reaction to the thought of a windows-based firewall</td> </tr> </tbody> </table> <h2 id="side-note-watchguard-t50">Side Note: WatchGuard T50</h2> <p>all of the above also applies to the T50, which is basically a T70 with some cut down hardware specs. it uses the same general platform and very similar bios (with the same password). only difference is the CPU (Celeron N3060 instead of N3160) and 3 less GBE ports (on the T50, all 5 ports are handled by a switch IC)</p> Voxelab Aquila V1.0.1 (and V1.0.2) Trimatic Stepper Control Mod 2025-02-10T00:00:00+00:00 https://shadow578.github.io/2025/02/10/tmc-stepper-control-mod <p>This post describes how to modify the Aquila V1.0.2 HC32 mainboard in order to add TMC Stepper UART control lines. Doing this allows changing stepper driver parameters (e.g. stepping mode, current) from Marlin firmware.</p> <blockquote> <p><strong>NOTICE:</strong> <br /> This post was written for the Aquila V1.0.2 mainboard. <br /> However, since the Aquila V1.0.1 mainboard is fairly similar, it should still work.</p> </blockquote> <h2 id="preword">Preword</h2> <p>Having the option to control the parameters of the stepper drivers is quite useful. For one, you’ll be able to enable the otherwise unavailable 1/256 microstepping mode that the TMC2208 and clones provide. Additionally, you can adjust the motor current more precisely than by using the “classic” analog pot.</p> <p>However, the biggest advantage is to change the stepping mode, for example to disable StealthChop (quiet) mode. By doing this, advanced motion features like Linear Advance and Input Shaping are said to work better.</p> <h2 id="hardware">Hardware</h2> <p>On my mainboard, the following ICs were used:</p> <ul> <li>HDSC HC32F460 MCU <ul> <li>this is the main processor. If you have a different one, the information in this document could still be useful but the pins will <strong>not</strong> match.</li> </ul> </li> <li>KalixChips MS35775 Stepper Drivers <ul> <li>These four ICs drive the stepper motors. These ICs are close to exact clones of the TMC2208 (see the <a href="/assets/images/hw-hacking/voxelab/aquila-v102/tmc-mod/MS35775_full.pdf">datasheet</a> (<a href="https://github.com/shadow578/hc32f460-documentation-and-sdk/blob/main/aquila_v101_v102/mainboard/datasheets/MS35775%20full.pdf">mirror</a>) to see yourself), including the UART control mode. Main difference is the horrible lack of documentation.</li> <li>If you have different stepper drivers, and they don’t happen to be original TMC2208, do not continue with this mod until you’ve confirmed they support the UART control mode.</li> <li>If your board has TMC2209 stepper drivers, consider combining this document with another one specifically for those drivers. They support connecting to a single UART line, so require less pins and less cabling. However, this exact mod should still mostly apply.</li> </ul> </li> </ul> <blockquote> <p><strong>VERY IMPORTANT:</strong> <br /> double-check that your mainboard matches the hardware described here</p> </blockquote> <h2 id="modification">Modification</h2> <p>This modification will set the mainboard up in such a way that each stepper driver is connected to one pin of the MCU, allowing half-duplex software-serial to be used. To ensure that a error while soldering or a broken stepper driver will not immediately damage the MCU, a 1k series resistor is added to each line.</p> <blockquote> <p><strong>NOTICE:</strong> <br /> this mod leaves the PDN_UART 10k pull-up resistor in place.<br /> this way, the modified mainboard will behave as before, unless the software takes control to drive the pins.</p> </blockquote> <h3 id="requirements">Requirements</h3> <ul> <li>sharp hobby knife</li> <li>soldering iron with fine tip</li> <li>thin, isolated wire</li> <li>4x 1k SMD resistor</li> <li>current Marlin firmware (with HC32 arduino core version 1.3.1 or newer)</li> </ul> <h3 id="choosing-the-pins">Choosing the Pins</h3> <p>On this mainboard, there aren’t many pins left free for use. The free pins are (assuming you’ve installed a BLTouch probe):</p> <table> <thead> <tr> <th>Useable</th> <th>Pin</th> <th>Function / Connection</th> <th>Note</th> </tr> </thead> <tbody> <tr> <td>⚠️</td> <td>PA8</td> <td>CH340G DTR</td> <td>connected to CH340G DTR; otherwise useable</td> </tr> <tr> <td>✅</td> <td>PB2</td> <td>Screen header #2</td> <td>free for DWIN screen</td> </tr> <tr> <td>✅</td> <td>PB10</td> <td>R90</td> <td> </td> </tr> <tr> <td>❌</td> <td>PB11</td> <td>MD</td> <td>multiplexed with MODE selection logic; input-only pin</td> </tr> <tr> <td>✅</td> <td>PC6</td> <td>Screen header #1</td> <td>free for DWIN screen</td> </tr> <tr> <td>✅</td> <td>PC14</td> <td>XTAL32</td> <td>can use GPIO</td> </tr> <tr> <td>❌</td> <td>PC15</td> <td>XTAL32</td> <td>datasheet says this pin works as GPIO, but in my testing input function didn’t work</td> </tr> <tr> <td>✅</td> <td>PH2</td> <td>R101</td> <td> </td> </tr> </tbody> </table> <p>I used pins PA8, PB10, PC14 and PH2 for my connections. This is what the rest of this document will describe too</p> <h3 id="cutting-trace-pa8---ch340g-dtr">Cutting trace PA8 -&gt; CH340G DTR</h3> <p>As you can see in the table, PA8 is connected to the CH340G’s DTR pin. Luckily, DTR is not used for anything in this printer, so we can just cut the trace.</p> <table> <thead> <tr> <th><img src="/assets/images/hw-hacking/voxelab/aquila-v102/tmc-mod/pcb_back.jpg" alt="Cut trace PA8 to CH340G with a hobby knife" /></th> </tr> </thead> <tbody> <tr> <td>Cut trace PA8 to CH340G with a hobby knife</td> </tr> </tbody> </table> <p>After cutting the trace, ensure there’s no copper sticking up that could short out to the case and measure with a multimeter that the trace is actually cut completely. To protect the area, place a bit of kapton tape over it.</p> <h3 id="soldering-the-wires">Soldering the Wires</h3> <p>Solder the wires as shown in the following image.</p> <table> <thead> <tr> <th><img src="/assets/images/hw-hacking/voxelab/aquila-v102/tmc-mod/pcb_front_mod_plan.jpg" alt="Modification Wiring Plan" /></th> </tr> </thead> <tbody> <tr> <td>Modification Wiring Plan</td> </tr> </tbody> </table> <p>to ensure the wires don’t come loose, secure them with a bit of hot glue. for the PA8 connection, scrape away the solder mask on one of the wires near the HC32 before soldering.</p> <table> <thead> <tr> <th><img src="/assets/images/hw-hacking/voxelab/aquila-v102/tmc-mod/pcb_front_done.jpg" alt="Finished Modification" /></th> </tr> </thead> <tbody> <tr> <td>Finished Modification</td> </tr> </tbody> </table> <h2 id="configuring-marlin">Configuring Marlin</h2> <p>Change the Marlin configuration to the following:</p> <ul> <li>In <code class="language-plaintext highlighter-rouge">Configuration.h</code>, in <code class="language-plaintext highlighter-rouge">@section stepper drivers</code>: <ul> <li>Change the stepper driver types from <code class="language-plaintext highlighter-rouge">TMC2208_STANDALONE</code> to <code class="language-plaintext highlighter-rouge">TMC2208</code> for X,Y,Z,E0</li> </ul> </li> <li>in Configuration_adv.h, in the <code class="language-plaintext highlighter-rouge">@section tmc/config</code> section: <ul> <li>add the pin definitions (RX is automatically set to TX if not defined): <ul> <li><code class="language-plaintext highlighter-rouge">#define X_SERIAL_TX_PIN PB10</code></li> <li><code class="language-plaintext highlighter-rouge">#define Y_SERIAL_TX_PIN PA8</code></li> <li><code class="language-plaintext highlighter-rouge">#define Z_SERIAL_TX_PIN PC14</code></li> <li><code class="language-plaintext highlighter-rouge">#define E0_SERIAL_TX_PIN PH2</code></li> </ul> </li> <li>add <code class="language-plaintext highlighter-rouge">#define TMC_BAUD_RATE 19200</code> to use 19200 baud for serial communication</li> <li>update the <code class="language-plaintext highlighter-rouge">RSENSE</code> of all axis to <code class="language-plaintext highlighter-rouge">#define [X]_RSENSE 0.15</code> (this board uses 150 mOhm current sense resistors)</li> <li>update the <code class="language-plaintext highlighter-rouge">CHOPPER_TIMING</code> to <code class="language-plaintext highlighter-rouge">#define CHOPPER_TIMING CHOPPER_DEFAULT_24V</code></li> <li>optionally enable <code class="language-plaintext highlighter-rouge">MONITOR_DRIVER_STATUS</code></li> <li>enable <code class="language-plaintext highlighter-rouge">TMC_DEBUG</code> until you’ve confirmed that everything works</li> </ul> </li> </ul> <p>Now, flash the firmware onto your modified mainboard. For initial testing, only the power cables have to be connected.</p> <h2 id="confirm-everything-works">Confirm everything works</h2> <p>to confirm the mod was sucessfull, send a <a href="https://marlinfw.org/docs/gcode/M122.html"><code class="language-plaintext highlighter-rouge">M122</code> command</a> to show the status of the stepper drivers. there should be no errors, and the output should look something like this:</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> X Y Z E Enabled false false false false Set current 800 800 800 800 RMS current 760 760 760 760 MAX current 1072 1072 1072 1072 Run current 17/31 17/31 17/31 17/31 Hold current 8/31 8/31 8/31 8/31 CS actual 8/31 8/31 8/31 8/31 PWM scale vsense 0=.325 0=.325 0=.325 0=.325 stealthChop true true true false msteps 16 16 16 16 interp true true true true tstep max max max max PWM thresh. [mm/s] OT prewarn false false false false OTPW trig. false false false false pwm scale sum 10 10 10 10 pwm scale auto 0 0 0 0 pwm offset auto 36 36 36 36 pwm grad auto 14 14 14 14 off time 4 4 4 4 blank time 24 24 24 24 hysteresis -end 2 2 2 2 -start 1 1 1 1 Stallguard thrs uStep count 72 72 856 40 DRVSTATUS X Y Z E sg_result stst olb * ola * s2gb s2ga otpw ot 157C 150C 143C 120C s2vsa s2vsb Driver registers: X 0xC0:08:00:00 Y 0xC0:08:00:00 Z 0xC0:08:00:00 E 0x80:08:00:C0 Testing X connection... OK Testing Y connection... OK Testing Z connection... OK Testing E connection... OK </code></pre></div></div> <p>if any of the last four tests fail, or if any of the driver registers show all zeros, double-check your wiring. additionally, you may want to double-check that the used pins are actually not connected to anything else (different mainboard revisions may differ here).</p> <p>if everything seems right, try different values for <code class="language-plaintext highlighter-rouge">TMC_BAUD_RATE</code>. Lower values may help with signal integrity issues, while higher values can help with the stepper drivers having issues detecting that there’s a UART signal present. 19200 baud should be a good starting point.</p>