Files
ghostwire/whatsapp_suite/assets/sqlite_inject.js
T

87 lines
7.1 KiB
JavaScript
Raw Normal View History

// REA-backed WinRT ABI for WhatsAppNativeProjection.dll build 2.2639.100.0.
// Native calls only; no CDP, UI automation or restart.
const native=Process.getModuleByName('WhatsAppNative.dll');
if(!native.path.includes('WhatsAppDesktop_2.2639.100.0_'))throw Error('Unsupported Desktop build; ABI must be revalidated with REA');
const combase=Process.getModuleByName('combase.dll');
function api(name,ret,args){return new NativeFunction(combase.getExportByName(name),ret,args);}
const createString=api('WindowsCreateString','int',['pointer','uint','pointer']);
const deleteString=api('WindowsDeleteString','int',['pointer']);
const rawString=api('WindowsGetStringRawBuffer','pointer',['pointer','pointer']);
function check(hr){if(hr<0)throw Error('HRESULT 0x'+(hr>>>0).toString(16));}
function hstring(value){const out=Memory.alloc(8);check(createString(Memory.allocUtf16String(value),value.length,out));return out.readPointer();}
function readHString(handle){if(handle.isNull())return '';const len=Memory.alloc(4);const p=rawString(handle,len);return p.readUtf16String(len.readU32());}
function guid(hex){const p=Memory.alloc(16);p.writeByteArray(hex.match(/../g).map(x=>parseInt(x,16)));return p;}
const dbIid=guid('eeb0ca996bf8523780d9cf1b934d62b5');
const stmtIid=guid('94f8c705a4cb4b3baa1b9b2ce2ff2734');
const propertyIid=guid('dd82d64b5475e9409a9b82654ede7e62');
function method(obj,slot,ret,args){return new NativeFunction(obj.readPointer().add(slot*8).readPointer(),ret,['pointer',...args]);}
function release(obj){if(!obj.isNull())method(obj,2,'uint',[])(obj);}
function query(obj,iid){const out=Memory.alloc(8);check(method(obj,0,'int',['pointer','pointer'])(obj,iid,out));return out.readPointer();}
let references=[];
function property(value){if(value.isNull())return null;let p=ptr(0);try{
p=query(value,propertyIid);const out=Memory.alloc(8);check(method(p,19,'int',['pointer'])(p,out));
const h=out.readPointer();try{return readHString(h);}finally{deleteString(h);}
}finally{release(p);release(value);}}
function statement(db,sql){const h=hstring(sql),error=Memory.alloc(4),partial=Memory.alloc(8),out=Memory.alloc(8);
try{check(method(db,9,'int',['pointer','pointer','pointer','pointer'])(db,h,error,partial,out));
// The integer out-parameter is the consumed SQL offset, not an error code.
const original=out.readPointer();if(original.isNull())throw Error('No prepared statement');
try{return query(original,stmtIid);}finally{release(original);}
}finally{deleteString(h);}
}
function select(db,sql,limit){let st=ptr(0);try{
st=statement(db,sql);const count=Memory.alloc(4);check(method(st,10,'int',['pointer'])(st,count));
const rows=[];while(rows.length<limit){const has=Memory.alloc(1);check(method(st,7,'int',['pointer'])(st,has));if(has.readU8()===0)break;
const row=[];for(let i=0;i<count.readS32();i++){const out=Memory.alloc(8);check(method(st,11,'int',['int','pointer'])(st,i,out));row.push(property(out.readPointer()));}rows.push(row);}
return rows;
}finally{if(!st.isNull()){try{check(method(st,6,'int',[])(st));}finally{release(st);}}}}
rpc.exports={
discover(maxMb){
// Obtain the real interface vtable from a fresh closed Sqlite object. No DB
// is opened and no credentials are read from the existing session.
const init=api('RoInitialize','int',['uint'])(1);if(init<0&&init!==-2147417850)check(init);
const name=hstring('WhatsAppNative.Sqlite'),factoryOut=Memory.alloc(8);
const getFactory=new NativeFunction(native.getExportByName('DllGetActivationFactory'),'int',['pointer','pointer']);
let factory=ptr(0),sample=ptr(0),iface=ptr(0);
try{check(getFactory(name,factoryOut));factory=factoryOut.readPointer();const out=Memory.alloc(8);
check(method(factory,6,'int',['pointer'])(factory,out));sample=out.readPointer();iface=query(sample,dbIid);
const vtable=iface.readPointer();const bytes=Memory.alloc(8);bytes.writePointer(vtable);
const pattern=Array.from(new Uint8Array(bytes.readByteArray(8))).map(v=>v.toString(16).padStart(2,'0')).join(' ');
let scanned=0,hitCount=0;const found=[],addresses=new Set(),failures=[];
for(const range of Process.enumerateRanges({protection:'rw-',coalesce:true})){
if(scanned>=maxMb*1024*1024)break;
const size=Math.min(range.size,maxMb*1024*1024-scanned);scanned+=size;
let hits=[];try{hits=Memory.scanSync(range.base,size,pattern);}catch{continue;}
hitCount+=hits.length;
for(const hit of hits){if(hit.address.equals(iface)||!hit.address.and(7).isNull())continue;
let object=ptr(0);try{object=query(hit.address,dbIid);if(addresses.has(object.toString())){release(object);continue;}
const tables=select(object,"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name",100);
const id=references.length;references.push(object);addresses.add(object.toString());
found.push({id,address:object.toString(),tables:tables.map(r=>r[0])});object=ptr(0);
}catch(error){if(failures.length<20)failures.push({address:hit.address.toString(),error:String(error)});if(!object.isNull())release(object);}
}
}return {databases:found,bytesScanned:scanned,vtable:vtable.toString(),vtableHits:hitCount,failures};
}finally{release(iface);release(sample);release(factory);deleteString(name);}
},
search(id,term,limit){if(!Number.isInteger(id)||id<0||id>=references.length)throw Error('Unknown database');
if(!Number.isInteger(limit)||limit<1||limit>1000)throw Error('Invalid row limit');
const tokens=term.trim().split(/\s+/).filter(Boolean);if(!tokens.length)throw Error('Empty query');
const match=tokens.map(t=>'"'+t.replace(/"/g,'""')+'"*').join(' AND ').replace(/'/g,"''");
const sql="SELECT CAST(t.id AS TEXT),CAST(t.chatId AS TEXT),CAST(t.timestamp AS TEXT),CAST(t.text AS TEXT) FROM message t JOIN message_fts f ON t.rowid=f.rowid WHERE message_fts MATCH '"+match+"' ORDER BY rank LIMIT "+limit;
return select(references[id],sql,limit);
},
oldest(id,limit){
if(!Number.isInteger(id)||id<0||id>=references.length)throw Error('Unknown database');
if(!Number.isInteger(limit)||limit<1||limit>1000)throw Error('Invalid row limit');
return select(references[id],"SELECT CAST(id AS TEXT),CAST(chatId AS TEXT),CAST(timestamp AS TEXT),CAST(text AS TEXT) FROM message WHERE timestamp > 0 AND text IS NOT NULL AND length(text) > 0 ORDER BY timestamp,id LIMIT "+limit,limit);
},
stats(id){return select(references[id],"SELECT CAST(COUNT(*) AS TEXT),CAST(MIN(timestamp) AS TEXT),CAST(MAX(timestamp) AS TEXT) FROM message",1)[0];},
indexbounds(id){return select(references[id],"SELECT CAST(COUNT(*) AS TEXT),CAST(COALESCE(MAX(rowid),0) AS TEXT) FROM message",1)[0];},
exportindex(id,after,upper,limit){
if(!Number.isInteger(id)||id<0||id>=references.length)throw Error('Unknown database');
if(!/^-?\d+$/.test(String(after))||!/^\d+$/.test(String(upper)))throw Error('Invalid row boundary');
if(!Number.isInteger(limit)||limit<1||limit>500)throw Error('Invalid batch size');
return select(references[id],"SELECT CAST(rowid AS TEXT),CAST(id AS TEXT),CAST(chatId AS TEXT),CAST(timestamp AS TEXT),CAST(text AS TEXT) FROM message WHERE rowid > "+after+" AND rowid <= "+upper+" ORDER BY rowid LIMIT "+limit,limit);
},
close(){for(const object of references)release(object);references=[];}
};