Принтерная эпопея

Разговоры на любые темы: вы можете обсудить здесь какой-либо сайт, найти единомышленников или просто пообщаться...
Хакер
Телепат
Телепат
Аватара пользователя
 
Сообщения: 16473
Зарегистрирован: 13.11.2005 (Вс) 2:43
Откуда: Казахстан, Петропавловск

Принтерная эпопея

Сообщение Хакер » 25.02.2017 (Сб) 22:33

Есть у меня 4 принтера. Два из них — цветные струйники, один из которых МФУ. Ещё два — ч/б лазерные, один из которых тоже МФУ. Струйники когда-то давно покупал себе я сам: они уже давно не используются, картриджы высохли и потерялись. Лазерники же достались мне в разное время на правах «спасённых от выкидывания» при ликвидации разных предприятий.

В связи с изменением политической обстановки обстоятельств встала задача хоть один из принтеров привести в порядок и сделать доступным через интернет или хотя бы в локальной сети. Чтобы для печати на нём не нужно было предпринимать специальных мер.

Казалось бы: чего сложно — подключил принтер к компьютеру, установил драйверы, сделал принтер сетевым. Но тут надо заглянуть немного в прошлое, потому что не всё гладко с этими принтерами.

Итак, по порядку.

Первый принтер (который достался мне раньше) — Canon LBP-1120.
Изображение

С этим принтером было два вида проблем: программная и механические.

Кривые драйвера рушат любое приложение
Первая проблема всплыла сразу же, как только принтер попал в мои руки и как только я попытался его установить. С сайта Canon были скачаны самые последние драйвера, принтер был установлен, но при первой же попытке что-то напечатать печатающее приложение крэшилось.

Этот принтер был выпущен как минимум 15 лет назад и относится к семейству так называемых win-принтеров. По аналогии с win-модемами, железная часть максимально упрощена, а основная работа по обработке документа возлагается на компьютер. Если говорить точнее, то этим занимается юзермодный драйвер принтера, который, будучи DLL, подгружается в АП печатающего процесса и проводит всю работу.

Так вот сразу же выяснилось, что драйвер Canon LBP-1120 не дружит с DEP и вызывает падение процесса, отправляющего что-то на печать.

Гипотетическим решением тогда могло бы стать отключение DEP для процессов, из которых предполагается осуществлять печать, либо глобальное отключение DEP. Но печатать часто нужно бывает, например, из браузера, а отключать DEP для браузера я бы ни за что не стал, а тем более ни за что я не стал бы отключать DEP глобально.

Поэтому я тогда сделал по другому: для этого принтера с его кривыми дровами был выделен отдельный компьютер. Принтер был подключён к этому компьютеру и на этом компьютере был глобально отключен DEP. Этот компьютер не использовался практически ни для чего, кроме как для печати. На всех компьютерах, откуда предполагалось что-то печатать, был установлен PDF-принтер и нужные документы конвертировались таким образом в PDF. Затем полученные PDF скидывались на тот специальный отдельный компьютер, туда заходили по RDP и отправляли PDF на печать. Для некоторых форматов (например Офис, но не только) на том компе был проинсталлирован соответствующий софт, поэтому можно было скидывать исходник, а не PDF.

Идея сделать принтер сетевым не представляла собой ничего хорошего: в случае сетевого принтера кривой драйвер закачивался машиной, которая собралась печтать, с машины, к которой был подключен компьютер, и этот кривой драйвер запускался на печатающей машине и вызывал на ней крах приложения. Так что практиковалась только печать путём передачи файлов на принт-компьютер.

Мне самому печатать приходилось не так часто, но были периоды, когда принтер был нужен интенсивно (не мне), и в эти периоды приходилось выслушивать много приятного о себе (сапожник без сапог — не может нормально настроить сетевую печать, а заставляют людей иметь такой геморрой с удалённым рабочим столом).


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

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

От такого безобразия механизмы принтера изнашивались, смазка в них густела и высыхала, а сами механизмы забивались тонером.

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

Тогда в 2014 году, не имея возможности заменить умирающий полигон-мотор (мотор, на шпинделе которого установлено 4-гранное (в моём случае) зеркало, отклоняющее лазерный луч), равно как и не имея возможности смазать его (в виду неразборности), было найдено весьма оригинальное решение.

Экспериментируя, я заметил, что зеркало легко раскручивается, если снять плату с полигон-мотором и держать её в руках. Иными словами, если амортизировать вибрацию, возникающую при раскрутке зеркала, если отводить энергию резонанса от платы, то раскрутка происходит нормально. Когда же плата была жёстко закреплена на корпусе лазерного блока, зеркало вообще не могло раскрутиться. При этом, когда плата находилась в руке, ощущалась приличная вибрация самой платы, что свидетельствовало о разбалансировке зеркала.

В два этапа мне тогда удалось реанимировать принтер: сперва вращая призматическое зеркало на оси и фиксирующую его стопорную пружину, я добился максимальной балансировки зеркала и устранения вибрации при раскрутке «в руке». Затем в таком виде я поставил плату и жестко зафиксировал её винтами — в таком состоянии оно иногда могло запускаться, а иногда нет (чаще нет, чем да). Учитывая то, что «в руке» оно раскручивалось без проблем, я поставил плату на место, затянул винты а потом стал их постепенно ослаблять и поймал в итоге определённую конфигурацию, когда с одной стороны платка не сильно болтается, а с другой стороны зеркало без проблем раскручивается со стопроцентной вероятностью.

Конечно, от того, что плата с полигон-мотором была плохо закреплена, работающий принтер стал существенно более шумным. Но на качестве печати это не сказалось: раскрутившееся зеркало стабилизирует свою ось вращения за счёт гироскопического эффекта, поэтому тот факт, что плата по сути дела болталась в корпусе, не приводил вообще ни к каким искажениям при печати (если только не шатать и не дёргать принтер в процессе печати).

Однако буквально месяц назад этот «хитроумный» фикс начал терять свою силу и вновь начались проблемы с раскручиванием зеркала. Когда приспичило и на горизонте появилась перспектива много печатать, состояние ухудшилось до такой степени, что для печати нужно было ассистировать принтер: в момент раскрутки зеркала наносить по боками принтера пару точных резких ударов, которые срывали резонанс и позволяли зеркалу выйти на режим. Без этих ударов зеркало не могло раскрутится быстрее некой резонансной частоты вибрации. Наносить эти «точечные» удары умел только я, что сильно ухудшало ситуацию.

Вторая механическая проблема была уже с первого дня попадания принтера ко мне в руки, но за 6 лет, сколько он у меня, прогрессировала ещё сильнее. Она заключается в том, что при работе принтера (если не обращать внимание на звук полигон-мотора) принтер издавал комбинацию из скрежета, скрипов, громыхания, грохотания, щелчков и тому подобных звуков старой разваливающейся телеги, производящих впечатление механизма, который вот-вот умрёт. Я прямо в душе испытывал что-то вроде чувства вины перед принтером, когда вынужден был что-то на нём печатать, за то, что мучил его и никак не находил времени, чтобы обслужить.


Второй принтер — HP LaserJet M1132
Этот достался мне позже, примерно 1.5 года назад. Первое время я даже не предпринимал попыток его подключить к какой-либо из своих машин и выполнить установку, а использовал только как высокопроизводительный копир (ибо это МФУ), который не нужно подключать к компьютеру, чтобы сделать копии.
Изображение

Сканер у меня уже был в виде HP PSC 1500, который я использовал именно как сканер. По мере того, как Canon начинал издавать всё более страшные звуки, было предпринято несколько попыток установить и заставить этот чёрный МФУ работать.

Но все они были неуспешны.

В первый раз всё сводилось к тому, что любой установочный комплект, который мне удавалось скачать, просто отказывался работать на моей версии Windows. Потом я всё-таки нашёл установочный комплект для XP. Но он на определённом этапе установки требовал подключить принтер к компьютеру, однако когда я принтер подключал, установщик в упор не обнаруживал этого факта и продолжал требовать вставить USB-кабель в принтер и компьютер.

Потом я где-то вычитал, что в этом принтере применена какая-то технология с названием типа easy install (или что-то вроде того). Мол, если установить специальный софт, то при подключении принтера к компьютеру принтер прикинется флешкой, с флешки автозапуск запустит установку нужных драйверов, и всё будет сделано в лучшем виде!.

Действительно, я добился того, что принтер прикидывался флешкой, но так ничего и не установилось.

