Декомпиляция 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, структур и констант.
Code (Assembler) : Убрать нумерацию
- ; GUID {5D02926A-212E-11D0-9DF9-00A0C922E6EC}
- CLSID_ITStorage \
- dd 05D02926Ah
- dw 0212Eh
- dw 011D0h
- db 09Dh, 0F9h, 000h, 0A0h, 0C9h, 022h, 0E6h, 0ECh
- ; GUID {88CC31DE-27AB-11D0-9DF9-00A0C922E6EC}
- IID_IITStorage \
- dd 088CC31DEh
- dw 027ABh
- dw 011D0h
- db 09Dh, 0F9h, 000h, 0A0h, 0C9h, 022h, 0E6h, 0ECh
- ; IID_IITStorage Interface
- struct IITStorage
- ; IUnknown
- QueryInterface dd ? ; 000h
- AddRef dd ? ; 004h
- Release dd ? ; 008h
- ; IITStorage
- StgCreateDocfile dd ? ; 00Ch
- StgCreateDocfileOnILockBytes dd ? ; 010h
- StgIsStorageFile dd ? ; 014h
- StgIsStorageILockBytes dd ? ; 018h
- StgOpenStorage dd ? ; 01Ch
- StgOpenStorageOnILockBytes dd ? ; 020h
- StgSetTimes dd ? ; 024h
- SetControlData dd ? ; 028h
- DefaultControlData dd ? ; 02Ch
- Compact dd ? ; 030h
- ends
- ; IEnumSTATSTG Interface
- struct IEnumSTATSTG
- ; IUnknown
- QueryInterface dd ? ; 000h
- AddRef dd ? ; 004h
- Release dd ? ; 008h
- ; IEnumSTATSTG
- Next dd ? ; 00Ch
- Skip dd ? ; 010h
- Reset dd ? ; 014h
- Clone dd ? ; 018h
- ends
- ; IID_IStorage Interface
- struct IStorage
- ; IUnknown
- QueryInterface dd ? ; 000h
- AddRef dd ? ; 004h
- Release dd ? ; 008h
- ; IStorage
- CreateStream dd ? ; 00Ch
- OpenStream dd ? ; 010h
- CreateStorage dd ? ; 014h
- OpenStorage dd ? ; 018h
- CopyTo dd ? ; 01Ch
- MoveElementTo dd ? ; 020h
- Commit dd ? ; 024h
- Revert dd ? ; 028h
- EnumElements dd ? ; 02Ch
- DestroyElement dd ? ; 030h
- RenameElement dd ? ; 034h
- SetElementTimes dd ? ; 038h
- SetClass dd ? ; 03Ch
- SetStateBits dd ? ; 040h
- Stat dd ? ; 044h
- ends
- ; IStream Interface
- struct IStream
- ; IUnknown
- QueryInterface dd ? ; 000h
- AddRef dd ? ; 004h
- Release dd ? ; 008h
- ; IStream
- Read dd ? ; 00Ch
- Write dd ? ; 010h
- Seek dd ? ; 014h
- SetSize dd ? ; 018h
- CopyTo dd ? ; 01Ch
- Commit dd ? ; 020h
- Revert dd ? ; 024h
- LockRegion dd ? ; 028h
- UnlockRegion dd ? ; 02Ch
- Stat dd ? ; 030h
- Clone dd ? ; 034h
- ends
- struct ULARGE_INTEGER
- LowPart dd ?
- HighPart dd ?
- ends
- struct STATSTG
- pwcsName dd ?
- type dd ?
- cbSize ULARGE_INTEGER
- mtime FILETIME
- ctime FILETIME
- atime FILETIME
- grfMode dd ?
- grfLocksSupported dd ?
- clsid rb 16
- grfStateBits dd ?
- reserved dd ?
- ends
- CLSCTX_INPROC_SERVER = 0x01
- CLSCTX_INPROC_HANDLER = 0x02
- CLSCTX_LOCAL_SERVER = 0x04
- CLSCTX_REMOTE_SERVER = 0x10
- CLSCTX_SERVER = CLSCTX_INPROC_SERVER + CLSCTX_LOCAL_SERVER\
- + CLSCTX_REMOTE_SERVER
- CLSCTX_ALL = CLSCTX_INPROC_HANDLER + CLSCTX_SERVER
- S_OK = 0
- STGM_READ = 0x00000000
- STGM_SHARE_EXCLUSIVE = 0x00000010
- STGM_SHARE_DENY_WRITE = 0x00000020
- STGTY_STORAGE = 1
Так как данные в CHM-файле хранятся в сжатом виде, для работы с ними нам понадобятся некоторые методы интерфейса IITStorage, который, в отличие от обычного IStorage, позволяет работать именно со сжатой информацией. По непонятной причине он официально не документирован, но формат вызова его методов можно посмотреть, например, в исходниках ReactOS. Впрочем, методы интерфейса IStorage нам тоже понадобятся. А для работы с потоками будем использовать стандартный интерфейс IStream.
"Стартовый блок", если можно так его назвать, будет выглядеть следующим образом. Инициализируем COM, создаем интерфейс IITStorage. При помощи метода StgIsStorageFile определяем, является ли обрабатываемый файл хранилищем. Если является, то открываем хранилище при помощи метода StgOpenStorage, рекурсивно извлекаем из него все содержимое, а потом освобождаем созданный объект.
Code (Assembler) : Убрать нумерацию
- ; Инициализировать COM-объект
- invoke CoInitialize,NULL
- ; Создать объект
- invoke CoCreateInstance,CLSID_ITStorage,NULL,\
- CLSCTX_ALL,IID_IITStorage,pITStDisp
- cmp eax,S_OK
- jne loc_exit
- ; Получить полный путь к файлу
- invoke GetFullPathName,sample,MAX_PATH,fname,NULL
- ; Проверить, является ли файл хранилищем
- mov eax,[pITStDisp]
- mov eax,[eax]
- stdcall dword [eax+IITStorage.StgIsStorageFile],[pITStDisp],fname
- cmp eax,S_OK
- jne loc_exit
- ; Создать каталог с декомпилированными данными
- invoke lstrcpy,dir,fname
- invoke PathRemoveExtension,dir
- invoke lstrcat,dir,tail
- invoke PathAddBackslash,dir
- invoke SHCreateDirectoryEx,NULL,dir,NULL
- ; Открыть хранилище
- mov eax,[pITStDisp]
- mov eax,[eax]
- stdcall dword [eax+IITStorage.StgOpenStorage],[pITStDisp],\
- fname,NULL,STGM_READ or STGM_SHARE_EXCLUSIVE,\
- NULL,0,pstgRoot
- cmp eax,S_OK
- jne loc_exit
- ; Рекурсивно извлечь данные из хранилища
- stdcall extract,dir,[pstgRoot]
- ; Прибраться за собой
- mov eax,[pITStDisp]
- mov eax,[eax]
- stdcall dword [eax+IITStorage.Release],[pITStDisp]
- loc_exit:
- ; Удалить объект
- invoke CoUninitialize
Code (Assembler) : Убрать нумерацию
- proc extract lpDir:DWORD, Root:DWORD
- locals
- SubFolder dd ?
- Enumerator dd ?
- TmpStream dd ?
- TmpElement STATSTG
- hMem dd ?
- pMem dd ?
- endl
- pusha
- ; Получить количество элементов в хранилище
- mov eax,[Root]
- mov eax,[eax]
- lea ebx,[Enumerator]
- stdcall dword [eax+IStorage.EnumElements],[Root],\
- 0,NULL,0,ebx
- cmp eax,S_OK
- jne .loc_ret
- .loc_extract:
- ; Извлечь элемент из хранилища
- mov eax,[Enumerator]
- mov eax,[eax]
- lea ebx,[TmpElement]
- stdcall dword [eax+IEnumSTATSTG.Next],[Enumerator],\
- 1,ebx,tmp
- cmp eax,S_OK
- jne .loc_ret
- ; Это папка?
- cmp [ebx+STATSTG.type],STGTY_STORAGE
- je .loc_folder
- .loc_file:
- ; Извлечь одиночный файл
- invoke lstrcat,[lpDir],[ebx+STATSTG.pwcsName]
- ; Создать файл
- invoke CreateFile,[lpDir],GENERIC_WRITE,0,0,CREATE_ALWAYS,0,0
- cmp eax,-1
- je .loc_file_error
- ; Хэндл файла
- mov esi,eax
- ; Открыть поток
- mov eax,[Root]
- mov eax,[eax]
- lea edx,[TmpStream]
- stdcall dword [eax+IStorage.OpenStream],[Root],\
- [ebx+STATSTG.pwcsName],NULL,\
- STGM_READ or STGM_SHARE_EXCLUSIVE,0,edx
- cmp eax,S_OK
- jne .loc_stream_error
- ; Выделить память под данные
- invoke GlobalAlloc,GMEM_MOVEABLE+GMEM_DDESHARE,\
- [ebx+STATSTG.cbSize.LowPart]
- mov [hMem],eax
- invoke GlobalLock,[hMem]
- mov [pMem],eax
- ; Прочитать данные из потока
- mov eax,[TmpStream]
- mov eax,[eax]
- stdcall dword [eax+IStream.Read],[TmpStream],\
- [pMem],[ebx+STATSTG.cbSize.LowPart],tmp
- ; Записать прочитанные данные в файл
- invoke WriteFile,esi,[pMem],\
- [ebx+STATSTG.cbSize.LowPart],tmp,NULL
- ; Прибраться за собой
- invoke GlobalUnlock,[hMem]
- mov eax,[TmpStream]
- mov eax,[eax]
- stdcall dword [eax+IStream.Release],[TmpStream]
- .loc_stream_error:
- ; Закрыть файл
- invoke CloseHandle,esi
- .loc_file_error:
- ; Убрать название файла из текущего пути
- invoke PathRemoveFileSpec,[lpDir]
- invoke PathAddBackslash,[lpDir]
- ; Следующий элемент
- jmp .loc_done
- .loc_folder:
- ; Открыть хранилище
- mov eax,[Root]
- mov eax,[eax]
- lea edx,[SubFolder]
- stdcall dword [eax+IStorage.OpenStorage],[Root],\
- [ebx+STATSTG.pwcsName],NULL,\
- STGM_READ+STGM_SHARE_DENY_WRITE,NULL,0,edx
- cmp eax,S_OK
- jne .loc_done
- ; Создать подкаталог
- invoke PathAddBackslash,[lpDir]
- invoke lstrcat,[lpDir],[ebx+STATSTG.pwcsName]
- invoke SHCreateDirectoryEx,NULL,[lpDir],NULL
- invoke PathAddBackslash,[lpDir]
- ; Рекурсивно извлечь данные из субхранилища
- push ebp
- stdcall extract,[lpDir],[SubFolder]
- pop ebp
- ; Вернуться на предыдущий уровень в каталоге
- invoke PathRemoveBackslash,[lpDir]
- invoke PathRemoveFileSpec,[lpDir]
- invoke PathAddBackslash,[lpDir]
- ; Прибраться за собой
- mov eax,[SubFolder]
- mov eax,[eax]
- stdcall dword [eax+IStorage.Release],[SubFolder]
- .loc_done:
- ; Следующий элемент
- jmp .loc_extract
- .loc_ret:
- popa
- ret
- endp
Перед созданием файла или подкаталога как раз надо провести анализ содержимого поля pwcsName на предмет запрещенных символов, дублей файлов, совпадений имени файла и каталога и подобных нештатных ситуаций. В приведенном примере эти проверки отсутствуют, чтобы не загромождать код, но вы можете легко добавить их самостоятельно.
Путешествие по уровням файловой структуры выполняется функциями PathRemoveFileSpec, PathAddBackslash, PathRemoveBackslash и конкатенацией текущего пути с именем обрабатываемого файла или подкаталога. Такой метод может показаться слишком топорным, но он позволяет избавиться от выделения дополнительного места на стеке для хранения текущего пути или создания каких-то вспомогательных сущностей.
В приложении пример программы с исходным текстом, которая декомпилирует CHM-файл и сохраняет его содержимое на диск.
Просмотров: 363 | Комментариев: 0
Комментарии
Отзывы посетителей сайта о статье
Комментариeв нет
Добавить комментарий
Заполните форму для добавления комментария