#![forbid(unsafe_code)] #![allow(missing_docs)] use std::{ env, fs::{self, OpenOptions}, io::{self, Read as _, Write as _}, net::TcpStream, os::unix::fs::OpenOptionsExt as _, path::{Path, PathBuf}, process::ExitCode, thread, time::Duration, }; fn main() -> ExitCode { match execute() { Ok(()) => ExitCode::SUCCESS, Err(error) => { let _ignored = writeln!(io::stderr().lock(), "fake child: {error}"); ExitCode::FAILURE } } } fn execute() -> Result<(), Box> { let mut arguments = env::args_os().skip(1); let behavior = arguments.next().ok_or("missing behavior")?; if behavior == "-version" { writeln!(io::stderr().lock(), "openjdk version \"21\"")?; return Ok(()); } if behavior == "exit-success" { return Ok(()); } if behavior == "network-probe" { let address = arguments.next().ok_or("missing probe address")?; TcpStream::connect(address.to_str().ok_or("probe address is not UTF-8")?)?; return Ok(()); } let behavior_text = behavior.to_str().ok_or("behavior is not UTF-8")?; if !matches!( behavior_text, "success" | "mismatch" | "temporary-only" | "no-response" | "sleep" | "flood" ) { return analyze_headless(PathBuf::from(behavior), arguments.collect()); } let request = PathBuf::from(arguments.next().ok_or("missing request path")?); let response = PathBuf::from(arguments.next().ok_or("missing response path")?); match behavior_text { "success" => write_response(&request, &response, false, true), "mismatch" => write_response(&request, &response, true, true), "temporary-only" => write_response(&request, &response, false, false), "no-response" => Ok(()), "sleep" => { let milliseconds = arguments .next() .ok_or("missing sleep milliseconds")? .to_str() .ok_or("sleep value is not UTF-8")? .parse::()?; thread::sleep(Duration::from_millis(milliseconds)); write_response(&request, &response, false, true) } "flood" => { let count = arguments .next() .ok_or("missing flood byte count")? .to_str() .ok_or("flood value is not UTF-8")? .parse::()?; write_repeated(io::stdout().lock(), b'o', count)?; write_repeated(io::stderr().lock(), b'e', count)?; write_response(&request, &response, false, true) } _ => Err("unknown behavior".into()), } } fn analyze_headless( project_directory: PathBuf, arguments: Vec, ) -> Result<(), Box> { if arguments.len() < 3 { return Err("fake analyzeHeadless received too few arguments".into()); } let project_name = arguments[0].to_str().ok_or("project name is not UTF-8")?; if arguments.iter().any(|argument| argument == "-import") { fs::create_dir_all(project_directory.join(format!("{project_name}.rep")))?; fs::write( project_directory.join(format!("{project_name}.gpr")), b"fake immutable Ghidra project", )?; fs::write( project_directory .join(format!("{project_name}.rep")) .join("program"), b"fake analyzed program", )?; } let request = PathBuf::from(&arguments[arguments.len() - 2]); let response = PathBuf::from(&arguments[arguments.len() - 1]); let request_value: serde_json::Value = serde_json::from_slice(&fs::read(&request)?)?; if request_value["staged_sample"] .as_str() .and_then(|path| fs::read(path).ok()) .is_some_and(|bytes| bytes.starts_with(b"interruptible sample")) { thread::sleep(Duration::from_secs(5)); } write_response(&request, &response, false, true) } fn write_repeated(mut writer: impl io::Write, byte: u8, mut remaining: usize) -> io::Result<()> { let buffer = vec![byte; 64 * 1024]; while remaining > 0 { let count = remaining.min(buffer.len()); writer.write_all(&buffer[..count])?; remaining -= count; } writer.flush() } fn write_response( request_path: &Path, response_path: &Path, mismatch: bool, publish: bool, ) -> Result<(), Box> { let mut request_bytes = Vec::new(); fs::File::open(request_path)?.read_to_end(&mut request_bytes)?; let request: serde_json::Value = serde_json::from_slice(&request_bytes)?; let invocation_id = if mismatch { serde_json::Value::String("ffffffffffffffffffffffffffffffff".to_owned()) } else { request["invocation_id"].clone() }; let response = serde_json::json!({ "protocol_version": request["protocol_version"], "invocation_id": invocation_id, "operation": request["operation"], "result": if fake_error(&request) { serde_json::json!({ "status": "error", "code": "analysis_failed", "message": "controllable fake worker error", "details": {} }) } else { serde_json::json!({ "status": "success", "data": fake_data(&request) }) } }); let bytes = serde_json::to_vec(&response)?; let temporary = response_path.with_file_name("response.json.tmp"); let mut file = OpenOptions::new() .write(true) .create_new(true) .mode(0o600) .open(&temporary)?; file.write_all(&bytes)?; file.sync_all()?; if publish { fs::rename(temporary, response_path)?; } Ok(()) } fn fake_error(request: &serde_json::Value) -> bool { request["staged_sample"] .as_str() .and_then(|path| fs::read(path).ok()) .is_some_and(|bytes| bytes.starts_with(b"other sample")) } fn fake_data(request: &serde_json::Value) -> serde_json::Value { match request["operation"].as_str() { Some("doctor") => serde_json::json!({ "ready": true, "ghidra_version": "12.1.2", "java_version": 21, "adapter_protocol_version": 1 }), Some("inspect") => query_data(serde_json::json!({ "image_base": address("0x0000000000400000"), "minimum_address": address("0x0000000000400000"), "maximum_address": address("0x0000000000400fff"), "function_count": 1 })), Some("functions") => { let offset = request["arguments"]["page"]["offset"].as_u64().unwrap_or(0); let items = if offset == 0 { vec![serde_json::json!({ "name": "main", "qualified_name": "main", "entry": address("0x0000000000401000"), "body_address_count": 32, "location": "memory", "is_external": false, "is_thunk": false, "thunk_target_entry": null, "decompilable": true })] } else { Vec::new() }; let limit = match &request["arguments"]["page"]["limit"] { serde_json::Value::String(value) if value == "all" => serde_json::Value::Null, value => value["bounded"].clone(), }; query_data(serde_json::json!({ "page": { "order": "location_then_entry_ascending", "offset": offset, "limit": limit, "returned": items.len(), "total": 1, "has_more": false }, "items": items })) } Some("decompile") => { let requested = request["arguments"]["selector"]["value"] .as_str() .unwrap_or("main"); let text = if requested == "large" { "x".repeat(100_000) } else { "int main(void) {\n return 0;\n}\n".to_owned() }; query_data(serde_json::json!({ "requested_selector": request["arguments"]["selector"], "function": { "name": requested, "qualified_name": requested, "entry": address("0x0000000000401000") }, "decompilation": { "syntax": "ghidra_c", "text": text } })) } _ => serde_json::json!({}), } } fn query_data(query: serde_json::Value) -> serde_json::Value { serde_json::json!({ "context": { "ghidra_version": "12.1.2", "java_version": 21, "target": { "loader": "ElfLoader", "format": "ELF", "processor_language": "x86:LE:64:default", "compiler_specification": "gcc" }, "loader_options": [], "analyzer_options": [] }, "query": query }) } fn address(offset: &str) -> serde_json::Value { serde_json::json!({"space": "ram", "offset": offset}) }