feat: integrate ghidra-cli v0.1
This commit is contained in:
commit
ffab80a1f2
52 changed files with 6689 additions and 126 deletions
271
tests/support/fake_child.rs
Normal file
271
tests/support/fake_child.rs
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
#![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<dyn std::error::Error>> {
|
||||
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::<u64>()?;
|
||||
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::<usize>()?;
|
||||
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<std::ffi::OsString>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue