Blog. Just Blog

Декомпиляция CHM-файлов на Ассемблере

Версия для печати Добавить в Избранное Отправить на E-Mail | Категория: Образ мышления: Assembler | Автор: ManHunter
Декомпиляция CHM-файлов на Ассемблере
Декомпиляция CHM-файлов на Ассемблере

CHM-файлы, как правило, содержат в себе справочную документацию в формате HTML, скомпилированную и сохраненную с помощью сжатия LZX. Справочный файл может также включать в себя содержание, предметный указатель, базу для полнотекстового поиска по страницам, а также файлы изображений, скрипты, таблицы стилей и даже вложенные архивы. Чтобы извлечь все эти данные из CHM-файла, его надо декомпилировать. Об этом и будет сегодняшняя статья.

Сперва немного теории. Для декомпиляции файлов справок создано достаточное количество инструментов разной мощности и функционала. Можно даже воспользоваться обычной системной командой

hh.exe -decompile <TargetFolder> <MyFile>.chm
Но при этом есть некоторое количество "но". Подавляющему большинству пользователей это вообще никак и никогда не пригодится, а вот любителям ковыряться в файлах может оказаться интересным.

Во-первых, именно декомпиляция требуется не всегда, могут быть задачи, связанные с поиском определенной страницы внутри СНМ-файла, например, при указании ссылки на эту страницу в параметрах открытия файла.

htmlhelp.exe sample.chm::/unknown/not_exists.html
В этом случае откроется страница с ошибкой. А правильнее было бы оповестить пользователя, что такой страницы в файле справки не существует или автоматически переадресовать его, скажем, на индексную страницу.

Во-вторых, при системной декомпиляции из CHM-файла не извлекаются служебные файлы и папки типа #IDXHDR, #TOPICS, $OBJINST и всякие другие. Да, польза от них примерно нулевая, но раз уж взялись декомпилировать файл, то надо извлекать из него абсолютно все содержимое.

Поврежденное имя файла
Поврежденное имя файла

Ну и в-третьих, содержимое CHM-файла можно модифицировать таким образом, чтобы имена одного или нескольких содержащихся в нем файлов повредились и стали недоступны для извлечения обычными способами. Системные средства при декомпиляции такие файлы молча пропускают, да и подавляющее большинство сторонних инструментов для работы с файлами справок их просто игнорируют или не видят. А хорошим тоном было бы предупредить пользователя, что имя файла содержит недопустимые символы и все-таки сохранить такие файлы с заменой запрещенных символов на безопасные. Это же касается ситуаций, когда после ручной модификации в файле получаются два файла с одинаковым именем и путем. Такие случаи тоже хорошо бы предусмотреть. Подобные модификации можно использовать, чтобы скрыть некоторую информацию внутри файла справки, сохраняя возможность извлечь ее в дальнейшем.

