Flare-On 2019 write-up

в 2:00, , рубрики: ctf, frida, ida, информационная безопасность, реверс-инжиниринг
Flare-On 2019 write-up - 1

-0x01 — Intro

Данная статья посвящена разбору всех заданий Flare-On 2019 — ежегодного соревнования по реверс-инжинирингу от FireEye. В данных соревнованиях я принимаю участие уже второй раз. В предыдущем году мне удалось попасть на 11-ое место по времени сдачи, решив все задачи примерно за 13 суток. В этом году набор тасков был проще, и я уложился в 54 часа, заняв при этом 3 место по времени сдачи.

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

Если вас заинтересовало, то добро пожаловать под кат!

0x00 — Содержание

  1. 0x01 — Memecat Battlestation [Shareware Demo Edition]
  2. 0x02 — Overlong
  3. 0x03 — Flarebear
  4. 0x04 — Dnschess
  5. 0x05 — demo
  6. 0x06 — bmphide
  7. 0x07 — wopr
  8. 0x08 — snake
  9. 0x09 — reloadered
  10. 0x0A — Mugatu
  11. 0x0B — vv_max
  12. 0x0C — help
  13. 0x0D — Итог

0x01 — Memecat Battlestation [Shareware Demo Edition]

Welcome to the Sixth Flare-On Challenge!

This is a simple game. Reverse engineer it to figure out what "weapon codes" you need to enter to defeat each of the two enemies and the victory screen will reveal the flag. Enter the flag here on this site to score and move on to the next level.

* This challenge is written in .NET. If you don't already have a favorite .NET reverse engineering tool I recommend dnSpy

** If you already solved the full version of this game at our booth at BlackHat or the subsequent release on twitter, congratulations, enter the flag from the victory screen now to bypass this level.

Данный таск был выложен заранее в рамках Black Hat USA 2019, примерно тогда же я его и решил. Я не помню, как его решал Таск довольно простой, поэтому рассматривать его решение не будем.

0x02 — Overlong

The secret of this next challenge is cleverly hidden. However, with the right approach, finding the solution will not take an overlong amount of time.

Дан x86 .exe файл. При попытке запуска выводится сообщение со следующим содержимым:

Flare-On 2019 write-up - 2

При анализе приложения можно обнаружить, что сообщение хранится в некоторой кодировке с переменной длиной символа (от 1 до 4 байт). При вызове функции декодирования ей передается длина ожидаемого результата, которая короче самого сообщения, из-за чего не виден флаг. Можно исправить передаваемое в функцию значение длины в режиме отладки и получить полное сообщение с флагом:

Flare-On 2019 write-up - 3

Также можно было переписать алгоритм декодирования на Python и получить флаг:

msg = [ ... ]  # сюда необходимо вставить закодированное сообщение

output = []
i = 0
while i < len(msg):
    if (msg[i] >> 3) == 0x1e:
        out_char = (
            ((msg[i + 3] & 0x3F) << 0 ) |
            ((msg[i + 2] & 0x3F) << 6 ) |
            ((msg[i + 1] & 0x3F) << 12) |
            ((msg[i + 0] &    7) << 18)
        )
        output.append(out_char)
        i += 4
    elif (msg[i] >> 4) == 0x0e:
        out_char = (
            ((msg[i + 2] & 0x3F) << 0 ) |
            ((msg[i + 1] & 0x3F) << 6 ) |
            ((msg[i + 0] & 0xF) << 12)
        )
        output.append(out_char)
        i += 3
    elif (msg[i] >> 5) == 6:
        out_char = (
            ((msg[i + 1] & 0x3F) << 0 ) |
            ((msg[i + 0] & 0xF) << 6 )
        )
        output.append(out_char)
        i += 2
    else:
        output.append(msg[i])
        i += 1

print(bytes([i for i in output]))
# b'I never broke the encoding: I_a_M_t_h_e_e_n_C_o_D_i_n_g@flare-on.com'

0x03 — Flarebear

We at Flare have created our own Tamagotchi pet, the flarebear. He is very fussy. Keep him alive and happy and he will give you the flag.

В данном таске дан apk файл для Android. Рассмотрим метод решения без запуска самого приложения.

Первым делом необходимо получить исходный код приложения. Для этого с помощью набора утилит dex2jar преобразуем apk в jar и затем получим исходный код на Java с помощью декомпилятора, в качестве которого я предпочитаю использовать cfr.

~/retools/d2j/d2j-dex2jar.sh flarebear.apk
java -jar ~/retools/cfr/cfr-0.146.jar --outputdir src flarebear-dex2jar.jar

Анализируя исходный код приложения, можно найти интересный метод .danceWithFlag(), который находится в файле FlareBearActivity.java. Внутри .danceWithFlag() происходит расшифровка raw-ресурсов приложения с помощью метода .decrypt(String, byte[]), первым аргументом которого является строка, полученная с помощью метода .getPassword(). Наверняка флаг находится в зашифрованных ресурсах, поэтому попробуем расшифровать их. Для этого я решил немного переписать декомпилированный код, избавившись от зависимостей Android и оставив только необходимые для расшифровки методы, чтобы в результате можно было скомпилировать полученный код. В дальнейшем, при анализе, было обнаружено, что метод .getPassword() зависит от трех целочисленных значений состояния. Каждое значение лежит в небольшом интервале от 0 до N, поэтому можно перебрать все возможные значения в поисках нужного пароля.

В результате получился следующий код:

Main.java

import java.io.InputStream;
import java.nio.charset.Charset;
import java.security.Key;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.KeySpec;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.stream.Collectors;
import java.util.Collections;
import java.io.*;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;

public final class Main {

    public static void main (String args []) throws Exception {
        Main a = new Main();

        InputStream inputStream = new FileInputStream("ecstatic");
        long fileSize = new File("ecstatic").length();
        byte[] file1 = new byte[(int) fileSize];
        inputStream.read(file1);

        inputStream = new FileInputStream("ecstatic2");
        fileSize = new File("ecstatic2").length();
        byte[] file2 = new byte[(int) fileSize];
        inputStream.read(file2);

        for(int i = 0; i < 9; i++)
        {
            for(int j = 0; j < 7; j++)
            {
                for(int k = 1; k < 16; k++)
                {
                    String pass = a.getPassword(i, j, k);
                    try {
                        byte[] out1 = a.decrypt(pass, file1);
                        byte[] out2 = a.decrypt(pass, file2);
                        OutputStream outputStream = new FileOutputStream("out1");
                        outputStream.write(out1);
                        outputStream = new FileOutputStream("out2");
                        outputStream.write(out2);
                        System.out.println("yep!");

                    } catch (javax.crypto.BadPaddingException ex) {
                    }
                }
            }
        }
    }

    public final byte[] decrypt(Object object, byte[] arrby) throws Exception  {
        Object object2 = Charset.forName("UTF-8");
        object2 = "pawsitive_vibes!".getBytes((Charset)object2);
        object2 = new IvParameterSpec((byte[])object2);
        object = ((String)object).toCharArray();
        Object object3 = Charset.forName("UTF-8");
        object3 = "NaClNaClNaCl".getBytes((Charset)object3);
        object = new PBEKeySpec((char[])object, (byte[])object3, 1234, 256);
        object = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1").generateSecret((KeySpec)object);
        object3 = new SecretKeySpec(((SecretKey)object).getEncoded(), "AES");
        object = Cipher.getInstance("AES/CBC/PKCS5Padding");
        ((Cipher)object).init(2, (Key)object3, (AlgorithmParameterSpec)object2);
        object = ((Cipher)object).doFinal(arrby);
        return (byte [])object;
    }

    public final String getPassword(int n, int n2, int n3) {
        String string2 = "*";
        String string3 = "*";
        switch (n % 9) {
            case 8: {
                string2 = "*";
                break;
            }
            case 7: {
                string2 = "&";
                break;
            }
            case 6: {
                string2 = "@";
                break;
            }
            case 5: {
                string2 = "#";
                break;
            }
            case 4: {
                string2 = "!";
                break;
            }
            case 3: {
                string2 = "+";
                break;
            }
            case 2: {
                string2 = "$";
                break;
            }
            case 1: {
                string2 = "-";
                break;
            }
            case 0: {
                string2 = "_";
            }
        }
        switch (n3 % 7) {
            case 6: {
                string3 = "@";
                break;
            }
            case 4: {
                string3 = "&";
                break;
            }
            case 3: {
                string3 = "#";
                break;
            }
            case 2: {
                string3 = "+";
                break;
            }
            case 1: {
                string3 = "_";
                break;
            }
            case 0: {
                string3 = "$";
            }
            case 5:
        }
        String string4 = String.join("", Collections.nCopies(n / n3, "flare"));
        String string5 = String.join("", Collections.nCopies(n2 * 2, this.rotN("bear", n * n2)));
        String string6 = String.join("", Collections.nCopies(n3, "yeah"));
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(string4);
        stringBuilder.append(string2);
        stringBuilder.append(string5);
        stringBuilder.append(string3);
        stringBuilder.append(string6);
        return stringBuilder.toString();
    }

    public final String rotN(String charSequence, int n) {
        Collection<String> collection = new ArrayList(charSequence.length());
        for (int i = 0; i < charSequence.length(); ++i) {
            char c;
            char c2 = c = charSequence.charAt(i);
            if (Character.isLowerCase(c)) {
                char c3;
                c2 = c3 = (char)(c + n);
                if (c3 > 'z') {
                    c2 = c3 = (char)(c3 - n * 2);
                }
            }
            collection.add(Character.valueOf(c2).toString());
        }
        return collection.stream().collect(Collectors.joining());
        // return ArraysKt.joinToString$default(CollectionsKt.toCharArray((List)collection), (CharSequence)FLARE_BEAR_NAME, null, null, 0, null, null, 62, null);
    }
}

