Привет!
Недавно я немного поигрался с 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.
Спасибо за прочтение!