In GNIF, we are seeing lots of different equipments being deployed for running networks. Many of them are working with VxWorks, especially version 5.5 that is common. It is a real-time OS supplied by WindRiver. For some initial views (still quite complete) on the particularity of this OS and its analysis, you can report to the following posts:
-
Matasano
-
HD Moore
-
Newsoft
This OS is running on many different systems, such as helium balloon for expanding satellite network or IP-enabled fridge and microwave oven. GNIF is proud to provide you some quick results on applying some existing reversing techniques on these systems.
In this 1st part on VxWorks, we are not going to dive into the system itself, but just going to check what you have in your hands when you get such a firmware. Let's go with the helium balloon: we have a binary file of some MB of data suffixed with a .bz, however file indicate it's just data. Let's open it with an hex-editor: we can first see an ascii header followed by hex data.
$ strings -a image.bz | head -20
AZC File Signature
3SZ_SW
* Header Length : 358 Bytes
* Product Version : Rel_9_0_0_8666
* Date & Time : Wed Nov 09 12:03:31 2004
* Code Length : 2358544 Bytes
* Bootrom Length : 176500 Bytes
* Unit Type : PIPO
* HW Revision : 8
* SW Type : S
************* End of Header *****************
4JKTJH
[...]
The data following the header is very entropic. One can guess it is compressed data, thanks to the zlib magic number few bytes after the end of the header: 0x789c. Furthermore, from the header, it looks like we have 2 images (one system and one bootrom). So we will use the most basic strategy to uncompress thoses files: the brute-force! Scan the file for the magic bytes 0x789c, and try to uncompress from the offset... and see what you get.
In few lines of python:
fd = open('image.bz', 'ro')
a = fd.read(); fd.close()
from zlib import decompress
of, f = 0, a.find('\x78\x9c')
while f >= 0:
of += f
try:
data = decompress( a[of:] )
print 'decrompressed at: 0x%x' % of
except:
pass
f = a[of+2:].find('\x78\x9c')
of += 2
This returns the following address:
decrompressed at: 0x210
decrompressed at: 0x2b384
OK, we have our 2 offsets corresponding to our 2 parts; furthermore, we see that the size of the compressed sections is equal to the length provided in the text header. We can now decompress it in files to check it further.
In [107]: fd = open('p1', 'wb')
In [108]: fd.write( decompress( a[0x210:] ))
In [109]: fd.close()
In [112]: fd = open('p2', 'wb')
In [113]: fd.write( decompress( a[0x2b384:] ))
In [114]: fd.close()
In [115]: !ls -l
total 11740
-rw-r--r-- 1 prout users 2535572 Dec 2 14:31 image.bz
-rw-r--r-- 1 prout users 178685 Dec 2 15:06 p1
-rw-r--r-- 1 prout users 9065873 Dec 2 15:07 p2
That's it!
Now let's move to the image of the IP-enabled fridge. Again, we have a file suffixed with .bin. File command does not recognize it => hexedit.
$ hexedit image.bin
00000000 76 65 72 20 39 38 32 00 00 00 00 00 00 00 00 00 ver 982.........
00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000040 01 01 63 00 00 07 85 53 00 00 01 00 00 00 00 00 ..c....S........
00000050 D6 0F E5 B9 66 00 00 00 00 00 00 00 00 00 00 00 ....f...........
00000060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000080 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000090 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000000A0 08 78 9C B4 5A 0D 70 54 55 96 3E 9D 0E 49 77 88 .x..Z.pTU.>.
We have some kind of binary header with a file version, and what could be file size and checksum, and again our gzip magic number: 0x789c. After checking the size of the file section after the gzip magic bytes, we get the corresponding value in the header at offset 0x44: 0x78553. We check in the same way the CRC32 of the file section and get the corresponding value in the header at offset 0x50: 0xD60FE5B9. This is good to know if we want to patch the firmware afterwards.
Now, we can apply the same decompressing script and get a single system image.
We have some VxWorks system images. Next step will be to start the statical analysis, try to retrieve the VxWorks symbol table (inspired from the Matasano recipe) and retrieve the firmware loading address in order to be able to disassemble it properly.
To conclude, we have seen that VxWorks images have similarities in that they are compressed sections with zlib after a header section. However, due to the diversity of board, CPU and uC suppliers, bootloading procedures may vary and may take different kind of header format.