В данной статье мы подробно поговорим об устройстве RPC. Также для лучшего понимания применим знания на практике и напишем свое RPC-приложение под Windows. Вся информация взята из официальной документации Microsoft, также из доки по протоколу RPC.
Общая идея
Remote Procedure Call позволяет клиенту вызывать функцию (процедуру), которая будет выполняться на сервере, а затем получать результат выполнения это функции, будто бы данная функция была вызвана локально.
Представьте, что вам, к примеру, по какой-то причине нужно получить список всех пользователей, которые в данный момент подключены к определенной машине. Возможно, у вас есть два DHCP-сервера в одной сети, которым надо синхронизировать информацию друг с другом, передавая друг другу конфигурационные файлы. Или, быть может, вам надо зашифровать какие-то данные ключом, который находится на удаленном сервере, при этом не зная самого этого ключа, поручив шифрование серверу. В реализации всех этих задач вам может помочь RPC.
Концепция работы RPC
Для RPC, как было сказано ранее, нужен клиент и сервер. Сервер реализовывает функционал процедуры, а клиент вызывает её, получая обратно результат.
Для того, чтобы клиент смог вызвать процедуру, он должен каким-то образом сообщить серверу: какую функцию он хочет вызвать и какие аргументы он передает. Сервер же должен отправить пользователю результат выполнения данной функции, либо сообщение об ошибке.
Работу, связанную с сетевым взаимодействием, берет на себя runtime-библиотека. Все, что нужно клиенту - это пользование API, которое предоставляет библиотека.
Для начала сервер должен описать интерфейсы, которые он делает доступными для пользователя. Интерфейс содержит в себе описания функций, которые доступны для вызова из данного интерфейса. Клиент, подключаясь к серверу указывает нужный ему интерфейс, а затем функцию, которую он хочет вызвать внутри этого интерфейса. Описание интерфейса содержится в .idl-файле (interface description language).
[ uuid(fba5a6ed-312d-4d81-9a2e-4733c50296c6), version(1.0) ] interface Greeting { [string] char* sayHi ([in, string] char* name); } [ uuid(1ba615e7-e2a5-45bb-949b-74d61c0f5453), version(1.0) ] interface Calculator { int add([in] int a, [in] int b); int subtract([in] int a, [in] int b); int multiply([in] int a, [in] int b); int divide([in] int a, [in] int b); }
В данном случае мы описали два интерфейса Greeting и Calculator. В начале в квадратных скобках мы указываем UUID интерфейса, а затем его версию. В фигурных скобках мы описываем функции в стиле прототипов C/C++, которые затем будет реализовывать сервер.
После скармливаем это компилятору MIDL и получаем три файлика: MyApp_c.c, MyApp_s.c и MyApp_h.h. Файл MyApp_c.c является Client Stub. В нём содержатся функции-заглушки, которые вызываются клиентом. В них выполняется маршаллинг - перевод параметров в формат NDR, пригодный для передачи по сети. NDR позволяет избежать проблем, при которых на клиенте данные хранятся одним способом, а на сервере - другим. После маршаллинга рантайм-библиотека формирует протокольное сообщение, указывая в нём какой интерфейс и какую функцию нужно вызвать, и отправляет его серверу.
На сервере рантайм-библиотека принимает RPC-сообщение и вызывает Server Stub, который описан в файле MyApp_s.c. Sever Stub выполняет демаршалинг и вызывает реальную имплементацию нужной нам функции. Затем эта функция возвращает результат своей работы в Server Stub, который переводит данные в NDR, вызывает рантайм-библиотеку, которая отправляет результат работы обратно клиенту.
В финале клиентский рантайм принимает запрос, отправляет данные в Client Stub, тот выполняет демаршалинг и возвращает результат выполнения функции.Программа на стороне клиента получает этот результат и продолжает работу так, как будто она получила данные из обычной локальной функции.

