commit 1c18c58b21d923abd5cc986d67be57e701b6c3e1 Author: DB Date: Sun Mar 8 01:40:15 2026 +0530 Initial commit diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..ba54493 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +target = "x86_64-pc-windows-gnu" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..ece870a --- /dev/null +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..8d35c48 --- /dev/null +++ b/Cargo.toml @@ -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",] } diff --git a/README.md b/README.md new file mode 100644 index 0000000..21c50bb --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# StackTracer diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..678c6f2 --- /dev/null +++ b/src/main.rs @@ -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 \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 = 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); +} diff --git a/src/procenum.rs b/src/procenum.rs new file mode 100644 index 0000000..948d815 --- /dev/null +++ b/src/procenum.rs @@ -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 { + 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::() 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 +} diff --git a/src/trace.rs b/src/trace.rs new file mode 100644 index 0000000..c19a7f9 --- /dev/null +++ b/src/trace.rs @@ -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::() 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::() + MAX_SYM_NAME_LEN * mem::size_of::(); + let mut buf = vec![0u8; buf_size]; + let sym = buf.as_mut_ptr() as *mut SYMBOL_INFOW; + (*sym).SizeOfStruct = mem::size_of::() 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 = 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); + } + } +}