Продолжаем решать головоломки: сегодня это 1337ReverseEngineer's VMAdventures 1

Задача: узнать верный пароль, на который программа выдаст "Correct key!".

Проверка пароля

С помощью дизассемблера находим строку "Correct key!" и код, что на нее ссылается. Над ним - цикл проверки пароля: eax пробегает по символам, а в edi - длина пароля.

Строка byte_4032E0 содержит непечатные символы: это не сам пароль, а хеш.

При помощи отладчика выясняется, что в esi хранится указатель на строку пароля, затем вызов loc_401170 "портит" символы.

Выполнение байт-кода

Работа функции управляется байт-кодом: цикл перебирает байты строки .rdata:004032D0 и выполняет команды. Функция предусматривает 5 случаев для следующих значений байтов: 0x0, 0x1, 0x2, 0x3, 0x4.  За исключением команды 0, которая останавливает выполнение байт-кода, команды 1-4 изменяют строку пароля. Таким образом программа шифрует пароль: чтобы расшифровать, выполним обратные действия в обратном порядке.

Расшифровка пароля

Изучим команды. 

0x1 Если внимательно изучать код, увидим, что на блоки размером 64 байта операцией XOR накладывается 16-байтная константа, а для остальных байтов выполняет XOR 54h. Заметим, что длина верного пароля - 32 и XOR по 64-блокам не будет выполняться. Повторное применение XOR,восстанавливает исходное значение: A XOR B XOR B = A.

0x2 Тот же алгоритм, что и у 0x1, но отличается константа: выполняет XOR 24h.

0x3 Выполняет циклический сдвиг влево на 2 бита для каждого символа пароля. Обратная операция - сдвиг вправо.

0x4 Выполняет сложение каждого байта пароля с 0xEB. Обратная операция - вычитание.

Теперь программа сама выдаст секретный пароль:

  • вводим пароль из 32-х символов;

  • подадим на вход функции loc_401170 шифр пароля из .rdata:004032E0;

  • изменим код loc_401170, чтобы выполнить обратные операции:

    • заменим в case 3 операцию сдвига ROL на ROR;

    • заменим в case 4 сложение на вычитание;

    • запишем команды байт-кода в обратном порядке.

Несложно написать и код дешифратора.

#include <bit>
#include <vector>
#include <iostream>
#include <string>

using namespace std;
using BytesVector = vector<uint8_t>;

BytesVector code{0x01, 0x03, 0x04, 0x02, 0x01, 0x03, 0x02, 0x01, 0x02, 0x01, 0x03, 0x02, 0x03, 0x04, 0x02, 0x03};

void xor1(BytesVector& key) {
    for (auto &c: key) c ^= 0x54;
}

void xor2(BytesVector& key) {
    for (auto &c: key) c ^= 0x24;
}

void rol2(BytesVector& key) {
    for (auto &c: key) c = rotl(c, 2);
}

void add(BytesVector& key) {
    for (auto &c: key) c += 0xEB;
}

void ror2(BytesVector& key) {
    for (auto &c: key) c = rotr(c, 2);
}

void sub(BytesVector& key) {
    for (auto &c: key) c -= 0xEB;
}


enum Opcode {
    XOR1 = 0x1,
    XOR2,
    ROL,
    ADD,
	ROR,
	SUB
};

void exec(const BytesVector& code, BytesVector& key) {
    for (auto i: code) {
        switch(i) {
            case XOR1:
                xor1(key);
                break;
            case XOR2:
                xor2(key);
                break;
            case ROL:
                rol2(key);
                break;
            case ADD:
                add(key);
                break;
			case ROR:
				ror2(key);
				break;
			case SUB:
				sub(key);
				break;
        }
    }
}

BytesVector explode(const string& s) {
    BytesVector result;
    for (char c: s) {
        result.push_back(static_cast<uint8_t>(c));
    }

    return result;
}

void encrypt(BytesVector& text) {
    exec(code, text);
}

void reverseBytecode(BytesVector& code) {
        for (uint8_t& op: code) {
        switch(op) {
            case ROL:
                op = ROR;
                break;
            case ADD:
                op = SUB;
                break;
            case ROR:
                op = ROL;
                break;
            case SUB:
                op = ADD;
                break;
        }
    }
}

void decrypt(BytesVector& text) {
    BytesVector rev{code.rbegin(), code.rend()};
    reverseBytecode(rev);
    exec(rev, text);
}

