Sign in Sign up
kretrod/lodestone Public
Branches
master
560 lines (525 loc) · 19.2 KB Raw
//! Lodestone — a fully external Minecraft trainer for the Java edition.
//!
//! No DLL injection, no agent, no mod: the game runs stock. Everything here is
//! ReadProcessMemory/WriteProcessMemory plus an understanding of how HotSpot
//! lays out its own heap, which HotSpot itself tells us via VMStructs.

#![cfg(windows)]

use lodestone_core::game::{Cheats, Game};
use lodestone_core::jvm::{JVal, Jvm};
use lodestone_core::vm;
use lodestone_core::win::{self, Process, Result};

fn main() {
    let args: Vec<String> = std::env::args().skip(1).collect();
    let cmd = args.first().map(|s| s.as_str()).unwrap_or("help");
    let rest: Vec<String> = args.iter().skip(1).cloned().collect();

    let r = match cmd {
        "procs" => cmd_procs(),
        "probe" => cmd_probe(&rest),
        "classes" => cmd_classes(&rest),
        "class" => cmd_class(&rest),
        "get" => cmd_get(&rest),
        "set" => cmd_set(&rest),
        "dump" => cmd_dump(&rest),
        "stream" => cmd_stream(&rest),
        "vmtype" => cmd_vmtype(&rest),
        "methods" => cmd_methods(&rest),
        "watch" => cmd_watch(&rest),
        "find" => cmd_find(&rest),
        "trainer" => cmd_trainer(&rest),
        "obj" => cmd_obj(&rest),
        "help" | "-h" | "--help" => {
            usage();
            Ok(())
        }
        other => Err(format!("unknown command {other:?}; try `lodestone help`")),
    };

    if let Err(e) = r {
        eprintln!("error: {e}");
        std::process::exit(1);
    }
}

fn usage() {
    println!(
        "lodestone — external HotSpot/Minecraft navigator (no injection)\n\
         \n\
         USAGE:\n\
           lodestone procs                        list candidate JVM processes\n\
           lodestone probe                        dump the VMStructs database\n\
           lodestone classes <substring>          find loaded classes\n\
           lodestone class <name>                 dump a class's fields\n\
           lodestone get <Class> <field> [path…]  read a static field, then walk\n\
           lodestone set <Class> <field> [path…] <value>\n\
         \n\
         Every command takes --pid N to choose the JVM.\n"
    );
}

/// A JVM running the game loads GLFW/LWJGL; a build server's JVM does not.
fn looks_like_minecraft(p: &Process) -> bool {
    p.modules()
        .map(|ms| {
            ms.iter().any(|m| {
                let n = m.name.to_ascii_lowercase();
                n.starts_with("lwjgl") || n.starts_with("glfw")
            })
        })
        .unwrap_or(false)
}

fn find_java_procs() -> Result<Vec<(u32, String, bool)>> {
    let mut out = Vec::new();
    for p in win::list_processes()? {
        let n = p.name.to_ascii_lowercase();
        if n == "javaw.exe" || n == "java.exe" {
            let mc = Process::attach(p.pid)
                .map(|h| looks_like_minecraft(&h))
                .unwrap_or(false);
            out.push((p.pid, p.name, mc));
        }
    }
    Ok(out)
}

fn cmd_procs() -> Result<()> {
    let procs = find_java_procs()?;
    if procs.is_empty() {
        println!("no JVM processes found");
        return Ok(());
    }
    for (pid, name, mc) in procs {
        println!("{:>8}  {:<12} {}", pid, name, if mc { "<- Minecraft" } else { "" });
    }
    Ok(())
}

/// Strip `--pid N` out of the argument list and return it separately.
fn split_pid(args: &[String]) -> (Option<u32>, Vec<String>) {
    let mut pid = None;
    let mut rest = Vec::new();
    let mut i = 0;
    while i < args.len() {
        if args[i] == "--pid" && i + 1 < args.len() {
            pid = args[i + 1].parse().ok();
            i += 2;
        } else {
            rest.push(args[i].clone());
            i += 1;
        }
    }
    (pid, rest)
}

