fix smoldd

This commit is contained in:
PoroCYon 2020-05-03 20:50:31 +02:00 committed by PoroCYon
parent 5c38b05c85
commit 7e384a44a6
5 changed files with 446 additions and 168 deletions

167
smoldd.py
View File

@ -1,167 +0,0 @@
#!/usr/bin/env python3
import argparse
import glob
import os.path
import shutil
import subprocess
import struct
import sys
from smolshared import *
def readbyte(blob, off):
return struct.unpack('<B', blob[off:off+1])[0], (off+1)
def readint(blob, off):
return struct.unpack('<I', blob[off:off+4])[0], (off+4)
def readlong(blob, off):
return struct.unpack('<Q', blob[off:off+8])[0], (off+8)
def readstr(blob, off):
text = bytearray()
while True:
char, off = readbyte(blob, off)
if char == 0:
break
text.append(char)
return text.decode('utf-8'), off
def get_def_libpaths(cc_bin, is32bit):
if is32bit:
return ['/usr/lib32/','/lib32/']
out = subprocess.check_output([cc_bin, '-print-search-dirs'],
stderr=subprocess.DEVNULL)
stuff = dict({})
for l in out.decode('utf-8').splitlines():
blah = l.split(': ')
stuff[blah[0]] = blah[1].lstrip('=').split(':')
return stuff["libraries"]
def find_libs(bits, deflibs, libname):
dirs = os.environ['LD_LIBRARY_PATH'].split(':') + deflibs
for d in dirs:
for f in glob.glob(glob.escape(d + libname) + '*'):
yield f
def build_hashtab(scanelf_bin, lib):
out = subprocess.check_output([scanelf_bin, '-B', '-F', '%s', '-s', '%pd%*', lib],
stderr=subprocess.DEVNULL)
blah = set(out.decode('utf-8').split('\n'))
ret = dict({})
for x in blah:
y = x.split()
if len(y) != 7:
continue
ret[hash_djb2(y[6])] = y[6]
return ret
def main():
parser = argparse.ArgumentParser()
parser.add_argument('input', type=argparse.FileType('rb'),
default=sys.stdin.buffer, help="input file")
parser.add_argument('--cc',
default=shutil.which('cc'), help="C compiler binary")
parser.add_argument('--scanelf',
default=shutil.which('scanelf'), help="scanelf binary")
args = parser.parse_args()
blob = args.input.read()
machnum = struct.unpack('<H', blob[18:18+2])[0]
is32bit = machnum == archmagic['i386']
deflibs = get_def_libpaths(args.cc, is32bit)
phoff, phsz, phnum = 0, 0, 0
if is32bit:
phoff = struct.unpack('<I', blob[28:28+4])[0]
phsz = struct.unpack('<H', blob[42:42+2])[0]
phnum = struct.unpack('<H', blob[44:52+2])[0]
elif machnum == archmagic['x86_64']:
phoff = struct.unpack('<Q', blob[32:32+8])[0]
phsz = struct.unpack('<H', blob[54:54+2])[0]
phnum = struct.unpack('<H', blob[56:56+2])[0]
else:
eprintf("Unknown architecture " + str(machnum))
sys.exit(1)
for i in range(phnum):
off = phoff + i * phsz
#print(hex(off))
ptyp, poff, pva, ppa, pfsz, pmsz, pfl, pal = 0,0,0,0,0,0,0,0
if is32bit:
ptyp, poff, pva, ppa, pfsz, pmsz, pfl, pal = \
struct.unpack('<ILLLIIII', blob[off:off+phsz])
else:
ptyp, pfl, poff, pva, ppa, pfsz, pmsz, pal = \
struct.unpack('<IIQQQQQQ', blob[off:off+phsz])
if ptyp != 2: # PT_DYNAMIC
continue
#print(hex(poff))
# right after the dynamic section, the smol 'symtab'/'hashtab' is found
#
# note that on i386, every lib name is followed by an E9 byte
# if the next libname/first byte of the hash is null, the table has
# come to an end.
if is32bit:
j = poff
strtaboff = 0
while j < poff + pfsz:
tag, j = readint(blob, j)
ptr, j = readint(blob, j)
if tag == 5: # DT_STRTAB
strtaboff = ptr
elif tag == 1: # DT_NEEDED
bakoff = j
smoltaboff = strtaboff + ptr - (pva - poff)
j = smoltaboff
libname, j = readstr(blob, j)
if len(libname) == 0:
break
sys.stdout.write("* " + libname)
libs = list(find_libs(32, deflibs, libname))
print(" -> NOT FOUND" if len(libs) == 0 else (" -> " + libs[0]))
ht = dict({}) if len(libs) == 0 else build_hashtab(args.scanelf, libs[0])
while True:
hashv, j = readint(blob, j)
if (hashv & 0xFF) == 0:
break
sys.stdout.write(" * " + hex(hashv))
print(" -> NOT FOUND" if hashv not in ht else (" -> " + ht[hashv]))
j = bakoff
break
else: # 64-bit
eprintf("Currently unsuppored, sorry.")
sys.exit(1)
if __name__ == '__main__':
main()

