Программа InDuLgEo V3-B горит пламенем на экране, печатает текст и трезвонит, как старый телефон.

  • 01H Дизассемблируем программу и патчим так, чтобы печатала другой текст.

  • 02H Узнаем, как программа работает с динамиком IBM PC и издает звуки, сыграем другую мелодию.

  • 03H Выясним, как программа рисует пламя, окрасим в другой цвет, изобразим горящий текст.

Будем есть слона по частям: сперва расскажу, как сменить текст, а про звук и видео - в следующий раз.

Запускаем программу

Программа работает под:

  • MS-DOS

  • Windows XP (SP2 или ранее)

  • DOSBox 0.74+

Под DOSBox пригодится Turbo Debugger, DEBUG.EXE или другой отладчик.

Текст, который скоро заменим
Текст, который скоро заменим

Поищем строки в .com-файле, тогда найдем и код, что печатает текст. Не повезло - строк в файле не вижу, значит, код расшифровывает строки перед тем, как печатает.

Распаковываем программу

MS-DOS загружает .com-файл по адресу 100h и выполняет программу с первого байта.

      //
      // ram 
      // ram:0000:0100-ram:0000:05a2
      //
      assume DF = 0x0  (Default)
0000:0100 81 fc 78 f5     CMP        SP,0xf578
0000:0104 77 02           JA         LAB_0000_0108
0000:0106 cd 20           INT        0x20
                      LAB_0000_0108                                   XREF[1]:     0000:0104(j)  
0000:0108 b9 a3 04        MOV        CX,0x4a3
0000:010b be a3 05        MOV        SI,0x5a3
0000:010e bf 18 f5        MOV        DI,0xf518
0000:0111 bb 00 80        MOV        BX,0x8000
0000:0114 fd              STD
0000:0115 f3 a4           MOVSB.REP  ES:DI,SI
0000:0117 fc              CLD
0000:0118 87 f7           XCHG       DI,SI
0000:011a 83 ee c8        SUB        SI,-0x38
0000:011d 57              PUSH       DI
0000:011e 57              PUSH       DI
0000:011f e9 a2 f3        JMP        LAB_0000_f4c4
0000:0122 45              ??         45h    E
0000:0123 64              ??         64h    d
0000:0124 64              ??         64h    d
0000:0125 79              ??         79h    y

Программа копирует себя в другую область памяти и передает управление туда - так она прячет точку входа от дизассемблера. Найдем смещение точки входа f4c4h от начала кода.

Инструкция MOVSB.REP копирует CX байтов c адреса DS:SI на адрес ES:DI. Программа устанавливает флаг DF, поэтому копирует байты задом наперед: начинает с 5a3h и двигается к 100h.

16-битный процессор способен адресовать 2^16 байтов = 2^6 Кб = 64 Кб памяти. Intel научили 16-битный процессор 8088 адресовать 2^20 = 1 Мб памяти или 2^16 * 2^4 = 16 сегментов памяти размера 64 Кб.

Intel дали 16-битному процессору 8088 20-битную шину адреса, чтобы увеличить оперативную память до 2^20 = 1 Мб. 8088 составляет 20-битный адрес из двух 16-битных регистров - сегмента и смещения - сдвигает регистр сегмента влево на 4 бита и складывает со смещением. Пример:

  • адрес следующей инструкции в сегменте кода: (cs << 4) + ip

  • адреса в сегменте данных: (ds << 4) + si, (ds << 4) + di, (ds << 4) + 46Ch

  • вершина стека в сегменте стека: (ss << 4) + sp

Программа занимает 4a3h байтов - от 100h до 5a2h, поэтому SI указывает на следующий за последней инструкцией байт 5a3h. Программа копирует 4a3h байтов, и DI указывает на f075h = f518h - 4a3h, но первый байт программы 100h еще не скопирован в f075h.

0000:05a2 c3              RET   ; Последяя инструкция в файле InDuLgEo_V3-B.CoM

