Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ formatters:
settings:
goimports:
local-prefixes:
- memodroid
- memdroid

linters:
enable:
Expand Down
22 changes: 19 additions & 3 deletions docs/api.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
# API Reference

All REST endpoints accept and return JSON. POST endpoints read the request body as JSON.
The WebSocket endpoint is at `/ws/watch`.
The WebSocket endpoint is at `/ws/watch`. Each endpoint is gated to a single
HTTP method (GET for reads, POST for mutations); other methods return `405`.
Request bodies are capped at 1 MiB.

Base URL: `http://localhost:8080`
Base URL: `http://localhost:8080` (the server binds to loopback by default;
use `-addr` to change it).

## Authentication

By default there is no auth and the server listens only on `127.0.0.1`. To
expose it safely, start with `-token <secret>` (or set `MEMDROID_TOKEN`). All
`/api` and `/ws` requests must then present the token via a `token` query
parameter, an `Authorization: Bearer <token>` header, or the `mdtoken` cookie.
Opening `http://<host>:<port>/?token=<secret>` once sets the cookie so the Web
UI keeps working.

## Status

Expand Down Expand Up @@ -57,7 +69,11 @@ Value types: `int32`, `int64`, `float32`, `float64`, `uint32`, `uint64`, `bytes`
|--------|------|------|-------------|
| POST | `/api/pointer/scan` | `{"addr":"0x7f...","max_depth":5,"max_offset":2048}` | Find pointer chains to address |

Response: `{"target":"0x...","chains":[{"base":"0x...","label":"libXX.so","offsets":[32,8],"path":"[libXX.so+0x1234]+0x20+0x8"}]}`
Response: `{"target":"0x...","chains":[{"base":"0x...","label":"libXX.so","base_offset":"0x1234","offsets":[32,8],"path":"[libXX.so+0x1234]+0x20+0x8"}]}`

Offsets are in base→final application order. `/api/pointer/resolve` takes
`{"label":"libXX.so","base_offset":"0x1234","offsets":[32,8]}` and walks the
chain in the current process, returning `{"resolved":"0x..."}`.

## Memory

Expand Down
45 changes: 24 additions & 21 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
## Overview

```
./memodroid
./memdroid
├── CLI (main goroutine) — interactive menu
├── HTTP server (:8080) — Web UI + REST API + WebSocket
└── app.State (shared, mutex)
Expand Down Expand Up @@ -46,44 +46,47 @@ internal/
│ ├── pattern.go # Byte pattern search with ?? wildcard, chunked reads
│ └── string.go # UTF-8 / UTF-16LE string search via SearchPattern
├── pointer/
│ └── pointer.go # Pointer chain scan — find stable base+offset paths
│ ├── pointer.go # Pointer chain scan — find stable base+offset paths
│ └── resolve.go # Re-resolve a saved chain against a new run
├── modify/
│ ├── modify.go # Write value / string to address
│ ├── undo.go # Save previous value before modify, revert stack
│ ├── freeze.go # Background goroutine to hold values + FreezeAll
│ └── dump.go # Hex dump memory region to file
│ ├── undo.go # Save previous value before modify, revert stack (mutex-guarded)
│ ├── freeze.go # Freezer — periodic re-write, backed by poller.Pool
│ ├── snapshot.go # Capture / diff a memory region
│ └── dump.go # Hex dump memory region to file (bulk ReadRegion)
├── watch/
│ └── watch.go # Background goroutine; notifies on value change via BroadcastFunc
│ ├── watch.go # Watcher — poll for value change, backed by poller.Pool
│ └── alert.go # AlertWatcher — conditional watch + auto-write
└── store/
├── bookmark.go # Named address bookmarks with bulk modify
└── save.go # Save / Load JSON state (bookmarks + candidates)
├── cheatengine.go # Import CheatEngine .CT tables as bookmarks
└── save.go # Save / Load versioned JSON state (bookmarks + candidates)

internal/poller/
└── poller.go # Keyed goroutine manager shared by Freezer/Watcher/AlertWatcher
```

## CLI Source Layout

The root-level `package main` is split into focused files:

```
main.go # main() entry point, REPL dispatch loop, printMenu()
cli_helpers.go # prompt(), parseAddr(), parseValue(), require* guards
cli_device.go # handleSelectDevice, handleConnectWifi, handleDisconnectWifi
cli_process.go # doAttach, handleAttach, handleAttachByName, handleDetach
cli_search.go # handleSetValueType, handleSearchFiltered, handleShowCandidates
cli_memory.go # handleWatch, handleDump, handlePointerScan, handleShowMaps, handleBookmarkList
```
The interactive menu lives in `package cli` under `internal/cli/`, one file per
concern (`run.go` dispatch loop, `prompt.go` input helpers, `device.go`,
`process.go`, `search.go`, `memory.go`, `pointer.go`, `alert.go`,
`bookmarks.go`, `menu.go`). `main.go` wires flags, the ADB driver, shared
state, and the HTTP server, then hands off to `cli.Run`.

## Dependency Graph

```
main (cli_*.go)
main
├── app.State
├── cli
├── driver/adb (no internal deps — exec.Command("adb", ...) only)
├── process → driver
├── server → app, driver/adb, memory/*, server/wswatch
├── server → app, driver, driver/adb, memory/*, server/wswatch
├── memory/search → driver
├── memory/pointer → driver
├── memory/modify → driver, memory/search
├── memory/watch → driver, memory/search
├── memory/modify → driver, memory/search, poller
├── memory/watch → driver, memory/search, poller
└── memory/store → memory/search
```

Expand Down
6 changes: 3 additions & 3 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@

```bash
go build -o memdroid .
./memodroid
./memdroid
```

No root on the host is required. All privileged operations run on the device via `adb shell su`.

## Connecting a Device

### USB
Connect the device and run MemoDroid. It auto-selects if only one device is found.
Connect the device and run memdroid. It auto-selects if only one device is found.
If multiple devices are connected, you will be prompted to select one.

### Wi-Fi ADB
Expand Down Expand Up @@ -157,7 +157,7 @@ Switch type with menu `6`. Changing type resets the search session.

## Web UI

Open `http://localhost:8080` while MemoDroid is running. All features are available.
Open `http://localhost:8080` while memdroid is running. All features are available.

Special Web UI features:
- **Watch panel**: real-time value change stream via WebSocket
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module memodroid
module memdroid

go 1.26

Expand Down
10 changes: 5 additions & 5 deletions internal/app/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ package app
import (
"sync"

"memodroid/internal/driver"
"memodroid/internal/memory/modify"
"memodroid/internal/memory/search"
"memodroid/internal/memory/store"
"memodroid/internal/memory/watch"
"memdroid/internal/driver"
"memdroid/internal/memory/modify"
"memdroid/internal/memory/search"
"memdroid/internal/memory/store"
"memdroid/internal/memory/watch"
)

// AttachedProcess holds info about an attached process.
Expand Down
4 changes: 2 additions & 2 deletions internal/cli/alert.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import (
"fmt"
"time"

"memodroid/internal/app"
"memodroid/internal/memory/watch"
"memdroid/internal/app"
"memdroid/internal/memory/watch"
)

// DefaultAlertPollInterval is the default polling interval for alert watches.
Expand Down
4 changes: 2 additions & 2 deletions internal/cli/bookmarks.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package cli
import (
"fmt"

"memodroid/internal/app"
"memodroid/internal/memory/store"
"memdroid/internal/app"
"memdroid/internal/memory/store"
)

func BookmarkList(st *app.State) {
Expand Down
2 changes: 1 addition & 1 deletion internal/cli/device.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"fmt"
"strconv"

"memodroid/internal/driver/adb"
"memdroid/internal/driver/adb"
)

