RPC на C++ своими руками. Часть 1

- - posted in rpc | Comments

RPC через CreateRemoteThread под Windows

Мое исследование возможных реализаций RPC. Для начала рассмотрим реализацию RPC через создание потока в удаленном процессе.

Одной из основных проблем при проектировании сложных систем является обеспечение взаимодействия между различными комплексами. Трудно представить, какое количество велосипедов было изобретено и ежеминутно куется в программных мануфактурах. Сетевое взаимодействие так возбуждает системных архитекторов, что они просто не могут остановиться и не написать протокол информационно-логического взаимодействия страниц так на 150. В таких протоколах обычно расписывается куча квитанций, которые обеспечивают надежность передачи данных. Самый эпичный велосипед, который я видел, реализовывал функциональность TCP поверх UDP. Интересно, что самый предпочтительный с точки зрения прозрачности и дальнейшей поддержки подход – RPC – считается сложным и ресурсоемким. И, если с последним можно отчасти согласиться, то вот насчет сложности можно поспорить. Для того чтобы RPC стал простым и понятным, я решил для себя собрать возможные реализации RPC под различные платформы. Для начала попробую сделать вызов функции в другом исполняемом модуле под ОС Windows.

Исходные данные:

  • Есть два исполняемых модуля A.exe и B.exe
  • В модуле B.exe есть функция foo

    Задача

  • Выполнить из модуля A.exe функцию foo

    Решение

    Для того, чтобы вызвать функцию, нужно знать ее адрес. Не все знают, но любой исполняемый модуль (exe и dll) может экспортировать функции. При импорте функции мы узнаем RVA функции в модуле. Для того чтобы получить полный адрес, нам нужно узнать базовый адрес загрузки модуля. Итак, в модуле B.exe объявляем функцию void foo() и экспортируем ее с помощью def файла.

Код модуля B (main_B.cpp) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <Windows.h>
#include <tchar.h>

#include <thread>
#include <chrono>
#include <atomic>

std::atomic_flag working = {true};

void foo()
{
  working.clear();
}

int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
{
  auto ev = OpenEvent(EVENT_ALL_ACCESS, FALSE, _T("B_ready_mutex"));
  SetEvent(ev);

  while(working.test_and_set())
  {
      std::this_thread::sleep_for(std::chrono::milliseconds(1));
  }
  
  ::MessageBox(NULL, _T("call succeeded"), _T("Remote Call"), MB_OK);

  CloseHandle(ev);

  return 0;
}
Экспорт функции (export.def) download
1
2
EXPORTS
foo @1

Итак, в модуле B.exe определена функция foo, которая при вызове выставит флаг working в значение false. После чего цикл в функции WinMain завершится после очередной итерации. Событие ev создается для того, чтобы модуль B.exe успел проинициализироваться прежде, чем мы начнем вызывать функцию foo.

Модуль A будет посложнее.

Точка входа модуля A (main_A.cpp) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include "rpc_caller.hpp"

#include <locale>

int _tmain(int argc, TCHAR* argv[])
{
  std::locale::global(std::locale(""));

  TString full_path(argv[0]);
  TString path = full_path.substr(0, full_path.find_last_of(_T("/\\")) + 1);
  
  HandleHolder ev(CreateEvent(NULL, FALSE, FALSE, _T("B_ready_mutex")));
  
  char buf[256];
  wcstombs ( buf, (TString(_T("start ")) + path.c_str() + TString(_T("B.exe"))).c_str(), sizeof(buf) );
  std::system(buf);
  
  if(WaitForSingleObject(ev(), 3000) == WAIT_OBJECT_0)
      RPC_caller(_T("B.exe")).call_in_ipc("foo");

  return 0;
}

Для начала получаем путь к B.exe. Этот танец с бубном вокруг path нужен для запуска по относительному пути в случае, если текущий путь отличается от папки где лежат A.exe и B.exe.

Класс удаленного вызова процедур (rpc_caller.hpp) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
#ifndef RPC_CALLER_HPP
#define RPC_CALLER_HPP

#pragma comment( lib, "psapi" )

#include <Windows.h>
#include <tchar.h>

#include <tlhelp32.h>
#include <Psapi.h>

#include <vector>
#include <string>
#include <cstdint>

typedef std::basic_string<TCHAR, std::char_traits<TCHAR>, std::allocator<TCHAR> > TString;

class LibHolder
{
public:
  LibHolder(HMODULE hLib) : _hLib(hLib){}
  ~LibHolder()
  {
      if(_hLib)
          FreeLibrary(_hLib);
  }
  HMODULE operator()()
  {
      return _hLib;
  }
private:
  HMODULE _hLib;
};

class HandleHolder
{
public:
  HandleHolder(HANDLE handle) : _handle(handle){}
  ~HandleHolder()
  {
      if(_handle)
          CloseHandle(_handle);
  }
  HANDLE operator()()
  {
      return _handle;
  }
private:
  HANDLE _handle;
};

class RPC_caller
{
public:
  explicit RPC_caller(const TString& module_name) : mod_name(module_name){}

  bool call_in_ipc(const std::string& func_name)
  {
      bool res = false;
      LibHolder hModule(LoadLibraryEx(mod_name.c_str(), NULL, 0));
      
      std::uintptr_t remote_proc = (std::uintptr_t)GetProcAddress(hModule(), func_name.c_str());
      if(!remote_proc)
          return res;


      PROCESSENTRY32 entry;
      entry.dwSize = sizeof(PROCESSENTRY32);

      HandleHolder snapshot(CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL));

