Skip to content

Commit 8b3133d

Browse files
author
Chris Lyne
committed
Initial commit
0 parents  commit 8b3133d

File tree

5 files changed

+1241
-0
lines changed

5 files changed

+1241
-0
lines changed

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# ESP32 Firmware Image to ELF
2+
This tool can be used to convert a flash dump from an ESP32 into an ELF file.
3+
4+
There are three actions:
5+
- **show_partitions** - will display all of the partitions found in an image file.
6+
- **dump_partition** - will dump the raw bytes of a specified partition into a file.
7+
- **create_elf** - reconstruct an ELF file from an 'app' partition (e.g. ota_0).
8+
9+
# Setup
10+
`pip install -r requirements.txt`
11+
12+
# Example Usage
13+
## Show all partitions
14+
$ python3 esp32_image_parser.py show_partitions ~/simplisafe_bs_espwroom32.bin
15+
16+
## Dump a specific partition
17+
Dumps to ble_data_out.bin
18+
`$ python3 esp32_image_parser.py dump_partition ~/simplisafe_bs_espwroom32.bin -partition ble_data`
19+
20+
Dumps to ble_data.dump
21+
`$ python3 esp32_image_parser.py dump_partition ~/simplisafe_bs_espwroom32.bin -partition ble_data -output ble_data.dump`
22+
23+
## Convert a specific app partition into an ELF file
24+
Converts ota_0 partition into ELF. Writes to ota_0.elf
25+
`$ python3 esp32_image_parser.py create_elf ~/simplisafe_bs_espwroom32.bin -partition ota_0 -output ota_0.elf`

esp32_firmware_reader.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import struct
2+
import sys
3+
import argparse
4+
from makeelf.elf import *
5+
6+
PART_TYPES = {
7+
0x00: "APP",
8+
0x01: "DATA"
9+
}
10+
11+
PART_SUBTYPES_APP = {
12+
0x00: "FACTORY",
13+
0x20: "TEST"
14+
}
15+
16+
for i in range(0,16):
17+
PART_SUBTYPES_APP[0x10 | i] = "ota_" + str(i)
18+
19+
PART_SUBTYPES_DATA = {
20+
0x00: "OTA",
21+
0x01: "RF",
22+
0x02: "WIFI",
23+
0x04: "NVS"
24+
}
25+
26+
def print_verbose(verbose, value):
27+
if verbose:
28+
print(value)
29+
30+
def read_partition_table(fh, verbose=False):
31+
fh.seek(0x8000)
32+
partition_table = {}
33+
34+
print_verbose(verbose, "reading partition table...")
35+
for i in range(0, 95): # max 95 partitions
36+
magic = fh.read(2)
37+
if(magic[0] != 0xAA or magic[1] != 0x50):
38+
print('Magic bytes are fudged')
39+
return partition_table
40+
41+
print_verbose(verbose, "entry %d:" % (i))
42+
part_type = ord(fh.read(1))
43+
part_subtype = ord(fh.read(1))
44+
part_offset = struct.unpack("<I", fh.read(4))[0]
45+
part_size = struct.unpack("<I", fh.read(4))[0]
46+
part_label = fh.read(16).decode('ascii').rstrip('\x00')
47+
part_flags = fh.read(4)
48+
49+
part_type_label = "unknown"
50+
if(part_type in PART_TYPES):
51+
part_type_label = PART_TYPES[part_type]
52+
53+
part_subtype_label = "unknown"
54+
if(part_type_label == "APP" and part_subtype in PART_SUBTYPES_APP):
55+
part_subtype_label = PART_SUBTYPES_APP[part_subtype]
56+
if(part_type_label == "DATA" and part_subtype in PART_SUBTYPES_DATA):
57+
part_subtype_label = PART_SUBTYPES_DATA[part_subtype]
58+
59+
print_verbose(verbose, " label : " + part_label)
60+
print_verbose(verbose, " offset : " + hex(part_offset))
61+
print_verbose(verbose, " length : " + str(part_size))
62+
print_verbose(verbose, " type : " + str(part_type) + " [" + part_type_label + "]")
63+
print_verbose(verbose, " sub type : " + str(part_subtype) + " [" + part_subtype_label + "]")
64+
print_verbose(verbose, "")
65+
66+
partition_table[part_label] = {"type":part_type, "subtype":part_subtype, "offset":part_offset, "size":part_size, "flags":part_flags}
67+
68+
def dump_bytes(fh, offset, length, filename, verbose=False):
69+
print_verbose(verbose, "Dumping " + str(length) + " bytes to " + filename)
70+
fh.seek(offset)
71+
data = fh.read(length)
72+
fh1 = open(filename, 'wb')
73+
fh1.write(data)
74+
fh1.close()
75+
return (filename, data)

