🎥

MJSXJ09CM - Recovering Firmware and Backdooring

First glance at the device

I decided to disassemble the device and take a closer look at the hardware.

ℹ️
The pictures have been taken after the project, therefore the probing lines are visible on the images and most of the non essential elements have been removed to simplify the reverse / test process.

Let’s talk with the board

Taking pictures of the device as been useful in identifying a serial port on the board. Although there is no label around the pins, I identified the UART pins with the help of a multimeter. I wired the board to simplify the probing of the device.

Probing layout

Here is the list of the pins :

- red[RX]
- green[TX]
- black[GND]

Connect with USB over UART

Now that we have the UART connection to the board, we can try to connect the device to a computer using a

I was expecting to end up on a shell output showing me the logs of the boot process and eventually some information regarding the configuration or the OS that was in the device.
However, once we connect to the UART, something unexpected happened.

Time for some debug

Reset the device with Mi Home application

The device came from a lab and had been already tested multiple times, this mean that it might have some configuration fault and so on.
Since I miss most of the device, I don’t know if I can completely performe the installation… But let’s give it a try anyways.

I created a WiFi network for this device, opened the Xiaomi “mi home” application and tried to perform on adding the device to my fake den

ℹ️
It appears that this device and I guess several other are impacted by the use emotes in the Wi-Fi AP name

(yeah, we try broken and unknown IoT stuff but I’ve build a fake den for them, mine wont get infected by all this)

Well, the camera is on the network, and the reset seems to have kinda worked, the issue now is that the camera should be displaying me some 4 digit pin code to enter on the application to finish the setup. I can’t get this code since I don’t have any components linked to a display or anything.

Reset the device and see what the UART tells us

The second idea I had was to read the output of the UART when proceeding to a factory reset, maybe we could get some more informations like a default auth code, a wifi network to access the device or anything that could unstuck the situation

Yes, yes that’s a lot of logs to navigate through… and at a first glance there are not much information that could be useful to fix our issue..

That being said, there are a few lines that gave me another idea, we can see sometimes lines referring to UBOOT

Disable MMU and D-cache before jump to UBOOT

U-Boot 2015.01 (May 17 2021 - 15:28:25), Build: jenkins-ipc029a02_new_key-283

Finding the ultimate Bypass to the broken device via UBOOT to get to the firmware anyways

Yes, Stupid things, I love testing stupid things, If it work I’ll probably get my paws on a corrupted and unstable firmware, but I wanna try it anyways. So let’s plug back the USB to UART adapter, start up the terminal and open a connection

First I booted the device one time to get it in the default mode, from here I could start the USB/UART connection to see what is going on. Then I pressed the reset button from the camera until I got the start of the prompt as shown in the previous debug attempt. From here, I tried to press enter from my pc to force the UBOOT to abort the preprogrammed process and get an access to the firmware

The stupidest it is, the best it is

Now that we have the paws on the shell, let’s try to get some information that could be interesting about the device we are trying to break

SigmaStar # printenv
baudrate=115200
bootargs=console=ttyS0,115200 root=/dev/mtdblock2 rootfstype=squashfs ro init=/linuxrc LX_MEM=0x3fe0000 mma_heap=mma_heap_name0,miu=0,sz=0x1400000 mma_memblock_remove=1
bootcmd=sf probe 0;sf read 0x22000000 ${sf_kernel_start} ${sf_kernel_size};bootm 0x22000000
bootdelay=0
cpu_part_start=14950000
ethact=sstar_emac
ethaddr=00:30:1b:ba:02:db
fileaddr=23b82cf8
filesize=109
gatewayip=172.17.190.1
ipaddr=172.17.190.5
netmask=255.255.255.0
serverip=172.17.190.64
sf_kernel_size=200000
sf_kernel_start=50000
sf_part_size=6b0000
sf_part_start=950000
stderr=serial
stdin=serial
stdout=serial

Environment size: 643/4092 bytes

Here are the boot parameters passed to the kernel.

bootargs=console=ttyS0,115200 root=/dev/mtdblock2 rootfstype=squashfs ro init=/linuxrc LX_MEM=0x3fe0000 mma_heap=mma_heap_name0,miu=0,sz=0x1400000 mma_memblock_remove=1