Программа обменивает указатели SI и DI, увеличивает SI на 38h и дважды толкает DI в стек. Позже это нам пригодится.

0000:0118 87 f7           XCHG       DI,SI ; DI=100h, SI=f075h
0000:011a 83 ee c8        SUB        SI,-0x38 ; SI=f0adh
0000:011d 57              PUSH       DI ; 100h
0000:011e 57              PUSH       DI ; 100h

Вычислим смещение точки входа f4c4h от начала области памяти f075h:

f4c4h - f075h = 44fh

Добавим 100h и получим адрес 54fh, дизассемблируем код:

                      LAB_0000_054e                                   XREF[1]:     0000:0552(j)  
0000:054e a4              MOVSB      ES:DI,SI
                      LAB_0000_054f                                   XREF[1]:     0000:057f(j)  
0000:054f e8 34 00        CALL       FUN_0000_0586                                    undefined FUN_0000_0586()
0000:0552 72 fa           JC         LAB_0000_054e
0000:0554 41              INC        CX
                      LAB_0000_0555                                   XREF[1]:     0000:055a(j)  
0000:0555 e8 29 00        CALL       FUN_0000_0581                                    undefined FUN_0000_0581()
0000:0558 e3 35           JCXZ       LAB_0000_058f
0000:055a 73 f9           JNC        LAB_0000_0555
0000:055c 83 e9 03        SUB        CX,0x3
0000:055f 72 06           JC         LAB_0000_0567
0000:0561 88 cc           MOV        AH,CL
0000:0563 ac              LODSB      SI
0000:0564 f7 d0           NOT        AX
0000:0566 95              XCHG       AX,BP
                      LAB_0000_0567                                   XREF[1]:     0000:055f(j)  
0000:0567 31 c9           XOR        CX,CX
0000:0569 e8 15 00        CALL       FUN_0000_0581                                    undefined FUN_0000_0581()
0000:056c 11 c9           ADC        CX,CX
0000:056e 75 08           JNZ        LAB_0000_0578
0000:0570 41              INC        CX
                      LAB_0000_0571                                   XREF[1]:     0000:0574(j)  
0000:0571 e8 0d 00        CALL       FUN_0000_0581                                    undefined FUN_0000_0581()
0000:0574 73 fb           JNC        LAB_0000_0571
0000:0576 41              INC        CX
0000:0577 41              INC        CX
                      LAB_0000_0578                                   XREF[1]:     0000:056e(j)  
0000:0578 41              INC        CX
0000:0579 8d 03           LEA        AX,[BP + DI]
0000:057b 96              XCHG       AX,SI
0000:057c f3 a4           MOVSB.REP  ES:DI,SI
0000:057e 96              XCHG       AX,SI
0000:057f eb ce           JMP        LAB_0000_054f

Цикл 054e-0552 снова копирует байты, пока CF=1. Заглянем в FUN_0000_0586 - наверняка, она влияет на CF.

                      FUN_0000_0586                                   XREF[2]:     0000:054f(c), 
                                                                                   FUN_0000_0581:0000:0581(c)  
0000:0586 01 db           ADD        BX,BX
0000:0588 75 04           JNZ        LAB_0000_058e
0000:058a ad              LODSW      SI
0000:058b 11 c0           ADC        AX,AX
0000:058d 93              XCHG       AX,BX
                      LAB_0000_058e                                   XREF[1]:     0000:0588(j)  
0000:058e c3              RET

Процедура удваивает BX, но, если получает 0, загружает в BX слово из памяти. Вспоминаем двоичную арифметику:

2 * 0 = 0
0000H << 1 = 0000H

2 * (-0) = 0
8000H << 1 = 10000H 
старший бит не влез в 16 бит, поэтому отбрасываем, получаем 0

Значит, процедура читает слово из памяти, когда BX = 0 или BX = -0. Чему равен BX?

0000:0111 bb 00 80        MOV        BX,0x8000

