That's no moon... that's a budong!

 Subscribe in a reader

Parsing firmware binary image for D-Link DNS-323

Tags: d-link, dns-323, firmware

Lets look at D-Link DNS-323 firmware structure and building procedures.

(You can get parseFirmware.py util here.)

Internally firmware binary contains three parts: - u-boot kernel image - u-boot rootfs image - archive with default parameters (default.tar.gz)

Layout of firmware binary is defined by this C structure:

typedef struct _CONTROL_HEADER_
{
  unsigned long offset_1;
  unsigned long len_1;
  unsigned long offset_2;
  unsigned long len_2;
  unsigned long offset_3;
  unsigned long len_3;
  unsigned long checksum_1;
  unsigned long checksum_2;
  unsigned long checksum_3;
  unsigned char magic_num[12];
  unsigned char product_id;
  unsigned char custom_id;
  unsigned char model_id;
  unsigned char sub_id;
  unsigned char NewVersion;
  unsigned char reserved[7];    //all structure is 64 bytes
  unsigned long Next_offset;
}

Where magic_num contains this string:

//the string is "0x55 0xaa FrodoII 0x00 0x55 0xaa"
unsigned char FRODO2_MAGIC_NUM[12] =
  {0x55,0xaa,0x46,0x72,0x6f,0x64,0x6f,0x49,0x49,0x00,0x55,0xaa};

This is common for D-Link and Conceptronic devices.

Where they differ is a product, custom, model and sub ids.

Those are read from custom.h file located at goahead Web-server sources in goahead/LINUX directory. Each manufacturer puts own Web-interface code for device including custom.h.

What we need looks like this.

#define PRODUCT_ID 7
#define CUSTOM_ID 1
#define MODEL_ID 1

I have already posted here how they get those values from custom.h. Just look at posts with tag "wtf".

We can translate C structure to Python struct string and see offsets, lengths and checksums of its components.

import struct
import sys

try:
    f = open(sys.argv[1], mode='rb')
# We read 9 unsigned longs from file
# unsigned long -- 4 bytes
    bytes = f.read(36)
except IOError:
    print "Unable to read header from firmware file"
    sys.exit()

    (kern_offset, kern_length, ramdisk_offset,
     ramdisk_length, defaults_offset, defaults_length,
     kernel_checksum, ramdisk_checksum,
     defaults_checksum) = struct.unpack('LLLLLLLLL', bytes)

    print """Kernel offset: %s
    Kernel length: %s
    Ramdisk offset: %s
    Ramdisk length: %s
    Defaults offset: %s
    Defaults length: %s
    Kernel checksum: %s
    Ramsdisk checksum: %s
    Defaults checksum: %s""" % (kern_offset, kern_length, ramdisk_offset,
                                ramdisk_length, defaults_offset,
                                defaults_length, kernel_checksum,
                                ramdisk_checksum, defaults_checksum)

So to get kernel, ramdisk and defaults archive we must read number of bytes from specified location and write them in separate files.

To slice components from firmware binary image run command like this.

$ ./parseFirmware.py <path to firmware image>

for example:

$ ./parseFirmware.py ~/devel/DNS323.1.04

parseFirmware.py will create three files:

We can't do much with kernel image as there are no modifications possible without recompilation.

As for defaults archive, we can completely ignore it and use only to build our firmware binary (why it can be ignored will be explained later).

All that left is ramdisk image. I made some shortcut in parseFirmware.py to get gzip-ed ramdisk ext2fs image instead of u-boot image so you can gunzip it and mount like this.

# gunzip Ramdisk.gz
# mkdir ./ramdisk
# mount -o loop Ramdisk ./ramdisk

Ramdisk image contains normal directory structure and cramfs image image.cfs.

# ls ./ramdisk
bin  etc   image.cfs  lost+found  proc  sbin  tmp  var  welcome.msg
dev  home  lib        mnt         root  sys   usr  web
#

Mount image.cfs to look inside.

# mkdir ./cramfsimage
# mount -o loop ./ramdisk/image.cfs ./cramfsimage/
# ls ./cramfsimage
bin        default       language  lltd   samba  scsi         upnp  web_page
codepages  etc_codepage  lib       LPRng  sbin   shared_name  web

We won't touch those directories for now. What's looks interesting is a default directory. Apparently its content similar to default.tar.gz content.

To understand what function each of them serving we can look at source (in goahead/webs.c function extractUploadedFileContent) where firmware is unpacked and written into flash memory. It's obvious from the source that only kernel and ramdisk are flashed to device and default.tar.gz is only get its checksum checked.

As for default directory at cramfs it is used at startup.

In ./ramdisk/etc/fstab:

/image.cfs      /sys/crfs       cramfs  loop            0       0

In ./ramdisk/etc/rc.sh:

ln -s /sys/crfs/default /default

Looks like cramfs image contains default parameters which are used after "Reset To Factory Defaults" command is issued from Web-interface.

Published: 2008, June 04
Leschinsky Oleg