If we set the init parameter here to /bin/sh, we are telling the Linux kernel to run /bin/sh as init instead of system init. This gives us the root shell during the boot time.

SigmaStar # setenv bootargs console=ttyS0,115200 root=/dev/mtdblock2 rootfstype=squashfs ro init=/bin/sh LX_MEM=0x3fe0000 mma_heap=mma_heap_name0,miu=0,sz=0x1400000 mma_memblock_remove=1

Then to boot on the shell, this is the command to use sf probe 0;sf read 0x22000000 ${sf_kernel_start} ${sf_kernel_size};bootm 0x22000000

Sadly, this shell did not allow us to make changes to the system as the file system is read-only squashfs. If we had a writable filesystem, maybe we could add a script to run a telnet or ssh service on the system.

Obtaining the Firmware through U-Boot

There is a technique that use the very primitive memory display method that is offered by UBOOT.

It is possible to print the memory contents on the U-boot console using the md command. All that is needed is to know the starting address of the firmware in the memory and the size, than all this space can be printed on the screen.

The simplest way to find out this information is to look at the bootcmd parameter in the output of the printenv command.

SigmaStar # printenv
...
bootcmd=sf probe 0;sf read 0x22000000 ${sf_kernel_start} ${sf_kernel_size};bootm 0x22000000
...

The sf read command is used to copy flash content to RAM. 0x22000000 specifies from which address in RAM the content will be copied. This was the first value we needed.

The other value we need is the flash size. We could already see this value when the bootloader first started. We have a 16 MB flash, which means 0x1000000 in hex.

Another way to find out the total mapped data size is to look at the partition information in the boot output.

Get the data

Here is the command that will print all the content of the flash : md.b 0x22000000 0x1000000

Here is an extract of what we can see in the terminal :

⚠️
Note that this process is super time consuming, extracting large volumes of information might take several hours

Process the data

I saved the output of the terminal in the file, that’s good, but now I have a file with the output of the entire console. Currently this file is still not a binary file, it just contains some plain-text and hex characters. It need to be cleaned of the unnecessary content in this file. Only memory output should remain. Something like this :

226404d0: 4d 40 26 68 b2 bf d9 97 4d 40 26 68 b2 bf d9 97    M@&h....M@&h....
226404e0: 4e 40 26 70 b1 bf d9 8f 4e 40 26 70 b1 bf d9 8f    N@&p....N@&p....
226404f0: 4f 40 26 78 b0 bf d9 87 4f 40 26 78 b0 bf d9 87    O@&x....O@&x....
22640500: 50 40 26 80 af bf d9 7f 50 40 26 80 af bf d9 7f    P@&.....P@&.....
22640510: 51 40 26 88 ae bf d9 77 51 40 26 88 ae bf d9 77    Q@&....wQ@&....w
22640520: 52 40 26 90 ad bf d9 6f 52 40 26 90 ad bf d9 6f    R@&....oR@&....o
22640530: 53 40 26 98 ac bf d9 67 53 40 26 98 ac bf d9 67    S@&....gS@&....g
22640540: 54 40 26 a0 ab bf d9 5f 54 40 26 a0 ab bf d9 5f    T@&...._T@&...._
22640550: 55 40 26 a8 aa bf d9 57 55 40 26 a8 aa bf d9 57    U@&....WU@&....W
22640560: 56 40 26 b0 a9 bf d9 4f 56 40 26 b0 a9 bf d9 4f    V@&....OV@&....O
22640570: 57 40 26 b8 a8 bf d9 47 57 40 26 b8 a8 bf d9 47    W@&....GW@&....G

Now this hexdump need to be converted into a binary file. The following python code should do the trick

import sys, struct

data_bin = bytearray()
with open(sys.argv[1]) as hexdump:
        for line in hexdump:
                data_hex= line[10:57].split(" ")
                for i in data_hex:
                        data_bin += struct.pack("B", int(i, 16))

binary_file = open(sys.argv[2], "wb")
binary_file.write(data_bin)
binary_file.close()

It is also available on GitHub, and if this specific one is not working, some other ones should be doing quite the same thing

Firmware-scripts/hex2bin.py at main · SungurLabs/Firmware-scripts
Contribute to SungurLabs/Firmware-scripts development by creating an account on GitHub.
https://github.com/SungurLabs/Firmware-scripts/blob/main/hex2bin.py

