Hacking into a Foscam FI9853EP camera, part 2

In my last post, I examined how to get U-Boot access and obtain the flash data from a Foscam FI9853EP camera. Whereas this data is very useful for offline analysis, I wanted to get a root shell so I can poke around in the system and run commands manually.

Extracting the root filesystem

Last time, we extracted a file called kernel.mtd, which contains an U-Boot uImage with the kernel itself. There will likely be an initrd (a root filesystem) embedded within this kernel, as it contains necessary files such as /sbin/init, the password file, startup scripts etc. This will be our initial target, but how do we get to it?

An easy way is to use binwalk, an utility intended to analyze, extract and reverse engineer firmware files. If we just run this on kernel.mtd, we learn the following:

$ binwalk kernel.mtd 

0             0x0             uImage header, header size: 64 bytes, header CRC: 0xAF395D8A, created: 2017-11-07 02:41:11, image size: 2976536 bytes, Data Address: 0x80008000, Entry Point: 0x80008000, data CRC: 0x36BDB2E2, OS: Linux, CPU: ARM, image type: OS Kernel Image, compression type: none, image name: "Linux-3.4.35"
64            0x40            Linux kernel ARM boot executable zImage (little-endian)
15124         0x3B14          xz compressed data
15356         0x3BFC          xz compressed data

The final line sure looks interesting: there’s a large blob om xz-compressed data there (we know it is large as it is the last item found). Fortunately for us, binwalk can copy all content it finds into files (--extract) and this can be done recursively (--matryoshka), which is convenient as we don’t want it to stop after extracting just the compressed data. Let’s give that a go:

$ binwalk --extract --matryoshka kernel.mtd 


Scan Time:     2023-03-12 15:10:21
Target File:   /tmp/z/_kernel.mtd.extracted/3BFC
MD5 Checksum:  ac84d449376b514839602c61d6ca2ada
Signatures:    411

3846256       0x3AB070        Linux kernel version 3.4.35

Scan Time:     2023-03-12 15:10:23
Target File:   /tmp/z/_kernel.mtd.extracted/_3BFC.extracted/4FD8E8
MD5 Checksum:  09ee7e6258feff31f07bfb9538b78478
Signatures:    411

0             0x0             ASCII cpio archive (SVR4 with no CRC), file name: "tmp", file name length: "0x00000004", file size: "0x00000000"
116           0x74            ASCII cpio archive (SVR4 with no CRC), file name: "root", file name length: "0x00000005", file size: "0x00000000"

Binwalk has found a cpio archive (which is what initrd uses) and extracted it for us to _kernel.mtd.extracted/_3BFC.extracted/_4FD8E8.extracted/cpio-root. We can start looking around.

Yay, root! (or not)

Let’s poke around and see if there is anything interesting:

$ cd _kernel.mtd.extracted/_3BFC.extracted/_4FD8E8.extracted/cpio-root
$ cat etc/passwd

Cool, we have the root password hash! Unfortunately, I’m not patient enough to feed this to a password cracker such as Hashcat.

We could try to update the passwd file and repack the initrd, but this is very tedious: we’d have to re-create the U-Boot uImage, with the proper headers, compression and all that. Perhaps there’s an easier way?

The boot process

The first userland program any UNIX-y system runs is called init (which is often implemented by systemd these days). Let’s see what our camera uses:

$ ls -ld sbin/init
lrwxrwxrwx 1 rink rink 14 Mar 12 15:10 sbin/init -> ../bin/busybox

Busybox is very common on embedded devices and this camera is no exception. Googling around for a bit learns us that Busybox’s init uses /etc/inittab, so let’s see what that holds (comments stripped)


::respawn:/sbin/getty -L ttyS000 115200 vt100 -n root -I "Auto login as root ..."


::shutdown:/bin/umount -a -r
::shutdown:/sbin/swapoff -a

I’m not sure about the auto-login bit, as it certainly greets me with a login prompt and not a root shell. However, we do learn that it runs /etc/init.d/rcS on startup, so let’s look at that:

/bin/mount -a

[ ...snip ascii art... ]

for initscript in /etc/init.d/S[0-9][0-9]*
	if [ -x $initscript ] ;
		echo "[RCS]: $initscript"

OK, so it runs /etc/init.d/S<number>*. What do we have there?

$ ls -l etc/init.d
-rwxr-xr-x 1 rink rink 380 Mar 12 15:10 rcS
-rwxr--r-- 1 rink rink 788 Mar 12 15:10 S00devs
-rwxr--r-- 1 rink rink 111 Mar 12 15:10 S01udev
-rwxr-xr-x 1 rink rink 828 Mar 12 15:10 S80network
-rwxr--r-- 1 rink rink 320 Mar 12 15:10 S90hibernate
-rwxr--r-- 1 rink rink 232 Mar 12 15:10 S90init

Listing all of these would be boring, so let’s summarize what happens:

S00devs creates several devices in /dev, and mounts several filesystems:

  • /dev/mtdblock2 -> /mnt/app (squashfs)
  • /dev/mtdblock3 -> /mnt/app_ext (jffs2)
  • /dev/mtdblock4 -> /mnt/para (jffs2)

It will then add some symlinks:

  • /mnt/app/mtd/boot.sh -> /mnt/mtd/boot.sh
  • [more which are not interesting for now]

S01udev, S80network and S90hiberate are not interesting for our purposes so I’ll skip them.

S90init is the final script. It will try to run either /mnt/para/Debug/boot.sh or /mnt/mtd/boot.sh (note that this is the symlink created by S00devs). These scripts reside on /mnt/app (squashfs) or /mnt/para (jffs2), which means they aren’t contained in the initrd! We ought to go with patching one of them and see what happens.

