From : Dan Bridges                                           3:640/820.2
Subj : NEXT_MCB Antivirus prog


Another snippet from Learning Assembler Using DEBUG Part 10. (The articles were
rather large so I'm only presenting some of the programs and a few connecting
comments.) In Part 9 I discussed the NO FRILLS V3 virus and Memory Control
Blocks, showing the effect of the NF3 virus on the size field of a program's MCB
and the location of the "next" MCB after a program had requested all available
low memory. This got me to thinking about using this method to detect resident
viruses (the vast majority of viruses go memory-resident). It will detect
viruses both known and unknown/yet-to-be-written and it takes less than a second
to run.

First off, the layout of the MCB's 16 bytes:

Offset   Size    Value      Comment
00h      Byte    TYPE       "M"; "Z" if last MCB.
01h      Word    OWNER      PSP of the owner.
03h      Word    SIZE       In paras.
05h      3 Bytes UNUSED
08h      8 Bytes OWNER NAME Filename of owner, for program MCB only, if
                            DOS 4 or better.

By adding a program's MCB size to its PSP you get the upper limit of its low
memory usage. If you then increment this you get the starting location of the
next MCB.

There are three common virus memory residence locations:

1. At top of low mem. Reduce TOM (top of mem) value in BDA (BIOS Data Area) at
40:13h from typical 280h (640 decimal; specifies total low mem in K) to say 27Dh
(638 decimal) if 2K is required by virus. All BSVs (Boot Sector Viruses) do
this. Examples: Michelangelo, Junkie, Monkey.

2. Top-of-mem exe infector. Don't alter TOM. Alter a prog's MCB size so that, if
it requests all free mem (most do), it won't overwrite virus sitting just below
TOM. Example: No Frills.

3. Bottom-of mem exe infector. Don't alter TOM. Virsus resides just after the
resident portion of command processor, as would other typical TSRs. Example:
Jerusalem.

Assume now that DOS and other basic stuff was consuming 16K from 0000-03FF
(paras) and therfore 9C00h paras (624K) is free out of the typical A000h (640K)
total. Here are the kind of figures you'd expect for free mem with a 2K (80h
paras) resident virus (ignores the program's copy of the environment and the
program's & its environment's PSP):

1. BSV.                 TOM/Next MCB=9F80h  Free Mem=9B80h (all in paras)
2. Top of mem exe infector. Next MCB=9F80h  Free Mem=9B80h
3. Bottom of mem exe inf.   Next MCB=A000h  Free Mem=9B80h

So the first two types of virus could be detected by their reduction on the
expected NEXT MCB value of A000h while all three could be detected by the
reduction in the "free" mem value available to the program.

This method will not work for non-resident viruses ("direct infectors"). It will
also not work for viruses that load themselves in UMBs. Perhaps someone could
present a program to total allocated UMBs.

To investigate the Next MCB concept here is a simple program (NEXTMCB1.COM) that
returns an errorlevel of 1 if the Next MCB's segment is less than expected:

Next_MCB_Should_Be  EQU  0A000h  ; In paragraphs. 640K.
MainProg  SEGMENT PUBLIC WORD 'CODE'
ASSUME CS:MainProg, DS:MainProg
ORG 0100h                ; Starting offset for a COM.
Starting_Point:
  Mov   bx, ds           ; In a COM file CS, DS, ES are all set to
          ; the PSP seg. Here we'll use DS.
  Dec   bx               ; Decrement it to get Prog MCB seg.
  Mov   ds, bx           ; ES now holds Prog MCB seg.
  Add   bx, word ptr ds:[3]       ; Add the size of the Prog's
                         ; MCB to the Prog MCB Seg to get end of the last seg.
  Inc   bx               ; Increment this to get the next "free" seg.
  Mov   ax, 4C00h        ; Int 21h, Func 4Ch being set up.
                         ; Terminate Program with Errorlevel.
                         ; Default errorlevel in AL is set to 00h.
  Cmp   bx, Next_MCB_Should_Be   ; Compare free seg with
  Jae        Exit        ; expected value and exit if not less.
  Inc   al               ; Was less than expected. Errorlevel to
Exit:                    ; be transmitted is 01h.
  Int   21h              ; 4Ch is still in AH. Errorlevel in AL.
MainProg  ENDS
END Starting_Point


Note: Some systems will have a TOM of less than A000h.

To test this program you need to have a method of reducing the size of a
expected free low memory value. Here is a DEBUG scriptfile to do that:


A100
MOV  AX, DS
DEC  AX
MOV  DS, AX
DS:
SUB  WO [3], 100
MOV  AH, 4C
INT  21

N DROP100.COM
RCX
10
W100
Q

