// memscan.js
'use strict';
/*
CheatEngine-ish memory scanner for numeric values (Frida).
FAST range scanning strategy:
- Bulk-read each range with Memory.readByteArray()
- Scan a local DataView buffer
- Store results per-range as blocks:
{ base, size, step, type, offsets: Uint32Array, last: (Float32Array|Int32Array|Uint32Array) }
Commands:
msRanges([prot="rw-"])
msNew(value, [type="u32"|"s32"|"f32"], [prot="rw-"])
msNewRange(min, max, [type="f32"|"u32"|"s32"], [prot="rw-"], [step=4])
msUse("exact"|"range")
msRefine("eq"|"changed"|"unchanged"|"inc"|"dec"|"gt"|"lt", [value])
msUndoRefine()
msList([limit=40])
msWrite(index, value)
msWriteAll(value)
msFreeze(index, [intervalMs=100])
msUnfreeze(index)
msUnfreezeAll()
msClear()
msHelp()
*/
const RANGE_CHUNK = 1024 * 1024;
const DEBUG_RANGE = false;
const FLOAT_EPSILON = 0.0001;
const state = {
type: 'u32',
active: 'exact',
exact: [],
exactPrev: null,
rangeBlocks: [],
rangeBlocksPrev: null,
frozen: new Map(),
last: {
ranges: 0,
failedRanges: 0,
readErrors: 0,
hits: 0
}
};
// ---------- Type helpers ----------
function clampType(t) {
const ok = new Set(['u32', 's32', 'f32']);
return ok.has(t) ? t : 'u32';
}
function enumerateRanges(protection) {
return Process.enumerateRanges({ protection, coalesce: true });
}
function fmt(v, t) {
if (t === 'f32') return Number.isFinite(v) ? v.toFixed(6) : String(v);
return String(v);
}
function makeLastArray(t, n) {
if (t === 'f32') return new Float32Array(n);
if (t === 's32') return new Int32Array(n);
return new Uint32Array(n);
}
function dvRead(dv, off, t) {
const byteLen = dv.byteLength;
if (off < 0 || off + 4 > byteLen) return null;
if (t === 'f32') return dv.getFloat32(off, true);
if (t === 's32') return dv.getInt32(off, true);
return dv.getUint32(off, true) >>> 0;
}
function floatEq(a, b) {
return Math.abs(a - b) < FLOAT_EPSILON;
}
function valuesEqual(a, b, t) {
if (t === 'f32') return floatEq(a, b);
return a === b;
}
function readAt(addr, t) {
switch (t) {
case 'u32': return addr.readU32() >>> 0;
case 's32': return addr.readS32() | 0;
case 'f32': return addr.readFloat();
default: return addr.readU32() >>> 0;
}
}
function writeAt(addr, t, v) {
switch (t) {
case 'u32': addr.writeU32(v >>> 0); break;
case 's32': addr.writeS32(v | 0); break;
case 'f32': addr.writeFloat(+v); break;
default: addr.writeU32(v >>> 0); break;
}
}
function safeRead(addr, t) {
try {
return { ok: true, v: readAt(addr, t) };
} catch (_) {
return { ok: false, v: null };
}
}
function safeWrite(addr, t, v) {
try {
writeAt(addr, t, v);
const after = readAt(addr, t);
return { ok: true, after };
} catch (e) {
return { ok: false, err: String(e) };
}
}
// ---------- Active list access ----------
function activeCount() {
if (state.active === 'exact') return state.exact.length;
let n = 0;
for (const b of state.rangeBlocks) n += b.offsets.length;
return n;
}
function rangeIndexToEntry(index) {
let i = index | 0;
for (const block of state.rangeBlocks) {
const len = block.offsets.length;
if (i < len) {
const off = block.offsets[i];
return { block, j: i, addr: block.base.add(off) };
}
i -= len;
}
return null;
}
function getAddrByIndex(index) {
if (state.active === 'exact') {
if (index >= 0 && index < state.exact.length) {
return state.exact[index].addr;
}
return null;
}
const entry = rangeIndexToEntry(index);
return entry ? entry.addr : null;
}
// ---------- Pattern helpers ----------
function u32ToPattern(value) {
const v = value >>> 0;
const b0 = (v & 0xff).toString(16).padStart(2, '0');
const b1 = ((v >>> 8) & 0xff).toString(16).padStart(2, '0');
const b2 = ((v >>> 16) & 0xff).toString(16).padStart(2, '0');
const b3 = ((v >>> 24) & 0xff).toString(16).padStart(2, '0');
return `${b0} ${b1} ${b2} ${b3}`;
}
function f32ToPattern(value) {
const p = Memory.alloc(4);
p.writeFloat(+value);
const u = p.readU32() >>> 0;
return u32ToPattern(u);
}
function valueToPattern(value, t) {
if (t === 'f32') return f32ToPattern(value);
return u32ToPattern(value >>> 0);
}
// ---------- Bulk read helper ----------
function readChunk(base, size) {
const buf = base.readByteArray(size);
if (buf === null) throw new Error('readByteArray returned null');
return buf;
}
// ---------- Undo snapshot helpers ----------
function snapshotExact() {
return state.exact.map(e => ({ addr: e.addr, last: e.last }));
}
function snapshotRangeBlocks() {
return state.rangeBlocks.map(b => ({
base: b.base,
size: b.size,
step: b.step,
type: b.type,
offsets: new Uint32Array(b.offsets),
last: b.last.slice()
}));
}
// ---------- Public API ----------
globalThis.msRanges = function (protection = 'rw-') {
const ranges = enumerateRanges(protection);
console.log(`[*] Ranges for protection="${protection}": ${ranges.length}`);
for (let i = 0; i < Math.min(10, ranges.length); i++) {
const r = ranges[i];
const prot = r.protection !== undefined ? r.protection : protection;
console.log(` [${i}] ${r.base} size=${r.size} prot=${prot}`);
}
console.log(`[*] Tip: if 0 ranges, try msRanges('r--') or msRanges('r-x').`);
};
globalThis.msUse = function (which) {
const w = (which || '').toLowerCase();
if (w !== 'exact' && w !== 'range') {
console.log('Usage: msUse("exact"|"range")');
return;
}
state.active = w;
console.log(`[*] Active list set to: ${w} (${activeCount()} match(es), type=${state.type})`);
};
globalThis.msClear = function () {
state.exact = [];
state.exactPrev = null;
state.rangeBlocks = [];
state.rangeBlocksPrev = null;
msUnfreezeAll();
console.log('[*] Cleared exact + range matches and frozen addresses.');
};
globalThis.msNew = function (value, type = 'u32', protection = 'rw-') {
if (typeof value !== 'number') {
console.log('Usage: msNew(<number>, [type="u32"|"s32"|"f32"], [protection="rw-"])');
return;
}
state.type = clampType(type);
state.active = 'exact';
state.exact = [];
state.exactPrev = null;
const pattern = valueToPattern(value, state.type);
console.log(`[*] Exact scan: value=${value} type=${state.type} prot=${protection} pattern=${pattern}`);
const ranges = enumerateRanges(protection);
let total = 0;
let failedRanges = 0;
for (const r of ranges) {
try {
const hits = Memory.scanSync(r.base, r.size, pattern);
for (const h of hits) {
const res = safeRead(h.address, state.type);
if (!res.ok) continue;
state.exact.push({ addr: h.address, last: res.v });
total++;
}
} catch (_) {
failedRanges++;
}
}
console.log(`[*] Scan complete — ${total} hit(s). Ranges scanned=${ranges.length}, failed=${failedRanges}.`);
console.log(`[*] Active list: exact. Use msList(), msRefine(), msWrite().`);
};
globalThis.msNewRange = function (min, max, type = 'f32', protection = 'rw-', step = 4, chunkSize = RANGE_CHUNK) {
if (typeof min !== 'number' || typeof max !== 'number') {
console.log('Usage: msNewRange(<min>, <max>, [type="f32"|"u32"|"s32"], [protection="rw-"], [step=4], [chunkSize])');
return;
}
if (max < min) { const t = min; min = max; max = t; }
state.type = clampType(type);
state.active = 'range';
state.rangeBlocks = [];
state.rangeBlocksPrev = null;
step = step | 0;
if (step <= 0) step = 4;
chunkSize = chunkSize | 0;
if (chunkSize <= 0) chunkSize = RANGE_CHUNK;
console.log(`[*] Range scan (CHUNKED): min=${min} max=${max} type=${state.type} prot=${protection} step=${step} chunk=${chunkSize}`);
if (state.type == 'f32') {
console.log(`[*] Float scans can take a few minutes, hold tight...`)
}
const ranges = enumerateRanges(protection);
let totalHits = 0;
let failedRanges = 0;
let failedChunks = 0;
let readErrors = 0;
for (const r of ranges) {
const size = r.size >>> 0;
if (size < 4) continue;
const offsetsTmp = [];
const valuesTmp = [];
let anyChunkOk = false;
for (let rel = 0; rel < size; rel += chunkSize) {
const remaining = size - rel;
// Only add overlap if there's another chunk after this one
const needsOverlap = remaining > chunkSize;
const thisChunk = needsOverlap ? chunkSize + 3 : remaining;
let buf, dv;
try {
buf = readChunk(r.base.add(rel), thisChunk);
dv = new DataView(buf);
anyChunkOk = true;
} catch (e) {
failedChunks++;
if (DEBUG_RANGE) console.log(`[dbg] chunk read failed @${r.base.add(rel)} size=${thisChunk}: ${e}`);
continue;
}
const limit = thisChunk - 4;
let startOff = 0;
const mod = (rel % step);
if (mod !== 0) startOff = (step - mod);
for (let off = startOff; off <= limit; off += step) {
const v = dvRead(dv, off, state.type);
if (v === null) {
readErrors++;
continue;
}
if (state.type === 'f32' && Number.isNaN(v)) continue;
if (v >= min && v <= max) {
const absOff = (rel + off) >>> 0;
offsetsTmp.push(absOff);
valuesTmp.push(v);
}
}
}
if (!anyChunkOk) {
failedRanges++;
if (DEBUG_RANGE) console.log(`[dbg] range read failed entirely: ${r.base} size=${r.size}`);
continue;
}
if (offsetsTmp.length > 0) {
const offsets = new Uint32Array(offsetsTmp);
const last = makeLastArray(state.type, offsetsTmp.length);
for (let i = 0; i < valuesTmp.length; i++) last[i] = valuesTmp[i];
state.rangeBlocks.push({
base: r.base,
size: r.size,
step,
type: state.type,
offsets,
last
});
totalHits += offsets.length;
}
}
console.log(`[*] Scan complete — ${totalHits} hit(s). Ranges scanned=${ranges.length}, failedRanges=${failedRanges}, failedChunks=${failedChunks}, readErrors=${readErrors}.`);
console.log(`[*] Active list: range. Use msList(), msRefine(), msWrite().`);
};
globalThis.msList = function (limit = 40) {
const lim = Math.max(0, limit | 0);
if (state.active === 'exact') {
const n = Math.min(lim, state.exact.length);
console.log(`[*] Listing ${n}/${state.exact.length} matches (type=${state.type}, active=exact)`);
for (let i = 0; i < n; i++) {
const m = state.exact[i];
const res = safeRead(m.addr, state.type);
const frozen = state.frozen.has(m.addr.toString()) ? ' [FROZEN]' : '';
if (!res.ok) {
console.log(`[${i}] ${m.addr} <unreadable now> last=${fmt(m.last, state.type)}${frozen}`);
continue;
}
const cur = res.v;
console.log(`[${i}] ${m.addr} cur=${fmt(cur, state.type)} last=${fmt(m.last, state.type)}${frozen}`);
m.last = cur;
}
if (state.exact.length > n) console.log(`[*] ... (${state.exact.length - n} more)`);
return;
}
const total = activeCount();
const n = Math.min(lim, total);
console.log(`[*] Listing ${n}/${total} matches (type=${state.type}, active=range)`);
let printed = 0;
let globalIndex = 0;
for (const block of state.rangeBlocks) {
if (printed >= n) break;
for (let j = 0; j < block.offsets.length && printed < n; j++, globalIndex++) {
const off = block.offsets[j];
const addr = block.base.add(off);
const res = safeRead(addr, state.type);
const frozen = state.frozen.has(addr.toString()) ? ' [FROZEN]' : '';
if (!res.ok) {
console.log(`[${globalIndex}] ${addr} <unreadable now> last=${fmt(block.last[j], state.type)}${frozen}`);
printed++;
continue;
}
const cur = res.v;
console.log(`[${globalIndex}] ${addr} cur=${fmt(cur, state.type)} last=${fmt(block.last[j], state.type)}${frozen}`);
block.last[j] = cur;
printed++;
}
}
if (total > n) console.log(`[*] ... (${total - n} more)`);
};
globalThis.msRefine = function (mode, value) {
const m = (mode || '').toLowerCase();
const needsValue = new Set(['eq', 'gt', 'lt']);
if (needsValue.has(m) && typeof value !== 'number') {
console.log('Usage: msRefine("eq"|"gt"|"lt", <number>)');
return;
}
if (!['eq', 'changed', 'unchanged', 'inc', 'dec', 'gt', 'lt'].includes(m)) {
console.log('Usage: msRefine("eq"|"changed"|"unchanged"|"inc"|"dec"|"gt"|"lt", [value])');
return;
}
if (state.active === 'exact') {
state.exactPrev = snapshotExact();
const before = state.exact.length;
const kept = [];
for (const entry of state.exact) {
const res = safeRead(entry.addr, state.type);
if (!res.ok) continue;
const cur = res.v;
let keep = false;
if (m === 'eq') keep = valuesEqual(cur, value, state.type);
else if (m === 'gt') keep = (cur > value);
else if (m === 'lt') keep = (cur < value);
else if (m === 'changed') keep = !valuesEqual(cur, entry.last, state.type);
else if (m === 'unchanged') keep = valuesEqual(cur, entry.last, state.type);
else if (m === 'inc') keep = (cur > entry.last);
else if (m === 'dec') keep = (cur < entry.last);
if (keep) kept.push({ addr: entry.addr, last: cur });
}
state.exact = kept;
console.log(`[*] Refine(${m}${needsValue.has(m) ? `, ${value}` : ''}) — ${before} -> ${kept.length} (active=exact)`);
return;
}
state.rangeBlocksPrev = snapshotRangeBlocks();
const before = activeCount();
const newBlocks = [];
let failedBlocks = 0;
let failedChunks = 0;
let readErrors = 0;
let keptTotal = 0;
for (const block of state.rangeBlocks) {
const offsets = block.offsets;
const last = block.last;
const keptOffsetsTmp = [];
const keptValuesTmp = [];
const buckets = new Map();
const chunkSize = RANGE_CHUNK;
for (let j = 0; j < offsets.length; j++) {
const absOff = offsets[j] >>> 0;
const chunkStart = Math.floor(absOff / chunkSize) * chunkSize;
let arr = buckets.get(chunkStart);
if (!arr) { arr = []; buckets.set(chunkStart, arr); }
arr.push(j);
}
let anyOk = false;
for (const [chunkStartRel, js] of buckets.entries()) {
const remaining = (block.size >>> 0) - chunkStartRel;
if (remaining <= 0) continue;
const needsOverlap = remaining > chunkSize;
const thisChunk = needsOverlap ? Math.min(remaining, chunkSize + 3) : remaining;
let dv;
try {
const buf = readChunk(block.base.add(chunkStartRel), thisChunk);
dv = new DataView(buf);
anyOk = true;
} catch (e) {
failedChunks++;
if (DEBUG_RANGE) console.log(`[dbg] refine chunk read failed @${block.base.add(chunkStartRel)} size=${thisChunk}: ${e}`);
continue;
}
for (const j of js) {
const absOff = offsets[j] >>> 0;
const relOff = absOff - chunkStartRel;
const cur = dvRead(dv, relOff, state.type);
if (cur === null) {
readErrors++;
continue;
}
if (state.type === 'f32' && Number.isNaN(cur)) continue;
const prev = last[j];
let keep = false;
if (m === 'eq') keep = valuesEqual(cur, value, state.type);
else if (m === 'gt') keep = (cur > value);
else if (m === 'lt') keep = (cur < value);
else if (m === 'changed') keep = !valuesEqual(cur, prev, state.type);
else if (m === 'unchanged') keep = valuesEqual(cur, prev, state.type);
else if (m === 'inc') keep = (cur > prev);
else if (m === 'dec') keep = (cur < prev);
if (keep) {
keptOffsetsTmp.push(absOff);
keptValuesTmp.push(cur);
}
}
}
if (!anyOk) {
failedBlocks++;
continue;
}
if (keptOffsetsTmp.length > 0) {
const newOffsets = new Uint32Array(keptOffsetsTmp);
const newLast = makeLastArray(state.type, keptOffsetsTmp.length);
for (let i = 0; i < keptValuesTmp.length; i++) newLast[i] = keptValuesTmp[i];
newBlocks.push({
base: block.base,
size: block.size,
step: block.step,
type: block.type,
offsets: newOffsets,
last: newLast
});
keptTotal += newOffsets.length;
}
}
state.rangeBlocks = newBlocks;
console.log(
`[*] Refine(${m}${needsValue.has(m) ? `, ${value}` : ''}) — ${before} -> ${keptTotal} (active=range)` +
` | failedBlocks=${failedBlocks} failedChunks=${failedChunks} readErrors=${readErrors}`
);
};
globalThis.msUndoRefine = function () {
if (state.active === 'exact') {
if (state.exactPrev === null) {
console.log('[!] No previous exact state to restore.');
return;
}
const before = state.exact.length;
state.exact = state.exactPrev;
state.exactPrev = null;
console.log(`[*] Undo refine: ${before} -> ${state.exact.length} (active=exact)`);
return;
}
if (state.rangeBlocksPrev === null) {
console.log('[!] No previous range state to restore.');
return;
}
const before = activeCount();
state.rangeBlocks = state.rangeBlocksPrev;
state.rangeBlocksPrev = null;
console.log(`[*] Undo refine: ${before} -> ${activeCount()} (active=range)`);
};
globalThis.msWrite = function (index, newValue) {
if (typeof newValue !== 'number') {
console.log('Usage: msWrite(<index>, <number>)');
return;
}
const i = index | 0;
const total = activeCount();
if (i < 0 || i >= total) {
console.log(`Usage: msWrite(<index 0..${total - 1}>, <number>)`);
return;
}
if (state.active === 'exact') {
const addr = state.exact[i].addr;
const res = safeWrite(addr, state.type, newValue);
if (!res.ok) {
console.log(`[!] Write failed at ${addr}: ${res.err}`);
return;
}
state.exact[i].last = res.after;
console.log(`[*] Wrote ${fmt(newValue, state.type)} at ${addr} verify=${fmt(res.after, state.type)}`);
return;
}
const entry = rangeIndexToEntry(i);
if (entry === null) {
console.log('[!] Internal error: index mapping failed');
return;
}
const { block, j, addr } = entry;
const res = safeWrite(addr, state.type, newValue);
if (!res.ok) {
console.log(`[!] Write failed at ${addr}: ${res.err}`);
return;
}
block.last[j] = res.after;
console.log(`[*] Wrote ${fmt(newValue, state.type)} at ${addr} verify=${fmt(res.after, state.type)}`);
};
globalThis.msWriteAll = function (newValue) {
if (typeof newValue !== 'number') {
console.log('Usage: msWriteAll(<number>)');
return;
}
const total = activeCount();
if (total === 0) {
console.log('[!] No matches to write.');
return;
}
let success = 0;
let failed = 0;
if (state.active === 'exact') {
for (const entry of state.exact) {
const res = safeWrite(entry.addr, state.type, newValue);
if (res.ok) {
entry.last = res.after;
success++;
} else {
failed++;
}
}
} else {
for (const block of state.rangeBlocks) {
for (let j = 0; j < block.offsets.length; j++) {
const addr = block.base.add(block.offsets[j]);
const res = safeWrite(addr, state.type, newValue);
if (res.ok) {
block.last[j] = res.after;
success++;
} else {
failed++;
}
}
}
}
console.log(`[*] WriteAll(${fmt(newValue, state.type)}): ${success} succeeded, ${failed} failed.`);
};
globalThis.msFreeze = function (index, intervalMs = 100) {
const i = index | 0;
const total = activeCount();
if (i < 0 || i >= total) {
console.log(`Usage: msFreeze(<index 0..${total - 1}>, [intervalMs=100])`);
return;
}
const addr = getAddrByIndex(i);
if (addr === null) {
console.log('[!] Internal error: could not resolve address.');
return;
}
const key = addr.toString();
if (state.frozen.has(key)) {
console.log(`[!] Address ${addr} is already frozen. Use msUnfreeze(${i}) first.`);
return;
}
const res = safeRead(addr, state.type);
if (!res.ok) {
console.log(`[!] Cannot read address ${addr} to freeze.`);
return;
}
const frozenValue = res.v;
const t = state.type;
const timer = setInterval(() => {
safeWrite(addr, t, frozenValue);
}, intervalMs);
state.frozen.set(key, { timer, value: frozenValue, index: i });
console.log(`[*] Frozen [${i}] ${addr} at value=${fmt(frozenValue, t)} (interval=${intervalMs}ms)`);
};
globalThis.msUnfreeze = function (index) {
const i = index | 0;
const addr = getAddrByIndex(i);
if (addr === null) {
console.log(`[!] Invalid index: ${i}`);
return;
}
const key = addr.toString();
if (!state.frozen.has(key)) {
console.log(`[!] Address ${addr} is not frozen.`);
return;
}
const entry = state.frozen.get(key);
clearInterval(entry.timer);
state.frozen.delete(key);
console.log(`[*] Unfrozen [${i}] ${addr}`);
};
globalThis.msUnfreezeAll = function () {
const count = state.frozen.size;
for (const [key, entry] of state.frozen) {
clearInterval(entry.timer);
}
state.frozen.clear();
console.log(`[*] Unfroze ${count} address(es).`);
};
globalThis.msHelp = function () {
console.log([
'msHelp()',
'msRanges([protection="rw-"])',
'msNew(value, [type="u32"|"s32"|"f32"], [protection="rw-"])',
'msNewRange(min, max, [type="f32"|"u32"|"s32"], [protection="rw-"], [step=4])',
'msUse("exact"|"range")',
'msRefine("eq"|"changed"|"unchanged"|"inc"|"dec"|"gt"|"lt", [value])',
'msUndoRefine()',
'msList([limit=40])',
'msWrite(index, value)',
'msWriteAll(value)',
'msFreeze(index, [intervalMs=100])',
'msUnfreeze(index)',
'msUnfreezeAll()',
'msClear()'
].join('\n'));
};