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