Сегодня мы сконцентрируемся на возможностях IdaPython, плагина ставшего штатной частью современной Иды и позволяющего делать с ней нечто невероятное!
Не пропустите прошлый выпуск!
Для того чтобы достойно оценить IdaPython нам потребуется свежий CrackMeByFereter#2 не использующий сложных трюков, но затрудняющий статический анализ.
Познакомиться с основами IdaPython можно в старом руководстве от Ero Carrera или этой статье. Современная документация располагается здесь. Примеры скриптов есть на github. Кроме того много полезных примеров можно найти в блоге Ильфака.
Обход защиты
Первое на что мы натыкаемся — это шифрованные строки
Легко заметить процедуру шифрования, ксорящую каждую строку на 0xC6:
Ида позволяет нам загружать свои скрипты, для этого нажмём alt+f9 и выберем ‘insert’, указав путь до скрипта. Давайте посмотрим на код скрипта:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
def replace_inv_char(name): new_str = '' pattern = re.compile("[a-zA-Z0-9_$?@]") for x in name: if pattern.match(x): new_str = new_str + x else: new_str = new_str + "_" return new_str def name2ida_str(name): return 'a' + replace_inv_char(name) def de_xor_str(addr): MAX_STR_LEN = 256 decrypted = '' for i in range(0, MAX_STR_LEN): cl = Byte(addr + i) ^ 0xC6 if cl==0: ida_name = name2ida_str(decrypted) MakeName(addr, ida_name) return decrypted = decrypted + chr(cl) print 'invalid' cur_addr = here() de_xor_str(cur_addr) |
Скрипт получает виртуальный адрес на который мы последним кликнули мышкой и дешифрует строку во внутренний буфер. Недопустимые символы заменяются на нижнее подчеркивание. Полученное имя назначается адресу начала строки:
По началу и стилю кода можно понять что CrackMe писался на ассемблере. В коде присутствует динамическое вычисление адреса перехода и защита от отладки.
Начнём с IsDebuggerPresent. Для красоты решения, мы не будем патчить код CraсkMe, а напишем скрипт, убирающий флаг IsDebugged из PEB, посмотрим на код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
def IsWow64(): for seg in Segments(): if SegName(seg) == 'wow64.dll': return True return False def GetTEB(tid): need = "TIB[%08X]" % tid for seg in Segments(): if need==SegName(seg): return seg return False def GetPEB(): tid = idc.GetCurrentThreadId() TEB = GetTEB(tid) if not TEB: return False #DWORD -> QWORD peb_offset = 0x60 if IsWow64() else 0x30 peb_pointer = Dword(TEB+peb_offset) return peb_pointer def GetPEB32(): peb = GetPEB() if not peb: return False # PEB64 after PEB32 peb_delta = 0x1000 if IsWow64() else 0 peb -= peb_delta return peb def HideDebuger_PEB(): peb = GetPEB32() # Set PEB!IsDebugged to zero if PatchDbgByte(peb + 2, 0): print 'PEB patched' else: print 'HideDebuger - Error' HideDebuger_PEB() |
Первая проблема с которой столкнуться обладатели x64 системы (при работе с x32 кодом), это подсистема WoW64. В адресном пространстве таких процессов существует две копии PEB в 32 и 64-битной версии. Мы перечисляем сегменты памяти в поисках TEB текущего потока. Если это x64 система, то и TEB будет иметь ссылки этой разрядности. Из TEB мы получаем ссылку на PEB.
И здесь будет немного магии. В адресном пространстве PEB64 всегда находится на расстоянии 0x1000 от PEB32. У меня нет внятного объяснения как это работает, но я нашел множество исходников подразумевающих это как данность.
Пора проверить скрипт в деле, кликаем в начало программы, нажимаем f4 и запускаем наш скрипт:
Следующим рубежом обороны является встроенная в цикл обработки сообщений окна, проверка кода на наличие байт 0x90 = NOP и 0xCC = INT3:
После установки бряка выбираем Edit BreakPoint -> Hardware и код приложения останется без изменений.
Защита повержена, но это далеко не конец, дальше нас ждёт:
Подбор ключа
Полистав оконную функцию находим место обработки сообщения WM_COMMAND:
Отсюда только трассировать, так что вбиваем произвольные данные и смотрим куда это приведёт. Ага, текст полей запрашивается через SendMessage(WM_GETTEXT). Функция возвращает длину текста и нам явно дают понять что ключ имеет 11 символов.
Имя юзера суммируется в один байт, и 2 раза делится на 9, к остаткам от деления прибавляется 0x30. Эти цифры должны быть в конкретном месте ключа. Это даёт нам две первые цифры.
С паролем немного сложнее, по сходному алгоритму хешируется сам пароль, после чего два остатка от деления + 0x30 должны совпадать с цифрами в середине ключа.
Не знаю решается ли это математически, но хеширование в 1 символ даёт невероятно много коллизий, так что мы легко подберем пароль:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
import itertools def get_hash(line): return Appcall.Summ_ebx_to_DL(line) & 0xFF def check_pass(pass_line): pass_hash = get_hash(pass_line) AX = pass_hash DX = AX % 9 AX = AX // 9 BX = AX % 9 DL = BX + 0x30 BL = DX + 0x30 print "HASH: %s DL: %s BL: %s" %(hex(pass_hash), hex(DL),hex(BL)) if ord(pass_line[6]) != DL: return False if ord(pass_line[5]) != BL: return False return True def pw_guess(): res = itertools.permutations('0123456789' ,4) for guess in res: yield guess def brute_force(): mask = "23%s-%s%s%s-XXX" for x in pw_guess(): key = mask % (x[0],x[1],x[2],x[3]) if check_pass(key): return key return False print brute_force() |
Как всё это работает? Что такое AppCall? Свежая IDA позволяет (во время отладки) вызывать ассемблерные функции из нашего скрипта! Нужно лишь задать правильный прототип, щёлкаем на функцию хеширования, нажимаем Y и вводим:
1 |
int __usercall Summ_ebx_to_DL@<edx>(LPVOID str@<ebx>) |
Теперь мы можем вызвать хеш-функцию прямо из скрипта! В силу слабого хеша, можно взять любые последние символы. Запускаем скрипт и вуаля:
FIN
Это было интересно! Спасибо автору CrackMe. Надеюсь пример работы с IdaPython получился достаточно познавательным. Возможно в следующем выпуске с IdaPython мы ещё встретимся =)
кто поможет
Сформулируйте вопрос нормально