esp32_image_parser.py

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
#!/usr/bin/env python
2+
3+
# Convert an ESP 32 OTA partition into an ELF
4+
5+
import os, argparse
6+
from makeelf.elf import *
7+
from esptool import *
8+
from esp32_firmware_reader import *
9+
10+
def image_base_name(path):
11+
filename_w_ext = os.path.basename(path)
12+
filename, ext = os.path.splitext(filename_w_ext)
13+
return filename
14+
15+
# section header flags
16+
def calcShFlg(flags):
17+
mask = 0
18+
if 'W' in flags:
19+
mask |= SHF.SHF_WRITE
20+
if 'A' in flags:
21+
mask |= SHF.SHF_ALLOC
22+
if 'X' in flags:
23+
mask |= SHF.SHF_EXECINSTR
24+
25+
return mask
26+
27+
# program header flags
28+
def calcPhFlg(flags):
29+
p_flags = 0
30+
if 'r' in flags:
31+
p_flags |= PF.PF_R
32+
if 'w' in flags:
33+
p_flags |= PF.PF_W
34+
if 'x' in flags:
35+
p_flags |= PF.PF_X
36+
return p_flags
37+
38+
def image2elf(filename, output_file, verbose=False):
39+
image = LoadFirmwareImage('esp32', filename)
40+
section_map = {
41+
'DROM' : '.flash.rodata',
42+
'BYTE_ACCESSIBLE, DRAM, DMA': '.dram0.data',
43+
'RTC_IRAM' : '.rtc.text', # TODO double-check
44+
'IROM' : '.flash.text'
45+
}
46+
47+
# parse image name
48+
image_name = image_base_name(filename)
49+
50+
# iram is split into .vectors and .text
51+
# however, the .vectors section can be identified by its unique 0x400 size
52+
53+
section_data = {}
54+
elf = ELF(e_machine=EM.EM_XTENSA, e_data=ELFDATA.ELFDATA2LSB)
55+
elf.Elf.Ehdr.e_entry = image.entrypoint
56+
57+
# TODO bss is not accounted for
58+
# 00 .flash.rodata
59+
# 01 .dram0.data .dram0.bss
60+
# 02 .iram0.vectors .iram0.text
61+
# 03 .flash.text
62+
print_verbose(verbose, "Entrypoint " + str(hex(image.entrypoint)))
63+
64+
section_ids = {}
65+
66+
# map to hold pre-defined elf section attributes
67+
sect_attr_map = {
68+
'.flash.rodata' : {'ES':0x00, 'Flg':'WA', 'Lk':0, 'Inf':0, 'Al':16},
69+
'.dram0.data' : {'ES':0x00, 'Flg':'WA', 'Lk':0, 'Inf':0, 'Al':16},
70+
'.iram0.vectors': {'ES':0x00, 'Flg':'AX', 'Lk':0, 'Inf':0, 'Al':4},
71+
'.iram0.text' : {'ES':0x00, 'Flg':'WAX', 'Lk':0, 'Inf':0, 'Al':4},
72+
'.flash.text' : {'ES':0x00, 'Flg':'AX', 'Lk':0, 'Inf':0, 'Al':4}
73+
}
74+
# TODO rtc not accounted for
75+
76+
idx = 0
77+
##### build out the section data #####
78+
######################################
79+
for seg in sorted(image.segments, key=lambda s:s.addr):
80+
idx += 1
81+
82+
# name from image
83+
segment_name = ", ".join([seg_range[2] for seg_range in image.ROM_LOADER.MEMORY_MAP if seg_range[0] <= seg.addr < seg_range[1]])
84+
85+
# TODO when processing an image, there was an empty segment name?
86+
if segment_name == '':
87+
continue
88+
89+
section_name = ''
90+
# handle special case
91+
if segment_name == 'IRAM':
92+
if len(seg.data) == 0x400:
93+
section_name = '.iram0.vectors'
94+
else:
95+
section_name = '.iram0.text'
96+
else:
97+
section_name = section_map[segment_name]
98+
99+
# if we have a mapped segment <-> section
100+
# add the elf section
101+
if section_name != '':
102+
# might need to append to section (e.g. IRAM is split up due to alignment)
103+
if section_name in section_data:
104+
section_data[section_name]['data'] += seg.data
105+
else:
106+
section_data[section_name] = {'addr':seg.addr, 'data':seg.data}
107+
108+
##### append the sections #####
109+
###############################
110+
for name in section_data.keys():
111+
data = section_data[name]['data']
112+
addr = section_data[name]['addr']
113+
# build the section out as much as possible
114+
# if we already know the attribute values
115+
if name in sect_attr_map:
116+
sect = sect_attr_map[name]
117+
flg = calcShFlg(sect['Flg'])
118+
section_ids[name] = elf._append_section(name, data, addr,SHT.SHT_PROGBITS, flg, sect['Lk'], sect['Inf'], sect['Al'], sect['ES'])
119+
else:
120+
section_ids[name] = elf.append_section(name, data, addr)
121+
122+
elf.append_special_section('.strtab')
123+
elf.append_special_section('.symtab')
124+
add_elf_symbols(elf)
125+
126+
# segment flags
127+
# TODO double check this stuff
128+
segments = {
129+
'.flash.rodata' : 'rw',
130+
'.dram0.data' : 'rw',
131+
'.iram0.vectors': 'rwx',
132+
'.flash.text' : 'rx'
133+
}
134+
135+
# there is an initial program header that we don't want...
136+
elf.Elf.Phdr_table.pop()
137+
138+
bytes(elf) # kind of a hack, but __bytes__() calculates offsets in elf object
139+
size_of_phdrs = len(Elf32_Phdr()) * len(segments) # to pre-calculate program header offsets
140+
141+
##### add the segments ####
142+
###########################
143+
print_verbose(verbose, "\nAdding program headers")
144+
for (name, flags) in segments.items():
145+
146+
if (name == '.iram0.vectors'):
147+
# combine these
148+
size = len(section_data['.iram0.vectors']['data']) + len(section_data['.iram0.text']['data'])
149+
else:
150+
size = len(section_data[name]['data'])
151+
152+
p_flags = calcPhFlg(flags)
153+
addr = section_data[name]['addr']
154+
align = 0x1000
155+
p_type = PT.PT_LOAD
156+
157+
shstrtab_hdr, shstrtab = elf.get_section_by_name(name)
158+
offset = shstrtab_hdr.sh_offset + size_of_phdrs # account for new offset
159+
160+
# build program header
161+
Phdr = Elf32_Phdr(PT.PT_LOAD, p_offset=offset, p_vaddr=addr,
162+
p_paddr=addr, p_filesz=size, p_memsz=size,
163+
p_flags=p_flags, p_align=0x1000, little=elf.little)
164+
165+
166+
print_verbose(verbose, name + ": " + str(Phdr))
167+
elf.Elf.Phdr_table.append(Phdr)
168+
169+
# write out elf file
170+
if output_file is not None:
171+
out_file = output_file
172+
else:
173+
out_file = image_name + '.elf'
174+
print("\nWriting ELF to " + out_file + "...")
175+
fd = os.open(out_file, os.O_WRONLY | os.O_CREAT | os.O_TRUNC)
176+
os.write(fd, bytes(elf))
177+
os.close(fd)
178+
179+
def add_elf_symbols(elf):
180+
181+
fh = open("symbols_dump.txt", "r")
182+
lines = fh.readlines()
183+
184+
bind_map = {"LOCAL" : STB.STB_LOCAL, "GLOBAL" : STB.STB_GLOBAL}
185+
type_map = {"NOTYPE": STT.STT_NOTYPE, "OBJECT" : STT.STT_OBJECT, "FUNC" : STT.STT_FUNC, "FILE" : STT.STT_FILE}
186+
187+
for line in lines:
188+
line = line.split()
189+
sym_binding = line[4]
190+
sym_type = line[3]
191+
sym_size = int(line[2])
192+
sym_val = int(line[1], 16)
193+
sym_name = line[7]
194+
# ABS
195+
elf.append_symbol(sym_name, 0xfff1, sym_val, sym_size, sym_binding=bind_map[sym_binding], sym_type=type_map[sym_type])
196+
197+
def flash_dump_to_elf(filename, partition):
198+
fh = open(filename, 'rb')
199+
part_table = read_partition_table(fh)
200+
fh.close()
201+
return part_table
202+
203+
def main():
204+
desc = 'ESP32 Firmware Image Parser Utility'
205+
arg_parser = argparse.ArgumentParser(description=desc)
206+
arg_parser.add_argument('action', choices=['show_partitions', 'dump_partition', 'create_elf'], help='Action to take')
207+
arg_parser.add_argument('input', help='Firmware image input file')
208+
arg_parser.add_argument('-output', help='Output file name')
209+
arg_parser.add_argument('-partition', help='Partition name (e.g. ota_0)')
210+
arg_parser.add_argument('-v', default=False, help='Verbose output', action='store_true')
211+
212+
args = arg_parser.parse_args()
213+
214+
with open(args.input, 'rb') as fh:
215+
verbose = False
216+
# read_partition_table will show the partitions if verbose
217+
if args.action == 'show_partitions' or args.v is True:
218+
verbose = True
219+
220+
# parse that ish
221+
part_table = read_partition_table(fh, verbose)
222+
223+
if args.action in ['dump_partition', 'create_elf']:
224+
part_name = args.partition
225+
226+
if args.action == 'dump_partition' and args.output is not None:
227+
dump_file = args.output
228+
else:
229+
dump_file = part_name + '_out.bin'
230+
231+
if part_name in part_table:
232+
print("Dumping partition '" + part_name + "' to " + dump_file)
233+
part = part_table[part_name]
234+
dump_bytes(fh, part['offset'], part['size'], dump_file) # dump_file will be written out
235+
236+
if args.action == 'create_elf':
237+
# can only generate elf from 'app' partition type
238+
if part['type'] != 0:
239+
print("Uh oh... bad partition type. Can't convert to ELF")
240+
else:
241+
if args.output is None:
242+
print("Need output file name")
243+
else:
244+
# we have to load from a file
245+
output_file = args.output
246+
image2elf(dump_file, output_file, verbose)
247+
else:
248+
print("Partition '" + part_name + "' not found.")
249+
250+
if __name__ == '__main__':
251+
main()

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
makeelf
2+
esptool

0 commit comments

Comments
 (0)