fn open(pid: Option<u32>) -> Result<Process> {
    if let Some(pid) = pid {
        return Process::attach(pid);
    }
    let procs = find_java_procs()?;
    let mc: Vec<_> = procs.iter().filter(|(_, _, mc)| *mc).collect();
    match mc.len() {
        1 => Process::attach(mc[0].0),
        0 => Err("no Minecraft JVM found (is the game running?) — use --pid".into()),
        _ => Err(format!("{} Minecraft JVMs found; pass --pid", mc.len())),
    }
}

/// Attach and index every loaded class.
fn open_jvm(pid: Option<u32>) -> Result<Jvm> {
    let p = open(pid)?;
    let m = p.module("jvm.dll")?;
    let mut j = Jvm::attach(p, &m)?;
    j.load_classes()?;
    Ok(j)
}

fn cmd_probe(args: &[String]) -> Result<()> {
    let (pid, _) = split_pid(args);
    let p = open(pid)?;
    println!("attached to pid {}", p.pid());

    let jvm = p.module("jvm.dll")?;
    println!("jvm.dll  base {:#x}  size {:#x}\n         {}", jvm.base, jvm.size, jvm.path);

    let db = vm::VmDb::load(&p, &jvm)?;
    println!(
        "VMStructs: {} fields, {} types, {} int consts, {} long consts",
        db.fields.len(),
        db.types.len(),
        db.int_consts.len(),
        db.long_consts.len()
    );

    let oop_base = p.u64(db.static_addr("CompressedOops", "_base")?)?;
    let oop_shift = p.i32(db.static_addr("CompressedOops", "_shift")?)?;
    let klass_base = p.u64(db.static_addr("CompressedKlassPointers", "_base")?)?;
    let klass_shift = p.i32(db.static_addr("CompressedKlassPointers", "_shift")?)?;
    println!("\ncompressed oops   base {:#x}  shift {}", oop_base, oop_shift);
    println!("compressed klass  base {:#x}  shift {}", klass_base, klass_shift);

    let mut j = Jvm::attach(p, &jvm)?;
    let n = j.load_classes()?;
    println!("\nloaded classes: {n}");
    Ok(())
}

fn cmd_classes(args: &[String]) -> Result<()> {
    let (pid, rest) = split_pid(args);
    let needle = rest.first().map(|s| s.replace('.', "/")).unwrap_or_default();
    let j = open_jvm(pid)?;
    let mut names: Vec<&String> = j
        .classes()
        .keys()
        .filter(|n| needle.is_empty() || n.contains(&needle))
        .collect();
    names.sort();
    for n in names.iter().take(2000) {
        println!("{n}");
    }
    println!("\n{} of {} loaded classes", names.len(), j.classes().len());
    Ok(())
}

fn cmd_class(args: &[String]) -> Result<()> {
    let (pid, rest) = split_pid(args);
    let name = rest.first().ok_or("usage: class <name>")?;
    let mut j = open_jvm(pid)?;
    let k = j
        .class(name)
        .ok_or_else(|| format!("class {name} is not loaded"))?;
    println!("{} -> Klass* {:#x}", j.klass_name(k)?, k);
    let mirror = j.mirror(k)?;
    println!("mirror (java.lang.Class) @ {:#x}\n", mirror);

    for (owner, f) in j.all_fields(k)? {
        let kindmark = if f.is_static() { "static" } else { "      " };
        let val = if f.is_static() && mirror != 0 {
            j.read_at(mirror, &f).map(|v| v.to_string()).unwrap_or_else(|e| format!("<{e}>"))
        } else {
            String::new()
        };
        println!(
            "  {} +{:<5} {:<34} {:<28} {} {}",
            kindmark,
            f.offset,
            f.name,
            f.sig,
            if f.injected { "[vm]" } else { "    " },
            val
        );
        let _ = owner;
    }
    Ok(())
}