So we have a choice: we can create /Debug/boot.sh on the app_ext2 jffs2 filesystem, or patch /app/mtd/boot.sh on the app squashfs image. Whatever we put in those scripts will be run as root, and thus we can do whatever we want and hopefully end up with our own root user!

Deciding which one to use

I decided to patch the app squashfs – mainly because the para jffs2 contains settings and I wasn’t sure when it was updated (squashfs images are always read-only so no problem there). Initially, I went with the para jffs2 image but it turned out my flash dump was old and vital configuration files were missing, causing all kinds of problems. Maybe I’ll try again later.

At any rate, once we’ve patched the appropriate image, we can use U-Boot to write it into the flash and we’re done.

Extracting and patching the app squashfs

First step is to extract the image. It turns out there is a dedicated utility available, unsquashfs, which does exactly as it says on the tin:

$ unsquashfs app.mtd
Parallel unsquashfs: Using 24 processors
426 inodes (531 blocks) to write

[========================================================================================|] 957/957 100%

created 422 files
created 36 directories
created 4 symlinks
created 0 devices
created 0 fifos
created 0 sockets
created 0 hardlinks
$ ls -l squashfs-root/mtd
drwxrwxrwx 8 rink rink  4096 Nov 16  2017 app
-rwxr-xr-x 1 rink rink 31253 Nov 16  2017 boot.sh
-rwxr-xr-x 1 rink rink    29 Nov 16  2017 pkg_info
-rwxr-xr-x 1 rink rink    34 Nov 16  2017 resolv.conf

That seemed to have worked! If we look at squashfs-root/mtd/boot.sh, it’s a shell script of almost 900 lines that does a lot of stuff. It ends with the following steps:

hostname IPCamera
#if [ -f ${APP_DIR}/zbin.tar.xz ];then
#   echo "The first time boot OK"
    #rtctool -rtctosys
    MsgServer &
    killall udevd # free 464k mem memory
    sleep 5
    /usr/bin/watchdog &

I found it interesting to see they killed udev after startup to reclaim extra memory. I added the following lines after hostname IPCamera:

echo "hello world"
echo "qq::0:0::/root:/bin/sh" >> /etc/passwd
/usr/sbin/telnetd &

This should show me that the modifications are active (by showing hello world) it would add a root user with the username qq and it would launch telnetd (which I also noticed on the image) so I can log in remotely.

Repacking the app squashfs

With the changes in place, we need to create our own squashfs image to flash into the device. However, we do need to make it compatible with the camera: it is pretty old and may not support everything squashfs supports these days.

Thankfully, unsquashfs has a -stat option to show this information:

$ unsquashfs -stat app.mtd 
Found a valid SQUASHFS 4:0 superblock on app.mtd.
Creation or last append time Thu Nov 16 08:51:11 2017
Filesystem size 11298912 bytes (11034.09 Kbytes / 10.78 Mbytes)
Compression xz
Block size 131072
Filesystem is exportable via NFS
Inodes are uncompressed
Data is compressed
Uids/Gids (Id table) are uncompressed
Fragments are not stored
Xattrs are compressed
Duplicates are removed
Number of fragments 0
Number of inodes 462
Number of ids 2
Number of xattr ids 0

A tool called mksquashfs can pack a directory into a squashfs image. Using the information above and verifying using unsquashfs -stat, I managed to find the correct flags after a few tries:

$ mksquashfs squashfs-root app-patched.mtd -comp xz -noI -no-fragments

All that remains is to flash this into the camera!

Flashing the image

Since we have U-Boot access, the easiest way is to TFTP our patched image into memory and flash it. The previous blog post detailed the offsets of the flash, but I’ll repeat them here:

Creating 5 MTD partitions on "hi_sfc":
0x000000000000-0x000000080000 : "boot"
0x000000080000-0x000000380000 : "kernel"
0x000000380000-0x000000e80000 : "app"
0x000000e80000-0x000000f80000 : "app_ext"
0x000000f80000-0x000001000000 : "para"

This means our new app-hacked.mtd must be written to offset 0xe80000 and is 0xe80000 - 0x380000 = 0xb00000 bytes in length. First, we need to download the new image to memory:

hisilicon # setenv serverip
hisilicon # setenv ipaddr
hisilicon # tftp 82000000 app-hacked.mtd

Then we have to initialize the flash, erase the correct bytes and write our new content:

hisilicon # sf probe 0
hisilicon # sf erase 380000 b00000
hisilicon # sf write 82000000 380000 b00000

Now on to the scary part: trying it out!

Trying it out

Resetting the camera, keeping our fingers crossed and watching the boot logs is promising:

init phy power successful!
load hi_mipi driver successful!
==== Your input Sensor type is ov9732 ====
com v200 ptz
ptz_init start ptz_state[1],HorPos[0],VerPos[0]
setup ptz gpio success
setup zoom gpio success111
ptz_init end ptz_state[1],HorPos[-1],VerPos[-1],flag[0]
Hisilicon Watchdog Timer: 0.01 initialized. default_margin=60 sec (nowayout= 0, nodeamon= 0)
mkdir: can't create directory '/usr/local': File exists
hello world
 #### APP_VER_0:  2 == 2 ? #### 
 #### not need to rewrite appVer_item0,old: 2 #### 
 #### APP_VER_2:  2 == 2 ? #### 
 #### not need to rewrite appVer_item2,old: 2 #### 
 #### APP_VER_3: 30 == 30 ? #### 

We see the hello world message, so we know our changed script was run. Let’s try to log in as qq:

Awesome! I like the greeting message in there, I’m sure they expected someone to tinker with this device at some point 🙂

Next step: figuring out the update process

Having root access should make it a lot easier to poke around in the device and see what we can find. In the next step, I’ll dig into the firmware update process. Stay tuned!

This entry was posted in Reverse engineering and tagged . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *