135 lines
8.3 KiB
Python
Executable File
135 lines
8.3 KiB
Python
Executable File
"""Inspect real Android ELF/APK outputs; a compiler exit code alone is insufficient."""
|
|
from __future__ import annotations
|
|
import hashlib, json, struct, zipfile
|
|
from pathlib import Path
|
|
from common import CPUS, ProbeError, sha256
|
|
|
|
ANDROID_SYSTEM = {'libc.so','libm.so','libdl.so','liblog.so','libandroid.so','libEGL.so',
|
|
'libGLESv1_CM.so','libGLESv2.so','libGLESv3.so','libOpenSLES.so','libz.so',
|
|
'libvulkan.so','libaaudio.so','libjnigraphics.so','libmediandk.so','libcamera2ndk.so',
|
|
'libnativewindow.so','libstdc++.so'}
|
|
|
|
def inspect_elf(data:bytes,cpu:str)->dict:
|
|
abi,machine,elfclass,_=CPUS[cpu]
|
|
if len(data)<64 or data[:4]!=b'\x7fELF': raise ProbeError('Not an ELF library')
|
|
if data[4]!=elfclass or data[5]!=1: raise ProbeError('Wrong ELF class or byte order')
|
|
kind,actual=struct.unpack_from('<HH',data,16)
|
|
if kind!=3 or actual!=machine: raise ProbeError(f'Wrong ELF type/machine: {kind}/{actual}, expected DYN/{machine}')
|
|
if elfclass==2:
|
|
phoff=struct.unpack_from('<Q',data,32)[0];phsize,phnum=struct.unpack_from('<HH',data,54);minsize=56
|
|
else:
|
|
phoff=struct.unpack_from('<I',data,28)[0];phsize,phnum=struct.unpack_from('<HH',data,42);minsize=32
|
|
if phsize<minsize or not 0<phnum<4096 or phoff+phsize*phnum>len(data):
|
|
raise ProbeError('Invalid ELF program header table')
|
|
loads=[];dynamic=None
|
|
for i in range(phnum):
|
|
pos=phoff+i*phsize
|
|
if elfclass==2:
|
|
typ,flags,offset,vaddr,_,filesz,memsz,align=struct.unpack_from('<IIQQQQQQ',data,pos)
|
|
else:
|
|
typ,offset,vaddr,_,filesz,memsz,flags,align=struct.unpack_from('<IIIIIIII',data,pos)
|
|
if offset+filesz>len(data):raise ProbeError('ELF segment outside file')
|
|
if typ==1:loads.append({'offset':offset,'vaddr':vaddr,'size':filesz,'alignment':align})
|
|
if typ==2:dynamic=(offset,filesz)
|
|
if not loads or dynamic is None:raise ProbeError('Missing ELF load/dynamic segments')
|
|
tags=[];fmt='<qQ' if elfclass==2 else '<iI';step=struct.calcsize(fmt)
|
|
for pos in range(dynamic[0],dynamic[0]+dynamic[1]-step+1,step):
|
|
tag,val=struct.unpack_from(fmt,data,pos)
|
|
if tag==0:break
|
|
tags.append((tag,val))
|
|
if any(tag==22 or (tag==30 and val&4) for tag,val in tags):raise ProbeError('DT_TEXTREL not supported for this Android probe')
|
|
needed_offsets=[v for k,v in tags if k==1];needed=[]
|
|
if needed_offsets:
|
|
address=next((v for k,v in tags if k==5),None)
|
|
stringsize=next((v for k,v in tags if k==10),None)
|
|
if address is None or stringsize is None:raise ProbeError('Missing ELF string table')
|
|
segment=next((s for s in loads if s['vaddr']<=address<s['vaddr']+s['size']),None)
|
|
if segment is None:raise ProbeError('String table has no load segment')
|
|
start=segment['offset']+address-segment['vaddr']
|
|
if start+stringsize>len(data):raise ProbeError('Invalid string table size')
|
|
for off in needed_offsets:
|
|
if off>=stringsize:raise ProbeError('Invalid DT_NEEDED index')
|
|
end=data.find(b'\0',start+off,start+stringsize)
|
|
if end<0:raise ProbeError('Unterminated DT_NEEDED')
|
|
needed.append(data[start+off:end].decode('ascii'))
|
|
return {'abi':abi,'machine':machine,'elf_class':elfclass,'type':'DYN','needed':needed,
|
|
'load_segments':loads,'elf_16k_alignment_candidate':all(s['alignment']>=16384 and
|
|
s['offset']%16384==s['vaddr']%16384 for s in loads),
|
|
'note':'ELF audit only. dlopen dependencies and Android loader execution require device test.'}
|
|
|
|
def runtime_identity(data:bytes,cpu:str)->str:
|
|
"""Fingerprint loadable sections/layout while allowing removal of debug symbols.
|
|
|
|
Android packaging may strip non-allocated sections. Do not require the whole
|
|
file to be byte-identical, but never accept different code/data or a library
|
|
without a usable section table on this fallback path.
|
|
"""
|
|
inspect_elf(data,cpu)
|
|
cls=CPUS[cpu][2]
|
|
if cls==2:
|
|
entry,phoff,shoff=struct.unpack_from('<QQQ',data,24)
|
|
phsize,phnum,shsize,shnum,shstr=struct.unpack_from('<HHHHH',data,54)
|
|
fmt='<IIQQQQIIQQ'; minimum=64
|
|
else:
|
|
entry,phoff,shoff=struct.unpack_from('<III',data,24)
|
|
phsize,phnum,shsize,shnum,shstr=struct.unpack_from('<HHHHH',data,42)
|
|
fmt='<IIIIIIIIII'; minimum=40
|
|
if shsize<minimum or not 0<shnum<65535 or not 0<shstr<shnum or shoff+shsize*shnum>len(data):
|
|
raise ProbeError('Cannot verify stripped library: invalid/absent section table')
|
|
sections=[struct.unpack_from(fmt,data,shoff+i*shsize) for i in range(shnum)]
|
|
names=sections[shstr]; ns,nn=names[4],names[5]
|
|
if ns+nn>len(data):raise ProbeError('Invalid ELF section-name table')
|
|
records=[];seen=set()
|
|
for name,typ,flags,addr,off,size,link,info,align,entsize in sections:
|
|
if not flags&2:continue # SHF_ALLOC; debug/symbol-only data is excluded.
|
|
if name>=nn:raise ProbeError('Invalid section-name offset')
|
|
end=data.find(b'\0',ns+name,ns+nn)
|
|
if end<0:raise ProbeError('Invalid section name')
|
|
text=data[ns+name:end].decode('ascii')
|
|
key=(text,addr)
|
|
if key in seen:raise ProbeError('Duplicate allocated section')
|
|
seen.add(key)
|
|
if typ!=8 and off+size>len(data):raise ProbeError('Allocated section outside ELF')
|
|
# Link/info may be section indices and legitimately change on strip.
|
|
records.append([text,typ,flags,addr,size,align,entsize,
|
|
None if typ==8 else hashlib.sha256(data[off:off+size]).hexdigest()])
|
|
if not records or not any(r[2]&4 for r in records):
|
|
raise ProbeError('Cannot verify stripped library: no executable allocated sections')
|
|
shape={'entry':entry,'program_headers':data[phoff:phoff+phsize*phnum].hex(),
|
|
'sections':sorted(records)}
|
|
return hashlib.sha256(json.dumps(shape,sort_keys=True).encode()).hexdigest()
|
|
|
|
def inspect_apk(path:Path,cpu:str,library_name:str,compiled_sha:str,compiled_data:bytes|None=None)->dict:
|
|
abi=CPUS[cpu][0]
|
|
with zipfile.ZipFile(path) as z:
|
|
if len(z.namelist())!=len(set(z.namelist())):raise ProbeError('Duplicate ZIP entries in APK')
|
|
if z.testzip() is not None:raise ProbeError('APK ZIP CRC failure')
|
|
for required in ('AndroidManifest.xml','classes.dex'):
|
|
if required not in z.namelist():raise ProbeError('APK lacks '+required)
|
|
if not z.read('classes.dex').startswith(b'dex\n'):raise ProbeError('APK has invalid DEX magic')
|
|
libs={Path(n).name:n for n in z.namelist() if n.startswith('lib/'+abi+'/') and n.endswith('.so')}
|
|
if not libs:raise ProbeError('APK lacks native libraries for '+abi)
|
|
matches=[]
|
|
for candidate,path_in_apk in libs.items():
|
|
data=z.read(path_in_apk);digest=hashlib.sha256(data).hexdigest()
|
|
if digest==compiled_sha:
|
|
matches.append((candidate,path_in_apk,data,digest,'exact_sha256'));continue
|
|
if compiled_data is None or hashlib.sha256(compiled_data).hexdigest()!=compiled_sha:continue
|
|
try:same_runtime=runtime_identity(data,cpu)==runtime_identity(compiled_data,cpu)
|
|
except ProbeError:continue
|
|
if same_runtime:matches.append((candidate,path_in_apk,data,digest,'identical_allocated_sections_and_program_headers_after_stripping'))
|
|
if len(matches)!=1:raise ProbeError('APK has no unique native library matching the compiled Android runtime; artifact may be different/stale')
|
|
main_name,main_path,main,packaged_sha,identity=matches[0]
|
|
records={}
|
|
for name,entry in libs.items():
|
|
rec=inspect_elf(z.read(entry),cpu)
|
|
missing=sorted(set(rec['needed'])-set(libs)-ANDROID_SYSTEM)
|
|
if missing:raise ProbeError('Unpackaged DT_NEEDED for '+name+': '+', '.join(missing))
|
|
info=z.getinfo(entry);rec['zip_compression']=info.compress_type
|
|
records[name]=rec
|
|
unexpected=sorted({n.split('/')[1] for n in z.namelist() if n.startswith('lib/') and n.endswith('.so')}- {abi})
|
|
if unexpected:raise ProbeError('Unexpected ABI in single-CPU probe: '+', '.join(unexpected))
|
|
return {'sha256':sha256(path),'size':path.stat().st_size,'abi':abi,'native_libraries':records,
|
|
'main_library_path':main_path,'main_library_name':main_name,'main_library_identity':identity,'packaged_main_sha256':packaged_sha,
|
|
'signing':'Verified separately by apksigner when available; ZIP inspection is not signature verification'}
|