様様様様様様様様様様様様様様様 Self Checking Executable Files 様様様様様様様様様様様様様様様 Demogorgon Phalcon/Skism 様様様様様様様様様様様様様様様 In this article I will explain a method that will allow .COM files to be immune to simple viruses. In order to infect a .COM file, a virus must change several bytes at the beginning of the code. Before the virus returns control to the original program, it will 'disinfect' it into memory, so that the program runs as it did before infection. This disinfection process is crucial, because it means that the image on the disk will not be the same as the memory image of the program. This article describes a method by which a .COM file can perform a self-check by reading its disk image and comparing it to its memory image. The full pathname of the program that is being executed by DOS is located in the environment block. The segment of the environment block can be read from the PSP. It is located at offset [2Ch]. The name of the program is the last entry in the environment block, and can be located by searching for two zeros. The next byte after the two zeros contains the length of the string that follows it. After the length is an ASCIIZ string containing the pathname of the current process. The following code opens the file being executed: nish: mov es, word ptr ds:[2Ch] ; segment of environment xor ax, ax mov di, 1 loop_0: dec di scasw jne loop_0 mov dx, di add dx, 2 ; start of pathname push es pop ds mov ax, 3D02h ; open, read/write access int 21h Next, we must read in the file (using dos services function 3Fh, read file or device). We can read the file into the heap space after the program, as long as we are sure we will not overwrite the stack. The sample program in this file reads itself in entirely, but remember, it is not necessary to do so. It is only necessary to read and compare the first few bytes. Also, the program could read itself in blocks instead of all at once. If a file finds itself to be infected, it should report this to the user. Remember, even though the file knows it is infected, the virus has already executed. Memory resident viruses will already have loaded themselves into memory, and direct action viruses will already have infected other files on the drive. Thus, any virus that employs disinfection on the fly will be able to avoid detection and removal. Here is the full source to the self checking program: ;();();();();();();();();();();();();();();();();();();();();() .model tiny .code org 100h start: mov es, word ptr ds:[2Ch] ; dos environment block xor ax, ax mov di, 1 loop_0: dec di scasw jne loop_0 mov dx, di add dx, 2 ; <- point to current push es ; process name pop ds mov ah, 3Dh ; open file with handle int 21h jc bad ; error opening file ? mov bx, ax push cs push cs pop es pop ds ; I am a com file. mov cx, heap - start ; length lea dx, heap ; where to read file into mov ah, 3Fh ; read file or device int 21h jc bad ; error reading file ? ; here, do a byte for byte compare lea si, start lea di, heap repe cmpsb ; compare 'em jne bad lea dx, clean mov ah, 9 int 21h jmp quit_ bad: mov ah, 9 lea dx, infected int 21h quit_: mov ax, 4C00h int 21h clean db 'Self check passed.$' infected db 'Self check failed. Program is probably infected.$' heap: end start ;();();();();();();();();();();();();();();();();();();();();() While some self checking routines opt to use a crc or checksum error detection method, the byte for byte method is both faster and more accurate. Weak points: This routine will not work against a stealth virus which employs disinfection on the fly. Such viruses take over the dos interrupt (int 21) and disinfect all files that are opened and read from. As the routine in this article attempts to read itself into memory, the stealth virus would disinfect it and write an uninfected copy to ram. Of course, there are ways to defeat this. If this program were to use some sort of tunneling, it could bypass the stealth virus and call DOS directly. That way, infections by even the most sophisticated viruses would be detectable. Disinfection: So, now you can write programs that will detect if they have been infected. How about disinfection? This too is possible. Most viruses simply replace the first three bytes of the executable file with a jump or a call, which transfers control to the virus code. Since only the first three bytes are going to be changed (in almost all cases), it will usually be possible for a program to disinfect itself by replacing the first three bytes with what is supposed to be there, and then truncating itself to the correct size. The next program writes the entire memory image to disk, rather than just the first three bytes. That way, it can be used to disinfect itself from all nonstealth viruses. The steps to disinfect are simple. First of all, you must move the file pointer back to the beginning of the file. Use interrupt 21, ah=42h for this. The AL register holds the move mode, which must be 00 in this case (move from beginning of file). CX:DX holds the 32bit number for how many bytes to move. Naturally, this should be 0:0. The second step is to write back the memory image to the file. Since the virus has already restored the first few bytes of our program in memory, we must simply write back to the original file, starting from 100h in the current code segment. i.e.: mov ah, 40h mov cx, heap - start ; bytes to write lea dx, start int 21h ; write file or device Finally, we must truncate the file back to its original size. To truncate a file, we must move the file pointer to the end and call the 'write file or device' function with cx, the bytes to write, equal to zero. To move the pointer, do this: mov ax, 4200h mov cx, (heap - start) SHR 16 ; high word of file ptr mov dx, (heap - start) ; low word of file ptr int 21h ; move file pointer Since we are dealing with .COM files here, it is safe to assume that cx, the most significant word of the file ptr, can be set to zero, because our entire file must fit into one segment. We do not need to calculate it as above. To truncate: xor cx, cx mov ah, 40h int 21h ; truncate file The full code for the self disinfecting program follows. ;();();();();();();();();();();();();();();();();();();();();() .model tiny .code org 100h start: mov es, word ptr ds:[2Ch] ; segment of environment xor ax, ax mov di, 1 loop_0: dec di scasw jne loop_0 mov dx, di add dx, 2 push es pop ds mov ax, 3D02h ; open, read/write access int 21h mov bx, ax ; handle into bx push cs push cs pop es pop ds mov cx, heap - start lea dx, heap mov ah, 3Fh ; read file or device int 21h jc quit_ ; can't read ? lea si, start lea di, heap repe cmpsb ; byte for byte compare jne bad lea dx, clean ; we are golden mov ah, 9 ; print string int 21h jmp main_program bad: mov ah, 9 ; we are infected lea dx, infected int 21h lea dx, disinfection int 21h ; now, disinfect. File handle is still in bx ; we must move the file pointer to the beginning xor cx, cx xor dx, dx mov ax, 4200h int 21h ; move file pointer mov ah, 40h ; 40hex! mov cx, heap - start lea dx, start int 21h ; write file or device jnc success lea dx, not__ mov ah, 9 int 21h success:mov ah, 9 lea dx, successful int 21h xor cx, cx mov ah, 40h ; 40hex! int 21h ; truncate file main_program: quit_: mov ax, 4C00h int 21h disinfection db 0Dh, 0Ah, 'Disinfection $' not__ db 'not ' successful db 'successful.$' clean db 'Self check passed.$' infected db 'Self check failed. Program is probably ' db 'infected.$' heap: end start ;();();();();();();();();();();();();();();();();();();();();() Weak points: The same weak points that apply above also apply here. Additionally, the program may, by writing itself back to disk, give the virus the opportunity to reinfect. Remember, any memory resident viruses will already have loaded into memory by the time the program disinfects itself. When the program tries to disinfect itself, any virus that intercepts the 'write file or device' interrupt will intercept this write and re-infect. Again, tunneling is the clear solution.