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

- - posted in rpc | Comments

Продолжение статьи об удаленном вызове функции с помощью CreateRemoteThread. Эта часть посвящена вызову функции с передачей параметра и возвратом кода результата.

RPC через CreateRemoteThread под Windows с передачей параметров в функцию

Итак, в первой части я сделал вызов функции одного процесса из другого. Но вызов этот был не вполне полноценным, потому что я не анализировал результат выполнения и не передавал параметры на вход. Для начала определимся, как вернуть результат из вызываемой функции. Так как функция у нас вызывается с помощью CreateRemoteThreadEx, то получить результат выполнения потока можно с помощью GetExitCodeThread. А это накладывает очевидное ограничение – эта функция может вернуть только 4 байта. Печально. Но если нужно вернуть больше, можно воспользоваться возвращением результата через аргументы функции. Переходим к передаче параметров функции. И опять ограничивающим фактором является использование CreateRemoteThreadEx, который позволяет передать в функцию аргумент размером: на x864 байта, на x648 байт.

Меняем сигнатуру вызываемой функции (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
31
32
33
34
35
36
37
#include <Windows.h>
#include <tchar.h>

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

std::atomic_flag working = {true};

std::wstring input_string;

int foo(const wchar_t* str)
{
  input_string = str;

  working.clear();

  return 0xAA55AA55;
}

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, input_string.c_str(), _T("Remote Call"), MB_OK);

  CloseHandle(ev);

  return 0;
}

В RPC_caller я перенес в конструктор подгрузку целевого модуля, что позволит не делать этого при каждом вызове. А также функция run была шаблонизирована, чтобы обрабатывать различные типы возврата функции в пределах 4х байт.

Шаблонизированный RPC_caller (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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
#ifndef RPC_CALLER_HPP
#define RPC_CALLER_HPP

#pragma comment( lib, "psapi" )

#include <Windows.h>

#include <cstdio>
#include <tlhelp32.h>
#include <TlHelp32.h>
#include <Psapi.h>
#include <tchar.h>

#include <vector>
#include <string>
#include <iostream>
#include <cassert>
#include <locale>
#include <codecvt>

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

class rpc_call_error : public std::exception
{
  std::string msg;
public:
  rpc_call_error(const std::string& err)
  {
      msg = err;
  }
  rpc_call_error(const std::wstring& err)
  {
      msg = std::wstring_convert<std::codecvt_utf8<wchar_t>>().to_bytes(err);
  }
  const char* what() const override
  {
      return msg.c_str();
  }
};

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

class HandleHolder
{
  HANDLE _handle;
public:
  HandleHolder() : _handle(0)
  {}
  HandleHolder(HANDLE handle) : _handle(handle){}
  ~HandleHolder()
  {
      try
      {
          Free();
      }
      catch(...)
      {}
  }
  HandleHolder& operator = (const HANDLE& hVal)
  {
      Free();
      _handle = hVal;

      return *this;
  }
  HANDLE operator()()
  {
      return _handle;
  }
private:
  void Free()
  {
      if(_handle)
          CloseHandle(_handle);
  }
};

class RPC_caller
{
  LibHolder hModule;
  HandleHolder hProcess;
  DWORD base;
public:
  explicit RPC_caller(const TString& module_name) : hModule(LoadLibraryEx(module_name.c_str(), NULL, 0))
  {
      PROCESSENTRY32 entry;
      entry.dwSize = sizeof(PROCESSENTRY32);

      HandleHolder snapshot(CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL));

      if (Process32First(snapshot(), &entry) == TRUE)
      {
          while (Process32Next(snapshot(), &entry) == TRUE)
          {
              if (module_name.compare(entry.szExeFile) == 0)
              {
                  hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, entry.th32ProcessID);
                  if(!hProcess())
                      throw rpc_call_error(TString(_TEXT("Unable to open process ")) + module_name);
                  base = this->GetModuleBase(hProcess(), module_name);
              }
          }
      }
  }

  template <class R, class T, int time_out>
  R call_in_ipc(const std::string& func_name, T* val, int size)
  {
      assert(hProcess());
      
      int remote_proc = (int)GetProcAddress(hModule(), func_name.c_str());
      if(!remote_proc)
          throw std::invalid_argument(std::string("No function ") + func_name + " found");

      return std::move(run<typename R, typename T, time_out>(hProcess(), remote_proc - (int)hModule(), func_name, val, size));
  }
private:
  template <class R, class T, int time_out>
  R run(HANDLE hProcess, int remote_proc, const std::string& func_name, T* user_param, int size)
  {
      static_assert(sizeof(R) <= sizeof(DWORD), "Only 4 byte types can be used as return type of a run function");
      void* param = nullptr;

      if(user_param)
      {
          param = VirtualAllocEx(hProcess, NULL, size, MEM_COMMIT, PAGE_READWRITE);
          WriteProcessMemory(hProcess, param, (void*)user_param, size, NULL);
      }

      int addr = base + remote_proc;
      auto hThread = CreateRemoteThreadEx(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)(addr), param, 0, NULL, NULL);
      if(WaitForSingleObject(hThread, time_out) != WAIT_OBJECT_0)
          throw std::runtime_error("call failed");

      DWORD res = -1;
      GetExitCodeThread(hThread, &res);

      return transform_return<R>(res);
  }
  template <class R>
  R transform_return(const DWORD& res)
  {
      static_assert(sizeof(R) <= 4, "Return type can not be longer than 4 bytes");

      static const int mask[] = {0xFF, 0xFFFF, 0xFFFFFF, 0xFFFFFFFF};
      return std::move(R(res & mask[sizeof(R) - 1]));
  }
  template <>
  bool transform_return(const DWORD& res)
  {
      return std::move((res & 0xFF)!=0);
  }

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

      if(!EnumProcessModules(hProc, NULL, 0, &cModules))
          throw rpc_call_error(TString(_TEXT("Module ")) + sModuleName + _TEXT(" is not loaded"));
      
      auto len = cModules/sizeof(HMODULE);
      std::vector<HMODULE> hModules(len);

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

      return dwBase;
  }
};
#endif

А теперь подробнее о ключевых моментах:

1
2
3
4
5
6
7
8
9
10
template <class R, class T, int time_out>
R call_in_ipc(const std::string& func_name, T* val, int size)
{
  assert(hProcess());
  
  int remote_proc = (int)GetProcAddress(hModule(), func_name.c_str());
  if(!remote_proc)
      throw std::invalid_argument(std::string("No function ") + func_name + " found");

  return std::move(run<typename R, typename T, time_out>(hProcess(), remote_proc - (int)hModule(), func_name, val, size));

При инстанциировании задается время ожидания выполнения целевой функции - time_out.

Для передачи параметра в удаленную функцию используется функция VirtualAllocEx. Она позволяет выделить память в целевом процессе, после чего можно заполнить ее требуемыми данными. Указатель на заполненный буфер затем передается как параметр в функцию CreateRemoteThreadEx.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
template <class R, class T, int time_out>
R run(HANDLE hProcess, int remote_proc, const std::string& func_name, T* user_param, int size)
{
  static_assert(sizeof(R) <= sizeof(DWORD), "Only 4 byte types can be used as return type of a run function");
  void* param = nullptr;

  if(user_param)
  {
      param = VirtualAllocEx(hProcess, NULL, size, MEM_COMMIT, PAGE_READWRITE);
      WriteProcessMemory(hProcess, param, (void*)user_param, size, NULL);
  }

  int addr = base + remote_proc;
  auto hThread = CreateRemoteThreadEx(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)(addr), param, 0, NULL, NULL);
  if(WaitForSingleObject(hThread, time_out) != WAIT_OBJECT_0)
      throw std::runtime_error("call failed");

  DWORD res = -1;
  GetExitCodeThread(hThread, &res);

  return transform_return<R>(res);
}

Для того чтобы корректно обработать возвращаемый результат, введена функция transform_return. Она позволяет наложить маску на возвращаемые GetExitCodeThread 4 байта. Специализированная функция для типа bool нужна для того, чтобы избежать предупреждения компилятора о принудительном приведении к типу bool.

1
2
3
4
5
6
7
8
9
10
11
12
13
template <class R>
R transform_return(const DWORD& res)
{
  static_assert(sizeof(R) <= 4, "Return type can not be longer than 4 bytes");

  static const int mask[] = {0xFF, 0xFFFF, 0xFFFFFF, 0xFFFFFFFF};
  return std::move(R(res & mask[sizeof(R) - 1]));
}
template <>
bool transform_return(const DWORD& res)
{
  return std::move((res & 0xFF)!=0);
}

Для того чтобы передать более сложную структуру, придется немного попотеть. Может потом как-нибудь опишу.

Осталось подвести некоторые итоги.

Плюсы:

  • Передаваемые данные находятся только в памяти взаимодействующих процессов
  • Вызов достаточно прост и надежен

Минусы:

  • Ограничение на количество параметров
  • Ограничение на возвращаемый тип

В следующий раз опишу взаимодействие через boost::ipc - это очень интересная и полезная библиотека.

Исходный код

Comments