Ray tracing dans Notepad.exe à 30 images par seconde

Il y a quelques mois, un article a été publié sur Reddit décrivant un jeu qui utilisait un clone de Bloc-notes open source pour gérer toutes les entrées et tous les rendus. En lisant à ce sujet, j'ai pensé que ce serait formidable de voir quelque chose de similaire fonctionner avec le bloc-notes Windows standard. Ensuite, j'ai eu trop de temps libre.





J'ai fini par créer un jeu Snake et un petit traceur de rayons qui utilisent le Bloc-notes standard pour toutes les tâches d'entrée et de rendu, et en cours de route j'ai appris l'injection de DLL, le hooking d'API et l'analyse de la mémoire. Décrire tout ce que j'ai appris au cours du processus peut être une lecture intéressante pour vous.





Je veux d'abord parler du fonctionnement des scanners de mémoire et de la manière dont je les ai utilisés pour transformer notepad.exe en une cible de rendu à plus de 30 images par seconde. Je vais également parler d'un traceur de rayons que j'ai construit pour le rendu dans le Bloc-notes.





Envoi des événements clés dans le bloc-notes

Je vais commencer par parler de la répartition des événements clés vers une instance de Bloc-notes en cours d'exécution. C'était la partie ennuyeuse du projet, donc je serai bref.





Win32 (, ), , , , , «», , . , Visual Studio Spy++, , .





Bloc-notes Spy ++
Spy++

Spy++ , , , «». , , Win32, HWND , . HWND :





HWND GetWindowForProcessAndClassName(DWORD pid, const char* className)
{
  HWND curWnd = GetTopWindow(0); //0 arg means to get the window at the top of the Z order
  char classNameBuf[256];

  while (curWnd != NULL){
    DWORD curPid;
    DWORD dwThreadId = GetWindowThreadProcessId(curWnd, &curPid);

    if (curPid == pid){
      GetClassName(curWnd, classNameBuf, 256);
      if (strcmp(className, classNameBuf) == 0) return curWnd;

      HWND childWindow = FindWindowEx(curWnd, NULL, className, NULL);
      if (childWindow != NULL) return childWindow;
    }
    curWnd = GetNextWindow(curWnd, GW_HWNDNEXT);
  }
  return NULL;
}
      
      



HWND , PostMessage WM_CHAR.





, Spy++, 64- . , Visual Studio 2019 . Visual Studio «spyxx_amd64.exe».





, 10 , , , , 30 . , .





CheatEngine

CheatEngine. , . , // , . .





CheatEngine , . , . , :





  • , (, 100)





  • - , (, 92)





  • , ( 100), , 92





  • , (, , , )









CheatEngine et Notepad "se sont fait des amis"
CheatEngine ""

, ,   , , . CheatEngine, ( ) . :





  1. UTF-16, , UTF-8.





  2. , CheatEngine (, ?)





  3. . ,





, , .





, , . CheatEngine, - , :





FOR EACH block of memory allocated by our target process
    IF that block is committed and read/write enabled
        Scan the contents of that block for our byte pattern
        IF WE FIND IT
            return that address

      
      



~ 40 .





, , — .





64- Windows ( 0x00000000000 0x7FFFFFFFFFFF), 0 VirtualQueryEx .





VirtualQueryEx MEMORY_BASIC_INFORMATION



, , , VirtualQueryEx , . MEMORY_BASIC_INFORMATION



.





MEMORY_BASIC_INFORMATION



, BaseAddress RegionSize VirtualQueryEx





char* FindBytePatternInProcessMemory(HANDLE process, const char* pattern, size_t patternLen)
{
  char* basePtr = (char*)0x0;

  MEMORY_BASIC_INFORMATION memInfo;

  while (VirtualQueryEx(process, (void*)basePtr, &memInfo, sizeof(MEMORY_BASIC_INFORMATION)))
  {
    const DWORD mem_commit = 0x1000;
    const DWORD page_readwrite = 0x04;
    if (memInfo.State == mem_commit && memInfo.Protect == page_readwrite)
    {
      // search this memory for our pattern
    }

    basePtr = (char*)memInfo.BaseAddress + memInfo.RegionSize;
  }
}
      
      



, / .State .Protect. MEMORY_BASIC_INFORMATION



, , , 0x1000 (MEM_COMMIT



) 0x04 (PAGE_READWRITE



).





( , , ). . ReadProcessMemory.





