LinkSys EA6100 AC1200 - Part 2 - A serial connection FTW!

12 min read
By 0x434b
LinkSys EA6100 AC1200 - Part 2 - A serial connection FTW!

Last time we left off with a pretty decent understanding about how our router is structured and what components were used. We also found two interesting debug pads that showed oscillating voltages during boot up. In this post, we will take a closer look at exactly these and try to get a Read-Write serial connection going. The goal this time will be to be able to analyze the firmware on the device and being able to decrypt the image we've seen earlier!

Recap:

The two oscillating voltages we were able to measure with our multi meter last time around were on pin 2 (turquoise box) and pin 5 (blue box). The left side (red box) did not look that promising at first. Let's first verify what these oscillating voltages actually are and, if they are indeed transmitting data, whether they are the same or actually different! I noticed that following along with only the PCB pictures is tough, so here is a schematic of how far we progressed last time:

                                                                                     +--+
                                                 +--+                                |  |  Pin 6: 0V (GND!)
                   +--+              TP 2: 3.3V  |  |                                +--+
       TP 1: 3.3V  |  |                       +-----+                                |  |  Pin 5: Oscillating (TX?)
                   +--+           TP 3: 3.3V  |  |                                   +--+
                                              +--+                                   |  |  Pin 4: 0V
                                                                                     +--+
             +--+                                                                    |  |  Pin 3: 0V
Pin 3: 0V    |  |                        +----------------+   +-----+                +--+
             +--+                        |                |   |     |                |  |  Pin 2: 0V
Pin 2: 3.3V  |  +----+                   |                |   |     |                +--+
             +--+    |                   |                |   |     |                |  |  Pin 1: 3.3V (VCC?)
Pin 1: 3.3V  |  +<---+                   |                |   |     |                +--+
             +--+                        |      CPU       |   | RAM |
                                         |                |   |     |                +--+
                                         |                |   |     |           +----+  |  Pin 3: Oscillating (TX?)
                                         |                |   |     |           |    +--+
                                         |                |   |     |           +--->+  |  Pin 2: Oscillating (TX?)
                                         |                |   |     |                +--+
                                         +----------------+   +-----+                |  |  Pin 1: 3.3V
                                                                                     +--+

I hope this diagram will help you follow along. Now let's try to confirm these 3 potential transmit pins (TX). We're starting with the 6 pin row. After some quick and dirty soldering, the backside of the router now looks like this:

6 pin row already hooked up to a logic analyzer

For the two rows with 3 through holes each, I quickly soldered some 2.54 mm headers on them. The row with 6 through holes were too small to fit the 2.54 mm ones, so I had to resort to using some cables... When powering on the device, a short signal capture allows us to peak at the signal curve:

CH1 clearly shows blocks of signal changing from high → low, which is typical for a TX line
Decoded signal as UART with a baudrate of 57600

When decoding channel 1 (CH1, blue) with the included UART protocol decoder on default settings and a baudrate of 57600 our signal is perfectly 'translated' to a human-readable ASCII format (on the right). I was only able to capture about roughly 2 seconds, as my logic analyzer at that time could not handle more (sad noises). However, the short capture already gives plenty of insight:

  • This pin 5 is actually transmitting data
  • The device uses UBoot v0.0.5
  • The board is a Ralink one
  • We have 128 MB of RAM (known to us already!)
  • Suddenly Ralink Uboot v4.2.S.1 takes over
  • CPU Freq is at 580Mhz (known to us already!)
  • There is an Uboot menu with at least 4 options
      1. Load system code to SDRAM via TFTP
      1. Load system code then write to Flash via TFTP
      1. Boot system code via Flash (default)
      1. Entr...?

First, are we dealing with a two stage bootloader variant here? Unlikely! It's more likely that Ralink pulled the ancient Uboot v0.0.5 from the official sources, and they modified it to their needs, which results in v4.2.S.1 for their internal bookkeeping. So, what about the other two pins that also showed oscillating voltages?

Right 3 pin row with the two oscillating voltage pins

They actually as TX lines as well (as we suspected). What we get by confirming this is the fact that actually have the same Uboot bootloader stdout log on these two pins. Also, the signal clearly shows that these two pins are identical (just visually compare them!). Finally, as I started capturing a little later, we also get an idea what post Uboot bootloader option 4 is happening:

  • There is an Uboot menu with at least 4 options
    • ...
      1. Entr boot command line interface
      1. Load Boot loader code then write to Flash via TFTP

