Разбор задач из 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-сообществу.