commit 15ab72bf965cae87a770adb394e1eb70003ad7af Author: Jay <154396700+JayGLXR@users.noreply.github.com> Date: Mon Mar 17 17:31:42 2025 -0400 Add files via upload diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..4dd806a --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,25 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "cc" +version = "1.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" +dependencies = [ + "shlex", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "stardust" +version = "0.1.0" +dependencies = [ + "cc", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..988c71d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "stardust" +version = "0.1.0" +edition = "2021" +authors = ["Stardust Team"] +description = "A modern and easy to use 32/64-bit shellcode template in Rust" + +[lib] +name = "stardust" +crate-type = ["staticlib", "cdylib"] + +[build-dependencies] +cc = "1.0" + +[features] +default = [] +debug = [] + +[profile.release] +opt-level = "z" +lto = true +codegen-units = 1 +panic = "abort" +strip = true + +[profile.dev] +panic = "abort" \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..30d6e39 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +FROM ubuntu:22.04 + +# Install dependencies +RUN apt-get update && apt-get install -y \ + curl \ + build-essential \ + mingw-w64 \ + nasm \ + binutils-mingw-w64 \ + && rm -rf /var/lib/apt/lists/* + +# Install Rust +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y +ENV PATH="/root/.cargo/bin:${PATH}" + +# Set up Rust targets for cross-compilation +RUN rustup target add x86_64-pc-windows-gnu i686-pc-windows-gnu + +# Create working directory +WORKDIR /stardust + +# Set default command +CMD ["bash"] \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a4a402d --- /dev/null +++ b/Makefile @@ -0,0 +1,56 @@ +NAME := stardust + +# Docker commands +DOCKER_BUILD := docker build -t stardust-rust . +DOCKER_RUN := docker run --rm -v $(PWD):/stardust stardust-rust + +# Cargo commands +CARGO := cargo + +# Build directories +BIN_DIR := bin +OBJ_DIR := bin/obj + +all: x64 x86 +debug: x64-debug x86-debug + +# Create necessary directories +dirs: + @mkdir -p $(BIN_DIR) $(OBJ_DIR) + +# Build Docker image +docker-build: + $(DOCKER_BUILD) + +# x64 builds +x64: dirs docker-build + @echo "Compiling x64 project" + $(DOCKER_RUN) $(CARGO) build --release --target x86_64-pc-windows-gnu + @echo "Extracting shellcode" + $(DOCKER_RUN) x86_64-w64-mingw32-objcopy --dump-section .text=$(BIN_DIR)/$(NAME).x64.bin target/x86_64-pc-windows-gnu/release/stardust.dll + +x64-debug: dirs docker-build + @echo "Compiling x64 project (debug)" + $(DOCKER_RUN) $(CARGO) build --features debug --target x86_64-pc-windows-gnu + @echo "Extracting shellcode" + $(DOCKER_RUN) x86_64-w64-mingw32-objcopy --dump-section .text=$(BIN_DIR)/$(NAME).x64.bin target/x86_64-pc-windows-gnu/debug/stardust.dll + +# x86 builds +x86: dirs docker-build + @echo "Compiling x86 project" + $(DOCKER_RUN) $(CARGO) build --release --target i686-pc-windows-gnu + @echo "Extracting shellcode" + $(DOCKER_RUN) i686-w64-mingw32-objcopy --dump-section .text=$(BIN_DIR)/$(NAME).x86.bin target/i686-pc-windows-gnu/release/stardust.dll + +x86-debug: dirs docker-build + @echo "Compiling x86 project (debug)" + $(DOCKER_RUN) $(CARGO) build --features debug --target i686-pc-windows-gnu + @echo "Extracting shellcode" + $(DOCKER_RUN) i686-w64-mingw32-objcopy --dump-section .text=$(BIN_DIR)/$(NAME).x86.bin target/i686-pc-windows-gnu/debug/stardust.dll + +clean: + @rm -rf target + @rm -rf $(BIN_DIR)/*.bin + @echo "Cleaned build artifacts" + +.PHONY: all debug x64 x86 x64-debug x86-debug clean dirs docker-build \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..2899638 --- /dev/null +++ b/README.md @@ -0,0 +1,170 @@ +# Rusty-Stardust + +A modern and easy to use 32/64-bit shellcode template, ported to Rust from the original [Stardust](https://github.com/Cracked5pider/Stardust) project by [C5pider](https://twitter.com/C5pider). + +## About + +This project is a Rust implementation of the Stardust design, which provides a modern approach to position-independent code development. The original Stardust project introduced several key innovations in shellcode design: + +- Separation of code and data into distinct sections +- Global instance access in position-independent code +- Raw string embedding capabilities +- Compile-time string hashing for stealth +- Exception handling and debug information stripping + +Rusty-Stardust maintains these core features while leveraging Rust's safety guarantees and modern tooling. This implementation serves as both a practical tool and an educational resource for those interested in learning about position-independent code development in Rust. + +## Features + +- Raw string embedding in shellcode +- Compile-time string hashing with fnv1a for both function and module resolution +- No_std implementation for minimal footprint +- Cross-compilation for both x86 and x64 Windows targets +- Docker-based build environment for cross-platform development +- Position-independent code using RIP-relative addressing +- Exception handling and debug information stripping for minimal footprint +- Proper MinGW toolchain integration for reliable shellcode extraction + +## Building + +### Prerequisites + +- Docker (for cross-compilation) +- Rust (for local development) +- NASM (assembled automatically in Docker) +- MinGW toolchain (provided in Docker environment) + +### Building with Docker + +Build for release: +```shell +$ make +``` + +Build for debug mode (with DbgPrint support): +```shell +$ make debug +``` + +### Build Outputs + +Output files will be placed in the `bin` directory: +- `stardust.x64.bin`: 64-bit shellcode (586 bytes) +- `stardust.x86.bin`: 32-bit shellcode (8.2KB) + +The shellcode is extracted using MinGW's objcopy tool and contains: +- Stack setup code +- DLL loading functionality +- Function resolution code +- Architecture-specific instructions + +### Build Configuration + +The project uses several key configurations for reliable shellcode generation: + +1. **Linker Script** (`scripts/linker.ld`): + - Discards exception handling sections (`.pdata`, `.xdata`) + - Removes debug information (`.debug*`) + - Strips unnecessary sections (`.eh_frame`, `.note*`, `.comment*`) + +2. **Cargo Config** (`.cargo/config.toml`): + - Position Independent Code (PIC) enabled + - Structured Exception Handling (SEH) disabled + - Windows subsystem configuration + - Custom linker settings for both architectures + +3. **Assembly Integration**: + - Assembly files are compiled with NASM + - RIP-relative addressing for position independence + - Proper section alignment and linking + +## Usage Example + +### Module Resolution + +Resolving modules from PEB: +```rust +// Resolve modules from PEB using hash +let ntdll_hash = hash_str!("ntdll.dll"); +let ntdll_handle = resolve::module(ntdll_hash); + +let kernel32_hash = hash_str!("kernel32.dll"); +let kernel32_handle = resolve::module(kernel32_hash); +``` + +### API Resolution + +Resolving function APIs: +```rust +// Resolve LoadLibraryA from kernel32 +let load_library_fn: *mut FnLoadLibraryA = resolve::api( + kernel32_handle, + hash_str!("LoadLibraryA") as usize +); + +// Load user32.dll +let user32 = unsafe { (*load_library_fn)(symbol("user32.dll".as_ptr())) }; + +// Resolve MessageBoxA from user32 +let msgbox_fn: *mut FnMessageBoxA = resolve::api( + user32 as usize, + hash_str!("MessageBoxA") as usize +); + +// Display message box +unsafe { + (*msgbox_fn)( + core::ptr::null_mut(), + symbol("Hello world".as_ptr()), + symbol("caption".as_ptr()), + MB_OK + ); +} +``` + +### Debugging Output + +When built in debug mode: +```rust +#[cfg(feature = "debug")] +{ + dbg_printf!(instance, "Shellcode @ %p [%d bytes]\n", base_addr, size); +} +``` + +## Testing + +A test program called "stomper" is included to load and execute the shellcode: + +```shell +$ cd tests +$ cargo build +$ ./target/debug/stomper ../bin/stardust.x64.bin +``` + +## Architecture + +This Rust port maintains the same architecture as the original C++ version: +- Assembly files are kept intact and linked using build.rs +- Memory layouts match the original for PE file parsing +- Windows structures are defined in Rust to match their C/C++ counterparts +- Function resolution uses the same hash-based approach for stealth +- MinGW toolchain integration for reliable shellcode extraction +- Exception handling and debug information stripping for minimal footprint + +## Credits + +This project is a Rust implementation of the original Stardust project. Special thanks to: + +- [C5pider](https://twitter.com/C5pider) for the original Stardust design and implementation +- [Modexp](https://twitter.com/modexpblog) for his work on Windows PIC +- [Austin Hudson](https://twitter.com/ilove2pwn_) for his work on titanldr-ng +- [Kyle Avery](https://twitter.com/kyleavery_) for his work on AceLdr +- [x86matthew](https://twitter.com/x86matthew) for assembly guidance +- [mrexodia](https://twitter.com/mrexodia) for linker script insights + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +The original Stardust project and its design concepts are the work of [C5pider](https://github.com/Cracked5pider/Stardust). This Rust implementation is provided as an educational resource and alternative implementation for those interested in learning about position-independent code development in Rust. \ No newline at end of file diff --git a/asm/x64/entry.asm b/asm/x64/entry.asm new file mode 100644 index 0000000..3da6859 --- /dev/null +++ b/asm/x64/entry.asm @@ -0,0 +1,27 @@ +[BITS 64] + +DEFAULT REL + +EXTERN entry +GLOBAL stardust +GLOBAL RipStart + +[SECTION .text$A] + stardust: + push rsi + mov rsi, rsp + and rsp, 0FFFFFFFFFFFFFFF0h + sub rsp, 020h + call entry + mov rsp, rsi + pop rsi + ret + + RipStart: + call RipPtr + ret + + RipPtr: + mov rax, [rsp] + sub rax, 0x1b + ret \ No newline at end of file diff --git a/asm/x64/utils.asm b/asm/x64/utils.asm new file mode 100644 index 0000000..b23786b --- /dev/null +++ b/asm/x64/utils.asm @@ -0,0 +1,15 @@ +[BITS 64] + +DEFAULT REL + +GLOBAL RipData + +[SECTION .text$C] + RipData: + call RetPtrData + ret + + RetPtrData: + mov rax, [rsp] + sub rax, 0x5 + ret \ No newline at end of file diff --git a/asm/x86/entry.asm b/asm/x86/entry.asm new file mode 100644 index 0000000..7b52bf1 --- /dev/null +++ b/asm/x86/entry.asm @@ -0,0 +1,25 @@ +[BITS 32] + +DEFAULT REL + +EXTERN _entry +GLOBAL _stardust +GLOBAL _RipStart + +[SECTION .text$A] + _stardust: + push ebp + mov ebp, esp + call _entry + mov esp, ebp + pop ebp + ret + + _RipStart: + call _RipPtr + ret + + _RipPtr: + mov eax, [esp] + sub eax, 0x11 + ret \ No newline at end of file diff --git a/asm/x86/utils.asm b/asm/x86/utils.asm new file mode 100644 index 0000000..6d2d883 --- /dev/null +++ b/asm/x86/utils.asm @@ -0,0 +1,15 @@ +[BITS 32] + +DEFAULT REL + +GLOBAL _RipData + +[SECTION .text$C] + _RipData: + call _RetPtrData + ret + + _RetPtrData: + mov eax, [esp] + sub eax, 0x5 + ret \ No newline at end of file diff --git a/bin/stardust.x64.bin b/bin/stardust.x64.bin new file mode 100644 index 0000000..6cfeca1 Binary files /dev/null and b/bin/stardust.x64.bin differ diff --git a/bin/stardust.x86.bin b/bin/stardust.x86.bin new file mode 100644 index 0000000..7a76dd4 Binary files /dev/null and b/bin/stardust.x86.bin differ diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..8421ebe --- /dev/null +++ b/build.rs @@ -0,0 +1,93 @@ +use std::env; +use std::path::Path; +use std::process::Command; + +fn main() { + let target = env::var("TARGET").unwrap(); + let out_dir = env::var("OUT_DIR").unwrap(); + + // Compile x64 or x86 assembly files based on target + if target.contains("x86_64") { + // Assemble x64 files + let entry_obj = format!("{}/entry.o", out_dir); + let utils_obj = format!("{}/utils.o", out_dir); + + let status = Command::new("nasm") + .args(&[ + "-f", "win64", + "asm/x64/entry.asm", + "-o", &entry_obj + ]) + .status() + .expect("Failed to assemble entry.x64.asm"); + + if !status.success() { + panic!("Failed to assemble entry.x64.asm"); + } + + let status = Command::new("nasm") + .args(&[ + "-f", "win64", + "asm/x64/utils.asm", + "-o", &utils_obj + ]) + .status() + .expect("Failed to assemble utils.x64.asm"); + + if !status.success() { + panic!("Failed to assemble utils.x64.asm"); + } + + // Use cc to link the object files + cc::Build::new() + .object(&entry_obj) + .object(&utils_obj) + .compile("asm"); + + } else if target.contains("i686") { + // Assemble x86 files + let entry_obj = format!("{}/entry.o", out_dir); + let utils_obj = format!("{}/utils.o", out_dir); + + let status = Command::new("nasm") + .args(&[ + "-f", "win32", + "asm/x86/entry.asm", + "-o", &entry_obj + ]) + .status() + .expect("Failed to assemble entry.x86.asm"); + + if !status.success() { + panic!("Failed to assemble entry.x86.asm"); + } + + let status = Command::new("nasm") + .args(&[ + "-f", "win32", + "asm/x86/utils.asm", + "-o", &utils_obj + ]) + .status() + .expect("Failed to assemble utils.x86.asm"); + + if !status.success() { + panic!("Failed to assemble utils.x86.asm"); + } + + // Use cc to link the object files + cc::Build::new() + .object(&entry_obj) + .object(&utils_obj) + .compile("asm"); + } else { + panic!("Unsupported target: {}", target); + } + + // Tell cargo to rerun if assembly files change + println!("cargo:rerun-if-changed=asm/x64/entry.asm"); + println!("cargo:rerun-if-changed=asm/x64/utils.asm"); + println!("cargo:rerun-if-changed=asm/x86/entry.asm"); + println!("cargo:rerun-if-changed=asm/x86/utils.asm"); + println!("cargo:rerun-if-changed=scripts/linker.ld"); +} \ No newline at end of file diff --git a/build_native.sh b/build_native.sh new file mode 100644 index 0000000..f4fdd01 --- /dev/null +++ b/build_native.sh @@ -0,0 +1,18 @@ +#!/bin/bash +set -e + +# This script can be used for local testing with a native Rust installation +# It won't generate the actual cross-compiled Windows shellcode but helps validate code + +echo "Building Rust code with native compiler..." +cargo build + +echo "Testing code structure..." +cargo check --all-features + +echo "Checking for errors..." +cargo clippy + +echo "All tests passed!" +echo "Note: This is only a code quality test - for actual Windows shellcode generation," +echo "you need to use the Docker-based build with cross-compilation." \ No newline at end of file diff --git a/scripts/linker.ld b/scripts/linker.ld new file mode 100644 index 0000000..5d650aa --- /dev/null +++ b/scripts/linker.ld @@ -0,0 +1,21 @@ +SECTIONS +{ + .text : + { + *( .text$A ); + *( .text$B ); + *( .rdata* ); + *( .text$C ); + } + + /* Discard exception handling and debug sections */ + /DISCARD/ : + { + *(.pdata); + *(.xdata); + *(.debug*); + *(.eh_frame); + *(.note*); + *(.comment*); + } +} \ No newline at end of file diff --git a/src/constants.rs b/src/constants.rs new file mode 100644 index 0000000..ecd4f43 --- /dev/null +++ b/src/constants.rs @@ -0,0 +1,18 @@ +#![allow(dead_code)] + +// FNV1a hash constants +pub const FNV1A_PRIME: u32 = 0x01000193; +pub const FNV1A_BASIS: u32 = 0x811c9dc5; + +// Constants for shellcode +#[cfg(target_arch = "x86_64")] +pub const END_OFFSET: usize = 0x10; + +#[cfg(target_arch = "x86")] +pub const END_OFFSET: usize = 0x10; + +// Windows constants +pub const IMAGE_DOS_SIGNATURE: u16 = 0x5A4D; +pub const IMAGE_NT_SIGNATURE: u32 = 0x00004550; +pub const IMAGE_DIRECTORY_ENTRY_EXPORT: usize = 0; +pub const MB_OK: u32 = 0x00000000; \ No newline at end of file diff --git a/src/hash.rs b/src/hash.rs new file mode 100644 index 0000000..f127fdf --- /dev/null +++ b/src/hash.rs @@ -0,0 +1,77 @@ +use crate::constants::{FNV1A_BASIS, FNV1A_PRIME}; + +// Runtime hash function for strings +pub unsafe fn hash_string(string: *const u8) -> u32 { + let mut hash = FNV1A_BASIS; + let mut ptr = string; + + while *ptr != 0 { + let mut byte = *ptr; + + // Convert to uppercase if lowercase + if byte >= b'a' { + byte -= 0x20; + } + + hash ^= byte as u32; + hash = hash.wrapping_mul(FNV1A_PRIME); + + ptr = ptr.add(1); + } + + hash +} + +// Runtime hash function for wide strings +pub unsafe fn hash_string_wide(string: *const u16) -> u32 { + let mut hash = FNV1A_BASIS; + let mut ptr = string; + + while *ptr != 0 { + let mut byte = (*ptr & 0xFF) as u8; + + // Convert to uppercase if lowercase + if byte >= b'a' { + byte -= 0x20; + } + + hash ^= byte as u32; + hash = hash.wrapping_mul(FNV1A_PRIME); + + ptr = ptr.add(1); + } + + hash +} + +// Compile-time hash function for const evaluation +#[allow(dead_code)] +pub const fn hash_const(s: &str) -> u32 { + let bytes = s.as_bytes(); + let mut hash = FNV1A_BASIS; + let mut i = 0; + + while i < bytes.len() { + let mut byte = bytes[i]; + + // Convert to uppercase if lowercase + if byte >= b'a' { + byte -= 0x20; + } + + hash ^= byte as u32; + hash = hash.wrapping_mul(FNV1A_PRIME); + + i += 1; + } + + hash +} + +// Macro for creating compile-time hashes +#[macro_export] +macro_rules! hash_str { + ($s:expr) => { + $crate::hash::hash_const($s) + }; +} \ No newline at end of file diff --git a/src/instance.rs b/src/instance.rs new file mode 100644 index 0000000..08e94bf --- /dev/null +++ b/src/instance.rs @@ -0,0 +1,193 @@ +use crate::constants::{END_OFFSET, MB_OK}; +use crate::hash_str; +use crate::memory::symbol; +use crate::resolve; +use crate::windows::*; +use core::ffi::c_void; + +// Type aliases for Windows API functions +type FnLoadLibraryA = unsafe extern "system" fn(lpLibFileName: PSTR) -> HMODULE; +type FnGetProcAddress = unsafe extern "system" fn(hModule: HMODULE, lpProcName: PSTR) -> PVOID; +type FnMessageBoxA = unsafe extern "system" fn(hWnd: PVOID, lpText: PSTR, lpCaption: PSTR, uType: u32) -> i32; + +#[cfg(feature = "debug")] +type FnDbgPrint = unsafe extern "system" fn(Format: PCH, ...) -> i32; + +// Keep resolve_import macro but mark it as #[allow(unused_macros)] since it might be needed later +#[allow(unused_macros)] +macro_rules! resolve_import { + ($instance:expr, $module:ident) => { + { + let base_addr = $instance.$module.handle; + if base_addr != 0 { + $( + // For each API in the module struct, resolve it using its hash + $instance.$module.$api = core::mem::transmute( + resolve::_api(base_addr, $instance.$module.$api as usize) + ); + )* + } + } + }; +} + +// Debug print macro +#[cfg(feature = "debug")] +#[macro_export] +macro_rules! dbg_printf { + ($instance:expr, $fmt:expr, $($arg:expr),*) => { + { + unsafe { + if !$instance.ntdll.DbgPrint.is_null() { + // Format like the original: [DEBUG::function::line] message + let prefix = symbol::(concat!("[DEBUG::", file!(), "::", line!(), "] ").as_ptr()); + let msg = symbol::($fmt.as_ptr()); + ($instance.ntdll.DbgPrint)(prefix, msg, $($arg),*); + } + } + } + }; +} + +#[cfg(not(feature = "debug"))] +#[macro_export] +macro_rules! dbg_printf { + ($instance:expr, $fmt:expr, $($arg:expr),*) => { + {} + }; +} + +// Main instance structure +pub struct Instance { + // Base address and size + pub base: BaseInfo, + + // Modules + pub ntdll: NtdllModule, + pub kernel32: Kernel32Module, +} + +// Base information +pub struct BaseInfo { + pub address: usize, + pub length: usize, +} + +// Ntdll module +pub struct NtdllModule { + pub handle: usize, + + #[cfg(feature = "debug")] + pub DbgPrint: *mut FnDbgPrint, +} + +// Kernel32 module +pub struct Kernel32Module { + pub handle: usize, + pub LoadLibraryA: *mut FnLoadLibraryA, + pub GetProcAddress: *mut FnGetProcAddress, +} + +impl Instance { + // Initialize the instance + pub fn new() -> Self { + unsafe { + // Create instance with empty values + let mut instance = Instance { + base: BaseInfo { + address: 0, + length: 0, + }, + + ntdll: NtdllModule { + handle: 0, + #[cfg(feature = "debug")] + DbgPrint: core::ptr::null_mut(), + }, + + kernel32: Kernel32Module { + handle: 0, + LoadLibraryA: core::ptr::null_mut(), + GetProcAddress: core::ptr::null_mut(), + }, + }; + + // Calculate shellcode base address + size + instance.base.address = crate::memory::RipStart(); + instance.base.length = (crate::memory::RipData() - instance.base.address) + END_OFFSET; + + // Resolve modules from PEB + instance.ntdll.handle = resolve::module(hash_str!("ntdll.dll")); + if instance.ntdll.handle == 0 { + return instance; + } + + instance.kernel32.handle = resolve::module(hash_str!("kernel32.dll")); + if instance.kernel32.handle == 0 { + return instance; + } + + // Resolve imports + #[cfg(feature = "debug")] + { + instance.ntdll.DbgPrint = core::mem::transmute( + resolve::_api(instance.ntdll.handle, hash_str!("DbgPrint") as usize) + ); + } + + instance.kernel32.LoadLibraryA = core::mem::transmute( + resolve::_api(instance.kernel32.handle, hash_str!("LoadLibraryA") as usize) + ); + + instance.kernel32.GetProcAddress = core::mem::transmute( + resolve::_api(instance.kernel32.handle, hash_str!("GetProcAddress") as usize) + ); + + instance + } + } + + // Start the shellcode execution + pub unsafe fn start(&self, _arg: *mut c_void) { + // Load user32.dll + let user32 = if !self.kernel32.LoadLibraryA.is_null() { + unsafe { (*self.kernel32.LoadLibraryA)(symbol("user32.dll".as_ptr())) } + } else { + core::ptr::null_mut() + }; + + #[cfg(feature = "debug")] + { + if !user32.is_null() { + dbg_printf!(self, "oh wow look we loaded user32.dll -> %p\n", user32); + } else { + dbg_printf!(self, "okay something went wrong. failed to load user32 :/\n"); + } + + let peb = NtCurrentPeb(); + dbg_printf!(self, "running from %ls (Pid: %d)\n", + (*(*peb).ProcessParameters).ImagePathName.Buffer, + (*NtCurrentTeb()).ClientId.UniqueProcess); + + dbg_printf!(self, "shellcode @ %p [%d bytes]\n", self.base.address, self.base.length); + } + + // Resolve MessageBoxA and display message + if !user32.is_null() { + let msgbox: *mut FnMessageBoxA = core::mem::transmute( + resolve::_api(user32 as usize, hash_str!("MessageBoxA") as usize) + ); + + if !msgbox.is_null() { + unsafe { + (*msgbox)( + core::ptr::null_mut(), + symbol("Hello world".as_ptr()), + symbol("caption".as_ptr()), + MB_OK + ); + } + } + } + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..89a28fa --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,50 @@ +#![no_std] +#![no_main] +#![allow(non_snake_case)] + +// Import modules +mod constants; +mod hash; +mod instance; +mod memory; +mod resolve; +mod windows; + +// Re-export key components but exclude memory to avoid shadowing +pub use constants::*; +pub use hash::*; +pub use instance::*; +// Don't re-export memory to avoid shadowing the assembly functions +pub use resolve::*; +pub use windows::*; + +use core::ffi::c_void; +use core::panic::PanicInfo; + +// External assembly functions (defined in our assembly files) +// These are now properly declared only here +#[link(name = "asm")] +extern "C" { + pub fn RipStart() -> usize; + pub fn RipData() -> usize; +} + +// Shellcode entry point - matches the assembly entry point +#[no_mangle] +pub unsafe extern "C" fn entry(args: *mut c_void) { + // Create instance and start execution + let instance = instance::Instance::new(); + instance.start(args); +} + +// Panic handler that does nothing +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + loop {} +} + +// Exit handler for #![no_std] +#[no_mangle] +pub unsafe extern "C" fn __aeabi_unwind_cpp_pr0() { + loop {} +} \ No newline at end of file diff --git a/src/memory.rs b/src/memory.rs new file mode 100644 index 0000000..bd1a188 --- /dev/null +++ b/src/memory.rs @@ -0,0 +1,79 @@ +//! Memory manipulation functions + +// Zero memory (Rust's equivalent of RtlSecureZeroMemory) +pub unsafe fn zero(memory: *mut u8, length: u32) { + for i in 0..length { + *memory.offset(i as isize) = 0; + } +} + +// Memory copy function +pub unsafe fn copy(destination: *mut u8, source: *const u8, length: u32) -> *mut u8 { + for i in 0..length { + *destination.offset(i as isize) = *source.offset(i as isize); + } + destination +} + +// Memory compare function +pub unsafe fn compare(memory1: *const u8, memory2: *const u8, length: usize) -> u32 { + let mut a = memory1; + let mut b = memory2; + let mut len = length; + + while len > 0 { + let val1 = *a; + let val2 = *b; + + if val1 != val2 { + return (val1 as i32 - val2 as i32) as u32; + } + + a = a.offset(1); + b = b.offset(1); + len -= 1; + } + + 0 +} + +// Get string from RIP-relative addressing +pub unsafe fn symbol(s: *const u8) -> T { + let rip_data = external_rip_data(); + let offset = (s as usize).wrapping_sub(rip_data_fn_addr() as usize); + let absolute_addr = rip_data.wrapping_sub(offset); + + core::mem::transmute_copy(&absolute_addr) +} + +// Use the external RipStart/RipData functions from lib.rs +extern "C" { + // These are now public functions + pub fn RipStart() -> usize; + pub fn RipData() -> usize; +} + +// Helper functions to get addresses +fn rip_data_fn_addr() -> usize { + RipData as usize +} + +fn external_rip_data() -> usize { + unsafe { RipData() } +} + +// Macro to define the RangeHeadList loop, improved version +#[macro_export] +macro_rules! range_head_list { + ($head_list:expr, $type:ty, |$current:ident| $body:block) => { + { + let head_ptr = $head_list as *const LIST_ENTRY; + let mut $current = (*head_ptr).Flink as $type; + + while $current as *const _ != head_ptr as *const _ { + $body + $current = (*$current).InLoadOrderLinks.Flink as $type; + } + } + }; +} \ No newline at end of file diff --git a/src/resolve.rs b/src/resolve.rs new file mode 100644 index 0000000..cf7ae79 --- /dev/null +++ b/src/resolve.rs @@ -0,0 +1,89 @@ +use crate::constants::{IMAGE_DIRECTORY_ENTRY_EXPORT, IMAGE_DOS_SIGNATURE, IMAGE_NT_SIGNATURE}; +use crate::hash::{hash_string, hash_string_wide}; +use crate::range_head_list; +use crate::windows::*; + +/// Finds a module by its hash in the PEB's loaded module list +pub unsafe fn module(library_hash: u32) -> usize { + let peb = NtCurrentPeb(); + let ldr = (*peb).Ldr; + let module_list = &(*ldr).InLoadOrderModuleList; + + let mut result = 0; + + // Use the fixed range_head_list macro instead of manual iteration + range_head_list!(module_list, PLDR_DATA_TABLE_ENTRY, |current| { + if library_hash == 0 { + result = (*current).OriginalBase as usize; + break; + } + + if hash_string_wide((*current).BaseDllName.Buffer) == library_hash { + result = (*current).OriginalBase as usize; + break; + } + }); + + result +} + +/// Finds an API function by its hash in the specified module +pub unsafe fn _api(module_base: usize, symbol_hash: usize) -> usize { + if module_base == 0 || symbol_hash == 0 { + return 0; + } + + let mut address = 0; + + // Get DOS header + let dos_header = module_base as *mut IMAGE_DOS_HEADER; + if (*dos_header).e_magic != IMAGE_DOS_SIGNATURE { + return 0; + } + + // Get NT headers + let nt_headers = (module_base + (*dos_header).e_lfanew as usize) as *mut IMAGE_NT_HEADERS; + if (*nt_headers).Signature != IMAGE_NT_SIGNATURE { + return 0; + } + + // Get export directory + let export_dir_rva = (*nt_headers).OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress; + let export_dir = (module_base + export_dir_rva as usize) as *mut IMAGE_EXPORT_DIRECTORY; + + // Get export tables + let names = (module_base + (*export_dir).AddressOfNames as usize) as *mut u32; + let functions = (module_base + (*export_dir).AddressOfFunctions as usize) as *mut u32; + let ordinals = (module_base + (*export_dir).AddressOfNameOrdinals as usize) as *mut u16; + + // Find the export by hash + for i in 0..(*export_dir).NumberOfNames { + let name_rva = *names.offset(i as isize); + let name = (module_base + name_rva as usize) as *const u8; + + if hash_string(name) == symbol_hash as u32 { + let ordinal = *ordinals.offset(i as isize) as isize; + let function_rva = *functions.offset(ordinal); + address = module_base + function_rva as usize; + break; + } + } + + address +} + +/// Generic wrapper for _api that returns a typed function pointer +pub unsafe fn api(module_base: usize, symbol_hash: usize) -> *mut T { + _api(module_base, symbol_hash) as *mut T +} + +// Macro to resolve an API by name +#[macro_export] +macro_rules! resolve_api { + ($module:expr, $name:ident) => { + $crate::resolve::api::( + $module, + $crate::hash_str!(stringify!($name)) as usize + ) as *const unsafe extern "system" fn() + }; +} \ No newline at end of file diff --git a/src/windows.rs b/src/windows.rs new file mode 100644 index 0000000..9144d10 --- /dev/null +++ b/src/windows.rs @@ -0,0 +1,300 @@ +#![allow(non_snake_case, non_camel_case_types)] + +use core::ffi::c_void; + +// Windows basic types +pub type BYTE = u8; +pub type WORD = u16; +pub type DWORD = u32; +pub type LONG = i32; +pub type ULONG = u32; +pub type ULONG_PTR = usize; +pub type PVOID = *mut c_void; +pub type LPVOID = *mut c_void; +pub type HANDLE = PVOID; +pub type HMODULE = HANDLE; +pub type BOOL = i32; +pub type PSTR = *mut u8; +pub type PWSTR = *mut u16; +pub type PCH = *const i8; +pub type LPCSTR = *const i8; +pub type LPCWSTR = *const u16; +pub type UINT = u32; +pub type WCHAR = u16; + +// Windows structures +#[repr(C)] +pub struct LIST_ENTRY { + pub Flink: *mut LIST_ENTRY, + pub Blink: *mut LIST_ENTRY, +} +pub type PLIST_ENTRY = *mut LIST_ENTRY; + +#[repr(C)] +pub struct UNICODE_STRING { + pub Length: u16, + pub MaximumLength: u16, + pub Buffer: *mut WCHAR, +} +pub type PUNICODE_STRING = *mut UNICODE_STRING; + +#[repr(C)] +pub struct CLIENT_ID { + pub UniqueProcess: HANDLE, + pub UniqueThread: HANDLE, +} + +#[repr(C)] +pub struct RTL_USER_PROCESS_PARAMETERS { + // Simplified - only what we need + pub Reserved1: [BYTE; 16], + pub Reserved2: [PVOID; 10], + pub ImagePathName: UNICODE_STRING, + pub CommandLine: UNICODE_STRING, +} +pub type PRTL_USER_PROCESS_PARAMETERS = *mut RTL_USER_PROCESS_PARAMETERS; + +#[repr(C)] +pub struct PEB_LDR_DATA { + pub Length: ULONG, + pub Initialized: BOOL, + pub SsHandle: HANDLE, + pub InLoadOrderModuleList: LIST_ENTRY, + pub InMemoryOrderModuleList: LIST_ENTRY, + pub InInitializationOrderModuleList: LIST_ENTRY, +} +pub type PPEB_LDR_DATA = *mut PEB_LDR_DATA; + +#[repr(C)] +pub struct LDR_DATA_TABLE_ENTRY { + pub InLoadOrderLinks: LIST_ENTRY, + pub InMemoryOrderLinks: LIST_ENTRY, + pub InInitializationOrderLinks: LIST_ENTRY, + pub OriginalBase: PVOID, + pub EntryPoint: PVOID, + pub SizeOfImage: ULONG, + pub FullDllName: UNICODE_STRING, + pub BaseDllName: UNICODE_STRING, + pub Flags: ULONG, + pub LoadCount: u16, + pub TlsIndex: u16, + pub HashLinks: LIST_ENTRY, + pub TimeDateStamp: ULONG, +} +pub type PLDR_DATA_TABLE_ENTRY = *mut LDR_DATA_TABLE_ENTRY; + +#[repr(C)] +pub struct PEB { + pub InheritedAddressSpace: BYTE, + pub ReadImageFileExecOptions: BYTE, + pub BeingDebugged: BYTE, + pub BitField: BYTE, + pub Mutant: HANDLE, + pub ImageBaseAddress: PVOID, + pub Ldr: *mut PEB_LDR_DATA, + // ... other fields not needed for our purposes +} +pub type PPEB = *mut PEB; + +#[repr(C)] +pub struct TEB { + pub Reserved1: [BYTE; 12], + pub ProcessEnvironmentBlock: PPEB, + pub Reserved2: [BYTE; 399], + pub ClientId: CLIENT_ID, + // Simplified - truncated remaining fields +} +pub type PTEB = *mut TEB; + +// PE Format structures +#[repr(C)] +pub struct IMAGE_DOS_HEADER { + pub e_magic: WORD, + pub e_cblp: WORD, + pub e_cp: WORD, + pub e_crlc: WORD, + pub e_cparhdr: WORD, + pub e_minalloc: WORD, + pub e_maxalloc: WORD, + pub e_ss: WORD, + pub e_sp: WORD, + pub e_csum: WORD, + pub e_ip: WORD, + pub e_cs: WORD, + pub e_lfarlc: WORD, + pub e_ovno: WORD, + pub e_res: [WORD; 4], + pub e_oemid: WORD, + pub e_oeminfo: WORD, + pub e_res2: [WORD; 10], + pub e_lfanew: LONG, +} +pub type PIMAGE_DOS_HEADER = *mut IMAGE_DOS_HEADER; + +#[repr(C)] +pub struct IMAGE_DATA_DIRECTORY { + pub VirtualAddress: DWORD, + pub Size: DWORD, +} +pub type PIMAGE_DATA_DIRECTORY = *mut IMAGE_DATA_DIRECTORY; + +#[repr(C)] +pub struct IMAGE_OPTIONAL_HEADER64 { + pub Magic: WORD, + pub MajorLinkerVersion: BYTE, + pub MinorLinkerVersion: BYTE, + pub SizeOfCode: DWORD, + pub SizeOfInitializedData: DWORD, + pub SizeOfUninitializedData: DWORD, + pub AddressOfEntryPoint: DWORD, + pub BaseOfCode: DWORD, + pub ImageBase: u64, + pub SectionAlignment: DWORD, + pub FileAlignment: DWORD, + pub MajorOperatingSystemVersion: WORD, + pub MinorOperatingSystemVersion: WORD, + pub MajorImageVersion: WORD, + pub MinorImageVersion: WORD, + pub MajorSubsystemVersion: WORD, + pub MinorSubsystemVersion: WORD, + pub Win32VersionValue: DWORD, + pub SizeOfImage: DWORD, + pub SizeOfHeaders: DWORD, + pub CheckSum: DWORD, + pub Subsystem: WORD, + pub DllCharacteristics: WORD, + pub SizeOfStackReserve: u64, + pub SizeOfStackCommit: u64, + pub SizeOfHeapReserve: u64, + pub SizeOfHeapCommit: u64, + pub LoaderFlags: DWORD, + pub NumberOfRvaAndSizes: DWORD, + pub DataDirectory: [IMAGE_DATA_DIRECTORY; 16], +} + +#[repr(C)] +pub struct IMAGE_OPTIONAL_HEADER32 { + pub Magic: WORD, + pub MajorLinkerVersion: BYTE, + pub MinorLinkerVersion: BYTE, + pub SizeOfCode: DWORD, + pub SizeOfInitializedData: DWORD, + pub SizeOfUninitializedData: DWORD, + pub AddressOfEntryPoint: DWORD, + pub BaseOfCode: DWORD, + pub BaseOfData: DWORD, + pub ImageBase: DWORD, + pub SectionAlignment: DWORD, + pub FileAlignment: DWORD, + pub MajorOperatingSystemVersion: WORD, + pub MinorOperatingSystemVersion: WORD, + pub MajorImageVersion: WORD, + pub MinorImageVersion: WORD, + pub MajorSubsystemVersion: WORD, + pub MinorSubsystemVersion: WORD, + pub Win32VersionValue: DWORD, + pub SizeOfImage: DWORD, + pub SizeOfHeaders: DWORD, + pub CheckSum: DWORD, + pub Subsystem: WORD, + pub DllCharacteristics: WORD, + pub SizeOfStackReserve: DWORD, + pub SizeOfStackCommit: DWORD, + pub SizeOfHeapReserve: DWORD, + pub SizeOfHeapCommit: DWORD, + pub LoaderFlags: DWORD, + pub NumberOfRvaAndSizes: DWORD, + pub DataDirectory: [IMAGE_DATA_DIRECTORY; 16], +} + +#[repr(C)] +pub struct IMAGE_FILE_HEADER { + pub Machine: WORD, + pub NumberOfSections: WORD, + pub TimeDateStamp: DWORD, + pub PointerToSymbolTable: DWORD, + pub NumberOfSymbols: DWORD, + pub SizeOfOptionalHeader: WORD, + pub Characteristics: WORD, +} + +#[cfg(target_arch = "x86_64")] +#[repr(C)] +pub struct IMAGE_NT_HEADERS { + pub Signature: DWORD, + pub FileHeader: IMAGE_FILE_HEADER, + pub OptionalHeader: IMAGE_OPTIONAL_HEADER64, +} + +#[cfg(target_arch = "x86")] +#[repr(C)] +pub struct IMAGE_NT_HEADERS { + pub Signature: DWORD, + pub FileHeader: IMAGE_FILE_HEADER, + pub OptionalHeader: IMAGE_OPTIONAL_HEADER32, +} + +pub type PIMAGE_NT_HEADERS = *mut IMAGE_NT_HEADERS; + +#[repr(C)] +pub struct IMAGE_EXPORT_DIRECTORY { + pub Characteristics: DWORD, + pub TimeDateStamp: DWORD, + pub MajorVersion: WORD, + pub MinorVersion: WORD, + pub Name: DWORD, + pub Base: DWORD, + pub NumberOfFunctions: DWORD, + pub NumberOfNames: DWORD, + pub AddressOfFunctions: DWORD, + pub AddressOfNames: DWORD, + pub AddressOfNameOrdinals: DWORD, +} +pub type PIMAGE_EXPORT_DIRECTORY = *mut IMAGE_EXPORT_DIRECTORY; + +// Helper functions for accessing TEB/PEB +#[cfg(target_arch = "x86_64")] +#[inline(always)] +pub unsafe fn NtCurrentTeb() -> PTEB { + let teb: PTEB; + core::arch::asm!( + "mov {}, gs:[0x30]", + out(reg) teb, + options(nostack, preserves_flags) + ); + teb +} + +#[cfg(target_arch = "x86")] +#[inline(always)] +pub unsafe fn NtCurrentTeb() -> PTEB { + let teb: PTEB; + core::arch::asm!( + "mov {}, fs:[0x18]", + out(reg) teb, + options(nostack, preserves_flags) + ); + teb +} + +#[cfg(target_arch = "x86_64")] +pub unsafe fn NtCurrentPeb() -> *mut PEB { + let peb: *mut PEB; + core::arch::asm!( + "mov {}, gs:[0x60]", + out(reg) peb, + options(nostack, preserves_flags) + ); + peb +} + +#[cfg(target_arch = "x86")] +pub unsafe fn NtCurrentPeb() -> *mut PEB { + let peb: *mut PEB; + core::arch::asm!( + "mov {}, fs:[0x30]", + out(reg) peb, + options(nostack, preserves_flags) + ); + peb +} \ No newline at end of file diff --git a/tests/Cargo.toml b/tests/Cargo.toml new file mode 100644 index 0000000..92fff9c --- /dev/null +++ b/tests/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "stomper" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "stomper" +path = "stomper.rs" + +[dependencies] +windows = { version = "0.48", features = ["Win32_System_Memory"] } \ No newline at end of file diff --git a/tests/stomper.rs b/tests/stomper.rs new file mode 100644 index 0000000..ee33fb9 --- /dev/null +++ b/tests/stomper.rs @@ -0,0 +1,76 @@ +use std::fs; +use std::mem; +use std::path::Path; +use std::ptr; +use std::env; +use windows::Win32::System::Memory::{ + VirtualAlloc, VirtualProtect, MEM_COMMIT, MEM_RESERVE, PAGE_EXECUTE_READWRITE, PAGE_READWRITE +}; + +fn main() -> Result<(), Box> { + // Determine architecture + let args: Vec = env::args().collect(); + let bin_path = if args.len() > 1 { + args[1].clone() + } else { + #[cfg(target_arch = "x86_64")] + let bin_name = "stardust.x64.bin"; + + #[cfg(target_arch = "x86")] + let bin_name = "stardust.x86.bin"; + + format!("../bin/{}", bin_name) + }; + + println!("Loading shellcode from: {}", bin_path); + + // Read shellcode from file + let shellcode = fs::read(Path::new(&bin_path))?; + let shellcode_len = shellcode.len(); + + println!("Shellcode size: {} bytes", shellcode_len); + + unsafe { + // Allocate memory for the shellcode + let memory = VirtualAlloc( + ptr::null(), + shellcode_len, + MEM_COMMIT | MEM_RESERVE, + PAGE_READWRITE + ); + + if memory.is_null() { + return Err("Failed to allocate memory".into()); + } + + println!("Memory allocated at: {:p}", memory); + + // Copy shellcode to allocated memory + ptr::copy_nonoverlapping( + shellcode.as_ptr(), + memory.cast::(), + shellcode_len + ); + + // Make memory executable + let mut old_protect = PAGE_READWRITE; + VirtualProtect( + memory, + shellcode_len, + PAGE_EXECUTE_READWRITE, + &mut old_protect + )?; + + println!("Memory protection changed to PAGE_EXECUTE_READWRITE"); + + // Create function pointer and execute shellcode + let shellcode_fn: extern "C" fn(arg: *const u8) = mem::transmute(memory); + + println!("Executing shellcode..."); + shellcode_fn(ptr::null()); + + println!("Shellcode execution completed"); + } + + Ok(()) +} \ No newline at end of file