TweetFollow Us on Twitter

Virus Patrol
Volume Number:5
Issue Number:2
Column Tag:Advanced Mac'ing

Security Patrol for Viruses

By Steven Seaquist, Washington, DC

Note: Source code files accompanying article are located on MacTech CD-ROM or source code disks.

Politics of Virus Protection

“SecurityPatrol” is a program I wrote to detect and optionally remove the “nVIR” and “Scores” viruses (all Macintosh viruses known at the time I wrote it), plus several other conditions I would find suspicious and would want to be warned about. This is version 1.1. It’s been tested against live strains of nVIR and Scores. It contains a code database of “fingerprints” to identify known viruses and detect alterations of a system’s “good” (trusted) resources by future viruses. [The source code disk for this month contains the complete library of system resource fingerprints for Security Patrol. In the interest of space, only a few example fingerprints are printed in the magazine. -Ed]

It wasn’t coded so much to be “user-friendly”; my main goal was to make it “security-stubborn”: It’s deliberately not easy to alter its code database of resources. You have to code them in manually and recompile. To put it another way, the easier it is for a user to alter the database, the easier it is for a virus to alter the database and masquerade as something innocuous.

On the other hand, there are advantages to the fact that it’s being published in source code format. It allows you to re-code it to deal with future viruses and/or to make it deliberately inconsistent with the published version. (A predictable defense is generally a weakness to attack.) Another significant advantage is that it inherently discloses its behavior. Sunlight is a strong disinfectant.

[While Apple Computer cannot and does not endorse anything in this article, it's publication in MacTutor was of sufficient concern that Scott Boyd, a contributing editor of MacTutor and a member of Apple's internal virus committee, asked and was given permission to review the article prior to publication. He indicated Apple's concern that an article of this sort that points out the system weaknesses can also give more ammunition to fuel future virsus makers. However, since three books have recently been published on how to create a computer virus, we feel the cat is out of the bag anyway so the protection this technology will allow developers is more vital than ever. Accordingly, we are publishing the article essentially unchanged from the author's version, except for a couple of places where Steven reveals "chinks in the armor" of Apple's system software. Scott did allow that many of Steven's techniques are being used in internal security programs at Apple so we feel this approach has merit. We invite comment on the problem that Scott and others at Apple must wrestle with: namely how much information should the public be given on the internal workings of Apple's system software and it's potential vulnerability to viral attack.-Ed]

User Interface

The program sends feedback to the screen and to a report file, but first the user has to select the report filename with a SFPutFile (Save As ) dialog. If AppleTalk is active, pressing Cancel will cause it to Quit; if it’s not active, pressing Cancel will cause it to stream the text to a direct-connect ImageWriter on the printer port. (There’s a bug in TML II that causes WriteLn output to the ImageWriter not to work, but it’ll be fixed soon. In the meantime, the user can send the report to a file or press Cancel to avoid creating a report file.) After selecting the report file, the user is presented with the Main Dialog.

Main Dialog’s “Scope of Work” Buttons

The user may want to patrol only a few files, or all files in a folder, or all files on several disks bought in a store. Maybe the user doesn’t have any particular files in mind, but wants to see whether or not any file on a partitioned hard disk is infected. The program cannot know in advance the scope of work to be done, so it asks the user in its Main Dialog.

Directories: The program puts up an SFGetFile (Open ) dialog to allow the user to select a file. The program patrols all files under the directory that contains the selected file. Then, under HFS, it recursively patrols all subdirectories. Then it returns to the SFGetFile dialog. This process repeats until the user presses the Cancel button.

Directory: Same as Directories, except that it doesn’t patrol subdirectories.

Everything: The program loops thru all currently mounted volumes and patrols Directories (plural) on each one starting from the root directory.

Files: The program puts up an SFGetFile dialog to allow the user to select a file, patrols only that file and returns to the SFGetFile dialog. This process repeats until the user presses the Cancel button.

Quit: The program prints file and resource totals across all patrols before returning to the Finder.

User Interface: Options

The Main Dialog also has options the user (usually to get the user’s attention) and for the programmer (usually to help ease the non-trivial programming burden). Both kinds are controlled by check boxes.

Await Keypress: When errors are reported, the program pauses until the user presses a key.

Beep: The program beeps at you when suspicious conditions occur. Complaints get 1 beep, errors get 2 beeps, viral infections get 3 beeps and aborts get 4 beeps. (Most aborts are caused by programming errors, but they could also signify that the program is infected.)

Fingerprint: The program lists the fingerprints of all executable resources it knows about, except CODEs.

Fingerprint CODEs: The program lists the fingerprints of CODE resources too, which generates a lot of output.

Long Listing: The program lists all filenames to its report file, not just those that encountered errors.

Remove Viruses: This is a last resort option for users to recover applications whose backups and masters are lost or unreadable. There are 3 major reasons why you might want to direct users not to use it: concern as to your legal liability if it doesn’t remove the virus properly (or damages uninfected applications), a desire to encourage the safer method (restoring from backups) so as to limit future headaches, and the possibility of using the infected disk as evidence in a criminal or civil prosecution (hard to do if you removed the virus as soon as you saw it).

Trace: The program traces its flow. You may want to turn on Await Keypress to step thru it slowly.

Program Design

SecurityPatrol was dually developed under MDS-compatible TML Pascal I, version 2.5 (“TML 2.5”) and MPW-compatible TML Pascal II, version 1.0.2 (“TML II”). Both versions are on the source code disk, with TML 2.5 filenames ending in “.Pas” and TMLII filenames ending in “.p”. The TML II files are the ones that appear in this article.

Major differences: The TML 2.5 version is roughly 50K and 3% faster, can output to a direct-connect ImageWriter on the printer port and types the report file as an MDS Edit document. The TML II version is roughly 60K, is much easier to develop in, has a larger text I/O window on larger monitors and types the report file as an MPW document.

The program is broken up into 4 major modules: a main program called SecurityPatrol and 3 UNITs called Globals, MainDlog and Patrol. The TML 2.5 versions of these 4 are 99% source code compatible; they differ only in the USES statement. In addition, the Globals UNIT of both versions include the “Fingerprint.ipas” file by means of the {$I} directive; that is, because it’s 100% source code compatible, it’s shared. The CodeSizeLimits UNIT is incompatible; there are different versions for TML 2.5 and TML II. Finally, there are BitProcs and PasLibIntf, which are used only in the TML 2.5 version to maintain source code compatibility with TML II.

The files were named such that, under TML 2.5, dependency relationships are properly observed if you simply compile them in alphabetical order. Under TML II, you can let the TML Project Manager and MPW Make facility handle that for you. (I edit the SecurityPatrol.Proj file to recompile Globals.p if Fingerprint.ipas has changed, and to give the Fingerprint and Globals CODE resources the preload and locked attributes.)

Within each module, the procedures are arranged alphabetically for easy lookup. Since they aren’t necessarily called in alphabetical order, some routines have to be declared FORWARD.

As far as conversion to other Pascals is concerned, the following may help: The OUTPUT in the PROGRAM header is a TML 2.5 convention to tell the compiler that the program will use WriteLn, etc. TML also defines a Text file variable called OUTPUT that’s implicitly used by all screen output WriteLn’s, etc, and PasLibIntf procedures. INC and DEC are built-in functions to increment and decrement a variable by one. (Both are much faster than an assignment such as x := x + 1 because they generate the ADDQ and SUBQ instructions.) That’s all I can think of for now.

Program Flow

The MainDlog UNIT generates and maintains the Main Dialog display, keyboard equivalents and option check boxes internally. It returns to the main program when a Scope of Work button is pushed.

The SecurityPatrol main program manages program initialization, termination, text I/O and high-level interface between MainDlog and Patrol: Depending on the Scope of Work button pushed, it passes off to the proper routine of Patrol.

The Patrol UNIT contains service routines to scan volumes and/or directories. (It’s been programmed to work under both MFS and HFS, but it hasn’t been tested under the 64K ROMs.) Its heart and soul is the recursive procedure PatrolDir, which, at the appropriate times, calls 5 routines in Globals: DirectoryBegins, DirectoryEnds, PatrolBegins, PatrolEnds and ProcessFile. Another routine in Patrol, called PatrolFiles (which patrols files manually, a file at a time), also calls those same routines in Globals.

Globals is the largest UNIT and contains all the basic service routines. The first 4 routines just mentioned are currently given only trivial feedback duties at present. The 5th one (ProcessFile) is where all the action starts. If the file being processed contains CODE resources, ProcessFile calls ProcessCodes, which calls LookForKnownViruses and then looks for anomalies in the CODE 0 jump table. Then, for every executable resource type it knows about, ProcessFile calls ProcessRsrcs with the address of a routine to process that resource type. LookForKnownViruses and ProcessRsrcs call routines in Fingerprint to do resource identification. If the Fingerprint routines say that a resource is infected, LookForKnownViruses calls a “Disinfected” routine that knows how to restore the CODE 0 jump table before removing the infected resource(s); ProcessRsrcs calls RemovedRsrc directly to remove it.

Fingerprint is where the “code database” of good (trusted) and bad resource identification resides. A resource’s fingerprint is a set of any tests you desire. In this article, 3 tests are used as the fingerprint; on the MacTutor source code disk(s), 7 tests. As new viruses are created, Fingerprint is where you would insert new code to detect them.

Commentary on “CodeSizeLimits.p”

CodeSizeLimits is used by PreprocessSelf in the main program, below. The idea behind it is to prevent a virus from creating a new CODE resource or imbedding itself in one of the existing CODE resources.

Since TML II uses the standard MPW Link tool, which creates 9 CODE resources, the gSizeLimit array is 0..8 and gMaxCode is 8. The size limits for CODEs 1 thru 3 contain a little room for expansion, but not enough for even the tiny nVIR virus to fit into. When you have the program the way you want it, you should set these values to exactly the same size as the CODE resources, so there won’t be any room for a virus to sneak into. I have never seen CODEs 4 thru 8 be any size other than exactly those shown, so I feel comfortable giving them no room for expansion. Unless you add, delete or move subroutines, gJTSize will always equal 1240, which represents 155 entries in the jump table. Under TML II, the main program’s main procedure cannot be referenced as if it were an external subroutine, because MPW Link doesn’t give us a symbolic name for it that we can reference. Tom Leonard suggested defining a subroutine just prior to the main program’s main procedure and adding a value to skip over its contents. The value turned out to be $1A.

Commentary on “Fingerprint.ipas”

FgPr: Fingerprinting begins with a call to InitFgPr and then proceeds by (possibly multiple) calls to FgPr. This allows a data fork to be fingerprinted even when it’s larger than the I/O buffer. Calling TML’s built-in INC procedure twice is faster than the generic Pascal “sWordPtr := sWordPtr + 2;”, but not quite as fast as the new, equivalent TML II syntax: “INC(LONGINT(sWordPtr),2);”.

As mentioned above, a resource’s fingerprint is a set of tests, any tests you desire. (The only significant restriction is that the test value is a LONGINT.) To fit within MacTutor column width restrictions, the version printed in this article uses only size, checksum, and checkxor. The version on the source code disk(s) uses those 3, plus the number of odd, negative, and positive words, and an alternating checksum/checkdiff. You shouldn’t use only the 3 tests in this article, because it’s possible to reverse-engineer an evil resource to have the same size, checksum and checkxor as a good one. The 7 tests used on the source code disk(s) make it a virtual certainty that, if two resources have the same fingerprint, they are bit-for-bit identical.

If you want to invest your energies into modifying this program, it wouldn’t be very fruitful to devise exotic new fingerprint tests. If you did manage to implement CRC-16, for example, and someone discovered a new virus, how would you find out what its CRC-16 value is?? If no one else has your test, you would need an actual copy of the virus to find out! (Scary!) It’s better that there should be just a few common tests, not too computationally intense, the union of which approaches certainty. It would be more fruitful if people devoted their efforts to searching more hiding places. In terms of the “security patrol” analogy, what we need are more guards on patrol, searching in more crevices, not more brands of flashlights.

Good and Evil: Any resource or data fork whose “known” flag is not set to TRUE will be reported as unknown. The Good and Evil routines set this flag. Calls to these routines form the “code database”, which presents a tabular appearance in the routines that follow.

Don’t be misled by the name “Good”. I chose that as its name because it’s a natural antonym of Evil and also 4 letters long. It obviously can’t know whether or not the original code from the software manufacturer contains malicious code, so it’s not known to be “good” in that sense of the term. In the context of what we’re doing, it actually means “trusted”.

To add the fingerprints of new trusted resources, just run the program against clean disks (masters) with the report being saved to a text file. After quitting the program, open the report file and the Fingerprint.ipas file. The MPW editor allows you to select everything within a parenthesis pair by double-clicking on the left or right parenthesis of the pair. Using that technique, it’s very easy to cut and paste back and forth previously unknown resource fingerprints from the report file to new “IF Good( ) THEN EXIT;” statements in the Fingerprint.ipas file.

SecurityPatrol runs noticeably faster if the fingerprint tests are done in reverse likelihood order. As far as which resources you mark as trusted and the order you test them is concerned, you have to consider what System levels the program will be run under, not just the level you’re using for development. On the source code disk(s), I have included major resource fingerprints for Systems 4.2, 4.1 and 3.2.

KnownDataFork: Because there isn’t a TRsrcRec associated with the data fork, this routine uses special globals, gKnown and gInfected. The commentary on Globals’ ProcessFile, below, discusses what kinds of files have their data forks fingerprinted.

LookForVirus_nVIR: Did you know this virus exists in 2 versions? The May 1988 issue of MacTutor described a 372 byte long version, but when I asked Howard Upchurch to send me a copy of nVIR, the one he sent was 422 bytes long. That was the strain that got onto the CD-ROM disk. Later I picked up the 372 byte strain quite by accident at Kinko’s. (I notified the Washington DC area Kinko’s about this, incidentally.)

LookForVirus_Scores: The first 12 bytes of the Scores virus CODE resource is used to hold the segment and jump table information, which vary according to which application it’s infecting; therefore, we skip those 12 bytes in the fingerprinting process.

“Generic” Process_xxx’s: To shorten this article, only Process_ADBS is shown as an example of generic Process_xxx routines, which include ADBS CACH, CDEF, CODE, DRVR, DSAT, FKEY, FMTR, LDEF, MBDF, MDEF, MMAP, NBPC, PACK, PDEF, PTCH, ROv#, ROvr, SERD, WDEF, XCMD, XFCN, boot, cdev, mppc, snth and view (if it contains methods). The versions on the source code disk(s) include all of them except CODE, which would’ve been more work than I could do before the MacTutor publication deadline. Generic routines all call ProcessCurrRsrc to fingerprint the resource over its entire length and then test only for trusted (“Good”) resources of that type.

Special Case Process_xxx’s: Routines that don’t fingerprint over the entire length of the resource, or that also test for known viral (“Evil”) resources, are published in this article.

Process_DATA: Most innocuous DATA resources contain volatile data, so we default to the known flag to TRUE. The Scores DATA resource is simply a copy of the Scores CODE resource, so we also have to skip its first 12 bytes (see LookForVirus_Scores, above).

Process_INIT: My suspicion that the Scores INITs might also store data internally was confirmed when different copies of the same INITs yielded different fingerprints. By analyzing raw memory dumps, I was able to determine that they skipped over variable parts by means of the hardware instruction BRA We must also skip over these parts, or else our fingerprints will vary and we won’t be able to recognize the INITs.

The test for $60 is to detect the beginning of a BRA instruction. If the next byte is non-zero, it was a BRA.S, and we have to skip over the number of bytes in that non-zero byte. If the next byte is zero, we have to skip over the number of bytes in the extension word that follows the zero byte.

To complicate the task of defense, future viruses may use other instructions to skip over variable data. Variable parts may be imbedded at different places in the resource, perhaps even in a variable way. If so, we’ll just have to write tests for those situations too. Even if the virus succeeds at making the fingerprint method too complicated, we will always find, thru detailed analysis, other identifying characteristics that we can test for. The advantage of bending the fingerprint technique in this case is the relative certainty of identification, which counter-balances the dangers inherent in deleting resources based on appearance. Yes, I agree, all of this is very tedious, but the alternative is vulnerability.

Commentary on “Globals.p”

CONSTs: To look up the low memory global values in Inside Macintosh, omit the initial “k”. The kIOBufferSize constant is used primarily by the KnownDataFork routine in Fingerprint.ipas. Under TML II, it’s possible to use conditional compilation directives instead of the kProcessSelf constant, but the result wouldn’t be TML 2.5 compatible (violating one of my goals). The kRsrc constants are arbitrary values used by InitRsrc, etc, below, to detect failure to call other Rsrc routines. This allows the program to display a complaint exactly diagnosing the problem, rather than going haywire by processing garbage. The values’ only significance is that they’re unlikely to occur randomly in memory. Just about any value other than 0 or -1 will do.

TYPEs: TCounts is used to keep counts in a consistent way; it’s used mainly by the ListCounts routine, below. TFeedback is used to keep track of what was written to the screen and report file. JTE means “Jump Table Entry”. TLoaded, TResIdOrIndex, TMainItem and TMainOpt are enumerations to make the code more readable; the TMain’s give Main Dialog items descriptive names that can also be used in indexing. TRsrcRec is used to keep track of everything about a resource; I didn’t want to undertake removing infected resources unless I kept a lot of information about them and handled them consistently (see InitRsrc, below). The TScores stuff is to get the JTE in bytes 4-11 (starting from 0) of the Scores CODE resource. TWordPtr is used just about everywhere.

VARs: gAbortPatrol and the gEvt stuff are used in small event loops in AbortPatrolIfCmdPeriodPressed and AwaitKeypress. gCode0 is used during PreprocessSelf in the main program and whenever the current file of the patrol contains CODE resources. gCounts and gTotals are used to keep counts within a patrol and overall, respectively. The gCurr variables track the current file, resource, etc of the patrol; most of them are passed to Globals by Patrol so it can know where we are in the patrol, but some are set within Globals. gDisabled and gOption are used to communicate with the user thru the Main Dialog; if gDisabled is set, the corresponding option cannot be changed by the user. gDlogPtr is used to keep track of the DialogPtr while the Main Dialog is hidden (usually during a patrol). gGrafPtr is used to save and restore TML Pascal’s “plain vanilla”/“Textbook” text window while the MainDlog is on the screen; to save processing, it’s saved only once, in InitGlobals. gInd is used for consistent indention. gSecsBegins and gSecsEnds are used in PatrolBegins and PatrolEnds to time the wall-clock duration of a patrol. gSFGetPt and gSFPutPt are used to center the SFGetFile and SFPutFile in the middle of the screen.

CallProcPtr: ProcessFile calls ProcessRsrcs with the address of the routine (a ProcPtr) to process each resource type; ProcessRsrcs has to use this INLINE to call that routine.

AbortPatrolIfCmdPeriodPressed: The major difference between this routine and the one that follows is the fact that AwaitKeypress stops the patrol until a key is pressed, while AbortPatrolIfCmdPeriodPressed checks for command-period on the fly. Both set the gAbortPatrol flag if command-period was pressed.

AwaitKeypress: This pauses the program (like REPEAT UNTIL KEYPRESS, which exists under TML 2.5 but not under TML II). In addition, it detects user request for cancellation with command-period.

Comment routines and Error routines: Error routines indent 2 deep (using gInd), comments 3. Error routines also have significance as far as beeping and awaiting are concerned.

