Привет!

Недавно я немного поигрался с Ghidra, инструментом реверс-инжиниринга, исходный код которого недавно был открыт АНБ. Официальный сайт описывает инструмент как:

Набор инструментов для обратной инженерии программного обеспечения (SRE), разработанный Управлением исследований NSA в поддержку миссии кибербезопасности.

Я нахожусь в начале своей карьеры в области реверс-инжиниринга, поэтому я не делал ничего продвинутого. Я не знаю, каких функций ожидать от такого профессионального инструмента, как этот, если вы хотите прочитать о расширенных функциях Ghidra, эта статья, скорее всего, не для вас.

В этой статье я попытаюсь решить простую задачу CrackMe, которую я нашел на сайте root-me. Задача, которую я решаю, называется ELF - CrackPass. Если вы хотите попробовать это самостоятельно, то вам следует подумать о том, чтобы не читать эту статью, потому что это испортит вам задачу.

Давайте начнем! Я открываю Ghidra и создаю новый проект, который называю RootMe.

Затем я импортирую файл задачи, перетаскивая его в папку проекта. Я оставлю настройки по умолчанию.

Получив некоторую информацию о двоичном файле, я нажимаю ОК, выбираю файл и дважды щелкаю по нему. Это открывает утилиту браузера кода Ghidra и спрашивает, хочу ли я проанализировать файл, затем я нажимаю «Да» и продолжаю использовать значения по умолчанию.

После импорта файла мы получаем некоторую информацию о двоичном файле. Если мы нажмем ОК и закроем это окно, а затем дважды щелкнем импортированный файл, откроется утилита браузера кода Ghidra. Я выбираю Да, когда мне предлагается проанализировать двоичный файл, и продолжаю использовать значения по умолчанию.

Браузер кода довольно удобен. На левой панели мы видим вид дизассемблирования, а на правой панели - вид декомпиляции.

Ghidra показывает нам непосредственно информацию заголовка ELF и точку входа в двоичный файл. После двойного щелчка по точке входа представление диземблера переходит к функции входа.

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

Прежде чем анализировать основную функцию, я хотел изменить ее сигнатуру. Я изменил тип возвращаемого значения на int и исправил тип и имя параметров. Это изменение вступило в силу в режиме декомпиляции, и это здорово! 👍

Выделение линии на виде декомпиляции также выделяет ее на виде сборки.

Давайте рассмотрим функцию FUN_080485a5, которую я переименую в CheckPassword.

Содержание функции CheckPassword можно найти ниже. Я скопировал код прямо из представления декомпиляции Ghidra, что является отличной функцией, которой не хватает многим инструментам этого типа! Возможность копировать сборку и код - это хорошо.

void CheckPassword(char *param_1)
 {
   ushort **ppuVar1;
   int iVar2;
   char *pcVar3;
   char cVar4;
   char local_108c [128];
   char local_100c [4096];
   cVar4 = param_1;   
    if (cVar4 != 0) {    
      ppuVar1 = __ctype_b_loc();     
      pcVar3 = param_1;     
      do {       
        if (((byte )(ppuVar1 + (int)cVar4) & 8) == 0) {
         puts("Bad password !");
                     /* WARNING: Subroutine does not return */
         abort();
       }
       cVar4 = pcVar3[1];
       pcVar3 = pcVar3 + 1;
     } while (cVar4 != 0);
   }
   FUN_080484f4(local_100c,param_1);
   FUN_0804851c(s_THEPASSWORDISEASYTOCRACK_08049960,local_108c);
   iVar2 = strcmp(local_108c,local_100c);
   if (iVar2 == 0) {
     printf("Good work, the password is : \n\n%s\n",local_108c);
   }
   else {
     puts("Is not the good password !");
   }
   return;
 }

Взглянув на код, я пришел к следующим выводам. Блок с if проверяет, предоставил ли пользователь пароль, и проверяет предоставленный пароль, чтобы проверить, действительный ли это символ или что-то в этом роде. Я не совсем уверен, что он проверяет, но вот что говорится в документации __ctype_b_loc ():

Функция __ctype_b_loc () должна возвращать указатель на массив символов в текущей локали, который содержит характеристики для каждого символа в текущем наборе символов. Массив должен содержать всего 384 символа и может быть проиндексирован любым знаковым или беззнаковым символом (т.е. со значением индекса от 128 до 255). Если приложение многопоточное, массив должен быть локальным для текущего потока.

В любом случае, этот блок кода не стоит потраченного времени, потому что он никоим образом не изменяет наш пароль, он просто проверяет его. Так что мы можем пропустить такую ​​проверку.

Следующая вызываемая функция - FUN_080484f4. Глядя на его код, мы можем сказать, что это всего лишь пользовательская реализация memcopy. Вместо того, чтобы копировать код C из представления декомпилятора, я скопировал код сборки - да, это весело.

