Skip to content

Commit a3f1b7c

Browse files
authored
Add resilience for degenerate cases present in files with only debug information (#287)
Some ELF files which contain only debug symbols have important sections present in the section table but marked as NOBITS instead of PROGBITS. Attempting to extract the segments can lead to crashes through parsing invalid data. The first patch modifies the dynamic segment/section specifically to add a flag for this case, since it seems to assume that there will always be at least one entry, DT_NULL. The second patch modifies the segment code more generally to return a dummy answer for what data it holds. The actual way that this change prevents a crash is while trying to parse .eh_frame when it is in fact NOBITS - originally I had a more targeted patch, but decided that it was important enough to do more generally
1 parent 85b30d2 commit a3f1b7c

File tree

5 files changed

+109
-22
lines changed

5 files changed

+109
-22
lines changed

elftools/elf/dynamic.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,22 @@ def __str__(self):
7373
class Dynamic(object):
7474
""" Shared functionality between dynamic sections and segments.
7575
"""
76-
def __init__(self, stream, elffile, stringtable, position):
76+
def __init__(self, stream, elffile, stringtable, position, empty):
77+
"""
78+
:param stream: The file-like object from which to load data
79+
:param elffile: The parent elffile object
80+
:param stringtable: A stringtable reference to use for parsing string references in entries
81+
:param position: The file offset of the dynamic segment/section
82+
:param empty: Whether this is a degenerate case with zero entries. Normally, every dynamic table
83+
will have at least one entry, the DT_NULL terminator.
84+
"""
7785
self.elffile = elffile
7886
self.elfstructs = elffile.structs
7987
self._stream = stream
80-
self._num_tags = -1
88+
self._num_tags = -1 if not empty else 0
8189
self._offset = position
8290
self._tagsize = self.elfstructs.Elf_Dyn.sizeof()
91+
self._empty = empty
8392

8493
# Do not access this directly yourself; use _get_stringtable() instead.
8594
self._stringtable = stringtable
@@ -125,6 +134,8 @@ def _get_stringtable(self):
125134
def _iter_tags(self, type=None):
126135
""" Yield all raw tags (limit to |type| if specified)
127136
"""
137+
if self._empty:
138+
return
128139
for n in itertools.count():
129140
tag = self._get_tag(n)
130141
if type is None or tag['d_tag'] == type:
@@ -141,6 +152,8 @@ def iter_tags(self, type=None):
141152
def _get_tag(self, n):
142153
""" Get the raw tag at index #n from the file
143154
"""
155+
if self._num_tags != -1 and n >= self._num_tags:
156+
raise IndexError(n)
144157
offset = self._offset + n * self._tagsize
145158
return struct_parse(
146159
self.elfstructs.Elf_Dyn,
@@ -153,7 +166,7 @@ def get_tag(self, n):
153166
return DynamicTag(self._get_tag(n), self._get_stringtable())
154167

155168
def num_tags(self):
156-
""" Number of dynamic tags in the file
169+
""" Number of dynamic tags in the file, including the DT_NULL tag
157170
"""
158171
if self._num_tags != -1:
159172
return self._num_tags
@@ -207,7 +220,7 @@ def __init__(self, header, name, elffile):
207220
Section.__init__(self, header, name, elffile)
208221
stringtable = elffile.get_section(header['sh_link'])
209222
Dynamic.__init__(self, self.stream, self.elffile, stringtable,
210-
self['sh_offset'])
223+
self['sh_offset'], self['sh_type'] == 'SHT_NOBITS')
211224

212225

213226
class DynamicSegment(Segment, Dynamic):
@@ -227,7 +240,8 @@ def __init__(self, header, stream, elffile):
227240
stringtable = elffile.get_section(section['sh_link'])
228241
break
229242
Segment.__init__(self, header, stream)
230-
Dynamic.__init__(self, stream, elffile, stringtable, self['p_offset'])
243+
Dynamic.__init__(self, stream, elffile, stringtable, self['p_offset'],
244+
self['p_filesz'] == 0)
231245
self._symbol_list = None
232246
self._symbol_name_map = None
233247

elftools/elf/sections.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ def data(self):
7474
Note that data is decompressed if the stored section data is
7575
compressed.
7676
"""
77+
# If this section is NOBITS, there is no data. provide a dummy answer
78+
if self.header['sh_type'] == 'SHT_NOBITS':
79+
return b'\0'*self.data_size
80+
7781
# If this section is compressed, deflate it
7882
if self.compressed:
7983
c_type = self._compression_type
@@ -137,7 +141,7 @@ def get_string(self, offset):
137141
"""
138142
table_offset = self['sh_offset']
139143
s = parse_cstring_from_stream(self.stream, table_offset + offset)
140-
return s.decode('utf-8') if s else ''
144+
return s.decode('utf-8', errors='replace') if s else ''
141145

142146

143147
class SymbolTableSection(Section):
@@ -267,7 +271,7 @@ def iter_stabs(self):
267271
while offset < end:
268272
stabs = struct_parse(
269273
self.structs.Elf_Stabs,
270-
self.elffile.stream,
274+
self.stream,
271275
stream_pos=offset)
272276
stabs['n_offset'] = offset
273277
offset += self.structs.Elf_Stabs.sizeof()

elftools/elf/structs.py

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,17 @@ def __init__(self, little_endian=True, elfclass=32):
4343
assert elfclass == 32 or elfclass == 64
4444
self.little_endian = little_endian
4545
self.elfclass = elfclass
46+
self.e_type = None
47+
self.e_machine = None
48+
self.e_ident_osabi = None
49+
50+
def __getstate__(self):
51+
return self.little_endian, self.elfclass, self.e_type, self.e_machine, self.e_ident_osabi
52+
53+
def __setstate__(self, state):
54+
self.little_endian, self.elfclass, e_type, e_machine, e_osabi = state
55+
self.create_basic_structs()
56+
self.create_advanced_structs(e_type, e_machine, e_osabi)
4657

4758
def create_basic_structs(self):
4859
""" Create word-size related structs and ehdr struct needed for
@@ -76,12 +87,16 @@ def create_advanced_structs(self, e_type=None, e_machine=None, e_ident_osabi=Non
7687
""" Create all ELF structs except the ehdr. They may possibly depend
7788
on provided e_type and/or e_machine parsed from ehdr.
7889
"""
79-
self._create_phdr(e_machine)
80-
self._create_shdr(e_machine)
90+
self.e_type = e_type
91+
self.e_machine = e_machine
92+
self.e_ident_osabi = e_ident_osabi
93+
94+
self._create_phdr()
95+
self._create_shdr()
8196
self._create_chdr()
8297
self._create_sym()
8398
self._create_rel()
84-
self._create_dyn(e_machine, e_ident_osabi)
99+
self._create_dyn()
85100
self._create_sunw_syminfo()
86101
self._create_gnu_verneed()
87102
self._create_gnu_verdef()
@@ -127,13 +142,13 @@ def _create_leb128(self):
127142
def _create_ntbs(self):
128143
self.Elf_ntbs = CString
129144

130-
def _create_phdr(self, e_machine=None):
145+
def _create_phdr(self):
131146
p_type_dict = ENUM_P_TYPE_BASE
132-
if e_machine == 'EM_ARM':
147+
if self.e_machine == 'EM_ARM':
133148
p_type_dict = ENUM_P_TYPE_ARM
134-
elif e_machine == 'EM_AARCH64':
149+
elif self.e_machine == 'EM_AARCH64':
135150
p_type_dict = ENUM_P_TYPE_AARCH64
136-
elif e_machine == 'EM_MIPS':
151+
elif self.e_machine == 'EM_MIPS':
137152
p_type_dict = ENUM_P_TYPE_MIPS
138153

139154
if self.elfclass == 32:
@@ -159,17 +174,17 @@ def _create_phdr(self, e_machine=None):
159174
self.Elf_xword('p_align'),
160175
)
161176

162-
def _create_shdr(self, e_machine=None):
177+
def _create_shdr(self):
163178
"""Section header parsing.
164179
165180
Depends on e_machine because of machine-specific values in sh_type.
166181
"""
167182
sh_type_dict = ENUM_SH_TYPE_BASE
168-
if e_machine == 'EM_ARM':
183+
if self.e_machine == 'EM_ARM':
169184
sh_type_dict = ENUM_SH_TYPE_ARM
170-
elif e_machine == 'EM_X86_64':
185+
elif self.e_machine == 'EM_X86_64':
171186
sh_type_dict = ENUM_SH_TYPE_AMD64
172-
elif e_machine == 'EM_MIPS':
187+
elif self.e_machine == 'EM_MIPS':
173188
sh_type_dict = ENUM_SH_TYPE_MIPS
174189

175190
self.Elf_Shdr = Struct('Elf_Shdr',
@@ -227,11 +242,11 @@ def _create_rel(self):
227242
self.Elf_sxword('r_addend'),
228243
)
229244

230-
def _create_dyn(self, e_machine=None, e_ident_osabi=None):
245+
def _create_dyn(self):
231246
d_tag_dict = dict(ENUM_D_TAG_COMMON)
232-
if e_machine in ENUMMAP_EXTRA_D_TAG_MACHINE:
233-
d_tag_dict.update(ENUMMAP_EXTRA_D_TAG_MACHINE[e_machine])
234-
elif e_ident_osabi == 'ELFOSABI_SOLARIS':
247+
if self.e_machine in ENUMMAP_EXTRA_D_TAG_MACHINE:
248+
d_tag_dict.update(ENUMMAP_EXTRA_D_TAG_MACHINE[self.e_machine])
249+
elif self.e_ident_osabi == 'ELFOSABI_SOLARIS':
235250
d_tag_dict.update(ENUM_D_TAG_SOLARIS)
236251

237252
self.Elf_Dyn = Struct('Elf_Dyn',

test/test_dbgfile.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""
2+
Test that elftools does not fail to load debug symbol ELF files
3+
"""
4+
import unittest
5+
import os
6+
7+
from elftools.elf.elffile import ELFFile, DynamicSection
8+
from elftools.dwarf.callframe import ZERO
9+
10+
class TestDBGFile(unittest.TestCase):
11+
def test_dynamic_segment(self):
12+
"""
13+
Test that the degenerate case for the dynamic segment does not crash
14+
"""
15+
with open(os.path.join('test', 'testfiles_for_unittests',
16+
'debug_info.elf'), 'rb') as f:
17+
elf = ELFFile(f)
18+
19+
seen_dynamic_segment = False
20+
for segment in elf.iter_segments():
21+
if segment.header.p_type != 'PT_DYNAMIC':
22+
continue
23+
24+
self.assertEqual(segment.num_tags(), 0, "The dynamic segment in this file should be empty")
25+
seen_dynamic_segment = True
26+
break
27+
28+
self.assertTrue(seen_dynamic_segment, "There should be a dynamic segment in this file")
29+
30+
def test_dynamic_section(self):
31+
"""
32+
Test that the degenerate case for the dynamic section does not crash
33+
"""
34+
with open(os.path.join('test', 'testfiles_for_unittests',
35+
'debug_info.elf'), 'rb') as f:
36+
elf = ELFFile(f)
37+
section = DynamicSection(elf.get_section_by_name('.dynamic').header, '.dynamic', elf)
38+
39+
self.assertEqual(section.num_tags(), 0, "The dynamic section in this file should be empty")
40+
41+
def test_eh_frame(self):
42+
"""
43+
Test that parsing .eh_frame with SHT_NOBITS does not crash
44+
"""
45+
with open(os.path.join('test', 'testfiles_for_unittests',
46+
'debug_info.elf'), 'rb') as f:
47+
elf = ELFFile(f)
48+
dwarf = elf.get_dwarf_info()
49+
eh_frame = list(dwarf.EH_CFI_entries())
50+
self.assertEqual(len(eh_frame), 1, "There should only be the ZERO entry in eh_frame")
51+
self.assertIs(type(eh_frame[0]), ZERO, "The only eh_frame entry should be the terminator")
52+
53+
if __name__ == '__main__':
54+
unittest.main()
29.1 KB
Binary file not shown.

0 commit comments

Comments
 (0)