×
Главная   »   Статьи   »   Реверс-инжиниринг на picoCTF: бинарные сдвиги на практике
Реверс-инжиниринг на picoCTF: бинарные сдвиги на практике
Метки:      ,   ,   

Повторим Python на практике, вспомним бинарные сдвиги, функции chr, ord и bin, работу с файлами и списковые включения. Да, и в итоге найдем флаг.

Автор статьи никого не призывает к правонарушениям и отказывается нести ответственность за ваши действия. Вся информация предоставлена исключительно в ознакомительных целях. Все действия происходят на виртуальных машинах и внутри локальной сети автора. Спасибо!

Вчера решив задачу из раздела «Форензика» немного расширил кругозор по метаданным EXIF. Сегодняшняя задача с сайта picoCTF.org из раздела «Реверс-инжинирнг» оказалась не такой простой.

Дан файл enc и задание: I wonder what this really is... enc ''.join([chr((ord(flag[i]) << 8) + ord(flag[i + 1])) for i in range(0, len(flag), 2)]). Открыв файл и увидев иероглифы, я задал себе тот же вопрос: «Что это на самом деле?». На всякий случай попробовал перевести через гугл переводчик. Да, это наивно.

Немного поразмыслив и прочитав еще раз данное условие, предположил, что строка кода и есть дешифратор (тогда в чем смысл задания?). Взглянем на нее еще раз:

''.join([chr((ord(flag[i]) << 8) + ord(flag[i + 1])) for i in range(0, len(flag), 2)])

Если вы проходите курсы на сайте Codebra, то вы с легкостью заметили здесь списковое включение. Отвлекся немного. Давайте напишем рабочий код:

with open("enc", "r", encoding='utf-8') as f:
    flag = f.read()
    print([chr((ord(flag[i]) << 8) + ord(flag[i + 1])) for i in range(0, len(flag) - 1, 2)])

Немного пришлось модифицировать range() после получения исключения IndexError. Не будем заострять на этом внимание.

Если вдруг забыли, как открывать файлы в Python: «Оператор with/as для работы с файлами в Python». В результате у нас ошибка, связанная с выходом за диапазон:

ValueError: chr() arg not in range(0x110000)

Взглянем на передаваемые в функцию chr() числа.

[7392367, 4430459, 3249506, 6940511, 6938996, 3381343, 7324984, 6252598, 3708722]

Они действительно больше 0x110000 (или 1114112 в десятичной системе). Здесь всего девять чисел и, следовательно, девять «возможных» символов (как раз хватит только на picoCTF{}, что должно было заставить задуматься). Меня это не остановило, и я продолжил движение не в ту сторону: брал модуль 0x110000 от этих чисел, чтобы попасть в диапазон и совершал другие бесполезные вещи.

Потеряв на все эти манипуляции около часа, все же перечитал задание и догадался: при помощи этого кода была получена строка из иероглифов. Ну да, задача в разделе «Реверс-инжиниринг» как бы предполагает от результата вернуться к исходному значению.

Взглянем на код и полученный результат и для начала определим количество исходных символов. В файле их 19. Если проанализировать код, то можно предположить о 38 исходных символах. Из двух символов получается один путем смещения битов первого и сложения со следующим.

Рисунок 1. Художества для визуализации задания

Этот конкретный ответ, который нужно реализовать в коде, поставил меня в тупик. Достаточно большое количество двух чисел дают одну и ту же сумму. Ответ кроется в бинарном сдвиге. Давайте разбираться. Начнем с простых примеров:

a = 3 # 011
print(a << 1) # 110

Число три в двоичной системе представляется как 011 (не будем указывать лишние ведущие нули). Надеюсь вы помните, как переводить из двоичной системы в десятичную. Во второй строке показан бинарный сдвиг влево на 1 регистр. Замечательно, вернемся к задаче. Для начала переведем иероглифы в числовые значения из таблицы символов Unicode при помощи функции ord() и сохраним в списке code.

code = [28777, 25455, 17236, 18043, 12598, 24418, 26996, 29535, 26990, 29556, 13108, 25695, 28518, 24376, 24370, 13878, 14388, 25394, 12413]

Теперь будем рассуждать, как получить из числа 28777 два числа (x и y), которые в сумме дают его, при этом первое было сдвинуто на 8 регистров.

28777 = (x << 8) + y

Проще это будет понять через код:

x = 28777
i = x
print(x, bin(x))  # 28777 0b111000001101001
x = x >> 8
print(x, bin(x))  # 112   0b1110000
y = x << 8
print(y, bin(y))  # 28672 0b111000000000000
y = i - y
print(y, bin(y))  # 105   0b000000001101001

Мы взяли число 28777 и сохранили в двух переменных. Для начала получим первое число сдвигом вправо на 8 регистров. Второе неизвестное число должно быть равно в двоичной системе 0b1101001, чтобы в сумме получить 28777. Поэтому сдвигаем полученное значение обратно на 8 регистров. Далее вычитаем из 0b111000001101001 число 0b111000000000000 и получаем 0b000000001101001 (добавил ведущие нули, чтобы нагляднее было). Первый иероглиф был получен из двух символов, которым соответствуют коды 112 и 105.

Теперь реализуем это в коде:

with open("enc", "r", encoding='utf-8') as f:
    flag = f.read()
    code = [28777, 25455, 17236, 18043, 12598, 24418, 26996, 29535, 26990, 29556, 13108, 25695, 28518, 24376, 24370, 13878, 14388, 25394, 12413]

    i = 0
    for item in range(0, len(flag)):
        print(chr((code[i] >> 8)), chr(abs(((code[i] >> 8) << 8) - code[i])), end='', sep='')
        i += 1

Таким нехитрым способом был найден флаг. Эта задача действительно заставила напрячь мозги и вспомнить функции chr, ord и bin, работу бинарного сдвига и ограничения chr().

436 просмотров
06.11.2022
Автор