Потом я нашёл ещё другой дистрибутив: при его запуске происходила долгая распаковка, а в конце запускался тупейший мануал по установке в формате видео-инструкций. Если закрыть анимированное руководство пользователя, то больше не происходило ничего. Но я обнаружил, что этот дистрибутив — это SFX, формат которого понимает 7zip. Разархивировал его вручную, после чего нашёл в обилии папок другой дистрибутив, подходящий моей модели.

Запустил его: опять дошёл до стадии «А сейчас подключите принтер к компьютеру используя USB-кабель», и на этот раз процесс установки успешно обнаруживал подключение принтера и шёл дальше! Однако же, дальше шла стадия «Принтер обнаружен, идёт установка драйверов», которая растягивалась навечно (максимум я выдержал сутки ожидания и убивал процесс).

В конце концов я бросил идею использовать HP-шные кастомные установщики и решил установить принтер через «Установку оборудования». Нашёл нужные файлы вручную, подсунул мастеру нужный inf-файлов и принтер установился.
Изображение
Однако при отправке на него документа или при печати пробной страницы не происходит ровным счётом ничего.


Подытожим проблемы
Итак, на момент, когда приспичило сделать всё по уму на фоне приближающейся нужды много печатать, имели место следующие проблемы.

Canon LBP-1120:
  1. Кривой драйвер делает невозможным использовать его на компьютере с DEP
  2. Нужно включать отдельный компьютер и держать его включенным, если нужно что-то печатать
  3. Отклоняющее зеркало не раскручивается без нанесения ударов — как следствие вообще не может печатать
  4. Дико шумит, даже если зеркало раскрутилось в результате удара.
  5. Помимо полигон-мотора, прочая механика дышит на ладан: скрипит, подклинивает, щёлкает и т.п.
  6. Не сетевой по своей природе: требует, чтобы компьютер, к которому он подключен, был включен.
  7. Не может быть сделан сетевым в принципе из-за проблемного драйвера.

HP LaserJet M1132
  1. Не устанавливается



План действий
Почему-то я решил начать именно с драйвера кэноновского принтера — уж этот принтер по крайней мере хоть как-то печатал. Если бы удалось избавиться от проблемы с драйвером, то это убило бы сразу проблемы №1, №2 и №7, и дало бы мотивацию на устранение других проблем.

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

И даже больше: мой ADSL-модем (тот самый) имеет USB-порт, куда можно подключать флешку, HDD или принтер. С накопителями я успешно имел сетевую шару, а вот в качестве принт-сервера использовать модем ещё не пробовал. Но с правильными драйверами появлялась перспектива подключить принтер к DSL-модему и проинсталлировать драйверы на все машины (которых в сумме — 5). Тогда можно было бы отправлять на принтер задания с любой машины, при этом не требовалось бы ради печати включать или держать включенным вообще ни одного полноценного компьютера. Таким образом удалось бы ещё и решить проблему №6.

Т.е. пришёл с ноутбуком (все компьютеры выключены), распечатал и ушёл с ноутбуком. Без необходимости включать хоть один из компьютеров (хотя, если честно, такого, чтобы все машины были выключены, у меня бывает только в экстренных случаях).

Так что если бы удалось починить драйвер, то половина проблем была бы решена, можно было бы и остальное добить.

Хакер
Телепат
Телепат
Аватара пользователя
 
Сообщения: 16473
Зарегистрирован: 13.11.2005 (Вс) 2:43
Откуда: Казахстан, Петропавловск

Лечим родной драйвер

Сообщение Хакер » 25.02.2017 (Сб) 22:38

Лечим родной драйвер
По этой причине я начал с драйвера. К тому же, чтобы заняться драйвером, не нужно вообще вставать из-за стола — все инструменты под рукой.

Подход был простым: пытаться печатать (хоть из Блокнота) и отладчиком смотреть, что вызывает падение. Драйвер-то не ядерный, а юзермодный, так что всё вообще легко. Да и было понимание, что с высокой степенью вероятности всё лечение будет супер-тривиальным: что скорее всего я наткнусь на вызов VirtualAlloc/HeapAlloc, при котором память выделяется без X-атрибута. Стоило бы чуть-чуть подправить вызов выделяющей функции и всё бы заработало.

Такой фикс заключался бы в правке одного единственного байта в файле! Очень заманчивая перспектива!

Итак, я погрузился в отладку. Стоило отправить абракадабру из Блокнота на печать, как Блокнот вылетел, и я подключился к нему отладчиком (OllyDbg).

Вот исключение и место, где оно возникло:
Изображение

Как и следовало ожидать, был осуществлён переход на адрес 0x0006D4A0, который принадлежит странице, имеющей RW- или R-- атрибуты, то есть без X-флага. Это и вызывает срабатывание DEP и исключение как итог.

В заголовке отладчика написано только «Main thread» и не содержится упоминание модуля, что говорит о том, что текущий фрагмент кода не относится к какому-либо загруженному модулю (EXE или DLL). Нажимаем Ctrl+A (команда Analyze) и видим, что ничего не меняется, что только подтверждает, что мы видим код, размещённый в памяти «на правах» данных.

Наша конечная цель — найти место, где выделяется фрагмент памяти, в который затем копируется код, частью которого является инструкция по адресу 0x0006D4A0, на которую потом впоследствии происходит переход, который всё рушит. Добраться до этого места проще всего двигаясь в обратном порядке.

То есть в первую очередь необходимо выяснить, откуда происходит переход на адрес 0x0006D4A0. Переход мог быть осуществлён инструкцией JMP (или условным джампом), и в этом случае найти место, откуда был сделан переход, можно только пошаговой трассировкой, либо инструкцией CALL, и тогда место, откуда был сделан переход, можно вычислить по адресу возврата, который в данный момент лежит на верхушке стека.

Выяснить, какой вариант из двух — правильный, можно только проверив второй. Если он окажется неверным, значит верный — первый.

Не верхушке стека лежит DWORD 0x50909DC4 с пометкой от отладчика «RETURN to 50909DC4». Судя по отсутствию в этой пометке названия модуля, адрес 0x50909DC4 вероятно тоже не принадлежит какому-либо модулю. При попытке кликнуть по этому DWORD-у правой кнопкой и посмотреть код по этому адресу мы видим, что в меню нет пункта «Follow in Disassembler». Есть только «Follow in Dump». Значит отладчик не предполагает, что по адресу 0x50909DC4 может быть какой-то код.

На самом деле, это ещё ничего не означает. Я всё равно перехожу на этот адрес используя команду at dword[esp] в командной строке отладчика, и вот что там за код:
Изображение

Выглядит как вполне нормальный код, вполне возможно, что инструкция call dword[ebp-8a] как раз и передала управление на адрес 0x0006D4A0 (по которому происходит исключение). Ставим брекпоинт на этой инструкции, чтобы убедиться, что именно она передаёт управление на адрес 0x0006D4A0.

Ставим и перезапускаем процесс.

Но после повторного прогона отладчик не останавливается на инструкции CALL, а останавливается опять на инструкции по адресу 0x0006D4A0, с которой мы начали наш поиск.

Значит ли это, что переход на 0006D4A0 делается не инструкцией CALL по адресу 50909DBE, а какой-то другой инструкцией, а число на верхушке стека — это чистая случайность? Ведь перед попаданием на 0006D4A0 мы не стопнулись на 50909DBE?

На самом деле — конечно нет! Для успешной отладки надо не только знать фичи и инструменты отладчика, но и понимать, как они устроены и работают. Обычные брекпоинты (точки останова) в OllyDbg (и большинстве других отладчиков) устроены так: при установке брекпоинта на инструкцию отладчик перезаписывает первый байт инструкции, меняя его на 0xCC, что соответствует инструкции INT3. INT3 это специальная короткая (однобайтовая) форма инструкции INT xx (кодируемой как CD xx — в случае int 3 это будет пара байтов CD 03), которая возбуждает отладочное прерывание, которое ядром Windows трансформируется в отладочное исключение, которое останавливает выполнение отлаживаемого процесса и передаёт управление отладчику. Отладчик в этот момент восстанавливает первый байт модифицированной инструкции и оставляет за пользователем возможность действовать. Если пользователь решает продолжить выполнении, первый байт опять заменяется на CC. Для пользователя отладчика механизм брекпоинтов кажется прозрачным, но на самом деле (под капотом) каждый брекпоинт — это внесённая отладчиком модификация в отлаживаемый код (которая отменяется на врем пауз в отладчике и делается вновь при продолжении выполнения отлаживаемого процесса).

Из-за такого принципа устройства брекпоинтов надо понимать, что брекпоинт не сработает, если отлаживаемый процесс сам перезаписывает код в своём АП, после того, как брекпоинт установлен. Точно так же брекпоинт не сработает, если он установлен на коде, который был выгружен, а потом вновь загружен по тому же адресу (и отладчик об этом не в курсе).

