Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

26 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

quickmem

An Android CLI memory tool powered by QuickJS. It exposes a frida-gum-like JavaScript API for reading and writing remote process memory, backed by Linux process_vm_readv / process_vm_writev.

Overview

quickmem lets you attach to a running Android process by PID and script memory operations in JavaScript. It bundles the QuickJS-ng v0.14.0 engine and provides a NativePointer-style API for inspecting and modifying target memory without writing native code.

Features

  • Evaluate JavaScript inline, from a file, or via stdin
  • Interactive REPL mode with --repl
  • ptr(value) to create native pointers from numbers or hex strings
  • Full set of typed read/write operations (8/16/32/64-bit, float, double)
  • Byte array read/write with ArrayBuffer / Uint8Array support
  • UTF-8 and C-string read/write helpers
  • Pointer arithmetic: add, sub, xor, shr, shl
  • Global hexdump(target, options) for dumping NativePointer or ArrayBuffer memory
  • Global console.log(...args) for debugging scripts
  • TypeError exceptions with descriptive errno translations (EPERM, ESRCH, EFAULT, EINVAL)

Build Instructions

Build the native executable with Gradle:

./gradlew :app:assembleDebug

The binary is produced at a path like:

app/build/intermediates/cmake/debug/obj/arm64-v8a/quickmem

Push it to your device and run:

adb push app/build/intermediates/cmake/debug/obj/arm64-v8a/quickmem /data/local/tmp/
adb shell chmod +x /data/local/tmp/quickmem
adb shell /data/local/tmp/quickmem <pid> -e "console.log('hello world')"

Usage

quickmem <pid> [-e "<js>"] [--repl] [script.js]

Arguments:

  • <pid>: target process ID (required)
  • -e "<js>": execute an inline JavaScript expression
  • --repl: start interactive REPL with line editing, history (up/down arrows), and cursor movement (left/right arrows). Type .exit to quit
  • script.js: path to a JavaScript file to execute
  • (none): read JavaScript from stdin until EOF

Examples

Inline evaluation

quickmem 1234 -e "console.log('hello world')"

Execute a script file

quickmem 1234 myscript.js

Read from stdin

quickmem 1234 < myscript.js

Interactive REPL

quickmem 1234 --repl
quickmem> 1+2
3
quickmem> ptr('0x1000').toString()
0x1000
quickmem> [press up arrow]  # recalls previous command
quickmem> 1+2
3
quickmem> .exit

JS API Reference

ptr(value)

Creates a NativePointer from a number, a hex string, or an existing NativePointer.

  • value (number | string | NativePointer): a numeric address, a hex string such as "0x7ffd0000", or an existing NativePointer
  • Returns: NativePointer
  • Throws TypeError if the argument is not a number, a hex string, or a NativePointer, or if BigInt is passed
const p = ptr(0x7ffd0000);
const q = ptr("0x7ffd0000");
const r = ptr(p);  // copies address from existing NativePointer

NativePointer

Methods

Method Description
toString() Returns a hex string like "0x7ffd0000"
add(val) Returns a new NativePointer with address + val
sub(val) Returns a new NativePointer with address - val
xor(val) Returns a new NativePointer with address ^ val
shr(val) Returns a new NativePointer with address >> val
shl(val) Returns a new NativePointer with address << val

Numeric reads

All read methods throw TypeError on failure.

Method Return type
readS8() signed 8-bit integer
readU8() unsigned 8-bit integer
readS16() signed 16-bit integer
readU16() unsigned 16-bit integer
readS32() signed 32-bit integer
readU32() unsigned 32-bit integer
readS64() signed 64-bit integer
readU64() unsigned 64-bit integer
readFloat() 32-bit float
readDouble() 64-bit float
readPointer() NativePointer (pointer-sized read)

Numeric writes

All write methods throw TypeError on failure. They return undefined on success.

Method Argument
writeS8(val) signed 8-bit integer
writeU8(val) unsigned 8-bit integer
writeS16(val) signed 16-bit integer
writeU16(val) unsigned 16-bit integer
writeS32(val) signed 32-bit integer
writeU32(val) unsigned 32-bit integer
writeS64(val) signed 64-bit integer
writeU64(val) unsigned 64-bit integer
writeFloat(val) 32-bit float
writeDouble(val) 64-bit float

Byte arrays

  • readByteArray(length) — reads length bytes and returns an ArrayBuffer. Throws if length <= 0 or if it exceeds the 1 MB cap.
  • writeByteArray(bytes) — accepts an ArrayBuffer or Uint8Array and writes it to memory. Throws if the payload exceeds the 1 MB cap.

