Decoding an ESP8266 Firmware Image

I recently coded a Python script for Decoding an ESP8266 Firmware Image. It’s the first part of my quest to create a Linux tool for creating a single flash image.

More specifically, the script will decode a single binary image or a ‘combined’ image. A combined image (single flash image) can be created with the flash download tool from Expressif. (Exressif is the company which makes the ESP8266.)  However, that download tool only runs on Windows. Here is what the Expressif Flash Tool looks like:

 

ESP8266 Flash Tool on Windows

Notice, the ‘CombineBin’ button. It is used to create a single binary image from the multiple files (‘segments’). In that image you can see seven files (the current SDK 2.1.0 files as of this post date) listed and the addresses to load them . I used the ‘CombineBin’ button and created a single binary image called sdk-2.1.0.bin. (The default file name it creates is called target.bin.)

I run Linux. and the recommend ESP8266 tool for it is esptool.py. The esptool script, however, does not have a option to create a single combined image. It may have that feature as an enhancement at some future time. Hence, my quest, to develop a Linux tool to create a single flash image.

I like to keep my ESP8266 firmware current. To flash the latest SDK, I use this script:

esptool.py --baud 115200 --port /dev/ttyUSB0 write_flash -fm dio -ff 40m -fs detect \
0x00000 /opt/arduino/esp8266/ESP8266_NONOS_SDK-master/bin/boot_v1.7.bin \
0x01000 /opt/arduino/esp8266/ESP8266_NONOS_SDK-master/bin/at/512+512/user1.1024.new.2.bin \
0x7E000 /opt/arduino/esp8266/ESP8266_NONOS_SDK-master/bin/blank.bin \
0x81000 /opt/arduino/esp8266/ESP8266_NONOS_SDK-master/bin/at/512+512/user2.1024.new.2.bin \
0xFB000 /opt/arduino/esp8266/ESP8266_NONOS_SDK-master/bin/blank.bin \
0xFC000 /opt/arduino/esp8266/ESP8266_NONOS_SDK-master/bin/esp_init_data_default_v08.bin \
0xFE000 /opt/arduino/esp8266/ESP8266_NONOS_SDK-master/bin/blank.bin

While that works fine, I think it would be more efficient to create a single combined image to flash. Especially since I have a number of ESP8266 modules to flash. The single image (sdk-2.1.0.bin created from the windows tool) can be flashed as follows:

esptool.py --baud 115200 --port /dev/ttyUSB0 write_flash -fm dio -ff 40m -fs detect 0x00 sdk-2.1.0.bin

To create a tool to combine all the ‘single’ images into one, one needs to know the format of the single images and the format of the ‘combined’ image.

Espressif has a brief document (HERE), describing the firmware image format. However, that document does not fully cover what is need to decode a combined image. Also, it does not cover all of the single image ‘formats’. Esptool.py has an image_info command, however, it only works on single modules. It does not work on combined modules.

In my ‘reverse engineering’ attempt at Decoding an ESP8266 Firmware Image, I created this Python script called esp8266_parse_bin.py. Here is the code:

#!/usr/bin/env python
#
# esp8266_parse_bin.py Version 1.1 1/22/2018
# (C)opyright 2018 Email: earl@microcontrollerelectronics.com
# Parses (decodes) ESP8266 single or combined binary image files
#
#
# Partial document on the ESP8266 Image Format
# https://github.com/espressif/esptool/wiki/Firmware-Image-Format
#
#    0        1   2         3   4-7   8
# 0xE9 segments SPI mem/speed entry  ...
# 0xEA segments SPI mem/speed entry  ...
#
#  --SEGMENTS--
#
#     0-3   4-7   8-n
#  Offset  size   data

import os, sys, string, struct

fsize    = 0
chk      = 0
htype    = ''
filename = sys.argv[1]
f        = open(filename, "rb")
afsize   = os.stat(filename).st_size
baddr    = ""
bsize    = 0
useg     = ""
uaddr    = ""
usize    = 0