Finally, this is a firmware of the device.

Examine the Firmware with binwalk

Now let’s extract it with the help of binwalk and examine it.

binwalk -e firmware.bin

Time for backdooring the device

Now that we have the firmware, it might be interesting to try to add some elements in it to get access to the device once in “production” mode

Extracting the partitions

The first thing to do is to extract the partitions of the firmware and then access the file system that we want to modify. The following script can help to do so

import sys

partitions = [
	("boot", 0x0, 0x50000),
	("uImage_kernel", 0x50000, 0x200000),
	("squashfs", 0x250000, 0x760000),
	("data", 0x9B0000, 16777216-0x9B0000)
]

firmware = open(sys.argv[1], "rb")
for part, offset, size in partitions:
	firmware.seek(offset, 0) # Moves the cursor up to the offset.
	data = firmware.read(size)
	output = open(part, "wb")
	output.write(data)
	print("{} - saved!".format(part))
	output.close()

Decompress Squashfs files

⚠️
Squashfs are a compressed read-only file system and are commonly found on embedded systems. In this way, users are prevented from making changes to the file system.
However, after uncompressing this file system with the 
unsquashfs tool, it is possible to make new additions to it. Then create a new squashfs filesystem with these updated files.
$ unsquashfs -d squashfs_out squashfs

Time to build the door

One of the best places to add backdoor is init scripts(/etc/init.d/). Because these scripts are run while the device is booting and does not require a condition. The actual init script here is rcS, and it runs files in this directory that starting names begin with a capital S, in numerical order. These are the start scripts. Likewise, the rcK script is run at shutdown.

A good candidate to make a backdoor could be telnet. however, the device doesn’t have it’s binary. So we can add a statically compiled version of busy box containing it. Available at the following link

%d %s
https://www.busybox.net/downloads/binaries/1.21.1/busybox-armv7l

And then run it from the sdcard by editing the rcS script by adding this line

/mnt/sdcard/busybox-armv7l telnetd

Let’s resquash all of our dirty modifications

Adding backdoor is complete. Now it’s time to create a new squashfs filesystem with updated init script.

The tool used to do this is called mksquashfs. But first we need to know the compression type and block size of the squashfs to create. To get these informations, it is possible to look at the details of the original squashfs by running unsquashfs with the -s parameter.

Compression is xz and blok size is 131072. Let’s create the new file system

mksquashfs squashfs_out/ squashfs_new -comp xz -b 131072
mv squashfs_new squashfs

Final repack

The last thing we need to do to create the final firmware is to repack the unpacked partitions. to do so, the following script should do the trick

import sys

partitions = [
	("boot", 0x0, 0x50000),
	("uImage_kernel", 0x50000, 0x200000),
	("squashfs", 0x250000, 0x760000),
	("data", 0x9B0000, 16777216-0x9B0000)
]

firmware = open(sys.argv[1], "wb")
for part, offset, size in partitions:
	p = open(part, "rb")
	data = p.read()
	firmware.write(data)
	if len(data) < size:
		size_padd = size - len(data)
		padd = size_padd * b'\x00'
		firmware.write(padd)
		# squashfs should be padded for alignment purposes

Pushing the new firmware to the device

There are two methods to upload the new firmware that has been modified to the device.

Using the SDCard AutoUpdate

One of them is firmware update process via sdcard.

The camera checking the existence of /mnt/sdcard/tf_update.img, /mnt/sdcard/tf_all.img, /mnt/sdcard/tf_all_recovery.img files in sdcard every time it boot up. We can start the update procedure of the device by placing the firmware on the sdcard (firmware has to be named tf_update.img).

However, this process is tricky because the device does the signature verification of the file.

Direct access flash

The other method is firmware uploading via direct access to flash. For this,let’s use a CH341A flash programmer with SOIC8 clip.

We’ll use the flashrom tool to write the firmware to flash. The -p  parameter is used for the programmer name, and the -c parameter is for the flash chip name.

After about 10 minutes of operation, The device have now written our backdoored firmware to flash. Now it’s time to power up the device and test if the telnet port is active.

Testing the backdoor

after some shenanigan I managed to get the IP of my brocken device. Once scanned with nmap, we can see that there is an open port on 23

If we try to access it over telnet, here is the result