Вирішуємо crackme від Лабораторії Касперського

В один прекрасний день різні канали в телеграмі почали кидати посилання на крэкмишку від ЛК, які Успішно виконали завдання будуть запрошені на співбесіду!. Після такої гучної заяви мені стало цікаво, наскільки складним буде реверс. Про те, як я вирішував цей таск можна почитати під катом (багато картинок).

Прийшовши додому, я ще раз уважно перечитав завдання, скачав архів і став дивитися, що ж там всередині. А всередині було це:

Запускаємо x64dbg, дампим після розпакування, дивимося, що всередині насправді:

Беремо ім’я файлу з аргументів командного рядка -> відкриваємо, читаємо -> шифруємо першою сходинкою -> шифруємо другий щаблем -> записуємо в новий файл.

Все просто, пора дивитися на шифрування.

Почнемо з stage1

За адресою 0x4033f4 знаходиться функція, яку я назвав crypt_64bit_up (пізніше ви зрозумієте чому), вона викликається з циклу десь всередині stage1

І трохи кривоватый результат декомпіляції

Спочатку я намагався переписати цей же алгоритм на пітоні, вбив на це кілька годин і вийшло щось таке (що робить get_dword і byteswap повинно бути зрозуміло з назв)

def _add(x1, x2):
 return (x1+x2) & 0xFFFFFFFF 

def get_buf_val(t, buffer):
 t_0 = t & 0xFF
 t_1 = (t >> 8) & 0xFF
 t_2 = (t >> 16) & 0xFF
 t_3 = (t >> 24) & 0xFF
 res = _add(get_dword(buffer, t_0 + 0x312), (get_dword(buffer, t_1 + 0x212) ^ _add(get_dword(buffer, t_2+0x112), get_dword(buffer, t_3+0x12))))
 # print('Got buf val: 0x%X' % res)
 return res

def crypt_64bit_up(initials, buffer):
 steps = []
 steps.append(get_dword(buffer, 0) ^ byteswap(initials[0])) # = z
 steps.append(get_buf_val(steps[-1], buffer) ^ byteswap(initials[1]) ^ get_dword(buffer, 1))
 for i in range(2, 17): 
 steps.append(get_buf_val(steps[-1], buffer) ^ get_dword(buffer, i) ^ steps[i-2])
 res_0 = steps[15] ^ get_dword(buffer, 17)
 res_1 = steps[16]
 print('Res[0]=0x%X, res[1]=0x%X' % (res_0, res_1))

Але потім я вирішив звернути увагу на константи 0x12, 0x112, 0x212, 0x312 (без хекса 18, 274, 536… не дуже схоже на щось незвичайне). Пробуємо їх загугліть і знаходимо цілий репозиторій (підказка: NTR) з реалізацією функцій шифрування і дешифрування, ось це удача. Пробуємо зашифрувати у вихідній програмі тестовий файл з рандомным вмістом, сдампить його і зашифрувати файл питонячим скриптом, все має працювати і результати мають бути однаковими. Після цього пробуємо його розшифрувати (я вирішив не вдаватися в деталі і просто скопипастить функцію розшифровки з исходников)

def crypt_64bit_down(initials, keybuf):
 x = initials[0]
 y = initials[1]
 for i in range(0x11, 1, -1):
 z = get_dword(keybuf, i) ^ x
 x = get_buf_val(z, keybuf)
 x = y ^ x
 y = z
 res_0 = x ^ get_dword(keybuf, 0x01) # x - step[i], y - step[i-1]
 res_1 = y ^ get_dword(keybuf, 0x0)
 return (res_1, res_0)

def stage1_unpack(packed_data, state):
 res = bytearray()
 for i in range(0, len(packed_data), 8):
 ciphered = struct.unpack('>II', packed_data[i:i+8])
 res += struct.pack('>II', *crypt_64bit_down(ciphered, state))
 return res

Важливе зауваження: ключ в репозиторії відрізняється від ключа в програмі (що цілком логічно). Тому після ініціалізації ключа я його просто сдампил в файлик, це і є buffer/keybuf

Читайте також  Представляємо Amazon Corretto, безкоштовний дистрибутив OpenJDK з довгостроковою підтримкою

Переходимо до другої частини

Тут все набагато простіше: спочатку створюється масив унікальних char розміром 0x55 байт в діапазоні (33, 118) (printable chars), потім 32-бітне значення упаковується в 5 printable chars з масиву, створеного раніше.

Так як ніякого рандома при створенні масиву згаданого вище немає, при кожному запуску програми цей масив буде однаковим, дампим його після ініціалізації і простий функцією можемо розпакувати stage_2

def stage2_unpack(packed_data, state): # checked!
 res = bytearray()
 for j in range(0, len(packed_data), 5):
 mapped = [state.index(packed_data[j+i]) for i in range(5)]
 res += struct.pack('>I', sum([mapped[4-i]*0x55**i for i in range(5)]))
 return res

Робимо щось таке:


f = open('stage1.state.bin', 'rb')
stage1 = f.read()
f.close()
f = open('stage2.state.bin', 'rb')
stage2 = f.read()
f.close()
f = open('rprotected.dat', 'rb')
packed = f.read()
f.close()
unpacked_from_2 = stage2_unpack(packed, stage2)
f = open('unpacked_from_2', 'wb')
f.write(unpacked_from_2)
f.close()
unpacked_from_1 = stage1_unpack(unpacked_from_2, stage1)
f = open('unpacked_from_1', 'wb')
f.write(unpacked_from_1)
f.close()

І отримуємо результат

Степан Лютий

Обожнюю технології в сучасному світі. Хоча частенько і замислююся над тим, як далеко вони нас заведуть. Не те, щоб я прям і знаюся на ядрах, пікселях, коллайдерах і інших парсеках. Просто приходжу в захват від того, що може в творчому пориві вигадати людський розум.

You may also like...

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *