"""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('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('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='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('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'}