mirror of
https://github.com/whokilleddb/stacktracer
synced 2026-06-06 17:00:04 +00:00
Initial commit
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
[build]
|
||||
target = "x86_64-pc-windows-gnu"
|
||||
@@ -0,0 +1 @@
|
||||
/target
|
||||
Generated
+32
@@ -0,0 +1,32 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "stackfinder"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "stackfinder"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
winapi = { version = "0.3.9", features = [
|
||||
"tlhelp32",
|
||||
"processthreadsapi",
|
||||
"handleapi",
|
||||
"winnt",
|
||||
"dbghelp",
|
||||
"minwindef",
|
||||
"basetsd",
|
||||
"errhandlingapi",] }
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
use std;
|
||||
mod procenum;
|
||||
mod trace;
|
||||
|
||||
use winapi::shared::minwindef::{FALSE, TRUE};
|
||||
use winapi::um::dbghelp::{
|
||||
SYMBOL_INFOW, SYMOPT_DEFERRED_LOADS, SYMOPT_UNDNAME, SymInitializeW, SymSetOptions,
|
||||
};
|
||||
use winapi::um::errhandlingapi::GetLastError;
|
||||
use winapi::um::handleapi::CloseHandle;
|
||||
use winapi::um::processthreadsapi::OpenProcess;
|
||||
use winapi::um::winnt::HANDLE;
|
||||
use winapi::um::winnt::{PROCESS_QUERY_INFORMATION, PROCESS_VM_READ};
|
||||
|
||||
fn usage() {
|
||||
println!("[+] Usage:\n\tstackfinder.exe PID <TID>\n");
|
||||
println!("[+] Description:\n\tPrint the stack trace of a Thread of a given process\n");
|
||||
println!(
|
||||
"[+] Values:\n\tPID\tPID of process to target\n\tTID\t[Optional] TID of the target thread. If not specified,\n\t\tit will print the stack trace of all current threads."
|
||||
);
|
||||
std::process::exit(-1);
|
||||
}
|
||||
|
||||
fn stacktrace(pid: u32, tid: u32) {
|
||||
let target_tids = procenum::collect_threads(pid, tid);
|
||||
|
||||
unsafe {
|
||||
// First open a handle to the thread
|
||||
let h_process: HANDLE =
|
||||
OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pid);
|
||||
if h_process.is_null() {
|
||||
eprintln!(
|
||||
"[!] OpenProcess failed for PID {} (error {})",
|
||||
pid,
|
||||
GetLastError()
|
||||
);
|
||||
return;
|
||||
}
|
||||
println!("[+] Opened handle to Process:\t{:?}", h_process);
|
||||
|
||||
// Initialize symbols
|
||||
SymSetOptions(SYMOPT_UNDNAME | SYMOPT_DEFERRED_LOADS);
|
||||
if SymInitializeW(h_process, std::ptr::null(), TRUE) == 0 {
|
||||
eprintln!("[!] SymInitializeW failed (error {})", GetLastError());
|
||||
CloseHandle(h_process);
|
||||
return;
|
||||
}
|
||||
println!("[+] Initialized Symbols!");
|
||||
|
||||
for x in &target_tids {
|
||||
trace::trace_thread_stack(h_process, *x);
|
||||
}
|
||||
if !h_process.is_null() {
|
||||
CloseHandle(h_process);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
|
||||
if (args.len() < 2) || (args.len() > 3) {
|
||||
usage();
|
||||
}
|
||||
|
||||
let pid: u32;
|
||||
let mut tid: u32 = 0;
|
||||
|
||||
pid = match args[1].parse() {
|
||||
Ok(id) => id,
|
||||
Err(_e) => {
|
||||
eprintln!("[-] Provided PID is invalid: {:#?}", args[1]);
|
||||
std::process::exit(-1);
|
||||
}
|
||||
};
|
||||
println!("[+] Targeting PID:\t\t{}", pid);
|
||||
|
||||
if args.len() == 3 {
|
||||
tid = match args[2].parse() {
|
||||
Ok(id) => id,
|
||||
Err(_e) => {
|
||||
eprintln!("[-] Provided TID is invalid: {:#?}", args[1]);
|
||||
std::process::exit(-1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if pid == std::process::id() {
|
||||
eprintln!("[-] DO NOT RUN THIS PROGRAM AGAINST THE CURRENT PROCESS");
|
||||
std::process::exit(-1);
|
||||
}
|
||||
stacktrace(pid, tid);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
use std;
|
||||
use std::mem;
|
||||
use winapi::um::tlhelp32::{
|
||||
CreateToolhelp32Snapshot, Thread32First, Thread32Next,
|
||||
THREADENTRY32, TH32CS_SNAPTHREAD,
|
||||
};
|
||||
use winapi::um::handleapi::CloseHandle;
|
||||
use winapi::shared::minwindef::FALSE;
|
||||
|
||||
pub fn collect_threads(pid: u32, tid: u32) -> Vec<u32> {
|
||||
let mut threads = Vec::new();
|
||||
|
||||
unsafe {
|
||||
let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
|
||||
|
||||
if snapshot.is_null() {
|
||||
return threads;
|
||||
}
|
||||
|
||||
let mut entry: THREADENTRY32 = mem::zeroed();
|
||||
entry.dwSize = mem::size_of::<THREADENTRY32>() as u32;
|
||||
|
||||
if Thread32First(snapshot, &mut entry) == FALSE {
|
||||
CloseHandle(snapshot);
|
||||
return threads;
|
||||
}
|
||||
|
||||
loop {
|
||||
if entry.th32OwnerProcessID == pid {
|
||||
if tid == 0 {
|
||||
threads.push(entry.th32ThreadID);
|
||||
}
|
||||
else {
|
||||
if tid == entry.th32ThreadID {
|
||||
threads.push(tid);
|
||||
CloseHandle(snapshot);
|
||||
return threads;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if Thread32Next(snapshot, &mut entry) == FALSE {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
CloseHandle(snapshot);
|
||||
}
|
||||
|
||||
if tid != 0 {
|
||||
eprintln!("[-] Target thread not found!");
|
||||
std::process::exit(-1);
|
||||
}
|
||||
threads
|
||||
}
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
#![allow(unused_imports)]
|
||||
use std::ffi::CStr;
|
||||
use std::mem;
|
||||
use std::path::Path;
|
||||
use std::ptr;
|
||||
use winapi::shared::minwindef::{DWORD, FALSE, TRUE};
|
||||
use winapi::shared::ntdef::NULL;
|
||||
use winapi::um::dbghelp::{
|
||||
AddrModeFlat, IMAGEHLP_MODULEW64, STACKFRAME64, SYMBOL_INFOW, SYMOPT_DEFERRED_LOADS,
|
||||
SYMOPT_UNDNAME, StackWalk64, SymCleanup, SymFromAddrW, SymFunctionTableAccess64,
|
||||
SymGetModuleBase64, SymGetModuleInfoW64, SymInitializeW, SymSetOptions,
|
||||
};
|
||||
use winapi::um::errhandlingapi::GetLastError;
|
||||
use winapi::um::handleapi::CloseHandle;
|
||||
use winapi::um::processthreadsapi::{
|
||||
GetThreadContext, OpenProcess, OpenThread, ResumeThread, SuspendThread,
|
||||
};
|
||||
use winapi::um::winnt::{
|
||||
CONTEXT, CONTEXT_FULL, HANDLE, IMAGE_FILE_MACHINE_AMD64, PROCESS_QUERY_INFORMATION,
|
||||
PROCESS_VM_READ, THREAD_GET_CONTEXT, THREAD_QUERY_INFORMATION, THREAD_SUSPEND_RESUME,
|
||||
};
|
||||
const MAX_SYM_NAME_LEN: usize = 512;
|
||||
// ---------------------------------------------------------------------------
|
||||
// Resolve a single PC address to "module.dll!Symbol+0xNN" / "module.dll+0xNN"
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn resolve_address(h_process: HANDLE, addr: u64) -> String {
|
||||
unsafe {
|
||||
// ── 1. Try to get the module that contains `addr` ──
|
||||
let mut mod_info: IMAGEHLP_MODULEW64 = mem::zeroed();
|
||||
mod_info.SizeOfStruct = mem::size_of::<IMAGEHLP_MODULEW64>() as DWORD;
|
||||
let has_module = SymGetModuleInfoW64(h_process, addr, &mut mod_info) != 0;
|
||||
|
||||
// Extract just the file name from the wide-string image path
|
||||
// e.g. "C:\Windows\System32\ntdll.dll" → "ntdll.dll"
|
||||
let module_name = if has_module {
|
||||
let raw = wide_to_string(&mod_info.ImageName);
|
||||
Path::new(&raw)
|
||||
.file_name()
|
||||
.map(|f| f.to_string_lossy().into_owned())
|
||||
.unwrap_or(raw)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
// ── 2. Try to resolve to a symbol via SymFromAddrW ──
|
||||
// SYMBOL_INFOW has a trailing `Name[1]` flexible-array member;
|
||||
// we over-allocate to hold up to MAX_SYM_NAME_LEN wide characters.
|
||||
let buf_size = mem::size_of::<SYMBOL_INFOW>() + MAX_SYM_NAME_LEN * mem::size_of::<u16>();
|
||||
let mut buf = vec![0u8; buf_size];
|
||||
let sym = buf.as_mut_ptr() as *mut SYMBOL_INFOW;
|
||||
(*sym).SizeOfStruct = mem::size_of::<SYMBOL_INFOW>() as u32;
|
||||
(*sym).MaxNameLen = MAX_SYM_NAME_LEN as u32;
|
||||
|
||||
let mut sym_displacement: u64 = 0;
|
||||
|
||||
if SymFromAddrW(h_process, addr, &mut sym_displacement, sym) != 0 {
|
||||
// We have a symbol name (wide)
|
||||
let name_slice =
|
||||
std::slice::from_raw_parts((*sym).Name.as_ptr(), (*sym).NameLen as usize);
|
||||
let sym_name = String::from_utf16_lossy(name_slice);
|
||||
|
||||
if module_name.is_empty() {
|
||||
format!("{sym_name}+0x{sym_displacement:x}")
|
||||
} else {
|
||||
format!("{module_name}!{sym_name}+0x{sym_displacement:x}")
|
||||
}
|
||||
} else if has_module {
|
||||
// No exported/debug symbol – fall back to module-relative offset
|
||||
let offset = addr.wrapping_sub(mod_info.BaseOfImage);
|
||||
format!("{module_name}+0x{offset:x}")
|
||||
} else {
|
||||
// Unknown region – raw address
|
||||
format!("0x{addr:x}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a null-terminated wide-char (`u16`) buffer to a Rust `String`.
|
||||
fn wide_to_string(buf: &[u16]) -> String {
|
||||
let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len());
|
||||
String::from_utf16_lossy(&buf[..len])
|
||||
}
|
||||
|
||||
pub fn trace_thread_stack(h_process: HANDLE, tid: u32) {
|
||||
let h_thread: HANDLE;
|
||||
println!("[+] Targeting TID:\t\t{}", tid);
|
||||
unsafe {
|
||||
h_thread = OpenThread(
|
||||
THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_QUERY_INFORMATION,
|
||||
FALSE,
|
||||
tid,
|
||||
);
|
||||
if h_thread.is_null() {
|
||||
eprintln!(
|
||||
"[-] OpenThread failed for TID {} (error {})",
|
||||
tid,
|
||||
winapi::um::errhandlingapi::GetLastError()
|
||||
);
|
||||
return;
|
||||
}
|
||||
println!("[+] Opened handle to Thread:\t{:?}", h_thread);
|
||||
|
||||
loop {
|
||||
let mut ctx: CONTEXT = mem::zeroed();
|
||||
ctx.ContextFlags = CONTEXT_FULL;
|
||||
|
||||
if GetThreadContext(h_thread, &mut ctx) == 0 {
|
||||
eprintln!("[!] GetThreadContext failed (error {})", GetLastError());
|
||||
break;
|
||||
}
|
||||
println!("[+] Fetched thread context");
|
||||
let mut frame: STACKFRAME64 = mem::zeroed();
|
||||
frame.AddrPC.Offset = ctx.Rip;
|
||||
frame.AddrPC.Mode = AddrModeFlat;
|
||||
frame.AddrFrame.Offset = ctx.Rbp;
|
||||
frame.AddrFrame.Mode = AddrModeFlat;
|
||||
frame.AddrStack.Offset = ctx.Rsp;
|
||||
frame.AddrStack.Mode = AddrModeFlat;
|
||||
|
||||
let mut entries: Vec<String> = Vec::new();
|
||||
// ── Walk the stack ──
|
||||
loop {
|
||||
let ok = StackWalk64(
|
||||
IMAGE_FILE_MACHINE_AMD64 as DWORD,
|
||||
h_process,
|
||||
h_thread as HANDLE,
|
||||
&mut frame,
|
||||
&mut ctx as *mut CONTEXT as *mut _,
|
||||
None, // ReadMemoryRoutine
|
||||
Some(SymFunctionTableAccess64), // FunctionTableAccessRoutine
|
||||
Some(SymGetModuleBase64), // GetModuleBaseRoutine
|
||||
None, // TranslateAddress
|
||||
);
|
||||
|
||||
if ok == 0 || frame.AddrPC.Offset == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
entries.push(resolve_address(h_process, frame.AddrPC.Offset));
|
||||
}
|
||||
// ── Output ──
|
||||
if entries.is_empty() {
|
||||
println!("(no frames captured)");
|
||||
} else {
|
||||
println!("{}", entries.join(","));
|
||||
}
|
||||
break;
|
||||
}
|
||||
// Initialize DbgHelp symbol handler
|
||||
// let opts = SymGetOptions();
|
||||
|
||||
if !h_thread.is_null() {
|
||||
CloseHandle(h_thread);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user