*************************************************************                     *                           FUNCTION                                               *************************************************************                     undefined  FUN_080484f4 (undefined4  param_1 , undefined4  p     undefined         AL:1           <RETURN>     undefined4        Stack[0x4]:4   param_1                                 XREF[1]:     080484f8 (R)        undefined4        Stack[0x8]:4   param_2                                 XREF[1]:     080484fb (R)                        FUN_080484f4                                    XREF[1]:     CheckPassword:080485f5 (c)   
 080484f4 55              PUSH       EBP
 080484f5 89  e5           MOV        EBP ,ESP
 080484f7 53              PUSH       EBX
 080484f8 8b  5d  08       MOV        EBX ,dword ptr [EBP  + param_1 ]
 080484fb 8b  4d  0c       MOV        ECX ,dword ptr [EBP  + param_2 ]
 080484fe 0f  b6  11       MOVZX      EDX ,byte ptr [ECX ]
 08048501 84  d2           TEST       DL,DL
 08048503 74  14           JZ         LAB_08048519
 08048505 b8  00  00       MOV        EAX ,0x0
             00  00
                         LAB_0804850a                                    XREF[1]:     08048517 (j)   
 0804850a 88  14  03       MOV        byte ptr [EBX  + EAX *0x1 ],DL
 0804850d 0f  b6  54       MOVZX      EDX ,byte ptr [ECX  + EAX *0x1  + 0x1 ]
             01  01
 08048512 83  c0  01       ADD        EAX ,0x1
 08048515 84  d2           TEST       DL,DL
 08048517 75  f1           JNZ        LAB_0804850a
                         LAB_08048519                                    XREF[1]:     08048503 (j)   
 08048519 5b              POP        EBX
 0804851a 5d              POP        EBP
 0804851b c3              RET
Comment: param_1 is dest, param_2 is src. 
08048501 checks if src is null and if it is 
it returns else it initializes EAX (index, current_character) 
with 0. The next instructions move bytes into EBX (dest) from EDX (src).
The loop stops when EDX is null.

А другая функция FUN_0804851c генерирует пароль из строки «THEPASSWORDISEASYTOCRACK». Смотрим на декомпилированный вид. мы можем примерно увидеть, как работает эта функция. Если бы у нас этого не было, нам пришлось бы вручную анализировать каждую инструкцию сборки из функции, чтобы понять, что она делает.

Затем мы сравниваем сгенерированный ранее пароль с паролем, который мы получили от пользователя (первый аргумент, argv [1]). Если он совпадает, программа сообщает о хорошей работе и распечатывает ее, в противном случае выводит сообщение об ошибке.

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

Посмотрим, что нам нужно исправить:

По адресу 0x0804868c мы вставляем инструкцию JNS в JMP. И вуаля, изменение отражено в представлении декомпилятора. Проверка результатов ptrace не выполняется.

{
   ptrace(PTRACE_TRACEME,0,1,0);
   if (argc != 2) {
     puts("You must give a password for use this program !");
                     /* WARNING: Subroutine does not return */
     abort();
   }
   CheckPassword(argv[1]);
   return 0;
}

По адресу 0x080485b8 мы вставляем инструкцию JZ в JMP. Мы обходим блокировку проверки пароля, которую видели ранее.

void CheckPassword(undefined4 param_1)
 {
   int iVar1;
   char local_108c [128];
   char local_100c [4096];
   CustomCopy(local_100c,param_1);
      GeneratePassword(s_THEPASSWORDISEASYTOCRACK_08049960,local_108c);
   iVar1 = strcmp(local_108c,local_100c);
   if (iVar1 == 0) {
     printf("Good work, the password is : \n\n%s\n",local_108c);
   }
   else {
     puts("Is not the good password !");
   }
   return;
 }

По адресу 0x0804861e мы исправляем JNZ на JZ. Это инвертирует условие if / else. Поскольку мы не знаем пароль, мы собираемся отправить случайный пароль, не равный сгенерированному, таким образом выполняя printf в блоке else.

void CheckPassword(undefined4 param_1)
 {
   int iVar1;
   char local_108c [128];
   char local_100c [4096];
   CustomCopy(local_100c,param_1);
   // constructs the password from the strings and stores it in
   // local_108c 
   GeneratePassword(s_THEPASSWORDISEASYTOCRACK_08049960,local_108c);
   iVar1 = strcmp(local_108c,local_100c);
   if (iVar1 == 0) { // passwords are equal
     puts("Is not the good password !");
   }
   else {
     printf("Good work, the password is : \n\n%s\n",local_108c);
   }
   return;
 }

Это все!

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

Чтобы экспортировать программу, заходим в Файл - ›Экспорт программы (O). Меняем формат на двоичный и нажимаем ОК.

Я получил экспортированную программу на моем рабочем столе, но она не работает - мне не удалось запустить экспортированную программу. После попытки прочитать заголовок с помощью программы readelf -h, я получил следующий вывод:

root@DESKTOP:/mnt/c/users/denis/Desktop# readelf -h Crack.bin
 ELF Header:
   Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
   Class:                             ELF32
   Data:                              2's complement, little endian
   Version:                           1 (current)
   OS/ABI:                            UNIX - System V
   ABI Version:                       0
   Type:                              EXEC (Executable file)
   Machine:                           Intel 80386
   Version:                           0x1
   Entry point address:               0x8048440
   Start of program headers:          52 (bytes into file)
   Start of section headers:          2848 (bytes into file)
   Flags:                             0x0
   Size of this header:               52 (bytes)
   Size of program headers:           32 (bytes)
   Number of program headers:         7
   Size of section headers:           40 (bytes)
   Number of section headers:         27
   Section header string table index: 26
 readelf: Error: Reading 1080 bytes extends past end of file for section headers

Стыд. Похоже, Ghidra испортил заголовок файла… и сейчас я не хочу исправлять заголовки вручную. Поэтому я запустил другой инструмент и применил те же патчи к файлу, сохранил его, запустил со случайным аргументом и проверил флаг.

Выводы

Ghidra - хороший инструмент с большим потенциалом. В текущем состоянии это не так уж и здорово, но работает. Я также столкнулся со странной ошибкой прокрутки при запуске на моем ноутбуке.

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

Будем надеяться, что как только код будет выпущен, сообщество начнет исправлять и улучшать Ghidra.

Спасибо за прочтение!