С теорией закончили, переходим к практике. Для работы с CHM-файлами будут использоваться средства COM, поэтому понадобятся описания интерфейсов, GUID, структур и констант.
  1. ; GUID {5D02926A-212E-11D0-9DF9-00A0C922E6EC}
  2. CLSID_ITStorage \
  3.     dd 05D02926Ah
  4.     dw 0212Eh
  5.     dw 011D0h
  6.     db 09Dh, 0F9h, 000h, 0A0h, 0C9h, 022h, 0E6h, 0ECh
  7.  
  8. ; GUID {88CC31DE-27AB-11D0-9DF9-00A0C922E6EC}
  9. IID_IITStorage \
  10.     dd 088CC31DEh
  11.     dw 027ABh
  12.     dw 011D0h
  13.     db 09Dh, 0F9h, 000h, 0A0h, 0C9h, 022h, 0E6h, 0ECh
  14.  
  15. ; IID_IITStorage Interface
  16. struct IITStorage
  17.     ; IUnknown
  18.     QueryInterface               dd ?   ; 000h
  19.     AddRef                       dd ?   ; 004h
  20.     Release                      dd ?   ; 008h
  21.     ; IITStorage
  22.     StgCreateDocfile             dd ?   ; 00Ch
  23.     StgCreateDocfileOnILockBytes dd ?   ; 010h
  24.     StgIsStorageFile             dd ?   ; 014h
  25.     StgIsStorageILockBytes       dd ?   ; 018h
  26.     StgOpenStorage               dd ?   ; 01Ch
  27.     StgOpenStorageOnILockBytes   dd ?   ; 020h
  28.     StgSetTimes                  dd ?   ; 024h
  29.     SetControlData               dd ?   ; 028h
  30.     DefaultControlData           dd ?   ; 02Ch
  31.     Compact                      dd ?   ; 030h
  32. ends
  33.  
  34. ; IEnumSTATSTG Interface
  35. struct IEnumSTATSTG
  36.     ; IUnknown
  37.     QueryInterface dd ?   ; 000h
  38.     AddRef         dd ?   ; 004h
  39.     Release        dd ?   ; 008h
  40.     ; IEnumSTATSTG
  41.     Next           dd ?   ; 00Ch
  42.     Skip           dd ?   ; 010h
  43.     Reset          dd ?   ; 014h
  44.     Clone          dd ?   ; 018h
  45. ends
  46.  
  47. ; IID_IStorage Interface
  48. struct IStorage
  49.     ; IUnknown
  50.     QueryInterface  dd ?   ; 000h
  51.     AddRef          dd ?   ; 004h
  52.     Release         dd ?   ; 008h
  53.     ; IStorage
  54.     CreateStream    dd ?   ; 00Ch
  55.     OpenStream      dd ?   ; 010h
  56.     CreateStorage   dd ?   ; 014h
  57.     OpenStorage     dd ?   ; 018h
  58.     CopyTo          dd ?   ; 01Ch
  59.     MoveElementTo   dd ?   ; 020h
  60.     Commit          dd ?   ; 024h
  61.     Revert          dd ?   ; 028h
  62.     EnumElements    dd ?   ; 02Ch
  63.     DestroyElement  dd ?   ; 030h
  64.     RenameElement   dd ?   ; 034h
  65.     SetElementTimes dd ?   ; 038h
  66.     SetClass        dd ?   ; 03Ch
  67.     SetStateBits    dd ?   ; 040h
  68.     Stat            dd ?   ; 044h
  69. ends
  70.  
  71. ; IStream Interface
  72. struct IStream
  73.     ; IUnknown
  74.     QueryInterface dd ?   ; 000h
  75.     AddRef         dd ?   ; 004h
  76.     Release        dd ?   ; 008h
  77.     ; IStream
  78.     Read           dd ?   ; 00Ch
  79.     Write          dd ?   ; 010h
  80.     Seek           dd ?   ; 014h
  81.     SetSize        dd ?   ; 018h
  82.     CopyTo         dd ?   ; 01Ch
  83.     Commit         dd ?   ; 020h
  84.     Revert         dd ?   ; 024h
  85.     LockRegion     dd ?   ; 028h
  86.     UnlockRegion   dd ?   ; 02Ch
  87.     Stat           dd ?   ; 030h
  88.     Clone          dd ?   ; 034h
  89. ends
  90.  
  91. struct ULARGE_INTEGER
  92.      LowPart  dd ?
  93.      HighPart dd ?
  94. ends
  95.  
  96. struct STATSTG
  97.     pwcsName          dd ?
  98.     type              dd ?
  99.     cbSize            ULARGE_INTEGER
  100.     mtime             FILETIME
  101.     ctime             FILETIME
  102.     atime             FILETIME
  103.     grfMode           dd ?
  104.     grfLocksSupported dd ?
  105.     clsid             rb 16
  106.     grfStateBits      dd ?
  107.     reserved          dd ?
  108. ends
  109.  
  110. CLSCTX_INPROC_SERVER  = 0x01
  111. CLSCTX_INPROC_HANDLER = 0x02
  112. CLSCTX_LOCAL_SERVER   = 0x04
  113. CLSCTX_REMOTE_SERVER  = 0x10
  114.  
  115. CLSCTX_SERVER = CLSCTX_INPROC_SERVER + CLSCTX_LOCAL_SERVER\
  116.                 + CLSCTX_REMOTE_SERVER
  117. CLSCTX_ALL    = CLSCTX_INPROC_HANDLER + CLSCTX_SERVER
  118. S_OK          = 0
  119.  
  120. STGM_READ = 0x00000000
  121. STGM_SHARE_EXCLUSIVE = 0x00000010
  122. STGM_SHARE_DENY_WRITE = 0x00000020
  123. STGTY_STORAGE = 1