DirectoryBegins: If there’s something you want to do only once for a volume, such as check its boot blocks, this is a good place to do it, subordinate to an “IF (gCurrDirId = 2) THEN”. (If gCurrDirId is 2, you’re at the root directory of the volume.)

FixedCode0: The way a virus jumps back into applications gives us clues as to how we might repair them. So far, we’ve been lucky to have JTEs lying around to repair them with (using this routine). In the future, who knows? Unfortunately, there’s no shortcut that doesn’t involve disassembly to find out how to restore an application. If a new virus comes along, and if you don’t do disassembly, but you want to code your own restoration code immediately, be sure to find out how to restore an application correctly from someone who has disassembled it. [Fake jump table entries is something we all should be concerned about. -Ed]

InitGlobals: This routine initializes the VARs defined above. Note that zeroing out a BOOLEAN sets it to FALSE and zeroing out a string sets it to the null string. The old way of centering a window or dialog was to use “screenBits.bounds”, but that was from the days when everyone had only 1 screen. Since gGrafPtr contains the “plain vanilla”/“Textbook” window’s GrafPtr, using gGrafPtr^.portBits.bounds will get the screen size of whatever screen it’s in.

InitRsrc, GetRsrc, ReleaseRsrc and RemovedRsrc: These routines are a higher level interface to the Resource Manager. Their use cycle is InitRsrc, then GetRsrc, then, when you’re done with it, ReleaseRsrc or RemovedRsrc. You may then return in a tighter circle to GetRsrc again. If you try to use these routines in any order other than this, they will report an error and abort the program. Also, if you want to see the program crash unpredictably, try releasing resources when processing Active Self and Active System.

JTEIsValid: TML 2.5 generates a longword compare with sign extension if you use $A9F0, which doesn’t compare correctly with the constant because it’s not sign extended, hence the -22032.

LookForKnownViruses: This routine calls itself recursively during virus CODE removal because of the possibility of multiple infections. For example, an application could have CODEs 0 (jump table), 1 (application), 3 (Scores), 256 (nVIR) and 258 (Scores). As long as some infections are still being found and removed, I see no reason why the process should stop.

ProcessCodes: Sometimes an application contains a CODE 256, but isn’t infected by nVIR. An example is PackIt. Note that PackIt also skips a CODE ResId, as an application infected with Scores does. Which CODE do you check to see if it’s infected with Scores? Also, I’m told that MacMoney has a CODE resource that’s exactly 7026 bytes long, like Scores, and that LaserSpeed installs atpl and DATA resources into your System file, like Scores.

These are the sorts of situations that made me start using fingerprinting in the first place, but still you have the problem of determining whether or not the application is actually infected. If a viral CODE resource is present but not linked-to by the jump table, then CODE 0 is not damaged, and you don’t want to try to repair it! That would damage it! ProcessCodes calls the LookFor routines using the CODE resource pointed to by the first JTE. This greatly simplifies these otherwise confusing situations.

ProcessCodes also looks for irregularities in the jump table that might signal tampering. It warns the user if the jump table doesn’t start with the first CODE resource, or if it ever descends (that could signal that an earlier resource was a new one added by a virus), or if it ever skips a resource ID as it ascends. (If the CODE resource pointed-to by the JTE stays the same, it avoids the test to see if JTEIsValid, which saves a considerable amount of I/O.) All applications infected by nVIR and/or Scores violate all 3 of these conditions.

Unfortunately, applications generated by Lightspeed often have jump tables that violate 2 or more of these conditions. I don’t have Lightspeed, so I couldn’t come up with an elegant way to deal with that. I’m open to suggestions. The program’s inelegant solution is to have an exception list of applications whose jump tables are allowed to jump around all over the place. (The exception list uses the beginning of the application name in case there’s a version number appended.) It’s inelegant because they lose the benefit of the jump table irregularities search. They could get infected by some future virus and this program wouldn’t detect anything suspicious in their jump tables.

ProcessFile: At present, the program fingerprints the data forks of only MacsBug and compiler object files. The latter is to guard against the possibility of a virus that doesn’t go away even after recompilation, because its original infection source is an altered object file. RELBs are MDS-compatible “.Rel” files and OBJs are MPW-compatible “.o” files.

