91 lines
5.4 KiB
Python
91 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())
|