Пишем сервер
После того, как MIDL создал нам нужные файлики, можно приступать к написанию нашего приложения. Для начала на сервере нужно зарегистрировать интерфейс с помощью функции RpcServerRegisterIf2.
RPC_STATUS status; status = RpcServerRegisterIf2( Calculator_v1_0_s_ifspec, nullptr, nullptr, RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH, RPC_C_LISTEN_MAX_CALLS_DEFAULT, (unsigned int)-1, nullptr );
Первым аргументом мы засовываем указатель на структуру, описывающую наш интерфейс. Имя строится по принципу <interface_name>_<version>_s_ifspec. Четвертый аргумент - флаг, который разрешает неаутентифицированные вызовы. Остальные флаги не стоят нашего внимания, любознательный читатель всегда может обратиться к официальной документации и/или подоставать ИИ.
Следующим шагом идет указание протокольных последовательностей (protocol sequence). Протокольная последовательность определяет: с помощью каких протоколов клиент и сервер будут обмениваться RPC-сообщениями. RPC поддерживает кучу протокольных последовательностей, включая, например, IPX, AppleTalk, IP с UDP и TCP, Named Pipes и много чего еще. Для указания того, какой протокольной последовательностью будет пользоваться сервер, мы вызываем функцию RpcServerUseProtseq.
RPC_WSTR prot_seq = (RPC_WSTR)L"ncacn_ip_tcp"; status = RpcServerUseProtseq( prot_seq, RPC_C_PROTSEQ_MAX_REQS_DEFAULT, //Constant equals to 10 nullptr );
Первым аргументом мы указываем протокольную последовательность, вторым следует следует размер TCP-бэклога для входящих соединений (актуально, очевидно, только для ncacn_ip_tcp). Третий параметр - указатель на дескриптор безопасности (аткуально только для ncacn_np и ncalrpc).
Сервер может поддерживать несколько протокольных последовательностей, например, он может быть доступен, используя именованные каналы и UDP. Для этого он просто несколько раз может вызвать RpcServerUseProtseq. Клиент в таком случае волен выбирать: как он будет взаимодействовать с сервером.
Помимо протокольных последовательностей в RPC есть такая штука, как endpoint. Endpoint позволяет идентифицировать процесс, на котором крутится сервер, содержащий интерфейсы. Разным протокольным последовательностям соответствуеют разные эндпоинты. Для ncacn_ip_tcp и ncacn_ip_udp это, к примеру, порт. Для ncacn_np - именованный канал и т.д.
Сочетание протокольной последовательности, адреса хоста и эндпоинта называется string binding. Ниже представлены примеры string binding.
ncacn_np:\\DESKTOP-BFD4CT0[\pipe\88d5090aed22a39a]
ncacn_ip_tcp:DESKTOP-BFD4CT0[53354]
ncalrpc:DESKTOP-BFD4CT0[LRPC-9c355872986fcf0737]
ncadg_ipx: ~0000000108002B30612C[5000]
P.S.
String Binding также могут содержать опции и UUID объекта. Подробнее с этим можете ознакомиться сами. Также адрес хоста может различаться для каждой протокольной последовательности.
Когда сервер регистрирует протокольную последовательность, то сервер динамически выделяет endpoint для этой протокольной последовательности. Также мы, в принципе, можем задать endpoint и сами, используя функцию RpcServerUseProtseqEp. В обоих случаях, клиенту надо как-то знать: какой endpoint соответствует нашему приложению. Для того, чтобы клиент узнал нужный ему endpoint, используется endpoint mapper,слушающий на 135 порту, который принимает UUID интерфейса и возвращает endpoint. Работу endpoint mapper'а можно сравнить с работой DNS-сервера.
Для того, чтобы endpoint mapper отвечал на запросы пользователей, мы должны обновить его базу данных, сообщив ему об endpoint'ах.
RPC_BINDING_VECTOR* rpcBindingVector; status = RpcServerInqBindings(&rpcBindingVector); annotation = (RPC_WSTR)L"Sums, subtracts, multiplies or divides provided numbers"; status = RpcEpRegister( Calculator_v1_0_s_ifspec, rpcBindingVector, nullptr, annotation);
Для начала мы получаем список с Server Binding Handle'ами. Server Binding Handle это указатель на структуру, которая содержит всю необходимую информацию, которая нужна для подключения к серверу (включая protocol sequence, host address и endpoint. Доподлинно неизвестно, какая инфа там содержится, ибо это opaque pointer). Так как как мы можем создать несколько протокольных последовательностей, то биндингов тоже может быть несколько. С помощью функции RpcServerInqBindings мы получаем массив наших биндингов, который потом передаем в функцию RpcEpRegister, чтобы сообщить Endpoint Mapper: какие string binding соответсвуют нашему интерфейсу.
Далее нам надо вызвать функцию RpcServerListen, которая запустит наш сервак.
status = RpcServerListen( 1, RPC_C_LISTEN_MAX_CALLS_DEFAULT, 0 );
Конечно же, нам надо также определить функции, которые будет реализовывать наш сервер. Помимо этого нам также надо реализовать функции, которые будут аллоцировать и освобождать память при маршалинге и анмаршилинге данных в stub'е.
server.cpp
#include "functions.h" using std::string, std::wcout, std::cout, std::endl; int main() { RPC_STATUS status; //Creating greeting interface status = RpcServerRegisterIf2( Greeting_v1_0_s_ifspec, nullptr, nullptr, RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH, RPC_C_LISTEN_MAX_CALLS_DEFAULT, (unsigned int) - 1, nullptr ); if (status) exit(status); //Creating Calculator interface status = RpcServerRegisterIf2( Calculator_v1_0_s_ifspec, nullptr, nullptr, RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH, RPC_C_LISTEN_MAX_CALLS_DEFAULT, (unsigned int)-1, nullptr ); if (status) exit(status); //Creating protocol sequences assosiated with our interfaces RPC_WSTR prot_seq = (RPC_WSTR)L"ncacn_ip_tcp"; status = RpcServerUseProtseq( prot_seq, RPC_C_PROTSEQ_MAX_REQS_DEFAULT, //Maximal size of SYN queue nullptr // Security descriptor (relevant for ncalrpc and ncacn_np) ); if (status) exit(status); prot_seq = (RPC_WSTR)L"ncalrpc"; status = RpcServerUseProtseq( prot_seq, RPC_C_PROTSEQ_MAX_REQS_DEFAULT, nullptr ); if (status) exit(status); /* * Create vector holding bindings. Binding holds information about prot_seq, address and port. * There can be more than one bindings, if you create multiple protocol sequences */ RPC_BINDING_VECTOR* rpcBindingVector; status = RpcServerInqBindings(&rpcBindingVector); if (status) exit(status); //Tell endpoint mapper about Greeting interface RPC_WSTR annotation = (unsigned short*)L"Takes the name and returns the greeting"; status = RpcEpRegister( Greeting_v1_0_s_ifspec, rpcBindingVector, nullptr, annotation); if (status) exit(status); //Tell the endpoint mapper about Calculator interface annotation = (unsigned short*)L"Sums, subtracts, multiplies or divides provided numbers"; status = RpcEpRegister( Calculator_v1_0_s_ifspec, rpcBindingVector, nullptr, annotation); if (status) exit(status); //Start to listen status = RpcServerListen( 1, RPC_C_LISTEN_MAX_CALLS_DEFAULT, 0); if (status) exit(status); return 0; }
functions.h
#include <iostream> #include <string> #include <windows.h> #include <string.h> #include <Ntdsapi.h> #include <authz.h> #include <sddl.h> #include "MyApp_h.h" #pragma comment(lib, "rpcrt4.lib") #pragma comment(lib,"Ntdsapi.lib") #pragma comment(lib,"Authz.lib") #pragma comment(lib,"Advapi32.lib") void* midl_user_allocate(size_t len); void midl_user_free(void __RPC_FAR* ptr); unsigned char* sayHi(handle_t user_handle, unsigned char* name); int add(handle_t user_handle, int a, int b); int subtract(handle_t user_handle, int a, int b); int multiply(handle_t user_handle, int a, int b); int divide(handle_t user_handle, int a, int b);
functions.cpp
#include "functions.h" using std::string, std::wcout, std::cout, std::endl; void* midl_user_allocate(size_t len) { return(malloc(len)); } void midl_user_free(void __RPC_FAR* ptr) { free(ptr); } unsigned char* sayHi(handle_t user_handle, unsigned char* name) { string greeting = "Hello, " + string((char*)name) + "!"; size_t len = greeting.size() + 1; unsigned char* result = (unsigned char*)malloc(len); memcpy(result, greeting.c_str(), len); return result; } int add(handle_t user_handle, int a, int b) { return a + b; } int subtract(handle_t user_handle, int a, int b) { return a - b; } int multiply(handle_t user_handle, int a, int b) { return a * b; } int divide(handle_t user_handle, int a, int b) { return a / b; }
Скомпилируем наш сервак и с помощью утилиты rpcdump.py из пакета impacket выведем информацию обо всех доступным endpoint'ах, в том числе и о наших.

В выводе программы мы видим два созданных нами интерфейса, их UUID, версию, аннотацию и биндинги.
Пишем клиента
Для начала нам нужно создать Server Binding, который мы сообщим рантайм библиотеке, чтобы она знала к кому мы хоти подключиться. Как можно видеть, мы можем указать endpoint. В таком случае нам не придется стучаться к endpoint mapper, чтобы узнать на каком endpoint'е висит сервак. Если же мы укажем nullptr, то рантайм побежит к endpoint mapper.
RPC_STATUS status; RPC_WSTR StringBinding; RPC_BINDING_HANDLE BindingHandle; RPC_WSTR prot_seq = (RPC_WSTR)L"ncacn_ip_tcp"; RPC_WSTR host_address = (RPC_WSTR)L"127.0.0.2"; RPC_WSTR endpoint = nullptr; //Creates string binding using provided parameters status = RpcStringBindingCompose( nullptr, //Object UUID prot_seq, host_address, endpoint, nullptr, //Options &StringBinding); RpcBindingFromStringBinding(StringBinding, &BindingHandle);
После, все что нам надо, это вызвать нужную нам функцию
//Call the procedure RpcTryExcept { cout << multiply(BindingHandle, 2,3); } RpcExcept(1) { unsigned long ulCode = RpcExceptionCode(); std::cerr << "Runtime reported exception 0x" << std::hex << ulCode << " = " << std::dec << ulCode << '\n'; } RpcEndExcept
client.cpp
#include <iostream> #include <windows.h> #include "Ntdsapi.h" #include "MyApp_h.h" #pragma comment(lib, "rpcrt4.lib") #pragma comment(lib,"Ntdsapi.lib") using std::cout, std::cerr, std::hex, std::dec; int main() { RPC_STATUS status; RPC_WSTR StringBinding; RPC_BINDING_HANDLE BindingHandle; RPC_WSTR prot_seq = (RPC_WSTR)L"ncacn_ip_tcp"; RPC_WSTR host_address = (RPC_WSTR)L"127.0.0.2"; RPC_WSTR endpoint = nullptr; //Creates string binding using provided parameters status = RpcStringBindingCompose( nullptr, //Object UUID prot_seq, //Protocol sequence to use host_address, //Server DNS or Netbios Name NULL if the host is local machine endpoint, //Endpoint nullptr, //Options &StringBinding); if (status) exit(status); //Creates binding handle from the string binding status = RpcBindingFromStringBinding(StringBinding, &BindingHandle); //Call the procedure RpcTryExcept { cout << multiply(BindingHandle, 2,3); } RpcExcept(1) { unsigned long ulCode = RpcExceptionCode(); std::cerr << "Runtime reported exception 0x" << std::hex << ulCode << " = " << std::dec << ulCode << '\n'; } RpcEndExcept RpcStringFree(&StringBinding); RpcBindingFree(&BindingHandle); return 0; } //As for the server stub we have to create allocate and free functions for the client stub void* midl_user_allocate(size_t len) { return(malloc(len)); } void midl_user_free(void __RPC_FAR* ptr) { free(ptr); }
Копаемся в трафике

Первое сообщние отправляется на 135 порт Endpoint Mapper'у. Это bind-сообщение, которое используется для установки соединения (да, общение с EP просиходит также по RPC). Внутри bind-сообщения мы можем найти, в каком формате у нас представлены данные, которые Client Stub передает Server Stub. Далее следует длина фрагмента данных, которая, кстати, совпадает с длиной пейлоада в TCP (взгляните в правый верхний угол). После мы видим, что длина аутентификаицонных данных нулевая, так как мы к ней не прибегали.

Далее идет согласование Presentation Context (сочетание Abstract Syntax и Transfer Syntax). B Abstract Syntax мы говорим: к какому интерфейсу какой версии мы желаем подключиться, а в Transfer Syntax мы предоставляем дополнительную информацию, например, в Ctx Item 1 и 2 мы говорим, что можем использовать как 64bit NDRv1, так и 32bit NDRv2 (далее сервер должен будет выбрать, какой именно Presentation Conetxt он предпочтет). В Ctx Item 3 клиент сообщает о некоторых дополнительных возможностях, которые он поддерживает.

В bind acknowledge сервер присылает нам ответы на Context Item'ы. Сервер сказал, что он отклоняет Context Item с Context ID 0,а с номером 1 принимает (это значит, что он готов использовать 64bit NDRv1). В ответе на Context Item с Context ID 2 сервер прислал инфу о своих дополнительных возможностях.

После подключения к Endpoint Mapper мы начинаем вызывать его функции (напомним, что EP это такое же RPC-приложение). Помимо всего прочего в вызове мы видим Context ID, равный единице, который был согласован на прошлом шаге. Докучи мы видим opnum. Opnum - это ��омер функции в интерфейсе, которую мы собираемся вызвать.

Далее идет stub data. Мы видим, что процедура под номером три - это функция map. Операции нумеруются по порядку объявления в файле .idl. Для endpoint mapper, например, ept_insert = 0, ept_delete = 1, ept_lookup = 2, ept_map = 3.
.idl-файл для Endpoint Mapper
[uuid(e1af8308-5d1f-11c9-91a4-08002b14a0fa), version(3.0), pointer_default(ptr)] interface ept { const long ept_max_annotation_size = 64; typedef struct { uuid_t object; twr_p_t tower; [string] char annotation[ept_max_annotation_size]; } ept_entry_t, *ept_entry_p_t; typedef [context_handle] void *ept_lookup_handle_t; /* * E P T _ I N S E R T */ void ept_insert( [in] handle_t h, [in] unsigned32 num_ents, [in, size_is(num_ents)] ept_entry_t entries[], [in] boolean32 replace, [out] error_status_t *status }; /* * E P T _ D E L E T E */ void ept_delete( [in] handle_t h, [in] unsigned32 num_ents, [in, size_is(num_ents)] ept_entry_t entries[], [out] error_status_t *status ); /* * E P T _ L O O K U P */ [idempotent] void ept_lookup( [in] handle_t h, [in] unsigned32 inquiry_type, [in] uuid_p_t object, [in] rpc_if_id_p_t interface_id, [in] unsigned32 vers_option, [in, out] ept_lookup_handle_t *entry_handle, [in] unsigned32 max_ents, [out] unsigned32 *num_ents, [out, length_is(*num_ents), size_is(max_ents)] ept_entry_t entries[], [out] error_status_t *status ); /* * E P T _ M A P */ [idempotent] void ept_map( [in] handle_t h, [in] uuid_p_t object, [in] twr_p_t map_tower, [in, out] ept_lookup_handle_t *entry_handle, [in] unsigned32 max_towers, [out] unsigned32 *num_towers, [out, length_is(*num_towers), size_is(max_towers)] twr_p_t towers[], [out] error_status_t *status ); /* * E P T _ L O O K U P _ H A N D L E _ F R E E */ void ept_lookup_handle_free( [in] handle_t h, [in, out] ept_lookup_handle_t *entry_handle, [out] error_status_t *status ); /* * E P T _ I N Q _ O B J E C T */ [idempotent] void ept_inq_object( [in] handle_t h, [out] uuid_t *ept_object, [out] error_status_t *status ); /* * E P T _ M G M T _ D E L E T E */ void ept_mgmt_delete( [in] handle_t h, [in] boolean32 object_speced, [in] uuid_p_t object, [in] twr_p_t tower, [out] error_status_t *status ); }
Далее идет UUID объекта (в данной статье объекты не рассматриваются, чтобы не перегружать и так перегруженное повествование, гляньте сами в доке). Далее идет информация о запрашиваемом нами Binding'е. Выглдядит это все в виде башни с уровнями, где каждый уровень сообщает некоторую информацию.
Мы видим UUID интерфейса, который соответсвует интерфейсу Calculator, который мы собираемся вызвать.
Далее мы сообщаем то, как будет кодировать данные, затем, что хотим использовать протокол, который поддерживает соединение.
После указываем, что это TCP (и зачем-то сообщает 135 порт. скорее всего это какая-то заглушка).
В конце мы говорим, что на сетевом уровне у нас будет протокол IP и указываем адрес целевого сервера.

В конце мы получаем ту же башню, но на 4 этаже которой мы видим искомый порт, на котором крутится наш сервак.

Узнав, как достучатся до сервера, мы скорее бежим к нему, повторяя ту же историю с bind и bind acknowldge, а затем обмениваемся данными. В stub data мы видим, что клиентское приложение передало серверу два числа: 2 и 3, и вызвало функцию multiply, передав в opnum число 2.

Сервер же посчитал результат и передал его клиенту.

Добавляем аутентификацию и авторизацию
Если клиент хочет использовать аутентификацию, то он отправляет об этом информацию в bind-сообщении. Он говорит о том, с помощью чего он будет аутентифицироваться, а также говорит об уровне аутентификации. После чего (конкретно в случае NTLM) сразу кидает NTLM-NEGOTHIATE-сообщения, знаменуя тем самым начало обмена NTLM-сообщениями. Windows поддерживает следующие возможные варианты уровня аутентификации и следующие варианты методов аутентификации.

Важный нюанс: клиент и сервер никак не согласуют то, как будет происходить аутентификация. Клиент просто констатирует, мол, я буду юзать вот это, вот это; сервер же может либо согласиться, продолжив аутентификацию, либо прислать пакет сообщение, в котором он отклоняет выбранный пользователем метод.
Чтобы сообщить runtime-библиотеке о желании аутентификации, надо вызвать функцию RpcBindingSetAuthInfo. В нее мы засовываем Server Binding Handle, куда запишется аутентификационная информация, затем мы засовываем туда константу, обозначающую аутентификационный уровень. После мы сообщаем о том, какой метод аутентификации мы будем юзать. Затем мы заполняем структуру данных, которая содержит наши аутентификационные данные. Указывая NULL в параметре, отвечающем за порт в SPN, мы говорим runtime-библе, что мы его получим от Endpoint Mapper.
// Makes server's SPN LPCWSTR service_class = L"CustomService"; LPCWSTR service_name = L"DESKTOP-BFD4CT0"; DWORD len = 19; WCHAR spn[19]; DsMakeSpn( service_class, service_name, nullptr, //InstanceName NULL, //Instance port nullptr, &len, spn ); //Provide authentication data SEC_WINNT_AUTH_IDENTITY_A auth_identity{}; authz_identity.User = (unsigned char*)"artem"; authz_identity.UserLength = 5; authz_identity.Domain = (unsigned char*)"DESKTOP-BFD4CT0"; authz_identity.DomainLength = 15; authz_identity.Password = (unsigned char*)"admin"; authz_identity.PasswordLength = 5; authz_identity.Flags = SEC_WINNT_AUTH_IDENTITY_ANSI; status = RpcBindingSetAuthInfo( BindingHandle, (RPC_WSTR) spn, RPC_C_AUTHN_LEVEL_PKT_INTEGRITY, //Authentication level RPC_C_AUTHN_WINNT, //Authentication service &auth_identity, RPC_C_AUTHZ_NONE );
На сервере мы должны сообщить runtime-библиотеке информацию об аутентификации, указав наш SPN, а также поддерживаемый метод аутентификации. Указывая NULL в параметре, отвечающем за порт, мы говорим runtime-библе, что получим его динамически.
//Register service SPN LPCWSTR service_class = L"CustomService"; LPCWSTR service_name = L"DESKTOP-BFD4CT0"; DWORD len = 19; WCHAR spn[19]; DsMakeSpn( service_class, service_name, nullptr, //InstanceName NULL, //Instance port nullptr, &len, spn ); RPC_STATUS status; status = RpcServerRegisterAuthInfo( (RPC_WSTR)spn, RPC_C_AUTHN_WINNT, //Authentication package nullptr, //key generation function nullptr //parameters to pass to key generation function );
Для авторизации пользователя мы должны последним параметром в функции RpcServerRegisterIf2 предоставить ссылку на callback-функцию, которая возьмет на себя эту задачу.
status = RpcServerRegisterIf2( Greeting_v1_0_s_ifspec, nullptr, nullptr, NULL, // Flags RPC_C_LISTEN_MAX_CALLS_DEFAULT, (unsigned)-1, security_callback);
Авторизовывать пользователя можно различными способами. В данном случае в учебных целях мы реализуем простой метод чисто для демонстрации, который заключается в проверке SID клиента. Для того, чтобы получить информацию о клиенте мы вызываем функцию RpcGetAuthorizationContextForClient, в которую передаем указатель на Client Binding Handle. Если в Server Binding Handle содержалась та информация, которую клиент передает рантайму для подключения к серверу, то в Client Binding Handle содержится информация, которую сервер получил от runtime'а, при подключении клиента. По сути, когда Server Bindign Handle передается клиентом серверу, он становится Client Binding Handle.
Получив структуру user_context, где содержится инфа о клиенте, мы используем функцию AuthzGetInformationFromContext, чтобы получить интересующую нас инфу оттуда. В данном случае - это SID клиента. Затем мы проверяем полученную информацию и, если она нас удовлетовряет, возвращаем константу RPC_S_OK. Если же - нет, то возвращаем RPC_S_ACCESS_DENIED. В зависимости от возвращенного значения рантайн будет принимать решение - авторизовывать клиента или - нет.
RPC_STATUS security_callback(RPC_IF_HANDLE InterfaceUuid, void* cleint_binding_handle) { //Get user context PVOID user_context; LUID luid{}; RpcGetAuthorizationContextForClient( client_binding_handle, false, //Impersonate or not to nullptr, NULL, luid, NULL, nullptr, &user_context ); //Get the required buffer size DWORD buffer_size{}; DWORD required_size{}; PVOID buffer = nullptr; AuthzGetInformationFromContext( (AUTHZ_CLIENT_CONTEXT_HANDLE)user_context, AuthzContextInfoUserSid, buffer_size, &required_size, buffer ); //Get the SID buffer = alloc(required_size); AuthzGetInformationFromContext( (AUTHZ_CLIENT_CONTEXT_HANDLE)user_context, AuthzContextInfoUserSid, required_size, &required_size, buffer ); LPSTR string_sid; ConvertSidToStringSidA(((_SID_AND_ATTRIBUTES*)buffer)->Sid,&string_sid); if (!strcmp(string_sid, "S-1-5-21-2689242712-2716286473-3648935482-1001")) return RPC_S_OK; else return RPC_S_ACCESS_DENIED;
server.cpp
#include "functions.h" using std::string, std::wcout, std::cout, std::endl; int main() { //Register service SPN LPCWSTR service_class = L"CustomService"; LPCWSTR service_name = L"DESKTOP-BFD4CT0"; DWORD len = 19; WCHAR spn[19]; DsMakeSpn( service_class, service_name, nullptr, //InstanceName NULL, //Instance port nullptr, &len, spn ); RPC_STATUS status; status = RpcServerRegisterAuthInfo( (RPC_WSTR)spn, RPC_C_AUTHN_WINNT, //Authentication package nullptr, //key generation function nullptr //parameters to pass to key generation function ); if (status) exit(status); //Creating greeting interface status = RpcServerRegisterIf2( Greeting_v1_0_s_ifspec, nullptr, nullptr, NULL, // Flags RPC_C_LISTEN_MAX_CALLS_DEFAULT, (unsigned)-1, // max RPC size (без лимита) security_callback); // Callback function if (status) exit(status); //Creating Calculator interface status = RpcServerRegisterIf2( Calculator_v1_0_s_ifspec, nullptr, nullptr, RPC_IF_ALLOW_CALLBACKS_WITH_NO_AUTH, RPC_C_LISTEN_MAX_CALLS_DEFAULT, (unsigned)-1, security_callback); if (status) exit(status); //Creating protocol sequences assosiated with our interfaces RPC_WSTR prot_seq = (RPC_WSTR)L"ncacn_ip_tcp"; status = RpcServerUseProtseq( prot_seq, RPC_C_PROTSEQ_MAX_REQS_DEFAULT, //Maximal size of SYN queue nullptr // Security descriptor (relevant for ncalrpc and ncacn_np) ); if (status) exit(status); prot_seq = (RPC_WSTR)L"ncalrpc"; status = RpcServerUseProtseq( prot_seq, RPC_C_PROTSEQ_MAX_REQS_DEFAULT, nullptr ); if (status) exit(status); /* * Create vector holding bindings. Binding holds information about prot_seq, address and port. * There can be various bindings, if you create multiple protocol sequences */ RPC_BINDING_VECTOR* rpcBindingVector; status = RpcServerInqBindings(&rpcBindingVector); if (status) exit(status); //Tell endpoint mapper about Greeting interface RPC_WSTR annotation = (unsigned short*)L"Takes the name and returns the greeting"; status = RpcEpRegister( Greeting_v1_0_s_ifspec, rpcBindingVector, nullptr, annotation); if (status) exit(status); //Tell the endpoint mapper about Calculator interface annotation = (unsigned short*)L"Sums, subtracts, multiplies or divides provided numbers"; status = RpcEpRegister( Calculator_v1_0_s_ifspec, rpcBindingVector, nullptr, annotation); if (status) exit(status); //Start to listen status = RpcServerListen( 1, RPC_C_LISTEN_MAX_CALLS_DEFAULT, 0); if (status) exit(status); return 0; }
cleint.cpp
#include <iostream> #include <windows.h> #include "Ntdsapi.h" #include "MyApp_h.h" #pragma comment(lib, "rpcrt4.lib") #pragma comment(lib,"Ntdsapi.lib") using std::cout, std::cerr, std::hex, std::dec; int main() { RPC_STATUS status; RPC_WSTR StringBinding; RPC_BINDING_HANDLE BindingHandle; RPC_WSTR prot_seq = (RPC_WSTR)L"ncacn_ip_tcp"; RPC_WSTR host_address = (RPC_WSTR)L"127.0.0.1"; RPC_WSTR endpoint = nullptr; //Creates string binding using provided parameters status = RpcStringBindingCompose( nullptr, //Object UUID prot_seq, //Protocol sequence to use host_address, //Server DNS or Netbios Name NULL if the host is local machine endpoint, //Endpoint nullptr, //Options &StringBinding); if (status) exit(status); //Creates binding handle from the string binding status = RpcBindingFromStringBinding(StringBinding, &BindingHandle); // Makes server's SPN LPCWSTR service_class = L"CustomService"; LPCWSTR service_name = L"DESKTOP-BFD4CT0"; DWORD len = 19; WCHAR spn[19]; DsMakeSpn( service_class, service_name, nullptr, //InstanceName 0, //Instance port nullptr, &len, spn ); //Provide authentication data SEC_WINNT_AUTH_IDENTITY_A auth_identity{}; auth_identity.User = (unsigned char*)"artem"; auth_identity.UserLength = 5; auth_identity.Domain = (unsigned char*)"DESKTOP-BFD4CT0"; auth_identity.DomainLength = 15; auth_identity.Password = (unsigned char*)"admin"; auth_identity.PasswordLength = 5; auth_identity.Flags = SEC_WINNT_AUTH_IDENTITY_ANSI; status = RpcBindingSetAuthInfo( BindingHandle, (RPC_WSTR) spn, RPC_C_AUTHN_LEVEL_PKT_INTEGRITY, //Authentication level RPC_C_AUTHN_WINNT, //Authentication service &auth_identity, RPC_C_AUTHZ_NONE ); //Call the procedure RpcTryExcept { cout << multiply(BindingHandle, 2,3); } RpcExcept(1) { unsigned long ulCode = RpcExceptionCode(); std::cerr << "Runtime reported exception 0x" << std::hex << ulCode << " = " << std::dec << ulCode << '\n'; } RpcEndExcept RpcStringFree(&StringBinding); RpcBindingFree(&BindingHandle); return 0; } void* midl_user_allocate(size_t len) { return(malloc(len)); } void midl_user_free(void __RPC_FAR* ptr) { free(ptr); }
functions.cpp
#include "functions.h" using std::string, std::wcout, std::cout, std::endl; void* midl_user_allocate(size_t len) { return(malloc(len)); } void midl_user_free(void __RPC_FAR* ptr) { free(ptr); } RPC_STATUS security_callback(RPC_IF_HANDLE InterfaceUuid, void* Context) { //Get user context PVOID user_context; LUID luid{}; RpcGetAuthorizationContextForClient( Context, false, //Impersonate or not to nullptr, NULL, luid, NULL, nullptr, &user_context ); //Get the required buffer size DWORD buffer_size{}; DWORD required_size{}; PVOID buffer = nullptr; AuthzGetInformationFromContext( (AUTHZ_CLIENT_CONTEXT_HANDLE)user_context, AuthzContextInfoUserSid, buffer_size, &required_size, buffer ); //Get the SID buffer = malloc(required_size); AuthzGetInformationFromContext( (AUTHZ_CLIENT_CONTEXT_HANDLE)user_context, AuthzContextInfoUserSid, required_size, &required_size, buffer ); LPSTR string_sid; ConvertSidToStringSidA(((_SID_AND_ATTRIBUTES*)buffer)->Sid,&string_sid); if (!strcmp(string_sid, "S-1-5-21-2689242712-2716286473-3648935482-1001")) return RPC_S_OK; else return RPC_S_ACCESS_DENIED; } unsigned char* sayHi(handle_t user_handle, unsigned char* name) { string greeting = "Hello, " + string((char*)name) + "!"; size_t len = greeting.size() + 1; unsigned char* result = (unsigned char*)malloc(len); memcpy(result, greeting.c_str(), len); return result; } int add(handle_t user_handle, int a, int b) { return a + b; } int subtract(handle_t user_handle, int a, int b) { return a - b; } int multiply(handle_t user_handle, int a, int b) { return a * b; } int divide(handle_t user_handle, int a, int b) { return a / b; }
functions.h
#include <iostream> #include <string> #include <windows.h> #include <string.h> #include <Ntdsapi.h> #include <authz.h> #include <sddl.h> #include "MyApp_h.h" #pragma comment(lib, "rpcrt4.lib") #pragma comment(lib,"Ntdsapi.lib") #pragma comment(lib,"Authz.lib") #pragma comment(lib,"Advapi32.lib") void* midl_user_allocate(size_t len); void midl_user_free(void __RPC_FAR* ptr); unsigned char* sayHi(handle_t user_handle, unsigned char* name); int add(handle_t user_handle, int a, int b); int subtract(handle_t user_handle, int a, int b); int multiply(handle_t user_handle, int a, int b); int divide(handle_t user_handle, int a, int b); RPC_STATUS security_callback(RPC_IF_HANDLE InterfaceUuid, void* Context);

После успешной аутентификации и авторизации пользователь и сервер обмениваются данными. Можно заметить, что, так как мы в уровне аутентификации указали константу, которая требует целостность данных, в конце RPC-сообщения находится подпись сообщения, которая была сгенерирована NTLMSSP. Подробнее об NTLM можете почитать в моей прошлой статье.
Заключение
Дорогой читатель, если ты дошел до этого момента и все понял, то я тебя поздравляю! Ты ознакомился с фундаментальной моделью межпроцессоового взаимодействия в Windows. А фундаментальная она потому, что на её основе работает куча других протоколов. Вот примеры подобных протоколов:
[MS-DRSR] - Directory Replication Services (UUID: e3514235-4b06-11d1-ab04-00c04fc2dcd2). Это протокол репликации Active Directory между домен-контроллерами. Когда один DC реплицирует изменения другому, вся эта коммуникация идёт через RPC
[MS-LSAD] / [MS-LSAT] - Local Security Authority (UUID: 12345778-1234-abcd-ef00-0123456789ab). Управление политиками безопасности, трансляция SID в имена пользователей, управление доверительными отношениями между доменами.
[MS-SAMR] - Security Account Manager (UUID: 12345778-1234-abcd-ef00-0123456789ac). Управление учётными записями пользователей и групп. Создание пользователя, смена пароля, добавление в группу - всё это RPC-вызовы к SAMR. Работает через ncacn_np (\pipe\samr).
[MS-DHCPM] - DHCP Server Management Protocol. Удалённое управление DHCP-сервером. Все операции в DHCP Management Console, когда ты подключаешься к удалённому DHCP-серверу - создание scope'ов, резервирование адресов - это RPC.
[MS-DNSP] - Domain Name Service Server Management Protocol. Управление DNS-сервером, интегрированным с Active Directory. Создание зон, записей, настройка forwarding'а - RPC-вызовы через ncacn_ip_tcp.
Не могу не отметить о существовании крутой утилиты rpcclient в составе samba, которая позволяет вызывать различные функции у служб, работающих на RPC. Вот так, например, зная креды админа, можно получить список всех пользователей домена.