def blank_header():
  global baddr,bsize
  print("%-8s %-12s %-12s %-12s %-12s %-12s") % ("","","Size/Hex","Size/Bytes","File/Offset","File/Offset")
  print("%-8s %-12s %-12s %-12s %-12s %-12s") % ("Blank(s)","0xff",'0x'+hex(bsize)[2:].zfill(8),bsize,'0x'+hex(baddr)[2:].zfill(8),baddr)
  baddr = ""
  bsize = 0

def unknown_header():
  global uaddr,usize,useg 
  print("%-8s %-12s %-12s %-12s %-12s %-12s") % ("","","Size/Hex","Size/Bytes","File/Offset","File/Offset")
  print("%-8s %-12s %-12s %-12s %-12s %-12s") % ("Unknown","",'0x'+hex(usize)[2:].zfill(8),usize,'0x'+hex(uaddr)[2:].zfill(8),uaddr)
  for b in bytearray(useg): print(hex(b)),
  print
  uaddr = ""
  useg  = ""
  usize = 0

print("Parsing: %s Size: %s/%s") % (filename,hex(afsize),afsize)

while(1):
  t = f.read(8)
  if not t: break
  l = len(t)
  sh = ord(t[0])
  if (sh != 0xff):
    if (baddr != ""): blank_header()
  if ((sh == 0xea) or (sh == 0xe9) or (sh == 0xff)):
    if (uaddr != ""): unknown_header()
  if l < 8:
    print("Extra Data [no-header] of Length: %s -> ") % (len(t)),
    for b in bytearray(t): print(hex(b)),
    print
    fsize += l
    break
  h = struct.Struct("<BBBBI").unpack(t)
  if (h[0] == 0xea): 
    segments = 1
    htype    = 0xea
  else:
    if (h[0] == 0xe9):
      segments = int(h[1])
      htype    = 0xe9
      chk      = 0 
    else:
      if (h[0] == 0xff):
        if (baddr == ""): baddr = fsize
        bsize += l
        fsize += l
        continue
      else:
        if (uaddr == ""): uaddr = fsize
        useg  += t
        usize += l
        fsize += l
        continue

  fsize += l
  print "Header: ",hex(h[0]),int(h[1]),hex(h[2]),hex(h[3]),hex(h[4])
  print("%-8s %-12s %-12s %-12s %-12s %-12s") % ("Segment","Offset","Size/Hex","Size/Bytes","File/Offset","File/Offset")
  for x in range(0,segments):
    data = f.read(8)
    if not data: break
    s = struct.Struct("<II").unpack(data)
    offset  = s[0]
    size    = s[1]
    print("%-8s %-12s %-12s %-12s %-12s %-12s") % (x+1,'0x'+hex(offset)[2:].zfill(8),'0x'+hex(size)[2:].zfill(8),size,'0x'+hex(fsize)[2:].zfill(8),fsize),
    if (htype != 0xea):
      if (offset > 0x40200000): print(" <-- Offset > 0x40200000 "),
      if (offset < 0x3ffe0000): print(" <-- Offset < 0x3ffe0000 "),
      if (size > 65536):        print(" <-- Size > 65536 "),
    print
    l = int(s[1])
    fsize += 8 + l
    data = f.read(l)
    for b in bytearray(data): chk ^= int(b)

  if (htype == 0xea): continue
  pad  = 16 - (fsize % 16)
  print("Padding: %s bytes -> ") % (pad),
  data = f.read(pad)
  for b in bytearray(data): print(hex(b)),
  print
  chk = chk ^ 0xEF
  print("Calculated Checksum: %s") % (hex(chk)),
  if (ord(data[-1]) == chk): print " [matches]",
  print
  fsize += pad
  continue

if (uaddr != ""): unknown_header()
if (baddr != ""): blank_header()

print("File Size: %s  Calculated: %s") % (afsize,fsize),
if (afsize == fsize): print(" [matches]"),
print

Esp8266_parse_bin.py can be used to decode any of the single modules that comprise the combined sdk-2.1.0.bin image or it can be used to decode the ‘combined’ image.

When it is used to decode the combined sdk-2.1.0.bin image via:

./esp8266_parse_bin.py sdk-2.1.0.bin

Here is what the output looks like:

