hot-patch

hot-patch ядер Linux/BSD/NT

крис касперски ака мыщъх, no-email

наложение заплаток на ядро обычно требует перезагрузки системы, что не всегда приемлемо (особенно, в отношении серверов), однако, [при наличии хорошей травы] ядро можно залатать и в «живую». аналогичным образом поступают и защитные системы, rootkit'ам, и прочим программы, модифицирующие ядро на лету, но! практически все они делают это неправильно! ядро нужно хачить совсем не так! мыщъх укажет верный путь, пролегающий сквозь извилистый серпантин технических проблем и подводных камней, особенно характерных для многопроцессорных систем.

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

Аналогичным образом обстоят дела и с модификацией ядер операционных систем, разработчики которых предоставляют программисту набор API-функций для управления памятью, процессами и прочими системными ресурсами, но… только не самим ядром! Дурной тон программирования, говорите?! Если бы это было так, никакой вменяемый программист не стал бы извращаться, рискуя нарушить стабильность системы! К модификации ядра прибегают не от хорошей жизни, а от нищеты стандартных механизмов.

Вмешательство во внутреннюю жизнь ядра — это грязный хак, всегда таящий в себе потенциальную опасность развалить все к чертям собачьим. Большинство программ, модифицирующих ядро, делают это настолько небрежно, что при знакомстве с ними остается только удивляться — как же они ухитряются работать и не падать? На самом деле, они падают, причем, на многопроцессорных машинах частота падений существенно увеличивается.

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

Расплатой за корректность становится резко возросшая сложность техники модификации, а так же некоторое замедление работы системы, поэтому, к hot-patch'уследует прибегать лишь в тех случаях, когда перезагрузка невозможна или крайне нежелательна.

hot-patch_image_0.jpg

Рисунок 1  самомодифицирующийся код при неосторожном обращении ведет к развалу системы

Для создания «горячей» заплатки необходимо иметь diff-файл, показывающий каким образом была заткнута дыра (см. листинг 1), после чего нам остается только перевести исправления на язык ассемблера, модифицируя ядро непосредственно в оперативной памяти.

— sys/kern/uipc_mbuf2.c17 Mar 2006 04:15:51 -00001.24

+++ sys/kern/uipc_mbuf2.c7 Mar 2007 19:21:48 -00001.24.2.1

