Трюки от КрисаКрис Касперски Хакер, номер #113, стр. 113-114-1 Долгое время мы витали вокруг чистого ANSI C, без реверансов в сторону нестандартных расширений от различных производителей, которых развелось столько, что игнорировать их невозможно. Сегодня мы поговорим об интимных взаимоотношениях Си с платформой .NET и управляемым (managed) кодом.Трюк #1 – управляемый код на СиОфициально платформа .NET «крышует» C#, F#, Visual Basic и некоторые другие языки, в перечень которых Си, увы, не входит. Однако последние версии компилятора Microsoft Visual C++ поддерживают возможность трансляции программ в управляемый байт-код (по «научному» называемый MSCIL – Microsoft Common Intermediate Language – Общий Промежуточный Язык от Microsoft, но это слишком длинно и заумно, так что мы ограничимся термином «байт-код»). Если сделать небольшой пируэт хвостом, то можно писать Си программы на плюсах, транслируя их в байт-код. Конечно, «чистого» Си мы все равно не получим, но, по крайней мере, обретем возможность вызывать функции стандартной библиотеки libc, «химичить» с указателями и т. д. Естественно, в силу строгой типизации языка Си++ придется ругаться матом (нецензурным кастингом), впрочем, об этом мы уже говорили в #09h выпуске «трюков». Чтобы заставить приплюснутый компилятор генерировать байт-код, достаточно воткнуть в начало программы «using namespace System;» и добавить к командной строке ключ «/CLR», пример использования которого приведен ниже: hello.cpp – программа на Си++, подготовленная к трансляции в управляемый код и вызывающая функции стандартной библиотеки языка Си #include <stdio.h> // используем пространство имен System (из .NET) using namespace System; void main() { printf("hello, nezumi!\n"); } Трансляция указанного кода в исполняемый файл из командной строки осуществляется следующим образом: $cl.exe /CLR hello.cpp Если все сделано правильно, на диске образуется файл hello.exe, готовый к непосредственному исполнению и победоносно выводящий «hello, nezumi!» на экран. Трюк #2 –управляемый код и переполняющиеся буфераПродвигая управляемый код на рынок, Microsoft неустанно перечисляла его преимущества: а) более высокую производительность на чисто вычислительных задачах; б) решение проблемы переполняющихся буферов; в) наличие автоматического сборщика мусора, предотвращающего утечки памяти. Что касается производительности, то первые версии .NET'а действительно обгоняли Си/Си++ программы в некоторых тестах за счет более компактной структуры байт-кода и динамической оптимизации при трансляции в память. Но уже начиная с .NET 2, производительность байт-кода заметно упала и положение спасает только то, что байт-код способен без перекомпиляции исполняться на процессорах разных типов (x86, x86-64, IA64), используя их преимущества, чего не может чистый машинный код. А вот контроль за буферами и сборка мусора реально работают только в C# программах (да и то не без оговорок). «Управляемый» код, полученный путем трансляции Си++ программы, наследует все худшие черты языка Си. Это мы сейчас и продемонстрируем на примере умышленного переполнения буфера: Программа, подготовленная к трансляции в управляемый код и допускающая переполнение буфера #include <stdio.h> #include <string.h> using namespace System; void main() { char buf0[0x6]; char buf1[0x6]; char buf2[0x6]; printf("enter str0 :");gets(buf0); printf("enter str1 :");gets(buf1); printf("enter str2 :")123;gets(buf2); printf("your str is :%s,%s,%s\n",buf0,buf1,buf2); } Компилируем написанную программу в управляемый код с помощью ключа /CLR и смотрим: сможет ли она справиться с ошибкой переполнения или нет. Мы имеем три массива по 06h байт каждый, куда вводим строки длинной в 09h байт (эта величина выбрана произвольно). Результат не заставляет себя ждать: $hello-over.exe enter str0 :111111111 enter str1 :222222222 enter str2 :333333333 your str is :1111111122222222333333333,22222222333333333, 333333333 Как видно, в buf0 «магическим» образом попали все три строки. В buf1 – вторая и третья строка; buf2 выглядит неповрежденным, но затирает находящиеся за ним данные (которых в данном случае нет). В общем, все происходит так, как и следовало ожидать. Буфера последовательно размещаются в памяти, а переполнение одного из них воздействует на последующие. В защиту управляемого кода упомянем невозможность подмены адреса возврата из функции, а точнее нетривиальной этой операции, поскольку архитектура виртуальной машины (с учетом компиляции части кода в память) чрезвычайно запутана и реализовать целенаправленную атаку с захватом управления намного сложнее. Так что какой-то смысл в управляемом коде все же есть, но утечки памяти – это кошмар! Управляемый код, не обремененный искусственным интеллектом, не может отличить ситуацию «выделил память и забыл освободить» от «выделил и решил (пока) не использовать». Сборщик мусора реально «отлавливает» лишь небольшую часть ошибок, когда указатель на динамическую память присваивается локальной переменной функции и «погибает» вместе с ней при закрытии стекового фрейма. Стоит функции перед выходом передать этот указатель кому-то еще или сохранить его в глобальной переменной — все! Сборщик мусора его не тронет. Трюк #3 – смесь управляемого и неуправляемого кодовПриложения, критические к производительности, а также программы, взаимодействующие с «внешним» миром (например, оборудованием), пишутся на смеси управляемого и неуправляемого кодов. К счастью, язык C# позволяет вызывать управляемые модули, написанные на Си++, из которых в свою очередь можно вызывать «нативные» (native) функции, компилируемые в машинный код. Формально, виртуальная .NET-машина поддерживает механизм P/Invoke, предназначенный для прямых вызовов нативного кода, но в языках С#/Cи++ он реализован не лучшим образом и для решения поставленной задачи приходится совершать большое количество телодвижений. Но мы не боимся трудностей! Для начала напишем Си++ программу, предназначенную для компиляции в машинный код. В ней нет ничего сложного за тем исключением, что все «экспортируемые» строки должны быть представлены в формате Unicode: nativecode.cpp – Си++ программа, предназначенная для компиляции в машинный код #include "string.h" #include "nativecode.h" void native_foo(wchar_t* c, int num) { wchar_t* s = L"hello, this is native code!"; wcsncpy_s(c, num, s, wcslen(s)); } Тут же создадим заголовочный файл (nativecode.h ) с прототипом функции native_foo(), включаемый в остальные файлы проекта: void native_foo(wchar_t* c, int num); Теперь пишем Си++ программу, транслируемую в управляемый код и вызывающую нашу нативную функцию native_foo(), что достигается за счет использования конструкции «ref class CPPClass»: clrcode.cpp — Си++ программа, подготовленная к трансляции в управляемый код и вызывающая нативную функцию native_foo() #include "nativecode.h" using namespace System; namespace souriz { ref class CPPClass { public: static String^ foo_wrapper() { wchar_t c[0x69]; native_foo(c, sizeof(c) / sizeof(c[0])); return gcnew String(c); } }; } Остается только заточить C# программу, вызывающую метод foo_wrapper() из Си++ программы. В свою очередь метод вызывает нативную функцию native_foo() – что осуществляется посредством конструкции «CPPClass.foo_wrapper()»: program.cs – программа на C#, вызывающая метод foo_weapper() из управляемого Си++ кода, который затем вызывает нативную функцию native_foo() using System; using souriz; namespace nezumi { class Program { static void Main(string[] args) { String s = CPPClass.foo_wrapper(); Console.WriteLine(s); } } } А теперь собираем все это вместе с помощью следующего командного файла: make.bat – командный файл, собирающий все файлы проекта воедино $cl.exe /c /MD nativecode.cpp $cl.exe /clr /LN /MD clrcode.cpp nativecode.obj $csc.exe /target:module /addmodule:clrcode.netmodule Program.cs $link.exe /LTCG /CLRIMAGETYPE:IJW /ENTRY:nezumi.Program.Main /SUBSYSTEM:CONSOLE /ASSEMBLYMODULE:clrcode.netmodule /OUT:mix.exe clrcode.obj nativecode.obj program.netmodule Если сборка прошла успешно, на диске образуется mix.exe файл, заглянув в который дизассемблером, мы увидим смесь управляемого и неуправляемого кодов. Проблема в том, что IDA Pro (самый популярный хакерский дизассемблер) не поддерживает смешанный режим и показывает либо машинный, либо управляемый код, в зависимости от настроек, выбранных еще на стадии загрузки исследуемого файла в базу. А потому написание «смешанных» программ – хороший защитный прием, существенно затрудняющий анализ (большинство начинающих хакеров вообще не увидят машинный код в .NET сборке и будут долго гадать, как же все это работает). Отладка «смешанных» программ, не содержащих отладочной информации (по умолчанию она не генерируется) – вообще кошмар, серьезно напрягающий даже гуру. |