Strings

  • readUtf8String(size?) — Without size: reads up to 2048 bytes and returns a null-terminated UTF-8 string (throws TypeError if no null terminator found). With size: reads exactly size bytes and returns them as a string (no null terminator required).
  • writeUtf8String(string) — writes a string plus a null terminator. Capped at 2048 bytes.
  • readCString(size?) — Without size: reads up to 2048 bytes and returns a null-terminated C string (throws TypeError if no null terminator found). With size: reads exactly size bytes and returns them as a string (no null terminator required).

hexdump(target, options?)

Returns a formatted hexdump string of a NativePointer or ArrayBuffer.

  • target: NativePointer or ArrayBuffer to dump
  • options (optional object):
    • address: NativePointer or number for display base address (default: target's address or 0)
    • offset: number, byte offset to start from (default: 0)
    • length: number, how many bytes to dump (default: 256 for NativePointer, all bytes for ArrayBuffer)
    • header: boolean, whether to include the header row (default: true)
  • Returns: string

Example output:

Address           Hex                                          ASCII
00000000  48 65 6c 6c 6f 20 57 6f  72 6c 64 21 0a 00 01 02  Hello World!....

Example usage:

// Dump memory at a NativePointer
const p = ptr(0x7ffd0000);
console.log(hexdump(p, { length: 32 }));

// Dump an ArrayBuffer
const buf = new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f]).buffer;
console.log(hexdump(buf));

Memory

Global object exposing memory-related utilities.

  • Memory.alloc(size) — allocates size bytes in the quickmem process and returns a NativePointer. The memory is zero-initialized. Maximum allocation is 1MB.
  • Memory.scanSync(address, size, pattern) — scans a memory range for a byte pattern and returns an array of matches. The pattern is a hex string with optional wildcards (??).
    • address: NativePointer or number — start address
    • size: number — bytes to scan
    • pattern: string — hex pattern like "7f 45 4c 46" or "00 ?? 13 37 ?? 42"
    • Returns: [{ address: NativePointer, size: number }, ...]
const m = Process.findModuleByName('libc.so');
const matches = Memory.scanSync(m.base, m.size, '7f 45 4c 46');
matches.forEach(match => {
    console.log('Found at:', match.address.toString());
});

Process

Global object for process introspection.

  • Process.findModuleByName(name) — searches /proc/<pid>/maps for a shared library matching name. Returns an object with:

    • base: NativePointer to the module base address
    • size: total mapped size in bytes
    • name: the queried name
    • path: full filesystem path to the library

    Returns null if the module is not found.

  • Process.pointerSize() — returns the size of a pointer in bytes (4 on 32-bit, 8 on 64-bit).

const m = Process.findModuleByName('libc.so');
if (m) {
    console.log('Base:', m.base.toString());
    console.log('Size:', m.size);
    console.log('Path:', m.path);
}

console

  • console.log(...args) — prints arguments to stdout, separated by spaces, followed by a newline. Non-string values are coerced to strings.

Error behavior

All API failures throw TypeError with a descriptive message. Messages include translated errno values when a memory operation fails:

  • EPERM: insufficient privileges to access process memory
  • ESRCH: target process does not exist or is a zombie
  • EFAULT: invalid memory address
  • EINVAL: invalid argument or alignment issue

Invalid PID arguments at the CLI level are rejected before any JS evaluation begins.

Extending

To add a new NativePointer method:

  1. Implement the C++ function in app/src/main/cpp/quickmem.cpp:
static JSValue js_my_method(JSContext* ctx, JSValue this_val, int argc, JSValue* argv) {
    NativePointerData* data = static_cast<NativePointerData*>(
        JS_GetOpaque(this_val, js_native_pointer_class_id));
    if (!data) {
        return JS_ThrowTypeError(ctx, "Not a NativePointer");
    }

    // Validate arguments, perform work, and return a value
    // Use JS_ThrowTypeError(ctx, "...") for errors
    return JS_UNDEFINED;
}
  1. Register it on the prototype inside quickjs_init():
JS_SetPropertyStr(g_ctx, np_proto, "myMethod",
    JS_NewCFunction(g_ctx, js_my_method, "myMethod", 1));
  1. Follow existing patterns:
    • Extract opaque data with JS_GetOpaque
    • Validate argument count and types
    • Return a new object or primitive, or throw on error
    • Do not free values after attaching them with JS_SetPropertyStr (QuickJS-ng v0.14.0 does not increment refcount on property set)

To add a new global function or object, use JS_SetPropertyStr(g_ctx, global_obj, "myGlobal", ...).

License

MIT

About

A lightweight, JavaScript-powered memory inspection and manipulation tool for Android, built around the QuickJS engine

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages