Flare-On 2019 write-up

bz4gsbxbvka4sjtartbqmxccw6q.png


-0×01 — Intro

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

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

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


0×00 — Содержание


  1. 0×01 — Memecat Battlestation [Shareware Demo Edition]
  2. 0×02 — Overlong
  3. 0×03 — Flarebear
  4. 0×04 — Dnschess
  5. 0×05 — demo
  6. 0×06 — bmphide
  7. 0×07 — wopr
  8. 0×08 — snake
  9. 0×09 — reloadered
  10. 0×0A — Mugatu
  11. 0×0B — vv_max
  12. 0×0C — help
  13. 0×0D — Итог


0×01 — 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, примерно тогда же я его и решил. Я не помню, как его решал Таск довольно простой, поэтому рассматривать его решение не будем.


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

gb0qeertq_z-3wk0jyry5tuyjts.png

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

5qlpbeezlg3hmwbr8-bqnmd-50o.png

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


0×03 — 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 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


eospwvjutdz_76f4qrwzqqzstie.png
mrcfwklfuon1ebo1ctqafdzicti.png

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


5t7mgn6jyrc2zndnuwliuiuzovc.png

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

br00-59yyrbti3rrdxgvizcfkdk.png

Весь трафик состоит из запросов к 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


0×05 — 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.

dbcwllgkqhasqqyvcmjv0iugwoc.png

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

banscbeyqmgqabaf6nbg5khebgk.png

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

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

hjemxvwadjd5f6z-paauo9vh4by.png

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

5lgl-4g3pzgrxf0gigfh5m99uqe.png

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

ghyc35pxpo9mhyi8dx-louqlcsy.png

wgskqlsgkdxsul3hqll8m-ohv8c.png

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

ke5wz52y1nkpe3jpgxsrl9vcg4c.png


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

v72ol4f8pcefzyg7lnmkywe4tyw.png

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

xti6e6knsrkyrapcutnkypgfeau.png

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

lqwoemy3gsysvsptxholl6ioyey.png

letwj6p_uzdmlglrgsky2gifwuy.png

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

jjbayqp9cpownsuf1wlrwar-rem.png


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

ji1p2w_9cpkv8vsaxbezvicozjc.png

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

ypoxjh3rafci2a5-ukpy2uvbgu0.png


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

tkfxsqug4puqwqs893v5zceereg.png

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

uo2bh8iawskqdff-153-964ljwq.png

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

jnnujhs5-fugmpqugrp1z9m7p0o.png


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

kiugmvs_ouxbeziqmqnzjhxgqmu.png

В ходе дальнейшего анализа я обнаружил интересный фрагмент кода, заполненный операцией 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))

thqz9bwdjdkt-a7smd_tb8uncxs.png


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

noauseew8d2jezeoaqj9kd-pp9e.png

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

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

f-cladmcmadtfx1v9bdhjhobt28.png

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

nppq5qbcpyirk9xgamtl0wh89h0.png

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


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

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

bmem6uqdty4xdugfaxuz0j9efuw.png

© Habrahabr.ru