To create DROP100.COM:    DEBUG < DROP100.SCR
Remember to leave a blank line in the middle to terminate assembler mode and to
press Enter after Q(uit) when typing it in.

Each time you run this you'll reduce memory a further 100h paras (4K). It can be
run more than once.

To set up the following program type in excessive intial values (assuming you're
running a low mem greater than 640K) such as NEXT_MCB B000 B000. You see
something like (I'm here running and OS/2 DOS session I've specified as 639K -
they can get much
bigger) :

[0] (601K) f:\> NEXT_MCB B000 B000


The Next MCB should have started at B000
                  but now starts at 9FC0

    Free paras have been reduced by 1040

Now rerun with the correct first parameter to determine the second parameters
value:

[0] (601K) f:\> NEXT_MCB 9FC0 B000


  Free low memory (paras) should be B000
                         but is now 9651

    Free paras have been reduced by 19AF


Finally check with NEXT_MCB 9FC0 9651


; NEXT_MCB.COM by Dan Bridges. Checks the loc of the next MCB
; and optionally checks the amount of free mem. If either is
; less than expected it shows the "should be" value, the actual
; value, the memory reduction and produces a pulsed beep until
; you press any key. It defaults to a next MCB seg value of
; A000h. It can have up to two parms.

; Examples:
; NEXT_MCB               Next MCB = A000h paras
; NEXT_MCB 9F80          Next MCB = 9F80h paras
; NEXT_MCB 9F80 9C74     Next MCB = 9F80h paras  Free Mem = 9C74h paras

Beep_Freq       EQU 1000        ; 1000 Hz beep.
Beep_Duration   EQU 2           ; Beep Duration.
Pause_Duration  EQU 1           ; Pause Duration.
Sq_Wave_Period  EQU 1193180/Beep_Freq
; TASM 2.0 will evaluate this. MASM 5.1 won't. With
; MASM 5.1 use the calculated value e.g. 
; Sq_Wave_Period  EQU  1193
; Note: I've recently found that TASM 3.1 doesn't evaluate it!

Program  SEGMENT WORD PUBLIC 'CODE'
ASSUME  CS:Program,DS:Program,ES:Program
ORG     0100h                   ; COM file.
Start_Point:                    ; DS & ES are set to PSP.
  Mov   [OrigStackPtr],sp       ; Store current stack
                                ; pointer position so it can be restored
                                ; in the event of jumping to the Exit
                                ; section from within a procedure.
  Cmp   byte ptr es:[80h],00h   ; Location in PSP of
  Jnz   Check_Cmd_Line          ; of num of parmater chars.
  Mov   cx,0A000h               ; No parms. Set 640K.
  Jmp   short Check_Next_MCB
Check_Cmd_Line:                 ; Parms were supplied.
  Mov   bl,byte ptr es:[80h]    ; Number of characters.
  Cmp   bl,05h                  ; e.g. " 9F80"
  Jnz   Check_Parm2
  Mov   [Num_OF_Parms],01h      ; 5 characters supplied.
  Jmp   short Calculate_First_Parm
Check_Parm2:                    ; Number of chars wasn't 5.
  Cmp   bl,0Ah                  ; 10 = 2 parms e.g. " 9F80 9C74".
  Jnz   Exit_Jump_Island        ; Jump if 10 chars were not
                                ; supplied. A conditional jump can only be
                                ; to a label up to -127/+128 bytes away.
  Mov   [Num_OF_Parms],02h      ; There were 2 parms.
Calculate_First_Parm:
  Mov   si,85h                  ; End of 1st parm in PSP.
  Call AscWord2Bin              ; Work out its value.
        ; Afterwards, CX contains value e.g. 9F80h.

Check_Next_MCB:
  Mov   bx,es                   ; BX = PSP seg.
  Dec   bx                      ; BX = Prog MCB seg.
  Mov   es,bx                   ; ES = Prog MCB seg.
  Add   bx,word ptr es:[3]      ; Add size of the Prog's MCB
                                ; to the Prog Seg for Seg end.
  Inc   bx                      ; Inc this to get the next seg.
  Cmp   bx,cx                   ; Temp subtract: Should_Be - Actual.
  Jae   No_Problem              ; Jump if big enough.
  Call NL                       ; Next seg was less than expected.
  Call NL
  Mov   dx,offset Next_MCB_Msg1
  Call  Show_Msg                ; Show 1st msg line.
  Mov   ax,cx
  Call  Show_Word               ; Show Should_Be from AX.
  Mov   dx,offset Next_MCB_Msg2
  Call  Show_Msg                ; Show 2nd msg line.
  Mov   ax,bx
  Call  Show_Word               ; Show actual value.
  Call  NL
  Mov   dx,offset Problem_Msg3
  Call  Show_Msg                ; Show 3rd msg line.
  Sub   cx,bx                   ; Work out reduction.
  Mov   ax,cx                   ; Put in AX for displaying.
  Call  Show_Word               ; Show reduction.
  Mov   dx,offset Message   
  Call Show_Msg                 ; Show "Press any key..."
  Jmp   Check_Keyboard_Status
No_Problem:                     ; No problem with next MCB.
  Cmp   [Num_Of_Parms],02h      ; 2nd parm given?
  Jne   Exit                    ; If no 2nd parm, finish up.
  Jmp   short Calculate_Second_Parm
Exit_Jump_Island:
  Jmp   short Exit              ; Stepping stone.
Calculate_Second_Parm:
  Mov   si,8Ah                  ; End of 2nd parm.
  Call  AscWord2Bin             ; Value ends up in CX.
Check_Free_Mem:
  Cmp   word ptr es:[3],cx
    ; Temp Subtract: Prog MCB size - Expected size.
  Jae   Exit                    ; Exit if enough mem.
  Call  NL                      ; Prepare to show the bad news.
  Call  NL
  Mov   dx,offset Free_Mem_Msg1
  Call  Show_Msg
  Mov   ax,cx                   ; Should_Be value.
  Call  Show_Word
  Mov   dx,offset Free_Mem_Msg2
  Call  Show_Msg
  Mov   ax,word ptr es:[3]      ; Actual value.
  Call  Show_Word
  Call  NL
  Mov   dx,offset Problem_Msg3
  Call  Show_Msg
  Sub   cx,word ptr es:[3]
  Mov   ax,cx
  Call  Show_Word               ; Reduction in free mem.
  Mov   dx,offset Message
  Call  Show_Msg

Check_Keyboard_Status:          ; Int 21h,Func 0Bh.
  Mov   ah,0Bh                  ; Check STDIN status.
  Int   21h                     ; AL = FFh if char ready.
  Cmp   al,0FFh                 ; Key pressed yet?
  Jz    Got_A_Keystroke         ; Yes.
  Call  Produce_A_Beep          ; No. Keep beeping.
  Mov   cx,Pause_Duration
  Call  Wait_Routine
  Jmp   short Check_Keyboard_Status
                                ; Time to check the keyboard again.
Got_A_Keystroke:                ; Key pressed. Use Int 21h Func 07h
  Mov   ah,07h                  ; (Input Char with No Echo) to
  Int  21h                      ; "swallow" keystroke.
Exit:
  Mov   sp,[OrigStackPtr]       ; Ensure that the stack
                                ; is in same state as when started.
  Mov   ah,4Ch
  Int   21h                     ; Int 21h, Func 4Ch. End Prog.
;*******************************************************

AscWord2Bin PROC NEAR           ; "9F80" -> 9F80h.
  Std                           ; Reverse Direction flag. Work
  Xor   cx,cx                   ; from end of string forward.
  Call  Hex2Bin                 ; After call, AX = bin value.
  Mov   cx,ax                   ; CX holds running total.
  Call  Hex2Bin                 ; Process the 2nd number.
  Mov   dx,10h                  ; Multiply it by 16.
  Mul   dx
  Add   cx,ax                   ; Add it to total.
  Call  Hex2Bin                 ; Process the 3rd number.
  Mov   dx,100h                 ; Multiply it by 256.
  Mul   dx
  Add   cx,ax                   ; Add it to total.
  Call  Hex2Bin                 ; Process the last number.
  Mov   dx,1000h                ; Multipy it by 4,096.
  Mul   dx
  Add   cx,ax                   ; Add it to total.
  Cld                           ; Set Dir flag to forward.
  Ret
AscWord2Bin ENDP

Hex2Bin  PROC NEAR              ; Hex num -> bin value.
  Xor   ax,ax                   ; Clear AX.
  Lodsb                         ; Load AL from [SI]. Dec SI.
  Cmp   al,"0"                  ; Was ASCII char a number?
  Jb    Exit
  Cmp   al,"9"
  Ja    Try_A_to_F
                            ; If not "0" to "9" might be "A" to "F".
  Sub   al,30h
                            ; "0"-"9" (30h-39h) -> 00h-09h in AL.
  Jmp   short Hex2Bin_1
Try_A_to_F:
  And   al,0DFh                 ; Bit mask to covert letter to
           ; uppercase. Only difference between "a" (01100001)
           ; and "A" (01000001) is that Bit 5 (counting
           ; from zero) is 0 in uppercase lettrrs. ANDing with
           ; DFh (11011111) ensures that Bit 5 is 0.
  Cmp   al,"A"
  Jb    Exit
  Cmp   al,"F"
  Ja    Exit
  Sub   al,37h                  ; "A"-"F" (41h-46h) -> 0Ah-0Fh.
