Новости Русского мира

Hackquest 2017. Results & Writeups / Блог компании Digital Security / Хабрахабр

0 6


You received an invitation to join the Masonic lodge of reverse engineers. But it’s not so simple. You must complete the initiation and solve 4 tasks in one day. Good luck! You will find us in the telegram (@remasonry_bot)

Task #1

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

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

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

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

Task #2 packer

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

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


По этой информации не составляет труда написать упаковщик.

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

Программа для чтения:

task2.py

import struct

data = open('2017.zn', 'br').read()
position = 0
lst_position = 0

def read_dword():
    global data, position, lst_position
    value = struct.unpack('<L', data[position:position + 4])[0]
    lst_position = position
    position += 4
    return value

def read_raw(n):
    global data, position, lst_position
    value = data[position:position+n]
    lst_position = position
    position += n
    return value

magic = read_dword()
print('%04x' % lst_position, 'magic'.rjust(20, ' '), hex(magic))

version = read_dword()
print('%04x' % lst_position, 'version'.rjust(20, ' '), hex(version))

student_pack = read_raw(12)
print('%04x' % lst_position, 'student_pack'.rjust(20, ' '), student_pack)

FILE_END_OFFSET = read_dword()
print('%04x' % lst_position, 'FILE_END_OFFSET'.rjust(20, ' '), hex(FILE_END_OFFSET))

ZN3_SIZE = read_dword()
print('%04x' % lst_position, 'ZN3_SIZE'.rjust(20, ' '), hex(ZN3_SIZE))

ZN1_AND_ZN3_SIZE = read_dword()
print('%04x' % lst_position, 'ZN1_AND_ZN3_SIZE'.rjust(20, ' '), hex(ZN1_AND_ZN3_SIZE))

ZN2_OFFSET = read_dword()
print('%04x' % lst_position, 'ZN2_OFFSET'.rjust(20, ' '), hex(ZN2_OFFSET))

ZN0_OFFSET = read_dword()
print('%04x' % lst_position, 'ZN0_OFFSET'.rjust(20, ' '), hex(ZN0_OFFSET))

data = data[position:]

ZN3_OFFSET = 0
ZN3_SIZE = ZN3_SIZE

ZN1_OFFSET = ZN3_OFFSET + ZN3_SIZE
ZN1_SIZE = ZN1_AND_ZN3_SIZE - ZN3_SIZE

FD_OFFSET = ZN3_OFFSET + ZN1_AND_ZN3_SIZE
FD_SIZE = ZN2_OFFSET - ZN1_AND_ZN3_SIZE

#ZN2_OFFSET
ZN2_SIZE = ZN0_OFFSET - ZN2_OFFSET

#ZN0_OFFSET
ZN0_SIZE = FILE_END_OFFSET - ZN0_OFFSET

print('ZN3', hex(ZN3_OFFSET), hex(ZN3_SIZE))    # folder - folder descriptors
print('ZN1', hex(ZN1_OFFSET), hex(ZN1_SIZE))    # file - folder descriptors
print('FD', hex(FD_OFFSET), hex(FD_SIZE))       # DATA
print('ZN2', hex(ZN2_OFFSET), hex(ZN2_SIZE))    # filename descriptors
print('ZN0', hex(ZN0_OFFSET), hex(ZN0_SIZE))    # filedata descriptors

for i in range(ZN3_OFFSET, ZN3_OFFSET + ZN3_SIZE, 0xC):
    print(struct.unpack('<LLL', data[i:i+0xC]))

print('separator1')

for i in range(ZN0_OFFSET, ZN0_OFFSET + ZN0_SIZE, 0xC):
    print(struct.unpack('<LLL', data[i:i+0xC]))

print('separator2')

Программа для упаковки:

task2_packer.py

import struct
import os

magic = 0x30324e5a
version = 0x3731

FOLDERS = []
FOLDERS_mirr = []
FILES = []
FILENAMES = []
FILENAMES_real = []
FILEDATAINFO = []
FILEDATA = b''

TARGET_FOLDER = 'pack_me'

FOLDERS_mirr += [TARGET_FOLDER]
FILES_mirr = []

