Опыт дизассемблирования большой .com программы
В данной статье мне хочется рассказать о дизассемблировании большой программы (графического редактора). Не будучи знатоком ассемблера, не зная до сих пор, как использовать большинство возможностей своего дизассемблера(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 b02430: add cx,bx ;02434 mov bx,99e7h ;02436 mov dx,WORD PTR ds:d02449 ;02439 mov al,BYTE PTR ds:d02446 ;0243d call s383 ;<09060> ;02440 push cs ;02443 pop ds ;02444 ret ;02445 ;----------------------------------------------------- d02446 db 00 ;02446 . d02447 db 00,00 ;02447 .. d02449 db 00,00 ;02449 ..
В поле комментариев указано смещение, которое имела данная инструкция в исходной программе. Например, если вы в исходной программе, подвергаемой дизассемблированию, посмотрите отладчиком смещение 02434, то там окажется инструкция add cx,bx - на это можно положиться! Очень хороши названия меток и элементов данных. По ним сразу можно понять, какое смещение они имели в исходной программе. Например, метка b02430 имела смещение 02430, элемент данных d02446 имел смещение 02446 и т.д. То же самое относится и к подпрограммам. После вызова подпрограммы в треугольных скобках указано смещение, которое имела эта подпрограмма в исходной программе. Например, подпрограмма s383 начиналась в исходной программе со смещения 09060. Такая организация листинга позволяет сохранить однозначное соответствие с исходной программой, что дает возможность проверить отладчиком сомнительные куски кода и данных, сравнить текст, выданный дизассемблером с тем, что есть на самом деле. Это поистине драгоценная возможность. Нужно сказать, что DisDoc имеет большие недостатки, о которых речь еще пойдет, и, следовательно, применение того или иного дизассемблера - дело вкуса.
В любом случае обязательно встретятся
Фундаментальные проблемы
1. Проблема OFFSETa
Предположим, что в тексте, который выдал дизаccемблер есть такой фрагмент:
mov ax,bx ;1 shl ax,1 ;004bc ;2 mov si,8429h ;3 add si,ax ;4 push WORD PTR [si] ;5
Что засылается в регистр si в третьей строчке - число 8429h или смещение некой метки? На этот вопрос позволяет ответить пятая строчка, из которой видно, что регистр si используется для косвенной адресации. Значит, исправленный фрагмент должен выглядеть следующим образом:
mov ax,bx ;1 shl ax,1 ;004bc ;2 mov si,OFFSET d08429 ;3 add si,ax ;4 push WORD PTR [si] ;5 ................................ d08429 db 0ff,0ff,0f6 ;8429 db 0ff,0d8,0ff,0a6,0ff,60 ;0842c .....`
Возможно, здесь у многих возникнет сомнение - нужно ли заменять число на соответствующий OFFSET - ведь, казалось бы, в заново ассемблированной программе данные будут иметь то же смещение? К сожалению, это не так. Во первых, мы,как правило, не знаем, какой ассемблер применялся при транслировании оригинального текста, а коды, полученные с помощью разных ассемблеров будут иметь разную длину, что приведет к изменению смещений. Например, команда AND CX,0007h транслируется MASMом 5.1 и TASMом 1.01 как 83E107 и занимает 3 байтa. Но эта же команда может быть транслирована как 81E10700 и занимать 4 байта. Во-вторых, даже если смещение сохранится, программа не поддастся модификации, так как при вставке какого-либо фрагмента кода изменятся смещения и все "развалится". Итак, OFFSETы позволяют склеить программу, делают ее пригодной для модификации. Разобранный пример достаточно примитивен. Попробуем рассмотреть более сложные ситуации и первым делом исследуем фрагмент текста, выданный дизассемблером:
mov bx,9006h ;08f66 b08f75: mov WORD PTR ds:d087d0,bx ;08f75 ................................. call WORD PTR cs:d087d0 ;08fc3 ...................................... ;----------------------------------------------------- push dx ;09006 call s419 ;<099a3> ;09007 mov al,BYTE PTR [si] ;0900a mov BYTE PTR [si],0ffh ;0900c pop dx ;0900f ret ;09010 ;-----------------------------------------------------
Здесь возникает тот-же вопрос - что такое 9006h в первой строчке фрагмента - смещение или просто число? Ответить на этот вопрос помогает информация, помещенная дизассемблером в поле комментариев. Мы уже говорили о том что числа, помещенные в этом поле, представляют собой смещения, которые имела инструкция в исходной программе, подвергаемой дизассемблированию. Нетрудно догадаться, что в приведенном фрагменте осуществляется косвенный вызов подпрограммы, и, следовательно, 9006h - это смещение, а не число. Фрагмент должен быть исправлен так:
mov bx,OFFSET d09006 ;08f66 ...................................... ;----------------------------------------------------- d09006: push dx ;09006 ...................................... ret ;09010
Рассмотрим еще один пример косвенного вызова подпрограммы, в котором OFFSET попадает в область данных.
s390 proc near .......................................................... mov ax,WORD PTR [bx+8792h] ;092c7 mov WORD PTR ds:d087d2,ax ;092cb ........................................................... call WORD PTR cs:d087d2 ;093c8 ret ;093d4 ;----------------------------------------------------- ror ah,1 ;093d5 ;LO]-->[HI..LO]-->[HI jb b093da ;093d7 ;Jump if < (no sign) ret ;093d9 b093da: inc si ;093da ret ;093db ............................................................
Чтобы выяснить, что представляет собой 8792h, нужно посмотреть в область со смещениями, близкими к этому числу. Приведем соответствующий фрагмент, выданный дизассемблером:
d08790 db 00,00,0d5,93 ;08790 ...... .............................................................
Видно, что смещению 08792 соответствует слово 0d5,93. Теперь остается заметить, что со смещения 093d5 в исходной программе начинается фрагмент повисшего кода
ror ah,1 ;093d5 !!!!!! ;LO]-->[HI..LO]-->[HI jb b093da ;093d7 ;Jump if < (no sign) ret ;093d9 b093da: inc si ;093da ret ;093db
Следовательно,весь разобранный пример - это хитроумный косвенный вызов подпрограммы. Исправленный фрагмент должен выглядеть так:
s390 proc near .......................................................... mov ax,WORD PTR [bx+OFFSET d08792] ;092c7 mov WORD PTR ds:d087d2,ax ;092cb ........................................................... call WORD PTR cs:d087d2 ;093c8 ret ;093d4 ;----------------------------------------------------- d093d5: ror ah,1 ;093d5 ;LO]-->[HI..LO]-->[HI jb b093da ;093d7 ;Jump if < (no sign) ret ;093d9 b093da: inc si ;093da ret ;093db ............................................................ d08790 db 00,00 ;08790 ...... d08792 dw OFFSET d093d5 ;08792
Здесь я предвижу большие возражения. Мне скажут, что все это можно интерпретировать иначе, что мои доказательства неубедительны и т.д. С этим я совершенно согласен. Более того, эти доказательства неубедительны и для меня. Гораздо сильнее убеждает то, что программа после ассемблирования работает! Дизассемблирование, как и отладка программ - процесс интуитивный. Опытный человек испытывает особое удовольствие от того, что его немотивированные догадки впоследствии подтверждаются. Как часто мысль, пришедшая в автобусе, во сне, в компании, в самой неподходящей обстановке - оказывается верной! Завершим этот пункт еще одним достаточно хитрым примером. В тексте, который выдал дизассемблер, встретился такой фрагмент:
mov bx,4f71h ;0522b b0522e: pop ax ;0522e cmp ax,bx ;0522f jnz b0522e ;05231 ;Jump not equal(ZF=0) mov BYTE PTR ds:d041f4,00 ;05233 push ax ;05238 ret ;05239 ................................. call s229 ;<04fc4> ;04f71
Возникает все тот же вопрос - что такое 4f71h - число или смещение? Чтобы ответить на этот вопрос, нужно понять, что делает этот участок программы. Давайте попробуем в этом разобраться. Очевидно, из стека выталкивается число, сравнивается с 4f71h и если нет равенства, выталкивается следующее число. Если число равно 4f71h, то оно снова заталкивается в стек и происходит возврат из подпрограммы. Но куда? Ясно, что в то место, смещение которого было в исходной программе равно 4f71h. Как видно из текста, в этом месте стоял вызов подпрограммы s229. Значит, таким странным образом вызывается подпрограмма и 4f71h - это смещение! Исправленный фрагмент должен выглядеть так:
mov bx, OFFSET d04f71 ;0522b b0522e: pop ax ;0522e cmp ax,bx ;0522f jnz b0522e ;05231 ;Jump not equal(ZF=0) mov BYTE PTR ds:d041f4,00 ;05233 push ax ;05238 ret ;05239 ................................. d04f71: call s229 ;<04fc4> ;04f71
2.Как отличить данные от команд?
Любой дизассемблер путает данные и команды. Особенно это относится к .COM программам, где все перемешано. Рассмотрим простой пример:
pop cx ;03e56 ret ;03e57 ;----------------------------------------------------- add BYTE PTR [bx+si],al ;03e58 add BYTE PTR [bx+si],al ;03e5a m03e5c: mov BYTE PTR ds:d05830,01 ;03e5c
В этом фрагменте встретились две вычурных, повисших инструкции:
add BYTE PTR [bx+si],al ;03e58 add BYTE PTR [bx+si],al ;03e5a
Сверху они ограничены инструкцией возврата из подпрограммы ret, а снизу - меткой m03e5c. Ясно, что эти инструкции могут быть только данными. После переделки приведенный фрагмент должен выглядеть так:
pop cx ;03e56 ret ;03e57 ;----------------------------------------------------- d03e58 dw 0 ;03e58 d03e5a db 0 ;03e5a d03e5b db 0 m03e5c: mov BYTE PTR ds:d05830,01 ;03e5c
Тут возникает еще один вопрос: почему в одном случае стоит dw, а в другом - db? Ответ содержится в тексте, который выдал дизассемблер. Там можно найти такие инструкции:
mov si,WORD PTR ds:d03e58 ;03dd0 mov bl,BYTE PTR ds:d03e5a ;03dd4,
Откуда следует, что d03e58 рассматривается как слово, а d03e5a - как байт. Рассмотрим чуть более сложный, но, тем не менее, очень характерный пример.
b03f53: cmp al,05 ;03f53 jnz b03f6b ;03f55 ;Jump not equal(ZF=0) ..................................................... ret ;03f69 ;----------------------------------------------------- add BYTE PTR [si],bh ;03f6a push es ;03f6c jnz b03f79 ;03f6d ;Jump not equal(ZF=0)
В приведенном фрагменте текста метка b03f6b отсутствует. Между тем эта метка должна "разрубить" пополам инструкцию add BYTE PTR [si],bh , которая начинается в оригинальной программе, подвергаемой дизассемблированию, со смещения 03f6a. Выход здесь может быть только один - смещению 03f6a соответствует байт данных, а инструкция начинается со смещения 03f6b. Исправленный фрагмент должен выглядеть так:
b03f53: cmp al,05 ;03f53 jnz b03f6b ;03f55 ;Jump not equal(ZF=0) ...................................................... ret ;03f69 ;----------------------------------------------------- d03f6a db 0 ;03f6a b03f6b: cmp al,06h ;03f6b jnz b03f79 ;03f6d ;Jump not equal(ZF=0)
Путаница между данными и инструкциями возникает довольно часто. SOURSER способен выдавать целые метры бессмысленных инструкций. DisDoc 2.3 в этом отношении ведет себя лучше.
3. Зависимость от транслятора
Программисты на ассемблере склонны пренебрегать правилами хорошего тона, нарушать все мыслимые табу, и это создает дополнительные трудности при дизассемблировании. В качестве примера приведем фрагмент кода, выданного дизассемблером
s25 proc near inc cx ;0086b add di,bp ;0086c adc si,00 ;0086e add dx,si ;00871 push di ;00873 shl di,1 ;00874 ;Multiply by 2's adc dx,00 ;00876 pop di ;00879 ret ;0087a
Этот фрагмент представляется совершенно невинным, и действительно, он дизассемблирован правильно. Вся беда в том, что программист задумал изменять этот фрагмент, то есть резать по живому. Оказывается, в программе есть еще такой кусок
mov di,086bh ;007f8 ...................................... mov BYTE PTR [di],4ah ;00800 mov BYTE PTR [di+07],0f1h ;00803 mov BYTE PTR [di+0ch],0d1h ;00807 ...................................... ret ;00815
Так как di используется для косвенной адресации, нам прежде всего необходимо заменить 086bh на соответствующий OFFSET d0086b и пометить этой меткой начало подпрограммы s25:
s25 proc near d0086b: inc cx ;0086b ..............................................
Далее следует понять, что делают инструкции, приведенные на рис.1 с подпрограммой s25. Пусть эта подпрограмма асслемблирована с помощью TASM 1.01. Выданный ассемблером код будет таким, как показано на рисунке 2.
41 INC CX 41 INC CX 03FD ADD DI,BP 01EF ADD DI,BP 83D600 ADC SI,0000 83D600 ADC SI,0000 03D6 ADD DX,SI 01F2 ADD DX,SI 57 PUSH DI 57 PUSH DI D1E7 SHL DI,1 D1E7 SHL DI,1 83D200 ADC DX,0000 83D2000 ADC DX,0000 5F POP DI 5F POP DI C3 RET C3 RET
Но вся беда в том, что исходная программа была ассемблирована другим ассемблером и имеет вид, показанный на рисунке 3. Как видно из сравнения рисунков 2 и 3, TASM 1.01 и неизвестный ассемблер транслируют инструкции ADD по-разному, и это приводит к катастрофическим последствиям. Действительно, посмотрим, как воздействует участок кода, показанный на Рис.1 (перед этим заменим 086bh на OFFSET d0086b) на подпрограмму s25, транслируемую TASMом (рис.4) и неизвестным ассемблером (рис.5).
4A DEC DX 4A DEC DX 03FD ADD DI,BP 01EF ADD DI,BP 83D600 ADC SI,0000 83D600 ADC SI,0000 03F1 ADD SI,CX ;!!!! 01F1 ADD CX,SI ;!!!! 57 PUSH DI 57 PUSH DI D1E7 SHL DI,1 D1E7 SHL DI,1 83D100 ADC CX,0000 83D100 ADC CX,0000 5F POP DI 5F POP DI C3 RET C3 RET
Сравнение рисунков 4 и 5 показывает, что логика работы программы меняется в зависимости от того, какой ассемблер применялся. Как выкрутиться из этой ситуации, если нужного ассемблера нет под рукой? Самый простой, но не очень красивый путь - поставить "заплатку". Чтобы можно было использовать TASM, подпрогроамма s25 должна выглядеть так:
s25 proc near d0086b: inc cx ;0086b add di,bp ;0086c adc si,00 ;0086e db 01,0f2 ;add dx,si !!!!!! ;00871 push di ;00873 shl di,1 ;00874 ;Multiply by 2's adc dx,00 ;00876 pop di ;00879 ret ;0087a
Особенности и ошибки дизассемблера DisDoc 2.3
К сожалению, DisDoc 2.3 совершает ошибки, иногда регулярные, а иногда редкие, коварные и даже подлые. Самая противная ошибка - случайный пропуск данныхвстречается довольно редко. Начнем с того, что встречается очень часто.
1. EQU - кто тебя выдумал?
В коде, выданном дизассемблером, часто попадаются такие загадочные куски:
;<00465> s12 proc near d0046c equ 00046ch cmp bx,5ah ;00465
Каков смысл присвоения d0046c equ 00046ch ? Чтобы выяснить это, нужно отыскать d0046c в тексте. В нашем случае элемент данных d0046c встречается очень далеко от своего первого появления - в подпрограмме s321
mov ax,0040h ;06257 ;<es = 0040> mov es,ax ;0625a mov al,BYTE PTR es:d0046c ;0625c sti ;06260 ;Turn ON Interrupts b06261: cmp al,BYTE PTR es:d0046c ;06261 jz b06261 ;06266 ;Jump if equal (ZF=1) mov al,BYTE PTR es:d0046c ;06268 dec cx ;0626c jnz b06261 ;0626d ;Jump not equal(ZF=0) pop ax ;0626f out 61h,al ;06270 ;060-067:8024 keybrd contrlr ;<es = 0000> pop es ;06272 ret ;06273 s321 endp
При виде этого текста возникает догадка, что здесь идет зваимодействие с областью данных BIOSa . Действительно, в регистр es засылается число 40, т.е. es будет указывать на адрес 400 - начало этой области. Тогда следующий вопрос - каков смысл адреса 046сh? Легко выяснить, что по этому адресу находится счетчик прерываний от таймера. Если это так, то фрагмент, приведенный на рис.6, обретает смысл - он дает задержку на число прерываний от таймера, заданное в регистре cx. Но если все сказанное верно, то d0046c должно быть равно не 46сh, а просто 6сh! И действительно, если посмотреть подпрограмму s321 отладчиком, то станет ясно, что вместо mov al,BYTE PTR es:d0046c в тексте должно стоять mov al,6ch.
Итак, чтобы исправить эту ошибку, необходимо:
- Удалить из начала подпрограммы s12 присвоение d0046c equ 00046ch
- Переписать приведенный на рис.6 фрагмент s321 следующим образом:
mov ax,0040h ;06257 ;<es = 0040> mov es,ax ;0625a mov al,BYTE PTR es:006ch ;0625c sti ;06260 ;Turn ON Interrupts b06261: cmp al,BYTE PTR es:006ch ;06261 jz b06261 ;06266 ;Jump if equal (ZF=1) mov al,BYTE PTR es:006ch ;06268 dec cx ;0626c jnz b06261 ;0626d ;Jump not equal(ZF=0) pop ax ;0626f out 61h,al ;06270 ;060-067:8024 keybrd contrlr ;<es = 0000> pop es ;06272 ret ;06273 s321 endp
Рассмотрим второй пример. В коде, выданном дизассемблером, встретился такой кусок:
;<0074e> s22 proc near d0076a equ 00076ah d00771 equ 000771h call s24 ;<00791> ;0074e ............... b0076a: push cx ;0076a call s25 ;<0086b> ;0076b call s23 ;<00776> ;0076e pop cx ;00771 dec bx ;00772
Поиск элемента данных d0076a окончился неудачей. А d00771 встретился в таком фрагменте:
..................................... mov BYTE PTR ds:b0076a,51h ;0080b mov BYTE PTR ds:d00771,59h ;00810 ......................................
Здесь явно идет модификация кода подпрограммы s22. Значит, необходимо заменить d00771 на b00771, пометить этой меткой соответствующую инструкцию в s22 и удалить присвоения
d0076a equ 00076ah d00771 equ 000771h
Исправленный фрагмент s22 будет выглядеть так:
;<0074e> s22 proc near call s24 ;<00791> ;0074e ...................................................... b0076a: push cx ;0076a call s25 ;<0086b> ;0076b call s23 ;<00776> ;0076e b00771: pop cx ;00771 dec bx ;00772 .............................................. mov BYTE PTR ds:b0076a,51h ;0080b mov BYTE PTR ds:b00771,59h ;00810 ................................................
Рассмотрим еще один пример. В начале s32 встретились уже знакомые псевдооператоры:
;<00bf7> s32 proc near d00c1c equ 000c1ch d00c1e equ 000c1eh
Если посмотреть в область со смещениями, близкими к с1с, то там окажется кусок повисшего кода, который может быть только данными:
....................................... or al,BYTE PTR [bp+di] ;00c14 add WORD PTR [bx+di],ax ;00c16 add BYTE PTR [bx+si],al ;00c18 add BYTE PTR [bx+si],al ;00c1a mov di,1306h ;00c1c add ax,06c0h ;00c1f ......................................
Теперь нужно поискать идентификаторы d00c1c и d00c1e в тексте, выданном дизассемблером. Очень быстро можно найти фрагменты типа: mov WORD PTR ds:d00c1c,ax, mov WORD PTR ds:d00c1e,ax. Значит, ошибка дизассемблера состоит в том, что он перепутал данные и команды и на этой почве сделал два неправильных присваивания, equ, попавших в начало подпрограммы s32.
Исправления будут заключаться в следующем:
- Убрать из начала подпрограммы s32 два псевдооператора equ.
- Переписать коды на рисунке 7 следующим образом:
d00c14 db 0a,03,01,01,00,00,00,00 ;00c14 d00c1c db 0bf,06 ;00c1c d00c1e db 13,05,0c0,06 ;00c1e
В заключение рассмотрим совсем простенький фрагмент кода:
;<01252> s39 proc near d0125d equ 00125dh d0125f equ 00125fh dec bh ;01252 jz b0124f ;01254 ;Jump if equal (ZF=1) xor ah,ah ;01256 shl al,1 ;01258 ;Multiply by 2's rcl ah,1 ;0125a ;CF<--[HI .. LO]<--CF ret ;0125c ;----------------------------------------------------- add BYTE PTR [bx+si],al ;0125d add BYTE PTR [bx+si],al ;0125f s39 endp
Укажем без комментариев, что подпрогромма s39 должна выглядеть так:
;<01252> s39 proc near dec bh ;01252 jz b0124f ;01254 ;Jump if equal (ZF=1) xor ah,ah ;01256 shl al,1 ;01258 ;Multiply by 2's rcl ah,1 ;0125a ;CF<--[HI .. LO]<--CF ret ;0125c ;----------------------------------------------------- d0125d db 00,00 ;0125d d0125f db 00,00 ;0125f s39 endp
В заключение этого пункта подведем итоги. Значки equ называют всевдооператорами. Если говорить о дизассемблере DisDoc 2.3, то это название удивительно точное. Если в тексте встретится equ - то ошибка рядом. Между тем, иногда DisDoc 2.3 употребляет equ вполне корректно. Так что будьте бдительны и не дайте себя обмануть.
2. Дурные ошибки.
Иногда поведение дизассемблера трудно объяснить. Например, он выдает
add WORD PTR ds:d96be3,07 ;038b6 shr WORD PTR ds:d96be3,cl ;038bb ;Divide by 2's вместо add WORD PTR ds:d06bf3,07 ;038b6 shr WORD PTR ds:d06bf3,cl ;038bb ;Divide by 2's ,
теряет или искажает куски данных. К счастью, это происходит очень редко.
Оставить комментарий
Комментарии
Например, ассоциативный дизассеблер ADAxx свободен от всех перечисленных в статье недостатков, к тому же может дизассемблировать модули, обработанные упаковщиками и протекторами.