В процессе работы по созданию эффективной системы generic-детектирования шелл-кода и проверки результатов на сгенерированных случайным образом входных данных мне пришлось применять фаззинг (fuzzing) для тестирования различных дизассемблеров с открытым исходным кодом. В качестве дизассемблера для своего нынешнего проекта я выбрал библиотеку libdasm, ориентируясь на относительно давнюю историю ее развития и лицензию, относящую ее к категории общественного достояния. Однако очевидно, что написание качественного и полного дизассемблера для x86 процессоров – нетривиальная задача в силу сложности набора команд процессоров, основанных на архитектуре x86.
В прошлом для libdasm были характерны проблемы, связанные с некорректным дизассемблированием некоторых инструкций с плавающей точкой, но они были вызваны простой ошибкой, которая заключалась в сдвиге значений в таблицах опкодов на три единицы (были пропущены три ряда со значениями NULL), и поэтому исправить ошибку было относительно легко.
Но сегодня я наткнулся на проблему, которая, по-видимому, связана не с опкодами, а с ошибкой, приводящей к неверной расшифровке инструкций. При дизассемблировании инструкций, использующих префикс размера адреса, libdasm его не учитывает, что приводит к получению неверного значения длины инструкции, вследствие некорректного распознования размера операнда, использующего непосредственный адрес:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
[~] Verifying shellcode candidate offset 8eb0f0 008fe0f0[ 67a02232e830] > mov al,[0x30e83222] 008fe0f6[ 61] > popa 008fe0f7[ f9] > stc 008fe0f8[ ff4038] > inc [eax+0x38] 008fe0fb[ b269] > mov dl,0x69 008fe0fd[ 52] > push edx 008fe0fe[ 3f] > aas 008fe0ff[ 5e] > pop esi 008fe100[ 1a3dc31168aa] > sbb bh,[0xaa6811c3] 008fe106[ 59] > pop ecx 008fe107[ 9c] > pushf 008fe108[................] < |
Инструкция по адресу 008fe0f0
виртуализированной памяти расшифровывается неверно:
67
это вышеупомянутый префикс размера адресаa0
это код операцииmov al, moffs8
2232
это 16-разрядный адрес, который должен интерпретироваться как операндe830
не относится к данной инструкции
Исходя из принципа, что при обнаружении экзотической болезни следует проконсультироваться со вторым врачом, я решил попытать счастья еще с одним дизассемблером – библиотекой udis86:
1 2 3 4 |
$ udcli -noff -32 -s `python -c 'print 0x8eb0f0'` -c 10 shellcode/urandom.bin 67a02232 a16 mov al, [0x3222] e83061f9ff call 0xfffffffffff96139 40 inc eax |
Отлично – в этот раз инструкция mov
дизассемблировалась правильно. И, поскольку последовательность байт e830
уже не интерпретируется как часть операнда, указывающего на память инструкции mov
, она теперь верно дизассемблируется как инструкция call rel32
. К сожалению, udis86 – дизассемблер, поддерживающий набор команд x86-64, и он расширяет операнд инструкции call
, до 64 битов с сохранением знака, вновь приводя к ошибке при дизассемблировании.
Какие же инструкции в действительности «видит» и исполняет мой процессор? Поскольку мы в любом случае имеем дело с эмуляцией кода, мы можем просто установить точку останова (int 3)
в начале блока и начать пошаговую отладку под управлением gdb (за вычетом кое-какого мусора):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
Program received signal SIGTRAP, Trace/breakpoint trap. (gdb) disas $eip, $eip+5 => 0x0804b0c1: jmp 0x804b134 (gdb) si (gdb) disas $eip, $eip+10 Dump of assembler code from 0x804b134 to 0x804b13e: => 0x0804b134: addr16 mov 0x3222,%al 0x0804b138: call 0x7fe126d 0x0804b13d: inc %eax End of assembler dump. (gdb) si (gdb) si (gdb) disas $eip, $eip+10 Dump of assembler code from 0x7fe126d to 0x7fe1277: => 0x07fe126d: Cannot access memory at address 0x7fe126d |
Таким образом, процессор действительно видит инструкцию call и пытается ее выполнить. В данном случае это могло бы привести к катастрофическим результатам, поскольку позволило бы коду, использующему уязвимость, дающую возможность повышения привилегий для выполнения произвольного кода (вероятнее всего, шелл-коду) преодолеть защиту, обеспечиваемую виртуализацией. Чтобы можно было корректно обработать файл в виртуализаторе необходимо в программе заменить все инструкции, изменяющие регистр EIP, такие как CALL. Однако если такая инструкция не встречается в дизассемблерном листинге, то мы её не сможем корректно обработать.
После установки патча для libdasm (которая, как оказалось, вообще игнорирует префиксы размера адреса при разборе операндов) получаем правильно дизассемблированный код:
1 2 3 4 5 6 |
[*] 543 shellcode candidate offsets [~] Verifying shellcode candidate offset 8eb0f0 008fe0f0[ 67a02232] > mov al,[0x3222] 008fe0f4[................] < Emulating 008fe0f4: call 0x894229 Emulating CALL instruction from 8fe0f9. |
Усвоенные сегодня уроки:
- Включение в процесс тестирования ПО фаззинга (fuzzing) с использованием произвольных вводных данных – отличная мысль, позволяющая (как в данном случае) выявлять интересные уязвимости. В данном случае использовать уязвимость все равно было бы очень непросто, поскольку сегменты кода и данных указывали на разные базовые адреса, но опытный злоумышленник, возможно, смог бы успешно осуществить атаку.
- Общедоступная версия libdasm неверно дизассемблирует все инструкции с префиксом размера адреса, порождая интересные версии атаки на некоторые проекты, использующие эту библиотеку. Ждите выпуска патча для libdasm!
Интерпретации байткода для x86 процессоров: подводные камни