Methods of extracting firmware from IoT devices – Part Two
Overview
In part one we discussed various ways of dumping the flash, sadly they all were unsuccessful.
In part two, we will continue working on understanding the device tree layout for the Eagle 3.
This should move us closer to the end goal of obtaining a persistent shell on the device.
Once we have that access, we will investigate the file system and some binaries present.
Finally, we will wrap up this post with GPL compliance issues and next steps.
Let's continue on where part one left off and start exploring the device tree and what opportunities that presents us.
Device Tree
The concept and usage of Device Tree's dates back to PowerPC and SPARC architectures that utilise Open-firmware.
Simply put, a device tree is a way to describe the hardware to the running kernel. This removes the need for hard-coded values in kernel modules and other device drivers.
SPI devices are a great example, for a start, the SPI bus might be present on different hardware pins and controlled by different groupings of a pinctrl on different boards. This means that even if we use the same SPI flash chip, we might need to address it differently on each different brand/type of board we use because of hardware connectivity/layout differences.
So now the kernel needs to be told the correct pincrtl
mapping to find the SPI bus and the correct reg list of address(s) for the device we wish to access.(The SPI flash in this example)
This information is defined in our Device Tree and read by the kernel to create the device nodes or mtd partitions in our case.
So what does this all mean?
The Eagle has a custom device tree that defines how to access the flash. Importantly, it also defines the memory addresses for each partition. Keep in mind mtd devices have no partition table stored anywhere. There are multiple ways to tell the kennel about the layout, but the most common is via the device tree.
Therefore, we need to construct a valid device tree…
We have the offset addresses for each partition from the original boot-log(See Part One), and we have the SPI device details too.
However, writing a custom device tree sounds error-prone and time-consuming.
Oh well, let's get into it!
Sysupgrade
Since the thought of handwriting a device tree was not inspiring me, I decided to go read the manual for the Eagle to see what other interesting things I could find out.
I then saw an interesting statement, The EAGLE 3 updates its firmware automatically. Okay, so how?
I've not set up a cloud account with Rainforest or done anything, but it can just upgrade itself?
Let's boot it up, maybe it might log something to the console…
Oh… Interesting:
[25290.815042] CHECKUPDATE: start -> /data/updates/filelist.txt
[25290.846216] VERSION: Loading config file '/etc/info/site_manager.info'
[25290.860421] VERSION: string = '3.22.24.549'
[25290.870666] VERSION: bits = 3/22/24
[25290.878117] VERSION: current version is ok
[25290.886773] VERSION: VER = 3 - 22 - 24
[25290.894361] DOWNLOAD: FILE: filelist.txt -> /data/updates/filelist.txt
[25290.910749] DOWNLOAD: URL : /retail-eagle3-file-update/filelist.txt
[25290.923787] DOWNLOAD: PORT: https
[25290.930875] DOWNLOAD: HOST: production-update-server.s3.us-west-2.amazonaws.com
[25290.947101] DOWNLOAD: TIMEOUT: 3600
[25292.200635] CHECKUPDATE: VER = 0 - 0 - 0
[25292.219110] NEWER: 0.0.0 > 3.22.24 ?
[25292.245369] NEWER: done (No)
[25292.251171] CHECKUPDATE: Up to date
Wait, so this thing just calls out to some AWS host and grabs stuff, oh dear…
So this gives us some options to investigate:
- Take a look at what we can find on that AWS site
- Redirect DNS requests on my LAN and serve up our update for the Eagle.
This AWS setup is also a prime target for eventual dangling subdomain takeovers.
Moving on…
Let's check out this host:
HOST: production-update-server.s3.us-west-2.amazonaws.com
Okay, so no authentication, and an XML file-listing we can view. So yes, the Eagle 3 just connects to some AWS instance and downloads an update/sysupgrade. There are checksums's of the updates, but we have no way to verify the integrity or ownership of the files since they are not signed via Public-key cryptography or use any form of digital signatures.
Look what we found:
<Contents>
<Key>retail-eagle3/sysupgrade-v3.22.22.547.bin</Key>
<LastModified>2023-09-25T19:03:38.000Z</LastModified>
<ETag>"c0c60ae3a7998d1ffef4aecd300e393d-2"</ETag>
<Size>20972294</Size>
<StorageClass>STANDARD</StorageClass>
</Contents>
This is an OpenWrt sysupgrade image for the Eagle 3.
For sysupgrade to work, it must be able to mount all file systems, that means this image MUST have a valid device tree embedded.
Now, this sounds way more fun than writing one.
Let's download that file and take a look.
# wget production-update-server.s3.us-west-2.amazonaws.com/retail-eagle3/sysupgrade-v3.22.22.547.bin
It won't matter if the version you get is different, we are only after the device tree that is embedded in the image, so any sysupgrade binary should be fine.
Lastly, if we had not found this sysupgrade file, I would have just written the device tree file myself. We have most of the information we need, and the rest should be in data sheets, tedious but very possible. Had that failed, I would have dumped the flash via external tools, and then extracted the device tree from that dump.
Getting a ready-made sysupgrade file just made our life easier, let's take a look at how we get what we require next.
Binwalk and sysupgrade
Once you have downloaded the file. We will use binwalk to see what we can discover.
Binwalk is a great way to analyse and extract things from firmware images.
I advise you to learn binwalk in depth, it is a very valuable tool for dealing with binary images/reverse engineering. In the sections below, we only touch the surface of what binwalk can do.
Let's run binwalk without any options to see what it discovers:
$ binwalk sysupgrade-v3.22.22.547.bin
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
0 0x0 uImage header, header size: 64 bytes, header CRC: 0xFCAA868, created: 2022-02-16 20:47:59, image size: 1707552 bytes, Data Address: 0x80000000, Entry Point: 0x80000000, data CRC: 0xFFB9330A, OS: Linux, CPU: MIPS, image type: OS Kernel Image, compression type: lzma, image name: "MIPS OpenWrt Linux-4.14.267"
64 0x40 LZMA compressed data, properties: 0x6D, dictionary size: 8388608 bytes, uncompressed size: 5381084 bytes
1707616 0x1A0E60 Squashfs filesystem, little endian, version 4.0, compression:xz, size: 19104375 bytes, 3374 inodes, blocksize: 262144 bytes, created: 2022-02-16 20:47:59
Okay, so as expected we find a bunch of stuff since the sysupgrade image is just a Linux firmware image for the device.
Looking at the above output, at the following offsets we see:
- Offset =
0x0
, a Linux kernel - Offset =
0x40
, a 5 MB LZMA archive of unknown data - Offset =
0x1A0E60
a Squashfs archive, most likely the root file system, or an overlay for it.
Let's extract each offset by using binwalk again. Issue the command:
binwalk -e sysupgrade-v3.22.22.547.bin
This should create a subdirectory called _sysupgrade-v3.22.22.547.bin.extracted
.
Change into that directory and let's take a look at the file binwalk extracted from the offset 0x40
.
Why 0x40
? We know that the MIPS architecture needs to load the device tree early in the boot process, unlike modern ARM systems that can support late loading. The two most common places for a device tree are in a initramfs
image booted before the kernel, or as a data blob inserted after the kernel inside a binary image. We know from the boot-log that the Eagle 3 is not using an initramfs
and we also see a small archive in the binwalk output, at offset 0x40
. So that seems a good place to start.
You will notice this file is just called 40
. Binwalk is unsure what to call it since it is just a LZMA archive, so binwalk uses the offset number as a file name.
Since we don't know what is inside the archive, rather than try to extract it, we can use binwalk again to inspect the contents to see if we spot anything interesting.
Let's run binwalk again:
$ binwalk 40
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
4159648 0x3F78A0 CRC32 polynomial table, little endian
4765516 0x48B74C xz compressed data
4800608 0x494060 Unix path: /lib/firmware/updates/4.14.267
4836688 0x49CD50 Unix path: /sys/firmware/devicetree/base
4837377 0x49D001 Unix path: /sys/firmware/fdt': CRC check failed
4845729 0x49F0A1 Neighborly text, "neighbor table overflow!tics"
4864700 0x4A3ABC Neighborly text, "NeighborSolicitsports"
4864720 0x4A3AD0 Neighborly text, "NeighborAdvertisements"
4867606 0x4A4616 Neighborly text, "neighbor %.2x%.2x.%pM lost rename link %s to %s"
5042176 0x4CF000 ELF, 32-bit LSB MIPS64 shared object, MIPS, version 1 (SYSV)
5371164 0x51F51C ASCII cpio archive (SVR4 with no CRC), file name: "dev", file name length: "0x00000004", file size: "0x00000000"
5371280 0x51F590 ASCII cpio archive (SVR4 with no CRC), file name: "dev/console", file name length: "0x0000000C", file size: "0x00000000"
5371404 0x51F60C ASCII cpio archive (SVR4 with no CRC), file name: "root", file name length: "0x00000005", file size: "0x00000000"
5371520 0x51F680 ASCII cpio archive (SVR4 with no CRC), file name: "TRAILER!!!", file name length: "0x0000000B", file size: "0x00000000"
5371684 0x51F724 Flattened device tree, size: 9400 bytes, version: 17
Oh, look what we found, a device tree binary/flattened deice tree at address 0x51F724
!
Let's extract the device tree, so we can use that, rather than trying to write our own.
Since we know the starting offset 0x51F724
of our data, and it is the last item in the archive, it will be straightforward to extract.
Extract the data from starting from offset 0x51F724
using dd
:
$ dd if=40 of=device-tree.dtb bs=1 skip=5371684
9400+0 records in
9400+0 records out
9400 bytes (9.4 kB, 9.2 KiB) copied, 0.0610784 s, 154 kB/s
NOTE: dd
uses decimal notation for counting blocks. So in the previous command we skip past blocks using a block size of 1
byte to count. Until we reach the block number 5371684
, since we do not specify count=
dd will just read until the end of the file and stop. Thus extracting all data from 5371684
to the end of the archive for us.
Check the output is a valid device tree binary:
$ file device-tree.dtb
device-tree: Device Tree Blob version 17, size=9400, boot CPU=0, string block size=800, DT structure block size=8544
Perfect!
We now have a device tree binary(dtb) which is the compiled version of the text file device tree source.(dts)
Next, we need to convert the binary file back to the text-based source(dts). This will let us inspect/modify and recompile the device tree binary. Allowing us to use it as we see fit.
Using a tool called dtc we can manipulate dts
and dtb
files.
Let's convert our extracted device tree binary back to its text representation:
$ dtc -s -I dtb device-tree.dtb -O dts -o eagle3.dts
eagle3.dts: Warning (avoid_unnecessary_addr_size): /gpio-keys-polled: unnecessary #address-cells/#size-cells without "ranges" or child "reg" property
Breaking down the above command:
-s
, sort the nodes and properties, very useful when using tools like diff.-I
, input format, device tree binary in our casedevice-tree.dtb
, our extracted dtb from the sysupgrade archive-O
, output format, device tree source-o
, output file name, in our exampleeagle3.dts
Now we have our device tree source.
$ file eagle3.dts
eagle3.dts: Device Tree File (v1), ASCII text
The dtc
tool can decompile and recompile from either format. So we can be very certain this dts
file contains everything we need.
Let's take a look and see what the SPI flash and mtd partitions look like.
spi@b00 {
#address-cells = <0x01>;
#size-cells = <0x00>;
compatible = "ralink,mt7621-spi";
pinctrl-0 = <0x06>;
pinctrl-names = "default";
reg = <0xb00 0x100>;
reset-names = "spi";
resets = <0x01 0x12>;
status = "okay";
flash@0 {
compatible = "jedec,spi-nor";
m25p,chunked-io = <0x20>;
reg = <0x00>;
spi-max-frequency = <0x989680>;
partitions {
#address-cells = <0x01>;
#size-cells = <0x01>;
compatible = "fixed-partitions";
partition@0 {
label = "u-boot";
read-only;
reg = <0x00 0x30000>;
};
partition@1C00000 {
label = "data";
reg = <0x1c00000 0x2400000>;
};
partition@30000 {
label = "u-boot-env";
read-only;
reg = <0x30000 0x10000>;
};
partition@40000 {
label = "factory";
linux,phandle = <0x12>;
phandle = <0x12>;
reg = <0x40000 0x10000>;
};
partition@50000 {
compatible = "denx,uimage";
label = "firmware";
reg = <0x50000 0x1bb0000>;
};
};
};
};
NOTE: the above is just a section of the dts
file, showing the flash, not the entire dts
file.
Taking a look at the file, we can see the SPI device and the offsets for the mtd partitions are defined correctly. We know they are correct from the boot-log/offsets we saw in Part One of this blog post.
Nice, now what?
U-boot, OEM and device tree
Okay, so we have our device tree source file(dts) and the compiled version (dtb) or flatted device tree format(fdt).
Looking at the manual for U-Boot, it seems we can pass a fdt
image to the bootm command. This should allow us to boot the generic OpenWrt image using the device tree binary we extracted.
Let's jump back into the Eagle's U-Boot environment and see how we can boot the image with a separate device tree binary.
Ah, what is this?
bootm [addr [arg ...]]
- boot application image stored in memory
passing arguments 'arg ...'; when booting a Linux kernel,
'arg' can be the address of an initrd image
MT7628 #
We can only specify a boot image and an initrd
image, but not a device tree?
This version of U-Boot seems not to support anything but a kernel uImage
and an initrd
as boot arguments.
Meaning we cannot load the dtb
file from tftp as the bootm
command has no way of loading that into memory… sigh.
So that's another option we can't make use of.
Device OEM
Okay, looking back at the device booting up we see Ralink UBoot Version: 5.0.0.0
and U-Boot 1.1.3 (Nov 22 2022 - 14:10:02)
.
It seems Ralink forked U-boot around version 1.1.3
, so from what I can understand, Ralink's 5.0.0.0
version is based on U-Boot 1.1.3
is over six years old.
The ability to load a fdt
was added in U-Boot version 1.3.2 via commit - 281ff9a.
Meaning, the ancient version of U-Boot on the Eagle 3 has no support for passing a dtb/fdt
to the bootm
command.
Where did Rainforest get this relic?
We know they don't make the hardware.
Looking at the device tree, I grab a few attributes smart-gateway zigbee MTK7688
.
A few hits from a firm called Dusun IoT show up in an online search, let's take a closer look at their website.
After a few moments of looking at their website, I see the DSGW-030 is close to the Eagle 3 in specs and appearance.
Funny enough, Dusun provide their source code used to build their images for the device.
You can find firmware, SDK's and a tarball of their OpenWrt build setup.
While I would prefer a source code repository like Git, an ugly tarball is still source code that includes their modifications and allows me to build a working image for the OEM device. However, we know the Eagle 3 is slightly different, so this is not an exact source of truth for the Eagle 3.
For example, we know the flash size/layout is different compared to the Eagle 3, so we also cannot use their dts
file.
I did consider upgrading U-Boot, but given how old the version is and the amount of back porting needed, makes this almost impossible/not worth the time and risk to brick the device.
So now we have no way to boot an existing image with the correct drive tree via tftp/bootm commands, ouch…
So, now what?
Back to OpenWrt
What we know so far:
- OpenWrt can build a generic image for this device that boots fine
- OpenWrt uses device tree to configure the boards via dts files
- We can build OpenWrt
- We have the dts and dtb files for the Eagle 3
Building OpenWrt
Given the above facts, it is clear, we require our own build of OpenWrt.
This build would make use of the dts
file we have extracted and allow us to use the OpenWrt failsafe mode to gain a root shell on the device.
Patching the OpenWrt dts file
When OpenWrt uses a dts
file, it checks to ensure the file is compatible with the device/target you are building for.
Looking at the extracted dts file, we can see that the compatible
attribute does not match.
Our extracted dts
file lists:
compatible = "bliot,smart-gateway\0mediatek,mt7628an-soc";
OpenWrt expects:
compatible = "mediatek,mt7628an-eval-board", "mediatek,mt7628an-soc";
Let's patch the dts
file to make the OpenWrt build system happy.
Here is a diff between the dts
we extracted and the OpenWrt generic dts
file.
$ diff device-tree.dtb mt7628an_mediatek_mt7628an-eval-board.dts
1c1
< /dts-v1/;
---
> #include "mt7628an.dtsi"
6,7c6,7
< compatible = "bliot,smart-gateway\0mediatek,mt7628an-soc";
< model = "bliot,smart-gateway";
---
> compatible = "mediatek,mt7628an-eval-board", "mediatek,mt7628an-soc";
> model = "Mediatek MT7628AN evaluation board";
Since this is a once off build, I will copy our extracted device-tree.dts
source file into the directory:
target/linux/ramips/dts/
Apply the above diff changes to our dts
file and then overwrite the OpenWrt file in the same directory named mt7628an_mediatek_mt7628an-eval-board.dts
.
You can also download a copy of a patched dts
that I created and used.
Save this file and overwrite the original mt7628an_mediatek_mt7628an-eval-board.dts
file if you would rather not patch the file yourself.
Building OpenWrt
Following the Build System guide I was able to build a working image of OpenWrt.
There is no need to make any modifications, we don't need any special packages or tools.
Just the generic OpenWrt build with our dts
file built in.
Detailed steps on how to build OpenWrt is out of scope for this post. However, the above page should be all you require.
Once the build is done, you can find the images in the bin/targets/ramips/mt76x8
directory.
We need the initramfs
image, in my case this was called:
openwrt-23.05-snapshot-r23001+509-38c150612c-ramips-mt76x8-mediatek_mt7628an-eval-board-initramfs-kernel.bin
We now should be able to tftp boot this image and, with any luck, have a set of correctly setup mtd partitions. Refer to Part One of this blog post for instructions around booting/tftp.
Access at last!
Time to see our hard work pay off… finally.
Load the image created in the above step via tftpboot
:
MT7628 # tftpboot
NetLoop,call eth_halt !
NetLoop,call eth_init !
Trying Eth0 (10/100-M)
Waitting for RX_DMA_BUSY status Start... done
ETH_STATE_ACTIVE!!
TFTP from server 192.168.0.120; our IP address is 192.168.0.3
Filename 'test.bin'.
TIMEOUT_COUNT=10,Load address: 0x82000000
Loading: Got ARP REPLY, set server/gtwy eth addr (34:13:e8:c6:39:a0)
Got it
#################################################################
#################################################################
#################################################################
#################################################################
#################################################################
#################################################################
#################################################################
#################################################################
#################################################################
#########################################
done
Bytes transferred = 5866567 (598447 hex)
LoadAddr=82000000 NetBootFileXferSize= 00598447
MT7628 #
Boot the image:
MT7628 # bootm 82000000
## Booting image at 82000000 ...
Image Name: MIPS OpenWrt Linux-5.15.134
Image Type: MIPS Linux Kernel Image (lzma compressed)
Data Size: 5866503 Bytes = 5.6 MB
Load Address: 80000000
Entry Point: 80000000
Verifying Checksum ... OK
Uncompressing Kernel Image ... OK
No initrd
## Transferring control to Linux (at address 80000000) ...
## Giving linux memsize in MB, 64
Starting kernel ...
[ 0.000000] Linux version 5.15.134 (builder@buildhost) (mipsel-openwrt-linux-musl-gcc
NOTE: Output of boot is truncated.
Soon enough, you should see the option to enter fail-safe mode:
Press the [f] key and hit [enter] to enter failsafe mode
Thereafter, you will be dropped into a root shell, time to check if the dts
file works:
root@(none):/# cat /proc/mtd
dev: size erasesize name
mtd0: 00030000 00010000 "u-boot"
mtd1: 02400000 00010000 "data"
mtd2: 00010000 00010000 "u-boot-env"
mtd3: 00010000 00010000 "factory"
mtd4: 01bb0000 00010000 "firmware"
mtd5: 001a0e60 00010000 "kernel"
mtd6: 01a0f1a0 00010000 "rootfs"
mtd7: 007d0000 00010000 "rootfs_data"
Nice, it works!
We have all the expected partitions discovered correctly.
Next, run the OpenWrt mount_root tool:
root@(none):/# mount_root
[ 37.865103] jffs2: notice: (337) jffs2_build_xattr_subsystem: complete building xattr subsystem, 33 of xdat.
[ 37.899334] mount_root: switching to jffs2 overlay
[ 37.912686] overlayfs: upper fs does not support tmpfile.
Good, let's check to ensure we have entered/switched root into the Eagle's root file system:
root@(none):/# df -h
Filesystem Size Used Available Use% Mounted on
tmpfs 28.0M 15.8M 12.2M 56% /rom
tmpfs 512.0K 0 512.0K 0% /dev
tmpfs 28.0M 28.0K 27.9M 0% /tmp
/dev/mtdblock7 7.8M 788.0K 7.0M 10% /overlay
overlayfs:/overlay 7.8M 788.0K 7.0M 10% /
root@(none):/#
Perfect, we can see mtd7
/ rootfs_data
partition has been mounted and OpenWrt has switched root into the Eagle 3's root file system located on the SPI flash.
Given the above, it's time to get us persistent access to the Eagle 3 when it boots normally.
First up, change the root password via passwd root
.
You can also enable SSH while we are here by editing the file /etc/config/sshd
.
Setting the option enable
to yes
is all that is needed.
Once you are done, reboot.
Now try login using root
and the password you set in the previous command.
You now should have a root shell:
eagle-xxxxxx{root}(~)#
Wonderful!
The ash
shell is also available if you do not prefer the tcsh
shell that is set by Rainforest.
So in part one, we were told that the source code is from OpenWrt 15.05. I guess thankfully that is wrong since we can see the Eagle 3 runs on OpenWrt, version 19.07 from February 2022:
# cat /etc/openwrt_release
DISTRIB_ID='OpenWrt'
DISTRIB_RELEASE='19.07-SNAPSHOT'
DISTRIB_REVISION='r11405-2a3558b'
DISTRIB_TARGET='ramips/mt76x8'
DISTRIB_ARCH='mipsel_24kc'
DISTRIB_DESCRIPTION='OpenWrt 19.07-SNAPSHOT r11405-2a3558b'
DISTRIB_TAINTS='no-all glibc busybox'
We can also see the build is a SNAPSHOT, this is a development/local build type of OpenWrt vs a release build. Further evidence to support that Rainforst do rebuild OpenWrt. Nothing prevents them from using a release mode, but it seems they just stuck with a SNAPSHOT for some reason.
Lastly, if the broken vim
configuration is annoying you, edit the file /overlay/upper/root/.vimrc
to make your changes and reboot.
Very sloppy to ship broken configs on a device image!
Miscellaneous findings
Once we have set the root password and enabled SSH access, it makes exploring the system a little easier. Since we can now log in via SSH vs over the serial connection.
Let's take a look around and understand some odd things about the Eagle 3.
HTTP access and hard-coded users
I noticed that when logged in via the Eagle's web UI, there was no way to change my username or password.
It seems like it is hard-coded to the details on the side of the device. (Ignoring any functionally you get via registering this device into their cloud service)
Let's take a look at lighttpd's config.
Using ps
to easily see where lighthttpd reads its configuration file from.
We see the following:
eagle-xxxxxx{root}(~)# ps aux | grep light
root 1195 0.0 1.7 6592 1020 ? S 02:20 0:01 /usr/sbin/lighttpd -f /data/etc/lighttpd.conf
Wonderful, it’s running as root…
Actually, everything but dnsmasq
and avahi-daemon
run as root on the Eagle, ouch!
eagle-xxxxxx{root}(~)# ps aux | awk '{print $1}' |uniq -u
USER
dnsmasq
nobody
Let's take a look at how lighthttpd is authenticating users:
eagle-xxxxxx{root}(~)# cat /data/etc/lighttpd.conf | grep auth.backend | grep -v "#"
auth.backend = "plain"
auth.backend.plain.userfile = "/data/etc/lighttpd.user"
Seriously?
It is using clear text to store passwords?
Taking a look at the file /data/etc/lighttpd.user
, you can see the plain text credentials.
They are hard-coded for each device, it is the same credentials on the sticker on the side of the device.
I would recommend changing the password in the /data/etc/lighttpd.user
file.
Since you can't seem to change it via the WebUI.
Lighthttpd supports multiple formats for encrypted user and password files. There is no excuse to be using hard-coded plain text passwords.
Miscellaneous certificates and keys
Who leaves private keys on a customer's device…
OpenADR Alliance
Seems like Rainforest were trying to get the Eagle 3 certified by OpenADR as a VEN device.
As you can see from the configuration file located at /data/etc/oadr/ven.xml
.
What's worse, however, is they left the test environment SSL certificate, and it's PRIVATE KEY on the device:
# pwd
/data/etc/certs/oadr
# ls -la
drwxr-xr-x 2 root root 0 Oct 17 02:53 ./
drwxr-xr-x 3 root users 0 Nov 1 10:59 ../
-rwxr-xr-x 1 root root 1525 Oct 17 02:53 cert.pem
-rwxr-xr-x 1 root root 1704 Oct 17 02:53 key.pem
#
A snippet from the cert.pem
file:
Issuer: C = US, O = OpenADR Alliance, OU = RSA VEN CA0001, CN = TEST OpenADR Alliance RSA VEN CA
Validity
Not Before: Mar 15 22:10:52 2016 GMT
Not After : Mar 16 22:10:52 2036 GMT
Subject: C = CA, O = Rainforest Automation Inc., OU = TEST OpenADR Alliance RSA VEN Certificate, CN = d8d5b9002dad
Little can be done with this certificate pair to be honest, but it shows a careless attitude to handling sensitive data like private keys.
OpenVPN
It seems like Rainforest has a method to start up an OpenVPN tunnel to connect back into the device. Maybe this is part of the "remote support" option in the WebUi.
We can see below the RC startup script for OpenVPN is enabled:
# pwd
/etc/rc.d
# ls -la S90openvpn
lrwxrwxrwx 1 root root 17 Feb 16 2022 S90openvpn -> ../init.d/openvpn*
#
If you recall system-V init, a capital S
at the start of the name of the symlink means the init process will run this script on system start.
This start script then calls the following script /data/etc/certs/link_certificates.sh
, let's see what its function is…
This script links a set of self-signed certificates into the location that the start script for OpenVPN expects them to be at. However, again we see the public certificate and the PRIVATE KEY are stored on the device:
/data/etc/certs/eagle-xxxxxx.crt
/data/etc/certs/eagle-xxxxxx.key
Where xxxxxx
would be Cloud-ID that is printed on the sticker on the side of the device.
A snippet from the eagle-xxxxxx.crt
file:
Issuer: C = CA, ST = BC, L = Vancouver, O = RainforestAutomation, OU = changeme, CN = changeme, name = changeme, emailAddress = support@rainforestautomation.com
Validity
Not Before: Nov 1 21:49:38 2017 GMT
Not After : Oct 30 21:49:38 2027 GMT
Subject: C = CA, ST = BC, L = Vancouver, O = RainforestAutomation, CN = eagle-xxxxxx
Long expiry, self-signed certificates with the private key on hand, not a great look.
Post_manager binary
While inspecting some general web traffic when I interacted with the Eagle 3, I saw a common item, a CGI binary called post_manager
.
I can now see that all HTTP traffic communicates with this post_manager
binary. Local API, local website, everything…
You can find it at located at: /tools/site_manager/www/cgi/post_manager
.
Let's take a more in-depth look at the binary.
Using strings
command, I am able to locate end points listed in the API guide.
For example, I can see device_details
in the strings output and many others. Let's see what happens if we select a few and query them.
Making use of curl
we can craft a request to the Eagle 3. You will need the base64
encoded username and password for your device and one of the endpoints/commands to try.
An example curl syntax would be:
curl -X POST http://eagle/cgi-bin/post_manager \
-H "Content-Type: application/xml" \
-H "Accept: application/xml" \
-H "Authorization: Basic YOUR-BASE64-CREDS" \
-d "<Command><Name>COMMAND_HERE</Name></Command>"
Where:
http://eagle
is the IP/Host nameYOUR-BASE64-CREDS
replace this with the base64 encoded username and passwordCOMMAND_HERE
replace this with a command, EG:device_details
This syntax is also defined in their local API guide.
Let's take a look at the response we get back from testing out a few commands:
device_details
- I get an HTTP 200 response with the expected data. This command is from the local API manual.network_query
— I get an HTTP 200 response with the expected data. However, this command is NOT listed in the local API manual.get_eagle_status
— I get an HTTP 200 response with an error (not located in the API manual):
<EagleStatus>
<Name>error</Name>
</EagleStatus>
start_eagle_status
— I get an HTTP 200 response, and it seems to have executed some task on the device(not located in the API manual):
<EagleStatus>
<Name>success</Name>
</EagleStatus>
dev_get_network_addresses
— I get an HTTP 200 response and a rather odd error (not located in the API manual):
<Error>
<Description>Duplicate message</Description>
</Error>
Auditing this binary really requires it own blog post, I do plan to load it up in Ghidra later on and see what I can find out. As we can already see, the binary shows some interesting behaviour.
This binary seems to be used by the entire system to configure/run and manage the device.
After this, there is no way I am putting this near the internet, nor would I allow the remote support
option in the WebUI.
GPL findings
Now we know a lot more about the Eagle and the operating system that runs on the device. We most certainly assert that the OpenWrt has been rebuilt with modifications, and it is distributed to users/consumers without the source code being made available. Meaning, this is most likely a GPL violation.
I do not have enough data to assert if any of the custom binaries running on the Eagle are linked to other libraries licensed under the GPL. If they are, that also could be a GPL violation, but it is also possible they have linked to no GPL libraries. Further investigation is needed.
Changes and modifications
This is a non-exclusive list of changes I have found so far:
- OpenWrt failsafe mode disabled. By setting
CONFIG_TARGET_PREINIT_DISABLE_FAILSAFE=Y
Upstream, OpenWrt does not set this, nor has the hardware vendor Dusun. (Checked via downloading their source/build tarball) - Device is not running uhttpd. Makes use of lighttpd with a custom Rainforest code-base.
- The root shell is
tcsh
and not the OpenWrt default of Busybox/ash. - Used a modified
dts
file, OpenWrt, and the files from Dusun do not have the correct SPI and partition entries/offsets. - Addition of:
/etc/init.d/site_manager
RC file. Started as:
S99site_manager -> ../init.d/site_manager
- Modification of
/etc/init.d/openvpn
RC file and started as:
S90openvpn -> ../init.d/openvpn
- Multiple custom public and private key-pairs on the file system.
- Custom root user environment files. (csh/tcsh shell related)
- Image built as a SNAPSHOT,
DISTRIB_RELEASE='19.07-SNAPSHOT'
. - Addition of a
scripts
user:
scripts:x:1001:1001::/data/scripts_root:/bin/ash
- A bunch of custom code under the
/tools
directory. - Broken vim configuration:
Error detected while processing /root/.vimrc.vim:
line 2:
E484: Can't open file /usr/share/vim/vimrc_example.vim
I assume there are more too. However, I think we can say that Rainforest did indeed rebuild and modify the upstream source code and then proceeded to distribute the resulting works.
What is a violation?
I am not a lawyer, I won't ever be one, but I do enjoy reading dull text documents.
The big thing with the GPL is that as soon as you distribute software that uses the GPL, you MUST make the complete source code available.
Distribute can mean give away, sell, bundle etc…
If you use and wrote a GPL licensed application and gave it to no one, and did not distribute it, you don't need to make the source available. As soon as you distribute it, that is when it changes. This is what makes the GPL a copyleft license and empowers the users by giving them the ability to build the code base.
Making source code available means the complete and corresponding source code. This includes any scripts used to build the image and all modifications to GPL licensed code, baring some exceptions.
This release must be complete so that a user can build the exact version you have distributed from the source code distributed.
Exceptions you say?
The GPL makes note of some exceptions around compilers and tool-chains, but this is also complex as it may turn out you still need to release GNU GCC with your source code at times too, but not always.
You also have the GCC Runtime exception, this allows a set of libraries to be linked into applications, and you are not required to use the GPL, you can select your license.
However, this exception list is minimal for valid reasons.
A great resource to learn more about the GPL is gpl-violations.org, copyleft.org and the GNU homepage.
What are you going to do about this?
I make yearly reoccurring donations to the Software Freedom Conservancy(SFC) and the Electronic Frontier Foundation(EFF) and others. The SFC helps individuals report violations and enforces the GPL.
I have submitted other violations to them as well, you can take a read of when I gained root access on an Alcatel GPON ONT device, used in a lot of FTTH deployments which also runs Linux and the vendor has not distributed the source code.
You can also read about the SFC Firmware / IoT Liberation Project or the Vizio GPL case for examples of what the SFC does.
I will also reach out again to Rainforest to give them the opportunity to comply before I make my formal submission to the SFC. I would rather work with the companies and assist them to comply then force them too, however sometimes force is needed.
Let's see where Rainforest takes this.
References and further reading
A list of supporting documents, data sheets, references and related further reading.
- OpenWrt failsafe and factory reset
- Device Tree history
- Device Tree data format
- Device Tree pinctrl binding
- Linux Kernel pinctrl pin documentation
- Device Tree master specification project
- MTD partition scheme and methods
- Rainforest support page - Eagle 3 updates
- AWS dangling domain take over demonstration
- Multi competent boot image - FDT/DTB binary, page 9
- binwalk
- U-Boot commit 281ff9a
- U-Boot version 1.3.2-rc1
- Device Tree Compiler and libfdt - dtc tool
- OpenWrt build system guide
- OpenWrt SNAPSHOT definition
- GNU FAQ on distribution of source code
- GNU FAQ on the definition of exact version
- GNU FAQ on GCC Runtime Exception
- GNU on violations of the GNU License
- GPL-Violations.org Source-code FAQ
- GPL-Violations.org Vendor FAQ
- Copyleft.org
- Software Freedom Conservancy - SFC report violations page
- SFC - Firmware Liberation Project
- SFC - Vizio case
- Electronic Frontier Foundation - EFF.