153
src/hackyelf.py Normal file
View File

@ -0,0 +1,153 @@
# stolen from the contrib folder in https://github.com/blackle/LZMA-Vizualizer
# (i.e. I'm stealing it from myself)
# custom elf parser because a standard one wouldn't be trustable because the
# ELFs we're parsing will be a bit wonky anyway
from struct import unpack
from typing import *
ELFCLASS32 = 1
ELFCLASS64 = 2
EM_386 = 3
EM_X86_64 = 62
PT_NULL = 0
PT_LOAD = 1
PT_DYNAMIC = 2
PT_INTERP = 3
DT_NULL = 0
DT_NEEDED = 1
DT_STRTAB = 5
DT_SYMTAB = 6
class Phdr(NamedTuple):
ptype: int
off : int
vaddr: int
paddr: int
filesz: int
memsz: int
flags: int
align: int
class Dyn(NamedTuple):
tag: int
val: int
class ELF(NamedTuple):
data : bytes
ident : bytes
eclass: int
mach : int
entry : int
phdrs : Sequence[Phdr]
dyn : Sequence[Dyn]
is32bit: bool
# yeah, there's some code duplication here
# idgaf
def parse_phdr32(data: bytes, phoff:int, phentsz:int, phnum:int) -> Sequence[Phdr]:
ps = []
for off in range(phoff, phoff+phentsz*phnum, phentsz):
ptype, off, vaddr, paddr, filesz, memsz, flags, align = \
unpack('<IIIIIIII', data[off:off+8*4])
p = Phdr(ptype, off, vaddr, paddr, filesz, memsz, flags, align)
ps.append(p)
return ps
def parse_dyn32(data: bytes, dynp: Phdr) -> Dyn:
ds = []
off = dynp.off
while True:
tag, val = unpack('<II', data[off:off+2*4])
ds.append(Dyn(tag, val))
if tag == DT_NULL: break
off = off + 2*4
return ds
def parse_32(data: bytes) -> ELF:
ident = data[:16]
eclass = data[4]
mach = unpack('<H', data[18:18+2])[0]
entry = unpack('<I', data[24:24+4])[0]
phoff = unpack('<I', data[28:28+4])[0]
phentsz = unpack('<H', data[42:42+2])[0]
phnum = unpack('<H', data[44:44+2])[0]
phdrs = parse_phdr32(data, phoff, phentsz, phnum)
dyn = None
for p in phdrs:
if p.ptype == PT_DYNAMIC:
dyn = parse_dyn32(data, p)
break
return ELF(data, ident, eclass, mach, entry, phdrs, dyn, True)
def parse_phdr64(data: bytes, phoff:int, phentsz:int, phnum:int) -> Sequence[Phdr]:
ps = []
for off in range(phoff, phoff+phentsz*phnum, phentsz):
# TODO
ptype, flags, off, vaddr, paddr, filesz, memsz, align = \
unpack('<IIQQQQQQ', data[off:off+2*4+6*8])
p = Phdr(ptype, off, vaddr, paddr, filesz, memsz, flags, align)
ps.append(p)
return ps
def parse_dyn64(data: bytes, dynp: Phdr) -> Dyn:
ds = []
off = dynp.off
while True:
tag, val = unpack('<QQ', data[off:off+2*8])
ds.append(Dyn(tag, val))
if tag == DT_NULL: break
off = off + 2*8
return ds
def parse_64(data: bytes) -> ELF:
ident = data[:16]
eclass = data[4]
mach = unpack('<H', data[18:18+2])[0]
entry = unpack('<Q', data[24:24+8])[0]
phoff = unpack('<Q', data[32:32+8])[0]
phentsz = unpack('<H', data[54:54+2])[0]
phnum = unpack('<H', data[56:56+2])[0]
phdrs = parse_phdr64(data, phoff, phentsz, phnum)
dyn = None
for p in phdrs:
if p.ptype == PT_DYNAMIC:
dyn = parse_dyn64(data, p)
break
return ELF(data, ident, eclass, mach, entry, phdrs, dyn, False)
def parse(data: bytes) -> ELF:
assert data[:4] == b'\x7FELF', "Not a valid ELF file" # good enough
ecls = data[4]
if ecls == ELFCLASS32: return parse_32(data)
elif ecls == ELFCLASS64: return parse_64(data)
else:
emch = unpack('<H', data[18:18+2])[0]
if emch == EM_386: return parse_32(data)
elif emch == EM_X86_64: return parse_64(data)
assert False, "bad E_CLASS %d" % ecls

117
src/linkmap.py Normal file
View File

@ -0,0 +1,117 @@
# stolen from the contrib folder in https://github.com/blackle/LZMA-Vizualizer
# (i.e. I'm stealing it from myself)
import re
from typing import *
class CommonSym(NamedTuple):
name: str
size: int
file: str
class Discard(NamedTuple):
name: str
size: int
file: str
class MemCfg(NamedTuple):
name: str
org : int
size: int
class MMap(NamedTuple):
sect: str
org : int
#size: int
sym : str
file: str
class XRef(NamedTuple):
name: str
deff: str
reff: Sequence[str]
class LinkMap(NamedTuple):
common : Sequence[CommonSym]
discard: Sequence[Discard]
memcfg : Sequence[MemCfg]
mmap : Sequence[MMap]
xref : Sequence[XRef]
def parse_common( ls: Sequence[str]) -> Sequence[CommonSym]: return [] # TODO
def parse_discard(ls: Sequence[str]) -> Sequence[Discard ]: return [] # TODO
def parse_memcfg( ls: Sequence[str]) -> Sequence[MemCfg ]: return [] # TODO
def parse_xref( ls: Sequence[str]) -> Sequence[XRef ]: return [] # TODO
def parse_mmap(ls: Sequence[str]) -> Sequence[MMap]:
rrr = []
bigsect = None
section = None
curfile = None
#size = -1
for l in ls:
def matcher(mobj):
return mobj.group(0).replace(' ', '_')
l = re.sub(r"\*\(.*\)", matcher, l)
#print(repr(l))
s = l.strip(); w = s.split()
if s.startswith('LOAD ') or s.startswith('OUTPUT('): continue#break
if l[0] != ' ':
bigsect = w[0]
del w[0]
elif l[1] != ' ':
section = w[0]
del w[0]
if len(w) == 0 or w[0] == "[!provide]":
continue # addr placed on next line for prettyprinting reasons
#print(repr(l), w[0])
assert w[0].startswith("0x"), "welp, bad symbol addr"
addr = int(w[0], 16)
size = -1
symn = ""
if w[1].startswith("0x"): # filename will prolly follow
size = int(w[1], 16)
curfile = w[2] if len(w) > 2 else ""
else: symn = w[1]
if len(symn) > 0:
rrr.append(MMap(section, addr, symn, curfile))
return rrr
def parse(s: str) -> LinkMap:
COMMON = 0
DISCARD = 1
MEMCFG = 2
MMAP = 3
XREF = 4
curpt = -1
commonl, discardl, memcfgl, mmapl, xrefl = [], [], [], [], []
for l in s.split('\n'):
if len(l.strip()) == 0: continue
ls = l.strip()
if ls == "Allocating common symbols": curpt = COMMON
elif ls == "Discarded input sections": curpt = DISCARD
elif ls == "Memory Configuration": curpt = MEMCFG
elif ls == "Linker script and memory map": curpt = MMAP
elif ls == 'Cross Reference Table': curpt = XREF
elif curpt == COMMON : commonl.append(l)
elif curpt == DISCARD: discardl.append(l)
elif curpt == MEMCFG : memcfgl.append(l)
elif curpt == MMAP : mmapl.append(l)
elif curpt == XREF : xrefl.append(l)
else:
assert False, "bad line %s" % ls
return LinkMap(parse_common(commonl), parse_discard(discardl), \
parse_memcfg(memcfgl), parse_mmap(mmapl), parse_xref(xrefl))