int main() {
    BytesVector magic{0xBD, 0x35, 0xA9, 0xA1, 0xD1, 0xE1, 0xD9, 0x35, 
                      0x31, 0x01, 0x39, 0xD9, 0xAA, 0x95, 0x01, 0xAA, 
                      0xFD, 0xB9, 0x28, 0xD5, 0x7C, 0xD9, 0x1D, 0x95, 
                      0x99, 0xCD, 0xD9, 0xF1, 0xAA, 0xD2, 0xEE, 0xF9};

    string password;
    cout << "Enter password: "s;
    cin >> password;
    cout << endl;
    
    BytesVector key = explode(password);
    encrypt(key);
	cout << "Encrypted: "s;
    for (auto k: key) {
        printf("%X ", k);
    }
    
    decrypt(key);
    cout << "\nDecrypted: "s;
	for (auto k: key) {
        printf("%c", k);
    }
    
    decrypt(magic);
    cout << "\nTOP SECRET: "s; 
    for(auto b: magic) { 
        printf("%c", b);
    }
    
	return 0;
}

Комментарии (5)


  1. khe404
    24.01.2023 18:22
    +1

    Очень интригующая статья.

    Это как публикация сообщения которое зашифровано с закрытым ключом, а все обладатели открытого прочли статью и поняли что происходит.

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

    Я люблю головоломки. Но тут прям головоломка головоломки.

    Вроде как сама задача не сложная взять вот такую хеш функцию.

    {XOR1, ROL, ADD, XOR2, XOR1, ROL, XOR2, XOR1, XOR2, XOR1, ROL, XOR2, ROL, ADD, XOR2, ROL};

    Реализация каждой из функции есть в статье, но по факту простейшие операции вроде сложения, вычитания, смещения.

    И построить к ней обратную.

    Что остается зашифрованным так это как взять exe файлик и понять где в нем хеш пароля, где прописан сам алгоритм хеширования и даже как из бинарника получить список инструкций в удобочитаемом ассемблере. Но еще сложнее делать это заведомо понимая, что скорее всего все усилия будут тщетны, так как простейший алгоритм sha1 уже не подастся подобному реверсу. ( не смотря на то что он вроде как считается ненадежным)

    А так как дизассемблирование и вообще ассемблер это очень специфическая область знаний то разобраться дано почти что лишь не всем.

    В общем сложно разобраться откуда взялась информация о хеширующей функции. Тут бы начать с ликвидации безграмотности в отношении ассемблера, дебагера и как вообще устроены бинарники.


    1. sa2304 Автор
      24.01.2023 19:55

      Спасибо за отзыв :) Здорово, что заинтриговало.

      Вы прочли текст, сразу поняли идею, задались вопросами, остались силы написать подробный комментарий :)

      Интерес помогает решать сложные задачи. Беда в том, что интерес легко отбить заумными текстами, что с первых страниц сыплют подробностями.

      Знатокам скучно читать подробное описание "очевидных" шагов, а непосвященным быстро наскучит продираться сквозь детали. Те и другие бросят чтение после второго абзаца :)

      Ассемблер учат и по ходу дизассемблирования: загрузил готовую программу в отладчик и изучай. Учебник по ассемблеру и Intel Software Developers Manual лежат под рукой как справочники.

      Инструменты: IDA Pro, OllyDbg, ImmDbg, x64dbg.


      1. khe404
        25.01.2023 11:40

        Предпринимал много попыток уложить в себя ассемблер. Даже более того проектировал свой простейший процессор. Нельзя сказать что я вообще не подготовлен.

        К сожалению, "очевидные" шаги пока кажутся более сложными чем все приведенное описание решения.

        Спасибо за набор инструментов.

        Жалею о том, что существует этот серьезный провал в доступности туториалов по ассемблеру в сравнении с туторалами по тому же питону и html.


        1. sa2304 Автор
          25.01.2023 14:19

          Авторы языка Си проделали много работы, чтобы избавить программисты не писали на ассемблере :) Многие задачи проще решаются на C++, Python. Даже прошивки микроконтроллеров пишут на Си.

          Ассемблер пригодится разработчику трансляторов, драйверов, вирусному аналитику, взломщику и спецу по информационной безопасности. Знание языка ничего не дает, если знание не применяется на практике.

          Задача определяет требования: появится цель, под нее найдутся инструменты и знания.


  1. EXL
    25.01.2023 23:09

    Спасибо за интересную статью. Кстати, можете посоветовать какой-нибудь удобный и функциональный HEX-редактор? В различных инструментах реверс-инжиниринга типа IDA Pro зачастую встроенные HEX-редакторы достаточно простенькие и различные полезные и часто используемые операции над бинарными файлами по типу: сохранить выделенный кусок в отдельный файл, добить файл определённым байтом до нужного размера и прочие подобные делаются в них несколько топорно и неудобно.