-
Notifications
You must be signed in to change notification settings - Fork 1
bank
- Категория: Forensics
- Стоимость: 700
- Автор: Александр Борзунов
- Репозиторий
На днях стало известно, что в начале декабря несколько back-end разработчиков и специалистов по анализу данных, работавших в банке «Мечта», практически одновременно уволились и выехали за границу России.
Руководство банка считает, что сотрудники могли оставить в исходном коде онлайн-банка намеренно сделанные ошибки, позволяющие получить кредит по заниженной ставке. Топ-менеджеры обратились к сообществу экспертов по информационной безопасности, чтобы те помогли с поиском бэкдора и расследованием инцидента.
В условии дана ссылка на сайт (и его исходный код), где можно заполнить анкету для получения кредита и узнать процентную ставку, которую готов предоставить банк. Чем менее надёжным является клиент, тем больше риски банка и выше процентная ставка.
Проанализируем код сайта. Сперва введённая анкета преобразуется в словарь с числовыми значениями:
{'credit_amount': 557107, 'dependents': 2, 'duration': 1, 'education': 1, 'expense:alimony': 0, 'expense:car_service': 0, 'expense:credits': 0, 'expense:housing': 0, 'expense:insurance': 0, 'expense:other': 0, 'expense:taxes': 0, 'has_overdue_debts': 0, 'income:dividents': 0, 'income:employee_wage': 151679, 'income:other': 0, 'income:rent': 49988, 'income:state_wage': 176765, 'married': 0, 'missed_deadlines': 1, 'property:apartment': 0, 'property:car': 3728820, 'property:house': 3073060, 'purpose:capital': 0, 'purpose:car': 0, 'purpose:education': 1, 'purpose:other': 0, 'purpose:real_estate': 0, 'was_bankrupt': 0}
Затем этот словарь сортируется по ключам, после чего остаются только числовые значения:
[557107, 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 151679, 0, 49988, 176765, 0, 1, 0, 3728820, 3073060, 0, 0, 1, 0, 0, 0]
Массив с этими значениями передаётся в функцию model.predict
, которая оценивает риски и вычисляет искомую ставку (например, если ставка равна 23% годовых, функция вернёт 1.23). Ниже в коде встречается переменная flag
, которая подсказывает, что флаг будет выдан в качестве кода, который нужно сообщить сотрудникам банка.
Как же работает функция model.predict
? Можно заметить, что модель является Python-объектом, загружаемым из файла model.pickle
. В интерактивной консоли Python посмотрим, что это за объект:
>>> import pickle
>>> model = pickle.load(open('model.pickle', 'rb'))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ModuleNotFoundError: No module named 'sklearn'
Просто так объект не загружается - нужно установить модуль sklearn:
>>> model = pickle.load(open('../flask/model.pickle', 'rb'))
>>> type(model)
<class 'sklearn.tree.tree.DecisionTreeRegressor'>
В файле находится решающее дерево - популярная модель, используемая в машинном обучении. Она представляет собой дерево, в каждой нелистовой вершине которого находится условие вида "i-й признак > x" (например, "сумма кредита > 100000"). Для каждой заполненной анкеты мы начинаем идти по дереву из корня (в зависимости от условия спускаясь влево или вправо) и в итоге приходим в лист, в котором записано значение, которая должна вернуть модель:
Оказывается, что такие деревья можно эффективно строить по большим наборам обучающих данных (где, например, кредитные ставки были определены по анкетам экспертами).
В данном случае нас интересует, существуют ли наборы условий, когда выдаваемая ставка является аномально низкой (именно такая ситуация невыгодна банку). Проанализируем, какие значения встречаются в листах дерева. Сначала посмотрим, какие свойства есть у объекта с деревом:
>>> dir(model)
['__abstractmethods__', '__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setstate__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_abc_cache', '_abc_negative_cache', '_abc_negative_cache_version', '_abc_registry', '_estimator_type', '_get_param_names', '_validate_X_predict', 'apply', 'class_weight', 'classes_', 'criterion', 'decision_path', 'feature_importances_', 'fit', 'get_params', 'max_depth', 'max_features', 'max_features_', 'max_leaf_nodes', 'min_impurity_decrease', 'min_impurity_split', 'min_samples_leaf', 'min_samples_split', 'min_weight_fraction_leaf', 'n_classes_', 'n_features_', 'n_outputs_', 'predict', 'presort', 'random_state', 'score', 'set_params', 'splitter', 'tree_']
>>> dir(model.tree_)
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__pyx_vtable__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setstate__', '__sizeof__', '__str__', '__subclasshook__', 'apply', 'capacity', 'children_left', 'children_right', 'compute_feature_importances', 'decision_path', 'feature', 'impurity', 'max_depth', 'max_n_classes', 'n_classes', 'n_features', 'n_node_samples', 'n_outputs', 'node_count', 'predict', 'threshold', 'value', 'weighted_n_node_samples']
Теперь выведем 10 минимальных значений в листах:
>>> sorted(set(model.tree_.value.flatten()))[:10]
[0.10000000000000001, 1.0999999999999961, 1.0999999999999963, 1.0999999999999965, 1.0999999999999968, 1.099999999999997, 1.0999999999999972, 1.0999999999999974, 1.0999999999999976, 1.0999999999999979]
Видно, что обычно банк требует назад большую сумму, чем была выдана заёмщику изначально (коэффициент больше 1), но в одном случае сумма будет умножаться на коэффициент, меньший 1 (кредитная ставка будет отрицательной!). Очевидно, это и есть искомый бэкдор.
Как нужно заполнить анкету, чтобы воспользоваться уязвимостью? Найдём путь от корня до листа с бэкдором с помощью поиска в глубину:
path = []
def dfs(tree, index):
if tree.children_left[index] == -1 and tree.children_right[index] == -1:
if tree.value[index] < 1:
return True
return False
if dfs(tree, tree.children_left[index]):
path.append((index, '<'))
return True
if dfs(tree, tree.children_right[index]):
path.append((index, '>'))
return True
return False
dfs(model.tree_, 0)
path.reverse()
Преобразуем номера признаков в их названия, которые встречались нам в словаре:
feature_names = ['credit_amount', 'dependents', 'duration', 'education', 'expense:alimony', 'expense:car_service', 'expense:credits', 'expense:housing', 'expense:insurance', 'expense:other', 'expense:taxes', 'has_overdue_debts', 'income:dividents', 'income:employee_wage', 'income:other', 'income:rent', 'income:state_wage', 'married', 'missed_deadlines', 'property:apartment', 'property:car', 'property:house', 'purpose:capital', 'purpose:car', 'purpose:education', 'purpose:other', 'purpose:real_estate', 'was_bankrupt']
constraints = []
for index, sign in path:
feature = feature_names[model.tree_.feature[index]]
threshold = model.tree_.threshold[index]
constraints.append((feature, sign, threshold))
print('\n'.join(sorted(' '.join(map(str, item))
for item in constraints)))
Получаем набор условий:
credit_amount > 429002.5
dependents > 0.5
duration > 1.5
expense:alimony > 17050.5
expense:alimony > 46715.5
expense:credits < 46768.5
expense:insurance < 19356.0
expense:other < 13149.5
has_overdue_debts > 0.5
income:dividents < 82775.5
income:employee_wage > 48568.0
income:employee_wage > 65122.0
income:other < 197203.0
income:other > 112665.5
income:other > 65113.5
income:rent > 59898.5
income:state_wage < 58667.5
missed_deadlines > 0.5
property:apartment > 1639374.0
property:apartment > 3340466.5
property:car < 1247488.5
property:house < 1511438.0
was_bankrupt < 0.5
Для проверки можно сгенерировать словарь, где значения признаков подходят под эти условия, и убедиться, что модель предсказывает коэффициент 0.1 для такого словаря. Наконец, заполним анкету на сайте в соответствии с найденными условиями и получим флаг: