
-0x01 — Intro
Данная статья посвящена разбору всех заданий Flare-On 2019 — ежегодного соревнования по реверс-инжинирингу от FireEye. В данных соревнованиях я принимаю участие уже второй раз. В предыдущем году мне удалось попасть на 11-ое место по времени сдачи, решив все задачи примерно за 13 суток. В этом году набор тасков был проще, и я уложился в 54 часа, заняв при этом 3 место по времени сдачи.
В данной статье я старался описать те моменты, которые вызвали у меня наибольший интерес, поэтому в разборе не будет описания рутиной работы в IDA, понимания алгоритмов каждой функции и других не самых интересных моментов. Надеюсь, прочитав это, Вы найдете для себя что-то новое и полезное. С разборами задач от авторов, а также с некоторой статистикой и призами для победителей Вы можете ознакомиться тут.
Если вас заинтересовало, то добро пожаловать под кат!
0x00 — Содержание
- 0x01 — Memecat Battlestation [Shareware Demo Edition]
- 0x02 — Overlong
- 0x03 — Flarebear
- 0x04 — Dnschess
- 0x05 — demo
- 0x06 — bmphide
- 0x07 — wopr
- 0x08 — snake
- 0x09 — reloadered
- 0x0A — Mugatu
- 0x0B — vv_max
- 0x0C — help
- 0x0D — Итог
0x01 — Memecat Battlestation [Shareware Demo Edition]
Welcome to the Sixth Flare-On Challenge!
This is a simple game. Reverse engineer it to figure out what "weapon codes" you need to enter to defeat each of the two enemies and the victory screen will reveal the flag. Enter the flag here on this site to score and move on to the next level.
* This challenge is written in .NET. If you don't already have a favorite .NET reverse engineering tool I recommend dnSpy
** If you already solved the full version of this game at our booth at BlackHat or the subsequent release on twitter, congratulations, enter the flag from the victory screen now to bypass this level.
Данный таск был выложен заранее в рамках Black Hat USA 2019, примерно тогда же я его и решил. Я не помню, как его решал Таск довольно простой, поэтому рассматривать его решение не будем.
0x02 — Overlong
The secret of this next challenge is cleverly hidden. However, with the right approach, finding the solution will not take an overlong amount of time.
Дан x86 .exe файл. При попытке запуска выводится сообщение со следующим содержимым:

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

