Files

92 lines
5.4 KiB
Python

"""Find WhatsApp Desktop pairing QR payload candidates using read-only process RAM.
No screenshots, browser, debug connection or injected code. Finding a payload
does not establish that its server reference is active; no session is created.
"""
import argparse, base64, ctypes, datetime as dt, hashlib, json, re, struct, sys
from pathlib import Path
from .ram import Memory, discover
REA_EVIDENCE = 'ev_6c3a7930e51a95d162c3634ecdc0b73652cb640663681a7cb9962a5b45a9c1da'
PATTERN = re.compile(r'(?:https://wa\.me/settings/linked_devices#)?[A-Za-z0-9+/=_:-]{8,512},[A-Za-z0-9+/]{43}=,[A-Za-z0-9+/]{43}=,[A-Za-z0-9+/]{43}=,8(?:,https://faq\.whatsapp\.com/r/ld)?')
def valid_payload(value):
fields = value.removeprefix('https://wa.me/settings/linked_devices#').split(',')
if len(fields) not in (5, 6) or fields[4] != '8': return False
if len(fields) == 6 and fields[5] != 'https://faq.whatsapp.com/r/ld': return False
try: return all(len(base64.b64decode(f, validate=True)) == 32 for f in fields[1:4])
except ValueError: return False
def scan(mem, budget_mb):
scanned, seen = 0, set()
budget = budget_mb * 1024 * 1024
for region in mem.regions():
for offset in range(0, region['size'], 1024 * 1024):
if scanned >= budget: return
address = region['base'] + offset
size = min(1024 * 1024, region['size'] - offset, budget - scanned)
data = mem.read(address, min(size + 2048, region['size'] - offset))
scanned += size
variants = [(data.decode('latin-1'), 1, 0)]
if b',\x008\x00' in data:
for alignment in (0, 1):
tail = len(data) - (len(data) - alignment) % 2
variants.append((data[alignment:tail].decode('utf-16-le', errors='surrogatepass'), 2, alignment))
for text, width, alignment in variants:
for match in PATTERN.finditer(text):
payload = match.group()
if not valid_payload(payload): continue
start = address + alignment + match.start() * width
header = mem.read(start - 12, 12)
if len(header) != 12: continue
map_tag, _, length = struct.unpack('<III', header)
if not map_tag & 1 or length != len(payload) or (start - 12) % 4: continue
raw = payload.encode('latin-1' if width == 1 else 'utf-16-le')
if mem.read(start, len(raw)) != raw: continue
digest = hashlib.sha256(payload.encode()).hexdigest()
if digest in seen: continue
seen.add(digest)
yield payload, {'pid': mem.pid, 'string_address': hex(start - 12),
'encoding': 'Latin-1' if width == 1 else 'UTF-16LE',
'payload_sha256': digest, 'server_validity': 'unverified',
'validation': 'field lengths, candidate V8 string header, repeat memory read'}
def main(argv=None):
p = argparse.ArgumentParser(description=__doc__)
p.add_argument('--output-dir', type=Path, default=Path.cwd() / 'whatsapp-desktop-pairing-memory')
p.add_argument('--max-mb', type=int, default=512)
args = p.parse_args(argv)
import qrcode
if sys.platform != 'win32' or ctypes.sizeof(ctypes.c_void_p) != 8: p.error('64-bit Windows Python required')
if not 16 <= args.max_mb <= 4096: p.error('--max-mb must be 16..4096')
targets = discover()
candidates, diagnostics = [], []
args.output_dir.mkdir(parents=True, exist_ok=True)
for target in targets:
pid = target['ProcessId']
try: mem = Memory(pid)
except OSError as error:
diagnostics.append({'pid': pid, 'error': str(error)}); continue
try:
for payload, metadata in scan(mem, args.max_mb):
path = args.output_dir / ('pairing-candidate-' + metadata['payload_sha256'][:16] + '.png')
qrcode.make(payload).save(path)
metadata['file'] = str(path.resolve())
candidates.append(metadata)
diagnostics.append({'pid': pid, 'bytes_read': mem.read_bytes, 'debug_flags_present': target['DebugFlagsPresent']})
finally: mem.close()
report = {'timestamp_utc': dt.datetime.now(dt.timezone.utc).isoformat(),
'mode': 'ReadProcessMemory; running Desktop renderer descendants only',
'rea_static_evidence': REA_EVIDENCE, 'candidates': candidates, 'diagnostics': diagnostics,
'limitations': ['Payloads can be expired or unreachable heap objects; active registration is not verified.',
'Bounded scan of private readable memory; Cons/external strings or separately stored fields are not reconstructed.',
'No new server reference is requested; a linked app may have no pairing payload.',
'PNG candidates contain temporary pairing credentials. JSON omits raw payload and private keys.']}
path = args.output_dir / 'report.json'
path.write_text(json.dumps(report, indent=2), encoding='utf-8')
print(json.dumps({'status': 'unverified_candidates_saved' if candidates else 'no_pairing_payload_found',
'candidates': len(candidates), 'targets': len(targets), 'report': str(path.resolve())}))
return 0 if candidates else 2
if __name__ == '__main__': raise SystemExit(main())