rms-fixed в DragonCTF 2019 (размещенный на DragonSector) представляет собой двоичный файл C, который использует потоки для параллельного выполнения HTTP-вызовов. Просто поигравшись с двоичным файлом, мы увидим, что он извлекает содержимое страниц в фоновом режиме. Нам нужно получить доступ к флагу из службы, работающей по адресу http://127.0.0.1:8000/flag
Я загрузил на свой привод двоичный файл с фиксированным среднеквадратичным значением. Вы можете получить к нему доступ здесь.
Давайте попробуем взаимодействовать с двоичным файлом, чтобы увидеть, что происходит:
Задача проста: нам нужно получить доступ к флагу из службы, привязанной к localhost / 127.0.0.1.
В двоичном файле есть проверки, которые не позволяют нам напрямую запрашивать 127.0.0.1.
Посмотрим, как выглядят чеки с помощью IDA.
Функция выборки вызывается в начале каждого потока.
Во-первых, он проверяет свой URL-адрес HTTP. Разделяет имя хоста и номер порта.
if ( strncmp("http://", *(url_1 + 1), v1) ) { errormsg = "not http"; LABEL_41: errorflag = 0; length = strlen(errormsg); goto LABEL_42; } url_afterhttp = (*(url_1 + 1) + 7LL); pathbegin = strchrnul(url_afterhttp, '/'); if ( pathbegin - url_afterhttp > 256 ) { errormsg = "host too long"; goto LABEL_41; } portstart = strchr(url_afterhttp, ':'); if ( portstart >= pathbegin ) portstart = 0LL; if ( portstart ) { hostname = strndup(url_afterhttp, portstart - url_afterhttp); port_str = strndup(portstart + 1, pathbegin - (portstart + 1)); if ( !port_str ) __assert_fail("atstr", "task/main.c", 0x7Fu, "fetch"); portnumber = atoi(port_str); if ( portnumber < 0 || portnumber > 0x10000 ) { puts("invalid port"); abort(); } free(port_str); } else { LOWORD(portnumber) = 80; hostname = strndup(url_afterhttp, pathbegin - url_afterhttp); }
затем пытается разрешить имя хоста с помощью gethostbyname2 ()
port_network = htons(portnumber); hostentpointer = gethostbyname2(hostname, 10); // 10 = AF_INET6 memset(&v22, 0, 0x80uLL); if ( hostentpointer ) { WORD1(v22) = port_network; LOWORD(v22) = 10; v2 = *hostentpointer->h_addr_list; v3 = *(v2 + 1); v23 = *v2; v24 = v3; if ( !memcmp(&v23, &in6addr_loopback, 0x10uLL) || !v23 ) { errormsg = "localhost not allowed"; goto LABEL_41; } } hostentpointer_1 = gethostbyname2(hostname, 2); // 2 = AF_INET4 if ( hostentpointer_1 ) { if ( hostentpointer_1->h_addrtype != 2 ) __assert_fail("hent4->h_addrtype == AF_INET", "task/main.c", 0xA0u, "fetch"); if ( **hostentpointer_1->h_addr_list == 127 || !**hostentpointer_1->h_addr_list ) { errormsg = "localhost not allowed"; goto LABEL_41; } }
Проверки localhost предназначены как для ipv6, так и для ipv4. ipv6 проверяет 128-битную память с последним установленным битом. Это :: 1 в форме памяти. Проверка ipv4 проверяет наличие 127 или 0 в первом октете. После нескольких часов попыток. Я не мог пройти эти проверки. Я пробовал транслировать через 255.255.255.0, но это тоже не сработало.
Позже программа отправляет GETS-запрос на нашу конечную точку:
if ( hostentpointer_1 ) { if ( hostentpointer_1->h_addrtype != 2 ) __assert_fail("hent4->h_addrtype == AF_INET", "task/main.c", 0xA0u, "fetch"); if ( **hostentpointer_1->h_addr_list == 127 || !**hostentpointer_1->h_addr_list ) { errormsg = "localhost not allowed"; goto LABEL_41; } } if ( !hostentpointer || !make_request(&v22, 0x80u, hostname, v11, &errormsg, &length) ) { if ( hostentpointer_1 ) { LOWORD(v22) = 2; WORD1(v22) = port_network; HIDWORD(v22) = **hostentpointer_1->h_addr_list; if ( make_request(&v22, 0x80u, hostname, v11, &errormsg, &length) ) goto LABEL_42; } else { v4 = __h_errno_location(); errormsg = hstrerror(*v4); } goto LABEL_41; }
Бинарный файл использует потоки, и оба потока получают содержимое параллельно без блокировок. Здесь может быть состояние гонки, но его немного сложно обнаружить. Я попытался найти глобальные переменные - если они есть - используемые обоими потоками параллельно. Нет такой вопиющей вещи, которую можно было бы заметить. Функция использует стек программы, который в любом случае будет разным для обоих потоков.
Ключ к решению этого вопроса - прочитать справочную страницу gethostbyname2.
Примечания
Функции gethostbyname () и gethostbyaddr () могут возвращать указатели на статические данные, которые могут быть перезаписаны более поздними вызовами. Копирования struct hostent недостаточно, поскольку он содержит указатели; требуется глубокая копия.
Поскольку мы используем gethostbyname в обоих потоках. Они оба будут возвращать указатели на один и тот же адрес, который используется потоками.
Гонка
Давайте покопаемся в make_request и посмотрим, как он работает:
Функция пытается установить соединение. После сбоя / тайм-аута возвращается 0. Поток выполнения продолжается в fetch ():
if ( !hostentpointer || !make_request(&v22, 0x80u, hostname, v11, &errormsg, &length) ) { if ( hostentpointer_1 ) { LOWORD(v22) = 2; WORD1(v22) = port_network; HIDWORD(v22) = **hostentpointer_1->h_addr_list; if ( make_request(&v22, 0x80u, hostname, v11, &errormsg, &length) ) goto LABEL_42; } else { v4 = __h_errno_location(); errormsg = hstrerror(*v4); } goto LABEL_41; }
После этого программа пытается подключиться в другой раз. Обновление sockaddr * с обновленным значением из hostentpointer_1- ›h_addr_list. Что может быть перезаписано в фоновом режиме другим потоком. Это загвоздка!
Эксплойт
Эксплойт для такой сложной проблемы настолько элегантен и прост.
Тема 1: http://google.com:8000/flag; занимает много времени и времени.
Тема 2: http://127.0.0.1/; не проходит проверку localhost и завершает работу.
Поток 2 завершится раньше потока 1 и перезапишет общую структуру хоста. Мы успешно перезапишем hostentpointer_1- ›h_addr_list на 127.0.0.1, и на данный момент мы уже прошли проверку localhost в программе. Давайте попробуем:
Удивительный CTF с умопомрачительными задачами. Большое спасибо, Dragon Sector :)