/// Headless driver for the engine: the same code the menu runs, without a GUI.
fn cmd_trainer(args: &[String]) -> Result<()> {
    let (pid, rest) = split_pid(args);
    let mut c = Cheats::default();
    let mut ticks = 40usize;
    let mut i = 0;
    while i < rest.len() {
        let a = rest[i].as_str();
        let mut val = |d: f32| -> f32 { rest.get(i + 1).and_then(|s| s.parse().ok()).unwrap_or(d) };
        match a {
            "--fly" => c.fly = true,
            "--noclip" => c.noclip = true,
            "--nofall" => c.nofall = true,
            "--god" => c.god = true,
            "--instabuild" => c.instabuild = true,
            "--fast-break" => c.fast_break = true,
            "--freeze-time" => c.freeze_time = true,
            "--fly-speed" => {
                c.fly = true;
                c.fly_speed = val(0.05);
                i += 1;
            }
            "--walk-speed" => {
                c.speed = true;
                c.walk_speed = val(0.1);
                i += 1;
            }
            "--rate" => {
                c.time_rate = rest.get(i + 1).and_then(|s| s.parse().ok());
                i += 1;
            }
            "--time" => {
                c.set_time = rest.get(i + 1).and_then(|s| s.parse().ok());
                i += 1;
            }
            "--ticks" => {
                ticks = rest.get(i + 1).and_then(|s| s.parse().ok()).unwrap_or(40);
                i += 1;
            }
            "--tp" => {
                let n = |k: usize| rest.get(i + k).and_then(|s| s.parse::<f64>().ok());
                c.teleport = match (n(1), n(2), n(3)) {
                    (Some(x), Some(y), Some(z)) => Some((x, y, z)),
                    _ => return Err("--tp needs x y z".into()),
                };
                i += 3;
            }
            other => return Err(format!("unknown trainer flag {other:?}")),
        }
        i += 1;
    }

    let mut g = Game::attach(pid)?;
    println!("attached to pid {}", g.pid());
    for n in 0..ticks {
        let s = g.tick(&mut c);
        if n == 0 || n == ticks - 1 {
            println!(
                "[{n:>3}] world={} sp={} pos=({:.2}, {:.2}, {:.2}) flying={} mayfly={} \
                 flySpeed={:.3} walkSpeed={:.3} onGround={} day={} {}",
                s.in_world,
                s.single_player,
                s.pos.0,
                s.pos.1,
                s.pos.2,
                s.flying,
                s.may_fly,
                s.fly_speed,
                s.walk_speed,
                s.on_ground,
                s.day_time,
                s.note
            );
        }
        std::thread::sleep(std::time::Duration::from_millis(50));
    }
    Ok(())
}

/// Re-resolve a path from its root every tick and print it when it changes.
/// Never cache the object address: a GC can move it between samples.
fn cmd_watch(args: &[String]) -> Result<()> {
    let (pid, rest) = split_pid(args);
    let mut j = open_jvm(pid)?;
    let mut last = String::new();
    loop {
        let text = match resolve(&mut j, &rest) {
            Ok((JVal::Obj(a), _, _)) if a != 0 => {
                format!("{} @{a:#x}", j.class_name_of(a).unwrap_or_default())
            }
            Ok((v, _, _)) => v.to_string(),
            Err(e) => format!("<{e}>"),
        };
        if text != last {
            println!("{text}");
            last = text;
        }
        std::thread::sleep(std::time::Duration::from_millis(100));
    }
}

/// Walk the object graph from a static root looking for a class by name.
fn cmd_find(args: &[String]) -> Result<()> {
    let (pid, rest) = split_pid(args);
    let mut depth = 6usize;
    let mut path: Vec<String> = Vec::new();
    let mut needle = String::new();
    let mut i = 0;
    while i < rest.len() {
        match rest[i].as_str() {
            "--depth" => {
                depth = rest.get(i + 1).and_then(|s| s.parse().ok()).unwrap_or(6);
                i += 2;
            }
            "--of" => {
                needle = rest.get(i + 1).cloned().unwrap_or_default();
                i += 2;
            }
            _ => {
                path.push(rest[i].clone());
                i += 1;
            }
        }
    }
    if needle.is_empty() {
        return Err("usage: find <Class> <field> [path…] --of <substring> [--depth N]".into());
    }
    let mut j = open_jvm(pid)?;
    let (v, _, _) = resolve(&mut j, &path)?;
    let root = match v {
        JVal::Obj(a) if a != 0 => a,
        other => return Err(format!("root is {other}, not an object")),
    };
    let hits = j.search(root, &needle, depth, 200_000)?;
    for (p, c) in hits.iter().take(200) {
        println!("  .{p}\n      {c}");
    }
    println!("{} matches", hits.len());
    Ok(())
}

