From dbd6f9c2583b9d37a81c540ba958a8ef4324fa12 Mon Sep 17 00:00:00 2001 From: Mike Frysinger Date: Sun, 9 Jun 2013 18:42:40 -0400 Subject: [PATCH] support parsing of dynamic ELFs w/out section headers MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit At runtime, ELFs do not use the section headers at all. Instead, only the program segments and dynamic tags get used. This means you can strip the section table completely from an ELF and have it still work. In practice, people rarely do this, but it's not unheard of. Make the Dynamic tags work even in these cases by loading the strings table the same way the runtime loader does: * parse the symtab address from DT_STRTAB * locate the file offset via the program segments In order to avoid circular deps (parsing a dyntag requires walking parsed dyntags), add a set of internal funcs for returning the raw values. You can see this in action: $ eu-strip -g --strip-sections a.out $ readelf -S a.out $ lddtree.py ./a.out a.out (interpreter => /lib64/ld-linux-x86-64.so.2) libïäöëß.so => None libc.so.6 => /lib64/libc.so.6 --- elftools/elf/dynamic.py | 76 +++++++++++++++--- test/test_dynamic.py | 28 ++++++- .../aarch64_super_stripped.elf | Bin 0 -> 4136 bytes 3 files changed, 93 insertions(+), 11 deletions(-) create mode 100755 test/testfiles_for_unittests/aarch64_super_stripped.elf diff --git a/elftools/elf/dynamic.py b/elftools/elf/dynamic.py index d9db870..9f985c2 100644 --- a/elftools/elf/dynamic.py +++ b/elftools/elf/dynamic.py @@ -11,7 +11,22 @@ import itertools from .sections import Section from .segments import Segment from ..common.exceptions import ELFError -from ..common.utils import struct_parse +from ..common.utils import struct_parse, parse_cstring_from_stream + + +class _DynamicStringTable(object): + """ Bare string table based on values found via ELF dynamic tags and + loadable segments only. Good enough for get_string() only. + """ + def __init__(self, stream, table_offset): + self._stream = stream + self._table_offset = table_offset + + def get_string(self, offset): + """ Get the string stored at the given offset in this string table. + """ + return parse_cstring_from_stream(self._stream, + self._table_offset + offset) class DynamicTag(object): @@ -61,27 +76,68 @@ class Dynamic(object): self._num_tags = -1 self._offset = position self._tagsize = self._elfstructs.Elf_Dyn.sizeof() + + # Do not access this directly yourself; use _get_stringtable() instead. self._stringtable = stringtable - def iter_tags(self, type=None): - """ Yield all tags (limit to |type| if specified) + def _get_stringtable(self): + """ Return a string table for looking up dynamic tag related strings. + + This won't be a "full" string table object, but will at least + support the get_string() function. + """ + if self._stringtable: + return self._stringtable + + # If the ELF has stripped its section table (which is unusual, but + # perfectly valid), we need to use the dynamic tags to locate the + # dynamic string table. + strtab = None + for tag in self._iter_tags(type='DT_STRTAB'): + strtab = tag['d_val'] + break + # If we found a dynamic string table, locate the offset in the file + # by using the program headers. + if strtab: + table_offset = next(self._elffile.address_offsets(strtab), None) + if table_offset is not None: + self._stringtable = _DynamicStringTable(self._stream, table_offset) + return self._stringtable + + # That didn't work for some reason. Let's use the section header + # even though this ELF is super weird. + self._stringtable = self._elffile.get_section_by_name(b'.dynstr') + return self._stringtable + + def _iter_tags(self, type=None): + """ Yield all raw tags (limit to |type| if specified) """ for n in itertools.count(): - tag = self.get_tag(n) - if type is None or tag.entry.d_tag == type: + tag = self._get_tag(n) + if type is None or tag['d_tag'] == type: yield tag - if tag.entry.d_tag == 'DT_NULL': + if tag['d_tag'] == 'DT_NULL': break - def get_tag(self, n): - """ Get the tag at index #n from the file (DynamicTag object) + def iter_tags(self, type=None): + """ Yield all tags (limit to |type| if specified) + """ + for tag in self._iter_tags(type=type): + yield DynamicTag(tag, self._get_stringtable()) + + def _get_tag(self, n): + """ Get the raw tag at index #n from the file """ offset = self._offset + n * self._tagsize - entry = struct_parse( + return struct_parse( self._elfstructs.Elf_Dyn, self._stream, stream_pos=offset) - return DynamicTag(entry, self._stringtable) + + def get_tag(self, n): + """ Get the tag at index #n from the file (DynamicTag object) + """ + return DynamicTag(self._get_tag(n), self._get_stringtable()) def num_tags(self): """ Number of dynamic tags in the file diff --git a/test/test_dynamic.py b/test/test_dynamic.py index 0ee9b35..f25feba 100644 --- a/test/test_dynamic.py +++ b/test/test_dynamic.py @@ -10,16 +10,42 @@ except ImportError: import unittest import os -from utils import setup_syspath; setup_syspath() +from utils import setup_syspath +setup_syspath() +from elftools.elf.elffile import ELFFile from elftools.common.exceptions import ELFError from elftools.elf.dynamic import DynamicTag class TestDynamicTag(unittest.TestCase): + """Tests for the DynamicTag class.""" + def test_requires_stringtable(self): with self.assertRaises(ELFError): dt = DynamicTag('', None) +class TestDynamic(unittest.TestCase): + """Tests for the Dynamic class.""" + + def test_missing_sections(self): + """Verify we can get dynamic strings w/out section headers""" + + libs = [] + with open(os.path.join('test', 'testfiles_for_unittests', + 'aarch64_super_stripped.elf'), 'rb') as f: + elf = ELFFile(f) + for segment in elf.iter_segments(): + if segment.header.p_type != 'PT_DYNAMIC': + continue + + for t in segment.iter_tags(): + if t.entry.d_tag == 'DT_NEEDED': + libs.append(t.needed.decode('utf-8')) + + exp = ['libc.so.6'] + self.assertEqual(libs, exp) + + if __name__ == '__main__': unittest.main() diff --git a/test/testfiles_for_unittests/aarch64_super_stripped.elf b/test/testfiles_for_unittests/aarch64_super_stripped.elf new file mode 100755 index 0000000000000000000000000000000000000000..0e5c2c4bc37b0e2376473c658510096b764ef454 GIT binary patch literal 4136 zcmeHKO-NKx6h7}gQ`3IN(zLRgsZB^~hWWDy?Hh~<#u>zsSf!BL26(I)1?ec%1g|2_9D?!A5X;yK&0 zK!y#EL9+~{MdmA7g9>UOn?;$3+i)u8idTYD99mHP9JJ3MADbO*WfqJF4Ff@`kdX3i z48==>tE?0Rvm5=TTH`y7d3>i|F9d724kh)lA5rql-evr@$&O!eGq|Wm0t}Er;9i`n}JvVNa zhC+SqomG`({k>%srhX5`q8x!smvvM+w=BIfA2~-Vi|mO1&B#OJJve6_waM)mGVdmd zA-M=-AKzjRxQ}+Ky9z>Adiw?-7$ixse<0L15WF7h>Osl1>%BcJ2}0oFh5Clzsj`Y{ zicIyJJy?1lv~L=tk$ZUjHk>jP=6Cz`%ECk9qc9qek=rP@*p?tWMV@N!Ns*`aEE#?l zO*#-c0WgvbPd}V;f^#Y1AddPWfT6tgK*F<77xCIh=8kxsw=z*Tk!Py}{vTSWc_#9s zDLe;xz2S#o@bzJwlylL6*n3AM7LGyW6Y?>DmKQib;{4*eqf0p3wkcgs2je@&L97Yl zfdpzNKe%q8)O`o*LEVIJ3?j5QK=c*k8sqx_X6kBSsS}`C_KNl`4_tTB3w3ipFpf@! zABsV-v_Hi_UyA2jYZ%tC*8F#UYyRO7z$1w@jSPp>{b}C6eLi`V8b;&}PVjKmwp9g7r*JV-7ze)k}Hju;$b})Q4pF ziOJPuMZdQ@(GiSsA7{~v>d43fPP=GkWW8Z{rPYppccA2(W5re+JGK|kVhqobPL^8EVrmEoOo{m=8I|j-O31&;I*aFYhOLr^&nG z0P9!zomI?S^mt9yL)l6rKil9Hb9!^p=(H69sZKjG`H+e;!3+q=vywu>{*WIc=j8OZzecL^rAo~^jm F?