OllyDbg при завершении отладки сохраняет список всех установленных брекпоинтов и в следующий раз при подключении к отлаживаемому процессу устанавливает эти брекпоинты вновь. Однако, если в момент подключения к отлаживаемому процессу или запуску процесса с нуля под отладчиком, если по нужному адресу в памяти в принципе ничего нет, OllyDbg в принципе не может установить там брекпоинты (потому что установка его есть запись байта CC, а нельзя записывать что-то в невыделенную память).

В нашем случае случае после перезапуска Блокнота под отладкой по адресу 50909DBE нет абсолютно ничего, поэтому OllyDbg и не может установить там брекпоинт. А момент, когда по этому адресу появляется какой-то код (в котором нужно поправить 1 байт, чтобы на нём стопалось выполнение), отладчик отловить не может (хотя и мог бы при должном старании автора OllyDbg).

Дело в том, что как обычное оконное приложение ожидает в цикле прихода новых оконных сообщений, и обычно не делает абсолютно ничего (пока новых сообщений нет), а тупо спит и не выполняется, точно так же отладчик в цикле ждёт отладочных событий (debug events), и пока отлаживаемый процесс выполняется без исключений и пауз, отладчик не получает шанса, чтобы его код выполнился. На самом деле, при желании автора, отладчик может выполнять какие-то действия в фоне и без всяких отладочных событий (как минимум GUI-отладчики обрабатывают оконные сообщения), но автор OllyDbg не стал делать механизм, который бы в фоне проверял появление в памяти новых фрагментов, на которые бы приходились ранее установленные запомненные брекпоинты, и их переустановку путём подмены первого байта инструкций на CC (хотя, безусловно, мог).

Поэтому наш брекпоинт по адресу 50909DBE не срабатал в первую очередь из-за того, что OllyDbg тупо прозевал момент, когда по этому адресу перестала быть абсолютная пустота, а появился какой-то код. Прозевал момент, когда ему нужно было подменить первый байт инструкции и ничего не подменил, поэтому и стоп не произошёл.

Чтобы брекпоинт по этому адресу был жизнеспособным, нужно чтобы с момента начала отладки процесса произошёл хотя бы одна остановка после того, как по адресу 50909DBE появляется код, но до того, как этот код получит шанс выполниться.

Поэтому нам нужно понять, каким образом некий код оказывается по адресу 50909DBE. Вообще-то этот адрес (само число) не похож на типичный адрес, выделяемый функциями HeapAlloc и им подобным — это больше похоже на загруженный модуль (DLL и EXE). Это и есть загруженный модуль, и это видно, если перейти в окно отладчика «Modules» (кнопка [E] в тулбаре).

Модули, которые появились в АП процесса с тех пор, как отладчик в последний раз «проверил» список загруженных модулей, подсвечены красным:
Изображение

Среди них видно и CAP3RDN.DLL с базовым адрес 50900000, на секцию кода которого и приходится та call-инструкция по адресу 50909DBE, на которую мы безуспешно ставили брекпоинт.

Кстати, команда контекстного меню «Actualize» (в окне списка исполняемых модулей) сделает все строки чёрными и позволит отследить ещё новые модули, загруженные уже после выполнения команды «Actualize».

Если вернуться к окну «CPU» отладчика, то уже видно, что после того, как отладчик осознал, что в АП отлаживаемого процесса за время его сна был подгружен модуль «CAP3RDN» (помимо прочих), он теперь отображает и название модуля в заголовке, и в стеке показывает не просто «RETURN to 50909DC4», а «RETURN to CAP3RDN.50909DC4»:
Изображение

Но это только потому, что мы заглянули в список модулей, заставив OllyDbg заново просканировать этот список и расставить брекпоинты ранее установленные брекпоинты там, где он не мог установить раньше.

Если отлаживаемый процесс перезапустить, брекпоинт по адресу 50909DBE опять не будет установлен вплоть до того момента, пока процесс не столкнётся с исключением уже по адресу 0x0006D4A0 — но тогда будет поздно.

Чтобы дать OllyDbg шанс вовремя пропатчить инструкцию по адресу 50909DBE, нужно сделать так, чтобы отлаживаемый процесс стопнулся сразу же после того, как CAP3RDN.DLL будет подгружен в АП отлаживаемого процесса. Для этого нужно найти место в коде, которое подгружает CAP3RDN.DLL. Это делается легко: можно установить брекпоинты на LoadLibraryA/LoadLibraryW/LoadLibraryExA/LoadLibraryExW в kernel32, а можно в опциях OllyDbg временно включить опцию «Break on new modules».

В итоге это место легко находится:
Изображение

Модуль CAP3K.DLL запрашивает загрузку CAP3RDN.DLL

Нужно поставить брекпоинт после вызова LoadLibraryW, потому что если отладчик стопнется до загрузки библиотеки, толку от этого не будет никакого, а вот если после — то он увидит новый код по по адресам 50900000—5090E000 и применит брекпоинт (то есть пропатчит первый байт инструкции по адресу 50909DBE) и тогда мы сможем на ней остановиться.

Теперь перезапускаем отладку процесса с нуля. Выводим произвольный текст на печать. Сначала мы стопаемся после момента подгрузки CAP3RDN.DLL. В этот момент нужно глянуть список модулей в OllyDbg и продолжить выполнение отлаживаемого процесса. А вот следующая остановка происходит как раз на интересующей нас call-инструкции:
Изображение

В итоге, мы оказались правы с изначальным предположением, что именно эта call-инструкция передаёт управление на фрагмент кода, лежащей в странице, не имеющей права на выполнение. Теперь достаточно найти место, где этот фрагмент памяти выделяется и поменять способ, которым он выделяется, на DEP-friendly.

Очевидно, что драйвер принтера выделяет в памяти кусочек, в который записывает код, а далее по ходу работы передаёт управление этому коду. Нужно найти место, где выделяется память под этот динамически формируемый код, и если это будет VirtualAlloc, то модифицировать комбинацию флагов, передаваемую в 4-й параметр, добавив бит PAGE_EXECUTE.
Если это будет HeapAlloc, то нужно найти момент создания кучи (вызов HeapCreate) и опять-таки модифицировать значение, передаваемое в 1-й параметр (добавив бит HEAP_CREATE_ENABLE_EXECUTE).

Я решил выяснить, какой функцией модуль CAP3RDN.DLL выделяет память под динамически формируемый кусочек кода, а потом на все вызовы этой функции поставить брекпоинты и выяснить, где именно (откуда) выделяется память под фрагмент и пропатчить этот код.

Однако, посмотрев импорты CAP3RDN.DLL я в первый раз разочаровался: она не использует ни VirtualAlloc, ни HeapAlloc.
Изображение
Все выделения памяти делаются с помощью GlobalAlloc, что портит дело, потому что фиксом в 1 байт уже не обойдётся: придётся модифицировать таблицу импорта и добавлять больше кода в секцию данных.

Я поставил брекпоинты на все вызовы GlobalAlloc, сделал прогон, но среди выделяемых фрагментов не оказалось фрагмента, по которому впоследствии осуществляется переход. Как же так? Ведь больше библиотека никак память выделять не может, она её выделяет, но не вызовом GlobalAlloc. А чем же ещё?

Оставался только один вариант, и я подошёл к выяснению этого момента с другой стороны.

Адрес, по которому нужно перейти, инструкция CALL берёт по адресу dword[EBP-8A]. А как этот адрес попадает попадает в DWORD[EBP-8A]?

Я просто просмотрел код снизу вверх и нашёл место, где адрес, по которому произойдёт вызов, помещается в ячейку по адресу EBP-8A:
Изображение

Вот оно что! Место под динамически формируемый кусок кода выделяется из стека! Поэтому ни один вызов GlobalAlloc и не вернул интересующий меня адрес. Вот тут хорошо видно, что из стека выделяется 400h байтов памяти, полученный адрес записывается в регистр EDI, а затем это значение записывается в локальную переменную по адресу EBP-8A. Далее вызывается процедура cblt, которая, как оказалось, на месте выделенного из стека участка и формирует код, который позднее будет выполнен. Чуть позже адрес кода извлекается из DWORD[EBP-8A] и делается вызов.

Сперва я подумал, что код этой процедуры (не cblt, а той, которая выделяет память на стеке, она называется RP_BITMAP1T01) написан на Си и использует alloca. Но вскоре я понял, что код написан не на Си, а скорее всего на ассемблере, по двум причинам:
  1. При вызове cblt адрес буфера, в котором cblt сформирует кусок кода, передаётся через регистр EDI, что не соответствует ни одному из стандартных соглашений о вызове (это не fastcall!).
  2. При том, что используется EBP-относительная адресация стека, после злосчастного вызова выделенные из стека 400h байтов «освобождаются» инструкцией add esp, 400h, что не соответствует тому, какой код генерируется сишными компиляторами при использовании alloca.

Кому интересно, кусок из RP_BITMAP1T01 полностью: выделение, генерация кода, вызов сгенерированного кода, освобождение.
Изображение


Что можно сказать? Авторы драйвера — те ещё затейники. Мало того, что они использует динамически генерируемый код (не имею ничего против), так они место под него выделяют прямо из стека! Хотя на первый взгляд это может показаться кривым подходом, в этом есть разумное зерно: видимо подход с динамической генерацией кода применён в целях увеличения производительности. При этом каждый раз генерируется новый код (раз формирование кода не вынесли «за скобки»). А раз так, выделять память нужно каждый раз. А выделение памяти с помощью WinAPI-функций убило бы весь выигрыш в скорости от использования динамически формируемого кода. В то время как выделение кусочка из стека — чрезвычайно быстрая вещь по сравнению с системным вызовом (если только при росте стека вверх не произойдёт расширение набора закоммиченных страниц стека (но оно происходит 1 раз)).

С другой стороны я посмотрел: во всей библиотеки есть только 1 обращение к процедуре cblt — вот это, которое мы видим. Может быть помимо cblt есть и другие процедуры, выполняющие генерацию кода, но если нет, то значит это единственное место с кодогенерацией, а раз так, то выделение буфера под генерируемый код можно было бы вынести «за скобки», то есть делать 1 раз — в таком случае нет ничего зазорного использовать системные вызовы.

Что ж, по крайней мере теперь у нас есть все знания, необходимые, чтобы пофиксить этот момент. Может быть, конечно, после того, как мы сделаем это место DEP-friendly, мы вывалимся следом где-нибудь ещё — узнаем только после того, как попробуем.

Есть два способа вылечить этот код: вместо выделения памяти со стека выделять из кучи, либо оставить выделение из стека, но менять атрибуты доступа страницы стека (исключительно на время выделения 1024-байтового фрагмента стека под код). Второй вариант не очень хорош, ведь он делает 1 или даже 2 страницы стека потенциально опасными с точки зрения уязвимиости.

Первый же осложнён тем, что либо память придётся выделять с помощью VirtualAlloc — а это влечёт за собой необходимость ещё и резервировать память. Резервируется память блоками по 64кб, а выделяется страницами по 4кб, а ведь нам нужно-то 1024 байта всего! Да и резервирование+выделение — долгая операция. Либо придётся выделять с помощью HeapAlloc, а это влечёт за собой необходимость создавать кучу с правом выполнения для её станиц. Создание кучи — чертовски тяжелая операция (в рассматриваемых масштабах!), к тому же, нужно где-то хранить хендл кучи, что представляет собой особую проблему, ведь код драйвера наверняка должен быть многопоточным, а значит «взять» себе место из секции данных под переменную не получится.

Модифицировать DllEntryPoint-процедуру и выделять память в ней тоже не получится: у этой DLL точки входа попросту нет! (Хотя можно сделать).

Только дальнейшее исследование может показать, какой вариант выбрать и как быть.

Для проверки я выбрал вариант с VirtualAlloc, причём на каждый вызов RP_BITMAP1T01 будет приходиться резервирование+выделение, а затем разрезервирование.
Я накидал прямо в отладчике вот такие кусочки кода, которые выделяют и освобождают память:
Изображение
А затем в оригинальном коде вместо выделения/освобождения участка на стеке сделал вызовы моих пробных кусочков:
Изображение

Запускаем на печать — и всё срабатывает на ура. Задание уходит на принтер, принтер начинает завывать, пытаясь раскрутить зеркало, а после правильных точечных ударов — распечатывает то, что нужно.

Важные выводы:
  1. Мы в принципе победили кривость драйвера, пусть и черновым путём пока.
  2. В драйвере только 1 место, где применена кодогенерация и где в качестве буфера для кода использовался стек.
  3. Размер этого буфера всегда 0x400 байт.
  4. Может быть выводы 2 и 3 ошибочны — ведь я тестировал пока только на Блокноте, а попытка печати сложной графики может дать опять крэш.

«Чёрность» этого чернового способа состоит из двух компонентов:
  • Абсолютная: мы не импортируем функции VirtualAlloc и VirtualFree, как это следовало бы делать, а пока что жестко вшили их адреса в свой кода. Это значит, что попади драйвер на чуть другую версию ОС с другой kernel32.dll или обновись сама kernel32.dll — и ничего работать не будет.
  • Относительная: мы выделяем и освобождаем блок по каждому чиху, то есть ухудшаем производительность.

Чтобы понять, насколько критичен такой наплевательский подход к производительности, а также развеять сомнения из пункта №4, я хочу погонять этот фикс в других приложениях, выводящих сложную растровую и векторную графику. Поскольку потенциальных приложений-кандидатов для тестирования много, и подключаться отладчиком к каждому и всякий раз вносить одни и те же правки в код — утомительно, я пропатченный образ DLL материализую в виде DLL-файла.

Но раз мы собираемся гонять пропатченный драйвер без отладчика, нужно же как-то получать от него информацию. Поэтому добавляем в начало кусочка hacked_alloc400 пару инструкций вызова OutputDebugString, ну и саму строчку чуть позже прямо в секцию кода:
Изображение

Естественно, поскольку начало кусочка hacked_alloc400 переместилось назад на 10 байт, нужно подкорректирвоать инструкцию call, которая вызывает hacked_alloc400.

Помимо этого, есть ещё один момент. Модуль CAP3K.DLL взаимодействует с CAP3RDN.DLL через несколько экспортируемых последней функций.

Вот что экспортирует CAP3RDN.DLL:
Изображение

И вот как сразу после загрузки (LoadLibrary, где мы ставили брекпоинт в начале) CAP3K.DLL отложенно импортирует эти функции:
Изображение

Так вот, я хочу понаблюдать, как коррелирует содержимое выводимого на печать документа с вызовами этих функций, и как вызовы этих функций соотносятся с вызовами процедуры RP_BITMAP1T01 (в которой мы теперь выделяем память). И как следствие узнать, сколько циклов выделения/освобождения приходится на обработку разных документов.

Для этого я «перехвачу», вернее модифицирую прологи всех этих экспортируемых функций, сделав вывод отладочного сообщения в начале каждой из них. Перехват покажу на примере hHREOpen:

Длинный джамп (инструкция) занимает 5 байтов (1 байт на опкод и 4 байта относительное смещение), значит первые 5 байтов функции нужно перенести в другое место.

Ставим в оригинальной функции метку (contOf_hHREOpen), куда мы собираемся вернуться из перенесённого кусочка:
Изображение
В конце секции кода набиваем кусочек кода из 5 оригинальных push-инструкций (которые будут заменены на джамп), пару инструкций вызова OutputDebugString и возврат к оригинальному коду:
Изображение
И в начале оригинальной функции ставим переход на собственный кусочек:
Изображение
Обратите внимание на маленькую хитрость: строчку «hHREOpen» (адрес 5090B070) я взял в директории экспорта DLL-модуля — зачем дублировать то, что уже есть.

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

В принципе можно было (и это было бы даже красивее!) перехватить начала функций иначе: не трогать и не модифицировать прологи, а просто модифицировать RVA-указатели в таблице экспорта, перевешав их на собственные переходнички, которые только выводят отладочные строки и сразу прыгают на оригинальные нетронутые прологи.

И сохраняем пропатченный DLL файл:
Изображение

Я сохранил два файла:
  • CAP3RDN_patched.DLL — с исправленным багом падения но без вывода отладочных сообщений.
  • CAP3RDN_patched_verbose.DLL — исправленный + вывод отл. сообщений с помощью OutputDebugString.

Теперь подменяем оригинальный драйвер в system32: благо он не охраняется SFP и не имеет цифровой подписи (которая бы нарушилась от наших модификаций).

Теперь мы можем из любого приложения выводить что-либо на печать и при помощи программы DebugView (или любого аналога) смотреть, какие функции из драйвера печати дёргаются, сколько раз, и сколько там циклов выделения/высвобождения памяти.

Вот несколько тестов (слева — что печатаем, справа — лог вызовов экспортируемых ф-ций и циклов выделения памяти):
Изображение
Изображение
Изображение
Изображение
Изображение
Изображение
Изображение

