Prologue
In 2011 I bought a DVR for my home video surveillance, an Avtech AVD 744B.The feature i preferred was the MJPEG streaming, so I made a webapp to view my cameras with any smartphone or pc without installing any kind of software, but only using a browser.
In 2012 Apple updated Safari including some new rules on the management of the received stream by multipart urls, so i couldn't see the stream on my iPhone any more.
I was very annoyed by this!
This is the why I started analyzing the firmware of my dvr.
Traffic Analysis
The first thing I did was dumping the traffic with netcat:Request:
$ nc 192.168.2.10 554 GET /cgi-bin/guest/Video.cgi?media=MJPEG&resolution=CIF&channel=1 HTTP/1.1 [...other session headers copied from browser request...]
Response:
HTTP/1.1 200 OK Expires: 0 Pragma: no-cache Cache-Control: no-cache Content-Type: multipart/x-mixed-replace;boundary="boundary" --boundary Content-Type: image/jpeg Content-Length: 13117 JFIFC [...frame...]--boundary Content-Type: image/jpeg Content-Length: 13155 JFIFC [...frame...]
Multipart standard want that any frame is recognized by the starting string --[boundary header variable] and a new line at the end.
How you can see the parameter boundary, is with double quotes in the header, while in the payload is without double quotes, in addition after any frames the \r\n is missing.
This mistakes make the streaming corrupted for updated browsers, freezing the video after the first frame, because they can't recognize the next frames.
Firmware Analysis
I started unpacking the firmware update to find where were this bugs:- Download
I downloaded the last firmware update.
The firmware of my dvr is 744B_AVD. - Extract
Extracting the archive, I found 4 files:
- AppImg_4.bin (Applications archive)
- kernel_4.bin (Linux kernel)
- fboot_4.bin (Bootloader)
- xml_4.bin (Default configs)
- AppImg_4.bin (Applications archive)
- Simple analysis
.bin extension is not enough to know what kind of file it is, in fact this means that it is a generic binary file, so I tried to see what the command file could find:$ file AppImg_4.bin AppImg_4.bin: data
It returns data, so the file might contains an header to obfuscate the firmware.
Analyzing it with Binwalk returned useful information:
$ binwalk AppImg_4.bin DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- 200 0xC8 JFFS2 filesystem, little endian 10948228 0xA70E84 Zlib compressed data, compressed
Removing the header I got the clean jffs2 image:
$ dd if=AppImg_4.bin of=AppImg_4.jffs2 bs=1 skip=200
To mount a JFFS2 file system I needed to load some kernel module and create a volume in ram (I chose 40 megabytes but for other firmware maybe required a larger size).
I needed to be root from now on.
# modprobe mtdblock # modprobe jffs2 # modprobe mtdram total_size=40000 erase_size=256 # dd if=AppImg_4.jffs2 of=/dev/mtdblock0 # mkdir AppImgMnt # mount -t jffs2 /dev/mtdblock0 AppImgMnt
To don't have to mount this file system each time I want to modify the firmware, I copied all files in a different directory.
# mkdir AppImgCnt # cp -a AppImgMnt/* AppImgCnt/
Moving inside AppImgCnt I could find a lot of interesting file, so I started looking for the "boundary" string to find the executable containing it:
# fgrep -r "boundary" .
I noticed that there were three squash file system:
- arch.lzma.sqfs (binary, libraries and html for web interface)
- osg.lzma.sqfs (theme for gui)
- osg-STYLE1.lzma.sqfs (other theme for gui)
Before extracting any squash i copied them outside AppImgCnt to don't dirty the firmware image.
I tried to extract these squash fs but they require modified tools to unsquash them because they are compressed with lzma, so I used slax tools.
After downloaded the unsquashfs tool I extracted arch.lzma.sqfs:
# wget http://ftp.slax.org/useful-binaries/linux/unsquashfs.gz # gunzip unsquashfs.gz # chmod 755 unsquashfs # ./unsquashfs arch.lzma.sqfs Parallel unsquashfs: Using 1 processor 625 inodes (625 blocks) to write [======================================================================-] 625/625 100% created 537 files created 45 directories created 88 symlinks created 0 devices created 0 fifos
- bin/
- HTML/
- lib/
Trying to search again for "boundary" in the unsquashed archive (inside squashfs-root):
# fgrep -r "boundary" . Binary file ./bin/streamd matches Binary file ./bin/SmtpClient matches Binary file ./lib/libcurl.so.4 matches Binary file ./lib/libcgi.so matches
To reduce the number of files to fix, I analyzed where this four binaries contained "boundary" string:
# strings bin/streamd | grep "boundary" --boundary --boundary --boundary --boundary multipart/x-mixed-replace;boundary="boundary" # strings bin/SmtpClient | grep "boundary" Content-Type: %s; boundary="%s" "; boundary="----%s"%s%s boundary # strings lib/libcurl.so.4 | grep "boundary" %s; boundary=%s Content-Type: multipart/mixed, boundary=%s # strings lib/libcgi.so | grep "boundary" find_next_boundary boundary multipart/x-mixed-replace;boundary="boundary" --boundary
So the only binaries to fix are:multipart/x-mixed-replace;boundary="boundary"
- bin/streamd
- lib/libcgi.so
Patching Firmware
- Fixing Binaries
To fix this binary I need to remove double quotes before and after boundary string, and insert two chars \r\n before each new frame header.
- Streamd
Before modifying anything I controlled the file size:# ls -l streamd -rwxr-xr-x 1 root root 434668 Mar 5 2012 streamd
- Streamd
Is very important keep the same file size, or the binary will result in a corrupted executable.
I opened streamd with an hex editor and I searched the string boundary:
The piece of string selected is a part of frame header found in traffic dump:
I need to insert hex byte 0D (\r) and 0A (\n) at offset 0x5CE2C and 0x5CE2D. Then I removed two null bytes (offset 0x5CE6A and 0x 5CE6B) at the end of the string to compensate the new length. This still works because there is at least one null byte (string terminator).--boundary Content-Type: image/jpeg Content-Length:
I did the same thing for other three headers.
Searching again boundary string I found "boundary" section to fix:
This is the Content type header, (reference from traffic dump):
I removed two double quotes (at addresses 0x5D18B and 0x5D194) and I added two null byte at the end (at addresses 0x5D193 and 0x5D194) always to compensate the new length.Pragma: no-cache Cache-Control: no-cache Content-Type: multipart/x-mixed-replace;boundary="boundary"
I controlled the binary size again:
The file size is the same, so if I modified all correctly I should still have a valid executable.# ls -l streamd -rwxr-xr-x 1 root root 434668 Feb 7 12:45 streamd
Control file size:
# ls -l libcgi.so -rwxr-xr-x 1 root root 43280 Mar 5 2012 libcgi.so
Open with hex editor and search boundary:
I removed double quotes (at offset 0x9AF3 and 0x9AFC) and added two null bytes (at offset 0x9AFB and 0x9AFC) at the end of the boundary header, then before --boundary i added a new line (bytes 0D and 0A at offset 0x9B00 and 0x9B01) and removed two null bytes at the end of the string (at offset 0x9B36 and 0x9B37).
Control the file size again:
Same size, perfect.# ls -l streamd -rwxr-xr-x 1 root root 43280 Feb 7 12:50 libcgi.so
Both binary are now fixed.
Considering I modified these two binary in their location (squashfs-root/bin/ and squashfs-root/lib/), I directly made the squash fs.
I need to know the Block size of original squashfs:
# ./unsquashfs -s arch.lzma.sqfs Found a valid little endian SQUASHFS 3:1 superblock on arch.lzma.sqfs. Creation or last append time Mon Mar 5 03:10:49 2012 Filesystem is exportable via NFS Inodes are compressed Data is compressed Fragments are compressed Check data is not present in the filesystem Fragments are present in the filesystem Always_use_fragments option is not specified Duplicates are removed Filesystem size 3532.44 Kbytes (3.45 Mbytes) Block size 1048576 Number of fragments 13 Number of inodes 670 Number of uids 6 Number of gids 0
Block size is 1048576. This is also a Little endian squashfs.
So I made the new squash fs.
Inside squashfs-root I have the new squashfs archive.# wget http://ftp.slax.org/useful-binaries/linux/mksquashfs.gz # gunzip mksquashfs.gz # chmod 755 mksquashfs # cd squashfs-root # ../mksquashfs bin/ HTML/ lib/ arch.lzma.sqfs -b 1048576 -le Parallel mksquashfs: Using 1 processor Creating little endian 3.1 filesystem on arch.lzma.sqfs, block size 1048576. lzmadic 1048576 [===========================================================================|] 537/537 100% Exportable Little endian filesystem, data block size 1048576, compressed data, compressed metadata, compressed fragments, duplicates are removed lzmadic 1048576 Filesystem size 3532.46 Kbytes (3.45 Mbytes) 27.85% of uncompressed filesystem size (12683.66 Kbytes) Inode table size 4657 bytes (4.55 Kbytes) 21.53% of uncompressed inode table size (21630 bytes) Directory table size 6132 bytes (5.99 Kbytes) 53.95% of uncompressed directory table size (11367 bytes) Number of duplicate files found 12 Number of inodes 670 Number of files 537 Number of fragments 13 Number of symbolic links 88 Number of device nodes 0 Number of fifo nodes 0 Number of socket nodes 0 Number of directories 45 Number of uids 6 root (0) unknown (503) unknown (1009) unknown (501) unknown (500) unknown (502) Number of gids 0
Now I have to replace the original squashfs archive with the fixed one. (I'm inside squashfs-root folder)
# cp arch.lzma.sqfs ../AppImgCnt/
Inside AppImgCnt there is the firmware completely patched, but I need to create a new jffs image to flash it on the dvr rom.
This rom has a erase block size of 128k (--eraseblock=0x20000) and is a nand (--no-cleanmarkers) so doesn't need cleanmakers.
(To make the new jffs2 I installed mtd-utils).
# apt-get install mtd-utils # cd AppImgCnt/ # mkfs.jffs2 -r . -o ../AppImg_fixed.jffs2 --eraseblock=0x20000 --little-endian --no-cleanmarkers
The file size is almost the same, so I shouldn't have any problem flashing it.# ls -l AppImg_4.jffs2 -rw-r--r-- 1 root root 10948772 Feb 7 12:10 AppImg_4.jffs2 # ls -l AppImg_fixed.jffs2 -rw-r--r-- 1 root root 10947556 Feb 7 13:15 AppImg_fixed.jffs2
Flashing Firmware
Now I have a valid firmware, but without a signed header (200 byte I removed from original firmware), so I can't flash it from web interface or usb, but I can try using serial console.(This does NOT avoid warranty)
- Opening dvr
To open the dvr I removed six screws, four on the back and the others two on both sides.
Raised upwards the case and I could see the mother board.
- Connecting to UART
UART scheme:
To connect to UART port I used my Arduino board to convert it from serial to usb.
I opened GTKterm, in Configuration > Port I set usbTTY0 as port and 34800 as baud rate.
- Boot message
Booting the dvr, on serial console appears this:
012346 U-Boot 2008.10 (Jul 1 2011 - 16:49:39) DRAM: 128 MB Flash: 16 MB Using default environment flash no default environment In: serial Out: serial Err: serial Net: FTGMAC#0 flash size=0x01000000 checking keyboard is exist.. iic type KEYBOARD_ID=00000098 PLAT_ID=0x81812000(T) PLAT_ID_ALL=0x81812141(T) boardid=0x27 version=0x02 force_output=0x30303030 Starting probe decoder... found NVP1114A decoder is nvp1114A Set 108Mz Setting.. check video std: no signal check video std: no signal check video std: no signal check video std: no signal Set 108Mz Setting.. Composite Out @LCD2 set scaler OEMName=OEM:STYLE1_AVD744B using IVS bitmap Not BMP Type !! Not support bmp 65535 Bits Per Pixel !! Error bitmap format using default bmp using RLE 8-bit/pixel rle total length=76802 clock info: PLL1: 810 MHz PLL2: 750 MHz PLL3: 648 MHz DDR: 810 MHz CPU : 540 MHz HCLK: 270 MHz PCLK: 135 MHz UART: 25 MHz IDE : 81 MHz PCI : 32 MHz MPEG: 162 MHz H.264 enc: 270 MHz H.264 dec: 270 MHz Scaler: 129 MHz SSP: 81 MHz Press SPACE to abort autoboot in 1 seconds
The number 8 allow tftp flash of AppImgPartition plan: Name:BootLoader offset:0x10000000 size:0x00040000 upgrade file:fboot_4.bin Name:Initial bitmap offset:0x10040000 size:0x00020000 upgrade file:custom_logo.bmp Name:Factory default offset:0x100A0000 size:0x00060000 upgrade file: Name:Linux System offset:0x10100000 size:0x00500000 upgrade file:kernel_4.bin Name:Application offset:0x10600000 size:0x00900000 upgrade file:AppImg_4.bin Name:XML offset:0x10F00000 size:0x00100000 upgrade file:xml_4.bin Name:FULL offset:0x10000000 size:0x01000000 upgrade file:full_4.bin ********************************************** * AVC793 Please select option... * 1 : Kermit Update BootLoader Firmware * 2 : Kermit Update Kernel Firmware * 3 : Kermit Update Application (AppImg) * 4 : Kermit Update XML * 5 : TFTP Configuration * 6 : TFTP Update BootLoader Firmware * 7 : TFTP Update Kernel Firmware * 8 : TFTP Update Application (AppImg) * 9 : TFTP Update XML * 0 : TFTP Update Initialize Bitmap * a : TFTP Update FullInOne Image * b : Dump to MEM & Execute Use TFTP * c : Reboot * d : Start Linux * r : Reset Default * u : USB Upgrade * v : Show fw version info ********************************************** avtech>
I installed and started a tftp server on my pc
apt-get install tftp tftp start
I moved the new jffs2 image (not signed) in the shared tftp folder.
I connect the LAN to the dvr and pressed 8.
It asked me three things:
- Host ip (dvr)
- Server ip (pc running tftp)
- File name (fixed jffs2 image)
Tftp upgrade Application HSOT IP 192.168.5.153: 192.168.2.10 SERVER IP 192.168.5.152: 192.168.2.15 File name AppImg_4.bin: AppImg_fixed.jffs2 Using eth0 device TFTP from server 192.168.2.15; our IP address is 192.168.2.10 Filename 'AppImg_fixed.jffs2'. Load address: 0x2000000 Loading: t RD_REQ, file: AppImg_fixed.jffs2 ################################################################# ################################################################# ################################################################# ################################################################# ################################################################# ################################################################# ################################################################# ################################################################# ################################################################# ################################################################# ################################################################# ################################################################# ################################################################# ################################################################# ################################################################# ################################################################# ################################################################# ################################################################# ################################################################# ################################################################# ################################################################# ################################################################# ################################################################# ##################### done Bytes transferred = 10947556 (a70be4 hex) upgrade(y/n):y upgrade fw ... upgrade Application erase 0x10600000 +0x00900000 ........................................................................ done Erased 72 sectors cp.b 0x02000000 0x10600000 $(filesize) Copy to Flash... done filesize=10947556
DO NOT POWER OFF the dvr while it is writing blocks in memory or it will result in a bad flash.
(Anyway is possible to flash again with tftp, the only dangerous thing to flash is u-boot, if the power is removed while it's flashing the bootloader, is very easy to break the dvr!)
I pressed "d" to boot the dvr after flash.