Notepad tapper

Sep 1 2024

Disclaimer: The techniques discussed here work on Windows 10 notepad, but not Windows 11 notepad

Introduction

In this blog, I'll discuss how you can change data displayed on notepad process using code, and then showcase a simple space shooter game for notepad. We'll use windows api and Cheat Engine to achieve this. For more information of Cheat Engine, please visit: https://wiki.cheatengine.org/index.php?title=Tutorials:Cheat_Engine_Tutorial_Guide_x64. This blog is inspired by Kyle Halladay's post: Hooking Keyboard Input To Play Snake In Notepad.exe

Initial Observe

With Cheat Engine, we can find the address of specific word within notepad process (what we're trying to do). As of notepad, though it specifically say UTF_8 at the bottom right corner, data is actually encoded by UTF_16.

How we know it? Let's examine using Cheat Engine. Let's try to find address of hello in the memory

Fire up Cheat Engine and search for hello in UTF_8 we have nothing in return

However, if we change the search to UTF_16, an address pops up

Change value of this memory address will result in change in notepad process. With this in mind, we'll use code to automate our process. First, we'll try to find the memmory address associated with a value matched target pattern. Then. we'll try to change the value, and update the windows.

Find pattern

VirtualQueryEx will help us run through all memmory address of target process (region size, data, premission, ...). We then check if the region has COMMIT and READWRITE permission. Once we capture all of those region, we'll initiate our find pattern function to find our target address.

char* GetProcessMemory(DWORD proc_id, char *pattern, size_t patternLen){
  HANDLE hproc = OpenProcess(PROCESS_ALL_ACCESS, false, proc_id);
  LPVOID base = 0x0;
  MEMORY_BASIC_INFORMATION memInfo;
  size_t bytesread;

  int skip = 0;

  while (VirtualQueryEx(hproc, base, &memInfo, sizeof(memInfo))){
    if (skip > 4){
      if (memInfo.State == MEM_COMMIT && memInfo.Protect == PAGE_READWRITE){
        char *lpBuffer = (char *)malloc(memInfo.RegionSize);

        ReadProcessMemory(hproc, base, lpBuffer, memInfo.RegionSize, NULL);
        char* match = FindPattern(lpBuffer, memInfo.RegionSize, pattern, patternLen);

        if (match){
            uint64_t diff = (uint64_t)match - (uint64_t)(lpBuffer);
            char* processPtr = (char *)base + diff;
            return processPtr;
        }
      }
    }
    // cout << endl << endl;

    skip ++;
    base += memInfo.RegionSize;
  }

  return nullptr;
}

To find pattern, first we'll initiate a pointer, ptr, points to the base address of target region. Then we'll move up until ptr is region size away from the base address. This can be done using this code:

char* FindPattern(char* src, size_t srcLen, const char* pattern, size_t patternLen){
    char* cur = src;
    size_t curPos = 0;

    while (curPos < srcLen){
        if (memcmp(cur, pattern, patternLen) == 0){
            return cur;
        }

        curPos++;
        cur = &src[curPos];
    }
    return nullptr;
}

Next up, it's time to change the value and we all done.

Change value

There's a problem with changing value in memory address: 1) Value is changed but notepad's display may not be updated immediately. Manual interact with notepad process is required. 2) Region size is fixed. Updated value is constrained within that box.

Tackling (1) is not that hard. Windows offers InvalidateRect functionality, which can force a process's window to update.

bool UpdateProcessMemory(DWORD proc_id, LPVOID base, size_t size){
    HANDLE hproc = OpenProcess(PROCESS_ALL_ACCESS, false, proc_id);
    MEMORY_BASIC_INFORMATION memInfo;

    // char *payload = UTF16Convert((char *)target.c_str());

    if(!WriteProcessMemory(hproc, base, buffer, size, NULL)){
        return false;
    }

    RECT r;
    GetClientRect(notepad_hwnd, &r);
    InvalidateRect(notepad_hwnd, &r, false);
    return true;
}

Let's get into space shooter for notepad

Space shooter

Each line in notepad associates with one memory address. To make a game, we'll need to have the control of entire screen or notepad windows. To achieve this, the simplest way is to enable word wrap in notepad; we just have to deal with 1 giant word (1 memory address).

However, recording keyboard input is not that easy. There's one way we can global hook keyboard; however, this will make anytime we press a key, an action will be initiated to space shooter game, no matter if we have notepad process as active window or not. To achieve this, we can install hook inside notepad process, then use socket api to transfer key stroke out to the main program to update space shooter.

Our plan here in general is to build a dll that acts both like a keystroke capture and a socket client. We'll then hook that dll into notepad process. SetWindowsHookExA really actually helps hook keyboard with a dll. Payload dll should be loaded first by LoadLibraryA. Once it's loaded, export function will be accessible to main program, whose address got by GetProcessAddress can be used in SetWindowsHookExA.

void InstallHook(DWORD thread_id){
  TCHAR dll_path[MAX_PATH];

  GetFullPathName(TEXT("./keyboard_dll.dll"), MAX_PATH, dll_path, NULL);

  cout << dll_path << endl;

  HMODULE hooklib = LoadLibraryA((LPCSTR)dll_path);
  HOOKPROC hookfunc = (HOOKPROC)GetProcAddress(hooklib, "HookProcedure");

  HHOOK keyboardhook = SetWindowsHookExA(WH_KEYBOARD, hookfunc, hooklib, thread_id);

  cout << "Run" << endl;
  while(GetMessage(NULL, NULL, WM_KEYFIRST, WM_KEYLAST)){}

  UnhookWindowsHookEx(keyboardhook);
}

Write dll

One can compile dll from C++ or C code; basically the way C++ and C code works and written is the same; however, with C++, we have to wrap the following around our exported function:

#ifdef __cplusplus   
extern "C" {         
#endif

#ifdef __cplusplus
}
#endif

Funtion to be exported can be defined like this:

__declspec(dllexport) LRESULT CALLBACK name_of_function(param)

Within dll file, DllMain is the entrypoint. DllMain can be defined like this

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved)

Exported function can be run when dll is attached or detached to or from a process or thread using the following template:

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved){
    switch(fdwReason){ 
        case DLL_PROCESS_ATTACH: 
            break;

        case DLL_THREAD_ATTACH:
            break;

        case DLL_THREAD_DETACH:
            break;

        case DLL_PROCESS_DETACH:
            break;
    }
    return TRUE;
}

Dll can be compiled by

g++ name.cpp -shared -o name.dll

Sometimes, additional library flag needs to be added to compile, for example -lws2_32 for socket use

Wrap up

I'll not discuss how to set up socket server and client using windows api here, but this documentation might help: Socket Client and Socket Server

Putting it all together, dll and server code can be found in my Github repo

I'll not discuss in detail the logic of game shooter here, but the code for it can be found in my Github repo as well

The full code can be found here.

Here's some result: