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 :)