Откуда берутся шелл-коды Степан Ильин (step@gameland.ru) Хакер, номер #085, стр. 085-050-4 Теперь рассмотрим второй пример. section .data name db '/bin/sh', 0 section .text global _start _start: ; setreuid(0, 0) mov eax, 70 mov ebx, 0 mov ecx, 0 int 0x80 ; execve("/bin/sh",["/bin/sh", NULL], NULL) mov eax, 11 mov ebx, name push 0 push name mov ecx, esp mov edx, 0 int 0x80 Если не брать в расчет вызов функции execve(), то большая часть кода аналогична предыдущему примеру. Те же сегменты, тот же принцип вызова функции setreuid(). Проблема лишь в том, что в качестве второго параметра подпрограммы execve() передается массив, состоящий из двух элементов. Такой параметр уместно передавать через стек, в который сначала помещается ноль (push 0) , а потом адрес начала строки name (push name). Параметры передаются в обратном порядке неслучайно. Стек использует принцип LIFO (последним пришел - первым вышел), поэтому при извлечении данных из стека, сначала будет получен адрес переменной name и лишь потом ноль, что нам и надо. Необходимо позаботиться о том, чтобы подпрограмма знала, где эти параметры искать: и в этом неоценимую помощь окажет специальный регистр ESP (сокращение от Stack Pointer), который всегда указывает на адрес вершины стека. Все, что от нас требуется, - это скопировать содержимое регистра ESP в регистр ECX, который при запуске 0x80 прерывания будет обработан, как второй параметр функции. [почему нет?] Полученный код на ассемблере работоспособен на все 100% процентов. Его без труда можно откомпилировать с помощью nasm'а, запустить и даже посмотреть бинарник в шестнадцатеричном редакторе, то есть получить готовый шелл-код. Одна лишь проблема: толку от такого шелл-кода будет немного. Обе программы используют собственные сегменты данных, что полностью лишает возможности выполнить их внутри другого приложения. Это значит, что эксплойт не сможет инжектировать требуемый код в стек и выполнить его. Именно поэтому следующим нашим шагом будет оптимизация программы на работу без использования сегмента данных. Для этого мы немного схитрим и воспользуемся одним приемом, основанным на инструкциях ассемблера jmp (перейти на метку) и call (вызвать процедуру). Обе инструкции осуществляют переход в нужное место программы, но call, помимо всего прочего, заносит в стек адрес возврата. Это необходимо для того, чтобы после завершения процедуры вернуться в исходное место программы и продолжить ее нормальное выполнение. Рассмотрим суть приема на примере: jmp two one: pop ebx [текст программы] two: call one db 'строка' В самом начале программы осуществляется переход на метку two, за которой расположен вызов процедуры one. На самом деле никакой процедуры нет, однако с таким именем есть еще одна метка, которой и передается управления. В этот момент в стек заносится адрес возврата, то есть адрес следующей за call инструкцией. В нашем случае это строка байт - db 'string'. Получается, что к моменту, когда начинается обработка метки one, в стек уже будет помещен адрес начала строки. Нам остается лишь извлечь ее и использовать, как заблагорассудиться. Попробуем этим воспользоваться в реальной программе: для этого модифицируем наш второй пример. Для удобства назовем его shell.asm, так как далее будем работать только с ним. |