Hex2Bin_1:
  Ret
Hex2Bin  ENDP

Show_Word  PROC NEAR            ; Show 2 bytes in word.
  Push  ax
  Mov   al,ah                   ; Show the higher byte 1st.
  Call  Show_Byte               ; Show what's in AL.
  Pop   ax
  Call  Show_Byte               ; Now show the lower byte.
  Call  NL
  Ret
Show_Word  ENDP

Show_Byte  PROC NEAR            ; Byte in AL will be displayed.
  Push  ax
  Shr   al,01h                  ; Consider only high nibble
  Shr   al,01h                  ; of AX by dividing it by
  Shr   al,01h                  ; 16 i.e. divide by 2^4.
  Shr   al,01h
  Add   al,30h                  ; Make char "0" or greater.
  Cmp   al,3Ah                  ; Is it "A" or greater?
  Jb    Set_High_Char           ; It's "0" to "9".
  Add   al,07h                  ; It's "A" to "F" so add 7h
                                ; since A" is 41h. 3Ah + 07h = 41h.
Set_High_Char:
  Mov   byte ptr [Hold_2Chars],al
    ; Put char in the character display buffer.
  Pop   ax                      ; Restore AX for low nibble.
  And   al,0Fh                  ; Consider low nibble in AL.
  Add   al,30h                  ; Make the char "0" or greater.
  Cmp   al,3Ah                  ; Is it "A" or greater?
  Jb    Set_Low_Char            ; It's "0" to "9".
  Add   al,07h                  ; It's "A" to "F".
Set_Low_Char:                   ; Put in display buffer.
  Mov  byte ptr [Hold_2Chars+1],al
Show_Them:
  Mov   ah,09h
  Mov   dx,Offset Hold_2Chars   ; Show 2 chars.
  Int   21h
  Ret
Show_Byte  ENDP

NL  PROC NEAR                   ; Sends CR/LF to screen.
  Mov   dx,offset Newline
  Mov   ah,09h
  Int   21h
  Ret
NL  ENDP

Show_Msg  PROC NEAR             ; Display "$"-terminated string
  Mov   ah,09h                  ; whose location is already in
  Int  21h                      ; DX.
  Ret
Show_Msg  ENDP

Produce_A_Beep  PROC NEAR
  Mov  al, 0B6h                 ; Prepare to set Timer2 for Periodic
                                ; Square Wave mode with Binary Counting
  Out  43h, al                  ; and LSB-then-MSB input. Set mode.
  Mov  ax, Sq_Wave_Period       ; Period value is a word.
  Call   IO_Delay
  Out   42h, al         ; Set Timer 2 Period value, Low Byte (first).
  Mov   al, ah                  ; Transfer count high byte to AL.
  Call   IO_Delay
  Out   42h, al         ; Set Timer 2 Period value, High Byte (next).
  Call   IO_Delay
  In    al, 61h                 ; Get current Speaker & Misc. setting.
  Mov   ah, al                  ; Save this in AH as well.
  Or    al, 03h                 ; Prepare to set Speaker ctrl bits 1&0.
  Call   IO_Delay
  Out   61h, al                 ; Make sure Speaker ctrl bits 1&0 are ON
  Mov   cx, Beep_Duration
  Call   Wait_Routine
  Mov   al, ah                  ; Transfer stored original ctrl value.
  Out   61h, al                 ; Reset original control byte value.
  Ret
Produce_A_Beep ENDP

Wait_Routine  PROC NEAR         ; Produces a constant delay.
  Mov   ah, 86h                 ; Int 15h, Func 86h.
  Mov   dx, 0000h               ; Wait for CX:DX s.
  Int   15h                     ; Each increment of CX is worth 66ms.
  Ret
Wait_Routine  ENDP

IO_Delay  PROC NEAR             ; Produces a delay of at least 1 s.
  Mov   cx, 64h                 ; Suitable for 90Mhz Pentium or less.
A_Waste_Of_Time:                ; 486 and less could use 0Eh instead.
  Loop A_Waste_Of_Time
  Ret
IO_Delay  ENDP

Newline   DB   0Dh, 0Ah, "$"
Message DB      0Dh, 0Ah, "Press any key to continue...$"
Free_Mem_Msg1   DB        "  Free low memory (paras) should be $"
Free_Mem_Msg2   DB        "                         but is now $"
Next_MCB_Msg1   DB        "The Next MCB should have started at $"
Next_MCB_Msg2   DB        "                  but now starts at $"
Problem_Msg3    DB        "    Free paras have been reduced by $"
Hold_2Chars     DB        0, 0, "$"
Num_Of_Parms    DB        0
OrigStackPtr    DW        ?
 
Program  ENDS
END  Start_Point

