Реверс-інжиніринг повідомлень Protocol Buffers

Під реверс-ининирингом, в даному контексті, я розумію відновлення вихідної схеми повідомлень найбільш близькі до оригіналу, що використовується розробниками. Існує кілька способів отримати бажане. По-перше, якщо у нас є доступ до клієнтського додатку, розробники не подбали про те, щоб приховати налагоджувальні символи та лінкуватися до LITE версії бібліотеки protobuf, то отримати оригінальні .proto-файли не складе праці. По-друге, якщо ж розробники використовують LITE збірку бібліотеки, то це звичайно ускладнює життя реверсеру, але аж ніяк не робить реверсинг марним заняттям: при певній вправності, навіть в цьому випадку, можна відновити .proto-файли досить близькі до оригіналу.

У даній статті я хотів би описати деякі техніки реверсу ptobobuf повідомлень, завдяки яким з'явився мій проект protodec. Зауважу, що все сказане стосується формату кодування protobuf повідомлень версії 2 (3 версія поки не підтримується, packed поля теж).

Підготовка
Для початку я створю об'єкти для дослідження. Нам знадобляться 2 файлу:

addressbook.proto
package tutorial;
option optimize_for = LITE_RUNTIME;
message Person {
required string name = 1;
required int32 id = 2;
optional string email = 3;

enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}

message PhoneNumber {
required string number = 1;
optional PhoneType type = 2 [default = HOME];
}

repeated PhoneNumber phone = 4;
}

message AddressBook {
repeated Person person = 1;
}

tut.cpp
#include < iostream>
#include <cassert>
#include < string>
#include "addressbook.pb.h"
int main() {
GOOGLE_PROTOBUF_VERIFY_VERSION;
tutorial::AddressBook book;
tutorial::Person * person = book.add_person();
person->set_id(1234);
person->set_name("John Doe");
person->set_email("jdoe@example.com");
tutorial::Person_PhoneNumber * phone = person->add_phone();
phone->set_number("555-4321");
phone->set_type(tutorial::Person_PhoneType_HOME);
std::string data = book.SerializeAsString();
assert(!data.empty());
std::cout.write(&data[0], data.size());
google::protobuf::ShutdownProtobufLibrary();
}

Зберігаємо їх і збираємо всі разом. Якщо ви не знаєте, що таке protoc, то Вам потрібно прочитати введення в бібліотеку Protobuf для вашого мови програмування.

protoc --cpp_out=. addressbook.proto && g++ addressbook.pb.cc tut.cpp `pkg-config --cflags --libs protobuf` -s -o tut.lite.exe && ./tut.lite.exe > A

Видаляємо або закомментіруем другий рядок файлу addressbook.proto і виконуємо команду:

protoc --cpp_out=. addressbook.proto && g++ addressbook.pb.cc tut.cpp `pkg-config --cflags --libs protobuf` -o tut.exe && ./tut.exe > B

Після виконання вищезазначених команд ми маємо два виконуваних файлу tut.lite.exe і tut.exe з LITE і повною збіркою бібліотеки libprotobuf відповідно. Обидві програми роблять одне і теж: створюється protobuf повідомлення, яке виводиться в std::cout. Так само у нас з'явилося два двійкові файли з іменами A і B. Перший згенерований lite версією, другий — повною версією програми. Вміст їх ідентично. На скріншоті нижче можна побачити бінарне подання цього повідомлення та його текстовий вигляд:



Видаляємо addressbook.proto і спробуємо його відновити.

Відновлення схеми повідомлень з Descriptor даних здійснимих файл
Глянемо вміст файлу adressbook.pb.cc, згенерованого раніше утилітою protoc. Нас повинна зацікавити функція protobuf_AddDesc_addressbook_2eproto. Одним з перших дій у ній — виклик функції: google::protobuf::DescriptorPool::InternalAddGeneratedFile, перший аргумент якої і є Descriptor protobuf повідомлення з інформацією про структуру оригінальних повідомлень.

// ...
void protobuf_AddDesc_addressbook_2eproto() {
static bool already_here = false;
if (already_here) return;
already_here = true;
GOOGLE_PROTOBUF_VERIFY_VERSION;

::google::protobuf::DescriptorPool::InternalAddGeneratedFile(
"\n\021addressbook.proto\022\010tutorial\"\332\001\n\006Person"
"\022\014\n\004name\030\001 \002(\t\022\n\n\002id\030\002 \002(\005\022\r\n\005email\030\003 \001("
"\t\022+\n\005phone\030\004 \003(\0132\034.підручник.Person.Phone"
"Number\032M\n\013PhoneNumber\022\016\n\006number\030\001 \002(\t\022.\n"
"\004type\030\002 \001(\0162\032.підручник.Person.PhoneType:"
"\004HOME\"+\n\tPhoneType\022\n\n\006MOBILE\020\000\022\010\n\004HOME\020\001"
"\022\010\n\004WORK\020\002\"/\n\013AddressBook\022 \n\006person\030\001 \003("
"\0132\020.підручник.Person", 299);
::google::protobuf::MessageFactory::InternalRegisterGeneratedFile(
"addressbook.proto", &protobuf_RegisterTypes);
Person::default_instance_ = new Person();
Person_PhoneNumber::default_instance_ = new Person_PhoneNumber();
AddressBook::default_instance_ = new AddressBook();
Person::default_instance_->InitAsDefaultInstance();
Person_PhoneNumber::default_instance_->InitAsDefaultInstance();
AddressBook::default_instance_->InitAsDefaultInstance();
::google::protobuf::internal::OnShutdown(&protobuf_ShutdownFile_addressbook_2eproto);
}
// ...