Как можно увидеть на этих тестовых примерах, не так уж и много циклов выделения памяти (в вызовах RP_BITMAP1T01) происходит при печати чего-то примитивного. Например можно сделать вывод, что на каждую выводимую букву приходится один такой цикл (но некоторый софт печатает некоторые группы букв дважды?).

Ладно, это всё баловство. Напечатаем что-нибудь тяжеловесное:
Изображение

Вывод этого чертежа выдал аж 153 тысячи отладочных строк в DebugView! И обработка задания заняла драйвером 20 секунд! Для желающих самостоятельно покопаться в этом логе, вот он:
prn_log_of_drawing.zip
Лог вывода отладочных сообщений при печати чертежа
(1.01 МиБ) Скачиваний: 181

Но рано думать, что такое падение производительности вызвано нашим фиксом (многократным вызовом VirtualAlloc/VirtualFree) — скорее всего bottleneck в данном случае кроется в самом выводе отладочной информации.

Сделаем ещё одну пропатченную версию драйвера, убрав вывод отладочной информации отовсюду, кроме hHREOpen и uiHREClose (которые вызываются в самом начале и в самом конце обработки задания).

Печатаем тот же самый чертёж и получаем уже совсем не такую страшную картину:
Изображение

Чуть меньше, чем половина секунды — вполне себе приемлемый результат.

Ладно, можно считать лечение успешным. После того, как остальные проблемы с принтером будут устранены, можно будет вернуться и улучшить фикс, вынеся выделение памяти «за скобки», применив TLS для хранения указателя на 400h-байтный буфер.

А пока нужно довести вылеченный драйвер до кондиции, сделав нормальный импорт функций VirtualAlloc и VirtualFree.

Сейчас у нас 5 импортов из kernel32.dll, а будет 7. Для этого размер таблицы IAT и таблицы лукапов нужно увеличить на 2 ячейки (каждую). Тут нас ждёт первая трудность: таблица IAT, в которую загрузчик расставит адреса импортируемых функций, расположенная (в данном случае) в самом начале секции кода, не может расти вниз, потому что там лежат какие-то непонятные данные, и таблица лукапов тоже не может расти вниз, потому что следом за ней идёт цепочка структур IMAGE_IMPORT_BY_NAME.

Цепочку структур IMAGE_IMPORT_BY_NAME мы легко можем подвинуть (подправив их RVA в IAT и таблице лукапов), а вот что за данные идут следом за IAT — совершенно не ясно. Причём там идёт большой блок данных, в котором есть даже строчка Win32 Render for CAPT. Причём я заставил OllyDbg поискать все ссылки к интересующему блоку и отладчик не нашёл ни одной! Что выглядит подозрительно и наводит на мысль, что для доступа к этим данным используется непрямой доступ с использованием адресной арифметики, которую отладчик, ясное дело, своим довольно-таки простым анализом обнаружить не может.

Вполне возможно, что это в самом деле неиспользуемое мусорное пространство, и можно было бы рискнуть и захватить 8 байт из этой области под свои нужды. Но мы не будем рисковать и пойдём другим путём.

В PE-заголовке находится RVA-указатель на начало таблицы десктрипторов импорта. Каждой импортируемой DLL-библиотеке соответствует отдельный дескриптор (элемент) в таблице дескрипторов импорта. Но, хоть это обычно не принято, ничего не мешает для одной и той же библиотеки иметь более одного дескриптора импорта. Обычно этим грешит линкер Borland'овских продуктов (Delphi и C++ Builder).
Изображение
Раз мы не можем увеличить размер IAT, мы сделаем вторую IAT и вторую таблицу лукапов и разместим их там, где нам удобно и ничего не мешает (а именно — в конце секции). Но расширить саму таблицу дескрипторов мешает следующая прямо за таблицей дескрипторов импорта таблица лукапов для первого дескриптора.

Пустой (терминирующий) дескриптор придётся сдвинуть вниз — он займёт место, которое сейчас занимает ILT для первого дескриптора. Она (ILT — таблица лукапов) имеет размер 6 DWORD-ов, что достаточно для помещения на её место сдвигаемого дескриптора, размер которого — 5 DWORD-ов. ILT мы перенесём вниз, а цепочку из структур IMAGE_IMPORT_BY_NAME для первого дескриптора можно оставить там, где она сейчас.

Сам дескриптор импорта имеет такую структуру:
Код: Выделить всё
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    DWORD   OriginalFirstThunk;      // RVA таблицы лукапов (ILT)
    DWORD   TimeDateStamp;           // Timestamp для bound-импорта
    DWORD   ForwarderChain;         
    DWORD   Name;                    // RVA строки с именем модуля
    DWORD   FirstThunk;              // RVA таблицы адресов (IAT)   
} IMAGE_IMPORT_DESCRIPTOR;


Копируем оригинальную Import Lookup Table вниз (начало будет по адресу 50909E70). Первое поле первого дескриптора (RVA на ILT) меняем с 9E00 на 9E70):
Изображение
Следом за перенесённой вниз оригинальной ILT формируем вторую ILT, вторую IAT и пару структур IMAGE_IMPORT_BY_NAME для функций VirtualFree и VirtualAlloc:
Изображение
В этих структурах помимо имён функций содержится 16-битный хинт — это просто порядковый номер функции в таблице экспорта соответствующей библиотеки. Этот номер позволяет загрузчику быстрее при импорте найти соответствующую запись в таблице экспорта.

Хинты можно посмотреть любой утилитой — я, например, посмотрел в Dependency Walker:
Изображение

Дальше на том месте, где раньше был нулевой (теминирующий) дескриптор импорта формируем второй дескриптор импорта для kernel32.dll. А на том месте, где раньше была оригинальная ILT, формируем новый пустой (терминирующий) дескриптор импорта. В новом (втором) дескрипторе импорта проставляет RVA на новую ILT, новую IAT и старую строчку KERNEL32.dll:
Изображение
После этого копируем внесённые изменения в представление файла-образа (команда Copy to executable → All modifications контекстного меню), а затем представление файла-образа в файл (команда Save file контекстного меню).

Теперь можно увидеть, что у полученного файла появился второй импорт из библиотеки kernel32.dll, отвечающий за пару VirtualAlloc+VirtualFree:
Изображение

После повторной загрузки драйвера под отладчиком на месте новой IAT должны быть уже не RVA, а самые настоящие адреса функций, проставленные сюда загрузчиком Windows.

Как бы не так! При попытке повторной отладки наш модифицированный драйвер вообще не может загрузиться — где-то в недрах NTDLL возникает исключение (access violation) при попытке записи! Это исключение возникает как раз в момент проставления реального адреса функции в ячейку IAT загрузчиком: дело в том, что страница секции кода, на которую приходится IAT, имеет атрибуты защиты, запрещающие запись (ибо вся секция имеет атрибуты защиты READ+EXECUTE, а не READ+WRITE+EXECUTE). При попытке записи адреса в страницу только-для-чтения и возникает исключение.

Но как же тогда загрузчик проставлял адреса в оригинальную (родную) IAT, которая была в драйвере всегда, ведь эта IAT тоже лежит в начале той же секции кода, недоступной для записи? Разгадка проста: на время заполнения таблицы IAT загрузчик изменяет атрибуты защиты соответствующих страниц на PAGE_READWRITE. Когда же IAT заполнена актуальными адресами, страницам, на которые приходилась IAT, возвращаются прежние (оригинальные) атрибуты защиты.

На обработку каждого дескриптора импорта вызывается функция LdrpSnapIAT из NTDLL. Она меняет атрибуты защиты страниц на PAGE_READWRITE (для обозначения сего действия разработки Windows применяют термин «unprotect»), затем в цикле вызывает LdrpSnapThunk для каждой ячейки IAT (т.е. для каждой импортируемой функции/сущности), после чего восстанавливает страницам прежние атрибуты защиты.

Если загрузчик так поступает, почему же в LdrpSnapThunk при проставлении значения (адреса) в ячейку IAT возникает исключение? Всё дело в том, как LdrpSnapIAT определяет диапазон страниц, которым нужно поменять атрибуты защиты. Для этого она смотрит отдельную директорию в таблице директорий PE-файла, которая называется «IAT Directory» (не путать с Import Directory!). Если в IAT-директории есть база и размер IAT-зоны, то именно этот диапазон страниц подвергается unprotect'ингу. Обычно модуль импортирует из множества других модулей, и IAT-ы разных модулей лежат компактно — вплотную друг к другу. На такое плотное компактное скопление IAT-таблиц и указывает IAT-директория. В случае же, если IAT-директория не заполнена, загрузчик пытается самостоятельно вычислить диапазон страниц, которые нужно unprotect'ить. При этом он действует довольно примитивно: ищет секцию, в которой лежит таблица дескрипторов импорта и unprotect-ит всю секцию целиком! Да, размер IAT нигде не хранится, но можно было бы пройтись IAT и вычислить её длину, а дальше менять атрибуты защиты только необходимым страницам, а не вообще всем, но так не сделали — оставим это на совести разработчиков Windows (кстати, может быть в последних версиях это поведение поменяли).

