Издательский дом ООО "Гейм Лэнд"ЖУРНАЛ ХАКЕР 113, МАЙ 2008 г.

Трюки от Криса

Крис Касперски

Хакер, номер #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,%sn",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 сборке и будут долго гадать, как же все это работает). Отладка «смешанных» программ, не содержащих отладочной информации (по умолчанию она не генерируется) – вообще кошмар, серьезно напрягающий даже гуру.

Содержание
ttfb: 4.0209293365479 ms