У ній збережена інформацією про перерахування, списку імпорту, повідомленнях, імена і типи даних їх полів і т. д. Формат не є секретом і поставляється разом з вихідним кодом; його можна глянути в google/protobuf/descriptor.proto. Ці дані використовується при рефлексії, для налагоджувального виведення вмісту повідомлень і т. д.

Утиліта protodec виконує пошук Descriptor даних в бінарному файлі і вміє зберігати відновлені з них .proto-файли. Для цього потрібно запустити команду:

protodec --grab tut.exe

У відповідь побачимо щось таке:



Тобто, у підсумку ми отримали майже оригінал вихідного .proto-файлу.

Відновлення схеми з байт повідомлення
Якщо до додатка немає доступу (припустимо, воно працює десь на сервері), то і до Descriptor даними дістатися буде проблематично. Те ж саме стосується, якщо додаток зібрано з LITE оптимізацією: рефлексія не використовується, тому і Descriptor опис .proto-файлів не генерується на етапі компіляції, а отже, відновити оригінальні .proto-файли методом згаданим раніше у нас не вийде. В цьому випадку можна спробувати аналізувати вміст protobuf повідомлень. Зазначу, що вони повинні бути 100% мати однакову структуру (кореневе повідомлення повинно у них збігатися). Таких повідомлень нам знадобляться як можна більше; чим більше у них даних, тим краще результат отримаємо в результаті.

Програма protodec може відновити схему зазначеного protobuf повідомлення з їх типами, завантаженого файлу. Для цього запустимо команду:

protodec --schema A



Цей висновок означає, що в даному protobuf повідомленні (завантаженому з файлу A), було виявлено 3 повідомлення. Якщо ми поглянемо на оригінальний addressbook.proto, то безсумнівно вгадується загальна: MSG1 це Person::PhoneNumber, MSG2 це Person, ну а MSG3 це AddressBook. Опишу впадають в очі невідповідності:

  1. Поле MSG3.fld1 повинно бути repeated. Проблема тут в тому, що в оригінальному повідомленні, AddressBook.person всього лише один елемент, а на бінарному рівні не можна розрізнити repeated поле в такому випадку. Якщо б у AddressBook.person, даних було хоча б 2 елемента, то він би визначився вірно. Саме тому нам потрібно кілька повідомлень даної схеми, з максимальною заповненою;

  2. Деякі required поля повинні бути optional. Дана проблема так само вирішується аналізом великої кількості повідомлень, завдяки якому можна зрозуміти де має бути required поле, а де optional;

  3. Поле MSG2.fld2 повинно бути int32, а воно int64. На низькому рівні, в protobuf всі цілочисельні типи (int32, int64, uint32, uint64, sint32, sint64, bool, enum) зберігаються як Varint. Потім можна зрозуміти з контексту, числа в цьому полі будуть вони знаковими або беззнаковими, int64 обраний для того, щоб в нього можна було зберегти максимально можливе цілочисельне значення для використовуваної мови програмування.
Імена полів так і повідомлень, що генеруються автоматично, ці метадані з тіла самого protobuf повідомлення «дістати» неможливо, т. к. їх там просто немає. В такому випадку можна поступово переміщувати повідомлення і поля, коли призначення їх стає більш-менш зрозуміло з контексту досліджуваних повідомлень. Так само, в самому додатку, списку експорту іноді можна виявити дану інформацію. Для цього нам знадобиться будь-яка утиліта вміє це робити, наприклад, IDA. Ось тут ми вибрали імена і порядок полів для повідомлення tutorial::Person, яке має 4 поля:


Робимо те ж саме для інших повідомлень і в результаті отримуємо практично оригінальний .proto-файл.

Перевірка
У результаті у нас вийшов приблизно такий .proto-файл:

tut2.proto
package ProtodecMessages;

message PHONE {
required string Number = 1;
required int64 Type = 2;
}

message PERSON {
required string Name = 1;
required int64 Id = 2;
required string Email = 3;
required PHONE Phone = 4;
}

message ADDRESSBOOK {
repeated PERSON Person = 1;
}

Напишемо невелику програму, щоб перевірити, що наша відновлена схема може редагувати оригінальні повідомлення.

tut2.cpp
#include < iostream>
#include < fstream>
#include < string>
#include <cassert>
#include "tut2.pb.h"

int main() {
GOOGLE_PROTOBUF_VERIFY_VERSION;
// читаємо вміст protobuf повідомлення з std::cin
std::string data;
ProtodecMessages::ADDRESSBOOK book;
while (std::cin.peek() != EOF)
data.push_back((char)std::cin.get());
// все вдало распарсили?
assert(book.ParseFromString(data));
assert(book.person_size() > 0);
// змінюємо повідомлення
ProtodecMessages::PERSON * person = book.mutable_person(0);
person->set_email("fake@name.com");
person->set_id(4321);
// виводимо змінене повідомлення у std::cout
data = book.SerializeAsString();
assert(!data.empty());
std::cout.write(&data[0], data.size());
// Optional: Delete all global objects allocated by libprotobuf.
google::protobuf::ShutdownProtobufLibrary();
}

Компілюємо і запускаємо:

protoc --cpp_out=. tut2.proto && g++ tut2.pb.cc tut2.cpp `pkg-config --cflags --libs protobuf` -o tut2.exe


Посилання:
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.