Разбор задач из CTF по реверсу: Shadelt900

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

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

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

По адресам 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;
}

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

В переменной 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))

Получаем:

И ещё один способ решения

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

Сразу за вершинным шейдером по адресам 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.

На сегодня всё, спасибо!

За подготовку материала автор выражает благодарность CTF-сообществу.