Нужно пройти все проверки в файле, чтобы получить флаг.

Solution

Посмотрим файл.

Info:
    File name: /spbctf_rev/task1/task1.out
    Size: 7396
    File type: ELF32
    String: ELF(386)
    Extension: elf
    Operation system: Linux(ABI: 2.6.32)
    Architecture: 386
    Mode: 32-bit
    Type: EXEC
    Endianness: LE

Закину его в тулу. А вот и наш main:

int32_t main(int32_t argc, char** argv, char** envp) {
    void* const __return_addr_1 = __return_addr;
    int32_t* var_10 = &argc;
    
    if (argc <= 1)
    {
        puts("What?");
        return 1;
    }
    
    if ((strlen(argv[1]) & 3) != 0)
    {
        puts("Ok, you are wrong");
        return 2;
    }
    
    if (strcmp(argv[1], "FLAG{123REALFLAG!!!}") == 0)
    {
        puts("Not enough!");
        return 3;
    }
    
    int32_t eax_14 = hashf(argv[1]);
    
    if (eax_14 == hashf("FLAG{123REALFLAG!!!}"))
    {
        int32_t str;
        __builtin_memcpy(&str, "\xe3\xc7\xed\x8f\xde\xdf\xe4\x81\xf6\xd4\xe5\x9b\xfa\xc6\xf5\x97\xee\xce\xf5\xb5", 0x14);
        char var_1d_1 = 0;
        decode(&str, eax_14);
        puts(&str);
    }
    
    return 0;
}

Разберемся:

if (argc <= 1)
{
	puts("What?");
    return 1;
}

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

if ((strlen(argv[1]) & 3) != 0)
{
	puts("Ok, you are wrong");
	return 2;
}

Похоже на то, что длина ключа не должна быть кратна 4.

Но это все потом. Я нашел несколько вариантов решения данной задачи. Итак, есть 3 пути. В каждом есть свои плюсы и минусы. Вперед)

Коллизионная атака

Это самый очевидный способ, нужно немножечко понимать за крипту) Что нам нужно сделать? Разреверсить функцию hashf, чтобы совершить коллизионную атаку. Если же не выпендриваться и говорить проще, то нужно найти ключ, который будет отличаться от FLAG{123REALFLAG!!!}, но давать такой же результат.

Как выглядит функция hashf:

08048490  int32_t hashf(char* arg1)
08048490  {
0804849c      int32_t eax = dwords_count(arg1);
080484a7      int32_t result = 0xcafedead;
080484b1      char* var_18 = arg1;
080484b1      
080484d9      for (int32_t i = 0; i < eax; i += 1)
080484d9      {
080484c8          result ^= *(uint32_t*)var_18;
080484cb          var_18 = &var_18[4];
080484d9      }
080484d9      
080484df      return result;
08048490  }

Мы можем тут увидеть, что для каждых 4 байтов arg1 вызывается XOR с переменной result. Итак, нам нужно получить такой же хеш, что и при вызове hashf("FLAG{123REALFLAG!!!}"). Как это сделать?

Первый вариант — брутфорс. Это долго и неинтересно.

Второй вариант — свойства XOR. Вы можете почитать о них в прошлом посте.

123 ^ 123 = 0

Это значит, если мы передадим в функцию 2 одинаковые группы по 4 байта, то наш хеш не изменится. Тогда возьмем нашу строку FLAG{123REALFLAG!!!} и добавим к ней 2 одинаковые группы: FLAG{123REALFLAG!!!}aaaaaaaa. Попробую запустить:

root@5626db1d1f64:/rev# ./task1.out 'FLAG{123REALFLAG!!!}aaaaaaaa'
FLAG{THIS_IS_MY_KEY}

Динамическая отладка

Данный способ требует некоторого опыта работы с gdb или любым другим отладчиком. Тут есть 2 варианта: перепрыгнуть все проверки до подсчета хеша (что вполне себе хороший вариант), или вызвать нужные функции отдельно.

Попробуем оба способа.

Перепрыгнем проверки

Что для этого нужно сделать? Первым делом нужно найти адрес программы для прыжка. Для этого идеально подойдет место, где вызывается hashf от переданного аргумента через аргументы командной строки. Вызов происходит по адресу 0x80485d6. Но не все так просто. Глянем на скрин ниже:

IMG

По данному адресу происходит именно вызов функции. А до этого нужно передать в нее аргументы) Передача аргументов начинается по адресу 0x80485ca.

Если разобрать asm, то происходит следующее:

  1. В eax помещается указатель на начало argv, которые находятся на стеке;
  2. В argv хранятся указатели на строки. Так как у нас 32-х битный бинарный файл, то каждый указатель занимает 4 байта информации. С помощью add мы перемещаем наш указатель на элемент argv[1];
  3. С помощью sub esp, 0xc выделяется нужное место на стеке для передачи аргументов;
  4. Через push указатель на argv[1] помещается на стек для передачи в функцию;
  5. Происходит вызов функции hashf;

Итак, что нам нужно сделать:

  1. Запустить бинарь через gdb;
  2. С помощью команды set args 'FLAG{123REALFLAG!!!}' установить нужный аргумент командной строки;
  3. Установить брекпоинт на функции main с помощью b *main;
  4. С помощью set $eip = 0x80485ca переместить место выполнения программы. eip — это указатель на текущую инструкцию. Предварительно можно пару раз нажать ni, чтобы выполнить пролог функции:

IMG

  1. Продолжить выполнение программы с помощью команды continue:
pwndbg> c
Continuing.
FLAG{THIS_IS_MY_KEY}

Флаг получен)

Вызов нужных функций

В отладчике мы можем вызвать функции с нужными аргументами. Давайте так и сделаем)

Вызовем функцию hashf с аргументом FLAG{123REALFLAG!!!}:

pwndbg> p (unsigned int)hashf("FLAG{123REALFLAG!!!}")
$1 = 3366751141

Переведем в hex для удобства:

3366751141 = 0xc8ac8ba5

Зачем это нужно? В функцию decode передается зашифрованный флаг и ключ. Ключом является хеш, который мы только что нашли.

Не особо удобно вызывать функцию, в которую передается указатель на строку, а сама строка меняется на месте. Разберем функцию:

080484e0  int32_t decode(char* arg1, int32_t arg2)

080484e0  {
080484e9      char* var_10 = arg1;
080484f2      int32_t eax_1 = dwords_count(arg1);
08048523      int32_t i;
08048523      
08048523      for (i = 0; i < eax_1; i += 1)
08048523      {
08048513          *(uint32_t*)var_10 ^= arg2;
08048515          var_10 = &var_10[4];
08048523      }
08048523      
08048527      return i;
080484e0  }

В ней используется XOR, но не самым простым способом, на следующей неделе я расскажу об этом подробнее (читай You either know, XOR you don't). Он совершается блоками по 4 байта. Напишем декодер на Python:

s = b'\xe3\xc7\xed\x8f\xde\xdf\xe4\x81\xf6\xd4\xe5\x9b\xfa\xc6\xf5\x97\xee\xce\xf5\xb5'
key = bytes.fromhex('c8ac8ba5')
key = key[::-1]

def xor(s: bytes, key: list[int]) -> bytes:
   return bytes([s[i] ^ key[i % len(key)] for i in range(len(s))])

nums = []

print(xor(s, key).decode())

Важный момент, ключик нужно нам перевернуть)

cu63:task1/ $ python decode.py                                                                                                                                                                                                   
FLAG{THIS_IS_MY_KEY}

Патчинг

Это тот вариант, который я выбрал первым) В чем суть: если мы сможем передать строку FLAG{123REALFLAG!!!} в качестве аргумента, то вот эта проверка всегда будет истиной, так как хеш-функции выдают одинаковый результат от одних и тех же данных:

int32_t eax_14 = hashf(argv[1]);
    
if (eax_14 == hashf("FLAG{123REALFLAG!!!}"))

Мешает же нам вот эта проверка:

IMG

Я пропатчил ее с помощью инструментов Binary Ninja: Rigth Click->Patch->Always Branch. Получилось вот так:

IMG

Это можно сделать разными путями: затереть проверку инструкцией nop, перепрыгнуть ее через jmp, изменить первый аргумент на рандомную строку и т.д. Сути это не меняет. Сохраню патченный файл и запущу его, передав аргумент FLAG{123REALFLAG!!!}.

root@5626db1d1f64:/rev# ./task1_patched.out 'FLAG{123REALFLAG!!!}'
FLAG{THIS_IS_MY_KEY}