Каким образом будет происходить процесс декомпиляции. Файл CHM будем рассматривать как структурированное хранилище объектов (storage). Такое хранилище является аналогом группы директорий на диске, а потоки (streams), содержащиеся в них, можно представить, как аналоги обычных файлов. Соответственно, работать с хранилищем мы будем так, как если бы мы сканировали на диске директории с вложенными субдиректориями и файлами и выполняли с ними нужные нам действия. Осталось подобрать для этого нужные инструменты.

Так как данные в CHM-файле хранятся в сжатом виде, для работы с ними нам понадобятся некоторые методы интерфейса IITStorage, который, в отличие от обычного IStorage, позволяет работать именно со сжатой информацией. По непонятной причине он официально не документирован, но формат вызова его методов можно посмотреть, например, в исходниках ReactOS. Впрочем, методы интерфейса IStorage нам тоже понадобятся. А для работы с потоками будем использовать стандартный интерфейс IStream.

"Стартовый блок", если можно так его назвать, будет выглядеть следующим образом. Инициализируем COM, создаем интерфейс IITStorage. При помощи метода StgIsStorageFile определяем, является ли обрабатываемый файл хранилищем. Если является, то открываем хранилище при помощи метода StgOpenStorage, рекурсивно извлекаем из него все содержимое, а потом освобождаем созданный объект.
  1.         ; Инициализировать COM-объект
  2.         invoke  CoInitialize,NULL
  3.  
  4.         ; Создать объект
  5.         invoke  CoCreateInstance,CLSID_ITStorage,NULL,\
  6.                 CLSCTX_ALL,IID_IITStorage,pITStDisp
  7.         cmp     eax,S_OK
  8.         jne     loc_exit
  9.  
  10.         ; Получить полный путь к файлу
  11.         invoke  GetFullPathName,sample,MAX_PATH,fname,NULL
  12.  
  13.         ; Проверить, является ли файл хранилищем
  14.         mov     eax,[pITStDisp]
  15.         mov     eax,[eax]
  16.         stdcall dword [eax+IITStorage.StgIsStorageFile],[pITStDisp],fname
  17.         cmp     eax,S_OK
  18.         jne     loc_exit
  19.  
  20.         ; Создать каталог с декомпилированными данными
  21.         invoke  lstrcpy,dir,fname
  22.         invoke  PathRemoveExtension,dir
  23.         invoke  lstrcat,dir,tail
  24.         invoke  PathAddBackslash,dir
  25.         invoke  SHCreateDirectoryEx,NULL,dir,NULL
  26.  
  27.         ; Открыть хранилище
  28.         mov     eax,[pITStDisp]
  29.         mov     eax,[eax]
  30.         stdcall dword [eax+IITStorage.StgOpenStorage],[pITStDisp],\
  31.                 fname,NULL,STGM_READ or STGM_SHARE_EXCLUSIVE,\
  32.                 NULL,0,pstgRoot
  33.         cmp     eax,S_OK
  34.         jne     loc_exit
  35.  
  36.         ; Рекурсивно извлечь данные из хранилища
  37.         stdcall extract,dir,[pstgRoot]
  38.  
  39.         ; Прибраться за собой
  40.         mov     eax,[pITStDisp]
  41.         mov     eax,[eax]
  42.         stdcall dword [eax+IITStorage.Release],[pITStDisp]
  43.  
  44. loc_exit:
  45.         ; Удалить объект
  46.         invoke  CoUninitialize