/// Dump a class's methods with their JVM descriptors.
fn cmd_methods(args: &[String]) -> Result<()> {
    let (pid, rest) = split_pid(args);
    let name = rest.first().ok_or("usage: methods <class> [name-filter]")?;
    let filter = rest.get(1).map(|s| s.to_lowercase());
    let mut j = open_jvm(pid)?;
    let k = j.class(name).ok_or_else(|| format!("class {name} is not loaded"))?;
    let inherited = rest.iter().any(|a| a == "--all");
    let list = if inherited {
        j.all_methods(k)?
    } else {
        j.methods(k)?
            .into_iter()
            .map(|(n, s)| (String::new(), n, s))
            .collect()
    };
    let mut shown = 0;
    for (owner, n, sig) in &list {
        if let Some(f) = &filter {
            if !n.to_lowercase().contains(f.as_str()) {
                continue;
            }
        }
        if owner.is_empty() {
            println!("  {n}{sig}");
        } else {
            println!("  {n}{sig}    [{owner}]");
        }
        shown += 1;
    }
    println!("{shown} of {} methods", list.len());
    Ok(())
}

/// Show what VMStructs says about one HotSpot C++ type.
fn cmd_vmtype(args: &[String]) -> Result<()> {
    let (pid, rest) = split_pid(args);
    let name = rest.first().ok_or("usage: vmtype <CppType>")?;
    let p = open(pid)?;
    let m = p.module("jvm.dll")?;
    let db = vm::VmDb::load(&p, &m)?;
    if let Some(t) = db.types.get(name.as_str()) {
        println!("type {} : {}  size {}", t.name, if t.superclass.is_empty() { "-" } else { &t.superclass }, t.size);
    }
    for f in db.fields_of(name) {
        if f.is_static {
            println!("  static {:<34} {:<24} @ {:#x}", f.field_name, f.type_string, f.address);
        } else {
            println!("  +{:<5} {:<34} {}", f.offset, f.field_name, f.type_string);
        }
    }
    Ok(())
}

/// Hex-dump a class's raw fieldinfo stream and the u5 values it decodes to.
fn cmd_stream(args: &[String]) -> Result<()> {
    let (pid, rest) = split_pid(args);
    let name = rest.first().ok_or("usage: stream <class>")?;
    let mut j = open_jvm(pid)?;
    let k = j.class(name).ok_or("class not loaded")?;
    let (bytes, vals) = j.fieldinfo_raw(k)?;
    println!("stream: {} bytes", bytes.len());
    for (i, row) in bytes.chunks(16).enumerate() {
        let hex: Vec<String> = row.iter().map(|x| format!("{x:02x}")).collect();
        println!("  {:04x}  {}", i * 16, hex.join(" "));
    }
    println!("\nu5 values: {vals:?}");
    Ok(())
}

/// Raw hex dump of target memory — the escape hatch when a decode looks wrong.
fn cmd_dump(args: &[String]) -> Result<()> {
    let (pid, rest) = split_pid(args);
    let addr = parse_addr(rest.first().ok_or("usage: dump <addr> [len]")?)?;
    let len: usize = rest.get(1).and_then(|s| s.parse().ok()).unwrap_or(64);
    let p = open(pid)?;
    let b = p.read_bytes(addr, len)?;
    for (i, row) in b.chunks(16).enumerate() {
        let hex: Vec<String> = row.iter().map(|x| format!("{x:02x}")).collect();
        let asc: String = row
            .iter()
            .map(|&x| if (0x20..0x7f).contains(&x) { x as char } else { '.' })
            .collect();
        println!("{:#018x}  {:<47}  {}", addr + (i * 16) as u64, hex.join(" "), asc);
    }
    Ok(())
}

/// Identify whatever object lives at an address.
fn cmd_obj(args: &[String]) -> Result<()> {
    let (pid, rest) = split_pid(args);
    let addr = parse_addr(rest.first().ok_or("usage: obj <addr>")?)?;
    let mut j = open_jvm(pid)?;
    let narrow = j.p.u32(addr + j.l.oop_klass)?;
    let klass = j.decode_klass(narrow);
    println!("oop {addr:#x}  narrowKlass {narrow:#x} -> Klass* {klass:#x}");
    println!("class {}", j.klass_name(klass)?);
    let mirror = j.mirror(klass)?;
    for (_, f) in j.all_fields(klass)? {
        if f.is_static() {
            continue;
        }
        let v = j.read_at(addr, &f).map(|v| v.to_string()).unwrap_or_else(|e| format!("<{e}>"));
        println!("  +{:<5} {:<28} {:<24} {}", f.offset, f.name, f.sig, v);
    }
    let _ = mirror;
    Ok(())
}

