| Опыт дизассемблирования большой .com программы |
|
|
| Автор Westry | |
| 19.06.2008 г. | |
|
В данной статье мне хочется рассказать о дизассемблировании большой программы (графического редактора). Не будучи знатоком ассемблера, не зная до сих пор, как использовать большинство возможностей своего дизассемблера(DisDoc 2.3), я все же решился написать эту статью, так как прекрасно помню, в какой кромешной тьме начинал заниматься дизассемблированием.
Тогда, год назад, я попробовал дизассемблировать простенькую программу и был страшно удивлен тем, что дизассемблер делает это неправильно, и при повторном ассеблировании программа не работала так, как надо. Тогда же мне удалось поговорить со знающим человеком и, хотя я чувствовал себя наивным дурачком, мне удалось выяснить главное: ПОЛНОЕ,АВТОМАТИЧЕСКОЕ ДИЗАССЕМБЛИРОВАНИЕ НЕВОЗМОЖНО, над тем текстом, который выдает дизассемблер, нужно довольно долго работать, прежде чем ассемблирование этого текста даст работоспособную программу. В дальнейшем я постараюсь рассказать о тех приемах, которые превращают "плохой" текст в "хороший" , т.е. в текст, который не только дает корректно работающую программу при ассемблировании, но и позволяет себя изменить, чтобы усовершенствовать исходную программу. ПОЧЕМУ DisDoc?SOURSER - это название знают все, кто хотя бы краем уха слышал о дизассеблировании. Считается, что это дизассеблер замечательный, мощный, не имеющий конкурентов. Я думаю, что слухи об огромных преимуществах SOURSERа силь но преувеличены. У меня сложилось такое впечатление, что при дизассемблирова нии небольших программ (до 7 кб.) SOURSER предпочтительнее. Когда программа велика (в моем случае - 58 кб ), SOURSER работает очень медленно и, на мой взгляд, не дает никаких преимуществ. Выбор дизассемблера DisDoc 2.3 был для меня во многом случаен. Начиная работу, я получил тексты на ассемблере как с помощью SOURSERa (версия 3.07), так и с помощью дизассемблера DisDoc 2.3. Затем оба текста после устранения очевидных ошибок были ассемблированы. И вот, то, что было выдано SOURSERом, повисло сразу, а то, что выдал DisDoc 2.3, прежде чем повиснуть, вывело на экран несколько линий. Это и определило выбор. В процессе работы я не раз имел возможность оценить основное преимущество дизассемблера DisDoc - интуитивно понятный, неизощренный, удобный и компактный листинг. Чтобы понять дальнейшее, необходимо познакомиться с отрывком из листинга, который выдает DisDoc 2.3 mov cx,WORD PTR ds:d02453 ;02430 В поле комментариев указано смещение, которое имела данная инструкция в исходной программе. Например, если вы в исходной программе, подвергаемой дизассемблированию, посмотрите отладчиком смещение 02434, то там окажется инструкция add cx,bx - на это можно положиться! Очень хороши названия меток и элементов данных. По ним сразу можно понять, какое смещение они имели в исходной программе. Например, метка b02430 имела смещение 02430, элемент данных d02446 имел смещение 02446 и т.д. То же самое относится и к подпрограммам. После вызова подпрограммы в треугольных скобках указано смещение, которое имела эта подпрограмма в исходной программе. Например, подпрограмма s383 начиналась в исходной программе со смещения 09060. Такая организация листинга позволяет сохранить однозначное соответствие с исходной программой, что дает возможность проверить отладчиком сомнительные куски кода и данных, сравнить текст, выданный дизассемблером с тем, что есть на самом деле. Это поистине драгоценная возможность. Нужно сказать, что DisDoc имеет большие недостатки, о которых речь еще пойдет, и, следовательно, применение того или иного дизассемблера - дело вкуса. В любом случае обязательно встретятся Фундаментальные проблемы1. Проблема OFFSETaПредположим, что в тексте, который выдал дизаccемблер есть такой фрагмент: mov ax,bx ;1 Что засылается в регистр si в третьей строчке - число 8429h или смещение некой метки? На этот вопрос позволяет ответить пятая строчка, из которой видно, что регистр si используется для косвенной адресации. Значит, исправленный фрагмент должен выглядеть следующим образом: mov ax,bx ;1 Возможно, здесь у многих возникнет сомнение - нужно ли заменять число на соответствующий OFFSET - ведь, казалось бы, в заново ассемблированной программе данные будут иметь то же смещение? К сожалению, это не так. Во первых, мы,как правило, не знаем, какой ассемблер применялся при транслировании оригинального текста, а коды, полученные с помощью разных ассемблеров будут иметь разную длину, что приведет к изменению смещений. Например, команда AND CX,0007h транслируется MASMом 5.1 и TASMом 1.01 как 83E107 и занимает 3 байтa. Но эта же команда может быть транслирована как 81E10700 и занимать 4 байта. Во-вторых, даже если смещение сохранится, программа не поддастся модификации, так как при вставке какого-либо фрагмента кода изменятся смещения и все "развалится". Итак, OFFSETы позволяют склеить программу, делают ее пригодной для модификации. Разобранный пример достаточно примитивен. Попробуем рассмотреть более сложные ситуации и первым делом исследуем фрагмент текста, выданный дизассемблером: mov bx,9006h ;08f66 Здесь возникает тот-же вопрос - что такое 9006h в первой строчке фрагмента - смещение или просто число? Ответить на этот вопрос помогает информация, помещенная дизассемблером в поле комментариев. Мы уже говорили о том что числа, помещенные в этом поле, представляют собой смещения, которые имела инструкция в исходной программе, подвергаемой дизассемблированию. Нетрудно догадаться, что в приведенном фрагменте осуществляется косвенный вызов подпрограммы, и, следовательно, 9006h - это смещение, а не число. Фрагмент должен быть исправлен так: mov bx,OFFSET d09006 ;08f66 Рассмотрим еще один пример косвенного вызова подпрограммы, в котором OFFSET попадает в область данных.
Чтобы выяснить, что представляет собой 8792h, нужно посмотреть в область со смещениями, близкими к этому числу. Приведем соответствующий фрагмент, выданный дизассемблером:
Видно, что смещению 08792 соответствует слово 0d5,93. Теперь остается заметить, что со смещения 093d5 в исходной программе начинается фрагмент повисшего кода
Следовательно,весь разобранный пример - это хитроумный косвенный вызов подпрограммы. Исправленный фрагмент должен выглядеть так:
Здесь я предвижу большие возражения. Мне скажут, что все это можно интерпретировать иначе, что мои доказательства неубедительны и т.д. С этим я совершенно согласен. Более того, эти доказательства неубедительны и для меня. Гораздо сильнее убеждает то, что программа после ассемблирования работает! Дизассемблирование, как и отладка программ - процесс интуитивный. Опытный человек испытывает особое удовольствие от того, что его немотивированные догадки впоследствии подтверждаются. Как часто мысль, пришедшая в автобусе, во сне, в компании, в самой неподходящей обстановке - оказывается верной! Завершим этот пункт еще одним достаточно хитрым примером. В тексте, который выдал дизассемблер, встретился такой фрагмент: mov bx,4f71h ;0522b Возникает все тот же вопрос - что такое 4f71h - число или смещение? Чтобы ответить на этот вопрос, нужно понять, что делает этот участок программы. Давайте попробуем в этом разобраться. Очевидно, из стека выталкивается число, сравнивается с 4f71h и если нет равенства, выталкивается следующее число. Если число равно 4f71h, то оно снова заталкивается в стек и происходит возврат из подпрограммы. Но куда? Ясно, что в то место, смещение которого было в исходной программе равно 4f71h. Как видно из текста, в этом месте стоял вызов подпрограммы s229. Значит, таким странным образом вызывается подпрограмма и 4f71h - это смещение! Исправленный фрагмент должен выглядеть так: mov bx, OFFSET d04f71 ;0522b 2.Как отличить данные от команд?Любой дизассемблер путает данные и команды. Особенно это относится к .COM программам, где все перемешано. Рассмотрим простой пример: pop cx ;03e56 В этом фрагменте встретились две вычурных, повисших инструкции: add BYTE PTR [bx+si],al ;03e58 Сверху они ограничены инструкцией возврата из подпрограммы ret, а снизу - меткой m03e5c. Ясно, что эти инструкции могут быть только данными. После переделки приведенный фрагмент должен выглядеть так: pop cx ;03e56 Тут возникает еще один вопрос: почему в одном случае стоит dw, а в другом - db? Ответ содержится в тексте, который выдал дизассемблер. Там можно найти такие инструкции: mov si,WORD PTR ds:d03e58 ;03dd0 Откуда следует, что d03e58 рассматривается как слово, а d03e5a - как байт. Рассмотрим чуть более сложный, но, тем не менее, очень характерный пример. b03f53: cmp al,05 ;03f53 В приведенном фрагменте текста метка b03f6b отсутствует. Между тем эта метка должна "разрубить" пополам инструкцию add BYTE PTR [si],bh , которая начинается в оригинальной программе, подвергаемой дизассемблированию, со смещения 03f6a. Выход здесь может быть только один - смещению 03f6a соответствует байт данных, а инструкция начинается со смещения 03f6b. Исправленный фрагмент должен выглядеть так: b03f53: cmp al,05 ;03f53 Путаница между данными и инструкциями возникает довольно часто. SOURSER способен выдавать целые метры бессмысленных инструкций. DisDoc 2.3 в этом отношении ведет себя лучше. 3. Зависимость от транслятораПрограммисты на ассемблере склонны пренебрегать правилами хорошего тона, нарушать все мыслимые табу, и это создает дополнительные трудности при дизассемблировании. В качестве примера приведем фрагмент кода, выданного дизассемблером s25 proc near Этот фрагмент представляется совершенно невинным, и действительно, он дизассемблирован правильно. Вся беда в том, что программист задумал изменять этот фрагмент, то есть резать по живому. Оказывается, в программе есть еще такой кусок mov di,086bh ;007f8Рис.1 Так как di используется для косвенной адресации, нам прежде всего необходимо заменить 086bh на соответствующий OFFSET d0086b и пометить этой меткой начало подпрограммы s25: s25 proc near Далее следует понять, что делают инструкции, приведенные на рис.1 с подпрограммой s25. Пусть эта подпрограмма асслемблирована с помощью TASM 1.01. Выданный ассемблером код будет таким, как показано на рисунке 2. 41 INC CX 41 INC CXРис.2 Рис.3 Но вся беда в том, что исходная программа была ассемблирована другим ассемблером и имеет вид, показанный на рисунке 3. Как видно из сравнения рисунков 2 и 3, TASM 1.01 и неизвестный ассемблер транслируют инструкции ADD по-разному, и это приводит к катастрофическим последствиям. Действительно, посмотрим, как воздействует участок кода, показанный на Рис.1 (перед этим заменим 086bh на OFFSET d0086b) на подпрограмму s25, транслируемую TASMом (рис.4) и неизвестным ассемблером (рис.5). 4A DEC DX 4A DEC DXРис.4 Рис.5 Сравнение рисунков 4 и 5 показывает, что логика работы программы меняется в зависимости от того, какой ассемблер применялся. Как выкрутиться из этой ситуации, если нужного ассемблера нет под рукой? Самый простой, но не очень красивый путь - поставить "заплатку". Чтобы можно было использовать TASM, подпрогроамма s25 должна выглядеть так: s25 proc near Особенности и ошибки дизассемблера DisDoc 2.3К сожалению, DisDoc 2.3 совершает ошибки, иногда регулярные, а иногда редкие, коварные и даже подлые. Самая противная ошибка - случайный пропуск данныхвстречается довольно редко. Начнем с того, что встречается очень часто. 1. EQU - кто тебя выдумал?В коде, выданном дизассемблером, часто попадаются такие загадочные куски: ;<00465> Каков смысл присвоения d0046c equ 00046ch ? Чтобы выяснить это, нужно отыскать d0046c в тексте. В нашем случае элемент данных d0046c встречается очень далеко от своего первого появления - в подпрограмме s321 mov ax,0040h ;06257Рис.6 При виде этого текста возникает догадка, что здесь идет зваимодействие с областью данных BIOSa . Действительно, в регистр es засылается число 40, т.е. es будет указывать на адрес 400 - начало этой области. Тогда следующий вопрос - каков смысл адреса 046сh? Легко выяснить, что по этому адресу находится счетчик прерываний от таймера. Если это так, то фрагмент, приведенный на рис.6, обретает смысл - он дает задержку на число прерываний от таймера, заданное в регистре cx. Но если все сказанное верно, то d0046c должно быть равно не 46сh, а просто 6сh! И действительно, если посмотреть подпрограмму s321 отладчиком, то станет ясно, что вместо mov al,BYTE PTR es:d0046c в тексте должно стоять mov al,6ch. Итак, чтобы исправить эту ошибку, необходимо:
mov ax,0040h ;06257 Рассмотрим второй пример. В коде, выданном дизассемблером, встретился такой кусок: ;<0074e> Поиск элемента данных d0076a окончился неудачей. А d00771 встретился в таком фрагменте: ..................................... Здесь явно идет модификация кода подпрограммы s22. Значит, необходимо заменить d00771 на b00771, пометить этой меткой соответствующую инструкцию в s22 и удалить присвоения d0076a equ 00076ah Исправленный фрагмент s22 будет выглядеть так: ;<0074e> Рассмотрим еще один пример. В начале s32 встретились уже знакомые псевдооператоры: ;<00bf7> Если посмотреть в область со смещениями, близкими к с1с, то там окажется кусок повисшего кода, который может быть только данными: .......................................Рис.7 Теперь нужно поискать идентификаторы d00c1c и d00c1e в тексте, выданном дизассемблером. Очень быстро можно найти фрагменты типа: mov WORD PTR ds:d00c1c,ax, mov WORD PTR ds:d00c1e,ax. Значит, ошибка дизассемблера состоит в том, что он перепутал данные и команды и на этой почве сделал два неправильных присваивания, equ, попавших в начало подпрограммы s32. Исправления будут заключаться в следующем:
d00c14 db 0a,03,01,01,00,00,00,00 ;00c14 В заключение рассмотрим совсем простенький фрагмент кода: ;<01252> Укажем без комментариев, что подпрогромма s39 должна выглядеть так: ;<01252> В заключение этого пункта подведем итоги. Значки equ называют всевдооператорами. Если говорить о дизассемблере DisDoc 2.3, то это название удивительно точное. Если в тексте встретится equ - то ошибка рядом. Между тем, иногда DisDoc 2.3 употребляет equ вполне корректно. Так что будьте бдительны и не дайте себя обмануть. 2. Дурные ошибки.Иногда поведение дизассемблера трудно объяснить. Например, он выдает add WORD PTR ds:d96be3,07 ;038b6теряет или искажает куски данных. К счастью, это происходит очень редко. |
| След. » |
|---|


