Skip to content
Venish Joe Clarence

Intercept Cleartext in Windows DLLs

The Breakdown

I was looking at a binary that used a custom XOR routine to obfuscate its configuration files. Instead of spending hours reversing the specific math of the algorithm, I decided to use Frida to intercept the data at the exact moment it was passed to the encryption function, grabbing the cleartext before it ever hit the disk.

However, my initial attempts were a complete mess. I ran into three distinct walls that almost derailed the investigation:

  1. The Export Table Wall: I tried to hook the function directly in the .exe. Nothing happened. I realized that unlike .dll files, standard Windows executables don’t maintain an export table, making it nearly impossible to find functions by name.
  2. The Race Condition: When I moved the logic into a DLL, I tried to “spawn” the process using frida -f. The script failed immediately because the DLL hadn’t actually been loaded into memory at the millisecond the script ran.
  3. The Stdin Hijack: When I tried to attach to the process, I found that Frida had my keyboard input. Because Frida was controlling the process, my fgets() calls in the target app were never receiving my keystrokes.

The Fix

I had to implement a three-pronged strategy: move the logic to a DLL, use a polling loop in the script to wait for the module load, and use the “attach” method to preserve CLI interactivity.

First, the Target DLL (target.c) holds the “protected” logic:

#include <stdio.h>
#include <string.h>

__declspec(dllexport) void encrypt_data(const char* input, char* output, int len) {
    char key = 0x42; 
    for (int i = 0; i < len; i++) {
        output[i] = input[i] ^ key;
    }
}target.c

Next, the Loader (loader.c) loads the DLL and handles the user interaction. Note the LoadLibrary call is at the very top to ensure the module is present as soon as possible:

#include <stdio.h>
#include <windows.h>

typedef void (*encrypt_func)(const char*, char*, int);

int main() {
    HMODULE hLib = LoadLibrary("target.dll");
    if (!hLib) return 1;

    char input[256];
    char output[256];

    printf("--- Secure Messaging App ---\n");
    printf("Enter a secret message: ");
    if (fgets(input, sizeof(input), stdin) == NULL) return 1;
    input[strcspn(input, "\n")] = 0; 
    int len = strlen(input);

    encrypt_func encrypt = (encrypt_func)GetProcAddress(hLib, "encrypt_data");
    if (!encrypt) return 1;

    encrypt(input, output, len);

    FILE *f = fopen("secret.enc", "wb");
    if (f) {
        fwrite(output, 1, len, f);
        fclose(f);
        printf("[+] Encrypted message saved to secret.enc\n");
    }

    FreeLibrary(hLib);
    printf("\n[!] Press ENTER to exit.\n");
    getchar(); 
    return 0;
}loader.c

Finally, the Frida Script (hook.js) uses a polling loop to wait for the DLL to appear and then uses the direct pointer.readUtf8String() method for maximum compatibility across Frida versions:

console.log("[*] Script loaded. Searching for target.dll...");

var interval = setInterval(function() {
    var modules = Process.enumerateModules();
    var targetModule = null;

    for (var i = 0; i < modules.length; i++) {
        if (modules[i].name.toLowerCase().indexOf("target") !== -1) {
            targetModule = modules[i];
            break;
        }
    }

    if (targetModule) {
        var targetFunc = targetModule.getExportByName("encrypt_data");
        if (targetFunc) {
            Interceptor.attach(targetFunc, {
                onEnter: function (args) {
                    try {
                        console.log("\n" + "=".repeat(30));
                        console.log("[!!!] INTERCEPTION SUCCESSFUL [!!!]");
                        console.log("Function: encrypt_data");
                        console.log("Cleartext: " + args[0].readUtf8String());
                        console.log("=".repeat(30) + "\n");
                    } catch (err) {
                        console.log("[-] Interception Error: " + err.message);
                    }
                }
            });
            console.log("[*] Hook attached! Ready for input.");
            clearInterval(interval);
        }
    }
}, 200);hook.js

To run this lab:

  1. Compile: gcc -shared -o target.dll target.c and gcc -o loader.exe loader.c.
  2. Command Window 1: Run .\loader.exe.
  3. Command Window 2: Run frida -n loader.exe -l hook.js.
  4. Type your message in Window 1 and watch the cleartext appear in Window 2.
# Window 1:
loader.exe

--- Secure Messaging App (DLL Loader) ---
Enter a secret message:

# Window 2:
frida -n loader.exe -l hook.js

Attaching...
[*] Script loaded. Searching for 'target' in modules...
[Local::loader.exe ]-> [+] Found module: target.dll
[*] Hook successfully attached! Ready for input.

# Window 1:
Enter a secret message: Hello World!
[+] Encrypted message saved to secret.enc

[!] Press ENTER to exit.

# Window 2:
==============================
[!!!] INTERCEPTION SUCCESSFUL [!!!]
Function: encrypt_data
Cleartext: Hello World!
==============================

If you are instrumenting interactive command-line tools, always use the attach method (-n) rather than spawn (-f) to avoid stdin hijacking. Also, if you can’t find a function in an EXE, don’t waste time. Look for the DLL it loads.



Previous Post
Fixing Windows Sandbox Error 0x8007051A (vPCI Protocol Mismatch)