fn parse_addr(s: &str) -> Result<u64> {
    let t = s.trim_start_matches("0x").trim_start_matches('@');
    u64::from_str_radix(t, 16).map_err(|e| format!("bad address {s:?}: {e}"))
}

/// Resolve `<Class> <staticField> [instance fields…]` to a value.
fn resolve(j: &mut Jvm, path: &[String]) -> Result<(JVal, u64, String)> {
    let class = path.first().ok_or("need a class name")?;
    let first = path.get(1).ok_or("need a field name")?;
    let k = j
        .class(class)
        .ok_or_else(|| format!("class {class} is not loaded"))?;
    let mut cur = j.static_field(k, first)?;
    // `base` is the object the last field lives on, so callers can write it.
    let mut base = j.mirror(k)?;
    let mut last = first.clone();
    for name in &path[2..] {
        let oop = match cur {
            JVal::Obj(0) => return Err(format!("{last} is null")),
            JVal::Obj(a) => a,
            other => return Err(format!("{last} = {other} is not an object")),
        };
        base = oop;
        // `[n]` indexes an object array instead of naming a field.
        if let Some(idx) = name.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
            let i: usize = idx.parse().map_err(|e| format!("bad index {idx:?}: {e}"))?;
            let els = j.obj_array(oop, i + 1)?;
            let el = *els.get(i).ok_or_else(|| format!("index {i} out of bounds"))?;
            cur = JVal::Obj(el);
            last = name.clone();
            continue;
        }
        cur = j.get(oop, name)?;
        last = name.clone();
    }
    Ok((cur, base, last))
}

fn cmd_get(args: &[String]) -> Result<()> {
    let (pid, rest) = split_pid(args);
    let mut j = open_jvm(pid)?;
    let (v, _, name) = resolve(&mut j, &rest)?;
    match v {
        JVal::Obj(a) if a != 0 => {
            let cls = j.class_name_of(a)?;
            if cls == "java/lang/String" {
                println!("{name} = {:?} (String @{a:#x})", j.read_string(a)?);
            } else {
                println!("{name} = {cls} @{a:#x}");
            }
        }
        other => println!("{name} = {other}"),
    }
    Ok(())
}

fn cmd_set(args: &[String]) -> Result<()> {
    let (pid, rest) = split_pid(args);
    if rest.len() < 3 {
        return Err("usage: set <Class> <field> [path…] <value>".into());
    }
    let (path, value) = rest.split_at(rest.len() - 1);
    let value = &value[0];
    let mut j = open_jvm(pid)?;
    let (old, base, name) = resolve(&mut j, path)?;

    // Type the new value the same way the old one was typed.
    let new = match old {
        JVal::Bool(_) => JVal::Bool(matches!(value.as_str(), "true" | "1")),
        JVal::Byte(_) => JVal::Byte(value.parse().map_err(|e| format!("{e}"))?),
        JVal::Char(_) => JVal::Char(value.chars().next().unwrap_or('?') as u16),
        JVal::Short(_) => JVal::Short(value.parse().map_err(|e| format!("{e}"))?),
        JVal::Int(_) => JVal::Int(value.parse().map_err(|e| format!("{e}"))?),
        JVal::Long(_) => JVal::Long(value.parse().map_err(|e| format!("{e}"))?),
        JVal::Float(_) => JVal::Float(value.parse().map_err(|e| format!("{e}"))?),
        JVal::Double(_) => JVal::Double(value.parse().map_err(|e| format!("{e}"))?),
        JVal::Obj(_) => return Err("refusing to write an object reference".into()),
    };

    let klass = j.klass_of(base).unwrap_or(0);
    let f = if klass != 0 && j.class_name_of(base).map(|c| c != "java/lang/Class").unwrap_or(false) {
        j.find_field(klass, &name)?
    } else {
        // `base` is a mirror: the field is static on the class it mirrors.
        let class = &path[0];
        let k = j.class(class).ok_or("class not loaded")?;
        j.find_field(k, &name)?
    };
    j.write_at(base, &f, new)?;
    println!("{name}: {old} -> {new}");
    Ok(())
}