      if (Process32First(snapshot(), &entry) == TRUE)
      {
          while (Process32Next(snapshot(), &entry) == TRUE)
          {
              if (mod_name.compare(entry.szExeFile) == 0)
              {
                  HandleHolder hProcess(OpenProcess(PROCESS_ALL_ACCESS, FALSE, entry.th32ProcessID));
                  if(!hProcess())
                      continue;

                  res = run(hProcess(), remote_proc - (std::uintptr_t)hModule(), func_name);

                  break;
              }
          }
      }

      return res;
  }
private:
  bool run(HANDLE hProcess, std::uintptr_t remote_proc, const std::string& func_name)
  {
      void* param = nullptr;

      std::uintptr_t addr = this->GetModuleBase(hProcess, mod_name) + remote_proc;
      auto hThread = CreateRemoteThreadEx(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)(addr), param, 0, NULL, NULL);
      return (WaitForSingleObject(hThread, 3000) == WAIT_OBJECT_0);

  }
  std::uintptr_t GetModuleBase(HANDLE hProc, const TString& sModuleName)
  {
      TCHAR szBuf[255];
      DWORD cModules = 0;
      std::uintptr_t dwBase = -1;

      EnumProcessModules(hProc, NULL, 0, &cModules);
      
      DWORD len = cModules/sizeof(HMODULE);
      std::vector<HMODULE> hModules(len);

      if(EnumProcessModulesEx(hProc, &hModules.front(), len, &cModules, LIST_MODULES_ALL))
      {
          for(std::size_t i = 0; i < len; ++i)
          {
              if(GetModuleBaseName(hProc, hModules[i], szBuf, sizeof(szBuf)))
              {
                  if(sModuleName.compare(szBuf) == 0)
                  {
                      dwBase = reinterpret_cast<std::uintptr_t>(hModules[i]);
                      break;
                  }
              }
          }
          len = cModules/sizeof(HMODULE);
          hModules.resize(len);
      }

      return dwBase;
  }
private:
  TString mod_name;
};
#endif

Надеюсь, отсутствие комментариев не смущает, потому что код достаточно красноречив. Итак, для чего нужен RPC_caller? Функция call_in_ipc, собственно, вызывает удаленную функцию.

  • Для начала получаем RVA удаленной функции.
1
2
3
4
5
  LibHolder hModule(LoadLibraryEx(mod_name.c_str(), NULL, 0));

  std::uintptr_t remote_proc = (std::uintptr_t)GetProcAddress(hModule(), func_name.c_str());
  if(!remote_proc)
      return res;
  • Затем делаем копию списка текущих процессов. В этом списке нам нужно найти целевой модуль, из которого мы будем вызывать функцию.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 HandleHolder snapshot(CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL));

  if (Process32First(snapshot(), &entry) == TRUE)
  {
      while (Process32Next(snapshot(), &entry) == TRUE)
      {
          if (mod_name.compare(entry.szExeFile) == 0)
          {
              HandleHolder hProcess(OpenProcess(PROCESS_ALL_ACCESS, FALSE, entry.th32ProcessID));
              if(!hProcess())
                  continue;

              res = run(hProcess(), remote_proc - (std::uintptr_t)hModule(), func_name);

              break;
          }
      }
  }

  • После того как мы найдем искомый модуль, необходимо определить адрес загрузки модуля. Собственно, после этого нам остается только сложить базовый адрес модуля и RVA функции - в результате получаем реальный адрес в удаленном модуле.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
  std::uintptr_t GetModuleBase(HANDLE hProc, const TString& sModuleName)
  {
      TCHAR szBuf[255];
      DWORD cModules = 0;
      std::uintptr_t dwBase = -1;

      EnumProcessModules(hProc, NULL, 0, &cModules);

      DWORD len = cModules/sizeof(HMODULE);
      std::vector<HMODULE> hModules(len);

      if(EnumProcessModulesEx(hProc, &hModules.front(), len, &cModules, LIST_MODULES_ALL))
      {
          for(std::size_t i = 0; i < len; ++i)
          {
              if(GetModuleBaseName(hProc, hModules[i], szBuf, sizeof(szBuf)))
              {
                  if(sModuleName.compare(szBuf) == 0)
                  {
                      dwBase = reinterpret_cast<std::uintptr_t>(hModules[i]);
                      break;
                  }
              }
          }
          len = cModules/sizeof(HMODULE);
          hModules.resize(len);
      }

      return dwBase;
  }
  • Теперь дело осталось за малым – запустить функцию в адресном пространстве другого процесса. Для этого в Windows существует функция CreateRemoteThread. Эта функция создает поток в целевом процессе, который выполняет заданную функцию. Собственно, поэтому в B.exe я использовал std::atomic_flag.
1
2
3
4
5
6
7
8
9
  bool run(HANDLE hProcess, std::uintptr_t remote_proc, const std::string& func_name)
  {
      void* param = nullptr;

      std::uintptr_t addr = this->GetModuleBase(hProcess, mod_name) + remote_proc;
      auto hThread = CreateRemoteThreadEx(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)(addr), param, 0, NULL, NULL);
      return (WaitForSingleObject(hThread, 3000) == WAIT_OBJECT_0);

  }

В результате мы получили вызов функции в удаленном процессе. Однако для полноценного RPC не хватает передачи в функцию параметров и приема результата выполнения. Об этом подробно расскажу во второй части.

Исходный код

Comments