Основная работа выполняется в рекурсивной функции извлечения данных. На выходе она принимает два параметра: текущий путь к каталогу на диске и интерфейс IStorage, уровень которого в настоящее время обрабатывается. Остальные параметры хранятся на стеке. О переполнении стека беспокоиться не стоит, вряд ли в декомпилируемом CHM-файле будет содержимое со столь гигантской вложенностью каталогов. В крайнем случае можно вынести структуру STATSTG в глобальное пространство для экономии места.
  1. proc extract lpDir:DWORD, Root:DWORD
  2.         locals
  3.                 SubFolder   dd ?
  4.                 Enumerator  dd ?
  5.                 TmpStream   dd ?
  6.                 TmpElement  STATSTG
  7.                 hMem        dd ?
  8.                 pMem        dd ?
  9.         endl
  10.  
  11.         pusha
  12.  
  13.         ; Получить количество элементов в хранилище
  14.         mov     eax,[Root]
  15.         mov     eax,[eax]
  16.         lea     ebx,[Enumerator]
  17.         stdcall dword [eax+IStorage.EnumElements],[Root],\
  18.                 0,NULL,0,ebx
  19.         cmp     eax,S_OK
  20.         jne     .loc_ret
  21.  
  22. .loc_extract:
  23.         ; Извлечь элемент из хранилища
  24.         mov     eax,[Enumerator]
  25.         mov     eax,[eax]
  26.         lea     ebx,[TmpElement]
  27.         stdcall dword [eax+IEnumSTATSTG.Next],[Enumerator],\
  28.                 1,ebx,tmp
  29.         cmp     eax,S_OK
  30.         jne     .loc_ret
  31.  
  32.         ; Это папка?
  33.         cmp     [ebx+STATSTG.type],STGTY_STORAGE
  34.         je      .loc_folder
  35.  
  36. .loc_file:
  37.         ; Извлечь одиночный файл
  38.         invoke  lstrcat,[lpDir],[ebx+STATSTG.pwcsName]
  39.  
  40.         ; Создать файл
  41.         invoke  CreateFile,[lpDir],GENERIC_WRITE,0,0,CREATE_ALWAYS,0,0
  42.         cmp     eax,-1
  43.         je      .loc_file_error
  44.  
  45.         ; Хэндл файла
  46.         mov     esi,eax
  47.  
  48.         ; Открыть поток
  49.         mov     eax,[Root]
  50.         mov     eax,[eax]
  51.         lea     edx,[TmpStream]
  52.         stdcall dword [eax+IStorage.OpenStream],[Root],\
  53.                 [ebx+STATSTG.pwcsName],NULL,\
  54.                 STGM_READ or STGM_SHARE_EXCLUSIVE,0,edx
  55.         cmp     eax,S_OK
  56.         jne     .loc_stream_error
  57.  
  58.         ; Выделить память под данные
  59.         invoke  GlobalAlloc,GMEM_MOVEABLE+GMEM_DDESHARE,\
  60.                 [ebx+STATSTG.cbSize.LowPart]
  61.         mov     [hMem],eax
  62.         invoke  GlobalLock,[hMem]
  63.         mov     [pMem],eax
  64.  
  65.         ; Прочитать данные из потока
  66.         mov     eax,[TmpStream]
  67.         mov     eax,[eax]
  68.         stdcall dword [eax+IStream.Read],[TmpStream],\
  69.                 [pMem],[ebx+STATSTG.cbSize.LowPart],tmp
  70.  
  71.         ; Записать прочитанные данные в файл
  72.         invoke  WriteFile,esi,[pMem],\
  73.                 [ebx+STATSTG.cbSize.LowPart],tmp,NULL
  74.  
  75.         ; Прибраться за собой
  76.         invoke  GlobalUnlock,[hMem]
  77.  
  78.         mov     eax,[TmpStream]
  79.         mov     eax,[eax]
  80.         stdcall dword [eax+IStream.Release],[TmpStream]
  81.  
  82. .loc_stream_error:
  83.         ; Закрыть файл
  84.         invoke  CloseHandle,esi
  85.  
  86. .loc_file_error:
  87.         ; Убрать название файла из текущего пути
  88.         invoke  PathRemoveFileSpec,[lpDir]
  89.         invoke  PathAddBackslash,[lpDir]
  90.  
  91.         ; Следующий элемент
  92.         jmp     .loc_done
  93.  
  94. .loc_folder:
  95.         ; Открыть хранилище
  96.         mov     eax,[Root]
  97.         mov     eax,[eax]
  98.         lea     edx,[SubFolder]
  99.         stdcall dword [eax+IStorage.OpenStorage],[Root],\
  100.                 [ebx+STATSTG.pwcsName],NULL,\
  101.                 STGM_READ+STGM_SHARE_DENY_WRITE,NULL,0,edx
  102.         cmp     eax,S_OK
  103.         jne     .loc_done
  104.  
  105.         ; Создать подкаталог
  106.         invoke  PathAddBackslash,[lpDir]
  107.         invoke  lstrcat,[lpDir],[ebx+STATSTG.pwcsName]
  108.         invoke  SHCreateDirectoryEx,NULL,[lpDir],NULL
  109.         invoke  PathAddBackslash,[lpDir]
  110.  
  111.         ; Рекурсивно извлечь данные из субхранилища
  112.         push    ebp
  113.         stdcall extract,[lpDir],[SubFolder]
  114.         pop     ebp
  115.  
  116.         ; Вернуться на предыдущий уровень в каталоге
  117.         invoke  PathRemoveBackslash,[lpDir]
  118.         invoke  PathRemoveFileSpec,[lpDir]
  119.         invoke  PathAddBackslash,[lpDir]
  120.  
  121.         ; Прибраться за собой
  122.         mov     eax,[SubFolder]
  123.         mov     eax,[eax]
  124.         stdcall dword [eax+IStorage.Release],[SubFolder]
  125.  
  126. .loc_done:
  127.         ; Следующий элемент
  128.         jmp     .loc_extract
  129.  
  130. .loc_ret:
  131.         popa
  132.         ret
  133. endp
