Prepare documented WhatsApp injector with portable Python bundle

This commit is contained in:
Ops Agent committed 2026-10-11 07:57:48 +02:00
commit d2b621bce7
49 files changed
+5737

No files matched your search

+78
View File
@@ -0,0 +1,78 @@
"""Search loaded WhatsApp Desktop message objects via read-only process RAM.
Does not invoke WhatsApp's native FTS engine or load older history. The query
matches Unicode-normalized, case-insensitive literal text in body/caption.
"""
from .common import normalized as normalize
import argparse, ctypes, datetime as dt, json, sys, unicodedata
from pathlib import Path
from .ram import Memory, discover, scan_markers, recover
EVIDENCE='ev_fd70aa06525b6f2259bf6d98bc879adb2472f63f2407a2d0d1186cc9e27b2f21'
def search_models(models,query,chat_id=None,redact=False):
needle=normalize(query)
if not needle.strip():raise ValueError('Query must contain non-whitespace text')
matches=[]
for model in models:
# Reject schema-only candidates without verified collection membership.
if not model.get('in_message_collection'):continue
if chat_id is not None and model.get('chat_id')!=chat_id:continue
fields=[field for field in ('body','caption') if isinstance(model.get(field),str) and needle in normalize(model[field])]
if not fields:continue
result=dict(model);result['matched_fields']=fields
if redact:
for field in ('body','caption'):
value=result.pop(field,None)
result[field+'_length']=len(value) if isinstance(value,str) else None
matches.append(result)
return matches
def main(argv=None):
p=argparse.ArgumentParser(description=__doc__)
p.add_argument('query')
p.add_argument('--chat-id')
p.add_argument('--max-mb',type=int,default=512)
p.add_argument('--max-models',type=int,default=10000)
p.add_argument('--redact-text',action='store_true')
p.add_argument('--output',type=Path,default=Path('whatsapp-process-search.json'))
args=p.parse_args(argv)
if sys.platform!='win32' or ctypes.sizeof(ctypes.c_void_p)!=8:p.error('64-bit Windows Python required')
if not args.query.strip():p.error('Query must contain non-whitespace text')
if not 16<=args.max_mb<=4096:p.error('--max-mb must be 16..4096')
if not 1<=args.max_models<=10000:p.error('--max-models must be 1..10000')
if args.output.exists():p.error('Output already exists; choose a new --output path')
targets=discover();results=[]
for target in targets:
pid=target['ProcessId']
try:mem=Memory(pid)
except OSError as error:
results.append({'pid':pid,'error':str(error),'matches':[]});continue
try:
regions=list(mem.regions())
markers=scan_markers(mem,regions,args.max_mb)
models,diagnostics=recover(mem,regions,markers,args.max_mb,args.max_models)
verified=[m for m in models if m.get('in_message_collection')]
matches=search_models(verified,args.query,args.chat_id,args.redact_text)
results.append({'pid':pid,'loaded_models_examined':len(verified),'matches':matches,
'diagnostics':diagnostics,'bytes_read':mem.read_bytes,
'model_limit_reached':len(models)>=args.max_models,
'debug_flags_present':target['DebugFlagsPresent']})
finally:mem.close()
report={'timestamp_utc':dt.datetime.now(dt.timezone.utc).isoformat(),
'mode':'ReadProcessMemory; literal search of collection-verified loaded Msg objects',
'whatsapp_native_search_invoked':False,'rea_search_evidence':EVIDENCE,
'results':results,'limitations':['Loaded message objects only; not the full IndexedDB or native FTS index.',
'No process execution, writes, injection, debugger, UI control, or new history query.',
'Live heap may move; the reader checks object layout and collection membership but is not an atomic snapshot.',
'Search uses NFC/casefold literal matching; it does not reproduce WhatsApp tokenizer, prefix matching or ranking.',
'No match is not evidence of absence from account history.']}
args.output.parent.mkdir(parents=True,exist_ok=True)
args.output.write_text(json.dumps(report,indent=2,ensure_ascii=False),encoding='utf-8')
print(json.dumps({'targets':len(results),'loaded_models_examined':sum(r.get('loaded_models_examined',0) for r in results),
'matches':sum(len(r['matches']) for r in results),'native_search_invoked':False,
'report':str(args.output.resolve())}))
return 0 if any(r.get('loaded_models_examined') for r in results) else 2
if __name__=='__main__':raise SystemExit(main())