Also, at one time ROM patches were in the data fork of the System file. Now that there are PTCH resources, this may have been eliminated. If anyone knows whether or not the System file data fork can still contain executable code, please let everyone know by telling MacTutor. The same goes for executable code in any other, lesser-known file type’s data fork, of course. [Apple's system file data fork does indeed include executable code so this is another "danger" point in the system design that should be fingerprinted. -Ed]

A significant amount of code is to assure that we don’t open and close the Active Self and Active System files. If you try treating them like any other file, the program will probably crash.

The Note Pad File’s data is in its data fork, and the Scrapbook File’s data is in its resource fork. The Scores virus adds INIT resources and alters their Type and Creator to conscript them for use with the INIT 31 mechanism documented in IM IV-256. But it doesn’t throw away their contents. (While infected, you’re still able to use both DAs, because they continue to access those files by name.) That’s why the program doesn’t delete a file if either fork contains anything at all. The same deletion code will throw away the bogus “Desktop” and Scores files if it succeeds at removing all their infectious resources, incidentally.

Finally, you may ask, why don’t we search for and destroy nVIR’s “nVIR” resources? Well, earlier (during LookForKnownViruses) it may have detected an nVIR CODE resource but, for some reason, was unable to restore the application. So far, this hasn’t ever happened, but if it ever did, the program would not have gotten to the code that removes the nVIR resources. One of those, nVIR 2, contains the JTE necessary to fix CODE 0. Because “nVIR” resources are not in themselves infectious, and because empty ones can be used to inoculate against nVIR (see the May 1988 MacTutor), you don’t want to delete nVIR resources willy-nilly. The only time it’s really safe to do so is after successfully restoring an infected application’s CODE 0. This is true in only one place (at the end of Disinfected_nVIR).

ProcessRsrcs: Routines in Fingerprint.ipas decide which resources are infected. ProcessRsrcs takes that determination on faith and removes the resource if gOption[eRmVir] is on. (This is used as a wholesale resource type remover at the end of Disinfected_nVIR.)

Also, notice that we don’t bump the index if a resource is removed. That’s because the resource following it now assumes the removed resource’s position in numerical order as far as Get1IndResource is concerned. This same consideration applies when a file is deleted. (See Patrol’s CallProcessFile discussion of gCurrFileDeleted, below.)

ShortHexDump: This doesn’t look right, but it is. Even though ‘0’ is $30 and ‘A’ is $40, you have to add $37 to get from 10 to ‘A’.

Commentary on “MainDlog.p”

In order for SecurityPatrol to have no resource types except CODE, we have to build the Main Dialog’s DITL in memory. It’s not hard to do, keeps our private globals in synch with the dialog and is actually a lot of fun. Really.

VARs: The gHdl array and gDBtnRect are used to keep from having to call GetDItem all the time in MainDlogWorkRequested and FrameDefaultBtn, respectively. gDBtnRect is a Rect inset by 4 bits to the outside of the default button’s Rect.

ChkChk: This sets the check boxes to the values represented by the gOption BOOLEANs. Note that the ORD of FALSE is 0 if and the ORD of TRUE is 1, a fact that’s also used in building the DITL in InitMainDlog (to keep the Dialog Items word-aligned).

FrameDefaultBtn: This routine frames the default button in accordance with the User Interface Guidelines chapter of Inside Macintosh.

InitMainDlog: This is the routine builds the DITL, calls NewDialog and sets up the private globals to speed up MainDlogWorkRequested. The sRect and sTitle local variable arrays are used to build the DITL with a FOR loop; this seems wordy, but actually uses fewer lines than doing them linearly, and is much more readable and maintainable.

KybdEquivsFilter: This routine is a filterProc to give every Main Dialog button and check box a keyboard equivalent.

MainDlogWorkRequested: This is the routine that actually manages the Main Dialog user interface. On entry, the dialog will always be hidden (InitMainDlog calls NewDialog with the “visible” parameter FALSE, and previous calls to will have hidden it before exiting. BringToFront is called before ShowWindow to keep the BringToFront from occurring visibly on screen and annoying the user; it doesn’t hurt anything to have an invisible window momentarily the front window. The WHILE loop manages check boxes to free up the main program from having to worry about those details. The DITL was built with check boxes after buttons, so the WHILE loop will terminate when a Scope Of Work button is pushed. It then returns a TMainItem enumeration value (defined in Globals) to tell the main program which scope was requested, so that it’ll know which Patrol routine to call.

Commentary on “Patrol.p”

Patrol is based on code examples that have already been published elsewhere, notably Tech Notes 24, 66, 68, 69 and 77, Inside Macintosh and earlier issues of MacTutor. It adds 2 new features: floating a Working Directory with the scan and keeping files in a directory together. Floating a Working Directory is to avoid overflowing 255 characters of partial pathname if the directories are deeply nested. (A virus creator might deliberately hide a virus deep in a series of nested folders to rely on other anti-virus utilities’ possible reliance on full pathnames. Such a virus could very easily be launched by a document at the root level.) I’ve tested the floating Working Directory feature to 52 levels deep of nested folders, each with 31-character-long folder names and different filenames in each folder. As for keeping files in a directory together, previous directory scan algorithms were set up to recursively scan subdirectories right in the middle of the files being scanned, so you would see files from the parent directory, then files from a child directory, then more files from the parent directory, etc. Patrol doesn’t go to subdirectories until it has patrolled all files in the current directory. This also minimizes opening and closing Working Directories.

kPatsInitd: This is an arbitrary value used by Patrol routines to detect failure to call InitPatrols. This allows the program to display a complaint exactly diagnosing the problem, rather than going haywire by processing garbage. The value’s only significance is that it’s unlikely to occur randomly in memory. Just about any value other than 0 or -1 will do.

TOverlappingPBs: For reasons unknown, Apple chose to define a wide variety of parameter blocks with mostly-the-same/sometimes-different field names. For the limited uses in Patrol, the only major difference between HParamBlockRec in CInfoPBRec is whether or not you’re calling PBGetCatInfo. Rather than continually moving data around to keep their fields in synch, this TYPE allows defining PBs with which both types’ field names can be used. This has been okay (so far) because the only PB values that Patrol uses happen to match up between the 2 types. Occasionally it can be a bit confusing, but it keeps field name usage usually correct.

VARs (private): gAppDirId and gAppVRefNum are used to detect “Active Self”. gInitdFlag is used with kPatsInitd (see above) to detect failure to call InitPatrols. gOnly1Deep is used in PatrolDir to avoid processing subdirectories when the Scope Of Work is Directory (singular). gOrigWDRefNum is used to remember the wdRefNum of the first directory of the patrol (see FloatWDDeeper and FloatWDShallower, below). gPBs is used to do the Low Level File Manager calls to do the patrol itself. gSFLst is passed to the SFGetFile dialogs in PatrolDirectories and PatrolFiles. gSysDirId and gSysVRefNum are used to detect “Active System”. gWDPBRec is used by FloatWDDeeper and FloatWDShallower to open and close working directories.

BuildDirname: This routine is called only when the current directory being patrolled changes. It uses a local variable of type TOverlappingPBs (see above) so as not to disturb the values in gPBs, which is still being used by the patrol. Starting from the new current directory (as pointed to by gCurrWDRefNum), it works backwards to the root directory of the volume; for example, if the new current directory is “A:B:C:”, it would build “C:”, then “B:C:”, then “A:B:C:”. ioFDirIndex is set to -1 because we’re getting info about directories. (How to set ioFDirIndex is documented in Tech Note #69.) Initializing the loop by setting ioDrParID to 0 has no direct effect on the PBGetCatInfo call, because that’s a field that’s returned by the File Manager. Instead, it’s being used in combination with “ioDrDirID := ioDrParID;” to set ioDrDirID to 0, which does affect the PBGetCatInfo call. (It will start the loop at the directory pointed-to by ioVRefNum, not some other directory on the same volume.) This may seem like a roundabout way to start getting folder names at gCurrWDRefNum, but the same line (“ioDrDirID := ioDrParID;”) continues the loop upward thru the folder hierarchy, so it’s actually pretty straightforward. The loop proceeds until ioDrDirID is 2, signifying the root directory of the volume. (Note that this works correctly even if gCurrWDRefNum points to a root directory, since the File Manager also returns ioDrDirID; the loop will have exactly one pass, beginning with ioDrDirID equal to 0 and ending with it equal to 2.) The routine simply terminates if an error occurs, such as gCurrDirname overflow. This is relatively acceptable because the program never uses the pathname to access a file during a patrol; instead, it uses gCurrWDRefNum and gCurrFilename to access the file, and treats gCurrDirname as commentary for the user. InitSecurityPatrol in the main program uses gCurrDirname in the ReWrite that creates the report file. This is unfortunately necessary because there isn’t a parameter for a wdRefNum in the ReWrite statement. On the bright side, the user is unlikely to place the report file more that 255 characters of full pathname deep.

CallProcessFile: Patrol centralizes all calls to ProcessFile via this routine to make absolutely sure that gActiveSelf and gActiveSys get set properly and that gCurrFileDeleted gets reset to FALSE. Correctly setting gActiveSelf and gActiveSys every time is vital to keep Security’s ProcessFile routine from closing their resource forks. Resetting gCurrFileDeleted to FALSE every time is vital to prevent skipping the file following the one deleted, which would assume the numerical position of the deleted file within the patrol (pointed to by ioFDirIndex).

FloatWDDeeper and FloatWDShallower (similarities): The problem with using pathnames to access files is the fact that you have only 255 characters to name it with. Since filenames and folder names can be up to 31 characters long, it’s possible to overflow that limit with folders nested only 8 deep.. Patrol gets past this problem by “floating” a working directory. As a general rule, both of them close the working directory pointed-to by gCurrWDRefNum, use a DirId to open another working directory, and put the new wdRefNum into gCurrWDRefNum. The difference is what the newly created working directory represents.

FloatWDDeeper: This routine creates a working directory to move from a parent directory to a subdirectory using the DirId encountered in the patrol (namely, gPBs.fCPBRec.ioDrDirID).

FloatWDShallower: This routine creates a working directory to return from a subdirectory to its parent. To do this, FloatWDShallower could use an ioDrParId returned by PBGetCatInfo (see BuildDirname, above), but there’s a much simpler method. PatrolDir calls itself recursively, so when it returns from patrolling a subdirectory, its pDrDirId parameter automatically reverts to the DirId of the parent. FloatWDShallower uses this more readily available value.

FloatWDDeeper and FloatWDShallower (exception): The only exception to the rule relates to the start directory of the patrol. If it’s the root directory, PatrolDir starts out with a true vRefNum, not a wdRefNum. This is documented in Tech Note #77, which seemed to be warning us never to pass a vRefNum to PBCloseWD. (It didn’t say so explicitly, but I haven’t tried it to see what would happen.) To get around this problem, FloatWDDeeper makes an exception at the start of the floats (leaving the start directory open) and FloatWDShallower makes an exception at the end (restoring the start directory with gOrigWDRefNum).

GetActualDirId: This routine assures that gCurrDirId will always be accurate, even when PatrolDir is called with pDrDirId equal to 0. Even though Globals doesn’t currently use gCurrDirId, Patrol uses it to detect the Active Self and Active System.

InitPatrols: This routine makes sure that all of Patrol’s globals are properly initialized. The ZeroFillRange calls implicitly set all BOOLEAN variables to FALSE, all strings to the null string and all ioCompletion fields to NIL. Using kFSFCBLen to see if HFS is active is documented in Tech Note #66. Calling GetVRefNum to get gSysVRefNum is documented in Tech Note #77. Calling PBHGetVInfo and referencing ioVFndrInfo[1] to get gSysDirId is documented in Tech Note #67. Referencing kBootDrive instead if HFS is not active is also in #77.

PatrolDir: This routine is the heart and soul of Patrol. Its techniques are documented in Tech Note #69. If you understand that, you’ll understand this, but PatrolDir’s way of doing things is considerably different. It patrols files first (ignoring subdirectories), then patrols subdirectories (ignoring files). Patrolling files uses PBGetFInfo (note: not PBHGetFInfo). Patrolling subdirectories uses PBGetCatInfo. There are three major advantages of doing it that way:

(1) filenames appear in an unbroken list,

(2) the program can work even if HFS isn’t active,

(3) it’s easier to patrol only 1 directory deep.

There are other differences from Tech Note #69: It calls FloatWDDeeper, FloatWDShallower and GetActualDirId, for reasons discussed above. Also, if gError <> NoErr after a recursive call, it backs out of the recursion rapidly until it reaches the start directory of the patrol; this saves a lot of unnecessary calls to FloatWDShallower, since doing only the last one has the same effect as doing them all.

PatrolFiles: I’ve gone to considerable trouble here to correctly call DirectoryEnds and DirectoryBegins whenever the user selects a file in a different directory from the previously selected file. That’s the purpose of the imbedded procedure CallPrevDirEnd. If you don’t go to that trouble, the TFeedback variables in Globals won’t get reset properly and the new directory name won’t get displayed on the screen nor in the report file.

Commentary on “SecurityPatrol.p”

SecurityPatrol uses standard Pascal text I/O to report its findings and progress to the user. (In TML terminology, the TML 2.5 version is called a “plain vanilla application” and the TML II version is called a “Textbook application”.) The main program also manages initialization, termination and high-level interface between MainDlog and Patrol.

Because there’s no event loop, the program doesn’t support DAs (on-purpose). It also specifically disables FKEYs. The only form of concurrent code that it doesn’t disallow is MultiFinder, but you shouldn’t run it under MultiFinder anyway, because you won’t be able to examine open files, such as the DeskTop file and the concurrent applications themselves (the Finder, eg).

There’s a very good reason why I don’t like the idea of concurrently executing code. Let me give you a hypothetical scenario: Suppose someone imbedded a virus in a WDEF and installed that WDEF into the DeskTop file of a disk. I don’t know how the Finder does things, but suppose it allows a runtime override of definition procedures using the standard Resource Manager precedence (document --> application --> System file). You’re patrolling files in foreground under MultiFinder, you get an SFGetFile dialog to patrol Directories, and you insert the infected disk. In background, the Finder reads the DeskTop file of the disk and display’s the disk’s window behind the SecurityPatrol window using the override WDEF. You’ve just been infected, and SecurityPatrol hasn’t even had a chance to look at the disk yet. My guess is that the published version of SecurityPatrol wouldn’t find anything on that disk. Whether or not your version would is up to you.

Of course, that scenario relies on a presupposition that the Finder allows a runtime override of WDEFs, which it probably doesn’t do. On the other hand, I don’t know for sure that it doesn’t, and I don’t like the uncontrolled variable it represents. Even if the Finder wouldn’t let a virus thru this way, maybe some other concurrent application or DA would. I didn’t go extraordinarily out of my way to prevent all concurrent code (VBLs, AppleTalk, etc), but I suspect there’s a security hole there somewhere. I’m voicing my concern here to focus more minds than my own on the subject. [Apple has also thought of this problem and is working on it presently. They just don't want the wrong kind of minds to be focused on it, if you know what I mean! -Ed]

“(OUTPUT)”: The presence of this phrase in the program header tells TML 2.5 that this is a “plain vanilla” application. Before the main procedure starts to execute, the runtime library will initialize all the managers it needs for text I/O, create the WriteLn window and put “TML Pascal” into its drag bar title. Because it initializes the managers, we don’t. It’s ignored by TML II.

kPreprocessSelf and kAwaitVerification: Having kPreprocessSelf turned on can get to be a real nuisance during development, but it’s an important safeguard and better than processing Active Self during the patrols. You may want to turn it off during development and back on again in the final cleanup before release to your friends and/or company’s users. The code controlled by kAwaitVerification will be cleaned up in Version 2.0: All of SecurityPatrol’s own CODE resources except the one containing Fingerprint.ipas should be fingerprinted and tested in Fingerprint.ipas. The verification task presented to the user could then be made much simpler.

ScrDmpEnbPtr and ScrDmpEnbSave: These are used in InitSecurityPatrol and ExitSecurityPatrol to save, disable and restore FKEYs.

{$Z*}: A TML 2.5 to TML II upgrade issue not mentioned in the TML II manual’s Appendix F is the fact that main program’s routine names, when referenced from a UNIT, will not be found at Link time unless you explicitly define them externally with this directive. It’s documented in Appendix C, but not as a difference.

InitSecurityPatrol: Textbook is a TML II routine in PasLibIntf, distributed by TML Systems with the compiler. It initializes all the managers it needs for text I/O, creates the WriteLn window and puts the application’s name into its drag bar title. Without it TML II would WriteLn all over the desktop. Because it initializes the managers, we don’t. For TML 2.5 users, a dummy version of Textbook is defined in the PasLibIntf.Pas UNIT on the source code disk(s).

The initial values of the Main Dialog check boxes are set between the calls to InitGlobals and InitMainDlog.

When AppleTalk is not active, the statement “ReWrite(o,’PRINTER:’)” is used to send the report file to the printer. Under TML 2.5, this prints to a direct-connect ImageWriter on serial port B in streaming text mode, which is even more draft than draft mode (the ImageWriter doesn’t recognize curly quotes, for example). Under TML II, this feature doesn’t work, and the report file output will be, in effect, thrown away. Tom Leonard is working on the problem. In the meantime, you can save the report to a text file and print it later with MPW, which has the distinct advantage of also being able to print to a LaserWriter.

TML 2.5 allows TextFace calls to affect the WriteLn output. TML II doesn’t, but it doesn’t hurt.

PreprocessSelf: These are the most stringent tests I could think of to guard against the program’s own infection. You’re invited to add more. If you leave in the line that turns on gOption[eRmVir], the program will disinfect itself of all viruses it knows how to remove. If you take out that line, it will detect known viruses and abort, so you should tell your users always to run it from a write protected disk.

TML 2.5 Link puts a JMP d(PC) instruction at the beginning of CODE 1 that jumps into the actual start address of SecurityPatrol. That’s why it checks for $4EFA and offsets by the extension word that follows. TML II is unaffected by this test.

Wryte routines: These routines manage text I/O. Under TML 2.5, Write, WriteLn, etc, can only appear in the main program, not in UNITs. This limitation was done away with in TML II, but this way of doing things is compatible. Moreover, by centralizing output, it allows calling PLFlush on every WriteLn.

PLFlush is a TML II routine in PasLibIntf. (See “(OUTPUT)”, above.) It flushes a file’s buffer. “PLFlush(OUTPUT);” assures that screen feedback will be current. Alternatively, you could call “PLSetVBuf (OUTPUT, NIL, $40, 256);” in InitSecurityPatrol, but I like this stick-shift level of control, and it seems to work a little better.

TML 2.5 used to print numbers with a default field width of 1. TML II now uses the ANSI-standard Pascal default field width (6, I think). To avoid inconsistent results between the 2 versions of the program, WryteNbr never uses the default field width, but instead specifies field width explicitly.

zzSecurityPatrol: See the end of the commentary on CodeSizeLimits.p, above.

The source code disk for this issue (see the MacTutor Mail Order Store Ad) contains both the TNL 2.5 and TML II versions with the complete system file resource fingerprints for previous system files. We recommend purchase of this disk, only $8!

Continued in next frame
Volume Number:5
Issue Number:2
Column Tag:Advanced Mac'ing

Security Patrol for Viruses (code)

  
{               Copyright Header
©1988 by Steve Seaquist.  All rights reserved.  Used by permission.  
Use at your own risk.  No warranty is expressed or implied. Neither Apple 
Computer nor MacTutor endorse or warrant this program in any way, nor 
are they responsible for its use or mis-use in any way.   
This Macintosh virus-detecting program was originally published and explained 
in the February 1989 issue of MacTutor magazine.  Some aspects of its 
design are important to security, and it uses some unusual techniques, 
so please read the article. }  
“CodeSizeLimits.p”
UNIT  CodeSizeLimits;
INTERFACE
VAR
  gJTSize:            INTEGER;
  gEntryPoint:        LONGINT;
  gSizeLimit:
    ARRAY [0..8] OF   LONGINT;
  gMaxCode:           INTEGER;
PROCEDURE  GetCodeSizeLimits;
IMPLEMENTATION
PROCEDURE  zzSecurityPatrol;        EXTERNAL;
PROCEDURE  GetCodeSizeLimits;
 BEGIN
 gEntryPoint   := ORD4(@zzSecurityPatrol)+$1A;
 gJTSize       := 1240;
 gMaxCode      := 8;
 gSizeLimit[0] := gJTSize + 16;
 gSizeLimit[1] := 15700;
 gSizeLimit[2] := 23900;
 gSizeLimit[3] := 11200;
 gSizeLimit[4] := 00844;
 gSizeLimit[5] := 01908;
 gSizeLimit[6] := 01606;
 gSizeLimit[7] := 01822;
 gSizeLimit[8] := 01312;
 END;
END.

“Fingerprint.ipas”
{ File  Fingerprint.ipas }
VAR
  gFgPr1,gFgPr2,gFgPr3,gFgPr4,gFgPr5,gFgPr6,gFgPr7:  LONGINT;
  PROCEDURE  CommentFgPr;
 BEGIN
 Wryte    (‘:  (‘);
 WryteNbr (gFgPr1,5);
 WryteChar(‘,’);
 WryteNbr (gFgPr2,9);
 WryteChar(‘,’);
 WryteNbr (gFgPr3,6);
 WryteChar(‘,’);
 WryteNbr (gFgPr4,5);
 WryteChar(‘,’);
 WryteNbr (gFgPr5,5);
 WryteChar(‘,’);
 WryteNbr (gFgPr6,5);
 WryteChar(‘,’);
 WryteNbr (gFgPr7,9);
 WryteChar(‘)’);
 END;
PROCEDURE  CommentFgPrData;
BEGIN
ErrorBegins(‘Unknown data fork’);
CommentFgPr;
ErrorEnds(0);
END;
PROCEDURE  CommentFgPrRsrc(pRsrcPtr: TRsrcPtr);
BEGIN
IF  (gFgPrTitle <> ‘’) THEN
  BEGIN
  ErrorMsg(gFgPrTitle,0);
  gFgPrTitle := ‘’;
  END;
CommentRsrcBegins(pRsrcPtr);
CommentFgPr;
WryteEoln;
END;
FUNCTION   Evil        { Edited so that     }
  (p1,p2,p3: LONGINT)  {   calls fit into   }
  :          BOOLEAN;  {   MacTutor column  }
BEGIN                  {   width.  Source   }
Evil := FALSE;         {   code disk        }
IF  (gFgPr1 = p1)      {   version tests    }
AND (gFgPr2 = p2)      {   all 7 gFgPr’s.   }
AND (gFgPr3 = p3) THEN
  WITH gCurrRsrc DO
    BEGIN
    Evil      := TRUE;
    fKnown    := TRUE;
    fInfected := TRUE;
    gInfected := TRUE;
    END;
END;
PROCEDURE  FgPr(pPtr:  Ptr; pSize:  Size);
VAR
  i,sTemp:            LONGINT;
  sWordPtr:     TWordPtr;
  PROCEDURE  FgPrTemp;
  BEGIN
  gFgPr2 := gFgPr2 +    sTemp;
  gFgPr3 := BXor(gFgPr3,sTemp);
  gFgPr4 := gFgPr4 +    ORD(ODD(sTemp));
  gFgPr5 := gFgPr5 +    ORD(sTemp< 0);
  gFgPr6 := gFgPr6 +    ORD(sTemp> 0);
  IF  ODD(i) THEN
    gFgPr7 := gFgPr7 +  sTemp
  ELSE
    gFgPr7 := gFgPr7 -  sTemp;
  END;
BEGIN
sWordPtr := TWordPtr(pPtr);
FOR i := 1 TO (pSize DIV 2) DO
  BEGIN
  sTemp  := ORD4(sWordPtr^);
  FgPrTemp;
  INC(LONGINT(sWordPtr));
  INC(LONGINT(sWordPtr));
  END;
IF  ODD(pSize) THEN
  BEGIN
  sTemp  := BAnd(ORD4(sWordPtr^),$FF00);
  FgPrTemp;
  END;
gFgPr1 := gFgPr1 + pSize;
END;
FUNCTION   Good        { Edited so that     }
  (p1,p2,p3: LONGINT)  {   calls fit into   }
  :          BOOLEAN;  {   MacTutor column  }
BEGIN                  {   width.  Source   }
Good := FALSE;         {   code disk        }
IF  (gFgPr1 = p1)      {   version tests    }
AND (gFgPr2 = p2)      {   all 7 gFgPr’s.   }
AND (gFgPr3 = p3)
AND (gFgPr4 = p4)
AND (gFgPr5 = p5)
AND (gFgPr6 = p6)
AND (gFgPr7 = p7) THEN
  WITH gCurrRsrc DO
    BEGIN
    Good      := TRUE;
    fKnown    := TRUE;
    fInfected := FALSE;
    END;
END;
PROCEDURE  InitFgPr;
BEGIN
gFgPr1   := 0;
gFgPr2   := 0;
gFgPr3   := 0;
gFgPr4   := 0;
gFgPr5   := 0;
gFgPr6   := 0;
gFgPr7   := 0;
END;
FUNCTION   KnownDataFork:  BOOLEAN;
VAR
  sBytesRemaining: LONGINT;
  sBytesThisPass:  LONGINT;
  PROCEDURE  KnownIF
    (p1,p2,p3,p4,p5,p6,p7: LONGINT);
  BEGIN
  IF  (gFgPr1 = p1)
  AND (gFgPr2 = p2)
  AND (gFgPr3 = p3) THEN
    EXIT(KnownDataFork);
  END;
BEGIN
KnownDataFork := TRUE;
InitFgPr;
sBytesRemaining := gCurrEOF;
WHILE sBytesRemaining > 0 DO
  BEGIN
  IF  (sBytesRemaining > kIOBufferSize) THEN
    sBytesThisPass := kIOBufferSize
  ELSE
    sBytesThisPass := sBytesRemaining;
  gError := FSRead(gCurrRefNum,
                   sBytesThisPass,
                   gCurrIOBuffer);
  IF  (gError <> NoErr) THEN
    BEGIN
    ErrorOSErr(‘Couldn’t read data fork’);
    EXIT(KnownDataFork);
    END;    
  FgPr(gCurrIOBuffer,sBytesThisPass);
  sBytesRemaining := 
    sBytesRemaining - sBytesThisPass;
  END;
KnownIF(  512,  1888894, 23566);{DRVRRuntime}
KnownIF(34304,122743292,-30376);{Interface}
KnownIF( 2560,  9884035,-13755);{ObjLib}
KnownIF( 8704, 38905655, -5215);{PerformLib}
KnownIF(22016,115306063, 12985);{Runtime}
KnownIF( 6656, 28028983,-22709);{ToolLibs}
KnownIF( 3072, 12526030,-25732);{HyperXCMD}
KnownIF( 2560, 11236115,-31819);{SANELib}
KnownIF( 4608, 16761354,-16062);{SANELib881}
KnownIF(15872, 62109326,-18394);{TMLPasLib}
KnownIF(27634,204503778, -8384);{MacsBug 5.5}
KnownDataFork := FALSE;
END;
PROCEDURE  LookForVirus_nVIR;
BEGIN
IF  (gCurrRsrc.fResId <> 256) THEN
  EXIT(LookForVirus_nVIR);
gCurrRsrc.fInfected := TRUE;
ProcessCurrRsrc;
IF Evil(  372,  1334566,-15078) THEN EXIT;
IF Evil(  422,  1445005,-22775) THEN EXIT;
END;
PROCEDURE  LookForVirus_Scores;
BEGIN
IF  (gCurrRsrc.fSize < 7026) THEN
  EXIT(LookForVirus_Scores);
InitFgPr;
FgPr(Ptr(ORD4(gCurrRsrc.fHdl^)+12),7014);
IF  gOption[eFgPr] THEN
  BEGIN
  gFgPrTitle := ‘Short fingerprint’;
  CommentFgPrRsrc(@gCurrRsrc);
  END;
IF Evil( 7014, 32071691, 16777) THEN EXIT;
END;
PROCEDURE  ProcessCurrRsrc;
BEGIN
InitFgPr;
FgPr(gCurrRsrc.fHdl^,gCurrRsrc.fSize);
END;
PROCEDURE  ProcessRemoveRsrc;
BEGIN
gCurrRsrc.fKnown    := TRUE;
gCurrRsrc.fInfected := TRUE;
END;
PROCEDURE  Process_ADBS;
BEGIN
ProcessCurrRsrc;
IF Good(  262,   946915,  -549) THEN EXIT;
END;
PROCEDURE  Process_DATA;
BEGIN
WITH gCurrRsrc DO
  BEGIN
  fKnown := TRUE;
  IF  (fSize < 7026) THEN
    EXIT(Process_DATA);
  InitFgPr;
  FgPr(Ptr(ORD4(fHdl^)+12),7014);
  IF  gOption[eFgPr] THEN
    BEGIN
    gFgPrTitle := ‘Short fingerprint’;
    CommentFgPrRsrc(@gCurrRsrc);
    END;
  END;
IF Evil( 7014, 32071691, 16777) THEN EXIT;
END;
PROCEDURE  Process_INIT;
VAR
  sOffset:   INTEGER;
  sPtr:      Ptr;
BEGIN
WITH gCurrRsrc DO
  BEGIN
  InitFgPr;
  IF  (fResId = 6)
  OR ((fResId = 10)
  AND (gCurrFilename <> ‘ Vaccine’))
  OR  (fResId = 17) THEN
    BEGIN
    sOffset := 0;
    sPtr := fHdl^;
    IF  (sPtr^ = $60) THEN
      BEGIN
      INC(LONGINT(sPtr));
      IF  (sPtr^ = 0) THEN
        BEGIN
        INC(LONGINT(sPtr));
        sOffset := 2 + TWordPtr(sPtr)^;
        END
      ELSE
        sOffset := 2 + sPtr^;
      END;
    FgPr(Ptr(ORD4(fHdl^)+sOffset),
                   fSize-sOffset);
    IF  gOption[eFgPr] THEN
      BEGIN
      gFgPrTitle := ‘Short fingerprint’;
      CommentFgPrRsrc(@gCurrRsrc);
      END;
    END
  ELSE
    FgPr(fHdl^,fSize);
  END;
IF Good( 2234,  5918345, 24783) THEN EXIT;
IF Good(    2,    20085, 20085) THEN EXIT;
IF Good(  580,  2390534, -3964) THEN EXIT;
IF Good(  256,   624295,-11467) THEN EXIT;
IF Good(  276,   821588,-22226) THEN EXIT;
IF Good(  318,  1106224, 15474) THEN EXIT;
IF Good(  372,  1325194, 13502) THEN EXIT;
IF Good(  262,   889886,-25106) THEN EXIT;
IF Good( 5200, 16204911,-21859) THEN EXIT;
IF Good(  514,  2113878,-13890) THEN EXIT;
IF Good(  264,   920108,  -860) THEN EXIT;
IF Good(  436,  1746895, -3577) THEN EXIT;
IF Good(  358,  1167886, 15360) THEN EXIT;
IF Good(   26,   102477, -9939) THEN EXIT;
IF Good(  944,  3348718,-13868) THEN EXIT;
IF Good(  820,  2799073,-29445) THEN EXIT;
IF Good(  572,  1840164, -5246) THEN EXIT;

IF Evil(  366,  1333180,-29162) THEN EXIT;
IF Evil(  416,  1443619, -5115) THEN EXIT;
IF Evil(  758,  3291138,  6608) THEN EXIT;
IF Evil( 1014,  4600985, 19785) THEN EXIT;
IF Evil(  474,  1932041, 18387) THEN EXIT;
END;
PROCEDURE  Process_atpl;
BEGIN
ProcessCurrRsrc;
IF Good( 4874, 17745131,  2851) THEN EXIT;

IF Evil( 2410, 10235053,-25635) THEN EXIT;
END;
“Globals.p”
UNIT  Globals;
INTERFACE
USES
  MemTypes,QuickDraw,OSIntf,ToolIntf, PackIntf;
CONST
  {---- Low Mem Globals ----}
  kCurApName          = $910;
  kCurApRefNum        = $900;
  kBootDrive          = $210;
  kResLoad            = $A5E;
  kScrDmpEnb          = $2F8;
  kSFCBLen            = $3F6;
  kSPConfig           = $1FB;
  kSysMap             = $A58;
  kSysResName         = $AD8;
  {---- Other constants ----}
  kIOBufferSize       = 10000;
  kProcessSelf        = FALSE;
  kRsrcHdlValid       = 9876543;
  kRsrcIsInitd        = 3456789;
  kZeroOutVirs        = TRUE;
TYPE
  TCountsPtr          = ^TCountsRec;
  TCountsRec          =
    RECORD
    fDeleted:         LONGINT;
    fExamined:        LONGINT;
    fFiles:           LONGINT;
    fInfected:        LONGINT;
    fRemoved:         LONGINT;
    fResources:       LONGINT;
    END;
  TFeedbackPtr        = ^TFeedbackRec;
  TFeedbackRec        =
    PACKED RECORD
    fWroteDirname:    BOOLEAN;
    fWroteFilename:   BOOLEAN;
    END;
  TJTEHdl             = ^TJTEPtr;
  TJTEPtr             = ^TJTERec;
  TJTERec             =
    RECORD
    fOffset:          INTEGER;
    fSkip3F3C:        INTEGER;
    fSegId:           INTEGER;
    fSkipA9F0:        INTEGER;
    END;
  TJTHdl              = ^TJTPtr;
  TJTPtr              = ^TJTRec;
  TJTRec              =
    RECORD
    fAboveA5Size:     LONGINT;
    fBelowA5Size:     LONGINT;
    fNbrBytesInTable: LONGINT;
    fTableOffset:     LONGINT;
    fJTEntry:         
      ARRAY [1..1] OF TJTERec;
    END;
  TLoaded  =(eNotYet,eAlreadyLoaded,eWeLoadedIt);
  TMainItem           = 
    (eNotADlogItem,
    eDirs,eDiry,eEvery,eFiles,eQuit,
    eAwait,eBeeps,eFgPr,eFgPrC,
    eLList,eRmVir,eTrace,
    eMain,eOpts,eScOW, 
    eDBtn);
  TMainOpt =  ARRAY [eAwait..eTrace] OF BOOLEAN;
  TPaocRec = PACKED ARRAY[1..1] OF CHAR;
  TResIdOrIndex       = (ResId,Index);
  TRsrcPtr            = ^TRsrcRec;
  TRsrcRec            =
    RECORD
    fFlag:            LONGINT;
    fHdl:             Handle;
    fInfected:        BOOLEAN;
    fKnown:           BOOLEAN;
    fLoaded:          TLoaded;
    fResAttrs:        INTEGER;
    fResId:           INTEGER;
    fResType:         ResType;
    fSize:            Size;
    fState:           SignedByte;
    END;
  TScoresHdl          = ^TScoresPtr;
  TScoresPtr          = ^TScoresRec;
  TScoresRec          =
    RECORD
    fOffsetToFirstJTE:INTEGER;
    fNbrJTEsForRsrc:  INTEGER;
    fOldJTE:          TJTERec;
    END;
  TWordHdl            = ^TWordPtr;
  TWordPtr            = ^INTEGER;
 VAR
  gAAGlobals:         SignedByte;
  gAbortPatrol,gActiveSelf,gActiveSys:  BOOLEAN;
  gCode0:             TRsrcRec;
  gCounts:            TCountsRec;
  gCurrDInfo:         DInfo;
  gCurrDirId,gCurrEOF:   LONGINT;
  gCurrDirname:       Str255;
  gCurrIOBuffer:      Ptr;
  gCurrFileDeleted:   BOOLEAN;
  gCurrFilename:      Str255;
  gCurrFInfo:         FInfo;
  gCurrIndex:         INTEGER;
  gCurrRefNum:        INTEGER;
  gCurrRsrc:          TRsrcRec;
  gCurrVRefNum:       INTEGER;
  gCurrWDRefNum:      INTEGER;
  gDateTimeRec:       DateTimeRec;
  gDisabled:          TMainOpt;
  gDlogPtr:           DialogPtr;
  gError:             OSErr;
  gEvt:               EventRecord;
  gEvtMask:           INTEGER;
  gFgPrTitle:         Str255;
  gGrafPtr:           GrafPtr;
  gHFS:               BOOLEAN;
  gInd:               STRING[10];
  gInfected:          BOOLEAN;
  gInfectedWritten:   BOOLEAN;
  gOption:            TMainOpt;
  gPgmrname:          Str255;
  gReportFlags:       TFeedbackRec;
  gScreenFlags:       TFeedbackRec;
  gSecsBegins:        LONGINT;
  gSecsEnds:          LONGINT;
  gSFGetPt:           Point;
  gSFPutPt:           Point;
  gSFRep:             SFReply;
  gTotals:            TCountsRec;
  gZZGlobals:         SignedByte;
FUNCTION   Code0IsValid:          BOOLEAN;
PROCEDURE  CommentBegins;
PROCEDURE  CommentFgPrRsrc(pRsrcPtr: TRsrcPtr);
PROCEDURE  CommentRsrcBegins(pRsrcPtr: TRsrcPtr);
PROCEDURE  DirectoryBegins;
PROCEDURE  DirectoryEnds;
PROCEDURE  ErrorBegins(pStr:     Str255);
PROCEDURE  ErrorEnds(pBeeps:   INTEGER);
PROCEDURE  ErrorOSErr(pStr:     Str255);
PROCEDURE  GetRsrc
  (pRsrcPtr: TRsrcPtr;
  pResType:  ResType;
  pInt:      INTEGER;
  pIntIs:    TResIdOrIndex);
PROCEDURE  InitGlobals;
PROCEDURE  InitRsrc(pRsrcPtr: TRsrcPtr);
FUNCTION   JTEIsValid(pJTEPtr:  TJTEPtr):   BOOLEAN;
PROCEDURE  ListCounts(pPtr:     TCountsPtr);
PROCEDURE  LookForKnownViruses;
PROCEDURE  PauseBriefly;
PROCEDURE  PatrolBegins;
PROCEDURE  PatrolEnds;
PROCEDURE  ProcessCurrRsrc;
PROCEDURE  ProcessFile;
PROCEDURE  ReleaseRsrc(pRsrcPtr: TRsrcPtr);
PROCEDURE  ShortHexDump(pPtr:  Ptr;pNbrBytes: SignedByte);
PROCEDURE  Trace(pStr:    Str255);
PROCEDURE  TraceNbr(pStr:     Str255;pNbr:      LONGINT);
PROCEDURE  ZeroOut(pStart:   Ptr;pCount:    Size);
PROCEDURE  ZeroOutRange(p1:       Ptr;p2:        Ptr);
IMPLEMENTATION
{$R-}
PROCEDURE  ExitSecurityPatrol;      EXTERNAL;
PROCEDURE  Wryte(pStr:     Str255);   EXTERNAL;
PROCEDURE  WryteChar(pChar:    CHAR);    EXTERNAL;
PROCEDURE  WryteEoln;               EXTERNAL;
PROCEDURE  WryteFilename;           EXTERNAL;
PROCEDURE  WryteFilenameToScreenOnlyForNow;EXTERNAL;
PROCEDURE  WryteLn(pStr:     Str255);  EXTERNAL;
PROCEDURE  WryteNbr(pNbr:LONGINT;pNbrDigits:INTEGER);EXTERNAL;
PROCEDURE  WryteType(pType:    ResType);   EXTERNAL;
PROCEDURE  CallProcPtr(pProcPtr: ProcPtr);
INLINE
  $205F,     { MOVE.L (A7)+,A0 }
  $4E90;     { JSR    (A0)     }
PROCEDURE  ErrorInfected
  (pStr:     Str255);  FORWARD;
PROCEDURE  ErrorMsg(pStr:Str255;pBeeps:INTEGER);      FORWARD;
FUNCTION   FixedCode0(pJTPtr:   TJTEPtr): BOOLEAN;    FORWARD;
PROCEDURE  ProcessRsrcs(pResType: ResType;pProcPtr:  ProcPtr);       
        FORWARD;
FUNCTION   RemovedRsrc(pRsrcPtr: TRsrcPtr):BOOLEAN;   FORWARD;
PROCEDURE  TraceRsrc(pStr:     Str255;
  pRsrcPtr:  TRsrcPtr);  FORWARD;
{$S Fingerprint}
{$I Fingerprint.ipas }
{$S Globals}
PROCEDURE  AbortPatrolIfCmdPeriodPressed;
BEGIN
WHILE GetNextEvent(gEvtMask,gEvt) DO
  WITH gEvt DO
    IF  (what = nullEvent) THEN
      LEAVE
    ELSE IF (what = keyDown) THEN
      IF  (BAnd(modifiers,cmdKey)=cmdKey)
      AND (BAnd(message,charCodeMask)=$2E)
      THEN
        BEGIN
        gAbortPatrol := TRUE;
        WryteLn(‘Patrol aborted’);
        LEAVE;
        END;
END;
PROCEDURE  AwaitKeypress;
BEGIN
WHILE TRUE DO
  BEGIN
  IF NOT(GetNextEvent(gEvtMask,gEvt)) THEN
    CYCLE;
  WITH gEvt DO
    IF (what = keyDown) THEN
      BEGIN
      IF  (BAnd(modifiers,cmdKey)=cmdKey)
      AND (BAnd(message,charCodeMask)=$2E)
      THEN
        BEGIN
        gAbortPatrol := TRUE;
        WryteLn(‘Patrol aborted’);
        END;
      LEAVE;
      END;
  END;
END;
FUNCTION   Code0IsValid:  BOOLEAN;
BEGIN
IF  gOption[eTrace] THEN
  Trace(‘Code0IsValid’);
WITH TJTHdl(gCode0.fHdl)^^ DO
  Code0IsValid := 
    (gCode0.fSize     >= 24)    AND
    (fAboveA5Size     >= 40)    AND
    (fNbrBytesInTable >=  8)    AND
    (fTableOffset     =  32)    AND
    (fAboveA5Size = fNbrBytesInTable+32) AND
    ((fNbrBytesInTable MOD 8) = 0);
END;
PROCEDURE  CommentBegins;
BEGIN
Wryte(gInd);
Wryte(gInd);
Wryte(gInd);
END;
PROCEDURE  CommentRsrcBegins(pRsrcPtr: TRsrcPtr);
BEGIN
CommentBegins;
WITH pRsrcPtr^ DO
  BEGIN
  WryteType(fResType);
  WryteNbr (fResId,7);
  Wryte    (‘ (‘);
  ShortHexDump(Ptr(ORD4(@fResAttrs)+1),1);
  WryteChar(‘)’);
  END;
END;
PROCEDURE  CountInfected;
BEGIN
IF  gOption[eTrace] THEN
  Trace(‘CountInfected’);
INC(gCounts.fInfected);
INC(gTotals.fInfected);
END;
PROCEDURE  DirectoryBegins;
BEGIN
IF  gOption[eTrace] THEN
  Trace(‘DirectoryBegins’);
gReportFlags.fWroteDirname := FALSE;
gScreenFlags.fWroteDirname := FALSE;
END;
PROCEDURE  DirectoryEnds;
BEGIN
IF  gOption[eTrace] THEN
  Trace(‘DirectoryEnds’);
(*
Wryte  (‘End of ‘);
WryteLn(gCurrDirname);
*)
END;
FUNCTION   Disinfected_nVIR: BOOLEAN;
VAR
  snVIR2:    TRsrcRec;
  sCodeGone: BOOLEAN;
BEGIN
IF  gOption[eTrace] THEN
  Trace(‘Disinfected_nVIR’);
Disinfected_nVIR := FALSE;
InitRsrc(@snVIR2);
GetRsrc (@snVIR2,’nVIR’,2,ResId);
WITH snVIR2 DO
  BEGIN
  IF  (fFlag <> kRsrcHdlValid) THEN
    BEGIN
    ErrorInfected(‘No nVIR 2!’);
    ReleaseRsrc(@gCurrRsrc);
    EXIT(Disinfected_nVIR);
    END;
  IF  (fSize < 8) THEN
    BEGIN
    ErrorInfected(‘Too small nVIR 2!’);
    ReleaseRsrc(@gCurrRsrc);
    ReleaseRsrc(@snVIR2);
    EXIT(Disinfected_nVIR);
    END;
  MoveHHi(fHdl);
  HLock  (fHdl);
  IF  NOT(FixedCode0(TJTEPtr(fHdl^))) THEN
    BEGIN
    ReleaseRsrc(@gCurrRsrc);
    ReleaseRsrc(@snVIR2);
    EXIT(Disinfected_nVIR);
    END;
  Disinfected_nVIR := TRUE;
  sCodeGone := RemovedRsrc(@gCurrRsrc);
  ReleaseRsrc(@snVIR2);
  ProcessRsrcs(‘nVIR’,@ProcessRemoveRsrc);
  IF  sCodeGone 
  AND (Count1Resources(‘nVIR’) = 0) THEN
    ErrorMsg(‘nVIR removed’,0)
  ELSE
    BEGIN
    ErrorMsg(‘nVIR “disinfected”:’,0);
    CommentBegins;
    Wryte  (‘All of its resources are now ‘);
    Wryte  (‘harmless, but some were not ‘);
    WryteLn(‘removed, for some reason.’);
    END;
  END;
END;
FUNCTION   Disinfected_Scores:  BOOLEAN;
BEGIN
IF  gOption[eTrace] THEN
  Trace(‘Disinfected_Scores’);
Disinfected_Scores := FALSE;
WITH gCurrRsrc DO
  BEGIN
  MoveHHi(fHdl);
  HLock  (fHdl);
  WITH TScoresHdl(fHdl)^^ DO
    IF  NOT(FixedCode0(@fOldJTE)) THEN
      BEGIN
      ReleaseRsrc(@gCurrRsrc);
      EXIT(Disinfected_Scores);
      END;
  Disinfected_Scores := TRUE;
  IF  RemovedRsrc(@gCurrRsrc) THEN
    ErrorMsg(‘Scores removed’,0)
  ELSE
    ErrorMsg(‘Scores disinfected’,0);
  END;
END;
PROCEDURE  ErrorBegins(pStr: Str255);
BEGIN
WryteFilename;
Wryte  (gInd);
Wryte  (gInd);
Wryte  (pStr);
END;
PROCEDURE  ErrorEnds(pBeeps:   INTEGER);
VAR
  i,sBeeps:         INTEGER;
BEGIN
IF  gOption[eBeeps] THEN
  BEGIN
  IF  (pBeeps > 4) THEN
    sBeeps := 4
  ELSE
    sBeeps := pBeeps;
  FOR i := 1 TO sBeeps DO
    SysBeep(3);
  END;
IF  gOption[eAwait] THEN
  BEGIN
  WryteLn(‘ (WAITING ON KEY PRESS)’);
  AwaitKeypress;
  END
ELSE
  WryteEoln;
END;
PROCEDURE  ErrorInfected(pStr: Str255);
BEGIN
IF  NOT(gInfectedWritten) THEN
  BEGIN
  ErrorBegins(‘**!INFECTED!** ‘);
  WryteEoln;
  gInfectedWritten := TRUE;
  END;
IF  (pStr <> ‘’) THEN
  BEGIN
  CommentBegins;
  Wryte(pStr);
  ErrorEnds(3);
  END;
END;
PROCEDURE  ErrorMsg(pStr: Str255;pBeeps:  INTEGER);
BEGIN
ErrorBegins(pStr);
ErrorEnds(pBeeps);
END;
PROCEDURE  ErrorOSErr(pStr: Str255);
BEGIN
IF  (pStr <> ‘’) THEN
  BEGIN
  ErrorBegins(pStr);
  WryteEoln;
  END;
CommentBegins;
Wryte   (‘OSErr code = ‘);
WryteNbr(gError,1);
ErrorEnds(2);
END;
FUNCTION   FixedCode0(pJTPtr: TJTEPtr): BOOLEAN;
BEGIN
FixedCode0 := FALSE;
IF  gOption[eTrace] THEN
  Trace(‘FixedCode0’);
IF  NOT(JTEIsValid(pJTPtr)) THEN
  BEGIN
  ErrorInfected(‘Bad Jump Table Entry!’);
  EXIT(FixedCode0);
  END;
IF  NOT(gOption[eRmVir]) THEN
  BEGIN
  ErrorInfected(‘Remove option off’);
  CommentBegins;
  WITH pJTPtr^ DO
    BEGIN
    Wryte   (‘Jumps to ‘);
    WryteNbr(fOffset,1);
    Wryte   (‘ of CODE ‘);
    WryteNbr(fSegId,1);
    WryteEoln;
    END;
  ErrorMsg(‘Not removed’,1);
  EXIT(FixedCode0);
  END;
WITH gCode0 DO
  BEGIN
  IF  gOption[eTrace] THEN
    BEGIN
    Trace(‘About to restore CODE 0’);
    AbortPatrolIfCmdPeriodPressed;
    IF  gAbortPatrol THEN
      EXIT(FixedCode0);
    END;
  TJTHdl(fHdl)^^.fJTEntry[1] := pJTPtr^;
  IF  (BAnd(fResAttrs,resProtected) <> 0)
  AND (fResAttrs <> -1) THEN
    BEGIN
    SetResAttrs(fHdl,0);
    ChangedResource(fHdl);
    gError := ResError;
    SetResAttrs(fHdl,fResAttrs);
    END
  ELSE
    BEGIN
    ChangedResource(fHdl);
    gError := ResError;
    END;
  IF  (gError <> NoErr) THEN
    BEGIN
    ErrorInfected(‘CODE 0 unchanged!’);
    IF  (gError = wPrErr) THEN
      ErrorMsg(‘Disk is locked’,0)
    ELSE
      ErrorOSErr(‘’);
    gError := 0;
    EXIT(FixedCode0);
    END;
  WriteResource(fHdl);
  gError := ResError;
  IF  (gError <> NoErr) THEN
    BEGIN
    ErrorInfected(‘CODE 0 unwritten!’);
    ErrorOSErr(‘’);
    EXIT(FixedCode0);
    END;
  END;
FixedCode0 := TRUE;
END;
PROCEDURE  GetRsrc
  (pRsrcPtr: TRsrcPtr;
  pResType:  ResType;
  pInt:      INTEGER;
  pIntIs:    TResIdOrIndex);
VAR
  sName:     Str255;
  sResLoad:  BOOLEAN;
  PROCEDURE  CommentWhich;
  BEGIN
  CommentBegins;
  WryteType(pResType);
  WryteChar(‘ ‘);
  WryteNbr (pInt,1);
  IF  (pIntIs = Index) THEN
    Wryte  (‘ (indexed)’);
  WryteEoln;
  END;
BEGIN
WITH pRsrcPtr^ DO
  BEGIN
  IF  (fFlag <> kRsrcIsInitd) THEN
    BEGIN
    ErrorMsg(‘Logic error using GetRsrc’,4);
    AwaitKeypress;
    ExitSecurityPatrol;
    END;
  fResType := pResType;
  fResId   := pInt;
  sResLoad := (TWordPtr(kResLoad)^ <> 0);
  IF  (gActiveSelf OR gActiveSys) THEN
    SetResLoad(FALSE);
  IF  (pIntIs = Index) THEN
    BEGIN
    IF  gOption[eTrace] THEN
      TraceRsrc(‘About to get ind’,pRsrcPtr);
    fHdl := Get1IndResource(pResType,pInt);
    END
  ELSE
    BEGIN
    IF  gOption[eTrace] THEN
      TraceRsrc(‘About to get’,pRsrcPtr);
    fHdl := Get1Resource(pResType,pInt);
    END;
  IF  sResLoad THEN
    BEGIN
    IF  (gActiveSelf OR gActiveSys) THEN
      SetResLoad(TRUE);
    IF  (fHdl = NIL)
    OR (ORD4(fHdl) = -1) THEN
      BEGIN
      gError := ResError;
      ErrorOSErr(‘Couldn’t get resource’);
      CommentWhich;
      InitRsrc(pRsrcPtr);
      EXIT(GetRsrc);
      END;
    fFlag := kRsrcHdlValid;
    fResAttrs := GetResAttrs(fHdl);
    IF  (ResError <> NoErr) THEN
      fResAttrs := -1;
    IF  (fHdl^ = NIL) THEN
      BEGIN
      LoadResource(fHdl);
      fLoaded := eWeLoadedIt;
      IF  gOption[eTrace] THEN
        Trace(‘We loaded it’);
      END
    ELSE
      BEGIN
      fLoaded := eAlreadyLoaded;
      IF  gOption[eTrace] THEN
        Trace(‘Already loaded’);
      END;
    IF  (fHdl^ = NIL) THEN
      BEGIN
      gError := ResError;
      IF  (gError <> NoErr) THEN
        BEGIN
        ErrorMsg(‘Couldn’t load resource’,0);
        IF  (gError = memFullErr) THEN
          ErrorMsg(‘No room in heap zone’,1)
        ELSE
          ErrorOSErr(‘’);
        CommentWhich;
        ReleaseRsrc(pRsrcPtr);
        EXIT(GetRsrc);
        END;
      END;
    fSize := SizeResource(fHdl);
    END
  ELSE
    BEGIN
    fFlag   := kRsrcHdlValid;
    fSize   := MaxSizeRsrc(fHdl);
    fLoaded := eNotYet;
    IF  gOption[eTrace] THEN
      Trace(‘No-load get, loaded not yet’);
    END;
  IF  (pIntIs = Index) THEN
    BEGIN
    GetResInfo(fHdl,fResId,fResType,sName);
    gError := ResError;
    IF  (gError <> NoErr) THEN
      BEGIN
      ErrorOSErr(‘Couldn’t get resource id’);
      CommentWhich;
      ReleaseRsrc(pRsrcPtr);
      EXIT(GetRsrc);
      END;
    END;
  IF  sResLoad THEN
    BEGIN
    fState := HGetState(fHdl);
    IF ((fResType = ‘CODE’)
    AND (fResId   = 0)) THEN
      BEGIN
      MoveHHi(fHdl);
      HLock  (fHdl);
      END
    ELSE
      HNoPurge(fHdl);
    END;
  END;
IF  gOption[eTrace] THEN
  TraceRsrc(‘Got’,pRsrcPtr);
INC(gCounts.fResources);
INC(gTotals.fResources);
END;
PROCEDURE  InitGlobals;
VAR
  sGetHdl,sPutHdl:   DialogTHndl;
  sGetSize,sPutSize,sScrnSize:  Point;
BEGIN
ZeroOutRange(@gAAGlobals,@gZZGlobals);
gCurrIOBuffer := NewPtr(kIOBufferSize);
InitRsrc(@gCode0);
InitRsrc(@gCurrRsrc);
gEvtMask := 
  everyEvent - (updateMask + activMask);
GetPort(gGrafPtr);
gInd      := ‘    ‘;
sGetHdl := DialogTHndl(GetResource(‘DLOG’,getDlgID));
IF  (sGetHdl = NIL)
OR  (LONGINT(sGetHdl) = -1) THEN
  SetPt(sGetSize,304,104)
ELSE
  BEGIN
  IF  (sGetHdl^ = NIL) THEN
    LoadResource(Handle(sGetHdl));
  sGetSize := sGetHdl^^.boundsRect.botRight;
  ReleaseResource(Handle(sGetHdl));
  END;
sPutHdl :=  DialogTHndl(GetResource(‘DLOG’,putDlgID));
IF  (sPutHdl = NIL)
OR  (LONGINT(sPutHdl) = -1) THEN
  SetPt(sPutSize,348,136)
ELSE
  BEGIN
  IF  (sPutHdl^ = NIL) THEN
    LoadResource(Handle(sPutHdl));
  sPutSize := sPutHdl^^.boundsRect.botRight;
  ReleaseResource(Handle(sPutHdl));
  END;
WITH gGrafPtr^.portBits.bounds DO
  BEGIN
  sScrnSize.h := right-left;
  sScrnSize.v := bottom-top;
  END;
gSFGetPt.h := (sScrnSize.h-sGetSize.h) DIV 2;
gSFGetPt.v := (sScrnSize.v-sGetSize.v) DIV 2;
gSFPutPt.h := (sScrnSize.h-sPutSize.h) DIV 2;
gSFPutPt.v := (sScrnSize.v-sPutSize.v) DIV 2;
END;
PROCEDURE  InitRsrc  (pRsrcPtr: TRsrcPtr);
BEGIN
ZeroOut(Ptr(pRsrcPtr),SIZEOF(TRsrcRec));
pRsrcPtr^.fFlag := kRsrcIsInitd;
END;
FUNCTION   JTEIsValid (pJTEPtr: TJTEPtr): BOOLEAN;
VAR
  sCode: TRsrcRec;
BEGIN
IF  gOption[eTrace] THEN
  Trace(‘JTEIsValid’);
JTEIsValid := FALSE;
WITH pJTEPtr^, sCode DO
  BEGIN
  InitRsrc(@sCode);
  SetResLoad(FALSE);
  GetRsrc(@sCode,’CODE’,fSegId,ResId);
  SetResLoad(TRUE);
  IF  (fFlag <> kRsrcHdlValid) THEN
    EXIT(JTEIsValid);
  JTEIsValid := 
    (fSkip3F3C  = $3F3C)  AND
    (fSegId     > 0)      AND
    (fSkipA9F0  = -22032) AND  { $A9F0 }
    (fSize      > 0);
  ReleaseRsrc(@sCode);
  END;
END;
PROCEDURE  ListCounts(pPtr: TCountsPtr);
BEGIN
IF  gOption[eTrace] THEN
  Trace(‘CountsListing’);
WITH pPtr^ DO
  BEGIN
  WryteLn (‘Files:’);
  WryteNbr(fFiles,    6);
  WryteLn (‘ processed’);
  WryteNbr(fExamined,6);
  WryteLn (‘ examined’);
  WryteNbr(fDeleted,  6);
  WryteLn (‘ deleted’);
  WryteLn (‘Resources:’);
  WryteNbr(fResources,6);
  WryteLn (‘ processed’);
  WryteNbr(fInfected, 6);
  WryteLn (‘ infected’);
  WryteNbr(fRemoved,  6);
  WryteLn (‘ removed’);
  Wryte   (‘Currently available memory is ‘);
  WryteNbr(MemAvail DIV 1024,1);
  WryteLn (‘K.’);
  PauseBriefly;
  END;
END;
PROCEDURE  LookForKnownViruses;
VAR
  sWeUsedToBeInfected: BOOLEAN;
  PROCEDURE  Get1stCode;
  BEGIN
  WITH TJTHdl(gCode0.fHdl)^^.fJTEntry[1] DO
    BEGIN
    GetRsrc(@gCurrRsrc,’CODE’,fSegId,ResId);
    IF  (gCurrRsrc.fFlag<>kRsrcHdlValid) THEN
      BEGIN
      ErrorInfected(‘Couldn’t get 1st CODE’);
      InitRsrc(@gCurrRsrc);
      EXIT(LookForKnownViruses);
      END;
    END;
  END;
BEGIN
IF  gOption[eTrace] THEN
  Trace(‘LookForKnownViruses’);
sWeUsedToBeInfected := FALSE;
Get1stCode;
WITH gCurrRsrc DO
  BEGIN
  LookForVirus_nVIR;
  IF  fInfected THEN
    BEGIN
    CountInfected;
    IF  fKnown AND (fSize = 372) THEN
      ErrorInfected(‘nVIR 372 virus’)
    ELSE IF fKnown AND (fSize = 422) THEN
      ErrorInfected(‘nVIR 422 virus’)
    ELSE
      BEGIN
      ErrorInfected(‘New nVIR virus!’);
      gFgPrTitle := ‘’;
      CommentFgPrRsrc(@gCurrRsrc);
      END;
    IF  Disinfected_nVIR THEN
      sWeUsedToBeInfected := TRUE;
    Get1stCode;
    END;
  LookForVirus_Scores;
  IF  fKnown AND fInfected THEN
    BEGIN
    CountInfected;
    ErrorInfected(‘Scores virus’);
    IF  Disinfected_Scores THEN
      sWeUsedToBeInfected := TRUE;
    END
  ELSE
    ReleaseRsrc(@gCurrRsrc);
  END;
IF  sWeUsedToBeInfected THEN
  LookForKnownViruses;
END;
PROCEDURE  PatrolBegins;
BEGIN
IF  gOption[eTrace] THEN
  Trace(‘PatrolBegins’);
WryteEoln;
WryteLn(‘*******************************’);
ZeroOut(@gCounts,SIZEOF(TCountsRec));
GetDateTime(gSecsBegins);
END;
PROCEDURE  PatrolEnds;
VAR
  sMins,sSecs:     INTEGER;
BEGIN
GetDateTime(gSecsEnds);
sSecs := gSecsEnds - gSecsBegins;
sMins := sSecs DIV 60;
sSecs := sSecs - (sMins * 60);
WryteEoln;
WryteLn(‘*******************************’);
WryteEoln;
Wryte   (‘End of patrol that took ‘);
WryteNbr(sMins,1);
WryteChar(‘:’);
IF  (sSecs < 10) THEN
  BEGIN
  WryteChar(‘0’);
  WryteNbr (sSecs,1);
  END
ELSE
  WryteNbr (sSecs,2);
WryteEoln;
ListCounts(@gCounts);
END;
PROCEDURE  PauseBriefly;
VAR
  sTicks:    LONGINT;
BEGIN
Delay(120,sTicks);
END;
PROCEDURE  ProcessCodes;
VAR
  i,sNbrEntries,sPrevId:  INTEGER;
  sWeirdCode0: BOOLEAN;
  PROCEDURE    CommentWhere;
  BEGIN
  CommentBegins;
  Wryte   (‘At entry ‘);
  WryteNbr(i,1);
  WryteEoln;
  END;
BEGIN
IF  gOption[eTrace] THEN
  Trace(‘ProcessCodes’);
GetRsrc(@gCode0,’CODE’,0,ResId);
IF  (gCode0.fFlag <> kRsrcHdlValid) THEN
  BEGIN
  ErrorMsg(‘Code rsrcs without CODE 0’,1);
  EXIT(ProcessCodes);
  END;
IF  NOT(Code0IsValid) THEN
  BEGIN
  ErrorMsg(‘Unexpected CODE 0 values’,1);
  ReleaseRsrc(@gCode0);
  EXIT(ProcessCodes);
  END;
LookForKnownViruses;
WITH TJTHdl(gCode0.fHdl)^^ DO
  BEGIN
  sNbrEntries := fNbrBytesInTable DIV 8;
  sPrevId     := 1;
  sWeirdCode0 := 
    (COPY(gCurrFilename,1,9)=’Red Ryder’) OR
    (COPY(gCurrFilename,1,6)=’Canvas’   ) OR
    (COPY(gCurrFilename,1,9)=’PageMaker’);
  FOR i := 1 TO sNbrEntries DO
    WITH fJTEntry[i] DO
      BEGIN
      IF  (fSkip3F3C  = $3F3C)
      AND (fSegId     = sPrevId)
      AND (fSkipA9F0  = -22032) THEN
        CYCLE;
      AbortPatrolIfCmdPeriodPressed;
      IF  gAbortPatrol THEN
        LEAVE;
      IF  NOT(JTEIsValid(@fJTEntry[i])) THEN
        BEGIN
        ErrorMsg(‘CODE 0 has invalid JTE’,1);
        CommentWhere;
        LEAVE;
        END;
      IF  sWeirdCode0 THEN
        BEGIN
        sPrevId := fSegId;
        CYCLE;
        END;
      IF  (fSegId < sPrevId) THEN
        BEGIN
        ErrorMsg(‘JT not ascending’,1);
        CommentWhere;
        LEAVE;
        END;
      INC(sPrevId);
      IF  (fSegId = sPrevId) THEN
        CYCLE;
      ErrorMsg(‘JT skips ResId’,1);
      CommentWhere;
      LEAVE;
      END;
  END;
ReleaseRsrc(@gCode0);
END;
PROCEDURE  ProcessFile;
VAR
  sSaveC1T:  INTEGER;
  PROCEDURE  ExitIfCantReadFork;
  BEGIN
  IF  (gError <> NoErr) THEN
    BEGIN
    IF  (gError = eofErr) THEN
      { no resource fork }
    ELSE IF  (gError = fnfErr) THEN
      ErrorMsg(‘File not found’,1)
    ELSE IF  (gError = nsvErr) THEN
      ErrorMsg(‘No such volume’,1)
    ELSE IF  (gError = opWrErr) THEN
      ErrorMsg(CONCAT(‘Already in use.  ‘,
        ‘(Don’t use under MultiFinder!)’),1)
    ELSE
      ErrorOSErr(‘Couldn’t open file’);
    gError := NoErr;
    EXIT(ProcessFile);
    END;
  END;
BEGIN
IF  gOption[eTrace] THEN
  Trace(‘ProcessFile’);
AbortPatrolIfCmdPeriodPressed;
IF  gAbortPatrol THEN
  EXIT(ProcessFile);
INC(gCounts.fFiles);
INC(gTotals.fFiles);
gInfected                   := FALSE;
gInfectedWritten            := FALSE;
gReportFlags.fWroteFilename := FALSE;
gScreenFlags.fWroteFilename := FALSE;
IF  gOption[eLList] THEN
  WryteFilename
ELSE
  WryteFilenameToScreenOnlyForNow;
IF  (LENGTH(gCurrFilename) > 0) THEN
  IF (gCurrFilename[1] = ‘.’) THEN
    BEGIN
    ErrorMsg(‘Filename begins with “.”’,1);
    EXIT(ProcessFile);
    END;
IF  gActiveSelf AND NOT(kProcessSelf) THEN
  EXIT(ProcessFile);
gCurrEOF := -1;
gError := FSOpen(gCurrFilename,gCurrWDRefNum, gCurrRefNum);
ExitIfCantReadFork;
gError := GetEOF(gCurrRefNum,gCurrEOF);
IF  (gError = NoErr) THEN
  BEGIN
  WITH gCurrFInfo DO
    IF  (COPY(gCurrFilename,1,7)=’MacsBug’) 
    OR  (fdType = ‘RELB’) 
    OR  (fdType = ‘OBJ ‘) THEN
      IF  NOT(KnownDataFork) THEN
        CommentFgPrData;
  gError := FSClose(gCurrRefNum);
  IF  (gError <> NoErr) THEN
    ErrorOSErr(‘Couldn’t close data fork’);
  END
ELSE
  ErrorOSErr(‘Couldn’t GetEOF’);
IF  gActiveSelf THEN
  BEGIN
  gCurrRefNum := TWordPtr(kCurApRefNum)^;
  gError  := NoErr;
  END
ELSE IF  gActiveSys THEN
  BEGIN
  gCurrRefNum := TWordPtr(kSysMap)^;
  gError  := NoErr;
  END
ELSE
  BEGIN
  SetResLoad(FALSE);
  gCurrRefNum := OpenRFPerm(gCurrFilename,gCurrWDRefNum,
                            fsRdWrPerm);
  gError  := ResError;
  SetResLoad(TRUE);
  ExitIfCantReadFork;
  END;
IF  (gCurrRefNum <> CurResFile) THEN
  BEGIN
  UseResFile(gCurrRefNum);
  gError := ResError;
  IF  (gError <> NoErr) THEN
    BEGIN
    ErrorOSErr(‘Couldn’t use resource fork’);
    gError := NoErr; 
    EXIT(ProcessFile);
    END;
  END;
INC(gCounts.fExamined);
INC(gTotals.fExamined);
IF  (Count1Resources(‘CODE’) > 0) THEN
  ProcessCodes;
gFgPrTitle := ‘Unknown Resource(s):’;
ProcessRsrcs(‘ADBS’,@Process_ADBS);
ProcessRsrcs(‘CACH’,@Process_CACH);
ProcessRsrcs(‘CDEF’,@Process_CDEF);
{etc}
IF  gOption[eFgPr] THEN
  BEGIN
  gFgPrTitle := ‘Fingerprint(s):’;
  ProcessRsrcs(‘ADBS’,@ProcessCurrRsrc);
  ProcessRsrcs(‘CACH’,@ProcessCurrRsrc);
  ProcessRsrcs(‘CDEF’,@ProcessCurrRsrc);
  IF  gOption[eFgPrC] THEN
    ProcessRsrcs(‘CODE’,@ProcessCurrRsrc);
  ProcessRsrcs(‘DATA’,@ProcessCurrRsrc);
  {etc}
  ProcessRsrcs(‘nVIR’,@ProcessCurrRsrc);
  END;
IF  gActiveSelf OR gActiveSys THEN
  EXIT(ProcessFile);
sSaveC1T := Count1Types;
CloseResFile(gCurrRefNum);
IF  NOT(gInfected) THEN
  EXIT(ProcessFile);
WITH gCurrFInfo DO
  BEGIN
  IF ((gCurrFilename = ‘Note Pad File’)
  OR  (gCurrFilename = ‘Scrapbook File’))
  AND (fdCreator = ‘ZSYS’)
  AND gOption[eRmVir] THEN
    BEGIN
    fdType    := ‘ZSYS’;
    fdCreator := ‘MACS’;
    fdFlags   := 4096;
    gError    := SetFInfo(gCurrFilename,
                          gCurrWDRefNum,
                          gCurrFInfo);
    IF  (gError = NoErr) THEN
      ErrorMsg(‘Reset to system document’,0)
    ELSE
      ErrorOSErr(‘FInfo not reset’);
    EXIT(ProcessFile);
    END;
  END;
IF  (gCurrEOF <> 0) THEN
  BEGIN
  ErrorMsg(‘File still has data fork’,0);
  ErrorMsg(‘File not deleted’,1);
  EXIT(ProcessFile);
  END;
IF  (sSaveC1T <> 0) THEN
  BEGIN
  ErrorMsg(‘File still has resources’,0);
  ErrorMsg(‘File not deleted’,1);
  EXIT(ProcessFile);
  END;
ErrorMsg(‘File emptied’,0);
gError := FSDelete(gCurrFilename,gCurrWDRefNum);
IF  (gError = NoErr) THEN
  BEGIN
  gCurrFileDeleted := TRUE;
  INC(gCounts.fDeleted);
  INC(gTotals.fDeleted);
  ErrorMsg(‘File deleted’,1);
  END
ELSE
  ErrorOSErr(‘File not deleted’);
END;
PROCEDURE  ProcessRsrcs(pResType:ResType; pProcPtr:  ProcPtr);
VAR
  i,sIdx:         INTEGER;
BEGIN
IF  gOption[eTrace] THEN
  Trace(‘ProcessRsrcs’);
WITH gCurrRsrc DO
  BEGIN
  sIdx := 1;
  FOR i := 1 TO Count1Resources(pResType) DO
    BEGIN
    AbortPatrolIfCmdPeriodPressed;
    IF  gAbortPatrol THEN
      LEAVE;
    GetRsrc(@gCurrRsrc,pResType,sIdx,Index);
    IF  (fFlag <> kRsrcHdlValid) THEN
      BEGIN
      INC(sIdx);
      CYCLE;
      END;
    CallProcPtr(pProcPtr);
    IF  fInfected THEN
      BEGIN
      CountInfected;
      ErrorInfected(‘’);
      CommentRsrcBegins(@gCurrRsrc);
      WryteLn(‘ is an infection’);
      IF  RemovedRsrc(@gCurrRsrc) THEN
        BEGIN
        ErrorMsg(‘Removed’,0);
        CYCLE;
        END;
      ErrorMsg(‘Not removed’,1);
      INC(sIdx);
      CYCLE;
      END;
    IF  NOT(fKnown) THEN
      CommentFgPrRsrc(@gCurrRsrc);
    ReleaseRsrc(@gCurrRsrc);
    INC(sIdx);
    END;
  END;
END;
PROCEDURE  ReleaseRsrc(pRsrcPtr: TRsrcPtr);
BEGIN
WITH pRsrcPtr^ DO
  BEGIN
  IF  (fFlag <> kRsrcHdlValid) THEN
    BEGIN
    ErrorMsg(‘Error using ReleaseRsrc’,4);
    AwaitKeypress;
    ExitSecurityPatrol;
    END;
  IF  gOption[eTrace] THEN
    TraceRsrc(‘About to release’,pRsrcPtr);
  IF  (gActiveSelf OR gActiveSys) THEN
    IF  gOption[eTrace] THEN
      Trace(‘Not Released’)
    ELSE
  ELSE
    BEGIN
    HSetState(fHdl,fState);
    ReleaseResource(fHdl);
    IF  gOption[eTrace] THEN
      Trace(‘Released’);
    END;
  InitRsrc(pRsrcPtr);
  END;
END;
FUNCTION   RemovedRsrc(pRsrcPtr: TRsrcPtr):   BOOLEAN;
VAR
  sBits0and7:LONGINT;
  PROCEDURE  ExitIfError(pStr:     Str255);
  BEGIN
  gError := ResError;
  IF  (gError <> NoErr) THEN
    BEGIN
    ErrorMsg(pStr,0);
    IF  (gError = wPrErr) THEN
      ErrorMsg(‘Disk is locked’,0)
    ELSE
      ErrorOSErr(‘’);
    CommentRsrcBegins(pRsrcPtr);
    WryteLn(‘ not removed’);
    ReleaseRsrc(pRsrcPtr);
    EXIT(RemovedRsrc);
    END;
  END;
BEGIN
RemovedRsrc := FALSE;
IF  gOption[eTrace] THEN
  Trace(‘RemovedRsrc’);
AbortPatrolIfCmdPeriodPressed;
IF  gAbortPatrol
OR  NOT(gOption[eRmVir]) THEN
  BEGIN
  ReleaseRsrc(pRsrcPtr);
  EXIT(RemovedRsrc);
  END;
WITH pRsrcPtr^ DO
  BEGIN
  IF  (fFlag <> kRsrcHdlValid) THEN
    BEGIN
    ErrorMsg(‘Error using RemovedRsrc’,4);
    AwaitKeypress;
    ExitSecurityPatrol;
    END;
  IF  gOption[eTrace] THEN
    BEGIN
    TraceRsrc(‘About to remove’,pRsrcPtr);
    AbortPatrolIfCmdPeriodPressed;
    IF  gAbortPatrol THEN
      EXIT(RemovedRsrc);
    END;
  IF  NOT(fInfected) THEN
    BEGIN
    ErrorMsg(‘Tried to remove uninfected’,4);
    AwaitKeypress;
    ExitSecurityPatrol;
    END;
  IF  kZeroOutVirs AND (fHdl^ <> NIL) THEN
    BEGIN
    ZeroOut(fHdl^,fSize);
    ChangedResource(fHdl);
    gError := ResError;
    IF  (gError = NoErr) THEN
      BEGIN
      WriteResource(fHdl);
      gError := ResError;
      IF  (gError <> NoErr) THEN
        ErrorOSErr(‘Couldn’t WriteResource’);
      END
    ELSE
      ErrorOSErr(‘Couldn’t ChangedResource’);
    END;
  sBits0and7 := BAnd(fResAttrs,$81);
  SetResAttrs(fHdl,LoWord(sBits0and7));
  RmveResource(fHdl);
  ExitIfError(‘Couldn’t remove resource’);
  UpdateResFile(gCurrRefNum);
  ExitIfError(‘Couldn’t update res file’);
  DisposHandle(fHdl);
  InitRsrc(pRsrcPtr);
  RemovedRsrc := TRUE;
  IF  gOption[eTrace] THEN
    Trace(‘RemovedRsrc successful’);
  END;
INC(gCounts.fRemoved);
INC(gTotals.fRemoved);
END;
PROCEDURE  ShortHexDump
  (pPtr:     Ptr;
  pNbrBytes: SignedByte);
VAR
  i:         INTEGER;
  sCh1,sCh2,sDigit: LONGINT;
  sIdx:      Ptr;
BEGIN
sIdx  := pPtr;
FOR i := 1 TO pNbrBytes DO
  BEGIN
  sDigit := ORD4(sIdx^);
  sCh1   := BSR(BAnd(sDigit,$F0),4);
  sCh2   :=     BAnd(sDigit,$0F);
  IF  sCh1 > 9 THEN
    WryteChar(CHR(sCh1 + $37))
  ELSE
    WryteChar(CHR(sCh1 + $30));
  IF  sCh2 > 9 THEN
    WryteChar(CHR(sCh2 + $37))
  ELSE
    WryteChar(CHR(sCh2 + $30));
  INC(LONGINT(sIdx));
  END;
END;
PROCEDURE  Trace(pStr:     Str255);
BEGIN
ErrorBegins(pStr);
ErrorEnds(0);
END;
PROCEDURE  TraceNbr(pStr:     Str255;pNbr:      LONGINT);
BEGIN
ErrorBegins(pStr);
WryteNbr(pNbr,1);
ErrorEnds(0);
END;
PROCEDURE  TraceRsrc(pStr: Str255;pRsrcPtr:  TRsrcPtr);
BEGIN
ErrorBegins(pStr);
WITH pRsrcPtr^ DO
  BEGIN
  WryteChar(‘ ‘);
  WryteType(fResType);
  WryteNbr (fResId,7);
  END;
ErrorEnds(0);
END;
PROCEDURE  ZeroOut(pStart:   Ptr;pCount:  Size);
VAR
  i:         INTEGER;
  sIdx:      Ptr;
BEGIN
sIdx := pStart;
FOR i := 1 TO pCount DO
  BEGIN
  sIdx^ := 0;
  INC(LONGINT(sIdx));
  END;
END;
PROCEDURE  ZeroOutRange(p1:  Ptr; p2:  Ptr);
VAR
  i:         INTEGER;
  sIdx:      Ptr;
BEGIN
IF  (ORD4(p1) < ORD4(p2)) THEN
  sIdx := p1
ELSE
  sIdx := p2;
FOR i := 1 TO ABS(ORD4(p2)-ORD4(p1))+1 DO
  BEGIN
  sIdx^ := 0;
  INC(LONGINT(sIdx));
  END;
END;
END.
“MainDlog.p”
UNIT  MainDlog;
INTERFACE
USES
  MemTypes,QuickDraw,OSIntf,ToolIntf, PackIntf,Globals;
PROCEDURE  InitMainDlog;
FUNCTION   MainDlogWorkRequested:  TMainItem;
IMPLEMENTATION
{$R-}
CONST
  {--------item range--------}
  kItemFst            = eDirs;
  kItemLst            = eDBtn;
  {----item type subranges--}
  kBtnFst             = eDirs;
  kBtnLst             = eQuit;
  kChkFst             = eAwait;
  kChkLst             = eTrace;
  kStatFst            = eMain;
  kStatLst            = eScOW;
  kUItmFst            = eDBtn;
  kUItmLst            = eDBtn;
  {--titled item subrange--}
  kTItmFst            = eDirs;
  kTItmLst            = eScOW;
TYPE
  TDitmPtr            = ^TDitmRec;
  TDitmRec            =
    PACKED RECORD
    fProcPtr:         ProcPtr;
    fRect:            Rect;
    fType:            Byte;
    fLen:             Byte;
    fData:            INTEGER;
    END;
VAR
  gDBtnRect:          Rect;
  gHdl:               ARRAY [eAwait..eTrace]
                      OF ControlHandle;
  gRect:              ARRAY [eAwait..eTrace]
                      OF Rect;
PROCEDURE  ChkChk(pItem:    TMainItem);
BEGIN
IF  (pItem < kChkFst)
OR  (pItem > kChkLst) THEN
  BEGIN
  SysBeep(3);
  EXIT(ChkChk);
  END;
SetCtlValue(gHdl[pItem],ORD(gOption[pItem]));
IF  gDisabled[pItem] THEN
  BEGIN
  SetDItem(gDlogPtr,ORD(pItem),
    ctrlItem+chkCtrl+itemDisable,
    Handle(gHdl[pItem]),gRect[pItem]);
  HiliteControl(gHdl[pItem],255);
  END
ELSE
  BEGIN
  SetDItem(gDlogPtr,ORD(pItem),
    ctrlItem+chkCtrl,
    Handle(gHdl[pItem]),gRect[pItem]);
  HiliteControl(gHdl[pItem],0);
  END;
END;
PROCEDURE  FrameDefaultBtn(pWindowPtr:WindowPtr; pItemNo:   INTEGER);
VAR
  sPenState: PenState;
BEGIN
GetPenState(sPenState);
PenSize (3,3);
FrameRoundRect(gDBtnRect,16,16);
SetPenState(sPenState);
END;
PROCEDURE  InitMainDlog;
CONST
  kTitleMax       = 27;
  kTItmLen        = 42;
    { 14 + kTitleMax + ord(odd(kTitleMax)); }
  kUItmLen        = 14;
VAR
  i:              TMainItem;
  sDitmPtr:       TDitmPtr;
  sDlogRect:      Rect;
  sHdl:           Handle;
  sNbrTItms:      INTEGER;
  sNbrUItms:      INTEGER;
  sRect:          ARRAY [eDirs..eScOW]
                  OF Rect;
  sSize:          Size;
  sTitle:         ARRAY [eDirs..eScOW]
                  OF STRING[kTitleMax];
  sType:          INTEGER;
BEGIN
IF  gOption[eTrace] THEN
  Trace(‘InitMainDlog’);
FOR i := kTItmFst TO kTItmLst DO
  IF  (i >= kBtnFst)
  AND (i <= kBtnLst) THEN
    BEGIN
    SetRect   (sRect[i], 266, 65, 346, 83);
    OffsetRect(sRect[i],0, 27*(ORD(i)-ORD(kBtnFst)));
    END
  ELSE IF (i >= kChkFst)
  AND     (i <= kChkLst) THEN
    BEGIN
    SetRect   (sRect[i],  24, 62, 185, 80);
    OffsetRect(sRect[i],0, 20*(ORD(i)-ORD(kChkFst)));
    END
  ELSE
    SetRect   (sRect[i],  12, 42, 216, 60);
OffsetRect(sRect[eMain], 080,-30);
OffsetRect(sRect[eOpts], 000,000);
OffsetRect(sRect[eScOW], 216,000);
gDBtnRect := sRect[kItemFst];
InsetRect (gDBtnRect, -4, -4);
WITH sDlogRect, gSFGetPt DO
  BEGIN
  top    := v - 10;
  left   := h - 10;
  bottom := top  + 210;
  right  := left + 368;
  END;
sTitle[eDirs]  := ‘Directories’;
sTitle[eDiry]  := ‘Directory’;
sTitle[eEvery] := ‘Everything’;
sTitle[eFiles] := ‘Files’;
sTitle[eQuit]  := ‘Quit’;
sTitle[eAwait] := ‘Await Keypress’;
sTitle[eBeeps] := ‘Beep’;
sTitle[eFgPr]  := ‘Fingerprint’;
sTitle[eFgPrC] := ‘Fingerprint CODEs’;
sTitle[eLList] := ‘Long Listing’;
sTitle[eRmVir] := ‘Remove Viruses’;
sTitle[eTrace] := ‘Trace’;
sTitle[eMain]  := 
  ‘Security Patrol Main Dialog’;
sTitle[eOpts]  := ‘Options:’;
sTitle[eScOW]  := ‘Scope Of Work:’;
sNbrTItms := (ORD(kTItmLst)-ORD(kTItmFst))+1;
sNbrUItms := (ORD(kUItmLst)-ORD(kUItmFst))+1;
sHdl := NewHandle(2 + (sNbrTItms*kTItmLen) + (sNbrUItms*kUItmLen));
TWordPtr(sHdl^)^ := ORD(kItemLst) - 1;
sSize := 2;
FOR i := kItemFst TO kItemLst DO
  BEGIN
  IF  gOption[eTrace] THEN
    TraceNbr(‘sSize = ‘,sSize);
  sDitmPtr := POINTER(ORD4(sHdl^)+sSize);
  WITH sDitmPtr^ DO
    IF  (i >= kTItmFst)
    AND (i <= kTItmLst) THEN
      BEGIN
      fProcPtr := NIL;
      fRect    := sRect[i];
      IF  (i <= kBtnLst) THEN
        fType  := ctrlItem + btnCtrl
      ELSE IF  (i <= kChkLst) THEN
        fType  := ctrlItem + chkCtrl
      ELSE
        fType  := statText + itemDisable;
      BlockMove(@sTitle[i],@fLen,
                LENGTH(sTitle[i])+1);
      sSize := sSize+14+fLen+ORD(ODD(fLen));
      END
    ELSE IF  (i = eDBtn) THEN
      BEGIN
      fProcPtr := @FrameDefaultBtn;
      fRect    := gDBtnRect;
      fType    := userItem + itemDisable;
      fLen     := 0;
      sSize    := sSize + 14;
      END
    ELSE
      SysBeep(60);
  END;
SetHandleSize(sHdl,sSize);
gDlogPtr := NewDialog(NIL,sDlogRect,’’,
  FALSE,dBoxProc,POINTER(-1),FALSE,0,sHdl);
FOR i := kChkFst TO kChkLst DO
  BEGIN
  GetDItem(gDlogPtr,ORD(i),sType,
           Handle(gHdl[i]),gRect[i]);
  IF  gOption[eTrace] THEN
    TraceNbr(‘ChkChk’ing item ‘,ORD(i));
  ChkChk(i);
  END;
END;
FUNCTION   KybdEquivsFilter(pDialogPtr:   DialogPtr;
VAR pEventRec: EventRecord;VAR pItemHit:  INTEGER): BOOLEAN;
VAR
  sChar:
    RECORD
    CASE INTEGER OF
    0:(F1,F2,F3: SignedByte;
       Enum:     TMainItem);
    1:(L:        LONGINT);
    END;
  sItem:         TMainItem;
BEGIN
sItem := eNotADlogItem;
WITH pEventRec DO
  IF  (what = keyDown) THEN
    BEGIN
    sChar.L := BAnd(message,charCodeMask);
    IF  (sChar.L = $03) 
    OR  (sChar.L = $0D) THEN
      sItem := eDirs
    ELSE IF (sChar.L = ORD(‘D’)) 
    OR      (sChar.L = ORD(‘d’)) THEN
      sItem := eDiry
    ELSE IF (sChar.L = ORD(‘E’)) 
    OR      (sChar.L = ORD(‘e’)) THEN
      sItem := eEvery
    ELSE IF (sChar.L = ORD(‘F’)) 
    OR      (sChar.L = ORD(‘f’)) THEN
      sItem := eFiles
    ELSE IF (sChar.L = ORD(‘Q’)) 
    OR      (sChar.L = ORD(‘q’)) 
    OR ((BAnd(modifiers,cmdKey)<>0) 
        AND (sChar.L = ORD(‘.’))) THEN
      sItem := eQuit
    ELSE
      BEGIN
      sChar.L := 
        (sChar.L-ORD4(‘1’)) + ORD(kChkFst);
      IF  (sChar.L >= ORD(kChkFst)) 
      AND (sChar.L <= ORD(kChkLst)) THEN
        IF  NOT(gDisabled[sChar.Enum]) THEN
          sItem := sChar.Enum;
      END;
    END;
IF  (sItem = eNotADlogItem) THEN
  KybdEquivsFilter := FALSE
ELSE
  BEGIN
  pItemHit         := ORD(sItem);
  KybdEquivsFilter := TRUE;
  END;
END;
FUNCTION   MainDlogWorkRequested:          TMainItem;
VAR
  sItem:
    RECORD
    CASE INTEGER OF
    0:(Filler: SignedByte;
       Enum:   TMainItem);
    1:(Int:    INTEGER);
    END;
BEGIN
IF  gOption[eTrace] THEN
  Trace(‘MainDlogWorkRequested’);
BringToFront(gDlogPtr);
ShowWindow  (gDlogPtr);
ModalDialog (@KybdEquivsFilter,sItem.Int);
WHILE (sItem.Enum >= kChkFst)
AND   (sItem.Enum <= kChkLst) DO
  BEGIN
  gOption[sItem.Enum] := 
    NOT(gOption[sItem.Enum]);
  ChkChk(sItem.Enum);
  IF  (sItem.Enum = eFgPr) THEN
    BEGIN
    gDisabled[eFgPrC] := NOT(gOption[eFgPr]);
    gOption  [eFgPrC] := FALSE;
    ChkChk(eFgPrC);
    END;
  ModalDialog (@KybdEquivsFilter,sItem.Int);
  END;
HideWindow  (gDlogPtr);
SetPort     (gGrafPtr);
IF  gOption[eTrace] THEN
  TraceNbr(‘Returning ‘,ORD(sItem.Enum));
IF  (sItem.Enum >= kItemFst)
AND (sItem.Enum <= kItemLst) THEN
  MainDlogWorkRequested := sItem.Enum
ELSE
  MainDlogWorkRequested := eQuit;
END;
END.

“Patrol.p”
UNIT  Patrol;
INTERFACE
USES
  MemTypes,QuickDraw,OSIntf,ToolIntf,PackIntf,Globals;
PROCEDURE  BuildDirname;
PROCEDURE  InitPatrols;
PROCEDURE  PatrolDirectories(pOnly1Deep:BOOLEAN);
PROCEDURE  PatrolEverything;
PROCEDURE  PatrolFiles;
IMPLEMENTATION
{$R-}
CONST
  kPatsInitd          = -12345;
TYPE
  TOverlappingPBs     =
    RECORD
    CASE INTEGER OF
    0:  (fPBRec:      HParamBlockRec);
    1:  (fCPBRec:     CInfoPBRec);
    END;
VAR
  gAAPatImpl,gZZPatImpl:  SignedByte;
  gAppDirId,gInitdFlag,gSysDirId:  LONGINT;
  gAppVRefNum,gOrigWDRefNum,gSysVRefNum:  INTEGER;
  gOnly1Deep:         BOOLEAN;
  gPBs:               TOverlappingPBs;
  gSFLst:             SFTypeList;
  gWDPBRec:           WDPBRec;
PROCEDURE  BuildDirname;
VAR
  sErr:      OSErr;
  sLen:      INTEGER;
  sName:     Str255;
  sPBs:      TOverlappingPBs;
BEGIN
IF  gOption[eTrace] THEN
  Trace(‘BuildDirname’);
WITH sPBs,fPBRec,fCPBRec DO
  BEGIN
  sPBs         := gPBs;
  ioNamePtr    := @sName;
  ioVRefNum    := gCurrWDRefNum;
  gCurrDirname := ‘’;
  IF  gHFS THEN
    BEGIN
    ioFDirIndex := -1;
    ioDrParID   := 0;
    REPEAT
      ioDrDirId := ioDrParID;
      sErr := PBGetCatInfo(@fCPBRec,FALSE);
      IF  (sErr <> NoErr) THEN
        EXIT(BuildDirname);
      sLen :=  LENGTH(sName)+1+LENGTH(gCurrDirname); 
      IF  (sLen <= 255) THEN
        gCurrDirname :=  CONCAT(sName,’:’,gCurrDirname);
    UNTIL ioDrDirId = 2;
    END
  ELSE
    BEGIN
    sErr := PBGetVol(@fPBRec,FALSE);
    IF  (sErr = NoErr) THEN
      gCurrDirname := CONCAT(sName,’:’);
    END;
  END;
END;
PROCEDURE  CallProcessFile;
BEGIN
gActiveSelf := 
  (gCurrVRefNum  = gAppVRefNum) AND 
  (gCurrDirId    = gAppDirId)   AND
  (gCurrFilename = StringPtr(kCurApName)^);
gActiveSys := 
  (gCurrVRefNum  = gSysVRefNum) AND 
  (gCurrDirId    = gSysDirId)   AND 
  (gCurrFilename = StringPtr(kSysResName)^);
gCurrFileDeleted := FALSE;
ProcessFile;
END;
PROCEDURE  FloatWDDeeper(pDrDirId:  LONGINT);
BEGIN
IF  gOption[eTrace] THEN
  TraceNbr(‘Begin FloatWDDeeper,    WD = ‘,    ORD4(gCurrWDRefNum));
WITH gPBs,fCPBRec DO
  BEGIN
  IF  NOT((pDrDirId=0) OR (pDrDirId=2)) THEN
    BEGIN
    gWDPBRec.ioVRefNum := gCurrWDRefNum;
    gWDPBRec.ioWDDirId := 0;
    gError := PBCloseWD(@gWDPBRec,FALSE);
    IF  (gError <> NoErr) THEN
      BEGIN
      ErrorOSErr(‘Couldn’t close WD’);
      EXIT(FloatWDDeeper);
      END;
    END;
  gWDPBRec.ioVRefNum := gCurrVRefNum;
  gWDPBRec.ioWDDirId := ioDrDirId;
  gError := PBOpenWD(@gWDPBRec,FALSE);
  IF  (gError <> NoErr) THEN
    BEGIN
    ErrorOSErr(‘Couldn’t open subdir WD’);
    EXIT(FloatWDDeeper);
    END;
  gCurrWDRefNum := gWDPBRec.ioVRefNum;
  END;
IF  gOption[eTrace] THEN
  TraceNbr(‘End   FloatWDDeeper,    WD = ‘,
              ORD4(gCurrWDRefNum));
END;
PROCEDURE  FloatWDShallower(pDrDirId:  LONGINT);
BEGIN
IF  gOption[eTrace] THEN
  TraceNbr(‘Begin FloatWDShallower, WD = ‘,
              ORD4(gCurrWDRefNum));
WITH gPBs,fCPBRec DO
  BEGIN
  gError := PBCloseWD(@gWDPBRec,FALSE);
  gWDPBRec.ioVRefNum := gCurrWDRefNum;
  gWDPBRec.ioWDDirId := 0;
  IF  (gError <> NoErr) THEN
    BEGIN
    ErrorOSErr(‘Couldn’t close subdir WD’);
    EXIT(FloatWDShallower);
    END;
  IF  (pDrDirId = 0)
  OR  (pDrDirId = 2) THEN
    gCurrWDRefNum := gOrigWDRefNum
  ELSE
    BEGIN
    gWDPBRec.ioVRefNum := gCurrVRefNum;
    gWDPBRec.ioWDDirId := pDrDirId;
    gError := PBOpenWD(@gWDPBRec,FALSE);
    IF  (gError <> NoErr) THEN
      BEGIN
      ErrorOSErr(‘Couldn’t reopen WD’);
      EXIT(FloatWDShallower);
      END;
    gCurrWDRefNum := gWDPBRec.ioVRefNum;
    END;
  END;
IF  gOption[eTrace] THEN
  TraceNbr(‘End   FloatWDShallower, WD = ‘,
              ORD4(gCurrWDRefNum));
END;
PROCEDURE  GetActualDirId(pDrDirId:  LONGINT);
BEGIN
WITH gPBs,fCPBRec DO
  BEGIN
  ioDrDirId   := pDrDirId;
  ioFDirIndex := -1;
  ioVRefNum   := gOrigWDRefNum;
  gError := PBGetCatInfo(@fCPBRec,FALSE);
  IF  (gError <> NoErr) THEN
    BEGIN
    ErrorOSErr(‘Couldn’t GetActualDirId’);
    EXIT(GetActualDirId);
    END;
  gCurrDInfo  := ioDrUsrWds;
  gCurrDirId  := ioDrDirId;
  IF  gOption[eTrace] THEN
    TraceNbr(‘ActualDirID = ‘,gCurrDirId);
  END;
END;
PROCEDURE  InitPatrols;
BEGIN
IF  gOption[eTrace] THEN
  Trace(‘InitPatrols’);
WITH gPBs,fPBRec,fCPBRec DO
  BEGIN
  ZeroOutRange(@gAAPatImpl,@gZZPatImpl);
  gHFS        := TWordPtr(kSFCBLen)^ > 0;
  ioNamePtr := @gCurrFilename;
  IF  gHFS THEN
    BEGIN
    gError  := GetVRefNum
      (TWordPtr(kSysMap)^,gSysVRefNum);
    IF  (gError <> NoErr) THEN
      ErrorOSErr(‘Couldn’t get act sys vol’);
    ioVRefNum  := gSysVRefNum;
    gError     := PBHGetVInfo(@fPBRec,FALSE);
    IF  (gError <> NoErr) THEN
      ErrorOSErr(‘Couldn’t get act sys dir’);
    IF  (fPBRec.ioVFndrInfo[1] = 0) THEN
      ErrorOSErr(‘Boot vol not Blessed’);
    gSysDirId  := ioVFndrInfo[1];
    gError     := PBHGetVol(@fPBRec,FALSE);
    IF  (gError <> NoErr) THEN
      ErrorOSErr(‘Couldn’t get own DirId’);
    gAppDirId  := ioDirId;
    gCurrDirId := ioDirId;
    gCurrWDRefNum := ioVRefNum;
    ioVolIndex := 0;
    gError     := PBHGetVInfo(@fPBRec,FALSE);
    IF  (gError <> NoErr) THEN
      ErrorOSErr(‘Couldn’t get own VInfo’);
    gAppVRefNum := ioVRefNum;
    END
  ELSE
    BEGIN
    gSysVRefNum := TWordPtr(kBootDrive)^;
    gError      := PBGetVol(@fPBRec,FALSE);
    IF  (gError <> NoErr) THEN
      ErrorOSErr(‘Couldn’t get own Vol’);
    gAppVRefNum   := ioVRefNum;
    gCurrWDRefNum := ioVRefNum;
    gAppDirId   := 2;
    gSysDirId   := 2;
    gCurrDirId  := 2;
    END;
  BuildDirname;
  gCurrFilename := StringPtr(kCurApName)^;
  ioFDirIndex   := 0;
  ioDirId       := 0;
  ioVRefNum     := gCurrWDRefNum;
  gError        := PBGetFInfo(@fPBRec,FALSE);
  IF  (gError <> NoErr) THEN
    ErrorOSErr(‘Couldn’t get own FInfo’);
  gCurrFInfo    := ioFlFndrInfo;
  gActiveSelf   := TRUE;
  gActiveSys    := FALSE;
  gWDPBRec.ioWDProcID := $50617472; {‘Patr’}
  gInitdFlag    := kPatsInitd;
  END;
END;
PROCEDURE  PatrolDir(pDrDirId:  LONGINT);
VAR
  sIndex:    INTEGER;
BEGIN
IF  gOption[eTrace] THEN
  TraceNbr(‘PatrolDir ‘,pDrDirId);
WITH gPBs,fCPBRec DO
  BEGIN
  IF  (gInitdFlag <> kPatsInitd) THEN
    BEGIN { shouldn’t happen }
    ErrorOSErr(‘InitPatrols not done’);
    EXIT(PatrolDir);
    END;
  IF  gHFS THEN
    BEGIN
    gCurrDirId := pDrDirId;
    GetActualDirId(pDrDirId);
    IF  (gError <> NoErr) THEN
      EXIT(PatrolDir);
    END
  ELSE
    gCurrDirId := 2;
  BuildDirname;
  DirectoryBegins;
  sIndex  := 1;
  REPEAT
    gCurrIndex  := sIndex;
    ioFDirIndex := sIndex;
    ioDrDirId   := 0;
    ioVRefNum   := gCurrWDRefNum;
    gError := PBGetFInfo(@fPBRec,FALSE);
    IF  (gError = NoErr) THEN
      BEGIN
      gCurrFInfo  := ioFlFndrInfo;
      CallProcessFile;
      END
    ELSE IF  (gError <> fnfErr) THEN
      BEGIN
      ErrorOSErr(‘Couldn’t get a file’);
      EXIT(PatrolDir);
      END;
    IF  NOT(gCurrFileDeleted) THEN
      INC(sIndex);
  UNTIL (gError <> NoErr) OR gAbortPatrol;
  IF  gAbortPatrol THEN
    EXIT(PatrolDir);
  IF  (gError <> fnfErr) THEN
    BEGIN
    ErrorOSErr(‘Error at end of files’);
    EXIT(PatrolDir);
    END;
  gError := NoErr;
  IF  gHFS AND NOT(gOnly1Deep) THEN
    BEGIN
    sIndex := 1;
    REPEAT
      gCurrIndex  := sIndex;
      ioFDirIndex := sIndex;
      ioDrDirId   := 0;
      ioVRefNum   := gCurrWDRefNum;
      gError := PBGetCatInfo(@fCPBRec,FALSE);
      IF  (gError = NoErr) THEN
        BEGIN
        IF  BTst(ORD4(ioFlAttrib),4) THEN
          BEGIN
          FloatWDDeeper (pDrDirId);
          IF  (gError <> NoErr) THEN
            EXIT(PatrolDir);
          PatrolDir(ioDrDirId);
          IF  (gError <> NoErr)
          AND (pDrDirId <> 0)
          AND (pDrDirId <> 2) THEN
            EXIT(PatrolDir);
          FloatWDShallower(pDrDirId);
          IF  (gError <> NoErr) THEN
            EXIT(PatrolDir);
          END;
        END
      ELSE IF  (gError <> fnfErr) THEN
        BEGIN
        ErrorOSErr(‘Couldn’t get a dir’);
        EXIT(PatrolDir);
        END;
      INC(sIndex);
    UNTIL (gError <> NoErr) OR gAbortPatrol;
    IF  gAbortPatrol THEN
      EXIT(PatrolDir);
    IF  (gError <> fnfErr) THEN
      BEGIN
      ErrorOSErr(‘Error at end of subdirs’);
      EXIT(PatrolDir);
      END;
    gError := NoErr;
    gCurrDirId := pDrDirId;
    GetActualDirId(pDrDirId);
    IF  (gError <> NoErr) THEN
      EXIT(PatrolDir);
    BuildDirname;
    END;
  DirectoryEnds;
  END;
END;
PROCEDURE  PatrolDirectories(pOnly1Deep:BOOLEAN);
BEGIN
IF  gOption[eTrace] THEN
  Trace(‘PatrolDirectories ‘);
WITH gPBs,fPBRec,fCPBRec,gSFRep DO
  BEGIN
  IF  (gInitdFlag <> kPatsInitd) THEN
    BEGIN { shouldn’t happen }
    ErrorOSErr(‘InitPatrols not done’);
    EXIT(PatrolDirectories);
    END;
  gOnly1Deep := pOnly1Deep;
  SFGetFile
    (gSFGetPt,’’,NIL,-1,gSFLst,NIL,gSFRep);
  WHILE good DO
    BEGIN
    IF  gHFS THEN
      BEGIN
      ioVRefNum   := gSFRep.vRefNum;
      ioVolIndex  := 0;
      gError  := PBHGetVInfo(@fPBRec,FALSE);
      IF  (gError <> NoErr) THEN
        BEGIN
        ErrorOSErr(‘Couldn’t get own VInfo’);
        LEAVE;
        END;
      gCurrVRefNum := ioVRefNum;
      END
    ELSE
      gCurrVRefNum := vRefNum;
    gCurrWDRefNum  := vRefNum;
    gOrigWDRefNum  := vRefNum;
    PatrolBegins;
    PatrolDir(0);
    PatrolEnds;
    IF  (gError <> NoErr) THEN
      LEAVE;
    SFGetFile
      (gSFGetPt,’’,NIL,-1,gSFLst,NIL,gSFRep);
    END;
  gError  := NoErr;
  END;
END;
PROCEDURE  PatrolEverything;
VAR
  sIndex:    INTEGER;
BEGIN
IF  gOption[eTrace] THEN
  Trace(‘PatrolEverything ‘);
WITH gPBs,fPBRec,fCPBRec DO
  BEGIN
  IF  (gInitdFlag <> kPatsInitd) THEN
    BEGIN { shouldn’t happen }
    ErrorOSErr(‘InitPatrols not done’);
    EXIT(PatrolEverything);
    END;
  gOnly1Deep := FALSE;
  PatrolBegins;
  ioVRefNum     := 0;
  sIndex        := 1;
  REPEAT
    gCurrIndex  := sIndex;
    ioVolIndex  := sIndex;
    gError      := PBGetVInfo(@fPBRec,FALSE);
    IF  (gError = NoErr) THEN
      BEGIN
      gCurrVRefNum  := ioVRefNum;
      gCurrWDRefNum := ioVRefNum;
      gOrigWDRefNum := ioVRefNum;
      PatrolDir(2);
      IF  (gError <> NoErr) THEN
        EXIT(PatrolEverything);
      INC(sIndex);
      END
    ELSE IF  (gError <> nsvErr) THEN
      BEGIN
      ErrorOSErr(‘Couldn’t get a volume’);
      EXIT(PatrolEverything);
      END;
  UNTIL gError <> NoErr;
  IF  (gError <> nsvErr) THEN
    BEGIN
    ErrorOSErr(‘Error at end of volumes’);
    EXIT(PatrolEverything);
    END;
  gError  := NoErr;
  PatrolEnds;
  END;
END;
PROCEDURE  PatrolFiles;
VAR
  sPrevDirId:   LONGINT;
  sPrevVRefNum: INTEGER;
  sPrevWDRefNum:INTEGER;
  PROCEDURE  CallPrevDirEnd;
  VAR
    sTempDirId:   LONGINT;
    sTempVRefNum: INTEGER;
    sTempWDRefNum:INTEGER;
  BEGIN
  IF  (sPrevWDRefNum <> 0) THEN
    BEGIN
    sTempDirId    := gCurrDirId;
    sTempVRefNum  := gCurrVRefNum;
    sTempWDRefNum := gCurrWDRefNum;
    gCurrDirId    := sPrevDirId;
    gCurrVRefNum  := sPrevVRefNum;
    gCurrWDRefNum := sPrevWDRefNum;
    DirectoryEnds;
    gCurrDirId    := sTempDirId;
    gCurrVRefNum  := sTempVRefNum;
    gCurrWDRefNum := sTempWDRefNum;
    END;
  END;
BEGIN
IF  gOption[eTrace] THEN
  Trace(‘PatrolFiles ‘);
WITH gPBs,fPBRec,fCPBRec,gSFRep DO
  BEGIN
  IF  (gInitdFlag <> kPatsInitd) THEN
    BEGIN { shouldn’t happen }
    ErrorOSErr(‘InitPatrols not done’);
    EXIT(PatrolFiles);
    END;
  PatrolBegins;
  sPrevWDRefNum := 0;
  SFGetFile
    (gSFGetPt,’’,NIL,-1,gSFLst,NIL,gSFRep);
  WHILE good DO
    BEGIN
    IF  gHFS THEN
      BEGIN
      ioVRefNum   := gSFRep.vRefNum;
      ioDrDirId   := 0;
      ioFDirIndex := -1;
      gError := PBGetCatInfo(@fPBRec,FALSE);
      IF  (gError <> NoErr) THEN
        BEGIN
        ErrorOSErr(‘Couldn’t get DirId’);
        LEAVE;
        END;
      gCurrDirId  := ioDrDirId;
      ioVolIndex  := 0;
      gError := PBHGetVInfo(@fPBRec,FALSE);
      IF  (gError <> NoErr) THEN
        BEGIN
        ErrorOSErr(‘Couldn’t get VInfo’);
        LEAVE;
        END;
      gCurrVRefNum := ioVRefNum;
      END
    ELSE
      BEGIN
      gCurrDirId   := 2;
      gCurrVRefNum := vRefNum;
      END;
    gCurrFilename := fName;
    gCurrIndex    := 0;
    gCurrWDRefNum := vRefNum;
    IF  (sPrevWDRefNum <> gCurrWDRefNum) THEN
      BEGIN
      CallPrevDirEnd;
      BuildDirname;
      DirectoryBegins;
      sPrevDirId      := gCurrDirId;
      sPrevVRefNum    := gCurrVRefNum;
      sPrevWDRefNum   := gCurrWDRefNum;
      END;
    ioDrDirId   := gCurrDirId;
    ioFDirIndex := gCurrIndex;
    ioVRefNum   := gCurrWDRefNum;
    gError := PBGetFInfo(@fPBRec,FALSE);
    IF  (gError <> NoErr) THEN
      BEGIN
      ErrorOSErr(‘Couldn’t get FInfo’);
      LEAVE;
      END;
    gCurrFInfo  := ioFlFndrInfo;
    CallProcessFile;
    IF  gAbortPatrol THEN
      LEAVE;
    SFGetFile
      (gSFGetPt,’’,NIL,-1,gSFLst,NIL,gSFRep);
    END;
  gError  := NoErr;
  CallPrevDirEnd;
  PatrolEnds;
  END;
END;
END.
“SecurityPatrol.p”
PROGRAM  SecurityPatrol(OUTPUT);
USES
  MemTypes,QuickDraw,OSIntf,ToolIntf,PackIntf,CodeSizeLimits,Globals,
  MainDlog,PasLibIntf,Patrol;
{$R-}
CONST
  kPreprocessSelf     = TRUE;
  kAwaitVerification  = FALSE;
VAR
  gInitSecPatDone,gRptFileOpen:    BOOLEAN;
  gScrDmpEnbPtr:      Ptr;
  gScrDmpEnbSave:     SignedByte;
  o:                  Text;
PROCEDURE  PreprocessSelf;           FORWARD;
PROCEDURE  Wryte(pStr:     Str255);                FORWARD;
PROCEDURE  WryteChar(pChar:    CHAR);              FORWARD;
PROCEDURE  WryteEoln;                FORWARD;
PROCEDURE  WryteLn(pStr:     Str255);              FORWARD;
PROCEDURE  WryteNbr(pNbr:LONGINT;pNbrDigits:INTEGER);FORWARD;
PROCEDURE  WryteType(pType:    ResType);           FORWARD;
{$Z*}
PROCEDURE  ExitSecurityPatrol;
BEGIN
IF  gOption[eTrace] THEN
  Trace(‘ExitSecurityPatrol’);
gScrDmpEnbPtr^ := gScrDmpEnbSave;
IF  gRptFileOpen THEN
  Close(o);
ExitToShell;
END;
{$Z-}
PROCEDURE  InitSecurityPatrol;
VAR
  sConfig:   LONGINT;
BEGIN
MaxApplZone;
MoreMasters; MoreMasters; MoreMasters;
gInitSecPatDone := FALSE;
gRptFileOpen    := FALSE;
gScrDmpEnbPtr   := Ptr(kScrDmpEnb);
gScrDmpEnbSave  := gScrDmpEnbPtr^;
gScrDmpEnbPtr^  := 0;
Textbook(@thePort);
Write  (‘SecurityPatrol is a Mac virus ‘);
Write  (‘detector.  ‘);
TextFace([bold,extend]);
WriteLn(‘Use at your own risk.  ‘);
TextFace([]);
Write  (‘The Save As... dialog below is ‘);
WriteLn(‘to save error reports.’);
PLFlush(OUTPUT);
PauseBriefly;
InitGlobals;
gOption[eBeeps]   := TRUE; {override }
gDisabled[eFgPrC] := TRUE; { defaults}
InitMainDlog;
InitPatrols;
sConfig := BAnd(ORD4(Ptr(kSPConfig)^),$F);
IF  (sConfig = useFree)
OR  (sConfig = useAsync) THEN
  SFPutFile
    (gSFPutPt,’Filename, or cancel to print’,
    ‘SecurityPatrol Report’,NIL,gSFRep)
ELSE
  SFPutFile
    (gSFPutPt,’Filename, or cancel to quit’,
    ‘SecurityPatrol Report’,NIL,gSFRep);
WITH gSFRep DO
  IF good THEN
    BEGIN
    gCurrWDRefNum := vRefNum;
    BuildDirname;
    ReWrite(o,CONCAT(gCurrDirname,fName));
    END
  ELSE
    IF  (sConfig = useFree)
    OR  (sConfig = useAsync) THEN
      ReWrite(o,’PRINTER:’)
    ELSE
      BEGIN
      WriteLn(‘Run cancelled.’);
      PLFlush(OUTPUT);
      PauseBriefly;
      ExitSecurityPatrol;
      END;
gRptFileOpen := TRUE;
Wryte  (‘This copy of Security Patrol 1.0 ‘);
Wryte  (‘is being maintained by ‘);
TextFace([bold,extend]);
gPgmrname := ‘<<your name here>>’;
WryteLn(gPgmrname);
TextFace([]);
Wryte  (‘The following run was done on ‘);
GetTime(gDateTimeRec);
WITH gDateTimeRec DO
  BEGIN
  year := year mod 100;
  WryteNbr (month,2);
  WryteChar(‘/’);
  WryteNbr (day,  2);
  WryteChar(‘/’);
  WryteNbr (year, 2);
  Wryte    (‘ at ‘);
  WryteNbr (hour, 2);
  WryteChar(‘:’);
  IF  minute < 10 THEN
    WryteChar(‘0’);
  WryteNbr (minute,1);
  WryteLn  (‘.’);
  END;
IF  kPreprocessSelf THEN
  PreprocessSelf
ELSE
  WryteLn(‘Didn’t perform self-tests.’);
gInitSecPatDone := TRUE;
END;
PROCEDURE  PreprocessSelf;
VAR
  i:          INTEGER;
  sC1Ptr,sEntActual,sEntShouldB,sOffActual,sOffShouldB:LONGINT;
  sResType:   ResType;
  sSave:      TMainOpt;
  PROCEDURE  Abort(pStr:     Str255);
  BEGIN
  ErrorBegins(pStr);
  WryteEoln;
  CommentBegins;
  WryteLn(‘Assuming infected.’);
  CommentBegins;
  Wryte  (‘Contact ‘);
  Wryte  (gPgmrname);
  WryteLn(‘ to be sure.’);
  CommentBegins;
  Wryte  (‘(These msgs apply to ‘);
  Wryte  (gCurrFilename);
  WryteLn(‘ itself, not to your system.)’);
  CommentBegins;
  gOption[eAwait] := TRUE;
  gOption[eBeeps] := TRUE;
  ErrorEnds(4);
  ExitSecurityPatrol;
  END;
BEGIN
BlockMove(@gOption,@sSave,SIZEOF(TMainOpt));
ZeroOut  (@gOption,       SIZEOF(TMainOpt));
gOption[eRmVir] := TRUE;
{ gOption[eTrace] := TRUE; }
IF  gOption[eTrace] THEN
  Trace(‘PreprocessSelf’);
GetCodeSizeLimits;
GetRsrc(@gCode0,’CODE’,0,ResId);
IF  (gCode0.fFlag <> kRsrcHdlValid) THEN
  Abort(‘Unable to get own CODE 0’);
IF  NOT(Code0IsValid) THEN
  Abort(‘Found unexpected CODE 0 header’);
LookForKnownViruses;
FOR i := 1 TO Count1Types DO
  BEGIN
  Get1IndType(sResType,i);
  IF  (sResType = ‘SIZE’) THEN
    BEGIN
    IF  (Count1Resources(‘SIZE’) > 1) THEN
      Abort(‘Too many SIZE resources’);
    GetRsrc(@gCurrRsrc,’SIZE’,1,Index);
    IF  (gCurrRsrc.fSize > 10) THEN
      Abort(‘SIZE resource too large’);
    ReleaseRsrc(@gCurrRsrc);
    END
  ELSE IF (sResType <> ‘CODE’) THEN
    BEGIN
    ErrorBegins(‘Found a rsrc of type ‘);
    WryteType(sResType);
    ErrorEnds(0);
    Abort    (‘Only CODE and SIZE allowed’);
    END;
  END;
WITH TJTHdl(gCode0.fHdl)^^,fJTEntry[1] DO
  BEGIN
  IF  (fNbrBytesInTable <> gJTSize) THEN
    BEGIN
    ErrorBegins(‘Jump table size is ‘);
    WryteNbr (fNbrBytesInTable,1);
    Wryte    (‘, should be ‘);
    WryteNbr (gJTSize,1);
    ErrorEnds(0);
    Abort    (‘Invalid Jump Table size’);
    END;
  IF  NOT(JTEIsValid(@fJTEntry[1])) THEN
    Abort(‘Invalid Jump Table entry’);
  IF  (fSegId  <> 1) THEN
    BEGIN
    ErrorBegins(‘Enters at CODE ‘);
    WryteNbr(fSegId,1);
    Wryte   (‘, should enter at CODE 1’);
    ErrorEnds(0);
    Abort   (‘Invalid start address’);
    END;
  sOffActual := 4 + fOffset;
  END;
WITH gCurrRsrc DO
  BEGIN
  GetRsrc(@gCurrRsrc,’CODE’,1,ResId);
  IF  (fFlag <> kRsrcHdlValid) THEN
    Abort(‘Couldn’t look at own CODE 1');
  sC1Ptr      := BAND($00FFFFFF,ORD4(fHdl^));
  sEntActual := sC1Ptr + sOffActual;
  IF  (TWordPtr(sEntActual)^ = $4EFA) THEN
    BEGIN
    sOffActual := 
      sOffActual+2+TWordPtr(sEntActual+2)^;
    sEntActual := sC1Ptr + sOffActual;
    END;
  sEntShouldB := gEntryPoint;
  sOffShouldB := sEntShouldB - sC1Ptr;
  IF  (sEntActual <> sEntShouldB) THEN
    BEGIN
    ErrorBegins(‘Invalid start address’);
    WryteEoln;
    CommentBegins;
    Wryte   (‘Enters at address $’);
    ShortHexDump(@sEntActual,4);
    Wryte   (‘ (CODE 1 + $’);
    ShortHexDump(@sOffActual,4);
    Wryte   (‘) --> $’);
    ShortHexDump(Ptr(sEntActual),4);
    WryteEoln;
    CommentBegins;
    Wryte   (‘Should enter at   $’);
    ShortHexDump(@sEntShouldB,4);
    Wryte   (‘ (CODE 1 + $’);
    ShortHexDump(@sOffShouldB,4);
    Wryte   (‘) --> $’);
    ShortHexDump(Ptr(sEntShouldB),4);
    WryteEoln;
    CommentBegins;
    Wryte   (‘CODE 1 begins at  $’);
    ShortHexDump(@sC1Ptr,4);
    ErrorEnds(0);
    Abort   (‘This is not a user error’);
    END;
  ReleaseRsrc(@gCurrRsrc);
  END;
ReleaseRsrc(@gCode0);
IF  (Count1Resources(‘CODE’)>gMaxCode+1) THEN
  Abort(‘Too many CODE resources.’);
IF  kAwaitVerification THEN
  BEGIN
  ErrorBegins(‘The following are the ‘);
  Wryte  (‘“fingerprints” of ‘);
  Wryte  (gCurrFilename);
  Wryte  (‘ itself:’);
  ErrorEnds(0);
  END;
FOR i := 0 TO gMaxCode DO
  WITH gCurrRsrc DO
    BEGIN
    GetRsrc(@gCurrRsrc,’CODE’,i,ResId);
    IF  (fFlag <> kRsrcHdlValid) THEN
      Abort(‘Couldn’t look at next CODE’);
    IF  (fSize > gSizeLimit[i]) THEN
      BEGIN
      ErrorBegins(‘Failed a CODE size test’);
      WryteEoln;
      CommentRsrcBegins(@gCurrRsrc);
      Wryte   (‘ size is ‘);
      WryteNbr(fSize,1);
      Wryte   (‘, which exceeds its ‘);
      WryteNbr(gSizeLimit[i],1);
      WryteLn (‘ size limit.’);
      Abort(‘May contain an imbedded virus’);
      END;
    IF  kAwaitVerification THEN
      BEGIN
      ProcessCurrRsrc;
      CommentFgPrRsrc(@gCurrRsrc);
      END;
    ReleaseRsrc(@gCurrRsrc);
    END;
IF  kAwaitVerification THEN
  BEGIN
  ErrorBegins(‘Time to make a decision:’);
  WryteEoln;
  CommentBegins;
  WryteLn(‘Verify fingerprints if you can’);
  CommentBegins;
  WryteLn(‘Press command-period to abort’);
  CommentBegins;
  WryteLn(‘Press any other key to continue’);
  CommentBegins;
  gOption[eAwait] := TRUE;
  ErrorEnds(0);
  IF  gAbortPatrol THEN
    BEGIN
    PauseBriefly;
    ExitSecurityPatrol;
    END;
  END;
WryteLn(‘Passed all current self-tests.’);
PauseBriefly;
BlockMove(@sSave,@gOption,SIZEOF(TMainOpt));
END;
{$Z*}
PROCEDURE  WriteFilenameToReport;
BEGIN
WITH gReportFlags DO
  IF  NOT(fWroteFilename) THEN
    BEGIN
    IF  NOT(fWroteDirname) THEN
      BEGIN
      WriteLn(o,gCurrDirname);
      fWroteDirname := TRUE;
      END;
    Write(o,gInd,gCurrFilename);
    IF  gActiveSelf THEN
      BEGIN
      Write(o,’ (Active Self)’);
      IF  gInitSecPatDone
      AND NOT(kProcessSelf) THEN
        Write(o,’ skipped’);
      END
    ELSE IF gActiveSys THEN
      Write(o,’ (Active System)’);
    WriteLn(o);
    fWroteFilename := TRUE;
    END;
END;
PROCEDURE  WriteFilenameToScreen;
BEGIN
WITH gScreenFlags DO
  IF  NOT(fWroteFilename) THEN
    BEGIN
    IF  NOT(fWroteDirname) THEN
      BEGIN
      WriteLn(gCurrDirname);
      fWroteDirname := TRUE;
      END;
    Write(gInd,gCurrFilename);
    IF  gActiveSelf THEN
      BEGIN
      Write(‘ (Active Self)’);
      IF  gInitSecPatDone
      AND NOT(kProcessSelf) THEN
        Write(‘ skipped’);
      END
    ELSE IF gActiveSys THEN
      Write(‘ (Active System)’);
    WriteLn;
    PLFlush(OUTPUT);
    fWroteFilename := TRUE;
    END;
END;
PROCEDURE  Wryte(pStr:     Str255);
BEGIN
Write(pStr);
IF  gRptFileOpen THEN
  Write(o,pStr);
END;
PROCEDURE  WryteChar(pChar:    CHAR);
BEGIN
Write(pChar);
IF  gRptFileOpen THEN
  Write(o,pChar);
END;
PROCEDURE  WryteEoln;
BEGIN
WriteLn;
PLFlush(OUTPUT);
IF  gRptFileOpen THEN
  BEGIN
  WriteLn(o);
  END;
END;
PROCEDURE  WryteFilename;
BEGIN
IF  gRptFileOpen THEN
  WriteFilenameToReport;
WriteFilenameToScreen;
END;
PROCEDURE  WryteFilenameToScreenOnlyForNow;
BEGIN
WriteFilenameToScreen;
END;
PROCEDURE  WryteLn(pStr:     Str255);
BEGIN
WriteLn(pStr);
PLFlush(OUTPUT);
IF  gRptFileOpen THEN
  BEGIN
  WriteLn(o,pStr);
  END;
END;
PROCEDURE  WryteNbr(pNbr:     LONGINT;pNbrDigits:INTEGER);
BEGIN
Write(pNbr:pNbrDigits);
IF  gRptFileOpen THEN
  Write(o,pNbr:pNbrDigits);
END;
PROCEDURE  WryteType(pType:    ResType);
BEGIN
Write(pType);
IF  gRptFileOpen THEN
  Write(o,pType);
END;
PROCEDURE  zzSecurityPatrol;
BEGIN
END;
BEGIN
InitSecurityPatrol;
WHILE TRUE DO
  BEGIN
  gAbortPatrol := FALSE;
  CASE MainDlogWorkRequested OF
  eDirs:    PatrolDirectories (FALSE);
  eDiry:    PatrolDirectories (TRUE);
  eEvery:   PatrolEverything;
  eFiles:   PatrolFiles;
  OTHERWISE LEAVE;
  END; {CASE}
  END;
WryteEoln;
WryteLn(‘*******************************’);
WryteEoln;
WryteLn (‘Totals over all patrols:’);
ListCounts(@gTotals);
ExitSecurityPatrol;
END.
SecurityPatrol.Link (TML  2.5 only)
!PAS$Xfer

SecurityPatrol
PAS$Library
MacIntf
MacIntfGlue
BitProcs
CodeSizeLimits
Globals
MainDlog
PasLibIntf
Patrol
<
Globals/Globals
<
BitProcs/Fingerprint
Globals/Fingerprint
/End
SecurityPatrol.Proj (TML II Only)
SecurityPatrol.p.o ƒ 
    CodeSizeLimits.p 
    Fingerprint.ipas 
    Globals.p 
 MainDlog.p 
 Patrol.p 
 SecurityPatrol.p
 TMLPascal SecurityPatrol.p
CodeSizeLimits.p.o ƒ 
  CodesSizeLimits.p
 TMLPascal CodeSizeLimits.p
Globals.p.o ƒ 
 Fingerprint.ipas 
 Globals.p
         TMLPascal Globals.p
MainDlog.p.o ƒ 
 Globals.p 
 MainDlog.p
 TMLPasal MainDlog.p
Patrol.p.o ƒ 
 Globals.p 
 Patrol.p
 TMLPascal Patrol.p

SecurityPatrol ƒƒ     
 CodeSizeLimits.p.o 
 Globals.p.o 
 MainDlog.p.o 
 Patrol.p.o 
    SecurityPatrol.p.o
        Link -w -t ‘APPL’ -c ‘????’ 
            -ra Fingerprint=$14 
            -ra Globals=$14 
            -ra Main=$14 
            SecurityPatrol.p.o 
 CodeSizeLimits.p.o 
 Globals.p.o 
 MainDlog.p.o 
 Patrol.p.o 
 "{Libraries}"Runtime.o 
 "{Libraries}"Interface.o 
 "{Libraries}"ObjLib.o 
 "{TMLPLibraries}"TMLPasLib.o 
 "{TMLPLibraries}"SANELib.o 
 -o SecurityPatrol

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

Latest Forum Discussions

See All

Aether Gazer unveils Chapter 16 of its m...
After a bit of maintenance, Aether Gazer has released Chapter 16 of its main storyline, titled Night Parade of the Beasts. This big update brings a new character, a special outfit, some special limited-time events, and, of course, an engaging... | Read more »
Challenge those pesky wyverns to a dance...
After recently having you do battle against your foes by wildly flailing Hello Kitty and friends at them, GungHo Online has whipped out another surprising collaboration for Puzzle & Dragons. It is now time to beat your opponents by cha-cha... | Read more »
Pack a magnifying glass and practice you...
Somehow it has already been a year since Torchlight: Infinite launched, and XD Games is celebrating by blending in what sounds like a truly fantastic new update. Fans of Cthulhu rejoice, as Whispering Mist brings some horror elements, and tests... | Read more »
Summon your guild and prepare for war in...
Netmarble is making some pretty big moves with their latest update for Seven Knights Idle Adventure, with a bunch of interesting additions. Two new heroes enter the battle, there are events and bosses abound, and perhaps most interesting, a huge... | Read more »
Make the passage of time your plaything...
While some of us are still waiting for a chance to get our hands on Ash Prime - yes, don’t remind me I could currently buy him this month I’m barely hanging on - Digital Extremes has announced its next anticipated Prime Form for Warframe. Starting... | Read more »
If you can find it and fit through the d...
The holy trinity of amazing company names have come together, to release their equally amazing and adorable mobile game, Hamster Inn. Published by HyperBeard Games, and co-developed by Mum Not Proud and Little Sasquatch Studios, it's time to... | Read more »
Amikin Survival opens for pre-orders on...
Join me on the wonderful trip down the inspiration rabbit hole; much as Palworld seemingly “borrowed” many aspects from the hit Pokemon franchise, it is time for the heavily armed animal survival to also spawn some illegitimate children as Helio... | Read more »
PUBG Mobile teams up with global phenome...
Since launching in 2019, SpyxFamily has exploded to damn near catastrophic popularity, so it was only a matter of time before a mobile game snapped up a collaboration. Enter PUBG Mobile. Until May 12th, players will be able to collect a host of... | Read more »
Embark into the frozen tundra of certain...
Chucklefish, developers of hit action-adventure sandbox game Starbound and owner of one of the cutest logos in gaming, has released their roguelike deck-builder Wildfrost. Created alongside developers Gaziter and Deadpan Games, Wildfrost will... | Read more »
MoreFun Studios has announced Season 4,...
Tension has escalated in the ever-volatile world of Arena Breakout, as your old pal Randall Fisher and bosses Fred and Perrero continue to lob insults and explosives at each other, bringing us to a new phase of warfare. Season 4, Into The Fog of... | Read more »

Price Scanner via MacPrices.net

Free iPhone 15 plus Unlimited service for $60...
Boost Infinite, part of MVNO Boost Mobile using AT&T and T-Mobile’s networks, is offering a free 128GB iPhone 15 for $60 per month including their Unlimited service plan (30GB of premium data).... Read more
$300 off any new iPhone with service at Red P...
Red Pocket Mobile has new Apple iPhones on sale for $300 off MSRP when you switch and open up a new line of service. Red Pocket Mobile is a nationwide MVNO using all the major wireless carrier... Read more
Clearance 13-inch M1 MacBook Airs available a...
Apple has clearance 13″ M1 MacBook Airs, Certified Refurbished, available for $759 for 8-Core CPU/7-Core GPU/256GB models and $929 for 8-Core CPU/8-Core GPU/512GB models. Apple’s one-year warranty is... Read more
Updated Apple MacBook Price Trackers
Our Apple award-winning MacBook Price Trackers are continually updated with the latest information on prices, bundles, and availability for 16″ and 14″ MacBook Pros along with 13″ and 15″ MacBook... Read more
Every model of Apple’s 13-inch M3 MacBook Air...
Best Buy has Apple 13″ MacBook Airs with M3 CPUs in stock and on sale today for $100 off MSRP. Prices start at $999. Their prices are the lowest currently available for new 13″ M3 MacBook Airs among... Read more
Sunday Sale: Apple iPad Magic Keyboards for 1...
Walmart has Apple Magic Keyboards for 12.9″ iPad Pros, in Black, on sale for $150 off MSRP on their online store. Sale price for online orders only, in-store price may vary. Order online and choose... Read more
Apple Watch Ultra 2 now available at Apple fo...
Apple has, for the first time, begun offering Certified Refurbished Apple Watch Ultra 2 models in their online store for $679, or $120 off MSRP. Each Watch includes Apple’s standard one-year warranty... Read more
AT&T has the iPhone 14 on sale for only $...
AT&T has the 128GB Apple iPhone 14 available for only $5.99 per month for new and existing customers when you activate unlimited service and use AT&T’s 36 month installment plan. The fine... Read more
Amazon is offering a $100 discount on every M...
Amazon is offering a $100 instant discount on each configuration of Apple’s new 13″ M3 MacBook Air, in Midnight, this weekend. These are the lowest prices currently available for new 13″ M3 MacBook... Read more
You can save $300-$480 on a 14-inch M3 Pro/Ma...
Apple has 14″ M3 Pro and M3 Max MacBook Pros in stock today and available, Certified Refurbished, starting at $1699 and ranging up to $480 off MSRP. Each model features a new outer case, shipping is... Read more

Jobs Board

Omnichannel Associate - *Apple* Blossom Mal...
Omnichannel Associate - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Read more
Operations Associate - *Apple* Blossom Mall...
Operations Associate - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Read more
Cashier - *Apple* Blossom Mall - JCPenney (...
Cashier - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Blossom Mall Read more
IT Systems Engineer ( *Apple* Platforms) - S...
IT Systems Engineer ( Apple Platforms) at SpaceX Hawthorne, CA SpaceX was founded under the belief that a future where humanity is out exploring the stars is Read more
*Apple* Systems Administrator - JAMF - Activ...
…**Public Trust/Other Required:** None **Job Family:** Systems Administration **Skills:** Apple Platforms,Computer Servers,Jamf Pro **Experience:** 3 + years of Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.