C помощью метода EnumElements получаем интерфейс перечислителя (enumerator) IEnumSTATSTG для данного уровня хранилища, затем с помощью его метода Next поочередно перебираем все субхранилища и потоки текущего уровня. Для каждого очередного элемента заполняется соответствующая структура STATSTG, в которой содаржится название субхранилища или название потока, его размер, а также поле type, по которому можно определить его тип. Если это субхранилище, то оно открывается с помощью метода OpenStorage и функция извлечения запускается для нового уровня вложенности. Если это поток (читай: файл), то он открывается методом OpenStream, из него извлекается содержимое и записывается в соответствующий физический файл на диске.

Перед созданием файла или подкаталога как раз надо провести анализ содержимого поля pwcsName на предмет запрещенных символов, дублей файлов, совпадений имени файла и каталога и подобных нештатных ситуаций. В приведенном примере эти проверки отсутствуют, чтобы не загромождать код, но вы можете легко добавить их самостоятельно.

Путешествие по уровням файловой структуры выполняется функциями PathRemoveFileSpec, PathAddBackslash, PathRemoveBackslash и конкатенацией текущего пути с именем обрабатываемого файла или подкаталога. Такой метод может показаться слишком топорным, но он позволяет избавиться от выделения дополнительного места на стеке для хранения текущего пути или создания каких-то вспомогательных сущностей.

В приложении пример программы с исходным текстом, которая декомпилирует CHM-файл и сохраняет его содержимое на диск.

Пример программы с исходным текстом (FASM)Пример программы с исходным текстом (FASM)

CHM.Decompile.Demo.zip (117,047 bytes)


Поделиться ссылкой ВКонтакте
Просмотров: 363 | Комментариев: 0

Комментарии

Отзывы посетителей сайта о статье
Комментариeв нет

Добавить комментарий

Заполните форму для добавления комментария
Имя*:
Текст комментария (не более 2000 символов)*:

*Все поля обязательны для заполнения.
Комментарии, содержащие рекламу, ненормативную лексику, оскорбления и т.п., а также флуд и сообщения не по теме, будут удаляться. Нарушителям может быть заблокирован доступ к сайту.
Наверх
Powered by PCL's Speckled Band Engine 0.2 RC3
© ManHunter / PCL, 2008-2025
При использовании материалов ссылка на сайт обязательна
Время генерации: 0.07 сек. / MySQL: 2 (0.0035 сек.) / Память: 4.5 Mb
Наверх