• Приглашаем на KubanCTF

    Старт соревнований 14 сентября в 10:00 по москве

    Ссылка на регистрацию в соревнованиях Kuban CTF: kubanctf.ru

    Кодебай является технологическим партнером мероприятия

Статья Простой пример Race Condition.

man.jpg



Последствия


- Бро, выручай, увели весь товар из магазина(( Думаю накрутили баланс, посмотри плиз. ©

Занятное сообщение? Особенно когда ты знаешь, что у твоего братана было больше, чем на 100k баксов товара. Но не будем думать о товаре и деньгах, а лучше поразмыслим над тем, как всё провернуть :) Ладно-ладно, мы не такие… Разберём один из вариантов, как это происходит и как этого избежать. Ведь все крепкие проекты, которые крутятся на просторах нами любимого интернета, закаляются временем, толковыми постоянными разработчиками и хакерам.


Race Condition


Состояние гонки, или как принято говорить в более строгой академической среде - неопределённость параллелизма. Смысл этой атаки в том, что при параллельном выполнении кода можно «накрутить баланс». Ну, скажем, при положительном внутреннем балансе в 100$ снять трижды по 100, и накупить себе вещей на 300$. А так же применить скидочные купоны по несколько раз, промокоды, да и кучу всего поделать, что должно выполняться раз, а мы имеем возможность сделать несколько. Конечно, при условии, что программист не учёл этот фактор.


Как это происходит


Уязвимость появляется при параллельном одновременном доступе к общему ресурсу ( балансу в нашем случае). Здесь я приведу простой пример на Python.

Python:
balance = 170
local_allb = 0

def buy(by):

    global balance,local_allb

    # Получаем текущее значение баланса

    priv_balance = balance

    # Списываем сумму
    
    priv_balance -= by

    # Проверка баланса

    if priv_balance <= 0:
        print('Недостаточно средств!')
        return False
    
    # Счётчик суммы всех операций
    
    local_allb += by

    print(f'Средств списалось: {by}')

    # Устанавливаем значение основного баланса

    balance = priv_balance

    print(f'Остаток: {balance}')

На балансе у нас 170 денег. Попробуем списать 50:

Python:
if buy(50) == False:
    print(balance)

Получаем логичный ответ:

1722339464590.png


Пробуем списать 300 и видим сообщение о том, что денег нам не хватает:

1722339518218.png


Пока что всё работает хорошо.

А теперь, мой дорогой друг, я накидаю товаров ( в вымышленную корзину, естественно ) на сумму больше той, что имеется на балансе.

Python:
Basket = [10,20,30,40,50,60,70] # 280 денег

И напишу функцию, которая в параллельном выполняющемся многопотоке будет покупать все эти товары:

Python:
def attack():

    with ThreadPoolExecutor(max_workers=len(basket)) as on1:

        on1.map(buy,basket)

        print(f'Значение баланса в итоге: {balance}')

1722339686630.png


Супер! Мы купили на 210 денег, при балансе в 170 и ещё осталось 60. Вот - простейший пример уязвимости данного типа. Но это в теории. На практике всё немного иначе. По сути, в примере выше, я смоделировал вариант для реализации Race Condition, но что усложняет эксплуатацию на удалённом ресурсе? Ответ один — временные задержки. И здесь речь идёт не только о времени задержки сети, но и внутренних задержках во время выполнения кода, по сути совокупности всего. И всё же я подчеркну — усложняет, но не лишает. Допустим я установлю функцией sleep задержку в 0.7 мс в функции buy(). И запущу 100 итераций атаки, результат уже будет иным:

Python:
for i in range(100):

    local_allb = 0

    balance = 170

    attack()

    print(f'Сумма всех выполненных операций: {local_allb}')

    if local_allb > 170:

        print('STOP!')

        exit()

Лишь единожды из сотни раз мне удалось добиться общей суммы 280 и несколько раз удалось получить значение в 150 с остатком баланса 50 ( т.е. 30 денег взялись из неоткуда ). Это при установке одинаковой задержки в начале функции. Естественно, установка одинаковой временной задержки не гарантирует, что она будет строго 0.7 мс. Там будут свои погрешности, о чём я расскажу немного позже.

1722339899664.png


Теперь я попробую функцию sleep(0.7) переместить после получения данных о глобальном балансе в той же функции buy():

1722339923696.png


1722339940761.png


И так почти каждую итерацию из 100. Всё что мы сделали - это изменили позицию задержки. Уязвимость чувствительна к положению, в котором происходит задержка. Отсюда следует вывод: если потоки получили доступ к ресурсу, то вероятность того, что атака пройдёт успешно - больше.

Если мы создадим случайную задержку для каждого потока, к примеру - от 0.7 мс до 1.5 с, и установим её перед выполнением кода функции, мы заметно сократим вероятность успешной атаки, но не уберём её полностью. Такой результат я получил на 217 проходе, вероятность ниже, но уязвимость присутствует.

1722340012421.png


1722340030637.png


Раньше или позже мы получаем желаемое. Даже, если пинг между запросами серьёзно отличается ( отличие от 0.7 мс до 1.5 я считаю весомым ). Даже если какие либо задержки реализованы или возникают по определённым причинам в функции, всё равно, увеличивая количество проходов, мы увеличиваем вероятность того, что атака пройдёт успешно. Может на 217 проходе, а может и на 10217 проходе, но конечная цель может быть достигнута.


Решения


Из выше сказанного становиться понятно, что для реализации подобных функций нужно создавать некую очередь и не позволять потокам одновременно получать доступ к каким-либо данным. Как это сделать? Есть множества вариантов реализации, такие как: механизмы блокировок, семафоры, атомарный операции, асинхронный код. Главное - нужно определиться с тем, как быстро должен работать твой код, а так же какие проблемы могут возникнуть при реализации механизмов защиты. Исходя из ответов - определиться с реализацией.
 
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!