Хабрахабр

Flare-On 2019 write-up

-0x01 — Intro

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

Надеюсь, прочитав это, Вы найдете для себя что-то новое и полезное. В данной статье я старался описать те моменты, которые вызвали у меня наибольший интерес, поэтому в разборе не будет описания рутиной работы в 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!

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. This is a simple game. Enter the flag here on this site to score and move on to the next level.

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

** 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

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

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

He is very fussy. We at Flare have created our own Tamagotchi pet, the flarebear. 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() происходит расшифровка raw-ресурсов приложения с помощью метода .decrypt(String, byte[]), первым аргументом которого является строка, полученная с помощью метода .getPassword(). Анализируя исходный код приложения, можно найти интересный метод .danceWithFlag(), который находится в файле FlareBearActivity.java. Для этого я решил немного переписать декомпилированный код, избавившись от зависимостей Android и оставив только необходимые для расшифровки методы, чтобы в результате можно было скомпилировать полученный код. Наверняка флаг находится в зашифрованных ресурсах, поэтому попробуем расшифровать их. Каждое значение лежит в небольшом интервале от 0 до N, поэтому можно перебрать все возможные значения в поисках нужного пароля. В дальнейшем, при анализе, было обнаружено, что метод .getPassword() зависит от трех целочисленных значений состояния.

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

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 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

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

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

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

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

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

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

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

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

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

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

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

Также при построении сеток их вершины зашифрованы с помощью XOR, что тоже вызывает подозрения. При дальнейшем анализе было обнаружено, что в программе создаются две полигональные сетки (меш, polygon mesh), хотя при работе программы мы видим только один объект. Наибольший интерес представляет вторая сетка, т.к. Давайте расшифруем и визуализируем вершины. Расшифровав все вершины, я обнаружил, что координата 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

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

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

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

// 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[]) происходит вставка преобразованных данных в изображение
  • Полученное изображение сохраняется с новым именем

Поверхностный анализ класса показал схожесть некоторых его методов с методами обфускатора ConfuserEx (файл AntiTamper. При инициализации приложения вызываются различные методы класса A. Приложение действительно защищено от отладки. 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); } }
}

Результат его работы зависит от различных глобальных значений, получаемых, в том числе, при инициализации в методе Program. Очень похоже на классический LSB, однако в местах, где ожидаются константы, используется метод int Program.j(byte). Было решено не реверсить его работу, а получить все возможные значения во время выполнения. Init(). Воспользуемся этим и перезапишем метод Program. dnSpy позволяет редактировать декомпилированный код приложения и сохранять измененные модули. 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. Далее я пробовал повторить алгоритм метода преобразования данных, но результат вычислений не совпадал с выводом программы. IncrementMaxStack). VerifySignature(MethodInfo m1, MethodInfo m2)) и модификации IL байт-кода методов (в A.

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

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

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

It does strongly resemble the classic 1983 movie WarGames. We used our own computer hacking skills to "find" this AI on a military supercomputer. If you can find the launch codes for us, we'll let you pass to the next challenge. Perhaps life imitates art? 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

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

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

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

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

В начале исполнения кода сохраним функцию print под другим именем и заменим ею print в блоке try-except. Можно понять, что функция print на самом деле переопределена как exec, а её аргумент зависит только от __doc__.encode() — текста в начале файла. Возможно, при декомпиляции __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)

В результате, при определенном значении i, код успешно выведется на экран. Исполним скрипт еще раз, заменив содержимое __doc__. В функции 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

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

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

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

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

0x09 — reloadered

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

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

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

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

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,

He clicked a link and was infected with MugatuWare! I’m working an incident response case for Derek Zoolander. 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.

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

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

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

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

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

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

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

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

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

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

Для перебора ключа алгоритм был переписан на 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.

Он представляет из себя реализацию виртуальной машины с набором 256-битных регистров и операций над ними. В таске дан бинарник vv_max.exe, принимающий две строки в качестве аргументов. В результате дизассемблирования байт-кода виртуальной машины получилось следующее: Операции производятся с помощью инструкций расширения 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. Также происходит сравнение r2 и r20. Таким образом, первый аргумент программы должен быть FLARE2019. Влияние второго аргумента на r2 линейное — каждый байт входа влияет на 6 бит r2. В ходе динамического анализа выяснилось, что значение r20 не зависит от второго аргумента программы. Для автоматизации я использовал Frida: Я решил просто перебирать каждый символ входных данных до тех пор, пока очередные 6 бит выхода не совпадут с нужным значением.

# 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

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

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

В ходе различных экспериментов в 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 со случайным ключом.

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

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

Скрипт для 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 — модуль отвечает за взаимодействие с файловой системой.

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

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

  • keys.kdb
  • C:\
  • C:\keypass\keys.kdb

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

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

Они были зашифрованы только с помощью XOR и, в данном случае, их всё же пришлось расшифровывать, т.к. В пакетах для порта 7777 были обнаружены скриншоты рабочего стола в формате BMP. В результате преобразований был получен набор скриншотов, на которых видно, как пользователь использует 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 для открытия базы хранилища паролей, но ничего не вышло. Например, в логе видно, что в команде ping не были учтены точки. Дело в том, что keylogger не учитывает некоторые нюансы нажатия клавиш и логирует не всё. Затем я попробовал поискать фрагменты полученного пароля в строках дампа и получил следующий результат: Далее были предприняты попытки использовать hashcat для подбора пароля для базы KeePass с учетом мутаций, но ничего не вышло.

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

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

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

0x0D — Итог

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть