Main menu

Pages

1506 SUNPLUS CHIP REAL METHED FOR CHEANG LOGO SUNPLUS LOGO CHEANG FULL METHED


1506 SUNPLUS CHIP REAL METHED FOR CHEANG LOGO SUNPLUS LOGO CHEANG FULL METHED 




Unpacking Firmware Images from Cable Modems


Hacking Cable modems used to be very popular during the early 2000’s. People like DerEngel and Isabella from TCNiSO carried lots of research on the topic and talks from bitemytaco (R.I.P) and BlakeSelf during DEFCON 16 and DEFCON 18 covered lots of information on the subject.

Securing cable modems is more difficult than other embedded devices because, on most cases, you can’t choose your own device/firmware and software updates are almost entirely controlled by your ISP. Most cable modems offer a limited administrative interface and management commands are sent using SNMP.

Cable Modem Firmware

There are basically three types of firmware images for cable modems:

- Signed and compresed (PKCS#7 & binary)
- Compressed binary images
- RAM dump images (uncompressed & raw)

You can dump your own firmware image using JTAG or sniffing the connection during upgrades, for example. I’m a big fan of binwalk and I always wondered why it doesn't unpack firmwares from popular Broadcom based cable modems so I decided to research on this.

Unpacking the Firmware

For this analysis I’ll use Cisco DPC3925, which is a very common DOCSIS 3.0 modem here in Brazil. Cisco DPC3925 has a BCM3380 chipset, 16MB Flash x 64MB DRAM memory configuration.



The compressed firmware image has around 4MB. Using strings against the file didn't help much and binwalk v1.2.1 (without any additional parameters) did not recognize it.



We can gather lots of useful information from the vendor’s page: user guides, datasheets, licensing information and open source disclaimer for the product. There are no sources available on Cisco's home, but the Copyright Notices section states that the product uses LZMA SDK 4.21.


So we know that the firmware is probably packed using LZMA but we still need to figure out how to unpack it. Binwalk -i displays results marked as invalid during the scan and we might get some clue:


The LZMA header is not well documented. There are some good resources on lzma-purejs Github and you can also check binwalk's magic file signatures (devttys0 already did all the hard work for us).

  Offset Size  Description
    0     1    lc, lp and pb in encoded form
    1     4    dictSize (little endian)
    5     8    uncompressed size (little endian)

The Bootloader in the beggining of the flash contains the necessary information to boot the firmware image. On the top of the firmware there's always an extractor which decompress the firmware into DRAM.

Offset 0x677 is a good candidate because it's located in the beginning of the file and it seems to have a valid header. 5D 00 00 00 01 indicates a LZMA compression level of -8 and the next 64 bits should be the data's uncompressed size (in little endian).


The 64 bits following the header (00 20 20 0E 3A 28 AB EF) is clearly not a valid uncompressed size (2898643604054482944 bytes). It represents the actual compressed data, making binwalk and 7zr unable to extract it.

What we need to do here is append a few extra bytes to the header so our regular 7zr binary can recognize and extract the data. We don't know the uncompressed size for the firmware yet: the good news is that we can append and specify a big value here, allowing 7zr utility to unpack it (although complaining that the EOF was reached too early). Let's specify 268435456 bytes (256MB), convert it to little endian (00 00 00 10 00 00 00 00) and append it to the original LZMA header. The new header should be something like ... 5D 00 00 00 01 00 00 00 10 00 00 00 00 00 20 20 ...

I took the opportunity to have a look on binwalk's API and wrote a simple lzma-unpacker.py:



#!/usr/bin/python
import os, sys
from binwalk import Binwalk
def lzma_callback(offset, results):
for result in results:
if result['description'].startswith('LZMA compressed data, properties: 0x5D'):
with open(sys.argv[1]) as f:
f.seek(result['offset'])
lzma_header = f.read(5)
uncompressed_size = '\x00\x00\x00\x10\x00\x00\x00\x00'
data = f.read()
output = open(sys.argv[1]+'.lzma', 'w')
output.write(lzma_header+uncompressed_size+data)
f.close()
if __name__ == '__main__':
nargs = len(sys.argv)
if nargs != 2:
print '\
\nLZMA Unpacker: Extract LZMA sections from firmware images\n\
\nTested with the following Cable Modems:\n\
- Cisco DPC3925, DPC2434\n\
- Motorola SB5100, SB5101, SVG6582, SVG1202\n\
- Thomson ACG905, DCM425, DHG534, DHG544, DWG850, DWG874\n\
- Webstar DPC2203\n\
\nBernardo Rodrigues, http://w00tsec.blogspot.com\n\
\nUsage: %s firmware_image.bin' % os.path.basename(sys.argv[0])+'\n'
else:
with Binwalk() as bw:
try:
with open(sys.argv[1], 'rb'):
bw.display.header()
bw.scan(sys.argv[1], callback=lzma_callback, show_invalid_results=True)
try:
with open(sys.argv[1]+'.lzma', 'rb'):
bw.extractor.add_rule('lzma:7z:7zr e -y %e')
bw.scan(sys.argv[1]+'.lzma', callback=bw.display.results)
except Exception:
print 'LZMA 0x5D signature not found'
exit
except IOError:
print 'File not found: '+sys.argv[1]
view rawlzma-unpacker.py hosted with ❤ by GitHub
This code will be obsolete in a couple of days because I'm pretty sure Binwalk incorporate this (a plugin maybe?)


The data was extracted successfully and contains 21982740 bytes. If we replace the uncompressed size on the LZMA header with the correct value in Little Endian (14 6E 4F 01 00 00 00 00), the 7zr tool would not complain about the file integrity.


Most Broadcom cable modems are packed this way, including the ones manufactured by different vendors. The script was fully tested and works fine for the following models:

  - Cisco DPC3925, DPC2434
  - Motorola SB5100, SB5101, SVG6582, SVG1202
  - Thomson ACG905, DCM425, DHG534, DHG544, DWG850, DWG874
  - Webstar DPC2203

Firmware Analysis

Now that you successfully unpacked the firmware, here's a couple of cool things you should do:

- Find default passwords



- Find backdoors



- Pentest the Web Application





Comments

table of contents title