Разбор нескольких заданий из CTF по реверсу

Разбор нескольких заданий из CTF по реверсу

https://t.me/w2hack

Into

Существует два варианта проведения соревнований. В нашем случае мы остановимся на

  • reverse — исследование программ без исходного кода (реверс-инжиниринг)

Обратная разработка — исследование некоторого устройства или программы, а также документации на него с целью понять принцип его работы; например, чтобы обнаружить недокументированные возможности (в том числе «программные закладки»), сделать изменение, или воспроизвести устройство, программу или иной объект с аналогичными функциями, но без копирования как такового. © Wikipedia

Эта группа категорий схожа по своей сути. Различия небольшие: reverse - обычно "раскрутка" алгоритмов, использованных в программе для получения флага; pwn - взлом программ и устройств теми же методами; binary - реверс программ под Linux; exploit, vuln - поиск и эксплуатация уязвимостей на удаленных сервисах (иногда имея в наличии исходный код, иногда сам сервис, иногда без него).

Greetz :)

CTFtime.org, Хабр, ][акер, семплы

1.Reverse_50. Console version 1.337

A.U.R.O.R.A.: Lieutenant, you are standing in the Alpha base in front of the SCI430422 mainframe art console where its sixty-four LED lights are blinking in hypnotic patterns. As you know, this system is renowned for its top-notch security measures. Only the most expert or resourceful hackers are able to break in — and you are definitely one of them.

Решение:

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

image

Что же делать? Открываем файл в отладчике OllyDbg и находим строчку «Please enter password:» — в основном окне пролистаем листинг вверх, до адреса 004010F9:

image

Как вы можете видеть, проверка введенного пароля осуществляется в функции, расположенной по адресу 00401000. Попробуем поставить на этот адрес брейкпоинт (клавиша F2 в OllyDbg), затем нажмем F9 (продолжить исполнение) и для примера введем какую-нибудь строку в окне программы:

image

Нажимаем «Enter», и происходит срабатывание брейкпоинта. Далее переходим в OllyDbg и нажимаем клавишу F7, чтобы перейти внутрь функции по адресу 00401000:

image

Сразу можно заметить, что по адресу 004010CF производится вызов функции strcmp() из стандартной библиотеки C. Эта функция сравнивает две строки и возвращает 0, если эти строки одинаковы. Ставим брейкпоинт на вызов функции (клавиша F2), нажимаем F9 (продолжить исполнение) и смотрим, какая еще строка, кроме введенной нами «password123», будет в нее передаваться:

image

В окне стека (и в окне регистров) мы видим, что строка s1 равна «ctfzone{l33t_haxx0r_is_you!!1}» (без кавычек).

image

Вот и флаг!

Ответ: ctfzone{l33t_haxx0r_is_you!!1}

2.Reverse_100. The Doors of Dorun

*A.U.R.O.R.A.: Lieutenant, your co-pilot was abducted by aliens and put into prison. They are out hunting now and it’s your chance to set him free! He is held behind the Doors, the jambs invisible to the eye, and matched so perfectly with the metal bulkhead that when closed the Doors could not be seen.

The inscription on the archivolt read:

"The Doors of Dorun, Lord of Omega. Speak, friend, and enter. I, Norvy, made them. Calabrimbor of Alpha Centauri drew these signs".

But be careful and hurry up. They can be back any moment.*

Решение:

В этом задании нам нужно было подобрать пароль к вратам, за которыми держали в плену нашего второго пилота. В первую очередь мы запускаем CrackMe, и на экране появляется следующее окно:

image

Попробуем ввести любое слово и нажать «Try», но пароль не подходит, и мы видим такое сообщение:

image

Сам по себе CrackMe представляет собой 64-битный исполняемый файл PE формата. Откроем его в IdaPro и попробуем найти строчку «the door is still closed!»:

image

На эту строчку существует только одна перекрестная ссылка:

image

Вот функция, в которой IdaPro нашла обращения к этой строчке:

image

Здесь мы также видим зашифрованный флаг (легко убедится, что функция sub_140001160 занимается дешифровкой) и функцию, определяющую правильность пароля: «sub_1400012C0». При помощи GetDlgItemTextW в эту функцию передается строка, введенная в поле для ввода пароля. Проанализируем эту функцию:

image

Здесь прослеживается цикл и два массива из пяти элементов. Также происходит проверка длины введенного пароля:

image

Проанализировав эту функцию, мы видим, что пароль из четырёх символов в кодировке UTF-16 (кодировка для WideChar в Windows) состоит из двух чисел c размером DWORD. Далее мы видим, что остатки от деления этих чисел на числа ((1 << (1 << i)) + 1) сравниваются с захардкоженными значениями.

Можно заметить, что ((1 << (1 << i)) + 1) = 2^(2^i) + 1, и что это числа Ферма: 3, 5, 17, 257, 65537. Алгоритм проверки пароля далее можно свести к двум системам сравнений:

X1 % 3 = 0
X1 % 5 = 0
X1 % 17 = 1
X1 % 257 = 241
X1 % 65537 = 995
X2 % 3 = 1
X2 % 5 = 4
X2 % 17 = 6
X2 % 257 = 104
X2 % 65537 = 413


Восстановить исходные числа нам поможет Китайская Теорема об Остатках. В интернете можно найти решатели таких сравнений:

image
image

Итак, мы получили два числа, которые теперь необходимо преобразовать в строку UTF16. Для этого можно использовать Python (при этом не забываем про обратный порядок байт):


image

Теперь осталось проверить полученный результат. Введем эту строку в окно для ввода пароля.

image
image

Вот и все! Мы открыли врата.

Ответ: ctfzone{ch1n4_t0wn}

3. Shadelt900

Задание Shadelt900, так же как и три предыдущих, было частью программы PHDays IV CTF Quals, прошедших в январе 2014 года. Команды должны были расшифровать изображение под названием 'derrorim_enc.bmp'. Было известно средство, примененное для его зашифрования, — оно как раз и называется Shadelt9000.exe, но декриптор обнаружить не удалось. Вот это изображение:

image

При ближайшем рассмотрении файла Shadelt9000.exe становится ясно, что приложение использует OpenGL. Также есть копирайт inflate 1.2.8 Copyright 1995-2013 Mark Adler, указывающий на то, что в программе используется популярная библиотека компрессии zlib.

Если в дизассемблере посмотреть, откуда идут обращения к функциям zlib, можно довольно быстро найти вот такой кусок кода:

image

По адресам 0x47F660 и 0x47F7B8 расположены массивы данных, упакованные zlib. Распакуем их: 

from zlib import decompress as unZ
base = 0x47C000 - 0x7AE00 # data section base
ab=open("ShadeIt9000.exe", "rb").read()
open("1.txt", "w").write(unZ(ab[0x47F660-base:],-15))
open("2.txt", "w").write(unZ(ab[0x47F7B8-base:],-15))

После распаковки файл 1.txt содержит пиксельный шейдер:

#version 330
uniform sampler2D u_texture;
uniform sampler2D u_gamma;
varying vec4 texCoord0;
varying vec3 v_param;
uint func(vec3 co){
    return uint(fract(sin(dot(co ,vec3(17.1684, 94.3498, 124.9547))) * 68431.4621) * 255.);
}
uvec3 rol(uvec3 value, int shift) {
    return (value << shift) | (value >> (8 - shift));
}
const uvec3 m = uvec3(0xff);
void main()
{
 uvec3 t = uvec3(texture2D(u_texture, vec2(texCoord0)).rgb * 0xff) & m;
 uvec3 g = uvec3(texture2D(u_gamma, vec2(texCoord0)).rgb * 0xff) & m;
 int s = int(mod(func(v_param), 8));
 t = rol(t, s);
 vec3 c = vec3((t ^ g) & m) / 0xff;
 gl_FragColor = vec4(c, 1.);
}

Файл 2.txt содержит вершинный шейдер:

attribute vec3 a_param;
varying vec4 texCoord0;
varying vec3 v_param;
void main(void)
{
 gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
 texCoord0 = gl_MultiTexCoord0;
 v_param = a_param;
}

Главная информация о пиксельном шейдере выделена красным:

image

В переменной t оказывается текущий элемент обрабатываемой текстуры (входного файла),

а в переменной g — текущий элемент гаммы (сгенерированной псевдослучайным образом).

В переменной s мы видим некоторое значение, используемое позже для циклического сдвига s.

Выходное значение фактически вычисляется как

(rol(t,s) ^ g)

Причем если запускать программу несколько раз с одним и тем же входным файлом, то для каждого элемента значение g будет меняться от запуска к запуску, а t и s будут оставаться одними и теми же.

Найдем, как генерируется гамма:

unsigned char *pbGamma = malloc(cbGamma);
srand(time(0));
for (i = 0; i < cbGamma; i++) {
  pbGamma[i] = rand();
}

Видно, что она зависит от текущего времени.

Из исходного архива можно узнать, что файл derrorim_enc.bmp создан 21.01.2014 в 18:37:52.

Получаем значение, которое в тот момент вернула бы функция time():

>>> import time
>>> print hex(int(time.mktime((2014,1,21,  18,37,52, 0,0,0))))

0x52de8640

Теперь копируем файл ShadeIt9000.exe в ShadeIt9000_f.exe и исправляем его.

По смещению 00015557 надо байты

E8 A5 31 01 00

заменить на

B8 40 86 DE 52

Это эквивалентно замене

call _time на mov eax,52de8640h.

Таким образом мы получили версию ShadeIt9000_f, которая будет всегда шифровать с той же гаммой, какая была в момент зашифрования интересующего нас файла.

Теперь нужно подготовить значения, которые помогут расшифровать изображение:

import os
bmp=open("derrorim_enc.bmp", "rb").read()
hdr = bmp[:0x36]
abData = bytearray(bmp[0x36:])
cbBody = len(bmp) - len(hdr)
open("00.bmp", "wb").write(hdr + '\0'*cbBody)
open("XX.bmp", "wb").write(hdr + '\2'*cbBody)
os.system("ShadeIt9000_f.exe 00.bmp")
os.system("ShadeIt9000_f.exe XX.bmp")


В файле 00_enc.bmp окажется результат зашифрования картинки, состоящий из нулевых байтов. Это и будет гамма в чистом виде.

В файле XX_enc.bmp окажется результат зашифрования картинки, состоящий из байтов со значением 2. Это поможет нам узнать, на сколько битов циклически сдвигался каждый байт.

Наконец, расшифровыванием Shadelt9000:

def rol(v,i): return (((v<<i) & 0xFF) | ((v>>(8-i)) & 0xFF))
def ror(v,i): return (((v>>i) & 0xFF) | ((v<<(8-i)) & 0xFF))
dRot = {rol(1,i):i for i in xrange(8)}
bmp=open("derrorim_enc.bmp", "rb").read()
hdr = bmp[:0x36]
abData = bytearray(bmp[0x36:])
abGamma = bytearray(open("00_enc.bmp", "rb").read()[0x36:])
abRot = bytearray(open("XX_enc.bmp", "rb").read()[0x36:])
for i,b in enumerate(abGamma): abRot[i] = dRot[abRot[i] ^ b]
for i,b in enumerate(abGamma): abData[i] = ror(abData[i] ^ b, abRot[i])
open("derrorim.bmp", "wb").write(hdr + str(abData))


получаем:

image


Выше был описан верный, но не самый эффективный путь решения задания. Есть способ короче.

Сразу за вершинным шейдером по адресам 0x47F848 и 0x47F9A0 лежит упакованный zlib-код пиксельного и вершинного шейдера для выполнения обратного преобразования. Возможно, он был случайно забыт разработчиком задания. А может и оставлен намеренно.

Коды вершинного шейдера для зашифрования и расшифрования идентичны, так что трогать их не имеет смысла. А что будет, если подменить пиксельный шейдер?

Копируем ShadeIt9000_f.exe в ShadeIt9000_d.exe и исправляем его:

00015775: 60 F6 ==> 48 F8

Затем запускаем ShadeIt9000_d.exe derrorim_enc.bmp. И получаем на выходе расшифрованный файл derrorim_enc_enc.bmp, который (за исключением мелких артефактов) совпадает с тем, который мы расшифровали скриптом на Python.

На сегодня все! Всем спасибо!

Report Page