@@ -226,16 +226,14 @@ m_dup1(struct mbuf *m, int off, int len,

{

struct mbuf *n;

int l;

-int copyhdr;

if (len > MCLBYTES)

return (NULL);

if (off == 0 && (m→m_flags & M_PKTHDR) != 0) {

copyhdr = 1;

MGETHDR(n, wait, m→m_type);

+M_DUP_PKTHDR(n, m);

l = MHLEN;

} else {

-copyhdr = 0;

MGET(n, wait, m→m_type);

l = MLEN;

}

@@ -249,8 +247,6 @@ m_dup1(struct mbuf *m, int off, int len,

if (!n)

return (NULL);

-if (copyhdr)

-M_DUP_PKTHDR(n, m);

m_copydata(m, off, len, mtod(n, caddr_t));

n→m_len = len;

Листинг 1 патч для OpenBSD, приведенный с незначительными сокращениями

К сожалению, раздобыть diff-файл удается далеко не всегда. Зачастую, разработчики распространяют кумулятивные обновления, включающие в себя множество исправлений, не имеющих к дыре никакого отношения и модифицирующий внутренние структуры ядра (см. рис. 2), в результате чего, «горячая» модификация кода влечет за собой необходимость перестройки данных, с которыми работает ядро, а это уже нереально, особенно с учетом того, что обработка данных не атомарна и в момент наложения заплатки старые данные могут находится на различных стадиях обработки, будучи загруженными в локальные переменные и регистры.

Рисунок 2 пример кумулятивной заплатки для Red Hat, исправляющий ошибку в подсистеме SCTP и затыкающий сразу две дыры — одна из которых опасная (important), а другая — не очень (moderate), в результате чего мы имеем множество изменений, значительная часть которых не имеет никакого отношения к безопасности вообще (http://rhn.redhat.com/errata/RHSA-2007-0085.html)

Скачав текущий CVS, можно попробовать отыскать изменения, относящиеся к дыре, заглянув в change-log (см. листинг 2). Если нам повезет, они будут явно обозначены в комментариях. Как будет показано в дальнейшем, совершенно необязательно, исправлять отдельные машинные команды. Гораздо проще скопировать всю функцию целиком. Значит, нам нужен список измененных функций, получить который намного проще.

commit b8fa2f3a82069304acac1f9e957d491585f4f49a

Author: Michael Chan mchan@broadcom.com

Date: Fri Apr 6 17:35:37 2007 -0700

[TG3]: Fix crash during tg3_init_one().

The driver will crash when the chip has been initialized by EFI before

tg3_init_one(). In this case, the driver will call tg3_chip_reset()

before allocating consistent memory.

The bug is fixed by checking for tp→hw_status before accessing it

during tg3_chip_reset().

Листинг 2 фрагмент change-log'а с комментариями, поясняющими каким именно способом была исправлена дыра (http://www.kernel.org/pub/linux/kernel/v2.6/testing/ChangeLog-2.6.21-rc7)

А что делать, если исходные тексты недоступны (как, например, в случае Windows) или в них не удается разобраться?! Тогда — необходимо прошмыгнуться по security-сайтам, раскурить имеющиеся exploit'ы, в общем, так или иначе разобраться где прячется уязвимость и как ее устранить.

Достаточно часто первооткрыватели дыры не только сообщают параметры вектора атаки, но и приводят дизассемблерные листинги двоичных модулей (или реконструированный псевдокод), с указанием ошибок (см. листинг 3) или на худой конец описывают обстоятельства атаки, позволяющие найти дыру самостоятельно.

_; stdcall NdisTapiDispatch(x, x) .text:000115E8 _NdisTapiDispatch@8 proc near ; DATA XREF: DriverEntry(x,x)+13E#o .text:000115E8 .text:000115E8push ebp .text:000115E9mov ebp, esp … .text:00011615cmp eax, 8FFF23C0h;IOCTL .text:0001161Ajz short loc_11669;DoIoctlConnectWork() .text:0001161Ccmp eax, 8FFF23C8h .text:00011621jz short loc_1165C {…} .text:00010B16 ; stdcall DoIoctlConnectWork(x, x, x, x)

.text:00010B16 _DoIoctlConnectWork@16 proc near ; CODE XREF:NdisTapiDispatch(x,x)+85#p

.text:00010B16

.text:00010B16mov ecx, _DeviceExtension

.text:00010B1Cpush edi

.text:00010B1Dmov edi, ds:imp_@KfAcquireSpinLock@4 ;KfAcquireSpinLock(x) .text:00010B23add ecx, 4Ch .text:00010B26call edi ; KfAcquireSpinLock(x); ⇐ BUG .text:00010B28cmp [esp+4+arg_8], 8 ;InputBuffer length .text:00010B2Dmov dl, al .text:00010B2Fjb loc_10BC5 .text:00010B35cmp [esp+4+arg_C], 4 ;OutputBuffer length .text:00010B3Ajb loc_10BC5 .text:00010B40mov ecx, _DeviceExtension … .text:00010B50mov esi, ds:imp_@KfReleaseSpinLock@8 ;KfReleaseSpinLock(x,x) .text:00010B56jnz short loc_10B8D .text:00010B58mov dword ptr [ecx+4], 2 .text:00010B5Fmov ecx, _DeviceExtension … .text:00010B73call esi ; KfReleaseSpinLock(x,x);KfReleaseSpinLock(x,x) .text:00010BC5 loc_10BC5:; Return … .text:00010BD7retn 10h Листинг 3  фрагмент драйвера Ndistapi.sys из Windows XP SP2, хорошо видно, что при срабатывании условных переходов jb loc_10BC5 функция DoIoctlConnectWorkвыходит без освобождения SpinLock'а, что ведет к краху системы и для исправления дыры достаточно всего лишь передвинуть вызов KfReleaseSpinLock на одну строку ниже (http://www.reversemode.com/index.php?option=com_remository&Itemid=2&func=fileinfo&id=47) ===== техника горячей модификации ядра ===== В операционных системах семейства Linux и NT, ядро проецируется на единое 4 Гбайтовое адресное пространство. В Linux ядро занимает 1 Гбайт, располагаясь по адресам C000000h – FFFFFFFFh. В NT/W2K/XP ядро по умолчанию «отъедает» 2 Гбайта, занимая старшую половину адресного пространства (8000000h – FFFFFFFFh), но если указать ключ /3GB в файле boot.ini (поддерживаемый начиная с Windows 2000 Advanced Server/Datacenter Server), то ядро ужмется до 1 Гбайта (см. рис. 3). Ядро FreeBSD вплоть до версии 3.х занимало всего 256 Мбайт, но начиная с версии 4.x разрослось до 1 Гбайта, оккупируя регион C000000h – FFFFFFFFh,однако, некоторые эмуляторы, запускающие Free- и NetBSD «поверх» других систем, размещают ядро начиная с адреса A000000h, но подробно вдаваться в эти и другие экзотичные случаи мы не будем, иначе вместо статьи получится настоящий талмуд. Рисунок 3 адресное пространство NT/W2K/XP в конфигурации по умолчанию (слева) и Linux/BSD/W2K_3GB Память ядра доступна с прикладного уровня через псевдоустройство\Device\PhysicalMemory (NT/W2K/XP) и /dev/kmem (Linux/BSD). В ранних версиях NT псевдоустройство PhysicalMemory было открыто для чтения/записи любому пользователю из группы «Администраторы», однако, начиная с Windows 2003 Server SP1 к нему не может получить доступ даже «System» (подробнее об этом рассказывается в заметке «ChangestoFunctionalityinMicrosoftWindowsServer 2003 ServicePack 1 Device\PhysicalMemoryObject»: www.microsoft.com/technet/prodtechnol/windowsserver2003/library/BookofSP1/e0f862a3-cf16-4a48-bea5-f2004d12ce35.mspx). Рисунок 4 упрощенная архитектура NT/W2K/XP UNIX-подобные системы так же закрывают доступ к kmem и уже недалек тот день, когда из большинства дистрибутив оно будет полностью изъято. И хотя псевдоустройство /dev/mem (физическая память до линейной трансляции) по-прежнему в строю и отказаться от него никак не получается (поскольку, его используют многие приложения, те же X'ы, например), для модификации ядра оно не годится, поскольку не обеспечивает атомарности, а, значит, наложение заплатки может привести к краху системы. Из драйвера (или, выражаясь терминологией UNIX-подобных систем, «загружаемого модуля»), работающего на нулевом кольце, память ядра защищена от _непреднамеренной_ модификации, однако, эту защиту легко отключить (исключение составляют 64-разрядные версии XP и Висты, в которых встроена неотключаемая защита от умышленной модификации под названием PatchGuard, техника обхода которой описана мыщъх'ем в статье «взлом patch-guard» — http://nezumi.org.ru/patch-guard-hack.zip). hot-patch_image_4.jpg Рисунок 5 упрощенная архитектура Linux В NT/W2K/XP/Виста-x86 существует два способа отключения защиты от непреднамеренной модификации из нулевого кольца: статический и динамический. Статический сводится к созданию параметра EnforceWriteProtection типа REG_DWORD со значением 0x0 в HKLM\SYSTEM\CurrentControlSet\Control\SessionManager\MemoryManagement, а динамический осуществляется сбросом WP-бита в управляющем регистре CR0, который расшифровывается как Write Protection. Повторная установка бита включает защиту. Практический пример использования приведен ниже (см. листинг 4): .386 .model flat, stdcall .code DriverEntry proc moveax, cr0; грузим управляющий регистр cr0 в регистр eax movebx, eax; сохраняем бит WP в регистре ebx andeax, 0FFFEFFFFh; сбрасываем бит WP, запрещающий запись movcr0, eax; обновляем управляющий регистр cr0 ; # теперь защита отключена! ; # накладываем заплатку или перехватываем системные функции, ; # модифицируя память ядра по своему усмотрению movcr0, ebx; восстанавливаембит WP ; # защита снова включена! mov eax, 0C0000182h; STATUS_DEVICE_CONFIGURATION_ERROR ret DriverEntry endp Листинг 4 код псевдодрайвера для NT, временно отключающего защиту ядра от модификации, а затем включающего ее обратно Аналогичным способом можно отключить и защиту ядра в UNIX-подобных системах. Сброс WP-бита действует на аппаратном уровне, открывая все accessibly-станицы для модификации независимо от того, разрешена ли в них запись или нет. Естественно, текущий уровень привилегий (CPL) не должен превышать CPL модифицируемой страницы, иначе процессор сгенерирует исключения типа «ошибка доступа» (то есть, с прикладного уровня ядро все равно остается недоступно). Пример реализации KLD-модуля (DynamicKernelLinker ) для FreeBSD приведен ниже (см. листинг 5): #include <sys/types.h> #include <sys/param.h> #include <sys/proc.h> #include <sys/module.h> #include <sys/sysent.h> #include <sys/kernel.h> #include <sys/sysproto.h> #include <sys/systm.h> #include <sys/syscall.h> unsigned long cr0; глобальная переменная, хранящая регистр cr0 #define read_cr0() (\ макрос для чтения регистра cr0 !asm(«movq cr0,%0\n\t"\ :"=r" (__cr0))) #define write_cr0(x) \// макрос для записи регистра cr0 ! __asm__("movq %0,cr0»: :«r» (x) /* процедура начальной загрузки модуля */ static int load (struct module *module, int cmd, void *arg) { int error = 0; switch (cmd) { case MOD_LOAD:/* загрузка модуля */ read_cr0(); читаем регистр cr0 cr0 = cr0 & 0xFFFEFFFF; сбрасываем WP-бит write_cr0(cr0); обновляем регистр cr0 printf («kernel protection is disabled\n»); break; case MOD_UNLOAD:/* выгрузка модуля */ read_cr0(); читаем регистр cr0 cr0 = cr0 | 0x10000; сбрасываем WP-бит write_cr0(__cr0); обновляем регистр cr0 printf («kernel protection isenabled\n»); break; default: error = EINVAL; break; } return error; } /* сердце программы - макрос DECLARE_MODULE, декларирующей модуль */ DECLARE_MODULE(syscall, syscall_mod, SI_SUB_DRIVERS, SI_ORDER_MIDDLE); Листинг 5 исходный текст KLD-модуля для FreeBSD, отключающего защиту ядра от записи при загрузке и включающий ее обратно при выгрузке Сброс WP-бита носит глобальное воздействие, затрагивающее не только ядро, но так же распространяющиеся и на прикладные процессы, поэтому, отключать защиту на долгое время крайне нежелательно. Некоторые программы (особенно протекторы исполняемых файлов и некоторые защиты) явно закладываются на генерацию исключения, возникающую при попытке записи в ReadOnly-страницу и после сброса WP-бита перестают работать. Как вариант, можно проиграться низкоуровневыми функциями семейства pte_x (например, pte_mkwrite), работающих с каталогом страниц. Это более красивый и надежный, однако, увы, системно-зависимый путь, поэтому на практике приходится идти на компромисс, жертвуя надежностью в пользу переносимости. ===== проблема когерентности и пути ее решения ===== ОК, теперь мы можем модифицировать ядро, накладывая «горячие» заплатки или перехватывая системные функции, внедряя в их начало команду перехода на свое тело. Большинство rootkit'ов именно так и поступает, забыв о том, что «подопытный» код может исполняться одновременно с его модификацией, приводя к краху системы. Причем, эта «одновременность» довольно относительна. Как известно, на однопроцессорных машинах потоки выполняются последовательно, а не параллельно и иллюзия «одновременности» создается лишь за счет быстрого переключения между ними. Допустим поток А был прерван при исполнении функции foo, после чего планировщик передал управление потоку B, выполняющему функцию bar. Вопрос: что произойдет, если мы модифицируем содержимое foo? Очевидно, когда поток А вновь получит управление, он окажется в совершенно другом окружении, возможно, даже пытаясь продолжить выполнение с _середины_ новой машинной команды!!! (примечание: разумеется термин «поток» условен, в некоторых операционных системах такой сущности просто нет и планировка осуществляется на уровне процессов, так же это может быть и обработчик исключений, и отложенная процедура, да все, что угодно!). Причем, никакой возможности узнать — находится ли данный участок кода под выполнением у нас нет! То есть как это нет?! Очень даже есть — просто просматриваем контексты всех потоков (процессов, отложенных функций), при необходимости дожидаясь момента, когда обозначенный код выйдет из под управления, после чего правим его. Вот и все! Просто, элегантно, но увы… неработоспособно. Во-первых, добраться до контекстов процессов/потоков/отложенных функций в одно мгновение невозможно! Поток, анализирующий контексты других потоков, исполняется параллельно с ними и пока мы читаем контекст очередного потока, предыдущие уже могли измениться. Теоретически возможно «замораживать» все потоки на время модификации (предварительно дождавшись, пока они покинут пределы модифицируемого кода), а потом «размораживать» их обратно, однако, этот трюк имеет довольно ограниченную область применения. В частности, он не работает с обработчиками аппаратных прерываний, блокирование которых крайне нежелательно или же вовсе недопустимо. Во-вторых, все это слишком системно-зависимо, а ковыряться во внутренних (и зачастую недокументированных) структурах оси — гиблое дело. Существует несколько универсальных решений данной проблемы. Вот, например, одно из них — внедряем в начало модифицируемой функции команду INT 03h, соответствующую однобайтовому опкоду CCh, и тогда при ее вызове процессор будет генерировать отладочное исключение, перехватываемое нашим обработчиком, передающим управление на «отпаченную» версию обозначенной функции, расположенную совсем в другом месте. Оригинальная функция (за исключением первого байта) остается неизменной и потому мы можем не волноваться, за то, что какой-то неожиданно проснувшийся поток продолжит ее выполнение. Поскольку, выполнение машинных команд — атомарная операция, то записывать INT 03h можно поверх любой команды и это _гарантированно_ не приведет к развалу систему, даже если модифицируемая команда исполняется в данный момент на другом процессоре! Процессор выполнят либо оригинальную команду, либо INT 03h. «Промежуточное» состояние у него попросту отсутствует. Достоинство данного решения в том, что оно не требует анализа ассемблерного кода исходной функции. Мы просто пишем INT 03h и все! Недостатки — а) при модификации более чем одной функции, обработчик должен анализировать адрес исключения, чтобы определить куда передать управление; б) это плохо работает с отладчиками (де-факто, INT 03h представляет собой программную точку останова); в) часто вызываемые функции при такой методике перехвата будут заметно тормозить, снижая общую производительность. Более сложное, но вместе с тем и более «технологическое», решение заключается в записи команды jmp near target поверх машинной команды равной или большей длины, где target – адрес модифицируемой функции, которой передается управления. В 32-битном режиме длина jmp near target составляет 5 байт, что существенно превышает среднюю длину x86, равную 2,5 байтам. Рассмотрим код наугад выбранной функции, дизассемблерный листинг которой приведен ниже: .text:С8001D60sub_С001D60proc near .text:С8001D60 .text:С8001D60 55pushebp .text:С8001D61 89 E5movebp, esp .text:С8001D63 57pushedi .text:С8001D64 56pushesi .text:С8001D65 53pushebx .text:С8001D66 8B 7D 08movedi, [ebp+arg_0] .text:С8001D69 8B 1D CC 64 00 08movebx, ds:dword_C80064CC .text:С8001D6F 85 DBtestebx, ebx .text:С8001D71 74 1Ejzshort loc_C8001D91 Листинг 6 фрагмент наугад взятой функции, в которую необходимо внедрить jmp Длина первых шести машинных команд варьируется от одного до трех байт, и потому ни одна из них не пригодна для патча и jmp near target может быть записана лишь поверх седьмой команды — «mov ebx,ds:dword_C80064CC». Поскольку, первые шесть байт модифицируемой функции выполняются до передачи управления на target, они не должны дублироваться в целевой функции, иначе произойдет крах. Очевидно, что подобным образом может быть модифицирована далеко не всякая функция. Ниже приведен пример функции, в которой между началом и первым условным переходом нет ни одной машинной команды длиннее трех байт (см. листинг 7), а это значит, что существует риск потери управления! Условный переход перепрыгивает через jmp near target и мы остаемся с носом. .text:C8002310sub_C8002310proc near .text:C8002310 .text:C8002310 55pushebp .text:C8002311 89 E5movebp, esp .text:C8002313 83 EC 04subesp, 4 .text:C8002316 57pushedi .text:C8002317 56pushesi .text:C8002318 53pushebx .text:C8002319 8B 75 0Cmovesi, [ebp+arg_4] .text:C800231C 85 F6testesi, esi .text:C800231E 75 10jnzshort loc_С8002330; continue –>—! .text:C8002320 31 C0xoreax, eax; ! .text:C8002322 EB 72jmpshort loc_C8002396; -to retn —>–!—-! .text:C8002324 8D B6 00 00+align 10h; [выравнивание] ! ! .text:C8002330loc_C8002330:; ←————-! ! .text:C8002330 80 3E 2Fcmpbyte ptr [esi], 2Fh; ! .text:C8002333 74 5Bjzshort loc_C8002390; to subroutine–! ! .text:C8002335 6A 2Fpush2Fh; ! ! .text:C8002337 8B 45 08moveax, [ebp+arg_0]; ! ! .text:C800233A 50pusheax; ! ! .text:C800233B E8 48 E8 FF FFcall_strrchr; ⇐[FIX_1 HERE] ! ! …; ! ! .text:C8002390loc_8002390:; ! ! .text:C8002390 56pushesi; ! ! .text:C8002391 E8 9A 1D 00 00callsub_C8004130; ⇐[FIX_2 HERE] ! ! .text:C8002396; ! ! .text:C8002396 loc_C8002396:; ←————-! ! .text:C8002396 8D 65 F0leaesp, [ebp+var_10]; ←——————! .text:C8002399 5Bpopebx .text:C800239A 5Epopesi .text:C800239B 5Fpopedi .text:C800239C 89 ECmovesp, ebp .text:C800239E 5Dpopebp .text:C800239F C3retn .text:C800239F sub_C8002310endp Листинг 7 фрагмент функции, в которой между началом и первым условным переходом нет ни одной машинной команды длиннее трех байт На самом деле, все не так уж и мрачно. Покурив хорошей травы (только где ее взять в это время года? ведь еще не сезон…) и как следует подумав головой мы догадаемся, что можно записать двухбайтовую команду jnz short C8002324 поверх трехбайтовой команды mov esi, [ebp+arg_4] (после выполнения «sub esp,4» флаг нуля _гарантированно_ сброшен), предварительно разместив по адресу C8002324h (0Сh свободных байт вставленные компилятором для выравнивания) команду jmp near target. Хорошо, а если бы свободных байт в нашем распоряжении не было — что бы мы стали делать тогда? ОК, смотрим на первый условный переход — jnz short loc_С8002330. Если он не выполняется — происходит выход из функции, что нас вполне устраивает. От того, что мы не перехватим вызов функции, заканчивающийся немедленным возвратом, мы много не потеряем. Идем дальше. И… встречаем второй условный переход jz short loc_С8002390, при срабатывании которого управление получаем функция sub_C8004130, в противном же случае продолжается нормальное выполнение программы. Короче, ветвление. А с раз это ветвление, то мы должны внедрить _два_ перехватчика: один — поверх команды call _strrchr и другой — поверх call sub_C8004130. Впрочем, в зависимости от условий задачи, может хватить и одного перехватчика (например, если нужно исправить код, находящейся в ветке С8002335h. Тем не менее, несложно вообразить себе функцию, целиком состоящую из коротких команд или вызывающую переполнение буфера прежде, чем удастся внедрить jmp на свой обработчик и тогда приходится прибегать к уже описанному трюку с INT 03h. Другие недостатки данного метода: перехват каждой функции осуществляется индивидуально и написание универсального перехватчика представляет довольно сложную задачу, требующую как минимум наличия встроенного дизассемблера, что плохо подходит для rootkit'ов, однако, вполне приемлемо для «горячих» заплаток (не так уж трудно выпустить серию заплаток, по одной для каждой версии операционной системы). ===== проблема атомарности и пути ее решения ===== Запись команды jmp near target должна представлять атомарную операцию, выполняемую целиком за один раз, в противном случае может сложиться ситуация, при которой процессор попытается выполнить «недописанную» команду со всеми вытекающими отсюда последствиями, но инструкция вида mov [mem], reg8/16/32 не позволяет записывать более четырех байт, а потому совершенно непригодна для решения поставленной задачи. Некоторые хакеры используют SSE-инструкции, позволяющие записывать более четырех байт и на однопроцессорных машинах такой трюк работает вполне нормально, но на многопроцессорных системах существует вероятность (пускай и ничтожная) модификации кода в процессе его выполнения, а префикс блокировки шины (LOCK) перед SSE-командами вставлять нельзя. К счастью, начиная с первопней в лексиконе процессоров существует замечательная команда CMPXCHG8B, поддерживающая префикс LOCK и записывающая одним махом целых восемь байт! Для внедрения пятибайтовой инструкции jmp near target этого более чем достаточно. Естественно, чтобы не затереть оставшиеся три байта, мы сначала должны прочитать восемь байт из памяти, наложить на них jmp near target и записать полученную смесь обратно. Вот тут некоторые спрашивают: зачем это делать, ведь jmp – это безусловный переход и находящиеся за ним команды никогда не получат управления. А затем, что находящиеся за ним команды могли получить управление еще _до_ модификации. (примечание: некоторые трансляторы не поддерживают инструкцию CMPXCHG8B и в этом случае ее можно задать через директиву DB или _emit в байтом виде: 0Fh C70Eh соответствующую команде CMPXCHG8B [ESI]). Готовый пример реализации внедрения jmp near target посредством CMPXCHG8B приведен ниже (см. листинг 8): PUSHEAX; сохраняем адрес модифицируемой команды ADDEAX, 5; sizeof(jmp near target) SUBEBX, EAX; вычислениеоперанда команды jmp near target POPESI; ESI - адрес модифицируемой команды XOREAX, EAX; обнуляем EDX:EAX чтобы они… XOREDX, EDX;…не совпадал с [ESI] CMPXCHG8B[ESI]; читаем 8 байт из [ESI] PUSHEDX; заносим в стек 4 старших прочитанных байта INCESP; оставляем из них три PUSHEBX; накладываем операнд команды jmp near target PUSH0E9000000h; накладываем опкод команды jmp near target ADDESP, 3; удаляем три незначащих нуля POPEBX; подготавливаем регистры… POPECX;…к выполнению CMPXCHG8B LOCK CMPXCHG8B[ESI]; записываем 8 байт в [ESI], блокируя шину** Листинг 8 внедрение jmp near target посредством команды CMPXCHG8B, в регистре EAX передается адрес записи jmp, а в регистре EBX – target ===== наложение заплатки (советы и рецепты) ===== Чтобы не связываться с ассемблером, достаточно скопировать исправленный вариант функции в свой модуль — нехай транслятор компилирует, тогда нам останется всего лишь передать на нее управление командой jmp near target (естественно, вместе с функцией необходимо скопировать и все макросы, заданные директивой define, а так же подключить необходимые заголовочные файлы). При этом мы наталкивается на следующие проблем: а) если функция обращается к глобальным переменным, то мы должны подставить адреса переменных оригинальной функции, иначе поведение системы станет непредсказуемым; б) адреса «внутренних» функций ядра, вызываемые данной функцией, так же необходимо подставлять вручную; в) мы не можем приказать компилятору исключить уже выполненные команды, поэтому прежде чем передавать управление откомпилированной функции, следует выполнить «откат», повесив на jmp near target промежуточный обработчик, который в случае листинга 9 будет выглядеть так: POPEBP POPESI POPEDI POPEBP Листинг 9 выполнение «отката» для «нейтрализации» уже выполненных команд функцией sub_С001D60 (см. листинг 6) Как видно, мы выполняем обратную последовательность команд, восстанавливая стек и содержимое регистров, а при необходимости освобождая выделенную функцией память и прочие системные ресурсы. С Windows в этом плане сложнее. Исходных текстов нет и вставить исправленную функцию в драйвер не получится. Здесь есть два пути: дизассемблировать ядро и переписать код на Си (трудоемко, зато надежно) или же скопировать функцию прямо в двоичном виде, корректируя ссылки на функции, вызываемые по относительным адресам. Поскольку, адрес загрузки драйвера наперед не известен, коррекцию приходится осуществлять на «лету»: заносим адреса машинных команд call target/jmp target в специальный массив, хранящийся в драйвере, а в процедуре инициализации обрабатываем все элементы, добавляя к непосредственному операнду базовый адрес загрузки, не забыв предварительно отключить защиту от записи, поскольку по умолчанию кодовая секция доступна только на чтение. hot-patch_image_5.jpg Рисунок 6 проникновение в ядро ===== »> врезка алгоритм работы команды CMPXCHG8B ===== Команда CMPXCHG8B сравнивает EDX:EAX с m64 и, если они равны, устанавливает флаг нуля, записывая ECX:EBX в m64, в противном случае сбрасывает флаг нуля, загружая m64 в ED:EAX. Команда поддерживает префикс блокировки шины LOCK. ===== »> врезка hot-patch монолитных ядер ===== В Linux и xBSD существует возможность скомпилировать монолитное ядро, без поддержки загружаемых модулей, что благотворно сказывается на безопасности, но затрудняет наложение «горячих» заплаток. Однако, если псевдоустройство /dev/mem остается доступным (а чаще всего дела обстоят именно так), мы можем найти в памяти таблицу системных вызовов и внедрить в ядро свой собственный код, работающий на нулевом кольце и накладывающий заплатку по описанной мыщъхем методике. ===== заключение ===== Заштопать ядро операционной системы без перезагрузки — очень сложно, но вполне реально. Конечно, далеко не всякому администратору это по силам, однако, фирмы, занимающиеся поддержкой, могут выпускать неофициальные «горячие» заплатки, расхватываемые словно пирожки!Ведь это не просто актуальная, а супер-актуальная тема, в которой заинтересованы миллионы пользователей, так что на счет спроса можно не сомневаться.