Initial import
This commit is contained in:
commit
dfcc43c72d
34 files changed
+3057
No files matched your search
Executable
+134
@@ -0,0 +1,134 @@
|
||||
"""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'}
|
||||
Reference in new issue
Block a user