for dirname, dirnames, filenames in os.walk(TARGET_FOLDER):
    for subdirname in dirnames:
        FOLDERS += [(len(FOLDERS) + 1, FOLDERS_mirr.index(dirname), len(FILENAMES))]
        FOLDERS_mirr += [dirname + '\' + subdirname]
        FILENAMES += [(dirname + '\' + subdirname, 1)]

for dirname, dirnames, filenames in os.walk(TARGET_FOLDER):
    for flnm in filenames:
        FILES += [(len(FILES), FOLDERS_mirr.index(dirname), len(FILENAMES))]
        FILES_mirr += [dirname + '\' + flnm]
        FILENAMES += [(dirname + '\' + flnm, 0)]

for idx, x in enumerate(FILENAMES):
    fn = FILENAMES[idx][0].split('\')[-1]
    FILENAMES_real += [(idx, len(fn), fn.encode('utf-8'))]

for idx, x in enumerate(FILENAMES):
    if x[1] == 0:
        fdata = open(x[0], 'rb').read()
        if fdata not in FILEDATA:
            FILEDATA += fdata
        offset = FILEDATA.index(fdata)
        fsize = len(fdata)
        FILEDATAINFO += [(FILES_mirr.index(x[0]), offset, fsize)]

zn_new_offset = [0 for i in range(5)]
zn_new_size = [0 for i in range(5)]

DATA = b''

for x in FOLDERS:
    DATA += struct.pack('<LLL', *x)

zn_new_size[3] = len(DATA)
zn_new_offset[1] = len(DATA)

for x in FILES:
    DATA += struct.pack('<LLL', *x)

zn_new_size[1] = len(DATA) - zn_new_offset[1]
zn_new_offset[4] = len(DATA)

DATA += FILEDATA

zn_new_size[4] = len(DATA) - zn_new_offset[4]
zn_new_offset[2] = len(DATA)

for x in FILENAMES_real:
    DATA += struct.pack('<LL', x[0], len(x[2])) + x[2]

zn_new_size[2] = len(DATA) - zn_new_offset[2]
zn_new_offset[0] = len(DATA)

for x in FILEDATAINFO:
    DATA += struct.pack('<LLL', x[0], x[1], x[2])

zn_new_size[0] = len(DATA) - zn_new_offset[0]

print([hex(d) for d in zn_new_offset], [hex(d) for d in zn_new_size])

FILE_END_OFFSET = zn_new_offset[0] + zn_new_size[0]
ZN3_SIZE = zn_new_size[3]
ZN1_AND_ZN3_SIZE = zn_new_size[3] + zn_new_size[1]
ZN2_OFFSET = zn_new_offset[2]
ZN0_OFFSET = zn_new_offset[0]
HEADER = struct.pack('<LL', magic, version) + TARGET_FOLDER.encode('utf-8') + b'x00' + struct.pack('<LLLLL', FILE_END_OFFSET, ZN3_SIZE, ZN1_AND_ZN3_SIZE, ZN2_OFFSET, ZN0_OFFSET)
open('new.zn', 'wb').write(HEADER + DATA)

Task #3 random.apk

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

Разберем его тоже.

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

Восстановим алгоритм проверки.

code

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

Обращаем внимание, что гамма не зависит от хеша, так что её можно считать константной (зависит только от строки и колонки).

Её можно вычислить заранее и сохранить в файл. Получится таблица 500×32 элементов.

Ниже, для примера, приведено несколько первых строк таблицы.

Очевидно, десятичное число после xor с символом хеша может быть длины 1, 2 или 3.

Зная, что символы хеша могут принимать только значения из диапазона [a-f0-9], можно однозначно определить длину результирующего десятичного числа на некоторых позициях, в то время как на других свести в точности к двум вариантам (подтверждено практически).

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

На данном этапе контрольное число 40762 – это сумма значений всей таблицы.

Понятно, что можно беспрепятственно вычесть из контрольного числа минимум каждого элемента (который является списком) таблицы, не забыв при этом уменьшить и соответствующие значения самого списка. Теперь контрольное число это 3449. А таблица будет состоять из элементов двух типов: [0] и [0, 1].

Рассмотрим один столбец такой таблицы. Попробуем представить все возможные комбинации из элементов [0, 1]. Ясно, что существуют невозможные варианты, которые нельзя получить, зафиксировав букву хеша. Поэтому, во время поиска решения, необходимо использовать только допустимые расстановки 0 или 1.

Зафиксируем букву хеша в некоторой позиции (от 0 до 31, включительно), так весь столбец таблицы, соответствующий этой позиции, примет однозначные значения. Просуммируем столбец и запишем в список (без повторений). Повторим эту операцию для всех остальных возможных символов хеша.

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

[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 файлом исследуемой программы.

Прогнав через декомпилятор, не получаем ничего хорошего, код обфусцирован.

Попробуем подглядеть внутренние состояние в момент проверки ключа.

Напишем программу(dll), которая перехватит PyObject_RichCompare и будет выводить в консоль (stderr) переданные её параметры.

Code:

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.

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

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

nooyhtortroornehopetrotnnrenohtopyeohnoeyonyrherpo teptooeonoohptppeoprtphprthrhpnnyyprrpnhepoportppr enppeoernnehtynrotyynerttoyeeteepepohhoyrptnhponro tpehooeonptnophoyphnp

Архив со всеми используемыми файлами (task2.py, task2_packer.py, re_task.pyc) ZN2017.7z‎.



Source link

Comments
Loading...