Parsing: sdk-2.1.0.bin Size: 0xff000/1044480
Header:  0xe9 3 0x0 0x20 0x4010057c
Segment  Offset       Size/Hex     Size/Bytes   File/Offset  File/Offset 
1        0x40100000   0x00000a20   2592         0x00000008   8           
2        0x3ffe8000   0x000002fc   764          0x00000a30   2608        
3        0x3ffe82fc   0x000002a4   676          0x00000d34   3380        
Padding: 16 bytes ->  0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x22
Calculated Checksum: 0x22  [matches]
                      Size/Hex     Size/Bytes   File/Offset  File/Offset 
Blank    0xff         0x00000010   16           0x00000ff0   4080        
Header:  0xea 4 0x0 0x1 0x40100004
Segment  Offset       Size/Hex     Size/Bytes   File/Offset  File/Offset 
1        0x00000000   0x0005e0f0   385264       0x00001008   4104        
Header:  0xe9 3 0x0 0x20 0x40100004
Segment  Offset       Size/Hex     Size/Bytes   File/Offset  File/Offset 
1        0x40100000   0x000076dc   30428        0x0005f108   389384      
2        0x3ffe8000   0x00000874   2164         0x000667ec   419820      
3        0x3ffe8880   0x000023bc   9148         0x00067068   421992      
Padding: 4 bytes ->  0x0 0x0 0x0 0x69
Calculated Checksum: 0x69  [matches]
                      Size/Hex     Size/Bytes   File/Offset  File/Offset 
Unknown               0x00000008   8            0x00069430   431152      
0x42 0xd 0x2b 0x65 0xff 0xff 0xff 0xff
                      Size/Hex     Size/Bytes   File/Offset  File/Offset 
Blank    0xff         0x00017bc8   97224        0x00069438   431160      
Header:  0xea 4 0x0 0x2 0x40100004
Segment  Offset       Size/Hex     Size/Bytes   File/Offset  File/Offset 
1        0x00000000   0x0005e0f0   385264       0x00081008   528392      
Header:  0xe9 3 0x0 0x20 0x40100004
Segment  Offset       Size/Hex     Size/Bytes   File/Offset  File/Offset 
1        0x40100000   0x000076dc   30428        0x000df108   913672      
2        0x3ffe8000   0x00000874   2164         0x000e67ec   944108      
3        0x3ffe8880   0x000023bc   9148         0x000e7068   946280      
Padding: 4 bytes ->  0x0 0x0 0x0 0x68
Calculated Checksum: 0x68  [matches]
                      Size/Hex     Size/Bytes   File/Offset  File/Offset 
Unknown               0x00000008   8            0x000e9430   955440      
0x98 0x88 0x89 0x38 0xff 0xff 0xff 0xff
                      Size/Hex     Size/Bytes   File/Offset  File/Offset 
Blank    0xff         0x00012bc8   76744        0x000e9438   955448      
                      Size/Hex     Size/Bytes   File/Offset  File/Offset 
Unknown               0x00000080   128          0x000fc000   1032192     
0x5 0x8 0x4 0x2 0x5 0x5 0x5 0x2 0x5 0x0 0x4 0x5 0x5 0x4 0x5 0x5 0x4 0xfe 0xfd 0xff 0xf0 0xf0 0xf0 0xe0 0xe0 0xe0 0xe1 0xa 0xff 0xff 0xf8 0x0 0xf8 0xf8 0x4e 0x4a 0x46 0x40 0x3c 0x38 0x0 0x0 0x1 0x1 0x2 0x3 0x4 0x5 0x1 0x0 0x0 0x0 0x0 0x0 0x2 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0xe1 0xa 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x1 0x93 0x43 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x1 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0
                      Size/Hex     Size/Bytes   File/Offset  File/Offset 
Blank    0xff         0x00002f80   12160        0x000fc080   1032320     
File Size: 1044480  Calculated: 1044480  [matches]

From that output, one can see where (address) and how each of the single images are placed in the ‘combined’ image.  Now that the combined image format is known, a script can be coded to create it from the individual files.

Leave a Reply

Your email address will not be published.