, , . , , . , .





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;
}
      
      



FindPattern() , . , FindPattern, , . .





char* FindBytePatternInProcessMemory(HANDLE process, const char* pattern, size_t patternLen)
{
  MEMORY_BASIC_INFORMATION memInfo;
  char* basePtr = (char*)0x0;

  while (VirtualQueryEx(process, (void*)basePtr, &memInfo, sizeof(MEMORY_BASIC_INFORMATION))){
    const DWORD mem_commit = 0x1000;
    const DWORD page_readwrite = 0x04;
    if (memInfo.State == mem_commit && memInfo.Protect == page_readwrite){
      char* remoteMemRegionPtr = (char*)memInfo.BaseAddress;
      char* localCopyContents = (char*)malloc(memInfo.RegionSize);

      SIZE_T bytesRead = 0;
      if (ReadProcessMemory(process, memInfo.BaseAddress, localCopyContents, memInfo.RegionSize, &bytesRead)){
        char* match = FindPattern(localCopyContents, memInfo.RegionSize, pattern, patternLen);

        if (match){
          uint64_t diff = (uint64_t)match - (uint64_t)(localCopyContents);
          char* processPtr = remoteMemRegionPtr + diff;
          return processPtr;
        }
      }
      free(localCopyContents);
    }
    basePtr = (char*)memInfo.BaseAddress + memInfo.RegionSize;
  }
}
      
      



, , «MemoryScanner» github. ! ( , ymmv, ).





UTF-16

, UTF-16, , FindBytePatternInMemory (), UTF-16. . MemoryScanner github :





//convert input string to UTF16 (hackily)
const size_t patternLen = strlen(argv[2]);
char* pattern = new char[patternLen*2];
for (int i = 0; i < patternLen; ++i){
  pattern[i*2] = argv[2][i];
  pattern[i*2 + 1] = 0x0;
}
      
      



, , WriteProcessMemory . , , , Edit.





, Win32 api InvalidateRect, .





, :





void UpdateText(HINSTANCE process, HWND editWindow, char* notepadTextBuffer, char* replacementTextBuffer, int len)
{
  size_t written = 0;
  WriteProcessMemory(process, notepadTextBuffer, replacementTextBuffer, len, &written);

  RECT r;
  GetClientRect(editWindow, &r);
  InvalidateRect(editWindow, &r, false);
}
      
      



. , , , , ,  .





:

















. MoveWindow , , .





, , ( ) , . MoveWindow , WM_CHAR . , .





, , , WM_CHAR.





, . github , .





void PreallocateTextBuffer(DWORD processId)
{
  HWND editWindow = GetWindowForProcessAndClassName(processId, "Edit");

  // it takes 131 * 30 chars to fill a 1365x768 window with Consolas (size 11) chars
  MoveWindow(instance.topWindow, 100, 100, 1365, 768, true); 

  size_t charCount = 131 * 30;
  size_t utf16BufferSize = charCount * 2;

  char* frameBuffer = (char*)malloc(utf16BufferSize);
  for (int i = 0; i < charCount; i++){
    char v = 0x41 + (rand() % 26);
    PostMessage(editWindow, WM_CHAR, v, 0);
    frameBuffer[i * 2] = v;
    frameBuffer[i * 2 + 1] = 0x00;
  }

  Sleep(5000); //wait for input messages to finish processing...it's slow. 
  //Now use the frameBuffer as the unique byte pattern to search for
}
      
      



, , , .





. , (Consolas, 11pt), - WM_SETFONT , , . Consolas 11pt , .





, , , . , ScratchAPixel . , .





, . WriteProcessMemory ( , ), , ( * 2 (- UTF16)). , WriteProcessMemory . :





void drawChar(int x, int y, char c); //local buffer
void clearScreen(); // local buffer
void swapBuffersAndRedraw(); // pushes changes and refreshes screen. 
      
      



, , (131 x 30), , «» . , , , , ascii. , .





. , , . , «» , , .





float aspect = (0.5f * SCREEN_CHARS_WIDE) / float(SCREEN_CHARS_TALL);
      
      



, , , . , , , WM_VSCROLL, « » , . , , , , .





2: Boogaloo!





La prochaine (et dernière) partie de ma quête pour créer un jeu en temps réel dans le Bloc-notes consistait à déterminer comment gérer les entrées des utilisateurs. Si vous en voulez plus, le prochain article peut être trouvé ici !








All Articles