Посмотрим на IAT Directory в PE-заголовке нашего драйвера:
Изображение
Как можно заметить, поля «RVA» и «Size» IAT-директории заполнены, и значит загрузчик (LdrpSnapIAT) использует именно этот диапазон для определения набора страниц, атрибуты защиты которых нужно поменять. В оригинале IAT из 5 ячеек попадала в этот диапазон, но сейчас у нас две IAT: одна в начале секции кода, другая в самом конце.

Выхода у нас два: либо занулить поля IAT-директории в PE-заголовке, либо дотянуть поле Size (которое сейчас равно 0x18) так, чтобы оно захватывало и нашу новую (вторую) IAT.

После исправления этой проблемы, как я и говорил, при загрузке драйвера в отладчике на месте новой IAT уже не RVA, а реальные адреса импортируемых функций:
Изображение

Остаётся только заново сделать другие варианты hacked_alloc400 и hacked_free400 — вызывающие VirtualAlloc и VirtualFree уже через IAT, а не через жестко вшитые адреса:
Изображение

После выполнения такой модификации и сброса модифицированного образа в файл драйвер успешно загружается в АП, печать работает на ура, и казалось бы всё сделано, но на самом деле нет!

На самом деле осталось ещё две вещи, о которых мы забыли, которые, пока не сделаны, оставляют наш пропатченный драйвер неполноценным уродцем.

Во-первых, наш проблемный драйвер печати — это DLL, а DLL, впрочем как и любой PE-модуль, может быть загружена не потому адресу, по какому планировал её создатель, а по совершенно другому, если изначально желаемый диапазон адресного пространства занят чем-то другим, либо если задействована технология ASLR.

При загрузке по неродному базовому адресу все абсолютные адреса, вшитые в код и данные образа, становятся некорректными и недействительными. Для исправления этой ситуации все они должны быть скорректированы на величину, равную разнице между фактическим базовым адресом загрузки и желаемым базовым адресом загрузки. При этом список мест, которые должны подвергнуться коррекции, содержится в самом PE-файле: это так называемые релоки (или релокации), которые обычно находятся в отдельной секции (как правило с именем .reloc и флагом IMAGE_SCN_MEM_DISCARDABLE, позволяющим не проецировать секцию на АП процесса после завершения загрузки PE-модуля).

Релоки могут отсутствовать в образе PE-модуля (и обычно всегда отсутствуют у EXE, и всегда присутствуют у DLL, OCX, SYS и т.п.) — в этом случае сам PE-модуль будет иметь установленным флаг IMAGE_FILE_RELOCS_STRIPPED в поле характеристик PE-заголовка, а при попытке загрузить такой модуль в АП, если соответствующий диапазон страниц АП окажется занятым хотя бы частично, загрузка вообще не состоится.

Наш проблемный драйвер имеет секцию релоков (и не имеет флага IMAGE_FILE_RELOCS_STRIPPED, соответственно), а значит является перемещаемым, то есть поддерживается загрузку по неродному базовому адресу. Проблема, однако же, состоит в том, что внедрённые нами в конец секции кода фрагменты hacked_alloc400 и hacked_free400 обращаются к ячейкам IAT, используя абсолютные адреса. Эти абсолютные адреса, вшитые в код, не подвергнутся коррекции при загрузке по неродному базовому адресу, потому что мы их сами добавили в образ, не добавив ничего в релоки, и в итоге окажутся недействительными — при попытке что-то печатать, как только дело дойдёт до выделения буфера под динамически генерируемый код, драйвер обрушит всю программу, пытающуюся что-то напечатать.
Изображение

Из этой ситуации есть два выхода:
  1. Своими руками расширить секцию релоков, добавив туда упоминание ещё двух новых мест, которые должны подвергаться коррекции.
  2. Выставить флаг IMAGE_FILE_RELOCS_STRIPPED, запретив таким образом загружать DLL по неродной базе и устранив проблему на корню.

Второй способ, при своей небывалой простоте, является невообразимо глупым решением: будучи драйвером печати, эта DLL должна подгружаться в самые невообразимые и фантастические окружения (имеются в виду АП процессов) и быть способной втискиваться в самые фрагментированные АП, лишь бы только в нём нашёлся свободный кусочек памяти (АП), достаточный для загрузки её туда. И загружается она в АП процесса не в начале его работы, когда АП ещё более пустое и менее фрагментированное, а как правило в конце (долго набирали/рисовали документ, теперь пришла пора напечатать), причём подгружаться ей приходится в самые разнообразные процессы (любые, которые потенциально что-то могут выводить на принтер). Совершенно невозможно предсказать или быть уверенным, что диапазон адресов 0x509000000x5090E000 в целевом процессе окажется свободным — запросто он может оказаться чем-то занятым к моменту подгрузки драйвера печати, и было бы совершенной глупостью делать драйвер, исход успешной загрузки которого будет зависеть от такого случайного фактора.

Остаётся только первый вариант.

Есть на самом деле ещё и третий вариант: при формировании внедряемых кусочков можно воспользоваться классическим «хакерским» трюком по определению EIP с помощью фиктивной call-инструкции:
Код: Выделить всё
         call label
label:   pop eax      ; EIP теперь содержится в EAX

А дальше вычислить адрес нужной IAT-ячейки относительно полученного EIP. Абсолютные адреса в таком случае вообще бы не использовались и проблемы с загрузкой по неродному базовому адресу (неродной базе) не было бы изначально. Но! Этот трюк не очень «чистый» и может действовать на антивирусы как красная тряпка на быка: проблема базонезависимости (точнее базозависимости) — это классическая проблема, возникающая при внедрении нового кода в уже сформированный PE-модуль, а внедрениями занимаются в 99% случаев именно вирусы.

Поэтому мы займёмся модификацией таблицы релоков. Кстати, в OllyDbg можно очень легко увидеть места, подвергаемые модификации при релокации (при загрузке по неродной базе) — при отображении дампа секции такие места подчёркиваются серой линией.
Изображение
На такие подчёркивания нужно внимательно смотреть, если вы модифицируете оригинальный код в прямо по месту: там, где до модификации кода были абсолютные адреса, нуждающиеся в коррекции при перемещении, после модификации могут оказаться опкоды или аргументы совершенно других инструкций, которые будут неизбежно испорчены с непредсказуемыми последствиями, если вдруг образ при загрузке будет загружен по неродному базовому адресу.

Хотя таблица релокаций обычно лежит в отдельной секции с именем .reloc, ни имя секции, ни сам факт её наличия — не играют никакой роли. Загрузчик PE-модулей в Windows-находит таблицу релокаций глядя в таблицу директорий данных (в соответствующей ячейке там хранится RVA и размер таблицы релокаций), существование при этом отдельной секции с каким-то определённым именем совершенно не обязательно — таблица релокаций может запросто лежать и в секции кода или любой другой.

Посмотрим на соответствующую ячейку таблицы директорий данных в составе PE-заголовка нашего драйвера. В нашем случае начало таблицы релокаций совпадает с началом секции .reloc:
Изображение

Если дважды щёлкнуть по секции .reloc в окне Memory Map, отладчик покажет нам содержимое этой секции, и к нерадости неискушённых товарищей содержимое будет отображено вот так:
Изображение

Иными словами, отладчик OllyDbg не умеет нормально отображать таблицу релокаций, показывая её элементы, структуры, поля, их смысл и назначение. Точно так же он не позволяет легко вносить в неё правки пользователю. Максимум — это отображение hex-дампа секции и возможность править произвольные байты произвольным образом. OllyDbg версии 2, который у меня тоже есть, но которым я реже пользуюсь (из-за меньшей стабильности и отсутствия поддержки плагинов), по прежнему не умеет красиво отображать таблицу релоков.

Но не стоит опускать руки раньше времени: таблица релокаций имеет сравнительно несложный формат, и поняв её устройство, будет не так уж сложно дополнить её голыми руками.

Вся таблица релокаций (таблицей она называется весьма условно) представляет собой последовательность блоков. Размер блоков не фиксированный. Каждый блок состоит из 8-байтного заголовка (два DWORD-а), с которого начинается блок, и некоторого числа 2-байтных элементов, которые идут следом за заголовком.