Запустим отладчик и убедимся, что BX=8000h, когда программа прыгает к f4c4h. Цикл 054e-0552 копирует байты поверх исходного образа программы, начиная с адреса 100h. Заметает следы или пишет новый код на место старого?

Лезть глубже пока не хочется. Осматриваю код и замечаю:

054f: CALL FUN_0000_0586
...
0558: JCXZ LAB_0000_058f
...
057f: JMP LAB_0000_054f

Цикл 054f-057f похож на while(true), а 0558: JCXZ - на if (0 == CX) break;. Выход из цикла ведет извилистой тропой к 05a2: RET, которая снимает адрес со стека и передает ему управление.

Отладчик подскажет, что на стеке лежит адрес 100h, который 011e: PUSH DI туда толкнула. Процессор возвращается к первой точке входа, но теперь выполняет другой код. Значит, код 054e-05a2 распаковывает программу. Не будем изучать распаковщик, чтобы не поседеть раньше времени.

Сохраним программу на диск, чтобы изучить дизассемблером. Исходный .com-файл занимал область памяти 100h-5a2h, но теперь распух до 65ch. Автор программы машет нам ручкой:

563h: "------------------------------------   Hello, Cracker, looking at my code !   Hope you like! :P                      See you in hell of binary code.        By _/\\_-=InDuLgEo 2011=-_/\\_           ------------------------------------"

Дизассемблируем программу

Программа MS-DOS работает с экраном с помощью системных вызовов, процедур BIOS и пишет в область памяти, которую компьютер проецирует на экран. Инструкция INT выполняет системный вызов или процедуру BIOS. Код содержит инструкции INT трех видов:

  • INT 0x10: Процедуры BIOS для работы с видео

  • INT 0x16: Процедуры BIOS для работы с клавиатурой

  • INT 0x21: Системный вызов MS-DOS

Вот их адреса:

  • 0000:012e INT 0x10, AH=00h Установить режим экрана, AL=13h

  • 0000:018e INT 0x10, AH=00h Установить режим экрана, AL=13h

  • 0000:028b INT 0x10, AH=00h Установить режим экрана, AL=13h

  • 0000:032a INT 0x10, AH=02h Переместить курсор на строку DH и столбец DL

  • 0000:0331 INT 0x21, AH=09h Напечатать строку по адресу DS:DX

  • 0000:033e INT 0x21, AH=09h Напечатать строку по адресу DS:DX

  • 0000:0345 INT 0x21, AH=09h Напечатать строку по адресу DS:DX

  • 0000:034f INT 0x21, AH=09h Напечатать строку по адресу DS:DX

  • 0000:0359 INT 0x21, AH=09h Напечатать строку по адресу DS:DX

  • 0000:0363 INT 0x21, AH=09h Напечатать строку по адресу DS:DX

  • 0000:036d INT 0x21, AH=09h Напечатать строку по адресу DS:DX

  • 0000:0377 INT 0x21, AH=09h Напечатать строку по адресу DS:DX

  • 0000:03b1 INT 0x16, AH=01h Проверить, введен ли символ с клавиатуры

  • 0000:03b7 INT 0x16, AH=00h Получить символ с клавиатуры

  • 0000:03bc INT 0x10, AH=00h Установить режим экрана, AL=03h

  • 0000:03d4 INT 0x16, AH=01h Проверить, введен ли символ с клавиатуры

  • 0000:03fb INT 0x21, AH=09h Напечатать строку по адресу DS:DX

  • 0000:0561 INT 0x21, AH=4Ch Завершить программу, вернуть код AL

Программа восемь раз вызывает MS-DOS Display String, а мы видим восемь строк на экране.