175
src/smoldd.py Executable file
View File

@ -0,0 +1,175 @@
#!/usr/bin/env python3
import os.path, struct, sys
import argparse, glob, shutil, subprocess
import hackyelf, linkmap
from smolshared import *
from smolparse import *
def readbyte(blob, off): return struct.unpack('<B', blob[off:off+1])[0], (off+1)
def readint(blob, off): return struct.unpack('<I', blob[off:off+4])[0], (off+4)
def readlong(blob, off): return struct.unpack('<Q', blob[off:off+8])[0], (off+8)
def readstr(blob, off):
text = bytearray()
while True:
char, off = readbyte(blob, off)
if char == 0: break
text.append(char)
return text.decode('utf-8'), off
def get_def_libpaths(cc_bin, is32bit):
# FIXME: HACK
if is32bit: return ['/usr/lib32/','/lib32/']
return get_cc_paths(cc_bin)['libraries']
def find_libs(deflibs, libname):
dirs = os.environ['LD_LIBRARY_PATH'].split(':') + deflibs
for d in dirs:
for f in glob.glob(glob.escape(d + '/' + libname) + '*'): yield f
def build_hashtab(scanelf_bin, lib):
out = subprocess.check_output([scanelf_bin, '-B', '-F', '%s', '-s', '%pd%*', lib],
stderr=subprocess.DEVNULL)
blah = set(out.decode('utf-8').split('\n'))
ret = dict({})
for x in blah:
y = x.split()
if len(y) != 7: continue
ret[hash_djb2(y[6])] = y[6]
return ret
def addr2off(elf, addr):
for x in elf.phdrs:
if x.ptype != hackyelf.PT_LOAD: continue
if addr >= x.vaddr and addr < x.vaddr + x.memsz:
aoff = addr - x.vaddr
assert aoff < x.filesz, ".bss address!"
return aoff + x.off
assert False, "Address %08x not in the static address range!" % addr
def get_needed_libs(elf, blob):
assert elf.dyn is not None, "No DYNAMIC table present in the ELF file!"
strtabs = [x.val for x in elf.dyn if x.tag == hackyelf.DT_STRTAB]
assert len(strtabs) == 1, "Only one DT_STRTAB may be present in an ELF file."
strtab = strtabs[0]
return [readstr(blob,addr2off(elf, strtab+x.val))[0]
for x in elf.dyn if x.tag == hackyelf.DT_NEEDED]
def get_hashtbl(elf, blob, args):
htaddr = None
if args.map is not None:
lmap = linkmap.parse(args.map.read())
tabs = [x for x in lmap.mmap if x.sym == '_symbols']
assert len(tabs) == 1, "One '_symbols' symbol must be present."
htaddr = tabs[0].org
elif elf.is32bit:
txtoff = addr2off(elf, elf.entry)
# scan for 'push IMM32'
while blob[txtoff] != 0x68:
txtoff = txtoff + 1
assert txtoff < len(blob), "wtf??? (can't find a push IMM32 instruction which pushes the hashtable address)"
txtoff = txtoff + 1
htaddr = struct.unpack('<I', blob[txtoff:txtoff+4])[0]
else: # 64-bit
txtoff = addr2off(elf, elf.entry)
# scan for 'push IMM32'
# but the first one we'll encounter pushes the entrypoint addr!
while blob[txtoff] != 0x68:
txtoff = txtoff + 1
assert txtoff < len(blob), "wtf??? (can't find a push IMM32 instruction which pushes the entrypoint address)"
txtoff = txtoff + 1
# now we can look for the interesting address
while blob[txtoff] != 0x68:
txtoff = txtoff + 1
assert txtoff < len(blob), "wtf??? (can't find a push IMM32 instruction which pushes the hashtable address)"
txtoff = txtoff + 1
htaddr = struct.unpack('<I', blob[txtoff:txtoff+4])[0]
assert htaddr is not None, "wtf? (no hashtable address)"
print("Hash table address: 0x%08x" % htaddr)
htoff = addr2off(elf, htaddr)
tbl = []
while True:
if len(blob)-htoff < 4:
if struct.unpack('<B', blob[htoff:htoff+1])[0] == 0:
break
val = struct.unpack('<I', blob[htoff:htoff+4])[0]
if (val & 0xFF) == 0: break
tbl.append(val)
htoff = htoff + (4 if elf.is32bit else 8)
return tbl
def main():
parser = argparse.ArgumentParser()
parser.add_argument('input', type=argparse.FileType('rb'),
default=sys.stdin.buffer, help="input file")
parser.add_argument('--cc',
default=shutil.which('cc'), help="C compiler binary")
parser.add_argument('--scanelf',
default=shutil.which('scanelf'), help="scanelf binary")
parser.add_argument('--map', type=argparse.FileType('r'), help=\
"Get the address of the symbol hash table from the "+\
"linker map output instead of attempting to parse the"+\
" binary.")
args = parser.parse_args()
blob = args.input.read()
elf = hackyelf.parse(blob)
deflibs = get_def_libpaths(args.cc, elf.is32bit)
needed = get_needed_libs(elf, blob)
neededpaths = dict((l,list(find_libs(deflibs, l))[0]) for l in needed)
htbl = get_hashtbl(elf, blob, args)
libhashes = dict((l, build_hashtab(args.scanelf, neededpaths[l])) for l in needed)
hashresolves = dict({})
noresolves = []
# TODO: group by libs
for x in htbl:
done = False
for l in libhashes.keys():
v = libhashes[l]
if x in v:
if l not in hashresolves: hashresolves[l] = dict({})
hashresolves[l][x] = v[x]
done = True
break
if not done: noresolves.append(x)
for l in hashresolves.keys():
print("%s:" % l)
v = hashresolves[l]
for x in v.keys():
print("\t%08x -> %s" % (x, v[x]))
if len(noresolves) > 0:
print("UNRESOLVED:")
for x in noresolves: print("\t%08x" % x)
return 0
if __name__ == '__main__':
rv = main()
if rv is None: pass
else:
try: sys.exit(int(rv))
except: sys.exit(1)

View File

@ -105,7 +105,7 @@ def get_cc_paths(cc_bin):
return paths return paths
def is_valid_elf(f): def is_valid_elf(f): # Good Enough(tm)
with open(f, 'rb') as ff: return ff.read(4) == b'\x7FELF' with open(f, 'rb') as ff: return ff.read(4) == b'\x7FELF'
def find_lib(spaths, wanted): def find_lib(spaths, wanted):