Разбор задач из CTF по реверсу: Shadelt900
2026-03-10 21:11 Diff

В этой заметке я хочу поделиться с вами решением одного из заданий по 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.

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

Причём, если запускать программу несколько раз с одним и тем же входным файлом, то для каждого элемента значение 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 надо байты:

заменить на:

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

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-сообществу.