Также можно было переписать алгоритм декодирования на Python и получить флаг:
msg = [ ... ] # сюда необходимо вставить закодированное сообщение output = [] i = 0 while i < len(msg): if (msg[i] >> 3) == 0x1e: out_char = ( ((msg[i + 3] & 0x3F) << 0 ) | ((msg[i + 2] & 0x3F) << 6 ) | ((msg[i + 1] & 0x3F) << 12) | ((msg[i + 0] & 7) << 18) ) output.append(out_char) i += 4 elif (msg[i] >> 4) == 0x0e: out_char = ( ((msg[i + 2] & 0x3F) << 0 ) | ((msg[i + 1] & 0x3F) << 6 ) | ((msg[i + 0] & 0xF) << 12) ) output.append(out_char) i += 3 elif (msg[i] >> 5) == 6: out_char = ( ((msg[i + 1] & 0x3F) << 0 ) | ((msg[i + 0] & 0xF) << 6 ) ) output.append(out_char) i += 2 else: output.append(msg[i]) i += 1 print(bytes([i for i in output])) # b'I never broke the encoding: I_a_M_t_h_e_e_n_C_o_D_i_n_g@flare-on.com'
0x03 — Flarebear
We at Flare have created our own Tamagotchi pet, the flarebear. He is very fussy. Keep him alive and happy and he will give you the flag.
В данном таске дан apk файл для Android. Рассмотрим метод решения без запуска самого приложения.
Первым делом необходимо получить исходный код приложения. Для этого с помощью набора утилит dex2jar преобразуем apk в jar и затем получим исходный код на Java с помощью декомпилятора, в качестве которого я предпочитаю использовать cfr.
~/retools/d2j/d2j-dex2jar.sh flarebear.apk java -jar ~/retools/cfr/cfr-0.146.jar --outputdir src flarebear-dex2jar.jar
Анализируя исходный код приложения, можно найти интересный метод .danceWithFlag(), который находится в файле FlareBearActivity.java. Внутри .danceWithFlag() происходит расшифровка raw-ресурсов приложения с помощью метода .decrypt(String, byte[]), первым аргументом которого является строка, полученная с помощью метода .getPassword(). Наверняка флаг находится в зашифрованных ресурсах, поэтому попробуем расшифровать их. Для этого я решил немного переписать декомпилированный код, избавившись от зависимостей Android и оставив только необходимые для расшифровки методы, чтобы в результате можно было скомпилировать полученный код. В дальнейшем, при анализе, было обнаружено, что метод .getPassword() зависит от трех целочисленных значений состояния. Каждое значение лежит в небольшом интервале от 0 до N, поэтому можно перебрать все возможные значения в поисках нужного пароля.
В результате получился следующий код:
import java.io.InputStream; import java.nio.charset.Charset; import java.security.Key; import java.security.spec.AlgorithmParameterSpec; import java.security.spec.KeySpec; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.stream.Collectors; import java.util.Collections; import java.io.*; import javax.crypto.Cipher; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.SecretKeySpec; public final class Main { public static void main (String args []) throws Exception { Main a = new Main(); InputStream inputStream = new FileInputStream("ecstatic"); long fileSize = new File("ecstatic").length(); byte[] file1 = new byte[(int) fileSize]; inputStream.read(file1); inputStream = new FileInputStream("ecstatic2"); fileSize = new File("ecstatic2").length(); byte[] file2 = new byte[(int) fileSize]; inputStream.read(file2); for(int i = 0; i < 9; i++) { for(int j = 0; j < 7; j++) { for(int k = 1; k < 16; k++) { String pass = a.getPassword(i, j, k); try { byte[] out1 = a.decrypt(pass, file1); byte[] out2 = a.decrypt(pass, file2); OutputStream outputStream = new FileOutputStream("out1"); outputStream.write(out1); outputStream = new FileOutputStream("out2"); outputStream.write(out2); System.out.println("yep!"); } catch (javax.crypto.BadPaddingException ex) { } } } } } public final byte[] decrypt(Object object, byte[] arrby) throws Exception { Object object2 = Charset.forName("UTF-8"); object2 = "pawsitive_vibes!".getBytes((Charset)object2); object2 = new IvParameterSpec((byte[])object2); object = ((String)object).toCharArray(); Object object3 = Charset.forName("UTF-8"); object3 = "NaClNaClNaCl".getBytes((Charset)object3); object = new PBEKeySpec((char[])object, (byte[])object3, 1234, 256); object = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1").generateSecret((KeySpec)object); object3 = new SecretKeySpec(((SecretKey)object).getEncoded(), "AES"); object = Cipher.getInstance("AES/CBC/PKCS5Padding"); ((Cipher)object).init(2, (Key)object3, (AlgorithmParameterSpec)object2); object = ((Cipher)object).doFinal(arrby); return (byte [])object; } public final String getPassword(int n, int n2, int n3) { String string2 = "*"; String string3 = "*"; switch (n % 9) { case 8: { string2 = "*"; break; } case 7: { string2 = "&"; break; } case 6: { string2 = "@"; break; } case 5: { string2 = "#"; break; } case 4: { string2 = "!"; break; } case 3: { string2 = "+"; break; } case 2: { string2 = "$"; break; } case 1: { string2 = "-"; break; } case 0: { string2 = "_"; } } switch (n3 % 7) { case 6: { string3 = "@"; break; } case 4: { string3 = "&"; break; } case 3: { string3 = "#"; break; } case 2: { string3 = "+"; break; } case 1: { string3 = "_"; break; } case 0: { string3 = "$"; } case 5: } String string4 = String.join("", Collections.nCopies(n / n3, "flare")); String string5 = String.join("", Collections.nCopies(n2 * 2, this.rotN("bear", n * n2))); String string6 = String.join("", Collections.nCopies(n3, "yeah")); StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append(string4); stringBuilder.append(string2); stringBuilder.append(string5); stringBuilder.append(string3); stringBuilder.append(string6); return stringBuilder.toString(); } public final String rotN(String charSequence, int n) { Collection<String> collection = new ArrayList(charSequence.length()); for (int i = 0; i < charSequence.length(); ++i) { char c; char c2 = c = charSequence.charAt(i); if (Character.isLowerCase(c)) { char c3; c2 = c3 = (char)(c + n); if (c3 > 'z') { c2 = c3 = (char)(c3 - n * 2); } } collection.add(Character.valueOf(c2).toString()); } return collection.stream().collect(Collectors.joining()); // return ArraysKt.joinToString$default(CollectionsKt.toCharArray((List)collection), (CharSequence)FLARE_BEAR_NAME, null, null, 0, null, null, 62, null); } }
Извлечем зашифрованные ресурсы, скомпилируем и запустим полученный файл:
$ ~/retools/apktool/apktool d flarebear.apk $ cp flarebear/res/raw/* . $ javac Main.java $ java Main
К счастью, из всех пе́ребранных вариантов пароля подходит всего один. В результате мы получим два изображения с флагом:
~/flareon2019/3 - Flarebear$ file out* out1: PNG image data, 2100 x 2310, 8-bit/color RGB, non-interlaced out2: PNG image data, 2100 x 2310, 8-bit/color RGB, non-interlaced


0x04 — Dnschess
Some suspicious network traffic led us to this unauthorized chess program running on an Ubuntu desktop. This appears to be the work of cyberspace computer hackers. You'll need to make the right moves to solve this one. Good luck!
В данном таске дан дамп трафика, исполняемый ELF-файл ChessUI и библиотека ChessAI.so. Запустив исполняемый файл, можно увидеть шахматную доску.

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

Весь трафик состоит из запросов к DNS-серверу типа A. Сами запросы состоят из названий фигур, описания хода в шахматной партии и постоянной части .game-of-thrones.flare-on.com, например rook-c3-c6.game-of-thrones.flare-on.com. По постоянной части можно легко найти нужное место в библиотеке ChessAI.so:
signed __int64 __fastcall getNextMove(int idx, const char *chess_name, unsigned int pos_from, unsigned int pos_to, \__int64 a5) { struct hostent *v9; // [rsp+20h] [rbp-60h] char *ip_addr; // [rsp+28h] [rbp-58h] char dns_name; // [rsp+30h] [rbp-50h] unsigned __int64 v12; // [rsp+78h] [rbp-8h] v12 = __readfsqword(0x28u); strcpy(&dns_name, chess_name); pos_to_str(&dns_name, pos_from); pos_to_str(&dns_name, pos_to); strcat(&dns_name, ".game-of-thrones.flare-on.com"); v9 = gethostbyname(&dns_name); if ( !v9 ) return 2LL; ip_addr = *v9->h_addr_list; if ( *ip_addr != 127 || ip_addr[3] & 1 || idx != (ip_addr[2] & 0xF) ) return 2LL; sleep(1u); flag[2 * idx] = ip_addr[1] ^ key[2 * idx]; flag[2 * idx + 1] = ip_addr[1] ^ key[2 * idx + 1]; *(_DWORD *)a5 = (unsigned __int8)ip_addr[2] >> 4; *(_DWORD *)(a5 + 4) = (unsigned __int8)ip_addr[3] >> 1; strcpy((char *)(a5 + 8), off_4120[idx]); return (unsigned __int8)ip_addr[3] >> 7; }
Из кода видно, что на основе получаемых ip-адресов расшифровывается некоторая байтовая строка, сохраняемая в другой области памяти, которую я назвал flag.
Для решения таска первым делом получим все ip-адреса из дампа трафика. Сделать это можно с помощью следующей команды:
tshark -r capture.pcap | grep -P -o '127.(\d+).(\d+).(\d+)' | grep -v '127.0.0.1'
Сохранив все ip-адреса в файл ips можно воспользоваться следующим кодом на Python для получения флага:
with open('ips') as f: ips = f.read().split() flag = bytearray(64) key = b'yZ\xb8\xbc\xec\xd3\xdf\xdd\x99\xa5\xb6\xac\x156\x85\x8d\t\x08wRMqT}\xa7\xa7\x08\x16\xfd\xd7' for ip in ips: a, b, c, d = map(int, ip.split('.')) if d & 1: continue idx = c & 0xf if idx > 14: continue flag[2*idx] = b ^ key[2*idx] flag[2*idx + 1] = b ^ key[2*idx + 1] print(flag.decode() + '@flare-on.com') # LooksLikeYouLockedUpTheLookupZ@flare-on.com
0x05 — demo
Someone on the Flare team tried to impress us with their demoscene skills. It seems blank. See if you can figure it out or maybe we will have to fire them. No pressure.
Дан исполняемый файл 4k.exe, который использует DirectX. При запуске в главном окне отображается вращающийся логотип FlareOn.

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

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

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

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


При дальнейшем анализе было обнаружено, что в программе создаются две полигональные сетки (меш, polygon mesh), хотя при работе программы мы видим только один объект. Также при построении сеток их вершины зашифрованы с помощью XOR, что тоже вызывает подозрения. Давайте расшифруем и визуализируем вершины. Наибольший интерес представляет вторая сетка, т.к. в ней значительно больше вершин. Расшифровав все вершины, я обнаружил, что координата Z у каждой из них равна 0, поэтому для визуализации решено было рисовать двухмерные графики с помощью matplotlib. Получился следующий код и результат с флагом:
import struct import matplotlib.pyplot as plt with open('vertexes', 'rb') as f: data = f.read() n = len(data) // 4 data = list(struct.unpack('{}I'.format(n), data)) key = [0xCB343C8, 0x867B81F0, 0x84AF72C3] data = [data[i] ^ key[i % 3] for i in range(len(data))] data = struct.pack('{}I'.format(n), *data) data = list(struct.unpack('{}f'.format(n), data)) x = data[0::3] y = data[1::3] z = data[2::3] print(z) plt.plot(x, y) plt.show()

0x06 — bmphide
Tyler Dean hiked up Mt. Elbert (Colorado's tallest mountain) at 2am to capture this picture at the perfect time. Never skip leg day. We found this picture and executable on a thumb drive he left at the trail head. Can he be trusted?
В таске дан исполняемый файл bmphide.exe и изображение image.bmp. Можно предположить, что в изображении с помощью методов стеганографии спрятано некоторое сообщение.
Бинарник написан на C#, поэтому для анализа я использовал утилиту dnSpy. Сразу можно заметить, что большинство названий методов обфусцированы. Если посмотреть на метод Program.Main, можно понять логику работы программы и сделать предположения о назначении некоторых из них:
// BMPHIDE.Program // Token: 0x06000018 RID: 24 RVA: 0x00002C18 File Offset: 0x00002C18 private static void Main(string[] args) { Program.Init(); Program.yy += 18; string filename = args[2]; string fullPath = Path.GetFullPath(args[0]); string fullPath2 = Path.GetFullPath(args[1]); byte[] data = File.ReadAllBytes(fullPath2); Bitmap bitmap = new Bitmap(fullPath); byte[] data2 = Program.h(data); Program.i(bitmap, data2); bitmap.Save(filename); }
- Происходит инициализация приложения с помощью метода
Program.Init() - Считывается файл данных и файл изображения
- С помощью метода
byte [] Program.h(byte [])происходит некоторое преобразование считанных данных - С помощью метода
Program.i(Bitmap, byte[])происходит вставка преобразованных данных в изображение - Полученное изображение сохраняется с новым именем
При инициализации приложения вызываются различные методы класса A. Поверхностный анализ класса показал схожесть некоторых его методов с методами обфускатора ConfuserEx (файл AntiTamper.JIT.cs). Приложение действительно защищено от отладки. При этом снять защитные механизмы с помощью утилиты de4dot и её форков не удалось, поэтому было решено продолжить анализ.
Рассмотрим метод Program.i, который используется для вставки данных в изображение.
public static void i(Bitmap bm, byte[] data) { int num = Program.j(103); for (int i = Program.j(103); i < bm.Width; i++) { for (int j = Program.j(103); j < bm.Height; j++) { bool flag = num > data.Length - Program.j(231); if (flag) { break; } Color pixel = bm.GetPixel(i, j); int red = ((int)pixel.R & Program.j(27)) | ((int)data[num] & Program.j(228)); int green = ((int)pixel.G & Program.j(27)) | (data[num] >> Program.j(230) & Program.j(228)); int blue = ((int)pixel.B & Program.j(25)) | (data[num] >> Program.j(100) & Program.j(230)); Color color = Color.FromArgb(Program.j(103), red, green, blue); bm.SetPixel(i, j, color); num += Program.j(231); } } }
Очень похоже на классический LSB, однако в местах, где ожидаются константы, используется метод int Program.j(byte). Результат его работы зависит от различных глобальных значений, получаемых, в том числе, при инициализации в методе Program.Init(). Было решено не реверсить его работу, а получить все возможные значения во время выполнения. dnSpy позволяет редактировать декомпилированный код приложения и сохранять измененные модули. Воспользуемся этим и перезапишем метод Program.Main следующим образом:
private static void Main(string[] args) { Program.Init(); Program.yy += 18; for (int i = 0; i < 256; i++) { Console.WriteLine(string.Format("j({0}) = {1}", i, Program.j((byte)i))); } }
При запуске мы получим следующие значения:
E:\>bmphide_j.exe j(0) = 206 j(1) = 204 j(2) = 202 j(3) = 200 j(4) = 198 j(5) = 196 j(6) = 194 j(7) = 192 j(8) = 222 j(9) = 220 j(10) = 218 j(11) = 216 j(12) = 214 j(13) = 212 j(14) = 210 j(15) = 208 j(16) = 238 j(17) = 236 j(18) = 234 j(19) = 232 j(20) = 230 ...
Заменим вызовы Program.j в методе Program.i на полученные константы:
public static void i(Bitmap bm, byte[] data) { int num = 0; for (int i = 0; i < bm.Width; i++) { for (int j = 0; j < bm.Height; j++) { bool flag = num > data.Length - 1; if (flag) { break; } Color pixel = bm.GetPixel(i, j); int red = ((int)pixel.R & 0xf8) | ((int)data[num] & 0x7); int green = ((int)pixel.G & 0xf8) | (data[num] >> 3 & 0x7); int blue = ((int)pixel.B & 0xfc) | (data[num] >> 6 & 0x3); Color color = Color.FromArgb(0, red, green, blue); bm.SetPixel(i, j, color); num += 1; } } }
Теперь становится понятен способ вставки каждого байта сообщения в изображение:
- биты с 0 по 2 помещаются в 3 младших бита красного канала точки
- биты с 3 по 5 помещаются в 3 младших бита зеленого канала точки
- биты с 6 по 7 помещаются в 2 младших бита синего канала точки
Далее я пробовал повторить алгоритм метода преобразования данных, но результат вычислений не совпадал с выводом программы. Как оказалось, в классе A также имеется функционал для замены методов (в A.VerifySignature(MethodInfo m1, MethodInfo m2)) и модификации IL байт-кода методов (в A.IncrementMaxStack).
Для выбора методов, которые необходимо заменить в Program, в Program.Init происходит хеширование IL байт-кода всех методов и сравнение с заранее подсчитанными значениями. Всего подменяется два метода. Чтобы выяснить, какие именно, запустим приложение под отладчиком, поставив брейкпоинты на вызовы A.VerifySignature, при этом необходимо пропустить вызов A.CalculateStack() в Program.Init, т.к. он препятствует отладке.

В результате можно увидеть, что метод Program.a заменяется на Program.b, а Program.c — на Program.d.
Теперь необходимо разобраться с модификацией байт-кода:
private unsafe static uint IncrementMaxStack(IntPtr self, A.ICorJitInfo* comp, A.CORINFO_METHOD_INFO* info, uint flags, byte** nativeEntry, uint* nativeSizeOfCode) { bool flag = info != null; if (flag) { MethodBase methodBase = A.c(info->ftn); bool flag2 = methodBase != null; if (flag2) { bool flag3 = methodBase.MetadataToken == 100663317; if (flag3) { uint flNewProtect; A.VirtualProtect((IntPtr)((void*)info->ILCode), info->ILCodeSize, 4u, out flNewProtect); Marshal.WriteByte((IntPtr)((void*)info->ILCode), 23, 20); Marshal.WriteByte((IntPtr)((void*)info->ILCode), 62, 20); A.VirtualProtect((IntPtr)((void*)info->ILCode), info->ILCodeSize, flNewProtect, out flNewProtect); } else { bool flag4 = methodBase.MetadataToken == 100663316; if (flag4) { uint flNewProtect2; A.VirtualProtect((IntPtr)((void*)info->ILCode), info->ILCodeSize, 4u, out flNewProtect2); Marshal.WriteInt32((IntPtr)((void*)info->ILCode), 6, 309030853); Marshal.WriteInt32((IntPtr)((void*)info->ILCode), 18, 209897853); A.VirtualProtect((IntPtr)((void*)info->ILCode), info->ILCodeSize, flNewProtect2, out flNewProtect2); } } } } return A.originalDelegate(self, comp, info, flags, nativeEntry, nativeSizeOfCode); }
Понятно, что модифицироваться будут методы с определенными значениями MetadataToken, а именно 0x6000015 и 0x6000014. Этим токенам соответствуют методы Program.h и Program.g. В dnSpy имеется встроенный hex-редактор, в котором при наведении подсвечиваются данные методов: их заголовок (выделен фиолетовым) и байт-код (выделен красным), как показано на скриншоте. Перейти к нужному методу в hex-редакторе можно нажав на соответствующий адрес в комментарии перед декомпилированным методом (например, File Offset: 0x00002924).

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


Осталось создать алгоритм обратного преобразования. Он довольно простой, поэтому приведу только итоговый скрипт на Python:
from PIL import Image # Rotate left: 0b1001 --> 0b0011 rol = lambda val, r_bits, max_bits: \ (val << r_bits%max_bits) & (2**max_bits-1) | \ ((val & (2**max_bits-1)) >> (max_bits-(r_bits%max_bits))) # Rotate right: 0b1001 --> 0b1100 ror = lambda val, r_bits, max_bits: \ ((val & (2**max_bits-1)) >> r_bits%max_bits) | \ (val << (max_bits-(r_bits%max_bits)) & (2**max_bits-1)) rol8 = lambda a, b: rol(a, b, 8) ror8 = lambda a, b: ror(a, b, 8) def extract(fname): img = Image.open(fname) w, h = img.size result = bytearray() for i in range(w): for j in range(h): r, g, b = img.getpixel((i, j)) # print('{:02x} {:02x} {:02x}'.format(r, g, b)) byte = (r & 0b111) | ((g & 0b111) << 3) | ((b & 0b11) << 6) result.append(byte) return result enc = extract('image.bmp') n = len(enc) dec = bytearray() def g(idx): b = ((idx + 1) * 309030853) & 0xff k = ((idx + 2) * 209897853) & 0xff return b ^ k j = 0 for i in range(n): x = enc[i] x = rol8(x, 3) x ^= g(2*i + 1) x = ror8(x, 7) x ^= g(2*i + 0) dec.append(x) with open('output', 'wb') as f: f.write(dec)
Запустив данный скрипт, мы получим еще одно bmp изображение без флага. Повторив процедуру на нем, получим итоговое изображение с флагом.

0x07 — wopr
We used our own computer hacking skills to "find" this AI on a military supercomputer. It does strongly resemble the classic 1983 movie WarGames. Perhaps life imitates art? If you can find the launch codes for us, we'll let you pass to the next challenge. We promise not to start a thermonuclear war.
В таске дано консольное приложение worp.exe. По всей видимости, для его решения необходимо подобрать некоторый код.

Анализ точки входа показывает, что это самораспаковывающийся архив. При запуске проверяется наличие переменной окружения _MEIPASS2. Если данной переменной нет, то создается временная директория, в которую распаковывается содержимое архива, и приложение запускается еще раз уже с заданной переменной окружения _MEIPASS2. Содержимое архива:
. ├── api-ms-win-core-console-l1-1-0.dll ├── ... ├── ... ├── api-ms-win-crt-utility-l1-1-0.dll ├── base_library.zip ├── _bz2.pyd ├── _ctypes.pyd ├── _hashlib.pyd ├── libcrypto-1_1.dll ├── libssl-1_1.dll ├── _lzma.pyd ├── pyexpat.pyd ├── python37.dll ├── select.pyd ├── _socket.pyd ├── _ssl.pyd ├── this │ ├── __init__.py │ └── key ├── ucrtbase.dll ├── unicodedata.pyd ├── VCRUNTIME140.dll └── wopr.exe.manifest 1 directory, 56 files
Судя по содержимому, мы имеем дело с запакованным в exe приложением на языке Python. В подтверждение этому в основном бинарнике можно найти динамический импорт соответствующих функций библиотеки Python: PyMarshal_ReadObjectFromString, PyEval_EvalCode и другие. Для дальнейшего анализа необходимо извлечь Python байт-код. Для этого сохраним содержимое архива из временной директории и пропишем в переменную окружения _MEIPASS2 путь до нее. Запустим основной бинарник в режиме отладки, поставив брейкпоинт на функцию PyMarshal_ReadObjectFromString. Данная функция принимает в качестве аргументов указатель на буфер с сериализованным Python-кодом и его длину. Сдампим содержимое буфера известной длины при каждом из вызовов. У меня получилось всего 2 вызова, при этом во втором сериализованный объект значительно больше, его и будем анализировать.
Достаточно простым способом анализа полученных данных является приведение их к формату .pyc файлов (скомпилированный байт-код Python) и декомпиляция с помощью uncompyle6. Для этого достаточно к полученным данным дописать 16-байтовый заголовок. В итоге у меня получился следующий файл:
00000000: 42 0d 0d 0a 00 00 00 00 de cd 57 5d 00 00 00 00 B.........W].... 00000010: e3 00 00 00 00 00 00 00 00 00 00 00 00 09 00 00 ................ 00000020: 00 40 00 00 00 73 3c 01 00 00 64 00 5a 00 64 01 .@...s<...d.Z.d. 00000030: 64 02 6c 01 5a 01 64 01 64 02 6c 02 5a 02 64 01 d.l.Z.d.d.l.Z.d.
Далее декомпилируем полученный файл с помощью uncompyle6:
uncompyle6 task.pyc > task.py
Если попробовать запустить декомпилированный файл, то мы получим исключение в строке BOUNCE = pkgutil.get_data('this', 'key'). Это легко исправить, просто назначив переменной BOUNCE содержимое файла key из архива. По��торно запустив скрипт, мы увидим только надпись LOADING.... По всей видимости, в таске используются какие-то техники, препятствующие декомпиляции. Приступим к анализу полученного Python-кода. В самом конце видим следующий цикл:
for i in range(256): try: print(lzma.decompress(fire(eye(__doc__.encode()), bytes([i]) + BOUNCE))) except Exception: pass
Можно понять, что функция print на самом деле переопределена как exec, а её аргумент зависит только от __doc__.encode() — текста в начале файла. В начале исполнения кода сохраним функцию print под другим именем и заменим ею print в блоке try-except. При запуске полученного скрипта нам снова ничего не выведется. Возможно, при декомпиляции __doc__ был записан неверно. Попробуем извлечь значение __doc__ напрямую из сериализованного кода следующим образом:
import marshal with open('pycode1', 'rb') as inp: data = inp.read() code = marshal.loads(data) doc = code.co_consts[0] with open('doc.txt', 'w') as outp: outp.write(doc)
Исполним скрипт еще раз, заменив содержимое __doc__. В результате, при определенном значении i, код успешно выведется на экран. Сохраним его в новом файле и проанализируем. В функции wrong можно обнаружить следующую строку:
trust = windll.kernel32.GetModuleHandleW(None)
С помощью нее получается указатель на текущий модуль в памяти, и далее происходят некоторые проверки на основе его содержимого. Я решил просто сдампить первые 0x100000 байт модуля из памяти во время обычного исполнения и переписал функцию wrong, чтобы данные для проверки считывались из файла дампа. В результате у меня получилось добиться такого же поведения скрипта, как и при запуске бинарника.
Последней частью таска является решение некоторой линейной системы уравнений. Для этого воспользуемся z3:
from z3 import * from stage2 import wrong xor = [212, 162, 242, 218, 101, 109, 50, 31, 125, 112, 249, 83, 55, 187, 131, 206] h = list(wrong()) h = [h[i] ^ xor[i] for i in range(16)] b = 16 * [None] x = [] for i in range(16): x.append(BitVec('x' + str(i), 32)) b[0] = x[2] ^ x[3] ^ x[4] ^ x[8] ^ x[11] ^ x[14] b[1] = x[0] ^ x[1] ^ x[8] ^ x[11] ^ x[13] ^ x[14] b[2] = x[0] ^ x[1] ^ x[2] ^ x[4] ^ x[5] ^ x[8] ^ x[9] ^ x[10] ^ x[13] ^ x[14] ^ x[15] b[3] = x[5] ^ x[6] ^ x[8] ^ x[9] ^ x[10] ^ x[12] ^ x[15] b[4] = x[1] ^ x[6] ^ x[7] ^ x[8] ^ x[12] ^ x[13] ^ x[14] ^ x[15] b[5] = x[0] ^ x[4] ^ x[7] ^ x[8] ^ x[9] ^ x[10] ^ x[12] ^ x[13] ^ x[14] ^ x[15] b[6] = x[1] ^ x[3] ^ x[7] ^ x[9] ^ x[10] ^ x[11] ^ x[12] ^ x[13] ^ x[15] b[7] = x[0] ^ x[1] ^ x[2] ^ x[3] ^ x[4] ^ x[8] ^ x[10] ^ x[11] ^ x[14] b[8] = x[1] ^ x[2] ^ x[3] ^ x[5] ^ x[9] ^ x[10] ^ x[11] ^ x[12] b[9] = x[6] ^ x[7] ^ x[8] ^ x[10] ^ x[11] ^ x[12] ^ x[15] b[10] = x[0] ^ x[3] ^ x[4] ^ x[7] ^ x[8] ^ x[10] ^ x[11] ^ x[12] ^ x[13] ^ x[14] ^ x[15] b[11] = x[0] ^ x[2] ^ x[4] ^ x[6] ^ x[13] b[12] = x[0] ^ x[3] ^ x[6] ^ x[7] ^ x[10] ^ x[12] ^ x[15] b[13] = x[2] ^ x[3] ^ x[4] ^ x[5] ^ x[6] ^ x[7] ^ x[11] ^ x[12] ^ x[13] ^ x[14] b[14] = x[1] ^ x[2] ^ x[3] ^ x[5] ^ x[7] ^ x[11] ^ x[13] ^ x[14] ^ x[15] b[15] = x[1] ^ x[3] ^ x[5] ^ x[9] ^ x[10] ^ x[11] ^ x[13] ^ x[15] solver = Solver() for i in range(16): solver.add(x[i] < 128) for i in range(16): solver.add(b[i] == h[i]) if solver.check() == sat: m = solver.model() print(bytes([m[i].as_long() for i in x])) else: print('unsat')
Запустив данный скрипт, мы получим нужный код: 5C0G7TY2LWI2YXMB

0x08 — snake
The Flare team is attempting to pivot to full-time twitch streaming video games instead of reverse engineering computer software all day. We wrote our own classic NES game to stream content that nobody else has seen and watch those subscribers flow in. It turned out to be too hard for us to beat so we gave up. See if you can beat it and capture the internet points that we failed to collect.
В таске дан NES-образ игры. Для запуска я решил использовать эмулятор FCEUX, т.к. он имеет достаточно богатые возможности отладки. Запустим игру, открыв редактор памяти.

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

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

0x09 — reloadered
This is a simple challenge, enter the password, receive the key. I hear that it caused problems when trying to analyze it with ghidra. Remember that valid flare-on flags will always end with @flare-on.com
В таске дан один файл reloaderd.exe, в который необходимо ввести ключ. На первый взгляд показалось, что решить его довольно просто, и это вызвало некоторые подозрения. Я разобрал алгоритм и выяснил, что под него может подходить множество ключей, и для каждого из них в ответе выводится XOR некоторой строки с ключом, и в конце добавляется @FLAG.com, что не соответствует формату флага.

В ходе дальнейшего анализа я обнаружил интересный фрагмент кода, заполненный операцией NOP. Но если посмотреть на это же место при запуске программы, поставив брейкпоинт на точку входа, можно увидеть код. Это было сделано с помощью определенным образом сформированной таблицы релокации. Сделаем снапшот отладчика, чтобы анализировать актуальный код программы. В ходе анализа выяснилось, что данный код в начале проверяет, запущено ли приложение на реальном аппаратном обеспечении. Если было определенно, что программа исполняется в виртуальной машине, код перезаписывается с помощью NOP, и управле��ие передается на фейковый чекер.
Если же приложение исполняется на реальном аппаратном обеспечении, то на стеке формируется некоторый буфер, к содержимому которого применяется операция XOR с ключом, введенным пользователем. Если итоговая строка содержит подстроку @flare-on.com, то ключ считается правильным. В итоге я написал следующий код для подбора ключа и получения флага:
flag = bytearray(b'D)6\n)\x0f\x05\x1be&\x10\x04+h0/\x003/\x05\x1a\x1f\x0f8\x02\x18B\x023\x1a(\x04*G?\x04&dfM\x107>(>w\x1c?~64*\x00') for i in range(0x539): for j in range(0x34): if (i % 3) == 0 or (i % 7) == 0: flag[j] ^= (i & 0xff) end = b'@flare-on.com' def xor(a, b): return bytes([i^j for i, j in zip(a, b)]) for i in range(len(flag)): print(i, xor(end, flag[i:])) print(xor(flag, b'3HeadedMonkey'*4))

0x0A — Mugatu
Hello,
I’m working an incident response case for Derek Zoolander. He clicked a link and was infected with MugatuWare! As a result, his new headshot compilation GIF was encrypted.
To secure an upcoming runway show, Derek needs this GIF decrypted; however, he refuses to pay the ransom.
We received an additional encrypted GIF from an anonymous informant. The informant told us the GIF should help in our decryption efforts, but we were unable to figure it out.
We’re reaching out to you, our best malware analyst, in hopes that you can reverse engineer this malware and decrypt Derek’s GIF.
I've included a directory full of files containing:
- MugatuWare malware
- Ransom note (GIFtToDerek.txt)
- Encrypted headshot GIF (best.gif.Mugatu)
- Encrypted informant GIF (the_key_to_success_0000.gif.Mugatu)
Thanks,
Roy
В таске даны следующие файлы:
- best.gif.Mugatu
- GIFtToDerek.txt
- Mugatuware.exe
- the_key_to_success_0000.gif.Mugatu
Судя по описанию, нам дан вредоносный файл, который шифрует GIF-изображения. Вероятно, к зашифрованным файлам добавляется расширение .Mugatu. Я начал анализ с файла Mugatuware.exe. Первое, что бросилось в глаза — странное использование импортируемых функций и несоответствие количества передаваемых в них аргументов. При запуске отладчика выяснилось, что функции действительно загружаются не так, как мы ожидаем.

Данную проблему можно решить следующим скриптом для IDA, запустив его в режиме отладки:
import ida_segment import ida_name import ida_bytes import ida_typeinf idata = ida_segment.get_segm_by_name('.idata') type_map = {} for addr in range(idata.start_ea, idata.end_ea, 4): name = ida_name.get_name(addr) if name: tp = ida_typeinf.idc_get_type(addr) if tp: type_map[name] = tp for addr in range(idata.start_ea, idata.end_ea, 4): imp = ida_bytes.get_dword(addr) if imp != 0: imp_name = ida_name.get_name(imp) name_part = imp_name.split('_')[-1] ida_name.set_name(addr, name_part + '_imp') if name_part in type_map: tp = type_map[name_part] ida_typeinf.apply_decl(addr, tp.replace('(', 'func(') + ';')
После применения скрипта код основной функции приобретает смысл:

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

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

Функция похожа на реализацию XTEA, однако имеется ошибка — ключ интерпретируется как массив BYTE, а не массив DWORD. Это сильно сокращает множество возможных ключей и позволяет произвести атаку перебором. Далее я реализовал функцию шифрования и дешифрования на Python:
def crypt(a, b, key): i = 0 for _ in range(32): t = (i + key[i & 3]) & 0xffffffff a = (a + (t ^ (b + ((b >> 5) ^ (b << 4))))) & 0xffffffff i = (0x100000000 + i - 0x61C88647) & 0xffffffff t = (i + key[(i >> 11) & 3]) & 0xffffffff b = (b + (t ^ (a + ((a >> 5) ^ (a << 4))))) & 0xffffffff return a, b def decrypt(a, b, key): i = 0xc6ef3720 for _ in range(32): t = (i + key[(i >> 11) & 3]) & 0xffffffff b = (0x100000000 + b - (t ^ (a + ((a >> 5) ^ (a << 4))))) & 0xffffffff i = (i + 0x61C88647) & 0xffffffff t = (i + key[i & 3]) & 0xffffffff a = (0x100000000 + a - (t ^ (b + ((b >> 5) ^ (b << 4))))) & 0xffffffff return a, b
Как оказалось, файл the_key_to_success_0000.gif.Mugatu необходим для проверки реализации алгоритма. Для его шифрования использовался ключ из нулевых байтов, что можно понять по названию. Дешифрованный файл выглядит следующим образом:

Можно заметить, что на изображении есть подсказка о реальном ключе, но я не сразу обратил на это внимание. Для перебора ключа алгоритм был переписан на C. При дешифровке проверяется заголовок GIF-изображения.
#include <stdio.h> #include <unistd.h> void decrypt(unsigned int * inp, unsigned int * outp, unsigned char * key) { unsigned int i = 0xc6ef3720; unsigned int a = inp[0]; unsigned int b = inp[1]; unsigned int t; for(int j = 0; j < 32; j++) { t = i + key[(i >> 11) & 3]; b -= t ^ (a + ((a >> 5) ^ (a << 4))); i += 0x61C88647; t = i + key[i & 3]; a -= t ^ (b + ((b >> 5) ^ (b << 4))); } outp[0] = a; outp[1] = b; } int main() { int fd = open("best.gif.Mugatu", 0); unsigned int inp[2]; unsigned int outp[2]; unsigned int key = 0; read(fd, inp, 8); close(fd); for(unsigned long long key = 0; key < 0x100000000; key++) { if((key & 0xffffff) == 0) { printf("%lf\n", ((double)key) / ((double)0x100000000) * 100.0); } decrypt(inp, outp, &key); if( ((char *)outp)[0] == 'G' && ((char *)outp)[1] == 'I' && ((char *)outp)[2] == 'F' && ((char *)outp)[5] == 'a') { printf("%#llx\n", key); } } }
В результате перебора было получено значение ключа 0xb1357331 и был расшифрован файл с флагом:

0x0B — vv_max
Hey, at least its not subleq.
В таске дан бинарник vv_max.exe, принимающий две строки в качестве аргументов. Он представляет из себя реализацию виртуальной машины с набором 256-битных регистров и операций над ними. Операции производятся с помощью инструкций расширения AVX2 процессора, таких как vpermd, vpslld и других. В результате дизассемблирования байт-кода виртуальной машины получилось следующее:
0000 clear_regs 0001 r0 = 393130324552414c46 0023 r1 = 3030303030303030303030303030303030303030303030303030303030303030 0045 r3 = 1a1b1b1b1a13111111111111111111151a1b1b1b1a1311111111111111111115 0067 r4 = 1010101010101010080408040201101010101010101010100804080402011010 0089 r5 = b9b9bfbf041310000000000000000000b9b9bfbf04131000 00ab r6 = 2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f 00cd r10 = 140014001400140014001400140014001400140014001400140014001400140 00ef r11 = 1100000011000000110000001100000011000000110000001100000011000 0111 r12 = ffffffff0c0d0e08090a040506000102ffffffff0c0d0e08090a040506000102 0133 r13 = ffffffffffffffff000000060000000500000004000000020000000100000000 0155 r16 = ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 0177 r17 = 6a09e667bb67ae853c6ef372a54ff53a510e527f9b05688c1f83d9ab5be0cd19 0199 r18 = 428a2f9871374491b5c0fbcfe9b5dba53956c25b59f111f1923f82a4ab1c5ed5 01bb r19 = 300000002000000010000000000000007000000060000000500000004 01dd r20 = 0 01ff r21 = 100000001000000010000000100000001000000010000000100000001 0221 r22 = 200000002000000020000000200000002000000020000000200000002 0243 r23 = 300000003000000030000000300000003000000030000000300000003 0265 r24 = 400000004000000040000000400000004000000040000000400000004 0287 r25 = 500000005000000050000000500000005000000050000000500000005 02a9 r26 = 600000006000000060000000600000006000000060000000600000006 02cb r27 = 700000007000000070000000700000007000000070000000700000007 02ed r20 = vpermd(r0, r20) 02f1 r21 = vpermd(r0, r21) 02f5 r22 = vpermd(r0, r22) 02f9 r23 = vpermd(r0, r23) 02fd r24 = vpermd(r0, r24) 0301 r25 = vpermd(r0, r25) 0305 r26 = vpermd(r0, r26) 0309 r27 = vpermd(r0, r27) 030d r7 = vpsrld(r1, 4) 0311 r28 = r20 ^ r21 0315 r28 = r28 ^ r22 0319 r28 = r28 ^ r23 031d r28 = r28 ^ r24 0321 r28 = r28 ^ r25 0325 r28 = r28 ^ r26 0329 r28 = r28 ^ r27 032d r7 = r7 & r6 0331 r29 = vpslld(r17, 7) 0335 r30 = vpsrld(r17, 25) 0339 r15 = r29 | r30 033d r8 = vpcmpeqb(r1, r6) 0341 r29 = vpslld(r17, 21) 0345 r30 = vpsrld(r17, 11) 0349 r29 = r29 | r30 034d r15 = r15 ^ r29 0351 r8 = vpcmpeqb(r1, r6) 0355 r29 = vpslld(r17, 26) 0359 r30 = vpsrld(r17, 6) 035d r29 = r29 | r30 0361 r15 = r15 ^ r29 0365 r29 = r20 ^ r16 0369 r30 = r20 & r18 036d r29 = r29 ^ r30 0371 r15 = add_d(r29, r15) 0375 r20 = add_d(r15, r0) 0379 r7 = add_b(r8, r7) 037d r29 = r20 ^ r28 0381 r17 = vpermd(r29, r19) 0385 r7 = vpshufb(r5, r7) 0389 r29 = vpslld(r17, 7) 038d r30 = vpsrld(r17, 25) 0391 r15 = r29 | r30 0395 r29 = vpslld(r17, 21) 0399 r30 = vpsrld(r17, 11) 039d r29 = r29 | r30 03a1 r15 = r15 ^ r29 03a5 r29 = vpslld(r17, 26) 03a9 r30 = vpsrld(r17, 6) 03ad r29 = r29 | r30 03b1 r15 = r15 ^ r29 03b5 r2 = add_b(r1, r7) 03b9 r29 = r21 ^ r16 03bd r30 = r21 & r18 03c1 r29 = r29 ^ r30 03c5 r15 = add_d(r29, r15) 03c9 r21 = add_d(r15, r0) 03cd r29 = r21 ^ r28 03d1 r17 = vpermd(r29, r19) 03d5 r20 = r20 ^ r21 03d9 r29 = vpslld(r17, 7) 03dd r30 = vpsrld(r17, 25) 03e1 r15 = r29 | r30 03e5 r29 = vpslld(r17, 21) 03e9 r30 = vpsrld(r17, 11) 03ed r29 = r29 | r30 03f1 r15 = r15 ^ r29 03f5 r29 = vpslld(r17, 26) 03f9 r30 = vpsrld(r17, 6) 03fd r29 = r29 | r30 0401 r15 = r15 ^ r29 0405 r7 = vpmaddubsw(r2, r10) 0409 r29 = r22 ^ r16 040d r30 = r22 & r18 0411 r29 = r29 ^ r30 0415 r15 = add_d(r29, r15) 0419 r22 = add_d(r15, r0) 041d r29 = r22 ^ r28 0421 r17 = vpermd(r29, r19) 0425 r20 = r20 ^ r22 0429 r29 = vpslld(r17, 7) 042d r30 = vpsrld(r17, 25) 0431 r15 = r29 | r30 0435 r29 = vpslld(r17, 21) 0439 r30 = vpsrld(r17, 11) 043d r29 = r29 | r30 0441 r15 = r15 ^ r29 0445 r29 = vpslld(r17, 26) 0449 r30 = vpsrld(r17, 6) 044d r29 = r29 | r30 0451 r15 = r15 ^ r29 0455 r2 = vpmaddwd(r7, r11) 0459 r29 = r23 ^ r16 045d r30 = r23 & r18 0461 r29 = r29 ^ r30 0465 r15 = add_d(r29, r15) 0469 r23 = add_d(r15, r0) 046d r29 = r23 ^ r28 0471 r17 = vpermd(r29, r19) 0475 r20 = r20 ^ r23 0479 r29 = vpslld(r17, 7) 047d r30 = vpsrld(r17, 25) 0481 r15 = r29 | r30 0485 r29 = vpslld(r17, 21) 0489 r30 = vpsrld(r17, 11) 048d r29 = r29 | r30 0491 r15 = r15 ^ r29 0495 r29 = vpslld(r17, 26) 0499 r30 = vpsrld(r17, 6) 049d r29 = r29 | r30 04a1 r15 = r15 ^ r29 04a5 r29 = r24 ^ r16 04a9 r30 = r24 & r18 04ad r29 = r29 ^ r30 04b1 r15 = add_d(r29, r15) 04b5 r24 = add_d(r15, r0) 04b9 r29 = r24 ^ r28 04bd r17 = vpermd(r29, r19) 04c1 r20 = r20 ^ r24 04c5 r29 = vpslld(r17, 7) 04c9 r30 = vpsrld(r17, 25) 04cd r15 = r29 | r30 04d1 r29 = vpslld(r17, 21) 04d5 r30 = vpsrld(r17, 11) 04d9 r29 = r29 | r30 04dd r15 = r15 ^ r29 04e1 r29 = vpslld(r17, 26) 04e5 r30 = vpsrld(r17, 6) 04e9 r29 = r29 | r30 04ed r15 = r15 ^ r29 04f1 r29 = r25 ^ r16 04f5 r30 = r25 & r18 04f9 r29 = r29 ^ r30 04fd r15 = add_d(r29, r15) 0501 r25 = add_d(r15, r0) 0505 r29 = r25 ^ r28 0509 r17 = vpermd(r29, r19) 050d r20 = r20 ^ r25 0511 r2 = vpshufb(r2, r12) 0515 r29 = vpslld(r17, 7) 0519 r30 = vpsrld(r17, 25) 051d r15 = r29 | r30 0521 r29 = vpslld(r17, 21) 0525 r30 = vpsrld(r17, 11) 0529 r29 = r29 | r30 052d r15 = r15 ^ r29 0531 r29 = vpslld(r17, 26) 0535 r30 = vpsrld(r17, 6) 0539 r29 = r29 | r30 053d r15 = r15 ^ r29 0541 r29 = r26 ^ r16 0545 r30 = r26 & r18 0549 r29 = r29 ^ r30 054d r15 = add_d(r29, r15) 0551 r26 = add_d(r15, r0) 0555 r29 = r26 ^ r28 0559 r17 = vpermd(r29, r19) 055d r20 = r20 ^ r26 0561 r29 = vpslld(r17, 7) 0565 r30 = vpsrld(r17, 25) 0569 r15 = r29 | r30 056d r29 = vpslld(r17, 21) 0571 r30 = vpsrld(r17, 11) 0575 r29 = r29 | r30 0579 r15 = r15 ^ r29 057d r29 = vpslld(r17, 26) 0581 r30 = vpsrld(r17, 6) 0585 r29 = r29 | r30 0589 r15 = r15 ^ r29 058d r2 = vpermd(r2, r13) 0591 r29 = r27 ^ r16 0595 r30 = r27 & r18 0599 r29 = r29 ^ r30 059d r15 = add_d(r29, r15) 05a1 r27 = add_d(r15, r0) 05a5 r29 = r27 ^ r28 05a9 r17 = vpermd(r29, r19) 05ad r20 = r20 ^ r27 05b1 r19 = ffffffffffffffffffffffffffffffffffffffffffffffff 05d3 r20 = r20 & r19 05d7 r31 = 2176620c3a5c0f290b583618734f07102e332623780e59150c05172d4b1b1e22
После завершения работы виртуальной машины значение в нулевом регистре сравнивается со строкой FLARE2019. Изначально это значение устанавливается равным первому аргументу программы, и из кода выше видно, что оно не меняется. Таким образом, первый аргумент программы должен быть FLARE2019. Также происходит сравнение r2 и r20. В ходе динамического анализа выяснилось, что значение r20 не зависит от второго аргумента программы. Влияние второго аргумента на r2 линейное — каждый байт входа влияет на 6 бит r2. Я решил просто перебирать каждый символ входных данных до тех пор, пока очередные 6 бит выхода не совпадут с нужным значением. Для автоматизации я использовал Frida:
# vvmax.py from __future__ import print_function import frida import string import hexdump def check(val): global gdata with open('vvmax.js', 'r') as f: script_src = f.read() pid = frida.spawn(['vv_max.exe', 'FLARE2019', val.ljust(32, 'a')]) session = frida.attach(pid) script = session.create_script(script_src) def handler(message, data): handler.data = data script.on('message', handler) script.load() frida.resume(pid) while not hasattr(handler, 'data'): pass session.detach() return handler.data alph = string.printable def to_bits(x): return ''.join(bin(ord(i))[2:].zfill(8) for i in x) target = to_bits('pp\xb2\xac\x01\xd2^a\n\xa7*\xa8\x08\x1c\x86\x1a\xe8E\xc8)\xb2\xf3\xa1\x1e\x00\x00\x00\x00\x00\x00\x00\x00') password = '' while len(password) != 32: for c in alph: data = to_bits(check(password + c)) i = 6*len(password + c) if data[:i] == target[:i]: password += c i += 1 break print() print('----->', `password`) print()
// vvmax.js var modules = Process.enumerateModules(); var base = modules[0].base; Interceptor.attach(base.add(0x1665), function() { var p = this.context.rdx.add(0x840); var res = p.readByteArray(32); send(null, res); });
С помощью данного способа и был получен флаг:

0x0C — help
You're my only hope FLARE-On player! One of our developers was hacked and we're not sure what they took. We managed to set up a packet capture on the network once we found out but they were definitely already on the system. I think whatever they installed must be buggy — it looks like they crashed our developer box. We saved off the dump file but I can't make heads or tails of it — PLEASE HELP!!!!!!
Вот мы и дошли до последнего таска. На этот раз нам дан RAM-дамп и дамп сетевого трафика. В трафике можно обнаружить интересные порты 4444, 6666, 7777 и 8888. Трафик в них, судя по всему, зашифрован, поэтому перейдем к RAM-дампу. Для анализа можно использовать утилиту volatility. При попытке определить профиль для работы с дампом встроенные средства volatility предложили мне Win10x64_15063, однако уже потом выяснилось, что правильнее было использовать Win7SP1x64, хотя это особо не повлияло на решение таска.
В ходе различн��х экспериментов в volatility в памяти были обнаружены интересные модули ядра:
$ volatility --profile Win7SP1x64 -f help.dmp modules Volatility Foundation Volatility Framework 2.6 Offset(V) Name Base Size File ------------------ -------------------- ------------------ ------------------ ---- 0xfffffa800183e890 ntoskrnl.exe 0xfffff80002a49000 0x5e7000 \SystemRoot\system32\ntoskrnl.exe ... 0xfffffa800428ff30 man.sys 0xfffff880033bc000 0xf000 \??\C:\Users\FLARE ON 2019\Desktop\man.sys
Извлечь данный модуль не удалось:
$ volatility --profile Win7SP1x64 -f help.dmp moddump --base 0xfffff880033bc000 -D drivers Volatility Foundation Volatility Framework 2.6 Module Base Module Name Result ------------------ -------------------- ------ 0xfffff880033bc000 man.sys Error: e_magic 0000 is not a valid DOS signature.
Видимо, заголовок модуля поврежден. Для решения проблемы можно воспользоваться командой volshell и сдампить модуль вручную.
$ volatility --profile Win7SP1x64 -f help.dmp volshell In [1]: db(0xfffff880033bc000) 0xfffff880033bc000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0xfffff880033bc010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0xfffff880033bc020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0xfffff880033bc030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0xfffff880033bc040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0xfffff880033bc050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0xfffff880033bc060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0xfffff880033bc070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ In [2]: db(0xfffff880033bc000 + 0x1100) 0xfffff880033bd100 01 48 8b 4c 24 20 48 8b 44 24 28 48 89 41 08 48 .H.L$.H.D$(H.A.H 0xfffff880033bd110 83 c4 18 c3 cc cc cc cc cc cc cc cc cc cc cc cc ................ 0xfffff880033bd120 48 89 4c 24 08 48 83 ec 38 48 8b 44 24 40 0f be H.L$.H..8H.D$@.. 0xfffff880033bd130 48 43 48 8b 44 24 40 0f be 40 42 83 c0 01 3b c8 HCH.D$@..@B...;. 0xfffff880033bd140 7e 27 45 33 c9 41 b8 15 5b 00 00 48 8d 15 de 44 ~'E3.A..[..H...D 0xfffff880033bd150 00 00 48 8d 0d 07 45 00 00 ff 15 71 4f 00 00 c7 ..H...E....qO... 0xfffff880033bd160 44 24 20 00 00 00 00 eb 08 c7 44 24 20 01 00 00 D$........D$.... 0xfffff880033bd170 00 48 8b 44 24 40 48 8b 80 b8 00 00 00 48 83 c4 .H.D$@H......H.. In [4]: man = addrspace().read(0xfffff880033bc000, 0xf000) In [5]: with open('man_writeup.sys', 'wb') as f: ...: f.write(man) ...:
Видно, что заголовок модуля стерт, поэтому функция moddump не сработала. Проанализируем модуль вручную. Идентифицировать методы модуля оказалось довольно просто. Процесс реверс-инженеринга модуля описывать не буду, опишу лишь результат, который был получен после длительного анализа.
В модуле активно используется шифрование строк на стеке с помощью RC4 со случайным ключом. Для упрощения преобразования подобных строк в начале был написан скрипт, который принимает строки из декомпилятора и расшифровывает сообщения.
Сам модуль представляет из себя нечто вроде прокси между зараженными процессами в user-space. Процессы заражаются инъектированием в них DLL-модулей с полезной нагрузкой. При инициализации один из процессов заражается встроенным в драйвер DLL-модулем (m.dll), отвечающим за принятие сообщений. Через него в дальнейшем могут быть загружены новые модули, информация о которых хранится в памяти драйвера в виде связного списка структур с необходимыми для взаимодействия данными. Для извлечения модулей из памяти нам интересны следующие поля структуры:
- Указатель на следующий элемент списка (смещение
+0x8) - Адрес структуры
_EPROCESSзараженного процесса (смещение+0x68) - Базовый адрес инъектированной библиотеки (смещение
+0x48) - Размер библиотеки (смещение
+0x58)
Инъектированные DLL-модули также не имеют заголовка и могут быть зашифрованы с помощью RC4, в качестве ключа при этом используются 0x2c-байт описанной выше структуры, начиная со смещения 0x48.
Для извлечения всех модулей в volatility можно воспользоваться следующим скриптом для volshell:
import struct from Crypto.Cipher import ARC4 head = 0xfffff880033c8158 krnl = addrspace() def u64(x): return struct.unpack('Q', x)[0] fd = u64(krnl.read(head, 8)) while True: proc_addr = u64(krnl.read(fd + 0x68, 8)) base = u64(krnl.read(fd + 0x48, 8)) key = krnl.read(fd + 0x48, 0x2c) sz = u64(krnl.read(fd + 0x58, 8)) fd = u64(krnl.read(fd, 8)) p = obj.Object('_EPROCESS', proc_addr, krnl) print p.ImageFileName.v(), hex(proc_addr), hex(base), hex(sz) proc_space = p.get_process_address_space() dump = proc_space.read(base, sz) if dump[:0x100] == '\x00' * 0x100: dump = ARC4.new(key).decrypt(dump) with open('proc_{:016x}'.format(base), 'wb') as f: f.write(dump) if fd == head: break
Начав анализ модулей, было обнаружено, что в них также используется шифрование строк на стеке с помощью RC4. Поэтому для упрощения был написан следующий скрипт для IDA, который расшифровывает строки, после чего переименовывает переменные в соответствии с их содержанием:
from __future__ import print_function import sys import re from idaapi import get_func, decompile, get_name_ea, auto_wait, BADADDR from idaapi import cot_call, cot_obj, init_hexrays_plugin, qexit import ida_typeinf import ida_lines def rc4(key, data): S = list(range(256)) j = 0 for i in list(range(256)): j = (j + S[i] + ord(key[i % len(key)])) % 256 S[i], S[j] = S[j], S[i] j = 0 y = 0 out = [] for char in data: j = (j + 1) % 256 y = (y + S[j]) % 256 S[j], S[y] = S[y], S[j] out.append(chr(ord(char) ^ S[(S[j] + S[y]) % 256])) return ''.join(out) def decrypt_stack_str_args(ea): func = get_func(ea) if func is None: return try: c_func = decompile(func) c_func.pseudocode except Exception as ex: return for citem in c_func.treeitems: citem = citem.to_specific_type if citem.is_expr() and\ citem.op == cot_call and\ citem.ea == ea: args = [] key = citem.a[0] key_len = citem.a[1] s = citem.a[2] s_len = citem.a[3] def get_var_idx(obj): while obj.opname != 'var': if obj.opname in ('ref', 'cast'): obj = obj.x else: raise Exception('can\'t find type') return obj.v.idx if key_len.opname != 'num' or s_len.opname != 'num': print('[!] can\'t get length: 0x{:08x}'.format(ea)) else: try: key_len_val = key_len.n._value s_len_val = s_len.n._value print('0x{:08x}'.format(ea), 'key_len =', key_len_val, ', s_len =', s_len_val) hx_view = idaapi.open_pseudocode(ea, -1) key_var_stkoff = hx_view.cfunc.get_lvars()[get_var_idx(key)].location.stkoff() s_var_stkoff = hx_view.cfunc.get_lvars()[get_var_idx(s)].location.stkoff() key_var = [v for v in hx_view.cfunc.get_lvars() if v.location.stkoff() == key_var_stkoff][0] tif = ida_typeinf.tinfo_t() ida_typeinf.parse_decl(tif, None, 'unsigned __int8 [{}];'.format(key_len_val), 0) hx_view.set_lvar_type(key_var, tif) s_var = [v for v in hx_view.cfunc.get_lvars() if v.location.stkoff() == s_var_stkoff][0] tif = ida_typeinf.tinfo_t() ida_typeinf.parse_decl(tif, None, 'unsigned __int8 [{}];'.format(s_len_val + 1), 0) hx_view.set_lvar_type(s_var, tif) key_var = [v for v in hx_view.cfunc.get_lvars() if v.location.stkoff() == key_var_stkoff][0] s_var = [v for v in hx_view.cfunc.get_lvars() if v.location.stkoff() == s_var_stkoff][0] key_regex = re.compile('{}\[(.+)\] = (.+);'.format(key_var.name)) s_regex = re.compile('{}\[(.+)\] = (.+);'.format(s_var.name)) key = bytearray(key_len_val) s = bytearray(s_len_val + 1) src = '\n'.join([ida_lines.tag_remove(i.line) for i in hx_view.cfunc.pseudocode]) for i, j in s_regex.findall(src): s[int(i)] = (0x100 + int(j)) & 0xff for i, j in key_regex.findall(src): key[int(i)] = (0x100 + int(j)) & 0xff key = ''.join(chr(i) for i in key) s = ''.join(chr(i) for i in s) result = rc4(key, s[:-1]) # unicode to ascii if set(ord(i) for i in result[1::2]) == {0}: result = 'wide_' + ''.join(result[0::2]) hx_view.rename_lvar(s_var, 's_' + result, True) except Exception as ex: print('[!] error: {}'.format(ex)) print('#### decryption helper script ####') xref_to = get_name_ea(BADADDR, 'decrypt_stack_str') xref_from = get_first_cref_to(xref_to) while xref_from != BADADDR: print('### 0x{:08x}'.format(xref_from)) decrypt_stack_str_args(xref_from) xref_from = get_next_cref_to(xref_to, xref_from)
Вот результат работы скрипта:

Далее был произведен анализ полученных модулей. Удалось определить следующую функциональность каждого из них:
m.dll— модуль, полученный из тела драйвера. Слушает порт4444на зараженной машине. Основная задача — прием команд и перенаправление их в основной драйвер для последующего выполнения;n.dll— модуль отвечает за отправку различных данных на хост192.168.1.243;c.dll— модуль отвечает за сжатие и шифрование данных с помощьюRC4. В качестве ключа шифрования используется имя пользователя;k.dll— модуль отвечает за логирование нажатий клавиш и получение текста из окон (keylogger);s.dll— модуль отвечает за создание скриншотов рабочего стола;f.dll— модуль отвечает за взаимодействие с файловой системой.
При анализе трафика также было обнаружено, что пакеты дополнительно зашифрованы с помощью XOR с длиной ключа 8. В случае входящих на порт 4444 пакетов с командами ключ легко удалось узнать, т.к. в исходном пакете было множество нулевых байтов. В отправляемых пакетах все оказалось еще проще: после недолгого анализа выяснилось, что данные в пакете отправляются дважды — в зашифрованном и в открытом виде. Видимо, это произошло из-за ошибки в реализации.
Во входящем трафике (порт 4444) был обнаружен еще один драйвер. Предположительно, он необходим для исполнения шел-кода в пространстве ядра. Для решения таска данный модуль не понадобился, поэтому его анализ проводить не буду. Также среди строк входящих пакетов были обнаружены следующие пути и названия файлов:
- keys.kdb
- C:\
- C:\keypass\keys.kdb
Предположительно, пакеты с этими строками предназначены для модуля f.dll: сначала происходит запрос на поиск файла keys.kdb, а затем запрос на его загрузку.
В трафике для порта 6666 было обнаружено два интересных пакета. Они были сжаты с помощью LZNT1 и зашифрованы с помощью RC4 и XOR. Ранее было замечено, что XOR-шифрование можно игнорировать, т.к. данные в пакете отправляются повторно не зашифрованными. Для расшифровки RC4 необходимо знать имя пользователя, которое было получено из RAM-дампа: FLARE ON 2019. Стоит отметить, что функция GetUserNameA, которая используется для получения имени пользователя в модуле, возвращает в качестве длины длину буфера для имени пользователя с учетом нуль-символа в конце строки, что стоит учитывать при использовании RC4. Для распаковки сжатых с помощью LZNT1 данных был написан следующий скрипт:
from ctypes import * nt = windll.ntdll for fname in ['input']: with open(fname, 'rb') as f: buf = f.read() dec_data = create_string_buffer(0x10000) final_size = c_ulong(0) status = nt.RtlDecompressBuffer( 0x102, # COMPRESSION_FORMAT_LZNT1 dec_data, # UncompressedBuffer 0x10000, # UncompressedBufferSize c_char_p(buf), # CompressedBuffer 0xFFFFFF, # CompressedBufferSize byref(final_size) # FinalUncompressedSize ) with open(fname + '.uncompressed', 'wb') as f: f.write(dec_data.raw[:final_size.value])
Для примера рассмотрим самый первый пакет для порта 6666. Изначально он выглядит так:
00000000: CC 69 94 FA 6A 37 18 29 CB 8D 87 EF 11 63 8E 73 .i..j7.).....c.s 00000010: FE AB 43 3B B3 94 28 4B 4D 19 00 00 00 4F DB C7 ..C;..(KM....O.. 00000020: F3 1E E4 13 15 34 8F 51 A9 2B C2 D7 C1 96 78 F7 .....4.Q.+....x. 00000030: 91 98
Если взять вторую половину пакета, получим следующее:
00000000: 19 00 00 00 4F DB C7 F3 1E E4 13 15 34 8F 51 A9 ....O.......4.Q. 00000010: 2B C2 D7 C1 96 78 F7 91 98 +....x...
Первые 4 байта пакета — это длина всего сообщения, в данном случае равная 25. Расшифруем оставшиеся данные:
00000000: 12 B0 00 43 3A 5C 6B 65 79 70 61 04 73 73 01 70 ...C:\keypa.ss.p 00000010: 73 2E 6B 64 62 s.kdb
Применим скрипт для декомпрессии и получим строку C:\keypass\keys.kdb. По всей видимости, это ответ на запрос поиска файла, о котором мы говорили выше. Во втором пакете для порта 6666 был обнаружен сам файл — это база для хранилища паролей KeePass.
В пакетах для порта 7777 были обнаружены скриншоты рабочего стола в формате BMP. Они были зашифрованы только с помощью XOR и, в данном случае, их всё же пришлось расшифровывать, т.к. повторно отправляемые не зашифрованные данные не вошли в пакет полностью. В результате преобразований был получен набор скриншотов, на которых видно, как пользователь использует KeePass.


В пакетах для порта 8888 были обнаружены данные модуля k.dll — сохраненные нажатия клавиш и названия окон.
C:\Windows\system32\cmd.exe nslookup googlecom ping 1722173110 nslookup soeblogcom nslookup fiosquatumgatefiosrouterhome C:\Windows\system32\cmd.exe Start Start menu Start menu chrome www.flare-on.com - Google Chrome tis encrypting something twice better than once Is encrypting something twice better than once? - Google Search - Google Chrome Start Start menu Start menu keeKeePass <DYN_TITLE> th1sisth33nd111 KeePass keys.kdb - KeePass Is encrypting something twice better than once? - Google Search - Google Chrome Start Start menu Start menu KeePass <DYN_TITLE> th1sisth33nd111 Open Database - keys.kdb KeePass Start Start menu Start menu KeePass Start menu Start menu Start menu KeePass <DYN_TITLE> th1sisth33nd111
После этого я попробовал использовать пароль th1sisth33nd111 для открытия базы хранилища паролей, но ничего не вышло. Также по скриншотам видно, что пароль должен быть длиннее. Дело в том, что keylogger не учитывает некоторые нюансы нажатия клавиш и логирует не всё. Например, в логе видно, что в команде ping не были учтены точки. Далее были предприняты попытки использовать hashcat для подбора пароля для базы KeePass с учетом мутаций, но ничего не вышло. Затем я попробовал поискать фрагменты полученного пароля в строках дампа и получил следующий результат:
$ strings help.dmp | grep -i '3nd!' !s_iS_th3_3Nd!!!
Дописав Th к полученной строке я получил доступ к хранилищу.

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