This allows us to update our ground truth in the ASCII diagram just slightly but every piece of knowledge brings us closer to our goal!

                                                                                     +--+
                                                 +--+                                |  |  Pin 6: GND
                   +--+              TP 2: 3.3V  |  |                                +--+
       TP 1: 3.3V  |  |                       +-----+                                |  |  Pin 5: TX
                   +--+           TP 3: 3.3V  |  |                                   +--+
                                              +--+                                   |  |  Pin 4: 0V
                                                                                     +--+
             +--+                                                                    |  |  Pin 3: 0V
Pin 3: 0V    |  |                        +----------------+   +-----+                +--+
             +--+                        |                |   |     |                |  |  Pin 2: 0V
Pin 2: 3.3V  |  +----+                   |                |   |     |                +--+
             +--+    |                   |                |   |     |                |  |  Pin 1: 3.3V
Pin 1: 3.3V  |  +<---+                   |                |   |     |                +--+
             +--+                        |      CPU       |   | RAM |
                                         |                |   |     |                +--+
                                         |                |   |     |           +----+  |  Pin 3: TX
                                         |                |   |     |           |    +--+
                                         |                |   |     |           +--->+  |  Pin 2: TX
                                         |                |   |     |                +--+
                                         +----------------+   +-----+                |  |  Pin 1: 3.3V
                                                                                     +--+

Finding GND and TX is the easy part. We still have to identify at least VCC, RX pins, potentially even a clock signal, reset line and others depending on the serial connection we're aiming for.  That

If we assume that VCC pins, used for powering components rely on having fatter traces on the PCB due to needing to transfer enough voltage with them, most likely be connected to a capacitor for storing some of this electrical energy we have a natural candidate for a VCC pin.  Pin 1 of the 6 pin connector on the right side of the PCB. From this assumption, we can also derive the fact that when powering off the device VCC pins should discharge slower, or in other words not hit 0V right away! I've tested this on all the not yet identified pins that showed a constant 3.3V when the device is powered on. Unfortunately, all the pins showed the same symptoms. After powering off the device, the voltages about instantly dropped to around 300mV and then very slowly discharged further from there. This would indicate that all of them are VCC pins. If we apply this to our diagram above, it changes as follows:

                                                                                     +--+
                                                 +--+                                |  |  Pin 6: GND
                   +--+              TP 2: VCC   |  |                                +--+
       TP 1: VCC   |  |                       +-----+                                |  |  Pin 5: TX
                   +--+           TP 3: VCC   |  |                                   +--+
                                              +--+                                   |  |  Pin 4: 0V
                                                                                     +--+
             +--+                                                                    |  |  Pin 3: 0V
Pin 3: 0V    |  |                        +----------------+   +-----+                +--+
             +--+                        |                |   |     |                |  |  Pin 2: 0V
Pin 2: VCC   |  +----+                   |                |   |     |                +--+
             +--+    |                   |                |   |     |                |  |  Pin 1: VCC
Pin 1: VCC   |  +<---+                   |                |   |     |                +--+
             +--+                        |      CPU       |   | RAM |
                                         |                |   |     |                +--+
                                         |                |   |     |           +----+  |  Pin 3: TX
                                         |                |   |     |           |    +--+
                                         |                |   |     |           +--->+  |  Pin 2: TX
                                         |                |   |     |                +--+
                                         +----------------+   +-----+                |  |  Pin 1: VCC
                                                                                     +--+

At this point, the VCC pins are not fully verified but if our labeling is going to turn out to be correct we just need to finish identifying those 4 pins that are at 0V.

With some trial and error while still being connected to the boot console via UART with our known TX and GND pins I was able to deduct that on the larger header on the right, Pin 3 is indeed a RX pin as I was able to type at one point during the boot process. The wiring was done with my trusty pal the Tigard:

Hooked up UART connection with the Tigard as our middleman.

With that out of the way, let's enter the device:

The UART prompt has a super secure login protection :p. For those of you who have watched the recording above, you saw me trying the first credentials coming to mind... Why bother enabling a prompt when the credentials are admin:admin? Who knows... With that said, we're the admin user at this point but actually not the root user!

Initial foothold on the device.

Now with the admin user, we're pretty restricted as all important files like the real /etc/shadow are only accessible by the root user, while many files in /etc have the same behavior of simply being a symlink to the /tmp directory and hence only being writable by root:

Dynamically mounted /tmp/ folder with its contents

The reason for it being that way should be clear. Most if not all config related files are stored in some separate file system that is being dynamically mounted during boot-up. We can see how and why in the /etc/inittab:

sysinit routine that is being executed on startup.
Specific location where /tmp/ is created.

Following inittab and with that, the /etc/system/once and /etc/system/wait we can learn a ton more about the initialization routine and especially about why the system behaves the way it behaves and where the configuration comes from. However, we're still stuck as the admin user, so we couldn't manipulate the current behavior even if we wanted to:

Other sysinit scripts.

At this point, I wanted to dump the file system to a USB drive and go from there but even though external USB devices are automatically mounted they're always mounted as only writable from root (copying from the USB drive to the device works just fine as any user though).

Mount permissions

Then I took a step back and thought, what if the root user login is as trivial as the admin one? I was grepping around the file system to see if some obvious root user configuration takes place, and what can I say... Look for yourself:

Interesting password setup for admin and root users.

Like what? Being at this point, it does not even matter where http_admin_password comes from as it's the same for both admin and root. For completeness, all defaults that are initialized during boot are stored in /etc/system_defaults:

Hard-coded password credentials stored in the ROM.

I'll just leave this here, in case you're wondering what "TSLIIHauhEfGE" is.

Root access to the device.

With that out of the way, we can also dump the file system to an external USB drive without worrying (alternative ways like using nc or the tftp which are present on the device per default would have worked too). So for the final task, let's figure out the firmware update process! Having access to the rootfs locally helped immensely. Grepping for a few obvious candidates again reveals the likeliest candidates where firmware updates are handled:

Grepping for files, which are potentially responsible for handling firmware updates.

Long story short, the magic happens /etc/init.d/service_autofwup.sh. Here are the most interesting bits of the file:

#!/bin/sh
source /etc/init.d/interface_functions.sh
source /etc/init.d/ulog_functions.sh
source /etc/init.d/event_handler_functions.sh
SERVICE_NAME="autofwup"
Debug()
{
	echo "[FW.sh] $@" >> /dev/console
} 
check_dual_partition_update()
{
	FIRMWARE_PART_1=`syscfg get fwup_part_1`
	FIRMWARE_PART_2=`syscfg get fwup_part_2`
	Debug "Checking dual partition update: $FORCED_UPDATE"
	Debug "part1: $FIRMWARE_PART_1"
	Debug "part2: $FIRMWARE_PART_2"
	if [ "$FORCED_UPDATE" == "$FIRMWARE_PART_1" ] && [ "$FORCED_UPDATE" == "$FIRMWARE_PART_2" ]; then
		Debug "Forced Update Done"
		PartitionUpdated=2
	fi
}

# [...]

verify_linksys_header () 
{
	ErrorCode=2
	Debug "verify_linksys_header"
	
	LINKSYS_HDR="/tmp/linksys.hdr"
	FILE_LENGTH=`stat -c%s "$1"`
	IMAGE_LENTGH=`expr "$FILE_LENGTH" - 256`
	dd if="$1" of="$LINKSYS_HDR" skip="$IMAGE_LENTGH" bs=1 count=256 > /dev/console
	magic_string="`cat $LINKSYS_HDR | cut -b 1-9`"
	if [ "$magic_string" != ".LINKSYS." ]
	then
		ulog autofwup status  "Fail : verify magic string "
		exit $ErrorCode
	fi
	hdr_version="`cat $LINKSYS_HDR | cut -b 10-11`"
	hdr_length="`cat $LINKSYS_HDR | cut -b 12-16`"
	sku_length="`cat $LINKSYS_HDR | cut -b 17`"
	sku_end=`expr 18 + "$sku_length" - 2`
	sku_string="`cat $LINKSYS_HDR | cut -b 18-$sku_end`"
	img_cksum="`cat $LINKSYS_HDR | cut -b 33-40`"
	sign_type="`cat $LINKSYS_HDR | cut -b 41`"
	signer="`cat $LINKSYS_HDR | cut -b 42-48`"
	kernel_ofs="`cat $LINKSYS_HDR | cut -b 50-56`"
	rfs_ofs="`cat $LINKSYS_HDR | cut -b 58-64`"
	crc1=`dd if="$1" bs="$IMAGE_LENTGH" count=1| cksum | cut -d' ' -f1`
	hex_cksum=`printf "%08X" "$crc1"`
	if [ "$img_cksum" != "$hex_cksum" ]
	then
		ulog autofwup status "Fail : verify image checksum "
		Debug "Checksum Error"
		exit $ErrorCode
	fi
	
	Debug "verify_linksys_header: success"
}
 
