R0 CREW

[ZNHQ2017] Day#4

Zero Nights HackQuest 2017 Day #4

Task #1 welcome

Имеется PE32 exe файл. Строки:

Подаем user_id и какой-нибудь пароль, смотрим, что программа с ними делает.

Подменим содержимое bytesUserId на “I’m ready!!!” (не забудем, обновить размер буфера для DIV EBX) и пропатчим немного программу.

После выполнения цикла szSalt содержит наш пароль.

Task #2 packer

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

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

Task#3 random.apk

Задание, по словам организатора, было с багом, так что всем просто сообщали ответ.
Разберем его тоже.

Имеется apk файл, который ожидает ввода некоторой строки.
Восстановим алгоритм проверки.

import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class Main {
    //private static final String base64chars = "A2CTEFGHnJKLMNsPQ7SDlVvXYZabcdefghijkUmIopqrOtuWwxyz01B3456R89+/";

    private static String CalcMD5(String paramString) throws NoSuchAlgorithmException {
        MessageDigest localMessageDigest = MessageDigest.getInstance("MD5");
        localMessageDigest.update(paramString.getBytes(), 0, paramString.length());
        return new BigInteger(1, localMessageDigest.digest()).toString(16);
    }

    //

    private static long Random(long paramLong) {
        return (0xFFFFFFF & 11L + 252149039L * paramLong) >> 8;
    }

    private static long RandomSkip50(long paramLong) {
        for (int i = 0; i < 50; i = i + 1) {
            paramLong = Random(paramLong);
        }
        return paramLong;
    }

    private static String RandomGetString(String paramString, long paramLong) {
        String str = "";
        for (int i = 0; i < 32; i++) {
            paramLong = Random(paramLong) & 0xFF;
            str += paramLong ^ paramString.charAt(i);
        }
        return str;
    }

    public static void main(String[] args) throws NoSuchAlgorithmException {
        String str = "INPUT_HASH_HERE"; // CalcMD5("SuperAndroidChallenge"); // baaee25a694971ac1e6dde4b2e8b1386
        String[] arrayOfString = new String[500];
        for (int i = 0; i < arrayOfString.length; i++) {
            arrayOfString[i] = RandomGetString(str, RandomSkip50(i));

        }
        int j = 0;
        for (int k = 0; k < arrayOfString.length; k++) {
            j += arrayOfString[k].length();
        }
        if (j == 40762) {
            System.out.println("OK");
        }
    }
}

В первую очередь попытаемся найти хеш, который пройдет контрольную проверку.
Алгоритм проверки сводится к суммированию длины некоторого набора строк (500 штук).
Каждая строка генерируется как конкатенация тридцати двух десятичных чисел, каждое из которых это xor гаммы и одного символа хеша (в строковом представлении, нижний регистр).
Обращаем внимание, что гамма не зависит от хеша, так что её можно считать константной (зависит только от строки и колонки).
Её можно вычислить заранее и сохранить в файл. Получится таблица 500x32 элементов.
Ниже, для примера, приведено несколько первых строк таблицы.

Очевидно, десятичное число после xor с символом хеша может быть длины 1, 2 или 3.
Зная, что символы хеша могут принимать только значения из диапазона [a-f0-9], можно однозначно определить длину результирующего десятичного числа на некоторых позициях, в то время как на других свести в точности к двум вариантам (подтверждено практически).
Преобразовываем таблицу так, чтобы её элементами были списки возможной длины результирующего десятичного числа.
На данном этапе контрольное число 40762 – это сумма значений всей таблицы.
Понятно, что можно беспрепятственно вычесть из контрольного числа минимум каждого элемента (который является списком) таблицы, не забыв при этом уменьшить и соответствующие значения самого списка. Теперь контрольное число это 3449. А таблица будет состоять из элементов двух типов: [0] и [0, 1].
Рассмотрим один столбец такой таблицы. Попробуем представить все возможные комбинации из элементов [0, 1]. Ясно, что существуют невозможные варианты, которые нельзя получить, зафиксировав букву хеша. Поэтому, во время поиска решения, необходимо использовать только допустимые расстановки 0 или 1.
Зафиксируем букву хеша в некоторой позиции (от 0 до 31, включительно), так весь столбец таблицы, соответствующий этой позиции, примет однозначные значения. Просуммируем столбец и запишем в список (без повторений). Повторим эту операцию для всех остальных возможных символов хеша.
Теперь для каждой позиции у нас есть массив возможных вкладов колонки в общую сумму.

