Опыт дизассемблирования большой .com программы

ОГЛАВЛЕНИЕ

Тогда, год назад, я попробовал дизассемблировать простенькую программу и был страшно удивлен тем, что дизассемблер делает это неправильно, и при повторном ассеблировании программа не работала так, как надо. Тогда же мне удалось поговорить со знающим человеком и, хотя я чувствовал себя наивным дурачком, мне удалось выяснить главное: ПОЛНОЕ,АВТОМАТИЧЕСКОЕ ДИЗАССЕМБЛИРОВАНИЕ НЕВОЗМОЖНО, над тем текстом, который выдает дизассемблер, нужно довольно долго работать, прежде чем ассемблирование этого текста даст работоспособную программу.

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

Почему 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. Проблема OFFSET'a

Предположим, что в тексте, который выдал диза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
Рис.1

Так как 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
Рис.2 Рис.3

Но вся беда в том, что исходная программа была ассемблирована другим ассемблером и имеет вид, показанный на рисунке 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

Сравнение рисунков 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 Рис.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.

Итак, чтобы исправить эту ошибку, необходимо:

  • Удалить из начала подпрограммы 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 ,

    теряет или искажает куски данных. К счастью, это происходит очень редко.