func SelectDevice(d *adb.ADB) {
Expand Down
4 changes: 2 additions & 2 deletions internal/cli/memory.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import (
"strconv"
"time"

"memodroid/internal/app"
"memodroid/internal/memory/modify"
"memdroid/internal/app"
"memdroid/internal/memory/modify"
)

// DefaultWatchInterval is the default polling interval when watching an address.
Expand Down
12 changes: 6 additions & 6 deletions internal/cli/menu.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ package cli
import (
"fmt"

"memodroid/internal/app"
"memodroid/internal/driver/adb"
"memdroid/internal/app"
"memdroid/internal/driver/adb"
)

// ServerAddr is displayed in the menu header. Set by the caller before rendering.
var ServerAddr = ":8080"
// ServerURL is displayed in the menu header. Set by the caller before rendering.
var ServerURL = "http://localhost:8080"

type menuSection struct {
title string
Expand Down Expand Up @@ -96,7 +96,7 @@ func PrintMenu(st *app.State, d *adb.ADB) {
vt := st.GetValueType()
sess := st.GetSession()

fmt.Println("\n=== MemoDroid ===")
fmt.Println("\n=== memdroid ===")
serial := d.DeviceSerial()
if serial == "" {
serial = "(none)"
Expand All @@ -112,7 +112,7 @@ func PrintMenu(st *app.State, d *adb.ADB) {
}
fmt.Println()
}
fmt.Printf("Web UI: http://localhost%s\n", ServerAddr)
fmt.Printf("Web UI: %s\n", ServerURL)
for _, section := range menu {
fmt.Printf("--- %s ---\n", section.title)
for _, it := range section.items {
Expand Down
18 changes: 14 additions & 4 deletions internal/cli/pointer.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import (
"strconv"
"strings"

"memodroid/internal/app"
"memodroid/internal/memory/pointer"
"memdroid/internal/app"
"memdroid/internal/memory/pointer"
)

func PointerScan(st *app.State) {
Expand Down Expand Up @@ -50,6 +50,15 @@ func PointerResolve(st *app.State) {
fmt.Println("Module name required")
return
}
var baseOffset uintptr
if s := Prompt("Base offset (hex, e.g. 0x1234): "); s != "" {
v, err := strconv.ParseUint(strings.TrimPrefix(s, "0x"), 16, 64)
if err != nil {
fmt.Println("Invalid base offset")
return
}
baseOffset = uintptr(v)
}
offsetsStr := Prompt("Offsets (comma-separated hex, e.g. 0x10,0x20,0x8): ")
if offsetsStr == "" {
fmt.Println("Offsets required")
Expand All @@ -61,8 +70,9 @@ func PointerResolve(st *app.State) {
return
}
chain := pointer.Chain{
BaseLabel: label,
Offsets: offsets,
BaseLabel: label,
BaseOffset: baseOffset,
Offsets: offsets,
}
resolved, err := pointer.ResolveChain(st.GetDriver(), st.GetPID(), chain)
if err != nil {
Expand Down
6 changes: 3 additions & 3 deletions internal/cli/process.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import (
"fmt"
"strconv"

"memodroid/internal/app"
"memodroid/internal/driver/adb"
"memodroid/internal/memory/search"
"memdroid/internal/app"
"memdroid/internal/driver/adb"
"memdroid/internal/memory/search"
)

func doAttach(st *app.State, pid int, name string) {
Expand Down
2 changes: 1 addition & 1 deletion internal/cli/prompt.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"strconv"
"strings"

"memodroid/internal/memory/search"
"memdroid/internal/memory/search"
)

var stdinReader = bufio.NewReader(os.Stdin)
Expand Down
20 changes: 10 additions & 10 deletions internal/cli/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import (
"os"
"strconv"

"memodroid/internal/app"
"memodroid/internal/driver"
"memodroid/internal/driver/adb"
"memodroid/internal/memory/modify"
"memodroid/internal/memory/search"
"memodroid/internal/memory/store"
"memodroid/internal/process"
"memdroid/internal/app"
"memdroid/internal/driver"
"memdroid/internal/driver/adb"
"memdroid/internal/memory/modify"
"memdroid/internal/memory/search"
"memdroid/internal/memory/store"
"memdroid/internal/process"
)

// DefaultStateFile is the default filename used by Save/Load State.
Expand Down Expand Up @@ -324,7 +324,7 @@ func modifyAt(st *app.State, drv driver.Driver, pid int, vt search.ValueType) {
if !ok {
return
}
if err := st.UndoStack.WithUndo(drv, pid, addr, val, vt); err != nil {
if err := st.UndoStack.WithUndo(drv, pid, addr, val); err != nil {
fmt.Printf("Modify failed: %v\n", err)
} else {
fmt.Printf("Modified 0x%x (undo available, depth: %d)\n", addr, st.UndoStack.Depth())
Expand Down Expand Up @@ -382,8 +382,8 @@ func loadSession(st *app.State) {
if path == "" {
path = DefaultStateFile
}
loaded := st.GetSession()
if err := store.LoadState(path, st.GetBookmarks(), &loaded); err != nil {
loaded, err := store.LoadState(path, st.GetBookmarks())
if err != nil {
fmt.Printf("Load failed: %v\n", err)
return
}
Expand Down
6 changes: 3 additions & 3 deletions internal/cli/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ package cli
import (
"fmt"

"memodroid/internal/app"
"memodroid/internal/driver"
"memodroid/internal/memory/search"
"memdroid/internal/app"
"memdroid/internal/driver"
"memdroid/internal/memory/search"
)

// MaxCandidatesDisplay caps how many search candidates are printed.
Expand Down
2 changes: 1 addition & 1 deletion internal/driver/adb/adb.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"strings"
"sync"

"memodroid/internal/driver"
"memdroid/internal/driver"
)

// execErr enriches an exec error with the stderr output from the failed command.
Expand Down
2 changes: 1 addition & 1 deletion internal/driver/adb/maps.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"fmt"
"strings"

"memodroid/internal/driver"
"memdroid/internal/driver"
)

// ReadMaps returns all rw memory regions for pid.
Expand Down
Loading