Skip to content

Instantly share code, notes, and snippets.

@liquidhelium
Created February 8, 2026 19:02
Show Gist options
  • Select an option

  • Save liquidhelium/0b03392e4e2ddd8c9600b2c89e55e300 to your computer and use it in GitHub Desktop.

Select an option

Save liquidhelium/0b03392e4e2ddd8c9600b2c89e55e300 to your computer and use it in GitHub Desktop.
//! ```cargo
//! [dependencies]
//! object = "0.32"
//! brotli = "3.4"
//! anyhow = "1.0"
//! ```
use object::{read::pe::PeFile64, Object, ObjectSection};
use std::env;
use std::fs;
use std::io::{Read, Write};
use std::path::Path;
fn main() -> anyhow::Result<()> {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
println!("Usage: rust-script extract_v3.rs <exe_path> [optional_path_filter]");
return Ok(());
}
let exe_path = &args[1];
// 允许用户指定任何已知的路径字符串作为锚点,默认还是用这个
let anchor_str = args.get(2).map(|s| s.as_str()).unwrap_or("/index.html");
let data = fs::read(exe_path)?;
let pe = PeFile64::parse(&*data)?;
let rdata = pe.section_by_name(".rdata").ok_or_else(|| anyhow::anyhow!("No .rdata"))?;
let rdata_data = rdata.data()?;
let rdata_rva_start = rdata.address() as u32;
// 1. 定位锚点字符串的 RVA
let target_bytes = anchor_str.as_bytes();
let target_rva = rdata_data.windows(target_bytes.len())
.position(|window| window == target_bytes)
.map(|pos| rdata_rva_start + pos as u32)
.ok_or_else(|| anyhow::anyhow!("String '{}' not found in .rdata", anchor_str))?;
println!("[*] Found '{}' at RVA: 0x{:08X}", anchor_str, target_rva);
// 2. 启发式探测结构 (Heuristic Structure Detection)
// 我们寻找包含 target_rva 的 32 字节块,并尝试分析字段布局
let mut best_entry = None;
let rva_le = target_rva.to_le_bytes();
for j in (0..(rdata_data.len() - 32)).step_by(4) {
if &rdata_data[j..j+4] == &rva_le {
// 找到了 RVA,现在看周围。Tauri 资源条目通常包含:名称RVA, 名称长, 数据RVA, 数据长
// 尝试几种常见的组合布局 (4字节或8字节长度)
let layouts = [
(4, 4, 4, 4), // NameRVA(4), NameLen(4), DataRVA(4), DataLen(4) -> 16 bytes
(4, 8, 4, 8), // NameRVA(4), padding(4), NameLen(8), DataRVA(4), padding(4), DataLen(8) -> 32 bytes
(8, 8, 8, 8), // 如果是 64 位对齐但内部只存了 32 位 RVA (前4字节是RVA,后4字节是0)
];
for (ptr_w, len_w, _, _) in layouts {
let len_pos = j + ptr_w;
if len_pos + len_w <= rdata_data.len() {
let reported_len = match len_w {
4 => u32::from_le_bytes(rdata_data[len_pos..len_pos+4].try_into().unwrap()) as u64,
8 => u64::from_le_bytes(rdata_data[len_pos..len_pos+8].try_into().unwrap()),
_ => 0,
};
if reported_len == anchor_str.len() as u64 {
let entry_size = ptr_w + len_w + ptr_w + len_w;
println!("[!] Detected Structure: Offset=0x{:X}, EntrySize={}, PtrWidth={}, LenWidth={}",
j, entry_size, ptr_w, len_w);
best_entry = Some((j, entry_size, ptr_w, len_w));
break;
}
}
}
}
if best_entry.is_some() { break; }
}
let (start_off, entry_size, ptr_w, len_w) = best_entry.ok_or_else(|| {
anyhow::anyhow!("Could not determine entry structure around the RVA. Hex near RVA: {:02X?}",
&rdata_data[target_rva as usize - rdata_rva_start as usize .. (target_rva as usize - rdata_rva_start as usize + 32).min(rdata_data.len())])
})?;
// 3. 提取
let stem = Path::new(exe_path).file_stem().unwrap().to_str().unwrap();
let extract_dir = Path::new(exe_path).parent().unwrap().join(format!("{}_extract", stem));
fs::create_dir_all(&extract_dir)?;
let to_offset = |rva: u32| -> Option<usize> {
if rva >= rdata_rva_start && rva < rdata_rva_start + rdata_data.len() as u32 {
Some((rva - rdata_rva_start) as usize)
} else { None }
};
let mut dump = |off: usize| -> Option<String> {
if off + entry_size > rdata_data.len() { return None; }
let s = &rdata_data[off..off + entry_size];
let n_rva = u32::from_le_bytes(s[0..4].try_into().ok()?);
let n_len = if len_w == 4 { u32::from_le_bytes(s[ptr_w..ptr_w+4].try_into().ok()?) as usize }
else { u64::from_le_bytes(s[ptr_w..ptr_w+8].try_into().ok()?) as usize };
let d_ptr_pos = ptr_w + len_w;
let d_rva = u32::from_le_bytes(s[d_ptr_pos..d_ptr_pos+4].try_into().ok()?);
let d_len_pos = d_ptr_pos + ptr_w;
let d_len = if len_w == 4 { u32::from_le_bytes(s[d_len_pos..d_len_pos+4].try_into().ok()?) as usize }
else { u64::from_le_bytes(s[d_len_pos..d_len_pos+8].try_into().ok()?) as usize };
let n_off = to_offset(n_rva)?;
let d_off = to_offset(d_rva)?;
let name_bytes = &rdata_data[n_off..(n_off + n_len).min(rdata_data.len())];
let name = String::from_utf8_lossy(name_bytes).trim_start_matches('/').to_string();
if name.is_empty() || name.contains('\0') { return None; }
let comp_data = &rdata_data[d_off..(d_off + d_len).min(rdata_data.len())];
let mut decompressor = brotli::Decompressor::new(comp_data, 4096);
let mut decompressed = Vec::new();
let final_data = match decompressor.read_to_end(&mut decompressed) {
Ok(_) => decompressed,
Err(_) => comp_data.to_vec(),
};
let out_path = extract_dir.join(&name);
if let Some(p) = out_path.parent() { let _ = fs::create_dir_all(p); }
fs::write(out_path, final_data).ok()?;
Some(name)
};
let mut count = 0;
// 双向扫描
let mut curr = start_off;
while let Some(name) = dump(curr) {
println!("Extracted: {}", name);
count += 1;
curr += entry_size;
}
if start_off >= entry_size {
let mut curr = start_off - entry_size;
while let Some(name) = dump(curr) {
println!("Extracted: {}", name);
count += 1;
if curr < entry_size { break; }
curr -= entry_size;
}
}
println!("\nTotal: {} files in {:?}", count, extract_dir);
Ok(())
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment