#c #assembly #x86 #reverse-engineering #decompiling
# #c #сборка #x86 #обратный инжиниринг #декомпиляция
Вопрос:
Этот код сравнивает каждый символ строки (расположенный по адресу ebp arg_0
) с различными константами (символами ASCII), такими как ‘I’, ‘o’ и ‘S’. Я предполагаю, основываясь на других частях кода, что этот код изначально написан на C.
Эта часть сравнения кода выглядит очень неэффективной. Мой вопрос: как, по-вашему, этот код будет выглядеть на языке Си? Какая конструкция кода использовалась изначально? мои мысли до сих пор
- Это не цикл for. Потому что я не вижу никакого скачка вверх и состояния остановки.
- Это не конструкция кода while / case / switch
- Мое лучшее предположение заключается в том, что это множество последовательных операторов if / else. Вы можете мне помочь?
Да, это часть задачи, у меня уже есть флаг / решение, не беспокойтесь об этом. Просто пытаюсь лучше понять код.
Комментарии:
1. Если это какой-то крэк, то он не должен быть эффективным. Весь смысл crackmes в том, чтобы сбить исследователя с толку.
2. Вы можете использовать временную атаку против этого, чтобы найти правильный пароль
Ответ №1:
Это не цикл for . Потому что я не вижу никакого перехода вверх и условия остановки.
Правильно.
Это не while / case / switch code constuct
Этого не может быть, он сравнивает разные показатели массива.
Мое лучшее предположение заключается в том, что это много последовательных if / elses . Вы можете помочь?
Похоже, это может быть этот код:
void f(const char* arg_0) {
if(arg_0[4] == 'I' amp;amp; arg_0[5] == 'o' amp;amp; arg_0[6] == 'S') {
printf("Gratz man :)");
exit(0); //noreturn, hence your control flow ends here in the assembly
}
puts("Wrong password"); // Or `printf("Wrong passwordn");` which gets optimized to `puts`
// leave, retn
}
Вот как gcc компилирует его без оптимизации:
.LC0:
.string "Gratz man :)"
.LC1:
.string "Wrong password"
f(char const*):
push ebp
mov ebp, esp
sub esp, 8
mov eax, DWORD PTR [ebp 8]
add eax, 4
movzx eax, BYTE PTR [eax]
cmp al, 73
jne .L2
mov eax, DWORD PTR [ebp 8]
add eax, 5
movzx eax, BYTE PTR [eax]
cmp al, 111
jne .L2
mov eax, DWORD PTR [ebp 8]
add eax, 6
movzx eax, BYTE PTR [eax]
cmp al, 83
jne .L2
sub esp, 12
push OFFSET FLAT:.LC0
call printf
add esp, 16
sub esp, 12
push 0
call exit
.L2:
sub esp, 12
push OFFSET FLAT:.LC1
call puts
add esp, 16
nop
leave
ret
Выглядит очень похоже на ваш дизассемблированный код.
Эта часть compare-code выглядит очень неэффективной
Похоже, он был скомпилирован без оптимизации. С включенной оптимизацией gcc скомпилировал код для:
.LC0:
.string "Gratz man :)"
.LC1:
.string "Wrong password"
f(char const*):
sub esp, 12
mov eax, DWORD PTR [esp 16]
cmp BYTE PTR [eax 4], 73
jne .L2
cmp BYTE PTR [eax 5], 111
je .L5
.L2:
mov DWORD PTR [esp 16], OFFSET FLAT:.LC1
add esp, 12
jmp puts
.L5:
cmp BYTE PTR [eax 6], 83
jne .L2
sub esp, 12
push OFFSET FLAT:.LC0
call printf
mov DWORD PTR [esp], 0
call exit
Не уверен, почему gcc решил перейти вниз и снова выполнить резервное копирование вместо прямой строки jne
s. Кроме того, ret
is gone, ваш printf
оптимизированный хвостовой вызов, т. Е. a jmp printf
является началом a call printf
, за которым следует a ret
.
Комментарии:
1. Вау, это действительно очень близко! Спасибо за вашу поддержку!
2.
puts
добавляет новую строку,printf
не делает. (И gcc не ищет оптимизацию использованияfputs(str, stdout)
для постоянных строк, которые не заканчиваются новой строкой.) В любом случае, asm использует puts, было бы проще всего использовать puts. В любом случае, отсутствует"n"
причина, по которой ваша версия gcc не оптимизируется для puts.3. @PeterCordes Спасибо, я отредактировал его, чтобы использовать puts. Я не понял
puts
, добавляет новую строку.4. Я, вероятно, не запомнил бы это
puts
так легко, если бы не посмотрел на вывод asm компилятора и не узнал об этой оптимизации: P Современный gcc может даже обрабатыватьprintf("%sn", "string")
, даже для случаев, когда"string"
это переменная с постоянным значением времени компиляции после постоянного распространения. Но я действительноputs
иногда печатаю (особенно в коде toy / experiment), потому что у него более короткое имя, чем printf, потому что он добавляет n, который мне не нужно вводить, и потому что он невосприимчив к уязвимостям строки формата для вывода строки переменной среды выполнения.
Ответ №2:
Первый аргумент ( arg_0
) является указателем на заданную строку пароля, например, const char *arg_0
. Этот указатель (адрес первого символа) загружается в eax
регистр ( mov eax, [ebp arg_0]
), а затем добавляется индекс текущего символа, чтобы продвинуть его до этого индекса ( add eax, 4
и т.д.). Затем один байт по этому адресу загружается в eax
( movzx eax, byte ptr [eax]
) .
Затем этот байт / символ сравнивается с правильным ( cmp eax, 'I'
и т.д.) . Если результат не равен нулю (т. Е., Если они не равны), программа переходит к ветви «Неправильный пароль» ( jnz
— перейти, если не ноль), в противном случае он переходит к следующему сравнению (и, в конечном счете, успех).
Следовательно, ближайший эквивалент прямого C будет чем-то вроде:
void check(const char *arg_0) {
// presumably comparisons 0-3 are omitted
if (arg_0[4] != 'I') goto fail;
if (arg_0[5] != 'o') goto fail;
if (arg_0[6] != 'S') goto fail;
printf("Gratz man :)");
exit(0);
fail:
puts("Wrong password");
}
(Конечно, фактический код C вряд ли выглядел бы так, поскольку goto fail
расположение не является типичным.)
Комментарии:
1. Спасибо! Когда я декомпилирую это, код сборки выглядит почти идентично.