Hide

[100, 103, 94, 102, 97, 98, 95]
[109, 123, 108, 115, 114, 111, 112]
[103, 116, 122, 114, 95, 93, 97, 112, 119]
[121, 100, 107, 125, 105, 124, 90, 104, 120]
[100, 101, 103, 87, 105, 102, 91, 95]
[121, 109, 123, 126, 110, 102, 118, 90, 119]
[100, 101, 128, 82, 119, 118, 88, 134, 130]
[121, 127, 106, 122, 97, 112, 88, 119]
[103, 116, 92, 111, 112, 110, 88]
[128, 81, 80, 92, 90, 104, 91, 131, 95]
[123, 127, 126, 93, 102, 104, 124, 98]
[117, 133, 100, 123, 108, 142, 96, 119]
[117, 109, 133, 103, 93, 112, 110, 88, 134]
[75, 71, 101, 129, 116, 110, 102, 91, 130]
[98, 87, 94, 93, 148, 86, 149]
[100, 158, 103, 90, 88, 83, 95]
[87, 84, 86, 90, 88, 89, 145]
[100, 101, 103, 150, 93, 151, 89, 95]
[117, 108, 105, 166, 165, 111, 93, 96]
[117, 100, 107, 150, 114, 90, 110, 83, 149]
[117, 99, 116, 114, 112, 152, 88, 96, 154]
[108, 101, 103, 99, 153, 111, 102, 156]
[117, 176, 101, 99, 80, 122, 96, 167]
[77, 117, 140, 138, 102, 86, 118]
[101, 99, 158, 159, 104, 102, 90, 96, 95]
[109, 101, 115, 84, 111, 143, 90]
[107, 82, 168, 161, 102, 90, 96, 98, 95]
[71, 108, 158, 94, 85, 86, 162, 88, 106]
[75, 71, 80, 94, 92, 93, 148, 91]
[100, 99, 150, 93, 155, 83, 95]
[133, 109, 142, 87, 113, 94, 93, 85, 91]
[74, 135, 76, 80, 94, 92, 88, 149]

Решение задачи сводится к нахождению всех комбинаций «по одному элементу из каждого списка», сумма которых равна контрольному числу 3449, что по сути является частным случаем задачи об укладке ранца.
Для решения воспользуемся Z3 (SMT решатель).

from z3 import *
STUFF = [[100, 95, 98, 94, 102, 97, 103], … [88, 74, 76, 92, 80, 149, 94, 135]]
s = Solver()
chars = []
for i in range(len(STUFF)):
   chxr = Int('c_%d' % i)
   s.add(Or([chxr == STUFF[i][j] for j in range(len(STUFF[i]))]))
   chars += [chxr]
s.add(Sum(*chars) == 3449)
while s.check() == sat:
  mod = s.model()
  d = [mod[Int('c_%d' % i)] for i in range(32)]
  print(d) 
  s.add(Not(And([Int(str(xx)) == mod[xx] for xx in mod])) )