0000:0339 ba 1b 04        MOV        DX,0x41b
0000:033c b4 09           MOV        AH,0x9
0000:033e cd 21           INT        0x21
0000:0340 ba 6d 04        MOV        DX,0x46d
0000:0343 b4 09           MOV        AH,0x9
0000:0345 cd 21           INT        0x21
0000:0347 e8 ac 00        CALL       FUN_0000_03f6                                    undefined FUN_0000_03f6()
0000:034a ba 95 04        MOV        DX,0x495
0000:034d b4 09           MOV        AH,0x9
0000:034f cd 21           INT        0x21
0000:0351 e8 a2 00        CALL       FUN_0000_03f6                                    undefined FUN_0000_03f6()
0000:0354 ba bd 04        MOV        DX,0x4bd
0000:0357 b4 09           MOV        AH,0x9
0000:0359 cd 21           INT        0x21
0000:035b e8 98 00        CALL       FUN_0000_03f6                                    undefined FUN_0000_03f6()
0000:035e ba e5 04        MOV        DX,0x4e5
0000:0361 b4 09           MOV        AH,0x9
0000:0363 cd 21           INT        0x21
0000:0365 e8 8e 00        CALL       FUN_0000_03f6                                    undefined FUN_0000_03f6()
0000:0368 ba 0d 05        MOV        DX,0x50d
0000:036b b4 09           MOV        AH,0x9
0000:036d cd 21           INT        0x21
0000:036f e8 84 00        CALL       FUN_0000_03f6                                    undefined FUN_0000_03f6()
0000:0372 ba 44 04        MOV        DX,0x444
0000:0375 b4 09           MOV        AH,0x9
0000:0377 cd 21           INT        0x21

FUN_0000_03f6 переводит курсор экрана к следующей строке - печатает строку "\r\n".

Ранее код 0240-0285 вызывает процедуру FUN_0000_03e8 для строк, что печатает. Эта процедура расшифровывает строку побайтовым XOR.

0000:0240 b9 1a 00        MOV        CX,0x1a
0000:0243 be 01 04        MOV        SI,0x401
0000:0246 e8 9f 01        CALL       FUN_0000_03e8                                    undefined FUN_0000_03e8()
0000:0249 b9 29 00        MOV        CX,0x29
0000:024c be 1b 04        MOV        SI,0x41b
0000:024f e8 96 01        CALL       FUN_0000_03e8                                    undefined FUN_0000_03e8()
0000:0252 b9 29 00        MOV        CX,0x29
0000:0255 be 44 04        MOV        SI,0x444
0000:0258 e8 8d 01        CALL       FUN_0000_03e8                                    undefined FUN_0000_03e8()
0000:025b b9 28 00        MOV        CX,0x28
0000:025e be 6d 04        MOV        SI,0x46d
0000:0261 e8 84 01        CALL       FUN_0000_03e8                                    undefined FUN_0000_03e8()
0000:0264 b9 28 00        MOV        CX,0x28
0000:0267 be 95 04        MOV        SI,0x495
0000:026a e8 7b 01        CALL       FUN_0000_03e8                                    undefined FUN_0000_03e8()
0000:026d b9 28 00        MOV        CX,0x28
0000:0270 be bd 04        MOV        SI,0x4bd
0000:0273 e8 72 01        CALL       FUN_0000_03e8                                    undefined FUN_0000_03e8()
0000:0276 b9 28 00        MOV        CX,0x28
0000:0279 be e5 04        MOV        SI,0x4e5
0000:027c e8 69 01        CALL       FUN_0000_03e8                                    undefined FUN_0000_03e8()
0000:027f b9 28 00        MOV        CX,0x28
0000:0282 be 0d 05        MOV        SI,0x50d
0000:0285 e8 60 01        CALL       FUN_0000_03e8                                    undefined FUN_0000_03e8()
                      FUN_0000_03e8                                   XREF[9]:     0000:0246(c), 0000:024f(c), 
                                                                                   0000:0258(c), 0000:0261(c), 
                                                                                   0000:026a(c), 0000:0273(c), 
                                                                                   0000:027c(c), 0000:0285(c), 
                                                                                   0000:03f3(j)  
