Files
kam-mobile/tools/artifacts.py
T

134 lines
8.3 KiB
Python
Raw Normal View History

2026-09-11 18:18:40 +05:00
"""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'}