Заголовок состоит из двух DWORD-полей — в одном из них хранится размер блока в байтах (включая сам заголовок, все 2-байтные элементы в составе этого блока и выравнивающие нули в конце блока (если они есть)). Следом за заголовком идут элементы: каждый элемент (16 бит) описывает отельное место в образе, которое подлежит коррекции в случае загрузки по неродной базе. Поскольку число элементов в блоке может быть нечётным, итоговый размер блока может оказаться не кратным 4 байтам. По этой причине начала блоков выравниваются по 4-байтной границе, а концы предшествующих блоков добиваются до этой границы нулями.

Структура 16-битного элемента в составе блока следующая: младшие 12 бит хранят смещение корректируемого места, а старшие 4 бита — тип релока (а их существует множество разных видов — об этом ниже). Поскольку смещение относительно базы (иными словами — RVA) не способно уместиться в 12-битное поле, общий (для всех элементов одного блока) RVA хранится в заголовке блока (это второе из полей заголовка, кроме поля с размером блока), а в каждом элементе хранится смещение корректируемого места относительно общего для всего блока RVA.

Иными словами, каждый такой блок таблицы релокаций сопоставляется с участком образа размером 4 Кб (по чистому совпадению размер региона, описываемого одним блоком таблицы релокаций, совпадает с размером страницы x86). В заголовке блока лежит RVA четырёхкилобайтного окна, на которое приходится некоторое количество релокаций, а каждый элемент в составе блока описывает описывает одну отдельно взятую релокацию из этого окна и хранит в себе смещение этой релокации относительно начала четырёхкилобайтного окна и тип релокации. Причём 4 кб регион, описываемый отдельным блоком, не обязан быть выровнен по границе страницы, блоки в принципе могут иметь начала в произвольных местах образа и вообще пересекаться между собой.

Примечательно, что в составе 16-битного элемента поля разделены по ширине именно как 4 и 12 бит: при таком разделении в hex-записи 12-битному полю (хранящему смещение) соответствует 3 младших hex-цифры, а 4-битному полю (хранящему тип) соответствует старшая hex-цифра.

Теперь что касается типов релокаций: их несколько. Как минимум, исходя из того факта, что под хранение типа релокации выделено 4 бита, количество возможных значений этого поля равно 16. На самом же деле, загрузчик Windows различает 12 типов релокаций:
Код: Выделить всё
0   ABSOLUTE             
1   HIGH                 
2   LOW                   
3   HIGHLOW               
4   HIGHADJ               
5   MIPS_JMPADDR         
6   SECTION               
7   REL32                 
9   IA64_IMM64           
10  DIR64                 
11  HIGH3ADJ             

Тип релокации определяет, сколько байтов и по какому принципу корректируется. Поскольку релокации как правило корректируют абсолютные адреса, коррекции может подвергаться старшая часть адреса, младшая часть, старшая и младшая части и т.п.

Типы MIPS_JMPADDR и IA64_IMM64 не относятся к архитектуре x86. Тип ABSOLUTE означает, что коррекция адреса не требуется (вопреки названию, потому что абсолютным адресам как раз таки коррекция и нужна — видимо имеется в виду абсолютный адрес, адресующий что-то, находящееся вне пределов перемещаемого образа. Типам SECTION и REL32 в загрузчике Windows пока не придумано никакой обработки — они попросту игнорируются. Самым широкоиспользуемым является тип HIGHLOW с кодом 3, означающий, что просто берётся DWORD и корректируется на нужную величину.

Даже если сейчас посмотреть на скриншот дампа секции релокаций (выше), то видно, что большинство DWORD-ов имееют тройку в позиции 8-ой и 4-ой цифр. Поскольку тип ABSOLUTE на практике никогда не используется, можно визуально легко находить границы блоков: ведь блоки начинаются с заголовков, а заголовок хранит два значения (RVA окна и размер блока), которые на практике никогда не бывают настолько большими, чтобы иметь старшие 4 бита отличные от нуля.

Если вновь взглянуть на содержимое секции релоков, имея все вышеизложенные знания, можно легко увидеть границы блоков (их тут 6), заголовки блоков и элементы (они все сплошь содержат тройки колонками):
Изображение

Можно на примере первого блока рассмотреть структуру элементов: если в составе каждого элемента раскрасить 12-битное поле, хранящее смещение корректируемого места относительно начала окна блока, то выглядеть это будет вот так:
Изображение
Тройки, которые остались незакрашенными — это 4-битное (старшие 4 бита) поле, хранящее тип элемента.
Как видно, все элементы здесь имеют тип HIGHLOW.

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

У нас есть два места, которые должны корректироваться при загрузке по нештатному базовому адресу:
Изображение
Их адреса:
  • VA при загрузке по штатной базе: 0x50909ECE+2=0x50909ED0 / RVA: 0x9ED0
  • VA при загрузке по штатной базе: 0x50909EE4+2=0x50909EE6 / RVA: 0x9EE6

Оба места попадают в окно предпоследнего блока (RVA начала окна = 0x9000), но к блоке нет свободного места, которое можно было бы использовать.

Выхода два:
  1. Добавить в конец ещё один новый блок: с RVA окна = 0x9000, размером блока = 12 байт и двумя 16-битными элементами.
  2. Расширить предпоследний блок, подвинув последний блок на 4 байта вперёд, в конец предпоследнего блока вставить 2 новых элемента

Поскольку OllyDbg имеет команды Binary Copy и Binary Paste, нет никакой проблемы подвинуть последний блок на 4 байта вперёд, и я выбираю именно этот путь.

  • Сдвигаем последний блок на 4 байта вперёд, в предпоследнем блоке меняем размер 0x44 на 0x48.
  • Окно этого блока начинается по RVA=0x9000, тогда 12-битное смещение корректируемых мест относительно начала окна будет 0x0ED0 и 0x0EE6.
  • Старшие 4 бита у элементов должны быть равны значению 3 (тип HIGHLOW), поэтому в качестве элементов дописываем в конец предпоследнего блока слова 3ED0 и 3EE6
После этих модификаций концовка секции релоков выглядит так:
Изображение

Вот иллюстрация, показывающая, как изменилась концовка секции при модификации (слева — было, справа — стало):
Изображение

Поскольку мы расширили таблицу релокаций (она увеличилась в размере на 4 байта), необходимо отразить это в двух местах:
  • В элементе таблицы директорий данных указать новый размер таблицы релокаций:
    Изображение
  • В таблице секций увеличить виртуальный размер секции и размер инициализированной части секции (подгружаемой из файла). Однако оказалось, что для секции .reloc эти величины изначально стоят с запасом, и наше изменение размера с 0x3F0 до 0x3F4 не требует какой-либо правки в таблице секций:
    Изображение

Теперь экспортируем модифицированный образ из OllyDbg в файл, перезагружаем DLL-файл под отладкой и видим столь желанные подчёркивания под вшитыми в инструкции абсолютными адресами IAT-ячеек, свидетельствующие о том, что релокации работают и абсолютные адреса будут подкорректированы в случае загрузки по неродной базе:
Изображение

Я выше писал(а):На самом деле осталось ещё две вещи, о которых мы забыли, которые, пока не сделаны, оставляют наш пропатченный драйвер неполноценным уродцем.

Это была первая и главная вещь. Вторая вещь не настолько критична и без всё работает, но лучше конечно не забывать об этом.

Мы внедрили кое-что (кусочки hacked_alloc400, hacked_free400, новую IAT и новый дескриптор импорта) в конец секции кода, а значит, нам нужно исправить виртуальный размер секции .text в таблице секций. Новая граница секции кода лежит по адресу 0x50909EEB, что соответствует RVA 0x9EEB, что с учётом RVA начала секции 0x1000 даёт новый виртуальный размер секции, равный 0x8EEB.

Исправляем:
Изображение

Кроме того, мы в общем и целом внесли множество изменений в PE-файл, что неизбежно привело к потере соответствия контрольной суммы, хранящейся в PE-заголовке, и реальной контрольной суммы образа. Нужно вычислить новую контрольную сумму и записать её в PE-заголовок. OllyDbg не умеет вычислять чексум (по крайней мере без сторонних плагинов) — это можно сделать множеством других утилит. Я же попросту написал такую утилиту из пары строк: всего-то вызвать MapFileAndCheckSum.

Обновляем чексум и сохраняем изменения в файл уже в последний раз:
Изображение

Всё! Теперь на руках имеется исправленный драйвер, который дружит с DEP, который не зависит от версии kernel32.dll, который импортирует функции как положено и при этом может загружаться по любому базовому адресу. Печать из любых приложений работает и ничего не падает!

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

Однако же в этот раз практически сразу я наткнулся на информацию о том, что Canon всё-таки выпустил новую версию драйвера, где исправил эту проблему! То есть на сайте Кэнона на странице, посвящённой принтеру, по прежнему под видом «самых последних, самых свежих» драйверов скачиваются файлы версии 1.0.0.7 с проблемой DEP-несовместимости. Но существует отдельный «архив», где лежит именно файл CAP3RDN.DLL версии 1.0.0.8, в котором этот баг исправлен силами самой фирмы Canon!

И если 5 лет назад, когда я усиленно искал готовое решение проблемы, ни одного упоминания исправленной версии не находилось, то сейчас о ней знает, кажется, половина интернета. И все старания по вылечиванию версии 1.0.0.7 были напрасными, ведь есть уже готовая хорошая версия 1.0.0.8.
Изображение

Надо сказать, что я не сильно расстроился: на все описанные манипуляции по вылечиванию драйвера у меня ушло не больше 40—60 минут. Зато на то, чтобы написать весь этот текст, описать процесс лечения, сделать все эти скриншоты, нарисовать на них стрелочки, подписи и выделения цветом ушла просто уйма времени. Без преувеличений.

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

Ну и житейская мудрость: семь раз погугли — один раз хакни.

Если кому-то интересно по итогам прочитанного посмотреть и поковырять драйвер в отладчике или дизассемблере, выкладываю:
Вложения
CAP3RDN_v7_original.zip
DEP-несовместимая версия, до лечения
(12.73 Кб) Скачиваний: 166
CAP3RDN_v7_messy_patched.zip
Промежуточные (черновые) версии с выводом отладочной информации и жестко вшитыми адресами API-функций (без импорта) — может запросто не заработать у вас, если версия kernel32.dll не совпадёт (что наиболее вероятно).
(38.54 Кб) Скачиваний: 171
CAP3RDN_v7_patched_final.zip
Окончательная вылеченная версия старого драйвера (правильный импорт, контрольная сумма).
(12.78 Кб) Скачиваний: 177
CAP3RDN_v8_original.rar
Исправленная версия 1.0.0.8 от Кэнона
(13.63 Кб) Скачиваний: 182

kibernetics
Постоялец
Постоялец
Аватара пользователя
 
Сообщения: 945
Зарегистрирован: 03.05.2006 (Ср) 13:31
Откуда: Minsk

Re: Принтерная эпопея

Сообщение kibernetics » 26.02.2017 (Вс) 12:05

Титанический труд.
Искренне надеюсь, что командование из Canon заметят этот опус, и как минимум, самый совершенный цветной лазерник, преподнесут Хакер'у в качестве благодарности за указание на явные недоработки. Или даже будут к нему обращаться за технической помощью. У меня есть тоже МФУ Panasonic 2051, он принимает факс в память, и по достижению количества в 100шт. больше не принимает. Все факсы из памяти сразу, одним скопом, нельзя удалить. Для этого надо зайти по веб-интерфейсу, зайти в каждый факс, посмотреть его, и только после этого можно удалить. Приходиться садить малого за комп, чтобы он последовательно тыкал View/Delete. Вот такая беда.

П.С. но Windows XP? Кажется, совсем недавно это было и в то же время так давно.

ger_kar
Продвинутый гуру
Продвинутый гуру
Аватара пользователя
 
Сообщения: 1957
Зарегистрирован: 19.05.2011 (Чт) 19:23
Откуда: Кыргызстан, Иссык-Куль, г. Каракол

Re: Принтерная эпопея

Сообщение ger_kar » 27.02.2017 (Пн) 16:05

kibernetics писал(а):Титанический труд.
Это точно. Большая работа проделана. Можно эту статью использовать как пособие по правке PE файлов. Мне много раз приходилось подправлять найденные баги, но я даже не задумывался о том, что после исправления самого бага нужно и заголовок подправлять. Теперь буду делать это обязательно.
Бороться и искать, найти и перепрятать

HandKot
Бывалый
Бывалый
Аватара пользователя
 
Сообщения: 283
Зарегистрирован: 28.06.2006 (Ср) 13:34
Откуда: Sergiev Posad

Re: Принтерная эпопея

Сообщение HandKot » 28.02.2017 (Вт) 7:27

офигеть, но когда читал, удивлялся, почему такие проблемы.
У моих родителей такой же принтер и из блокнота принтер печатает на ура. Единственное, в чем проблема, что нет дров 64х. Пришлось винду переустанавливать.

Хакер писал(а):Я принялся искать темы на разных форумах, где поднималась бы проблема кривого оригинального драйвера, и уже представлял себе, что я приду в эти топики и как мессия выложу вылеченный драйвер и принесу людям избавление от этой проблемы.

есть вообще возможность такой переделки ? и мечта сбудется 8)
I Have Nine Lives You Have One Only
THINK!

Хакер
Телепат
Телепат
Аватара пользователя
 
Сообщения: 16473
Зарегистрирован: 13.11.2005 (Вс) 2:43
Откуда: Казахстан, Петропавловск

Re: Принтерная эпопея

Сообщение Хакер » 28.02.2017 (Вт) 7:30

HandKot писал(а):есть вообще возможность такой переделки ? и мечта сбудется

Эээ... ты читал топик? Вся часть истории, что написана на данный момент, как раз и описывает переделку драйвера. И даже сам переделанный драйвер выложен. А ты спрашиваешь — есть ли возможность.

HandKot писал(а):У моих родителей такой же принтер и из блокнота принтер печатает на ура.

Видимо на этом компьютере не активен DEP.
—We separate their smiling faces from the rest of their body, Captain.
—That's right! We decapitate them.

nouyana
Продвинутый пользователь
Продвинутый пользователь
Аватара пользователя
 
Сообщения: 104
Зарегистрирован: 29.01.2016 (Пт) 17:42

Re: Принтерная эпопея

Сообщение nouyana » 09.03.2017 (Чт) 14:02

Хакер писал(а):Подытожим проблемы
Canon LBP-1120:
  1. Кривой драйвер делает невозможным использовать его на компьютере с DEP
  2. Нужно включать отдельный компьютер и держать его включенным, если нужно что-то печатать
  3. Отклоняющее зеркало не раскручивается без нанесения ударов — как следствие вообще не может печатать
  4. Дико шумит, даже если зеркало раскрутилось в результате удара.
  5. Помимо полигон-мотора, прочая механика дышит на ладан: скрипит, подклинивает, щёлкает и т.п.
  6. Не сетевой по своей природе: требует, чтобы компьютер, к которому он подключен, был включен.
  7. Не может быть сделан сетевым в принципе из-за проблемного драйвера.


Я пару лет назад купил себе два б/у принтера HP LaserJet 1100 (близкий родственник твоему кэнону, картриджи одинаковые). Оба принтера обошлись мне в 1500 российских рублей. Я собрал из двух один + запчасти на замену + подкупил кое-такие ролики/валики в сервисном центре HP, они ещё есть на складах. Все пластмассовые части намылил мыльной пеной и промыл под душем - принтер выглядит как новый, никакой химией его так не протереть и не пропылесосить. Советую и тебе купить такой HP, а кэнон разобрать на запчасти (если есть, что разбирать).

На сайте микрософта есть драйвера для HP 1100 для Win7-10. Я набрал там в поисковике "Canon Laser", он тоже нашел какой-то "Canon Laser Null Driver", но я так и не понял, является ли он драйвером принтера.

Даже имея драйвер, я не смог самостоятельно установить его в Win7 (принтер у меня подключен через LPT, хотя существуют переходники на USB, а также принт-серверы D-Link). Установить драйвер мне помогла специальная утилита от MS под названием PrinterDiagnostic.diagcab

У HP 1100 есть одна проблема - его драйвер медленно обрабатывает PDF документы - каждая страница может печататься по несколько минут на моём Pentium 4, 3ghz. С MS Word/Excel проблем нет.

Хакер
Телепат
Телепат
Аватара пользователя
 
Сообщения: 16473
Зарегистрирован: 13.11.2005 (Вс) 2:43
Откуда: Казахстан, Петропавловск

Re: Принтерная эпопея

Сообщение Хакер » 09.03.2017 (Чт) 17:05

nouyana писал(а): Советую и тебе купить такой HP, а кэнон разобрать на запчасти (если есть, что разбирать).

Вышенаписанное — это только 1/3 часть эпопеи. Продолжение я просто пока ниак не найду времени написать.
—We separate their smiling faces from the rest of their body, Captain.
—That's right! We decapitate them.


Вернуться в Народный треп

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и гости: 6

    TopList