0000:03e8 8a 04           MOV        AL,byte ptr [SI]
0000:03ea 34 18           XOR        AL,0x18
0000:03ec 88 04           MOV        byte ptr [SI],AL
0000:03ee 46              INC        SI
0000:03ef 49              DEC        CX
0000:03f0 83 f9 00        CMP        CX,0x0
0000:03f3 75 f3           JNZ        FUN_0000_03e8
0000:03f5 c3              RET

Применим XOR к тем же байтам в файле и увидим те же строки, что на экране.

def xorString(str, b):
  result = ''
  for c in str:
    result += chr(ord(c) ^ b)
  
  return result

def stringFromHex(hexedString):
  str = ''
  for b in hexedString.split(' '):
    str += chr(int(b, 16))
    
  return str

ciphertext = '24 47 37 44 47 35 25 51 76 5c 6d 54 7f 5d 77 38 2a 28 29 29 25 35 47 37 44 47 3c 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 3c 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 5d 36 57 36 5e 36 22 31 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 32 3c 51 76 6c 6a 77 36 22 31 38 29 35 26 38 42 20 28 59 38 20 37 29 2e 38 5a 71 6c 6b 38 30 5a 79 6b 71 7b 35 59 4b 55 31 3c 2a 35 26 20 28 20 20 38 2e 2c 28 53 5a 38 30 5c 77 6b 35 27 31 38 38 2b 35 26 5f 79 4c 7d 4b 22 48 37 4d 76 51 60 31 3c 35 59 6a 6c 38 57 7e 38 4a 7d 6e 7d 6a 6b 7d 38 5d 76 7f 71 76 7d 7d 6a 71 76 7f 35 38 38 5d 76 72 77 61 38 51 6c 39 3c 35 4a 7d 4e 7d 4a 6b 5d 6a 37 5b 77 5c 7d 4a 35 38 4b 7d 7d 38 75 61 38 6f 7d 7a 23 38 35 26 70 6c 6c 68 6b 22 37 37 3c 38 38 6b 71 6c 7d 6b 36 7f 77 77 7f 74 7d 36 7b 77 75 37 6b 71 6c 7d 37 71 76 7c 6d 74 7f 7d 77 7d 7c 7c 61 37 38 38 3c'
key = 0x18

print(xorString(stringFromHex(ciphertext), key))
<_/\_-=InDuLgEo 2011=-_/\_$****************************************$***************E.O.F.:)*****************$Intro.:) 1-> Z80A 8/16 Bits (Basic-ASM)$2->8088 640KB (Dos-?)  3->GaTeS:P/UnIx)$-Art Of Reverse Engineering-  Enjoy It!$-ReVeRsEr/CoDeR- See my web; ->https://$  sites.google.com/site/indulgeoeddy/  $

Заменим строки в файле другими - правим распакованную программу, что сохранили в файл DUMP.COM:

  • Заменим инструкцию 0000:03ea 34 18 XOR AL,0x18 на 0000:03ea 34 00 XOR AL,0, или две NOP, чтобы отключить шифрование.

  • Запишем свой текст вместо зашифрованных строк.

Пусть программа печатает одну строку, а остальные семь вызовов Display String получат пустые строки. Строки лежат в части 301h-434h файла DUMP.COM. Запишем строку Cracked by sa2304 по смещению 301h от начала файла, а остальные байты заменим символом $.

Строка MS-DOS оканчивается символом $

dd if=cracked-by.txt bs=1 count=308 of=DUMP.COM seek=769 conv=notrunc

Запустим DUMP.COM, убедимся, что программа печатает новый текст.

Теперь программа печатает другой текст
Теперь программа печатает другой текст

Ссылки

До встречи!

