Flare-On 2019 write-up



    -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 файл. При попытке запуска выводится сообщение со следующим содержимым:



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



    Также можно было переписать алгоритм декодирования на 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




    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. Запустив исполняемый файл, можно увидеть шахматную доску.



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



    Весь трафик состоит из запросов к 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'yZ\xb8\xbc\xec\xd3\xdf\xdd\x99\xa5\xb6\xac\x156\x85\x8d\t\x08wRMqT}\xa7\xa7\x08\x16\xfd\xd7'
    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.



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



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


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



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



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




    При дальнейшем анализе было обнаружено, что в программе создаются две полигональные сетки (меш, 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()



    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, т.к. он препятствует отладке.



    В результате можно увидеть, что метод 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).



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




    Осталось создать алгоритм обратного преобразования. Он довольно простой, поэтому приведу только итоговый скрипт на 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 изображение без флага. Повторив процедуру на нем, получим итоговое изображение с флагом.




    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. По всей видимости, для его решения необходимо подобрать некоторый код.



    Анализ точки входа показывает, что это самораспаковывающийся архив. При запуске проверяется наличие переменной окружения _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




    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, т.к. он имеет достаточно богатые возможности отладки. Запустим игру, открыв редактор памяти.



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



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




    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, что не соответствует формату флага.



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


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


    flag = bytearray(b'D)6\n)\x0f\x05\x1be&\x10\x04+h0/\x003/\x05\x1a\x1f\x0f8\x02\x18B\x023\x1a(\x04*G?\x04&dfM\x107>(>w\x1c?~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))



    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. Первое, что бросилось в глаза — странное использование импортируемых функций и несоответствие количества передаваемых в них аргументов. При запуске отладчика выяснилось, что функции действительно загружаются не так, как мы ожидаем.



    Данную проблему можно решить следующим скриптом для 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(') + ';')

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



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



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


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

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



    Функция похожа на реализацию 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 необходим для проверки реализации алгоритма. Для его шифрования использовался ключ из нулевых байтов, что можно понять по названию. Дешифрованный файл выглядит следующим образом:



    Можно заметить, что на изображении есть подсказка о реальном ключе, но я не сразу обратил на это внимание. Для перебора ключа алгоритм был переписан на 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("%lf\n", ((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("%#llx\n", key);
            }
        }
    }

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




    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('pp\xb2\xac\x01\xd2^a\n\xa7*\xa8\x08\x1c\x86\x1a\xe8E\xc8)\xb2\xf3\xa1\x1e\x00\x00\x00\x00\x00\x00\x00\x00')
    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);
    });

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




    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 \SystemRoot\system32\ntoskrnl.exe
    
    ...
    0xfffffa800428ff30 man.sys              0xfffff880033bc000             0xf000 \??\C:\Users\FLARE ON 2019\Desktop\man.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)

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



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


    • 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:\keypass\keys.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:\keypass\keys.kdb. По всей видимости, это ответ на запрос поиска файла, о котором мы говорили выше. Во втором пакете для порта 6666 был обнаружен сам файл — это база для хранилища паролей KeePass.


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




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


    C:\Windows\system32\cmd.exe
    nslookup googlecom
    ping 1722173110
    nslookup soeblogcom
    nslookup fiosquatumgatefiosrouterhome
    C:\Windows\system32\cmd.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 к полученной строке я получил доступ к хранилищу.



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



    0x0D — Итог


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


    • +24
    • 2,8k
    • 4
    Digital Security
    157,77
    Безопасность как искусство
    Поделиться публикацией

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

      +3
      Впечатляет!
        +3
        обалдеть.
        Интересно как долго писался пост :)
          +3
          Годнота
            +2
            У меня получилось достать последний флаг сдампив память keepass и применив
            CryptUnprotectMemory в Unicorn.

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

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