Немного подождав, получим не меньше 18к вариантов решений.
Однако каждое из таких решений может дать далеко не одно подходящее значение хеша.
Так, например, решение:
[100, 123, 122, 125, 105, 126, 134, 127, 116, 131, 127, 117, 134, 116, 149, 103, 84, 89, 93, 83, 88, 99, 80, 77, 90, 143, 107, 71, 148, 83, 85, 74]
распадается на всевозможные значения хеша, удовлетворяющие регулярному выражению:
^[6789][89][45][01][89][89][89][89][0123][f][01][23][de][01][de][a][23][bc][89][bc][bc][bc][89][bc][bc][def][a][89][def][89][bc][45]$

Примеры:
684088880f02d0da2b8bbb8ccfa9e9c4
684088880f02d0da2b8bbb8ccfa9e9c5
684088880f02d0da2b8bbb8ccfa9f8b4
684088880f02d0da2b8bbb8ccfa9f8b5
684088880f02d0da2b8bbb8ccfa9f8c4
684088880f02d0da2b8bbb8ccfa9f8c5
684088880f02d0da2b8bbb8ccfa9f9b4

Итого: Задача имеет огромное множество решений, вопрос только в обращении md5 хеша.

Task#4 pythonre

Задание представляет собой собранную в exe программу на языке Python.
Извлекаем pyc файл.
Процесс перезапускает себя, так что перехватим CreateProcessW, поправим CreationFlags, приаттачимся.

По строкам найдем функцию, в которой можно перехватить буффер с pyc файлом исследуемой программы.

Файл re_task.pyc во вложении.
Прогнав через декомпилятор не получаем ничего хорошего, код обфусцирован.
Попробуем подглядеть внутренние состояние в момент проверки ключа.
Напишем программу(dll), которая перехватит PyObject_RichCompare и будет выводить в консоль (stderr) переданные её параметры.

typedef void PyObject;
typedef void (CDECL * _PyObject_Dump)(PyObject *o1);
_PyObject_Dump PyObject_Dump;

PHOOK hook1;
PyObject* CDECL xPyObject_RichCompare(PyObject *o1, PyObject *o2, int opid) {
   PyObject* result = ((PyObject*(CDECL*)(PyObject*,PyObject*,int))hook1->original)(o1, o2, opid);
   PyObject_Dump(o1);
   PyObject_Dump(o2);
   return result;
}

typedef PyObject*(CDECL * PyObject_RichCompare)(PyObject *o1, PyObject *o2, int opid);
void hackFunctions() {
   PyObject_Dump = (_PyObject_Dump)GetProcAddress(GetModuleHandleA("python27.dll"), "_PyObject_Dump");
   hook1 = HookFunction(GetProcAddress(GetModuleHandleA("python27.dll"), "PyObject_RichCompare"), xPyObject_RichCompare);
}

BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID lpReserved) {
   switch (reason) {
   case DLL_PROCESS_ATTACH:
      hackFunctions();
   }
   return TRUE;
}

Заинжектим её в процесс re_task.exe и посмотрим несколько первых и последних записей.

Сразу обращаем внимание на регулярное выражение (которое очевидно является преобразованным содержимым data.txt, так как размер в точности совпадает).

Ну и перед самым выводом сообщения о неудаче, видим вызов re.sub и сравнение с None.

Такое регулярное выражение уже где-то встречалось (LabyREnth 2016), но там оно было несколько проще и мой солвер оттуда не подходил для решения такой системы. Я решил посмотреть чужие решения и нашел в точности такое-же задание на PlaidCTF 2015.

http://irq5.io/2016/08/17/labyrenth-2016-write-up-regex/
https://github.com/ctfs/write-ups-2015/tree/master/plaidctf-2015/reversing/re-gex

Берем любой готовый солвер, немного подправляем и скармливаем нашу регулярку.

Спустя несколько часов получаем ответ.

nooyhtortroornehopetrotnnrenohtopyeohnoeyonyrherpoteptooeonoohptppeoprtphprthrhpnnyyprrpnhepoportpprenppeoernnehtynrotyynerttoyeeteepepohhoyrptnhponrotpehooeonptnophoyphnp