Так вышло, что я большой поклонник творчества Уильяма Гибсона. Моё знакомство с этим замечательным прозаиком случилось вследствие глубокого увлечения эстетикой киберпанка и последующего обращения к "корням" жанра, отцом-основателям которого считают Гибсона (пусть сам он от этого всячески открещивается). Трилогией "Киберпространство" (Sprawl trilogy, 1984-1988), а именно открывающим трилогию романом "Нейромант" (Neuromancer, 1984), Гибсон популяризовал идеи киберпространства, виртуальной реальности и всемирной компьютерной сети (непосредственно термин "киберпространство" был изобретён самим же автором и впервые ведён в рассказе "Сожжение Хром" (Burning Chrome, 1982), однако широкое распространение термин получил лишь после публикации "Нейроманта"). Помимо очевидного влияния на поп-культуру (Sonic Youth, U2, Ghost in the Shell, The Matrix, Deus Ex, Shadowrun и ещё множество тайтлов, так или иначе испытавших влияние автора), существует мнение о менее очевидном влиянии Гибсона на сферу информационных технологий. В предисловии к юбилейному изданию "Нейроманта" американский писатель-фантаст Джек Уомак задаётся вопросом:
Может ли быть так, что видение Гибсоном глобального информационного пространства в конечном счёте стало причиной, по которой Интернет сегодня выглядит так, как он выглядит, и работает так, как он работает?
Я склоняюсь к положительному ответу на этот вопрос.
Об игронизации "Нейроманта" я узнал в самом начале своего знакомства с личностью Гибсона. Одноимённая видеоигра была разработана студией Interplay Productions (Wasteland, Fallout) и издана команией Mediagenic (сейчас известна как Activision) в 1988 году на платформах Amiga, Apple II, Apple IIGS, Commodore 64 и MS-DOS. В своё время игра была тепло принята прессой и игровым сообществом, а 1996 году была включена журналом Computer Game Magazine в список 150 лучших видеоигр всех времён. Навязчивая идея — "портировать эту игру на Win64", возникла в моей голове во время очередного запуска "Нейроманта" под DosBox-ом. Получив первые результаты от её воплощения, я решил задокументировать свои действия в виде статьи (в будущем, надеюсь, цикла статей), которую вы видите перед собой.
Не думаю, что смогу довести эту работу до конца. Однако, на данный момент, я намерен продолжать, поскольку для меня это:
- идеальная площадка для тренировки навыков реверс-инжиниринга
- отличная возможность повысить уровень владения Ассемблером
- насколько трудоёмкий, настолько же затягивающий и увлекательный процесс
Надеюсь, мой опыт тем или иным образом окажется вам полезен. В конце концов, учитывая содержание оригинала, не находите ли вы сам процесс реверс-инжиниринга "Нейроманта" несколько ироничным? :)
Попавший ко мне дистрибутив "Нейроманта" включает один 16-битный экзешник формата MZ — NEURO.EXE
, и два бинарника — NEURO1.DAT
и NEURO2.DAT
(вероятно — ресурсы). Загрузив "пациента" в IDA сразу получил предупреждение "Possibly packed file" с последующим sp-analysis failed
— явное свидетельство того, что файл либо сжат, либо зашифрован, либо и то и другое. Но скорее первое. В эпоху борьбы за каждый байт дискового пространства, сжатие — необходимость. За несколько минут нагуглил досовскую утилиту UNP. Прогнав через неё экзешник, подтвердил свою гипотезу о сжатии:
> С:\UNP411\UNP.EXE C:\NEURO\NEURO.EXE
UNP 4.11 Executable file restore utility, written by Ben Castricum, 05/30/95
processing file : C:\NEURO\NEURO.EXE
DOS file size : 94921
file-structure : executable (EXE)
EXE part sizes : header 512, image 94409, overlay 0 bytes
processed with : EXEPACK V4.05 or V4.06
action : decompressing... done
new file size : 313376
writing to file : C:\NEURO\NEURO.EXE
Первый слой "льда" пробит, и теперь IDA отрабатывает молча и без ошибок.
Смотрю, с чем я имею дело. [ФЛЭШФОРВАРД: тогда я не задумывался о том, что для поверхностного изучения можно воспользоваться программами визуализации бинарных файлов. Опробовав их сейчас, я понимаю, что мог бы сэкономить себе кучу времени. По распакованному экзешнику сервис binvis.io рисует картину, которую вы видите слева (изображение кликабельно).]
Быстро пробежавшись по IDA прикидываю, что непосредственно код занимает примерно 1/5 от общего объёма (~64k, верхняя часть на изображении). По коду регулярно встречаются системные вызовы MS-DOS (int 21
) и вызовы различных стандартных Сишных функций. В сегменте данных обнаруживаю большое количество читабельных строк (голубые сектора в нижней части). Подавляющую часть бинарника занимают нули (чёрные сектора) — скорее всего, статически выделенные рабочие массивы и структуры.
В целом всё круто! Учитвыая распознанные системные и библиотечные вызовы, многие из которых связаны с файловым вводом-выводом — решил для начала пореверсить систему управления ресурсами.
Трассируя процедуры, открывающие файловые дескрипторы, я наткнулся на функцию, которая оказалась ключом к пониманию структуры .DAT
-файлов игры. [Обфусцированный продукт дизассемблирования далеко не самая приятная, на вид, вещь, но, всё-же, иногда я буду сопровождать свои слова кодом]. Функция определяет номер, открывает дескриптор (_dos_open
) и устанавливает смещение чтения/записи (lseek
) файла содержащего ресурс, имя которого передаётся функции в виде указателя на нуль-терминированную строку. Посмотрим на одну из, ведущих к этой функции (locate_resource_in_dat
), цепочек вызовов:
...
mov ax, 4CD2h
push ds
push ax
mov ax, 505Eh
push ax ; resource_name
call sub_127DA ; sub_127DA(char *resource_name, void *, void *)
...
sub_127DA proc:
...
arg_0 = word ptr 4 ; resource_name
arg_2 = word ptr 6
arg_4 = word ptr 8
push bp
mov bp, sp
sub sp, 6
...
push [bp+arg_0] ; resource_name
call locate_resource_in_dat ; locate_resource_in_dat(char *resource_name)
...
В сегменте данных, по адресу 0x505E
лежит строка:
dseg:505E aConfigNmc db 'config.nmc',0
Принадлежность ресурса к конкретному .DAT
-файлу определяется внутри функции locate_resource_in_dat
, путём сравнения этой статической строки (sub_127DA("config.nmc", ...)
) со строками, расположенными в сегменте данных через каждые 22 (0x16
) байта, начиная с явно заданных адресов. Вот, что находится по однному из них:
dseg:00A2 db 'R1.BIH', 0
dseg:00A9 db 0
dseg:00AA db 0
dseg:00AB db 0
dseg:00AC db 0
dseg:00AD db 0
dseg:00AE db 0
dseg:00AF db 0
dseg:00B0 db 0
dseg:00B1 db 0
dseg:00B2 db 0
dseg:00B3 db 0
dseg:00B4 db 0EEh
dseg:00B5 db 5
dseg:00B6 db 0
dseg:00B7 db 0
dseg:00B8 db 'R1.PIC', 0
dseg:00BF db 0
dseg:00C0 db 0
dseg:00C1 db 0
dseg:00C2 db 0
dseg:00C3 db 0
dseg:00C4 db 0
dseg:00C5 db 0
dseg:00C6 db 0EEh
dseg:00C7 db 5
dseg:00C8 db 0
dseg:00C9 db 0
dseg:00CA db 046h
dseg:00CB db 013h
dseg:00CC db 0
dseg:00CD db 0
dseg:00CE db 'R1.ANH', 0
...
Это выглядит как массив структур вида:
struct resource_t
{
char name[14]; // имя ресурса
int offset; // смещение от начала файла
int size; // байтовый размер ресурса
};
struct resource_t resources[] = {
{ "R1.BIH", 0, 0x5EE },
{ "R1.PIC", 0x5EE, 0x1346 },
...
};
Таких массивов оказалось два. По одному на .DAT
-файл. Они расположены по адресам 0xA2
и 0x81C
и состоят из 87 и 151 элемента соответсвенно. Всего я насчитал 8 различных типов ресурсов (по расширениям в именах): .BIH
, .PIC
, .ANH
, .BIN
, .IMH
, .NMC
, .TXH
, .SAV
. Имена с расширениями .NMC
и .SAV
встречаются лишь однажды и говорят сами за себя: CONFIG.NMC
и SAVEGAME.SAV
. Говорящие имена также у ресов с расширением .IMH
: CURSORS.IMH
, SPRITES.IMH
, TITLE.IMH
и др. С остальными не так очевидно. Есть предположение, что .TXH
— это текст, а .PIC
— изображения. [Сейчас я практически уверен в том, что в .PIC
хранятся задники локаций]. Содержание остальных типов пока неизвестно.
Гуляя по коду под отладчиком, случайно разобрался с назначением .BIN
-ресурсов. А началось всё с того, что в листинге я не нашёл места, в котором бы инициализировался 256-цветный режим VGA (mode 0x13). Однако вывод отладчика в DosBox показывал, что, и, примерно, когда это происходит:
43597504: INT10:Set Video Mode 13
43597504: VGA:Blinking 0
Потрассировав программу до этого момента я выяснил, что код, непосредственно выполняющий эти действия, загружается в память из ресурса TVGA.BIN
, а программа просто делает call
по адресу, в который тот загружен. Таким образом .BIN
-ы содержат некий, динамически подгружаемый, скомпилированный код. Что-то вроде динамических библиотек, но не совсем. И пока не понятно, почему это было сделано именно так. [Так я думал, пока не обсудил это с человеком, практиковавшим в те времена. Оказалось, что это вполне стандартный, для Доса, подход, повсеместно применявшийся для сокращения объёма исполняеых файлов.]
Хотелось получить какой-нибудь осязаемый результат. Решил попробовать раскрутить формат .IMH
, ведь судя по именам с таким расширением (SPRITES.IMH
) — это графика. Из всех ресурсов этого типа, выбрал один минимального размера и сохранил в отдельный файл:
CURSORS.IMH: 243 байта
0x0000: 0001 0203 0400 060D 0009 0A0B 0000 000F
0x0010: 0001 0203 0405 080B 0809 0A0B 0C0D 0E0F
0x0020: 3F01 0000 0440 7EEC D0C2 2D0A 46A3 3FF2
0x0030: 7111 6230 640E 104C E0C5 8505 FCFF 5001
0x0040: 04C0 260F E2C0 85C2 5017 FA05 54B0 6C2D
0x0050: 7413 E9C7 451D D8B6 2C18 E714 7B8B D79B
0x0060: BBE5 EB60 4D2F EF70 3A1E 42FE D9C0 5DC0
0x0070: EBCD EE07 B9BF 821C 0141 BB15 360B 743A
0x0080: 4BC8 749E 45D7 5B26 63F4 24E9 DAEA 5CC6
0x0090: E859 D793 41F7 94DF B7AC 4DB7 EFC2 CC4F
0x00A0: 5D5F 66D1 E5E3 3F2B AC42 7C5E 3AF1 9F95
0x00B0: D7D9 E8E9 3D75 62DE 9B05 86E6 0EBF 794C
0x00C0: 1D0B 3A76 9A97 31BA 1274 ED75 2663 FE79
0x00D0: 175F BC87 FD58 9B6F DD0B 12F2 652F 12FB
0x00E0: 1E99 5E37 B24C 424C 4F4C AF1B 6CC4 BEC7
0x00F0: C99F C0
Это точно не битмап, а значит нужно прикинуть варианты того, чем это может быть [в порядке увеличения сложности]:
- это сжатое изображение какого либо известного формата
- это данные, сжатые некоторым известным алгоритмом общего назначения
- это данные, сжатые некоторым собственным (для Interplay) алгоритмом
Первый вариант отпал достаточно быстро. Перепробовав с дюжину различных расширений и программ-просмотрщиков я так и не смог открыть этот файл. Доходило до экзотики — нагуглил, что изображения с расширением .imh
используются в специльном астрономическом (буквально — им пользуются астрономы) программном комплексе IRAF (Image Reduction and Analysis Facility). Но и это мимо.
На этом этапе для меня не было большой разницы между вторым и третьим вариантом. Конечно, зная конкретный алгоритм, было бы куда проще реализовать его на высокоуровневом языке или, что ещё лучше, взять готовую реализацию. Однако, не являясь экспертом (и даже не любителем) в области сжатия данных, идентифицировать алгоритм по дизассемблированному коду или по каким-то сигнатурам крайне затруднительно. В любом случае, задача сводилась к тому, чтобы выделить в IDA участок кода, отвечающий за декомпрессию данных, и адаптировать его под 64-битный Ассемблер (в моём случае — MASM).
И мне повезло. Мой CURSORS.IMH
подгружается в main
-е одним из первых:
...
mov ax, 2
mov dx, seg seg009
push dx
push ax
mov ax, 506Ah ; "cursors.imh"
push ax
call sub_126CB
...
Дальше следовала долгая и упорная трассировка функции sub_126CB
. Но игра стоила свеч. Результат работы этой функции обнаруживаем по адресу, хранящемуся в dx
:
0x0000: 0000 0000 0800 0A00 0666 6666 6666 6600
0x0010: 6777 7777 7777 7760 6777 7777 7766 7776
0x0020: 0666 6676 6677 7776 0006 7767 7777 7766
0x0030: 0000 6667 7777 7676 0000 6776 6666 6776
0x0040: 0000 0666 6667 7766 0000 0067 7677 7660
0x0050: 0000 0006 6666 6600 0400 0000 0600 0C00
0x0060: 0000 3000 0000 0003 B300 0000 003B BB30
0x0070: 0000 03BB BBB3 0000 3BBB BBBB 3000 333B
0x0080: BB33 3000 003B BB30 0000 003B BB30 0000
0x0090: 003B BB30 0000 003B BB30 0000 003B BB30
0x00A0: 0000 0033 3330 0000 0B00 0400 0600 0900
0x00B0: 0000 0033 0000 0000 003B 3000 3333 333B
0x00C0: B300 3BBB BBBB BB30 3BBB BBBB BBB3 3BBB
0x00D0: BBBB BB30 3333 333B B300 0000 003B 3000
0x00E0: 0000 0033 0000 0400 0B00 0600 0C00 0033
0x00F0: 3330 0000 003B BB30 0000 003B BB30 0000
0x0100: 003B BB30 0000 003B BB30 0000 003B BB30
0x0110: 0000 333B BB33 3000 3BBB BBBB 3000 03BB
0x0120: BBB3 0000 003B BB30 0000 0003 B300 0000
0x0130: 0000 3000 0000 0000 0400 0600 0900 0000
0x0140: 3300 0000 0003 B300 0000 003B B333 3333
0x0150: 03BB BBBB BBB3 3BBB BBBB BBB3 03BB BBBB
0x0160: BBB3 003B B333 3333 0003 B300 0000 0000
0x0170: 3300 0000
Вот это уже похоже на битмап! А если это действительно так, по покрутив эти данные, я должен получить картинку. Пробую составлять ряды используя различные значения ширины — безрезультатно. Обращаю внимание на значения 5-го (0x08
) и 7-го (0x0A
) байтов. Допустим, это ширина и высота, умножаем, отсчитываем следующие 80 байт (0x08 * 0x0A = 50 (80)
):
0x0008: 0666 6666 6666 6600 6777 7777 7777 7760
0x0018: 6777 7777 7766 7776 0666 6676 6677 7776
0x0028: 0006 7767 7777 7766 0000 6667 7777 7676
0x0038: 0000 6776 6666 6776 0000 0666 6667 7766
0x0048: 0000 0067 7677 7660 0000 0006 6666 6600
Составляем так, что бы в ряду было 8 байт, а всего рядов было 10:
0666 6666 6666 6600
6777 7777 7777 7760
6777 7777 7766 7776
0666 6676 6677 7776
0006 7767 7777 7766
0000 6667 7777 7676
0000 6776 6666 6776
0000 0666 6667 7766
0000 0067 7677 7660
0000 0006 6666 6600
Это оно! [Я это знал, поскольку видел, как выглядел курсор на экране]. Пробую завернуть эти данные в
.BMP
заголовок со стандартной палитрой — что-то рисует, но совсем не то, что я ожидал. Ставлю в отладчике брейкпоинт на именение состояния байтов видеопамяти (0xA000+
), что бы увидеть, что на самом деле выводится на экран. Понимаю, что нужно добавить нулей [сделайте в браузере поиск по занчению "06" и, в массиве ниже, вы уидите нечто похожее на ладонь с оттопыренным указательным пальцем]:
00 06 06 06 06 06 06 06 06 06 06 06 06 06 00 00
06 07 07 07 07 07 07 07 07 07 07 07 07 07 06 00
06 07 07 07 07 07 07 07 07 07 06 06 07 07 07 06
00 06 06 06 06 06 07 06 06 06 07 07 07 07 07 06
00 00 00 06 07 07 06 07 07 07 07 07 07 07 06 06
00 00 00 00 06 06 06 07 07 07 07 07 07 06 07 06
00 00 00 00 06 07 07 06 06 06 06 06 06 07 07 06
00 00 00 00 00 06 06 06 06 06 06 07 07 07 06 06
00 00 00 00 00 00 06 07 07 06 07 07 07 06 06 00
00 00 00 00 00 00 00 06 06 06 06 06 06 06 00 00
Заворачиваю эти (и оставшиеся в исходном массиве) данные в BMP
и получаю на выходе вот такие картинки (здесь, чуть увеличенные):
Обнаружилось, что первые 20 байт CURSORS.IMH
[и остальных .IMH
] загружаются в другую область памяти и не учавствуют в декомпрессии. Сперва подумал, что это палитра, но нет, корректные цвета получаются с использованием стандартной (на рисунке выше). [В коде я нашёл место, в котором происходит обращение к этой памяти, но пока не разбирался, что именно там происходит].
Переписал функцию sub_126CB
, всего вышло около 800 строк MASM-а. Но оно работает, и теперь я могу распаковать любой .IMH
-ресурс. Иллюстрация в заголовке, кстати, оттуда же (TITLE.IMH
). А вот спрайты главного героя (SPRITES.IMH
):
Да, я могу распаковать любой .IMH
-ресурс, но не все сразу. При переносе кода на 64-битный Ассемблер я допустил ошибки связанные с переносом разрядов при сложении адресов, из-за этого, даже на одном ресурсе, программа работает через раз. Но это поправимо, нужно лишь переписать этот код так, как если бы я изначально проектировал его под 64-битную платформу. В идеале, переписать его на Си, тогда эти ошибки упразднятся сами собой. Однако, по тому, что я увидел, у меня никак не складывался алгоритм, а переписывая обфусцированный Ассемблер на Си, есть риск получить ещё более обфусцированный Си. Мне нужна была помощь. Сняв данные в ключевых точках, я опубликовал вопрос на Reverse Engineering StackExchange в надежде, что по этим данным кто-нибудь сможет определить — с каким-именно алгоритмом сжатия я имею дело.
Currently I'm in reversing an old MS-DOS Neuromancer game (Interplay Productions, 1988. Based on William Gibson's novel). For now I have already written an utility that parses game resources and extracts sprites. Sprites are bitmaps that compressed with some fancy algorithm. I have restored that algorithm by porting it's 16-bit disassembly to 64-bit assembly (I'm working on Windows 10 and using MASM in MSVS 2017). It results in ~800 lines of pretty obfuscated assembly (which became even more obfuscated after porting it to 64-bit) that works and I'm about rewriting it in plain old C. However it makes no sense because i can't identify an actual algorithm.
And here is the problem. I assume that the game uses some widly known data compression algorithm. I will provide an example by posting chunks of data that are observable during the decompression process. I hope that there are some compression experts who will recognize the algorithm (if my theory about "widly known algorithm" is correct). Thank you in advance!
Here we go, decompressing CURSORS.IMH file that contains a series of bitmaps that shows diffrent in-game cursors:
SRC: Initial data, 211 bytes stored in 512 byte buffer:
0x0000 3f 01 00 00 04 40 7e ec d0 c2 2d 0a 46 a3 3f f2
0x0010 71 11 62 30 64 0e 10 4c e0 c5 85 05 fc ff 50 01
0x0020 04 c0 26 0f e2 c0 85 c2 50 17 fa 05 54 b0 6c 2d
0x0030 74 13 e9 c7 45 1d d8 b6 2c 18 e7 14 7b 8b d7 9b
0x0040 bb e5 eb 60 4d 2f ef 70 3a 1e 42 fe d9 c0 5d c0
0x0050 eb cd ee 07 b9 bf 82 1c 01 41 bb 15 36 0b 74 3a
0x0060 4b c8 74 9e 45 d7 5b 26 63 f4 24 e9 da ea 5c c6
0x0070 e8 59 d7 93 41 f7 94 df b7 ac 4d b7 ef c2 cc 4f
0x0080 5d 5f 66 d1 e5 e3 3f 2b ac 42 7c 5e 3a f1 9f 95
0x0090 d7 d9 e8 e9 3d 75 62 de 9b 05 86 e6 0e bf 79 4c
0x00A0 1d 0b 3a 76 9a 97 31 ba 12 74 ed 75 26 63 fe 79
0x00B0 17 5f bc 87 fd 58 9b 6f dd 0b 12 f2 65 2f 12 fb
0x00C0 1e 99 5e 37 b2 4c 42 4c 4f 4c af 1b 6c c4 be c7
0x00D0 c9 9f c0
Processing starts from the fist byte of SRC
and suspends on 44th byte. Here is the intermediate result of that processing stored in 2048 byte buffer located right after the SRC
buffer (actually, there are 10 bytes between them, where among other things an address of 44th byte is stored):
0x0200 00 80 01 00 5c ea 3f 01 00 00
0x020A 02 00 02 00 0e 00 04 00 1f 00 05 00 04 00 04 00
0x021A 18 00 05 00 3a 00 07 00 0d 00 05 00 00 00 00 00
0x022A 05 00 04 00 df 00 08 00 0d 00 08 00 de 00 08 00
0x023A 05 00 07 00 00 00 00 00 00 00 00 00 00 00 00 00
0x024A 00 00 05 00 05 00 05 00 00 00 00 00 00 00 00 00
0x025A 00 00 00 00 00 00 00 00 0c 00 08 00 00 00 00 00
0x026A 00 00 00 00 0f 00 08 00 0e 00 08 00 00 00 00 00
... ZEROES ...
0x02CA 1a 00 05 00 00 00 00 00 00 00 00 00 0c 00 05 00
0x02DA 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x02EA 04 00 05 00 00 00 00 00 00 00 00 00 00 00 00 00
... ZEROES ...
0x038A 6e 00 07 00 1c 00 06 00 00 00 00 00 00 00 00 00
0x039A 00 00 00 00 00 00 00 00 09 00 08 00 00 00 00 00
... ZEROES ...
0x040A 19 00 05 00 00 00 00 00 00 00 00 00 07 00 05 00
0x041A 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x042A 06 00 05 00 00 00 00 00 00 00 00 00 00 00 00 00
... ZEROES ...
0x05EA 00 00 00 00 3b 00 07 00 00 00 00 00 08 00 08 00
0x05FA 36 00 06 00 0f 00 05 00 1e 00 05 00 01 00 04 00
0x060A 05 10 00 00 05 10 00 00 05 10 00 00 05 10 00 00
0x061A 05 10 00 00 05 10 00 00 05 10 00 00 05 10 00 00
0x062A 08 fb 00 00 08 66 00 00 07 0c 00 00 07 0c 00 00
0x063A 08 16 00 00 08 0a 00 00 08 1a 00 00 08 19 00 00
0x064A 04 ff 00 00 04 ff 00 00 04 ff 00 00 04 ff 00 00
0x065A 04 ff 00 00 04 ff 00 00 04 ff 00 00 04 ff 00 00
0x066A 04 ff 00 00 04 ff 00 00 04 ff 00 00 04 ff 00 00
0x067A 04 ff 00 00 04 ff 00 00 04 ff 00 00 04 ff 00 00
0x068A 05 38 00 00 05 38 00 00 05 38 00 00 05 38 00 00
0x069A 05 38 00 00 05 38 00 00 05 38 00 00 05 38 00 00
0x06AA 05 11 00 00 05 11 00 00 05 11 00 00 05 11 00 00
0x06BA 05 11 00 00 05 11 00 00 05 11 00 00 05 11 00 00
0x06CA 05 88 00 00 05 88 00 00 05 88 00 00 05 88 00 00
0x06DA 05 88 00 00 05 88 00 00 05 88 00 00 05 88 00 00
0x06EA 05 83 00 00 05 83 00 00 05 83 00 00 05 83 00 00
0x06FA 05 83 00 00 05 83 00 00 05 83 00 00 05 83 00 00
0x070A 04 03 00 00 04 03 00 00 04 03 00 00 04 03 00 00
0x071A 04 03 00 00 04 03 00 00 04 03 00 00 04 03 00 00
0x072A 04 03 00 00 04 03 00 00 04 03 00 00 04 03 00 00
0x073A 04 03 00 00 04 03 00 00 04 03 00 00 04 03 00 00
0x074A 04 08 00 00 04 08 00 00 04 08 00 00 04 08 00 00
0x075A 04 08 00 00 04 08 00 00 04 08 00 00 04 08 00 00
0x076A 04 08 00 00 04 08 00 00 04 08 00 00 04 08 00 00
0x077A 04 08 00 00 04 08 00 00 04 08 00 00 04 08 00 00
0x078A 05 33 00 00 05 33 00 00 05 33 00 00 05 33 00 00
0x079A 05 33 00 00 05 33 00 00 05 33 00 00 05 33 00 00
0x07AA 05 06 00 00 05 06 00 00 05 06 00 00 05 06 00 00
0x07BA 05 06 00 00 05 06 00 00 05 06 00 00 05 06 00 00
0x07CA 06 61 00 00 06 61 00 00 06 61 00 00 06 61 00 00
0x07DA 07 05 00 00 07 05 00 00 07 f9 00 00 07 f9 00 00
0x07EA 05 fd 00 00 05 fd 00 00 05 fd 00 00 05 fd 00 00
0x07FA 05 fd 00 00 05 fd 00 00 05 fd 00 00 05 fd 00 00
0x080A 02 00 00 00 02 00 00 00 02 00 00 00 02 00 00 00
0x081A 02 00 00 00 02 00 00 00 02 00 00 00 02 00 00 00
0x082A 02 00 00 00 02 00 00 00 02 00 00 00 02 00 00 00
0x083A 02 00 00 00 02 00 00 00 02 00 00 00 02 00 00 00
0x084A 02 00 00 00 02 00 00 00 02 00 00 00 02 00 00 00
0x085A 02 00 00 00 02 00 00 00 02 00 00 00 02 00 00 00
0x086A 02 00 00 00 02 00 00 00 02 00 00 00 02 00 00 00
0x087A 02 00 00 00 02 00 00 00 02 00 00 00 02 00 00 00
0x088A 02 00 00 00 02 00 00 00 02 00 00 00 02 00 00 00
0x089A 02 00 00 00 02 00 00 00 02 00 00 00 02 00 00 00
0x08AA 02 00 00 00 02 00 00 00 02 00 00 00 02 00 00 00
0x08BA 02 00 00 00 02 00 00 00 02 00 00 00 02 00 00 00
0x08CA 02 00 00 00 02 00 00 00 02 00 00 00 02 00 00 00
0x08DA 02 00 00 00 02 00 00 00 02 00 00 00 02 00 00 00
0x08EA 02 00 00 00 02 00 00 00 02 00 00 00 02 00 00 00
0x08FA 02 00 00 00 02 00 00 00 02 00 00 00 02 00 00 00
0x090A 05 04 00 00 05 04 00 00 05 04 00 00 05 04 00 00
0x091A 05 04 00 00 05 04 00 00 05 04 00 00 05 04 00 00
0x092A 05 80 00 00 05 80 00 00 05 80 00 00 05 80 00 00
0x093A 05 80 00 00 05 80 00 00 05 80 00 00 05 80 00 00
0x094A 05 30 00 00 05 30 00 00 05 30 00 00 05 30 00 00
0x095A 05 30 00 00 05 30 00 00 05 30 00 00 05 30 00 00
0x096A 06 fc 00 00 06 fc 00 00 06 fc 00 00 06 fc 00 00
0x097A 07 60 00 00 07 60 00 00 08 0b 00 00 08 09 00 00
0x098A 04 01 00 00 04 01 00 00 04 01 00 00 04 01 00 00
0x099A 04 01 00 00 04 01 00 00 04 01 00 00 04 01 00 00
0x09AA 04 01 00 00 04 01 00 00 04 01 00 00 04 01 00 00
0x09BA 04 01 00 00 04 01 00 00 04 01 00 00 04 01 00 00
0x09CA 05 fe 00 00 05 fe 00 00 05 fe 00 00 05 fe 00 00
0x09DA 05 fe 00 00 05 fe 00 00 05 fe 00 00 05 fe 00 00
0x09EA 05 02 00 00 05 02 00 00 05 02 00 00 05 02 00 00
0x09FA 05 02 00 00 05 02 00 00 05 02 00 00 05 02 00 00
Processing continues from 44th byte of SRC
. The remaining bytes are processed with result stored in external intemediate buffer:
IMM: Intermediate data, 319 bytes:
0x0000 00 00 00 00 08 00 0a 00 ff 06 05 66 fe 00 61 05
0x0010 11 ff 60 04 00 fc 11 00 16 61 01 11 ff 01 01 11
0x0020 01 00 fe 06 60 02 11 01 00 fc 10 00 06 11 02 00
0x0030 fe 01 10 01 00 ff 01 03 11 02 00 fc 61 10 00 01
0x0040 01 10 01 00 fe 06 01 01 10 fe 01 06 02 00 fb 61
0x0050 10 11 10 60 04 00 00 00 06 00 0c 00 01 00 ff 30
0x0060 03 00 fe 03 83 03 00 fd 38 08 30 01 00 fc 03 80
0x0070 00 83 01 00 ff 38 01 00 f9 08 30 00 08 80 00 88
0x0080 01 00 ff 33 01 00 fe 03 30 19 00 fe 08 88 02 00
0x0090 0b 00 04 00 06 00 09 00 02 00 ff 33 04 00 fd 08
0x00A0 30 00 02 33 fc 00 83 00 08 01 88 fd 80 08 30 04
0x00B0 00 ff 83 04 00 fe 83 08 01 88 fd 80 08 30 02 33
0x00C0 fe 00 83 03 00 fd 08 30 00 04 00 0b 00 06 00 0c
0x00D0 00 ff 00 01 33 ff 30 02 00 fe 08 88 1a 00 ff 33
0x00E0 01 00 f9 03 30 00 08 80 00 88 01 00 ff 38 01 00
0x00F0 f9 08 30 00 03 80 00 83 02 00 fd 38 08 30 02 00
0x0100 fe 03 83 02 00 00 00 04 00 06 00 09 00 01 00 ff
0x0110 33 03 00 fe 03 80 03 00 fe 38 00 02 33 fd 03 80
0x0120 08 01 88 fe 80 38 04 00 ff 38 04 00 fd 03 80 08
0x0130 01 88 fc 80 00 38 00 02 33 fd 00 03 80 02 00
Finally, some processing performed on IMM
and we have the result:
DST: The Result, 372 bytes:
0x0000 00 00 00 00 08 00 0A 00 06 66 66 66 66 66 66 00
0x0010 67 77 77 77 77 77 77 60 67 77 77 77 77 66 77 76
0x0020 06 66 66 76 66 77 77 76 00 06 77 67 77 77 77 66
0x0030 00 00 66 67 77 77 76 76 00 00 67 76 66 66 67 76
0x0040 00 00 06 66 66 67 77 66 00 00 00 67 76 77 76 60
0x0050 00 00 00 06 66 66 66 00 04 00 00 00 06 00 0C 00
0x0060 00 00 30 00 00 00 00 03 B3 00 00 00 00 3B BB 30
0x0070 00 00 03 BB BB B3 00 00 3B BB BB BB 30 00 33 3B
0x0080 BB 33 30 00 00 3B BB 30 00 00 00 3B BB 30 00 00
0x0090 00 3B BB 30 00 00 00 3B BB 30 00 00 00 3B BB 30
0x00A0 00 00 00 33 33 30 00 00 0B 00 04 00 06 00 09 00
0x00B0 00 00 00 33 00 00 00 00 00 3B 30 00 33 33 33 3B
0x00C0 B3 00 3B BB BB BB BB 30 3B BB BB BB BB B3 3B BB
0x00D0 BB BB BB 30 33 33 33 3B B3 00 00 00 00 3B 30 00
0x00E0 00 00 00 33 00 00 04 00 0B 00 06 00 0C 00 00 33
0x00F0 33 30 00 00 00 3B BB 30 00 00 00 3B BB 30 00 00
0x0100 00 3B BB 30 00 00 00 3B BB 30 00 00 00 3B BB 30
0x0110 00 00 33 3B BB 33 30 00 3B BB BB BB 30 00 03 BB
0x0120 BB B3 00 00 00 3B BB 30 00 00 00 03 B3 00 00 00
0x0130 00 00 30 00 00 00 00 00 04 00 06 00 09 00 00 00
0x0140 33 00 00 00 00 03 B3 00 00 00 00 3B B3 33 33 33
0x0150 03 BB BB BB BB B3 3B BB BB BB BB B3 03 BB BB BB
0x0160 BB B3 00 3B B3 33 33 33 00 03 B3 00 00 00 00 00
0x0170 33 00 00 00
Decompression completed and it's easy to derive bitmaps like this:
- Get first 8 bytes of DST:
00 00 00 00 08 00 0A 00
- Multiply last 2 words:
08 * 0A = 50 (80)
Get next 80 bytes of DST :
06 66 66 66 66 66 66 00 67 77 77 77 77 77 77 60 67 77 77 77 77 66 77 76 06 66 66 76 66 77 77 76 00 06 77 67 77 77 77 66 00 00 66 67 77 77 76 76 00 00 67 76 66 66 67 76 00 00 06 66 66 67 77 66 00 00 00 67 76 77 76 60 00 00 00 06 66 66 66 00
Arrange those bytes assuming that 08 and 0A are width and height respectively:
06 66 66 66 66 66 66 00 67 77 77 77 77 77 77 60 67 77 77 77 77 66 77 76 06 66 66 76 66 77 77 76 00 06 77 67 77 77 77 66 00 00 66 67 77 77 76 76 00 00 67 76 66 66 67 76 00 00 06 66 66 67 77 66 00 00 00 67 76 77 76 60 00 00 00 06 66 66 66 00
Extend this with zeroes:
00 06 06 06 06 06 06 06 06 06 06 06 06 06 00 00 06 07 07 07 07 07 07 07 07 07 07 07 07 07 06 00 06 07 07 07 07 07 07 07 07 07 06 06 07 07 07 06 00 06 06 06 06 06 07 06 06 06 07 07 07 07 07 06 00 00 00 06 07 07 06 07 07 07 07 07 07 07 06 06 00 00 00 00 06 06 06 07 07 07 07 07 07 06 07 06 00 00 00 00 06 07 07 06 06 06 06 06 06 07 07 06 00 00 00 00 00 06 06 06 06 06 06 07 07 07 06 06 00 00 00 00 00 00 06 07 07 06 07 07 07 06 06 00 00 00 00 00 00 00 00 06 06 06 06 06 06 06 00 00
Thats it! Wrapping it in BMP header gives us a cute cursor image.
Мне опять повезло! Оказалось, что там последовательно применяются два алгоритма. Первый — некий неизвестный алгоритм сжатия, а второй — разновидность Run-Length кодирования [здесь, я не буду расписывать какая именно — это я сделал в ответе на свой же вопрос на Reverse Exchange]. В итоге, вторая часть, вместо ~400 строк Ассемблера, уместилась в 50 строк на Си:
typedef struct rle_hdr_t
{
uint32_t unknown;
uint16_t width;
uint16_t height;
} rle_hdr_t;
static int decode_rle(uint8_t *_src, uint32_t len, uint8_t *_dst)
{
uint32_t total_len = 0;
uint8_t *src = _src, *dst = _dst;
rle_hdr_t *rle;
while (len)
{
uint32_t bm_size = 0, bm_width = 0, bm_height = 0;
uint8_t *p = NULL;
rle = (rle_hdr_t*)src;
bm_width = rle->width;
bm_height = rle->height;
bm_size = bm_width * bm_height;
memmove(dst, src, sizeof(rle_hdr_t));
total_len += sizeof(rle_hdr_t) + bm_size;
src += sizeof(rle_hdr_t);
dst += sizeof(rle_hdr_t);
len -= sizeof(rle_hdr_t);
p = dst;
while (bm_size)
{
if (*src > 0x7F)
{
int i = 0x100 - *src++;
len--;
while (i--)
{
*dst++ = *src++;
bm_size--;
len--;
}
}
else
{
int num = *src++, val = *src++;
len -= 2;
memset(dst, val, (size_t)++num);
dst += num;
bm_size -= num;
}
}
for (uint32_t i = 0; i < bm_height - 1; i++)
{
for (uint32_t j = 0; j < bm_width; j++)
{
p[((i + 1)*bm_width) + j] ^= p[(i*bm_width) + j];
}
}
}
return total_len;
}
После, я довёл до ума первую часть функции и закончил программу распаковывающую все .IMH
-ресурсы. Из 27-ми таких ресурсов получилось 108 BMP
изображений.
Пока на этом всё. Как я уже сказал в начале, я намерен продолжать. Следующим шагом планирую окончательно разобраться с графикой (.PIC
) и перейти на текст. Возможно, в скором времени выложу свои наработки на github. И посмотрим куда меня это приведёт. Надеюсь, не в суд :)
Реверсим «Нейроманта». Часть 2: Рендерим шрифт