Извлечем зашифрованные ресурсы, скомпилируем и запустим полученный файл:

$ ~/retools/apktool/apktool d flarebear.apk
$ cp flarebear/res/raw/* .
$ javac Main.java
$ java Main

К счастью, из всех пе́ребранных вариантов пароля подходит всего один. В результате мы получим два изображения с флагом:

~/flareon2019/3 - Flarebear$ file out*
out1: PNG image data, 2100 x 2310, 8-bit/color RGB, non-interlaced
out2: PNG image data, 2100 x 2310, 8-bit/color RGB, non-interlaced

Flare-On 2019 write-up - 4
Flare-On 2019 write-up - 5

0x04 — Dnschess

Some suspicious network traffic led us to this unauthorized chess program running on an Ubuntu desktop. This appears to be the work of cyberspace computer hackers. You'll need to make the right moves to solve this one. Good luck!

В данном таске дан дамп трафика, исполняемый ELF-файл ChessUI и библиотека ChessAI.so. Запустив исполняемый файл, можно увидеть шахматную доску.

Flare-On 2019 write-up - 6

Начнем анализ с дампа трафика.

Flare-On 2019 write-up - 7

Весь трафик состоит из запросов к DNS-серверу типа A. Сами запросы состоят из названий фигур, описания хода в шахматной партии и постоянной части .game-of-thrones.flare-on.com, например rook-c3-c6.game-of-thrones.flare-on.com. По постоянной части можно легко найти нужное место в библиотеке ChessAI.so:

signed __int64 __fastcall getNextMove(int idx, const char *chess_name, unsigned int pos_from, unsigned int pos_to, __int64 a5)
{
  struct hostent *v9; // [rsp+20h] [rbp-60h]
  char *ip_addr; // [rsp+28h] [rbp-58h]
  char dns_name; // [rsp+30h] [rbp-50h]
  unsigned __int64 v12; // [rsp+78h] [rbp-8h]

  v12 = __readfsqword(0x28u);
  strcpy(&dns_name, chess_name);
  pos_to_str(&dns_name, pos_from);
  pos_to_str(&dns_name, pos_to);
  strcat(&dns_name, ".game-of-thrones.flare-on.com");
  v9 = gethostbyname(&dns_name);
  if ( !v9 )
    return 2LL;
  ip_addr = *v9->h_addr_list;
  if ( *ip_addr != 127 || ip_addr[3] & 1 || idx != (ip_addr[2] & 0xF) )
    return 2LL;
  sleep(1u);
  flag[2 * idx] = ip_addr[1] ^ key[2 * idx];
  flag[2 * idx + 1] = ip_addr[1] ^ key[2 * idx + 1];
  *(_DWORD *)a5 = (unsigned __int8)ip_addr[2] >> 4;
  *(_DWORD *)(a5 + 4) = (unsigned __int8)ip_addr[3] >> 1;
  strcpy((char *)(a5 + 8), off_4120[idx]);
  return (unsigned __int8)ip_addr[3] >> 7;
}

Из кода видно, что на основе получаемых ip-адресов расшифровывается некоторая байтовая строка, сохраняемая в другой области памяти, которую я назвал flag.

Для решения таска первым делом получим все ip-адреса из дампа трафика. Сделать это можно с помощью следующей команды:

tshark -r capture.pcap | grep -P -o '127.(d+).(d+).(d+)' | grep -v '127.0.0.1'

Сохранив все ip-адреса в файл ips можно воспользоваться следующим кодом на Python для получения флага:

with open('ips') as f:
    ips = f.read().split()

flag = bytearray(64)
key = b'yZxb8xbcxecxd3xdfxddx99xa5xb6xacx156x85x8dtx08wRMqT}xa7xa7x08x16xfdxd7'
for ip in ips:
    a, b, c, d = map(int, ip.split('.'))
    if d & 1:
        continue
    idx = c & 0xf
    if idx > 14:
        continue
    flag[2*idx] = b ^ key[2*idx]
    flag[2*idx + 1] = b ^ key[2*idx + 1]
print(flag.decode() + '@flare-on.com')
# LooksLikeYouLockedUpTheLookupZ@flare-on.com

0x05 — demo

Someone on the Flare team tried to impress us with their demoscene skills. It seems blank. See if you can figure it out or maybe we will have to fire them. No pressure.

Дан исполняемый файл 4k.exe, который использует DirectX. При запуске в главном окне отображается вращающийся логотип FlareOn.

Flare-On 2019 write-up - 8

При статическом анализе программы обнаруживается единственная функция, которая и является точкой входа. По содержимому функция напоминает реализацию расшифровки кода. Не будем тратить время на анализ алгоритма работы данной функции, просто поставим брейкпоинт на инструкцию ret и посмотрим, куда передается управление. После возврата оказываемся по адресу 0x00420000, код по которому дизассемблируется как нечто адекватное:

Flare-On 2019 write-up - 9

Далее было решено перенести данный код из режима отладки в базу IDA с помощью API и продолжить статический анализ.

Новый код в начале импортирует необходимые функции из различных библиотек. Таблицу этих функций также можно восстановить в динамике. В результате получился следующий набор функций:

Flare-On 2019 write-up - 10

"Настоящая" точка входа в программу будет такой:

Flare-On 2019 write-up - 11

Обратите внимание на создание DeviceInterface типа IDirect3DDevice9 **. В дальнейшем данный интерфейс активно используется, и для упрощения реверса необходимо определить таблицу его методов. Найти определение интерфейса удалось достаточно быстро, например, вот тут. Я распарсил данную таблицу и преобразовал в структуру для IDA. Применив получившийся тип к DeviceInterface, можно значительно упростить дальнейший анализ кода. На следующих скриншотах представлен результат работы декомпилятора для основной функции цикла отрисовки сцены до и после применения типа.

Flare-On 2019 write-up - 12

Flare-On 2019 write-up - 13

При дальнейшем анализе было обнаружено, что в программе создаются две полигональные сетки (меш, polygon mesh), хотя при работе программы мы видим только один объект. Также при построении сеток их вершины зашифрованы с помощью XOR, что тоже вызывает подозрения. Давайте расшифруем и визуализируем вершины. Наибольший интерес представляет вторая сетка, т.к. в ней значительно больше вершин. Расшифровав все вершины, я обнаружил, что координата Z у каждой из них равна 0, поэтому для визуализации решено было рисовать двухмерные графики с помощью matplotlib. Получился следующий код и результат с флагом:

import struct
import matplotlib.pyplot as plt

with open('vertexes', 'rb') as f:
    data = f.read()

n = len(data) // 4
data = list(struct.unpack('{}I'.format(n), data))
key = [0xCB343C8, 0x867B81F0, 0x84AF72C3]
data = [data[i] ^ key[i % 3] for i in range(len(data))]
data = struct.pack('{}I'.format(n), *data)
data = list(struct.unpack('{}f'.format(n), data))

x = data[0::3]
y = data[1::3]
z = data[2::3]

print(z)

plt.plot(x, y)
plt.show()

Flare-On 2019 write-up - 14

0x06 — bmphide

Tyler Dean hiked up Mt. Elbert (Colorado's tallest mountain) at 2am to capture this picture at the perfect time. Never skip leg day. We found this picture and executable on a thumb drive he left at the trail head. Can he be trusted?

В таске дан исполняемый файл bmphide.exe и изображение image.bmp. Можно предположить, что в изображении с помощью методов стеганографии спрятано некоторое сообщение.

Бинарник написан на C#, поэтому для анализа я использовал утилиту dnSpy. Сразу можно заметить, что большинство названий методов обфусцированы. Если посмотреть на метод Program.Main, можно понять логику работы программы и сделать предположения о назначении некоторых из них:

// BMPHIDE.Program
// Token: 0x06000018 RID: 24 RVA: 0x00002C18 File Offset: 0x00002C18
private static void Main(string[] args)
{
    Program.Init();
    Program.yy += 18;
    string filename = args[2];
    string fullPath = Path.GetFullPath(args[0]);
    string fullPath2 = Path.GetFullPath(args[1]);
    byte[] data = File.ReadAllBytes(fullPath2);
    Bitmap bitmap = new Bitmap(fullPath);
    byte[] data2 = Program.h(data);
    Program.i(bitmap, data2);
    bitmap.Save(filename);
}

  • Происходит инициализация приложения с помощью метода Program.Init()
  • Считывается файл данных и файл изображения
  • С помощью метода byte [] Program.h(byte []) происходит некоторое преобразование считанных данных
  • С помощью метода Program.i(Bitmap, byte[]) происходит вставка преобразованных данных в изображение
  • Полученное изображение сохраняется с новым именем

При инициализации приложения вызываются различные методы класса A. Поверхностный анализ класса показал схожесть некоторых его методов с методами обфускатора ConfuserEx (файл AntiTamper.JIT.cs). Приложение действительно защищено от отладки. При этом снять защитные механизмы с помощью утилиты de4dot и её форков не удалось, поэтому было решено продолжить анализ.

Рассмотрим метод Program.i, который используется для вставки данных в изображение.

public static void i(Bitmap bm, byte[] data)
{
  int num = Program.j(103);
  for (int i = Program.j(103); i < bm.Width; i++)
  {
    for (int j = Program.j(103); j < bm.Height; j++)
    {
      bool flag = num > data.Length - Program.j(231);
      if (flag)
      {
        break;
      }
      Color pixel = bm.GetPixel(i, j);
      int red = ((int)pixel.R & Program.j(27)) | ((int)data[num] & Program.j(228));
      int green = ((int)pixel.G & Program.j(27)) | (data[num] >> Program.j(230) & Program.j(228));
      int blue = ((int)pixel.B & Program.j(25)) | (data[num] >> Program.j(100) & Program.j(230));
      Color color = Color.FromArgb(Program.j(103), red, green, blue);
      bm.SetPixel(i, j, color);
      num += Program.j(231);
    }
  }
}

Очень похоже на классический LSB, однако в местах, где ожидаются константы, используется метод int Program.j(byte). Результат его работы зависит от различных глобальных значений, получаемых, в том числе, при инициализации в методе Program.Init(). Было решено не реверсить его работу, а получить все возможные значения во время выполнения. dnSpy позволяет редактировать декомпилированный код приложения и сохранять измененные модули. Воспользуемся этим и перезапишем метод Program.Main следующим образом:

private static void Main(string[] args)
{
    Program.Init();
    Program.yy += 18;
    for (int i = 0; i < 256; i++)
    {
        Console.WriteLine(string.Format("j({0}) = {1}", i, Program.j((byte)i)));
    }
}

При запуске мы получим следующие значения:

E:>bmphide_j.exe
j(0) = 206
j(1) = 204
j(2) = 202
j(3) = 200
j(4) = 198
j(5) = 196
j(6) = 194
j(7) = 192
j(8) = 222
j(9) = 220
j(10) = 218
j(11) = 216
j(12) = 214
j(13) = 212
j(14) = 210
j(15) = 208
j(16) = 238
j(17) = 236
j(18) = 234
j(19) = 232
j(20) = 230
...

Заменим вызовы Program.j в методе Program.i на полученные константы:

public static void i(Bitmap bm, byte[] data)
{
  int num = 0;
  for (int i = 0; i < bm.Width; i++)
  {
    for (int j = 0; j < bm.Height; j++)
    {
      bool flag = num > data.Length - 1;
      if (flag)
      {
        break;
      }
      Color pixel = bm.GetPixel(i, j);
      int red = ((int)pixel.R & 0xf8) | ((int)data[num] & 0x7);
      int green = ((int)pixel.G & 0xf8) | (data[num] >> 3 & 0x7);
      int blue = ((int)pixel.B & 0xfc) | (data[num] >> 6 & 0x3);
      Color color = Color.FromArgb(0, red, green, blue);
      bm.SetPixel(i, j, color);
      num += 1;
    }
  }
}

Теперь становится понятен способ вставки каждого байта сообщения в изображение:

  • биты с 0 по 2 помещаются в 3 младших бита красного канала точки
  • биты с 3 по 5 помещаются в 3 младших бита зеленого канала точки
  • биты с 6 по 7 помещаются в 2 младших бита синего канала точки

Далее я пробовал повторить алгоритм метода преобразования данных, но результат вычислений не совпадал с выводом программы. Как оказалось, в классе A также имеется функционал для замены методов (в A.VerifySignature(MethodInfo m1, MethodInfo m2)) и модификации IL байт-кода методов (в A.IncrementMaxStack).

Для выбора методов, которые необходимо заменить в Program, в Program.Init происходит хеширование IL байт-кода всех методов и сравнение с заранее подсчитанными значениями. Всего подменяется два метода. Чтобы выяснить, какие именно, запустим приложение под отладчиком, поставив брейкпоинты на вызовы A.VerifySignature, при этом необходимо пропустить вызов A.CalculateStack() в Program.Init, т.к. он препятствует отладке.

Flare-On 2019 write-up - 15

В результате можно увидеть, что метод Program.a заменяется на Program.b, а Program.c — на Program.d.

Теперь необходимо разобраться с модификацией байт-кода:

private unsafe static uint IncrementMaxStack(IntPtr self, A.ICorJitInfo* comp, A.CORINFO_METHOD_INFO* info, uint flags, byte** nativeEntry, uint* nativeSizeOfCode)
{
    bool flag = info != null;
    if (flag)
    {
        MethodBase methodBase = A.c(info->ftn);
        bool flag2 = methodBase != null;
        if (flag2)
        {
            bool flag3 = methodBase.MetadataToken == 100663317;
            if (flag3)
            {
                uint flNewProtect;
                A.VirtualProtect((IntPtr)((void*)info->ILCode), info->ILCodeSize, 4u, out flNewProtect);
                Marshal.WriteByte((IntPtr)((void*)info->ILCode), 23, 20);
                Marshal.WriteByte((IntPtr)((void*)info->ILCode), 62, 20);
                A.VirtualProtect((IntPtr)((void*)info->ILCode), info->ILCodeSize, flNewProtect, out flNewProtect);
            }
            else
            {
                bool flag4 = methodBase.MetadataToken == 100663316;
                if (flag4)
                {
                    uint flNewProtect2;
                    A.VirtualProtect((IntPtr)((void*)info->ILCode), info->ILCodeSize, 4u, out flNewProtect2);
                    Marshal.WriteInt32((IntPtr)((void*)info->ILCode), 6, 309030853);
                    Marshal.WriteInt32((IntPtr)((void*)info->ILCode), 18, 209897853);
                    A.VirtualProtect((IntPtr)((void*)info->ILCode), info->ILCodeSize, flNewProtect2, out flNewProtect2);
                }
            }
        }
    }
    return A.originalDelegate(self, comp, info, flags, nativeEntry, nativeSizeOfCode);
}

Понятно, что модифицироваться будут методы с определенными значениями MetadataToken, а именно 0x6000015 и 0x6000014. Этим токенам соответствуют методы Program.h и Program.g. В dnSpy имеется встроенный hex-редактор, в котором при наведении подсвечиваются данные методов: их заголовок (выделен фиолетовым) и байт-код (выделен красным), как показано на скриншоте. Перейти к нужному методу в hex-редакторе можно нажав на соответствующий адрес в комментарии перед декомпилированным методом (например, File Offset: 0x00002924).

Flare-On 2019 write-up - 16

Попробуем применить все описанные модификации: создадим копию файла, в любом hex-редакторе изменим значения по нужным смещениям, которые мы узнали из dnSpy и сделаем замену методов a -> b и c -> d в Program.h. Также уберем из Program.Init все обращения к модулю A. Если всё сделано правильно, то при попытке вставить некоторое сообщение в картинку с помощью модифицированного приложения мы получим такой же результат, как и при работе оригинального приложения. На скриншотах ниже представлен декомпилированный код методов оригинального и модифицированного приложений.

Flare-On 2019 write-up - 17

Flare-On 2019 write-up - 18

Осталось создать алгоритм обратного преобразования. Он довольно простой, поэтому приведу только итоговый скрипт на Python:

from PIL import Image

# Rotate left: 0b1001 --> 0b0011
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)))

# Rotate right: 0b1001 --> 0b1100
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))

rol8 = lambda a, b: rol(a, b, 8)
ror8 = lambda a, b: ror(a, b, 8)

def extract(fname):
    img = Image.open(fname)
    w, h = img.size
    result = bytearray()
    for i in range(w):
        for j in range(h):
            r, g, b = img.getpixel((i, j))
            # print('{:02x} {:02x} {:02x}'.format(r, g, b))
            byte = (r & 0b111) | ((g & 0b111) << 3) | ((b & 0b11) << 6)
            result.append(byte)
    return result

enc = extract('image.bmp')
n = len(enc)
dec = bytearray()

def g(idx):
    b = ((idx + 1) * 309030853) & 0xff
    k = ((idx + 2) * 209897853) & 0xff
    return b ^ k

j = 0
for i in range(n):
    x = enc[i]
    x = rol8(x, 3)
    x ^= g(2*i + 1)
    x = ror8(x, 7)
    x ^= g(2*i + 0)
    dec.append(x)

with open('output', 'wb') as f:
    f.write(dec)

Запустив данный скрипт, мы получим еще одно bmp изображение без флага. Повторив процедуру на нем, получим итоговое изображение с флагом.

Flare-On 2019 write-up - 19

0x07 — wopr

We used our own computer hacking skills to "find" this AI on a military supercomputer. It does strongly resemble the classic 1983 movie WarGames. Perhaps life imitates art? If you can find the launch codes for us, we'll let you pass to the next challenge. We promise not to start a thermonuclear war.

В таске дано консольное приложение worp.exe. По всей видимости, для его решения необходимо подобрать некоторый код.

Flare-On 2019 write-up - 20

Анализ точки входа показывает, что это самораспаковывающийся архив. При запуске проверяется наличие переменной окружения _MEIPASS2. Если данной переменной нет, то создается временная директория, в которую распаковывается содержимое архива, и приложение запускается еще раз уже с заданной переменной окружения _MEIPASS2. Содержимое архива:

.
├── api-ms-win-core-console-l1-1-0.dll
├── ...
├── ...
├── api-ms-win-crt-utility-l1-1-0.dll
├── base_library.zip
├── _bz2.pyd
├── _ctypes.pyd
├── _hashlib.pyd
├── libcrypto-1_1.dll
├── libssl-1_1.dll
├── _lzma.pyd
├── pyexpat.pyd
├── python37.dll
├── select.pyd
├── _socket.pyd
├── _ssl.pyd
├── this
│   ├── __init__.py
│   └── key
├── ucrtbase.dll
├── unicodedata.pyd
├── VCRUNTIME140.dll
└── wopr.exe.manifest

1 directory, 56 files

Судя по содержимому, мы имеем дело с запакованным в exe приложением на языке Python. В подтверждение этому в основном бинарнике можно найти динамический импорт соответствующих функций библиотеки Python: PyMarshal_ReadObjectFromString, PyEval_EvalCode и другие. Для дальнейшего анализа необходимо извлечь Python байт-код. Для этого сохраним содержимое архива из временной директории и пропишем в переменную окружения _MEIPASS2 путь до нее. Запустим основной бинарник в режиме отладки, поставив брейкпоинт на функцию PyMarshal_ReadObjectFromString. Данная функция принимает в качестве аргументов указатель на буфер с сериализованным Python-кодом и его длину. Сдампим содержимое буфера известной длины при каждом из вызовов. У меня получилось всего 2 вызова, при этом во втором сериализованный объект значительно больше, его и будем анализировать.

Достаточно простым способом анализа полученных данных является приведение их к формату .pyc файлов (скомпилированный байт-код Python) и декомпиляция с помощью uncompyle6. Для этого достаточно к полученным данным дописать 16-байтовый заголовок. В итоге у меня получился следующий файл:

00000000: 42 0d 0d 0a 00 00 00 00 de cd 57 5d 00 00 00 00  B.........W]....
00000010: e3 00 00 00 00 00 00 00 00 00 00 00 00 09 00 00  ................
00000020: 00 40 00 00 00 73 3c 01 00 00 64 00 5a 00 64 01  .@...s<...d.Z.d.
00000030: 64 02 6c 01 5a 01 64 01 64 02 6c 02 5a 02 64 01  d.l.Z.d.d.l.Z.d.

Далее декомпилируем полученный файл с помощью uncompyle6:

uncompyle6 task.pyc > task.py

Если попробовать запустить декомпилированный файл, то мы получим исключение в строке BOUNCE = pkgutil.get_data('this', 'key'). Это легко исправить, просто назначив переменной BOUNCE содержимое файла key из архива. Повторно запустив скрипт, мы увидим только надпись LOADING.... По всей видимости, в таске используются какие-то техники, препятствующие декомпиляции. Приступим к анализу полученного Python-кода. В самом конце видим следующий цикл:

for i in range(256):
    try:
        print(lzma.decompress(fire(eye(__doc__.encode()), bytes([i]) + BOUNCE)))
    except Exception:
        pass

Можно понять, что функция print на самом деле переопределена как exec, а её аргумент зависит только от __doc__.encode() — текста в начале файла. В начале исполнения кода сохраним функцию print под другим именем и заменим ею print в блоке try-except. При запуске полученного скрипта нам снова ничего не выведется. Возможно, при декомпиляции __doc__ был записан неверно. Попробуем извлечь значение __doc__ напрямую из сериализованного кода следующим образом:

import marshal

with open('pycode1', 'rb') as inp:
    data = inp.read()
    code = marshal.loads(data)
    doc = code.co_consts[0]
    with open('doc.txt', 'w') as outp:
        outp.write(doc)

Исполним скрипт еще раз, заменив содержимое __doc__. В результате, при определенном значении i, код успешно выведется на экран. Сохраним его в новом файле и проанализируем. В функции wrong можно обнаружить следующую строку:

trust = windll.kernel32.GetModuleHandleW(None)

С помощью нее получается указатель на текущий модуль в памяти, и далее происходят некоторые проверки на основе его содержимого. Я решил просто сдампить первые 0x100000 байт модуля из памяти во время обычного исполнения и переписал функцию wrong, чтобы данные для проверки считывались из файла дампа. В результате у меня получилось добиться такого же поведения скрипта, как и при запуске бинарника.

Последней частью таска является решение некоторой линейной системы уравнений. Для этого воспользуемся z3:

from z3 import *
from stage2 import wrong

xor = [212, 162, 242, 218, 101, 109, 50, 31, 125, 112, 249, 83, 55, 187, 131, 206]
h = list(wrong())
h = [h[i] ^ xor[i] for i in range(16)]
b = 16 * [None]

x = []
for i in range(16):
    x.append(BitVec('x' + str(i), 32))

b[0] = x[2] ^ x[3] ^ x[4] ^ x[8] ^ x[11] ^ x[14]
b[1] = x[0] ^ x[1] ^ x[8] ^ x[11] ^ x[13] ^ x[14]
b[2] = x[0] ^ x[1] ^ x[2] ^ x[4] ^ x[5] ^ x[8] ^ x[9] ^ x[10] ^ x[13] ^ x[14] ^ x[15]
b[3] = x[5] ^ x[6] ^ x[8] ^ x[9] ^ x[10] ^ x[12] ^ x[15]
b[4] = x[1] ^ x[6] ^ x[7] ^ x[8] ^ x[12] ^ x[13] ^ x[14] ^ x[15]
b[5] = x[0] ^ x[4] ^ x[7] ^ x[8] ^ x[9] ^ x[10] ^ x[12] ^ x[13] ^ x[14] ^ x[15]
b[6] = x[1] ^ x[3] ^ x[7] ^ x[9] ^ x[10] ^ x[11] ^ x[12] ^ x[13] ^ x[15]
b[7] = x[0] ^ x[1] ^ x[2] ^ x[3] ^ x[4] ^ x[8] ^ x[10] ^ x[11] ^ x[14]
b[8] = x[1] ^ x[2] ^ x[3] ^ x[5] ^ x[9] ^ x[10] ^ x[11] ^ x[12]
b[9] = x[6] ^ x[7] ^ x[8] ^ x[10] ^ x[11] ^ x[12] ^ x[15]
b[10] = x[0] ^ x[3] ^ x[4] ^ x[7] ^ x[8] ^ x[10] ^ x[11] ^ x[12] ^ x[13] ^ x[14] ^ x[15]
b[11] = x[0] ^ x[2] ^ x[4] ^ x[6] ^ x[13]
b[12] = x[0] ^ x[3] ^ x[6] ^ x[7] ^ x[10] ^ x[12] ^ x[15]
b[13] = x[2] ^ x[3] ^ x[4] ^ x[5] ^ x[6] ^ x[7] ^ x[11] ^ x[12] ^ x[13] ^ x[14]
b[14] = x[1] ^ x[2] ^ x[3] ^ x[5] ^ x[7] ^ x[11] ^ x[13] ^ x[14] ^ x[15]
b[15] = x[1] ^ x[3] ^ x[5] ^ x[9] ^ x[10] ^ x[11] ^ x[13] ^ x[15]

solver = Solver()

for i in range(16):
    solver.add(x[i] < 128)

for i in range(16):
    solver.add(b[i] == h[i])

if solver.check() == sat:
    m = solver.model()
    print(bytes([m[i].as_long() for i in x]))
else:
    print('unsat')

Запустив данный скрипт, мы получим нужный код: 5C0G7TY2LWI2YXMB

Flare-On 2019 write-up - 21

0x08 — snake

The Flare team is attempting to pivot to full-time twitch streaming video games instead of reverse engineering computer software all day. We wrote our own classic NES game to stream content that nobody else has seen and watch those subscribers flow in. It turned out to be too hard for us to beat so we gave up. See if you can beat it and capture the internet points that we failed to collect.

В таске дан NES-образ игры. Для запуска я решил использовать эмулятор FCEUX, т.к. он имеет достаточно богатые возможности отладки. Запустим игру, открыв редактор памяти.

Flare-On 2019 write-up - 22

Немного поиграв, можно обнаружить, что значение по смещению 0x25 соответствует количеству съеденных яблок. В этом можно убедиться, попытавшись поменять его. Далее я решил загрузить NES-образ в IDA. Для этого можно воспользоваться загрузчиком inesldr. Посмотрим обращения к смещению 0x25. По адресу C82A происходит загрузка этого значения, которое затем увеличивается на единицу и записывается по тому же смещению. Далее происходит сравнение значения с 0x33.

Flare-On 2019 write-up - 23

Первое, что пришло в голову — установить значение 0x32 по смещению 0x25 и съесть одно яблоко на игровом поле. После этого игра началась сначала, но с увеличенной скоростью. К счастью, FCEUX позволяет настраивать скорость эмуляции. Повторив те же действия еще несколько раз был получен флаг.

Flare-On 2019 write-up - 24

0x09 — reloadered

This is a simple challenge, enter the password, receive the key. I hear that it caused problems when trying to analyze it with ghidra. Remember that valid flare-on flags will always end with @flare-on.com

В таске дан один файл reloaderd.exe, в который необходимо ввести ключ. На первый взгляд показалось, что решить его довольно просто, и это вызвало некоторые подозрения. Я разобрал алгоритм и выяснил, что под него может подходить множество ключей, и для каждого из них в ответе выводится XOR некоторой строки с ключом, и в конце добавляется @FLAG.com, что не соответствует формату флага.

Flare-On 2019 write-up - 25

В ходе дальнейшего анализа я обнаружил интересный фрагмент кода, заполненный операцией NOP. Но если посмотреть на это же место при запуске программы, поставив брейкпоинт на точку входа, можно увидеть код. Это было сделано с помощью определенным образом сформированной таблицы релокации. Сделаем снапшот отладчика, чтобы анализировать актуальный код программы. В ходе анализа выяснилось, что данный код в начале проверяет, запущено ли приложение на реальном аппаратном обеспечении. Если было определенно, что программа исполняется в виртуальной машине, код перезаписывается с помощью NOP, и управление передается на фейковый чекер.

Если же приложение исполняется на реальном аппаратном обеспечении, то на стеке формируется некоторый буфер, к содержимому которого применяется операция XOR с ключом, введенным пользователем. Если итоговая строка содержит подстроку @flare-on.com, то ключ считается правильным. В итоге я написал следующий код для подбора ключа и получения флага:

flag = bytearray(b'D)6n)x0fx05x1be&x10x04+h0/x003/x05x1ax1fx0f8x02x18Bx023x1a(x04*G?x04&dfMx107>(>wx1c?~64*x00')

for i in range(0x539):
    for j in range(0x34):
        if (i % 3) == 0 or (i % 7) == 0:
            flag[j] ^= (i & 0xff)

end = b'@flare-on.com'

def xor(a, b):
    return bytes([i^j for i, j in zip(a, b)])

for i in range(len(flag)):
    print(i, xor(end, flag[i:]))

print(xor(flag, b'3HeadedMonkey'*4))

Flare-On 2019 write-up - 26

0x0A — Mugatu

Hello,

I’m working an incident response case for Derek Zoolander. He clicked a link and was infected with MugatuWare! As a result, his new headshot compilation GIF was encrypted.

To secure an upcoming runway show, Derek needs this GIF decrypted; however, he refuses to pay the ransom.

We received an additional encrypted GIF from an anonymous informant. The informant told us the GIF should help in our decryption efforts, but we were unable to figure it out.

We’re reaching out to you, our best malware analyst, in hopes that you can reverse engineer this malware and decrypt Derek’s GIF.

I've included a directory full of files containing:

  • MugatuWare malware
  • Ransom note (GIFtToDerek.txt)
  • Encrypted headshot GIF (best.gif.Mugatu)
  • Encrypted informant GIF (the_key_to_success_0000.gif.Mugatu)

Thanks,
Roy

В таске даны следующие файлы:

  • best.gif.Mugatu
  • GIFtToDerek.txt
  • Mugatuware.exe
  • the_key_to_success_0000.gif.Mugatu

Судя по описанию, нам дан вредоносный файл, который шифрует GIF-изображения. Вероятно, к зашифрованным файлам добавляется расширение .Mugatu. Я начал анализ с файла Mugatuware.exe. Первое, что бросилось в глаза — странное использование импортируемых функций и несоответствие количества передаваемых в них аргументов. При запуске отладчика выяснилось, что функции действительно загружаются не так, как мы ожидаем.

Flare-On 2019 write-up - 27

Данную проблему можно решить следующим скриптом для IDA, запустив его в режиме отладки:

import ida_segment
import ida_name
import ida_bytes
import ida_typeinf

idata = ida_segment.get_segm_by_name('.idata')

type_map = {}

for addr in range(idata.start_ea, idata.end_ea, 4):
    name = ida_name.get_name(addr)
    if name:
        tp = ida_typeinf.idc_get_type(addr)
        if tp:
            type_map[name] = tp

for addr in range(idata.start_ea, idata.end_ea, 4):
    imp = ida_bytes.get_dword(addr)
    if imp != 0:
        imp_name = ida_name.get_name(imp)
        name_part = imp_name.split('_')[-1]
        ida_name.set_name(addr, name_part + '_imp')
        if name_part in type_map:
            tp = type_map[name_part]
            ida_typeinf.apply_decl(addr, tp.replace('(', 'func(') + ';')

После применения скрипта код основной функции приобретает смысл:

Flare-On 2019 write-up - 28

Дальнейший анализ показал, что одна из функций загружает данные из ресурсов, которые затем используются для in-memory загрузки PE-файла. После этого в отдельном потоке запускается одна из функций загруженного файла, и в качестве аргумента ей передается строка CrazyPills!!!. Запустим приложение в режиме отладки, поставив брейкпоинт на создание нового потока. При этом необходимо обойти цикл с Sleep, внутри которого происходят попытки выполнить http-запрос. Дойдя до создания потока, перейдем по адресу вызываемой функции, пометим его и сделаем снапшот памяти, чтобы продолжить анализ этого кода уже без отладки. Последующий анализ показал, что в этом коде для вызова библиотечных функций используются обертки, инвертирующие адрес вызываемой функции, как показано на рисунке ниже. Это незначительно усложняет анализ.

Flare-On 2019 write-up - 29

После реверс-инжиниринга кода и восстановления структур удалось понять примерный алгоритм работы:

  • Основной поток обращается к серверу и получает ключ шифрования;
  • Запускается поток шифрования;
  • Поток шифрования получает ключ из главного потока с помощью механизма Mailslots;
  • На дисковых устройства производится рекурсивный поиск поддиректории really, really, really, ridiculously good looking gifs;
  • В найденной директории шифруются все файлы с расширением .gif. К зашифрованным файлам добавляется расширение .Mugatu. Также в директории создается файл GIFtToDerek.txt с сообщением пользователю.

Шифрование блочное, длина блока — 8 байт. Сам указатель на функцию шифрования блока зашифрован с помощью XOR с двумя байтами строки CrazyPills!!!, переданной ранее в функцию потока в качестве аргумента. Расшифровав указатель, получаем адрес функции шифрования блока и саму функцию:

Flare-On 2019 write-up - 30

Функция похожа на реализацию XTEA, однако имеется ошибка — ключ интерпретируется как массив BYTE, а не массив DWORD. Это сильно сокращает множество возможных ключей и позволяет произвести атаку перебором. Далее я реализовал функцию шифрования и дешифрования на Python:

def crypt(a, b, key):
    i = 0
    for _ in range(32):
        t = (i + key[i & 3]) & 0xffffffff
        a = (a + (t ^ (b + ((b >> 5) ^ (b << 4))))) & 0xffffffff

        i = (0x100000000 + i - 0x61C88647) & 0xffffffff

        t = (i + key[(i >> 11) & 3]) & 0xffffffff
        b = (b + (t ^ (a + ((a >> 5) ^ (a << 4))))) & 0xffffffff
    return a, b

def decrypt(a, b, key):
    i = 0xc6ef3720
    for _ in range(32):
        t = (i + key[(i >> 11) & 3]) & 0xffffffff
        b = (0x100000000 + b - (t ^ (a + ((a >> 5) ^ (a << 4))))) & 0xffffffff

        i = (i + 0x61C88647) & 0xffffffff

        t = (i + key[i & 3]) & 0xffffffff
        a = (0x100000000 + a - (t ^ (b + ((b >> 5) ^ (b << 4))))) & 0xffffffff
    return a, b

Как оказалось, файл the_key_to_success_0000.gif.Mugatu необходим для проверки реализации алгоритма. Для его шифрования использовался ключ из нулевых байтов, что можно понять по названию. Дешифрованный файл выглядит следующим образом:

Flare-On 2019 write-up - 31

Можно заметить, что на изображении есть подсказка о реальном ключе, но я не сразу обратил на это внимание. Для перебора ключа алгоритм был переписан на C. При дешифровке проверяется заголовок GIF-изображения.

#include <stdio.h>
#include <unistd.h>

void decrypt(unsigned int * inp, unsigned int * outp, unsigned char * key) {

    unsigned int i = 0xc6ef3720;

    unsigned int a = inp[0];
    unsigned int b = inp[1];
    unsigned int t;

    for(int j = 0; j < 32; j++)
    {
        t = i + key[(i >> 11) & 3];
        b -= t ^ (a + ((a >> 5) ^ (a << 4)));

        i += 0x61C88647;

        t = i + key[i & 3];
        a -= t ^ (b + ((b >> 5) ^ (b << 4)));
    }

    outp[0] = a;
    outp[1] = b;
}

int main() {
    int fd = open("best.gif.Mugatu", 0);
    unsigned int inp[2];
    unsigned int outp[2];
    unsigned int key = 0;
    read(fd, inp, 8);
    close(fd);

    for(unsigned long long key = 0; key < 0x100000000; key++)
    {
        if((key & 0xffffff) == 0) {
            printf("%lfn", ((double)key) / ((double)0x100000000) * 100.0);
        }
        decrypt(inp, outp, &key);
        if( ((char *)outp)[0] == 'G' &&
            ((char *)outp)[1] == 'I' &&
            ((char *)outp)[2] == 'F' &&
            ((char *)outp)[5] == 'a')
        {
            printf("%#llxn", key);
        }
    }
}

В результате перебора было получено значение ключа 0xb1357331 и был расшифрован файл с флагом:

Flare-On 2019 write-up - 32

0x0B — vv_max

Hey, at least its not subleq.

В таске дан бинарник vv_max.exe, принимающий две строки в качестве аргументов. Он представляет из себя реализацию виртуальной машины с набором 256-битных регистров и операций над ними. Операции производятся с помощью инструкций расширения AVX2 процессора, таких как vpermd, vpslld и других. В результате дизассемблирования байт-кода виртуальной машины получилось следующее:

Код виртуальной машины

0000    clear_regs
0001    r0 = 393130324552414c46
0023    r1 = 3030303030303030303030303030303030303030303030303030303030303030
0045    r3 = 1a1b1b1b1a13111111111111111111151a1b1b1b1a1311111111111111111115
0067    r4 = 1010101010101010080408040201101010101010101010100804080402011010
0089    r5 = b9b9bfbf041310000000000000000000b9b9bfbf04131000
00ab    r6 = 2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f
00cd    r10 = 140014001400140014001400140014001400140014001400140014001400140
00ef    r11 = 1100000011000000110000001100000011000000110000001100000011000
0111    r12 = ffffffff0c0d0e08090a040506000102ffffffff0c0d0e08090a040506000102
0133    r13 = ffffffffffffffff000000060000000500000004000000020000000100000000
0155    r16 = ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
0177    r17 = 6a09e667bb67ae853c6ef372a54ff53a510e527f9b05688c1f83d9ab5be0cd19
0199    r18 = 428a2f9871374491b5c0fbcfe9b5dba53956c25b59f111f1923f82a4ab1c5ed5
01bb    r19 = 300000002000000010000000000000007000000060000000500000004
01dd    r20 = 0
01ff    r21 = 100000001000000010000000100000001000000010000000100000001
0221    r22 = 200000002000000020000000200000002000000020000000200000002
0243    r23 = 300000003000000030000000300000003000000030000000300000003
0265    r24 = 400000004000000040000000400000004000000040000000400000004
0287    r25 = 500000005000000050000000500000005000000050000000500000005
02a9    r26 = 600000006000000060000000600000006000000060000000600000006
02cb    r27 = 700000007000000070000000700000007000000070000000700000007
02ed    r20 = vpermd(r0, r20)
02f1    r21 = vpermd(r0, r21)
02f5    r22 = vpermd(r0, r22)
02f9    r23 = vpermd(r0, r23)
02fd    r24 = vpermd(r0, r24)
0301    r25 = vpermd(r0, r25)
0305    r26 = vpermd(r0, r26)
0309    r27 = vpermd(r0, r27)
030d    r7 = vpsrld(r1, 4)
0311    r28 = r20 ^ r21
0315    r28 = r28 ^ r22
0319    r28 = r28 ^ r23
031d    r28 = r28 ^ r24
0321    r28 = r28 ^ r25
0325    r28 = r28 ^ r26
0329    r28 = r28 ^ r27
032d    r7 = r7 & r6
0331    r29 = vpslld(r17, 7)
0335    r30 = vpsrld(r17, 25)
0339    r15 = r29 | r30
033d    r8 = vpcmpeqb(r1, r6)
0341    r29 = vpslld(r17, 21)
0345    r30 = vpsrld(r17, 11)
0349    r29 = r29 | r30
034d    r15 = r15 ^ r29
0351    r8 = vpcmpeqb(r1, r6)
0355    r29 = vpslld(r17, 26)
0359    r30 = vpsrld(r17, 6)
035d    r29 = r29 | r30
0361    r15 = r15 ^ r29
0365    r29 = r20 ^ r16
0369    r30 = r20 & r18
036d    r29 = r29 ^ r30
0371    r15 = add_d(r29, r15)
0375    r20 = add_d(r15, r0)
0379    r7 = add_b(r8, r7)
037d    r29 = r20 ^ r28
0381    r17 = vpermd(r29, r19)
0385    r7 = vpshufb(r5, r7)
0389    r29 = vpslld(r17, 7)
038d    r30 = vpsrld(r17, 25)
0391    r15 = r29 | r30
0395    r29 = vpslld(r17, 21)
0399    r30 = vpsrld(r17, 11)
039d    r29 = r29 | r30
03a1    r15 = r15 ^ r29
03a5    r29 = vpslld(r17, 26)
03a9    r30 = vpsrld(r17, 6)
03ad    r29 = r29 | r30
03b1    r15 = r15 ^ r29
03b5    r2 = add_b(r1, r7)
03b9    r29 = r21 ^ r16
03bd    r30 = r21 & r18
03c1    r29 = r29 ^ r30
03c5    r15 = add_d(r29, r15)
03c9    r21 = add_d(r15, r0)
03cd    r29 = r21 ^ r28
03d1    r17 = vpermd(r29, r19)
03d5    r20 = r20 ^ r21
03d9    r29 = vpslld(r17, 7)
03dd    r30 = vpsrld(r17, 25)
03e1    r15 = r29 | r30
03e5    r29 = vpslld(r17, 21)
03e9    r30 = vpsrld(r17, 11)
03ed    r29 = r29 | r30
03f1    r15 = r15 ^ r29
03f5    r29 = vpslld(r17, 26)
03f9    r30 = vpsrld(r17, 6)
03fd    r29 = r29 | r30
0401    r15 = r15 ^ r29
0405    r7 = vpmaddubsw(r2, r10)
0409    r29 = r22 ^ r16
040d    r30 = r22 & r18
0411    r29 = r29 ^ r30
0415    r15 = add_d(r29, r15)
0419    r22 = add_d(r15, r0)
041d    r29 = r22 ^ r28
0421    r17 = vpermd(r29, r19)
0425    r20 = r20 ^ r22
0429    r29 = vpslld(r17, 7)
042d    r30 = vpsrld(r17, 25)
0431    r15 = r29 | r30
0435    r29 = vpslld(r17, 21)
0439    r30 = vpsrld(r17, 11)
043d    r29 = r29 | r30
0441    r15 = r15 ^ r29
0445    r29 = vpslld(r17, 26)
0449    r30 = vpsrld(r17, 6)
044d    r29 = r29 | r30
0451    r15 = r15 ^ r29
0455    r2 = vpmaddwd(r7, r11)
0459    r29 = r23 ^ r16
045d    r30 = r23 & r18
0461    r29 = r29 ^ r30
0465    r15 = add_d(r29, r15)
0469    r23 = add_d(r15, r0)
046d    r29 = r23 ^ r28
0471    r17 = vpermd(r29, r19)
0475    r20 = r20 ^ r23
0479    r29 = vpslld(r17, 7)
047d    r30 = vpsrld(r17, 25)
0481    r15 = r29 | r30
0485    r29 = vpslld(r17, 21)
0489    r30 = vpsrld(r17, 11)
048d    r29 = r29 | r30
0491    r15 = r15 ^ r29
0495    r29 = vpslld(r17, 26)
0499    r30 = vpsrld(r17, 6)
049d    r29 = r29 | r30
04a1    r15 = r15 ^ r29
04a5    r29 = r24 ^ r16
04a9    r30 = r24 & r18
04ad    r29 = r29 ^ r30
04b1    r15 = add_d(r29, r15)
04b5    r24 = add_d(r15, r0)
04b9    r29 = r24 ^ r28
04bd    r17 = vpermd(r29, r19)
04c1    r20 = r20 ^ r24
04c5    r29 = vpslld(r17, 7)
04c9    r30 = vpsrld(r17, 25)
04cd    r15 = r29 | r30
04d1    r29 = vpslld(r17, 21)
04d5    r30 = vpsrld(r17, 11)
04d9    r29 = r29 | r30
04dd    r15 = r15 ^ r29
04e1    r29 = vpslld(r17, 26)
04e5    r30 = vpsrld(r17, 6)
04e9    r29 = r29 | r30
04ed    r15 = r15 ^ r29
04f1    r29 = r25 ^ r16
04f5    r30 = r25 & r18
04f9    r29 = r29 ^ r30
04fd    r15 = add_d(r29, r15)
0501    r25 = add_d(r15, r0)
0505    r29 = r25 ^ r28
0509    r17 = vpermd(r29, r19)
050d    r20 = r20 ^ r25
0511    r2 = vpshufb(r2, r12)
0515    r29 = vpslld(r17, 7)
0519    r30 = vpsrld(r17, 25)
051d    r15 = r29 | r30
0521    r29 = vpslld(r17, 21)
0525    r30 = vpsrld(r17, 11)
0529    r29 = r29 | r30
052d    r15 = r15 ^ r29
0531    r29 = vpslld(r17, 26)
0535    r30 = vpsrld(r17, 6)
0539    r29 = r29 | r30
053d    r15 = r15 ^ r29
0541    r29 = r26 ^ r16
0545    r30 = r26 & r18
0549    r29 = r29 ^ r30
054d    r15 = add_d(r29, r15)
0551    r26 = add_d(r15, r0)
0555    r29 = r26 ^ r28
0559    r17 = vpermd(r29, r19)
055d    r20 = r20 ^ r26
0561    r29 = vpslld(r17, 7)
0565    r30 = vpsrld(r17, 25)
0569    r15 = r29 | r30
056d    r29 = vpslld(r17, 21)
0571    r30 = vpsrld(r17, 11)
0575    r29 = r29 | r30
0579    r15 = r15 ^ r29
057d    r29 = vpslld(r17, 26)
0581    r30 = vpsrld(r17, 6)
0585    r29 = r29 | r30
0589    r15 = r15 ^ r29
058d    r2 = vpermd(r2, r13)
0591    r29 = r27 ^ r16
0595    r30 = r27 & r18
0599    r29 = r29 ^ r30
059d    r15 = add_d(r29, r15)
05a1    r27 = add_d(r15, r0)
05a5    r29 = r27 ^ r28
05a9    r17 = vpermd(r29, r19)
05ad    r20 = r20 ^ r27
05b1    r19 = ffffffffffffffffffffffffffffffffffffffffffffffff
05d3    r20 = r20 & r19
05d7    r31 = 2176620c3a5c0f290b583618734f07102e332623780e59150c05172d4b1b1e22

После завершения работы виртуальной машины значение в нулевом регистре сравнивается со строкой FLARE2019. Изначально это значение устанавливается равным первому аргументу программы, и из кода выше видно, что оно не меняется. Таким образом, первый аргумент программы должен быть FLARE2019. Также происходит сравнение r2 и r20. В ходе динамического анализа выяснилось, что значение r20 не зависит от второго аргумента программы. Влияние второго аргумента на r2 линейное — каждый байт входа влияет на 6 бит r2. Я решил просто перебирать каждый символ входных данных до тех пор, пока очередные 6 бит выхода не совпадут с нужным значением. Для автоматизации я использовал Frida:

# vvmax.py
from __future__ import print_function
import frida
import string
import hexdump

def check(val):
    global gdata
    with open('vvmax.js', 'r') as f:
        script_src = f.read()

    pid = frida.spawn(['vv_max.exe', 'FLARE2019', val.ljust(32, 'a')])

    session = frida.attach(pid)
    script = session.create_script(script_src)

    def handler(message, data):
        handler.data = data

    script.on('message', handler)

    script.load()

    frida.resume(pid)

    while not hasattr(handler, 'data'):
        pass

    session.detach()
    return handler.data

alph = string.printable

def to_bits(x):
    return ''.join(bin(ord(i))[2:].zfill(8) for i in x)

target = to_bits('ppxb2xacx01xd2^anxa7*xa8x08x1cx86x1axe8Exc8)xb2xf3xa1x1ex00x00x00x00x00x00x00x00')
password = ''

while len(password) != 32:
    for c in alph:
        data = to_bits(check(password + c))
        i = 6*len(password + c)
        if data[:i] == target[:i]:
            password += c
            i += 1
            break
    print()
    print('----->', `password`)
    print()

// vvmax.js
var modules = Process.enumerateModules();
var base = modules[0].base;

Interceptor.attach(base.add(0x1665), function() {
    var p = this.context.rdx.add(0x840);
    var res = p.readByteArray(32);
    send(null, res);
});

С помощью данного способа и был получен флаг:

Flare-On 2019 write-up - 33

0x0C — help

You're my only hope FLARE-On player! One of our developers was hacked and we're not sure what they took. We managed to set up a packet capture on the network once we found out but they were definitely already on the system. I think whatever they installed must be buggy — it looks like they crashed our developer box. We saved off the dump file but I can't make heads or tails of it — PLEASE HELP!!!!!!

Вот мы и дошли до последнего таска. На этот раз нам дан RAM-дамп и дамп сетевого трафика. В трафике можно обнаружить интересные порты 4444, 6666, 7777 и 8888. Трафик в них, судя по всему, зашифрован, поэтому перейдем к RAM-дампу. Для анализа можно использовать утилиту volatility. При попытке определить профиль для работы с дампом встроенные средства volatility предложили мне Win10x64_15063, однако уже потом выяснилось, что правильнее было использовать Win7SP1x64, хотя это особо не повлияло на решение таска.

В ходе различных экспериментов в volatility в памяти были обнаружены интересные модули ядра:

$ volatility --profile Win7SP1x64 -f help.dmp modules
Volatility Foundation Volatility Framework 2.6
Offset(V)          Name                 Base                             Size File
------------------ -------------------- ------------------ ------------------ ----
0xfffffa800183e890 ntoskrnl.exe         0xfffff80002a49000           0x5e7000 SystemRootsystem32ntoskrnl.exe

...
0xfffffa800428ff30 man.sys              0xfffff880033bc000             0xf000 ??C:UsersFLARE ON 2019Desktopman.sys

Извлечь данный модуль не удалось:

$ volatility --profile Win7SP1x64 -f help.dmp moddump --base 0xfffff880033bc000  -D drivers
Volatility Foundation Volatility Framework 2.6
Module Base        Module Name          Result
------------------ -------------------- ------
0xfffff880033bc000 man.sys              Error: e_magic 0000 is not a valid DOS signature.

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

$ volatility --profile Win7SP1x64 -f help.dmp volshell

In [1]: db(0xfffff880033bc000)
0xfffff880033bc000  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
0xfffff880033bc010  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
0xfffff880033bc020  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
0xfffff880033bc030  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
0xfffff880033bc040  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
0xfffff880033bc050  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
0xfffff880033bc060  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
0xfffff880033bc070  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................

In [2]: db(0xfffff880033bc000 + 0x1100)
0xfffff880033bd100  01 48 8b 4c 24 20 48 8b 44 24 28 48 89 41 08 48   .H.L$.H.D$(H.A.H
0xfffff880033bd110  83 c4 18 c3 cc cc cc cc cc cc cc cc cc cc cc cc   ................
0xfffff880033bd120  48 89 4c 24 08 48 83 ec 38 48 8b 44 24 40 0f be   H.L$.H..8H.D$@..
0xfffff880033bd130  48 43 48 8b 44 24 40 0f be 40 42 83 c0 01 3b c8   HCH.D$@..@B...;.
0xfffff880033bd140  7e 27 45 33 c9 41 b8 15 5b 00 00 48 8d 15 de 44   ~'E3.A..[..H...D
0xfffff880033bd150  00 00 48 8d 0d 07 45 00 00 ff 15 71 4f 00 00 c7   ..H...E....qO...
0xfffff880033bd160  44 24 20 00 00 00 00 eb 08 c7 44 24 20 01 00 00   D$........D$....
0xfffff880033bd170  00 48 8b 44 24 40 48 8b 80 b8 00 00 00 48 83 c4   .H.D$@H......H..

In [4]: man = addrspace().read(0xfffff880033bc000, 0xf000)

In [5]: with open('man_writeup.sys', 'wb') as f:
   ...:     f.write(man)
   ...:

Видно, что заголовок модуля стерт, поэтому функция moddump не сработала. Проанализируем модуль вручную. Идентифицировать методы модуля оказалось довольно просто. Процесс реверс-инженеринга модуля описывать не буду, опишу лишь результат, который был получен после длительного анализа.

В модуле активно используется шифрование строк на стеке с помощью RC4 со случайным ключом. Для упрощения преобразования подобных строк в начале был написан скрипт, который принимает строки из декомпилятора и расшифровывает сообщения.

Сам модуль представляет из себя нечто вроде прокси между зараженными процессами в user-space. Процессы заражаются инъектированием в них DLL-модулей с полезной нагрузкой. При инициализации один из процессов заражается встроенным в драйвер DLL-модулем (m.dll), отвечающим за принятие сообщений. Через него в дальнейшем могут быть загружены новые модули, информация о которых хранится в памяти драйвера в виде связного списка структур с необходимыми для взаимодействия данными. Для извлечения модулей из памяти нам интересны следующие поля структуры:

  • Указатель на следующий элемент списка (смещение +0x8)
  • Адрес структуры _EPROCESS зараженного процесса (смещение +0x68)
  • Базовый адрес инъектированной библиотеки (смещение +0x48)
  • Размер библиотеки (смещение +0x58)

Инъектированные DLL-модули также не имеют заголовка и могут быть зашифрованы с помощью RC4, в качестве ключа при этом используются 0x2c-байт описанной выше структуры, начиная со смещения 0x48.

Для извлечения всех модулей в volatility можно воспользоваться следующим скриптом для volshell:

import struct
from Crypto.Cipher import ARC4

head = 0xfffff880033c8158
krnl = addrspace()

def u64(x):
    return struct.unpack('Q', x)[0]

fd = u64(krnl.read(head, 8))
while True:
    proc_addr = u64(krnl.read(fd + 0x68, 8))
    base = u64(krnl.read(fd + 0x48, 8))
    key = krnl.read(fd + 0x48, 0x2c)
    sz = u64(krnl.read(fd + 0x58, 8))
    fd = u64(krnl.read(fd, 8))

    p = obj.Object('_EPROCESS', proc_addr, krnl)
    print p.ImageFileName.v(), hex(proc_addr), hex(base), hex(sz)

    proc_space = p.get_process_address_space()
    dump = proc_space.read(base, sz)
    if dump[:0x100] == 'x00' * 0x100:
        dump = ARC4.new(key).decrypt(dump)
    with open('proc_{:016x}'.format(base), 'wb') as f:
        f.write(dump)

    if fd == head:
        break

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

Скрипт для IDA

from __future__ import print_function
import sys
import re
from idaapi import get_func, decompile, get_name_ea, auto_wait, BADADDR
from idaapi import cot_call, cot_obj, init_hexrays_plugin, qexit
import ida_typeinf
import ida_lines

def rc4(key, data):
    S = list(range(256))
    j = 0
    for i in list(range(256)):
        j = (j + S[i] + ord(key[i % len(key)])) % 256
        S[i], S[j] = S[j], S[i]
    j = 0
    y = 0
    out = []
    for char in data:
        j = (j + 1) % 256
        y = (y + S[j]) % 256
        S[j], S[y] = S[y], S[j]
        out.append(chr(ord(char) ^ S[(S[j] + S[y]) % 256]))
    return ''.join(out)

def decrypt_stack_str_args(ea):

    func = get_func(ea)
    if func is None:
        return

    try:
        c_func = decompile(func)
        c_func.pseudocode
    except Exception as ex:
        return

    for citem in c_func.treeitems:
        citem = citem.to_specific_type

        if citem.is_expr() and
                citem.op == cot_call and
                citem.ea == ea:

            args = []

            key = citem.a[0]
            key_len = citem.a[1]
            s = citem.a[2]
            s_len = citem.a[3]

            def get_var_idx(obj):
                while obj.opname != 'var':
                    if obj.opname in ('ref', 'cast'):
                        obj = obj.x
                    else:
                        raise Exception('can't find type')
                return obj.v.idx

            if key_len.opname != 'num' or s_len.opname != 'num':
                print('[!] can't get length: 0x{:08x}'.format(ea))
            else:
                try:
                    key_len_val = key_len.n._value
                    s_len_val = s_len.n._value
                    print('0x{:08x}'.format(ea),
                        'key_len =', key_len_val,
                        ', s_len =', s_len_val)

                    hx_view = idaapi.open_pseudocode(ea, -1)

                    key_var_stkoff = hx_view.cfunc.get_lvars()[get_var_idx(key)].location.stkoff()
                    s_var_stkoff = hx_view.cfunc.get_lvars()[get_var_idx(s)].location.stkoff()

                    key_var = [v for v in hx_view.cfunc.get_lvars() if v.location.stkoff() == key_var_stkoff][0]
                    tif = ida_typeinf.tinfo_t()
                    ida_typeinf.parse_decl(tif, None,
                        'unsigned __int8 [{}];'.format(key_len_val), 0)
                    hx_view.set_lvar_type(key_var, tif)

                    s_var = [v for v in hx_view.cfunc.get_lvars() if v.location.stkoff() == s_var_stkoff][0]
                    tif = ida_typeinf.tinfo_t()
                    ida_typeinf.parse_decl(tif, None,
                        'unsigned __int8 [{}];'.format(s_len_val + 1), 0)
                    hx_view.set_lvar_type(s_var, tif)

                    key_var = [v for v in hx_view.cfunc.get_lvars() if v.location.stkoff() == key_var_stkoff][0]
                    s_var = [v for v in hx_view.cfunc.get_lvars() if v.location.stkoff() == s_var_stkoff][0]
                    key_regex = re.compile('{}[(.+)] = (.+);'.format(key_var.name))
                    s_regex = re.compile('{}[(.+)] = (.+);'.format(s_var.name))
                    key = bytearray(key_len_val)
                    s = bytearray(s_len_val + 1)
                    src = 'n'.join([ida_lines.tag_remove(i.line) for i in hx_view.cfunc.pseudocode])
                    for i, j in s_regex.findall(src):
                        s[int(i)] = (0x100 + int(j)) & 0xff
                    for i, j in key_regex.findall(src):
                        key[int(i)] = (0x100 + int(j)) & 0xff
                    key = ''.join(chr(i) for i in key)
                    s = ''.join(chr(i) for i in s)
                    result = rc4(key, s[:-1])
                    # unicode to ascii
                    if set(ord(i) for i in result[1::2]) == {0}:
                        result = 'wide_' + ''.join(result[0::2])
                    hx_view.rename_lvar(s_var, 's_' + result, True)

                except Exception as ex:
                    print('[!] error: {}'.format(ex))

print('#### decryption helper script ####')
xref_to = get_name_ea(BADADDR, 'decrypt_stack_str')
xref_from = get_first_cref_to(xref_to)
while xref_from != BADADDR:
    print('### 0x{:08x}'.format(xref_from))
    decrypt_stack_str_args(xref_from)
    xref_from = get_next_cref_to(xref_to, xref_from)

Вот результат работы скрипта:

Flare-On 2019 write-up - 34

Далее был произведен анализ полученных модулей. Удалось определить следующую функциональность каждого из них:

  • m.dll — модуль, полученный из тела драйвера. Слушает порт 4444 на зараженной машине. Основная задача — прием команд и перенаправление их в основной драйвер для последующего выполнения;
  • n.dll — модуль отвечает за отправку различных данных на хост 192.168.1.243;
  • c.dll — модуль отвечает за сжатие и шифрование данных с помощью RC4. В качестве ключа шифрования используется имя пользователя;
  • k.dll — модуль отвечает за логирование нажатий клавиш и получение текста из окон (keylogger);
  • s.dll — модуль отвечает за создание скриншотов рабочего стола;
  • f.dll — модуль отвечает за взаимодействие с файловой системой.

При анализе трафика также было обнаружено, что пакеты дополнительно зашифрованы с помощью XOR с длиной ключа 8. В случае входящих на порт 4444 пакетов с командами ключ легко удалось узнать, т.к. в исходном пакете было множество нулевых байтов. В отправляемых пакетах все оказалось еще проще: после недолгого анализа выяснилось, что данные в пакете отправляются дважды — в зашифрованном и в открытом виде. Видимо, это произошло из-за ошибки в реализации.

Во входящем трафике (порт 4444) был обнаружен еще один драйвер. Предположительно, он необходим для исполнения шел-кода в пространстве ядра. Для решения таска данный модуль не понадобился, поэтому его анализ проводить не буду. Также среди строк входящих пакетов были обнаружены следующие пути и названия файлов:

  • keys.kdb
  • C:
  • C:keypasskeys.kdb

Предположительно, пакеты с этими строками предназначены для модуля f.dll: сначала происходит запрос на поиск файла keys.kdb, а затем запрос на его загрузку.

В трафике для порта 6666 было обнаружено два интересных пакета. Они были сжаты с помощью LZNT1 и зашифрованы с помощью RC4 и XOR. Ранее было замечено, что XOR-шифрование можно игнорировать, т.к. данные в пакете отправляются повторно не зашифрованными. Для расшифровки RC4 необходимо знать имя пользователя, которое было получено из RAM-дампа: FLARE ON 2019. Стоит отметить, что функция GetUserNameA, которая используется для получения имени пользователя в модуле, возвращает в качестве длины длину буфера для имени пользователя с учетом нуль-символа в конце строки, что стоит учитывать при использовании RC4. Для распаковки сжатых с помощью LZNT1 данных был написан следующий скрипт:

from ctypes import *
nt = windll.ntdll

for fname in ['input']:
    with open(fname, 'rb') as f:
      buf = f.read()
    dec_data = create_string_buffer(0x10000)
    final_size = c_ulong(0)

    status = nt.RtlDecompressBuffer(
        0x102,             # COMPRESSION_FORMAT_LZNT1
        dec_data,          # UncompressedBuffer
        0x10000,           # UncompressedBufferSize
        c_char_p(buf),     # CompressedBuffer
        0xFFFFFF,          # CompressedBufferSize
        byref(final_size)  # FinalUncompressedSize
    )

    with open(fname + '.uncompressed', 'wb') as f:
        f.write(dec_data.raw[:final_size.value])

Для примера рассмотрим самый первый пакет для порта 6666. Изначально он выглядит так:

00000000: CC 69 94 FA 6A 37 18 29  CB 8D 87 EF 11 63 8E 73  .i..j7.).....c.s
00000010: FE AB 43 3B B3 94 28 4B  4D 19 00 00 00 4F DB C7  ..C;..(KM....O..
00000020: F3 1E E4 13 15 34 8F 51  A9 2B C2 D7 C1 96 78 F7  .....4.Q.+....x.
00000030: 91 98

Если взять вторую половину пакета, получим следующее:

00000000: 19 00 00 00 4F DB C7 F3  1E E4 13 15 34 8F 51 A9  ....O.......4.Q.
00000010: 2B C2 D7 C1 96 78 F7 91  98                       +....x...

Первые 4 байта пакета — это длина всего сообщения, в данном случае равная 25. Расшифруем оставшиеся данные:

00000000: 12 B0 00 43 3A 5C 6B 65  79 70 61 04 73 73 01 70  ...C:keypa.ss.p
00000010: 73 2E 6B 64 62                                    s.kdb

Применим скрипт для декомпрессии и получим строку C:keypasskeys.kdb. По всей видимости, это ответ на запрос поиска файла, о котором мы говорили выше. Во втором пакете для порта 6666 был обнаружен сам файл — это база для хранилища паролей KeePass.

В пакетах для порта 7777 были обнаружены скриншоты рабочего стола в формате BMP. Они были зашифрованы только с помощью XOR и, в данном случае, их всё же пришлось расшифровывать, т.к. повторно отправляемые не зашифрованные данные не вошли в пакет полностью. В результате преобразований был получен набор скриншотов, на которых видно, как пользователь использует KeePass.

Flare-On 2019 write-up - 35

Flare-On 2019 write-up - 36

В пакетах для порта 8888 были обнаружены данные модуля k.dll — сохраненные нажатия клавиш и названия окон.

C:Windowssystem32cmd.exe
nslookup googlecom
ping 1722173110
nslookup soeblogcom
nslookup fiosquatumgatefiosrouterhome
C:Windowssystem32cmd.exe
Start
Start menu
Start menu
chrome
www.flare-on.com - Google Chrome
tis encrypting something twice better than once
Is encrypting something twice better than once? - Google Search - Google Chrome
Start
Start menu
Start menu
keeKeePass
<DYN_TITLE>
th1sisth33nd111
KeePass
keys.kdb - KeePass
Is encrypting something twice better than once? - Google Search - Google Chrome
Start
Start menu
Start menu
KeePass
<DYN_TITLE>
th1sisth33nd111
Open Database - keys.kdb
KeePass
Start
Start menu
Start menu
KeePass
Start menu
Start menu
Start menu
KeePass
<DYN_TITLE>
th1sisth33nd111

После этого я попробовал использовать пароль th1sisth33nd111 для открытия базы хранилища паролей, но ничего не вышло. Также по скриншотам видно, что пароль должен быть длиннее. Дело в том, что keylogger не учитывает некоторые нюансы нажатия клавиш и логирует не всё. Например, в логе видно, что в команде ping не были учтены точки. Далее были предприняты попытки использовать hashcat для подбора пароля для базы KeePass с учетом мутаций, но ничего не вышло. Затем я попробовал поискать фрагменты полученного пароля в строках дампа и получил следующий результат:

$ strings help.dmp | grep -i '3nd!'
!s_iS_th3_3Nd!!!

Дописав Th к полученной строке я получил доступ к хранилищу.

Flare-On 2019 write-up - 37

Также я рекомендую почитать этот райтап от другого участника соревнований. В нем приведен способ решения таска, не требующий реверс-инжиниринга.

0x0D — Итог

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

Flare-On 2019 write-up - 38

Автор: a1exdandy

Источник


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


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