Комментарии (9)


  1. NeriaLab
    31.07.2025 15:58

    Статья интересная, но выявлено несколько допущений:

    Под DOSBox пригодится Turbo Debugger, DEBUG.EXE или другой отладчик.

    Хороший Turbo Debugger трудно найти и если вы используете какой-то конкретный из всего списка указанного на сайте old-dos, то указывайте какой именно из списка, там не все корректно работают. Я хороший нашел в Turbo Assember 4.0 (TASM) (последняя строка)

    Сам DOSBox "дохлик", у меня стоят DOSBox 0.74.3 и DOSBox-X (этот форк DB развивается)

    Почему то не увидел совета по использованию PCEmu для использования DOS/Windows софта на современных машинах? DOS программы, по разному могут себя вести в DB и под PCEmu

    Если Вы рекомендуете использовать TD, то почему листинги даёте из Ghidra? Ghidra не видит упаковки таких как PKLITE и других... в TD, можно в работающем коде, хотя бы догадаться. Надо уже иметь опыт в реверсе, чтобы работать с упакованными данными в Ghidra или в IDA


    1. sa2304 Автор
      31.07.2025 15:58

      Спасибо за дополнения.

      Это мой первый реверс под MS-DOS, так что я не претендую на звание профессора :) Удовлетворил любопытство - нашел повод поработать с 16-битным ассемблером под MS-DOS и познакомиться с Ghidra.

      Работает первый же TurboDebugger из списка, аерсия 3.1 от 1992. В том и прелесть реверса - каждый пробует и выбирает инструменты, что работают у него :) Не работает TD.EXE, бросаем, берем другой отладчик.

      Автор crackme указал в README, что программа работает под MS-DOS, WinXP и DOSBox - так и скопировал.

      Надо уже иметь опыт в реверсе, чтобы работать с упакованными данными

      Вот с этим согласен :) Для меня дизассемблер еще ни разу не опознал упаковщик автоматически. Сохраняю распакованную программу с помощью отладчика.

      Еще один намек, что файл упакован - мы не найдем инструкций INT в исходном файле, хотя, кажется, они должны быть.


      1. NeriaLab
        31.07.2025 15:58

        Почему, найти можно - это проходил на одной из моих любимых игр созданной на Watcom C + 2ная упаковка PKLITE + UPX В этом году еще руки не дошли до продолжения, так как начал создавать дизассемблер для DOS под Windows, Именно для того, чтобы точно видеть упаковщики и можно было их "выпиливать", такие как: ExePack, LzExe, PKLite, TityProg, Upx


        1. sa2304 Автор
          31.07.2025 15:58

          На каждую новую версию упаковщика ведь нужен новый распаковщик? Наверное, универсального и всесильного распаковщика никогда не создадут, но задача интересная - автоматически опознать и вырубать хотя бы один распаковщик. А затем еще один, и еще... :)

          Нашел файл с сигнатурами для PeID - говорит, содержит 1832 сигнатуры, но это только для PE-файлов. Сколько еще алгоритмов можно выдумать для других форматов - не счесть. Пожалуй, составлять сигнатуры упакованных или зараженных файлов - то еще искусство.


          1. NeriaLab
            31.07.2025 15:58

            Да, универсального распаковщика нет, у меня есть исходники для всех известных, кроме PKLite. А вот распаковщики для самого PKLite, пересоздавались и одна из версий называется PKLite Arena, другая PKLite 1.2 и т.д.


            1. sa2304 Автор
              31.07.2025 15:58

              Интересно, что упаковщик предлагает опцию -x expand a compressed file. Жаль, что от иных программ остаются только скриншоты и воспоминания.

              Упаковщики сжимают файл - за это их и любили? Двоичные файлы сжимать трудно, но когда на дискете всего 1.44 Мб, радовались каждому лишнему байту?

              Должно быть, порядочный крекер распаковывал программу, побеждал защиту, а затем упаковывал не хуже оригинала :)


              1. NeriaLab
                31.07.2025 15:58

                Сейчас не нужно снова упаковывать программу, после дизассемблирования. Её лучше переписать, как одни из примеров - KeeperFX is an open-source remake and fan expansion of Dungeon Keeper


  1. sa2304 Автор
    31.07.2025 15:58

    ...


  1. WRP
    31.07.2025 15:58

    Дизассемблер Sourcer надо использовать, чтобы всё было «лампово»)