verify_header () 
{
	header_file="/tmp/img_hdr"
	magic="`cat $header_file | cut -b 1-6`"
	version="`cat $header_file | cut -b 7-8`"
	img_cksum="`cat $header_file | cut -b 25-32`"
	rm -rf $header_file
	if [ "$magic" != ".CSIH." ]
	then
		ulog autofwup status "Fail : verify magic "
		exit 1
	fi
	
	if [ "$version" != "01" ]
	then
		ulog autofwup status "Fail : verify version "
		exit 1
	fi
	crc1=`cksum $1 | cut -d' ' -f1`
	hex_cksum=`printf "%08X" "$crc1"`
	if [ "$img_cksum" != "$hex_cksum" ]
	then
		ulog autofwup status "Fail : verify checksum "
		exit 1
	fi
}

update_key_data()
{
	Server=$(syscfg get fwup_server_uri)
	Model=$(syscfg get device::modelNumber)
	Hardware=$(syscfg get device::hw_revision)
	Mac=$(syscfg get device::mac_addr | tr -s ':' '-')
	Version=$(syscfg get fwup_firmware_version)
	Serial=$(syscfg get device::serial_number)
	Request=$(printf "%s/api/v2/key?manufacturer=linksys&mac_address=%s&model_number=%s&hardware_version=%\ 
    s&installed_version=%s&serial_number=%s" $Server $Model $Hardware $Mac $Version $Serial)
	KeyData1=/var/config/keydata
	KeyData2=/etc/keydata
	echo "$Request"
	if [ -e "$1" ]; then
		rm "$1"
	fi
	Response="$1.dat"
	curl --capath "/etc/certs/root" -o "$Response" "$Request" 
	if [ $? -eq 0 ] && [ -s "$Response" ]; then
		fwkey "$Response" "$1"
	else
		Debug "updating key: failed"
	fi
	if [ -s "$1" ]; then
		diff -q "$1" "$KeyData1"
		if [ $? -ne 0 ]; then
			cp "$1" "$KeyData1"
		fi
	else
		if [ -s "$KeyData1" ]; then
			cp "$KeyData1" "$1"
		else
			if [ -s "$KeyData2" ]; then
				cp "$KeyData2" "$1"
			fi
		fi
	fi
}
check_gpg_signature()
{
	Error=2
	Debug "check_gpg_signature"
	export GNUPGHOME=/tmp/gpg
	if [ ! -d $GNUPGHOME ]; then
		mkdir $GNUPGHOME
		chmod 700 $GNUPGHOME
	fi
	cd $GNUPGHOME
	KeyData="$GNUPGHOME/keydata"
	update_key_data $KeyData
	gpg --import --ignore-time-conflict "$KeyData"
	if [ $? -ne 0 ]; then
		return $Error
	fi
	gpg --verify --ignore-time-conflict "$1" 
	if [ $? -ne 0 ]; then
		return $Error
	fi
	
	Debug "check_gpg_signature: success"
}

decrypt_gpg_image()
{
	Error=2
	Debug "decrypt_gpg_image"
	ImageFile="$GNUPGHOME/firmware"
	gpg --ignore-time-conflict -d "$1" > $ImageFile
	
	if [ $? -ne 0 ]; then
		return $Error
	fi
	FirmwareImage="$ImageFile"
	Debug "decrypt_gpg_image: success"
}
extract_gpg_image()
{
	Error=2
	check_gpg_signature "$1"
	
	if [ $? -ne 0 ]; then
		return $Error		
	fi
	decrypt_gpg_image "$1"
	if [ $? -ne 0 ]; then
		return $Error		
	fi
}

check_signature()
{
	ErrorCode=2
	Debug "check_signature [$1]"
	if [ ! -e "$1" ]; then
		exit $ErrorCode
	fi
	check_gpg_signature "$1"
	if [ $? -ne 0 ]; then
		verify_linksys_header "$1"
	fi
}
verify_signature()
{
	ErrorCode=2
	Debug "verify_signature [$1]"
	if [ ! -e "$1" ]; then
		exit $ErrorCode
	fi
	RegionCode=`skuapi -g cert_region | awk -F"=" '{print $2}' | sed 's/ //g'`
	ProdType=$(cat /etc/product.type)
	GpgMode=$(syscfg get fwup_gpg_mode)
	extract_gpg_image "$1"
	if [ $? -ne 0 ]; then
		FirmwareImage=""
	fi
	if [ "$FirmwareImage" == "" ]; then
		if [ "$RegionCode" == "US" ] && [ "$ProdType" == "production" ]; then
			exit $ErrorCode
		elif [ "$GpgMode" == "1" ]; then
			exit $ErrorCode
		else
			FirmwareImage="$1"
		fi
	fi
}

# [...]

Ultimately, there are a few interesting bits and pieces like the verify(_linksys)_header functions where we can exactly see which bytes in the raw data correspond to what field in the header. That said, what we need to decrypt our official firmware update from part1, namely FW_EA6100_1.1.6.181939_prod.gpg.img are the couple of gpg related functions! Our decryption routine roughly looks as follows now:

GNUPGHOME=/tmp/gpg
KeyData="$GNUPGHOME/keydata"
gpg --import --ignore-time-conflict "$KeyData"
gpg --ignore-time-conflict -d EncryptedFW > DecryptedFW

If we look closer, we're able to see that the content of KeyData is also dynamically generated by querying the system config contents via syscfg get <key> and if the new key data differs from the currently present key data we update it accordingly:

update_key_data()
{
	Server=$(syscfg get fwup_server_uri)
	Model=$(syscfg get device::modelNumber)
	Hardware=$(syscfg get device::hw_revision)
	Mac=$(syscfg get device::mac_addr | tr -s ':' '-')
	Version=$(syscfg get fwup_firmware_version)
	Serial=$(syscfg get device::serial_number)
	Request=$(printf "%s/api/v2/key?manufacturer=linksys&mac_address=%s&model_number=%s&hardware_version=%s\
    &installed_version=%s&serial_number=%s" $Server $Model $Hardware $Mac $Version $Serial)
	KeyData1=/var/config/keydata
	KeyData2=/etc/keydata
	echo "$Request"
	if [ -e "$1" ]; then
		rm "$1"
	fi
	Response="$1.dat"
	curl --capath "/etc/certs/root" -o "$Response" "$Request" 
	if [ $? -eq 0 ] && [ -s "$Response" ]; then
		fwkey "$Response" "$1"
	else
		Debug "updating key: failed"
	fi
	if [ -s "$1" ]; then
		diff -q "$1" "$KeyData1"
		if [ $? -ne 0 ]; then
			cp "$1" "$KeyData1"
		fi
	else
		if [ -s "$KeyData1" ]; then
			cp "$KeyData1" "$1"
		else
			if [ -s "$KeyData2" ]; then
				cp "$KeyData2" "$1"
			fi
		fi
	fi
}

At the time of writing, this article, $Request holds this value for me: https://update1.linksys.com/api/v2/key?manufacturer=linksys&mac_address=EA6100-EU&model_number=1&hardware_version=<redacted>&installed_version=1.1.6.173444&serial_number=<redacted>. Curling this API endpoint works and returns the needed GPG key:

Putting it all together enables us to fully decrypt the new firmware and start analyzing that one as well. The number of devices for which this encryption scheme with the same GPG key works is yet to be tested:

And with that, we're already coming to an end here. Analyzing the whole  firmware image is out of scope for this article, but maybe we can take a look at a later time :) (Hit me up if you're interested). To summarize, in this little blog post series, we:

  1. Learned some PCB reversing,
  2. Reviewed some UART basics,
  3. Gained some more insight into how firmware updates for IoT devices could potentially be delivered securely (at least to some degree),
  4. We peeked at how init.d is used for system initialization, and finally
  5. We saw how to use QEMU locally to utilize our firmware dump that has a different architecture effectively.

Note: This blog post is purely for educational purposes and does not highlight anything new or groundbreaking. It's mostly for my little archive and if anyone finds it interesting to read or even learns just a tiny bit, putting it out here was the right choice.

P.S.: I'm fully aware that this is a thing and LinkSys apparently failed to properly delivery GPG encrypted firmware images from the start, so they had to put this huge disclaimer there and provide two firmware samples, one unencrypted that contains the GPG key and the newer encrypted ones. Also, don't ask me why that's only true for the US region but not the "other regions":