TUCoPS :: HP Unsorted I :: bx2973.htm

Insufficient argument validation of hooked SSDT functions on multiple Antivirus and Firewalls
CORE-2008-0320 - Insufficient argument validation of hooked SSDT functions on multiple Antivirus and Firewalls
CORE-2008-0320 - Insufficient argument validation of hooked SSDT functions on multiple Antivirus and Firewalls



-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

      Core Security Technologies - CoreLabs Advisory
http://www.coresecurity.com/corelabs/ 

  Insufficient argument validation of hooked SSDT functions
          on multiple Antivirus and Firewalls


*Advisory Information*

Title: Insufficient argument validation of hooked SSDT functions on
multiple Antivirus and Firewalls
Advisory ID: CORE-2008-0320
Advisory URL: http://www.coresecurity.com/?action=item&id=2249 
Date published: 2008-04-28
Date of last update: 2008-04-28
Vendors contacted: BitDefender, Comodo, Sophos and Rising
Release mode: Coordinated release (BitDefender, Comodo, Rising), User
release (Sophos)


*Vulnerability Information*

Class: Invalid memory reference
Remotely Exploitable: No
Locally Exploitable: Yes
Bugtraq ID: 28741 28742 28743 28744	
CVE Name: CVE-2008-1735 CVE-2008-1736 CVE-2008-1737 CVE-2008-1738	


*Vulnerability Description*

Insufficient argument validation of hooked SSDT functions on multiple
Antivirus and Firewalls (BitDefender Antivirus [1], Comodo Firewall [2],
Sophos Antivirus [3] and Rising Antivirus [4]) have been found that
could lead to a Denial of Service (DoS) and possibly to code execution
attacks. An attacker, utilizing these flaws, could be able to locally
reboot the whole system shutting down the firewall or anti-virus
protection. However, in some cases it may be possible to extend the
impact of these bugs, and they could lead to the execution of arbitrary
code in the privileged kernel mode.


*Vulnerable Packages*

. BitDefender Antivirus 2008 Build 11.0.11
. Comodo Firewall Pro 2.4.18.184
. Sophos Antivirus 7.0.5
. Rising Antivirus 19.60.0.0 and 19.66.0.0
. Older versions may be affected, but were not checked.


*Non-vulnerable Packages*

. BitDefender Antivirus 2008 builds available through automatic updates,
posterior to January 18th.
. Comodo Firewall Pro 3.0
. Rising Antivirus 20.38.20


*Vendor Information, Solutions and Workarounds*

1) BITDEFENDER ANTIVIRUS (BID 28741, CVE-2008-1735)

According to BitDefender, the flaw was not exploited by any malicious
application, and it was corrected through automatic updates. Information
on this issue can be found on BitDefender website at this location:
http://kb.bitdefender.com/KB419-en--Security-vulnerability-in-BitDefender-2008.html. 



2) COMODO FIREWALL PRO (BID 28742, CVE-2008-1736)

The vulnerability is fixed in Comodo Firewall Pro 3.0, available at:
http://www.personalfirewall.comodo.com/download_firewall.html 


3) SOPHOS ANTIVIRUS (BID 28743, CVE-2008-1737)

Vendor statement:

"Sophos Anti-Virus 7.x for Windows 2000, 2003 and XP is affected by this
vulnerability.

Non-vulnerable products from Sophos are earlier versions of Sophos
Anti-Virus for Windows, Sophos Anti-Virus for non-Windows platforms and
all other Sophos products.

The vulnerability is only exploitable if Runtime Behavioural Analysis is
switched on. Even then the exploit will only be effective if the end
user is using security settings that are lower than the defaults for
most web browsers today, or if the end user agrees to activate an
ActiveX or Java Applet from the webpage hosting the exploit.

Workarounds to avoid this vulnerability include:

a. Using the default security settings or higher on the latest version
of your chosen web browser. In line with general security best practice
we would also encourage end users not to download ActiveX or Java
Applets unless confident about their content.

b. Turning off the Runtime Behavioural Analysis functionality within
Sophos Anti-Virus (customers will still benefit from Sophos Behavioural
Genotype protection and other means of protecting endpoints against
malware).

N.B. Should an exploit be released into the wild, Sophos will deploy
protection against that exploit.

The fix for this vulnerability requires customers to reboot their
endpoints. Given the low severity of the vulnerability, to minimise
disruption to our customers Sophos will release the fix at the earliest
opportunity that coincides with a necessary reboot of the product."


4) RISING ANTIVIRUS (BID 28744, CVE-2008-1738)

A fixed version of Rising Antivirus can be downloaded from:
http://rsdownload.rising.com.cn/for_down/rsfree/ravolusrfree.exe 

All Rising customers can also update up to a patched version through
automatic updates.


*Credits*

These vulnerabilities (except the Rising one) were discovered by Damian
Saura, Anibal Sacco, Dario Menichelli, Norberto Kueffner, Andres Blanco
y Rodrigo Carvalho from Core Security Technologies, during Bugweek 2007.
The Rising vulnerability was discovered by Anibal Sacco from Core
Security Technologies exploit writers team.

These vulnerabilities were researched by Anibal Sacco and Damian Saura
from Core Security Technologies.


*Technical Description / Proof of Concept Code*

We have found that BitDefender Antivirus, Rising Antivirus, Comodo
Firewall and Sophos Antivirus have hooks that do not properly validate
the arguments of the hooked functions before accessing them, and lead to
the program trying to reference some invalid memory, leading in some
scenarios to a BSOD (Blue Screen of Death).

In our tests we used the kernel hooks probing tool BSODhook [5] in order
to find any kind of insufficient argument validation of hooked SSDT
functions. From Matousec paper [6]:

"Hooking kernel functions by modifying the System Service Descriptor
Table (SSDT) is a very popular method of implementation of additional
security features and is used frequently by personal firewalls and other
security and low-level software. Although undocumented and despised by
Microsoft, this technique can be implemented in a correct and stable
way. However, many software vendors do not follow the rules and
recommendations for kernel-mode code writing and many drivers that
implement SSDT hooking do not properly validate the parameters of the
hooking functions."

"Hooking SSDT functions requires extra caution. SSDT function handlers
are executed in the kernel mode but their callers are executed in the
user mode. Hence all function arguments come from the user mode. This is
why it is necessary to validate these arguments properly. Otherwise a
simple user call can easily crash the whole system. This bug usually
results in a system crash. However, it may happen that this bug is even
more dangerous and may lead to the execution of an arbitrary code in the
privileged kernel mode."

A local DoS attack, despite not being a very sophisticated intrusion
attack, could be used as an accessory under several scenarios. It is
commonly used by viruses as added feature, when the specific AV is
detected on the infected machine, crashing the system just to annoy. Or
by a human attacker, after a succesful remote intrusion with
unprivileged credentials to make a computer resource unavailable to its
intended users. Besides, this could be a very valuable resource when
trying to fake some service that answers broadcasts request like a DHCP,
allowing to start the service in another location replacing the original
one.


1) BITDEFENDER ANTIVIRUS (BID 28741, CVE-2008-1735)

BitDefender fails to validate the pointer to the 'CLIENT_ID' structure
provided to 'NtOpenProcess'. So, if we pass an invalid pointer, we will
crash the whole system.

/-----------

NtOpenProcess(PHANDLE ProcessHandle,
ACCESS_MASK AccessMask,
POBJECT_ATTRIBUTES ObjectAttributes,
PCLIENT_ID ClientId )

.text:00010ADE  push    0Ch
.text:00010AE0  push    offset stru_114E8
.text:00010AE5  call    __SEH_prolog
.text:00010AEA  call    KeGetCurrentThread
.text:00010AEF  xor     ebx, ebx
.text:00010AF1  cmp     [eax+140h], bl
.text:00010AF7  jz      short loc_10B0D
.text:00010AF9  call    PsGetCurrentProcessId
.text:00010AFE  call    PsGetCurrentProcessId
.text:00010B03  push    eax
.text:00010B04  call    sub_10724
.text:00010B09  test    eax, eax
.text:00010B0B  jnz     short loc_10B12
.text:00010B0D
.text:00010B0D loc_10B0D:                   ; CODE XREF: sub_10ADE+19_j
.text:00010B0D  push    [ebp+ClientId]
.text:00010B10  jmp     short loc_10B73

.text:00010B12
.text:00010B12 loc_10B12:                   ; CODE XREF: sub_10ADE+2D_j
.text:00010B12  mov     edi, [ebp+ClientId]
.text:00010B15  cmp     edi, ebx            ; Little check to avoid a
Null Pointer

- -----------/

Here it gets the pointer to the 'ClientId' value, and if it is non zero
('!= 0') it does not care where it is pointing to.

/-----------

.text:00010B17  jnz     short loc_10B1C
.text:00010B19  push    ebx
.text:00010B1A  jmp     short loc_10B73

.text:00010B1C
.text:00010B1C loc_10B1C:                   ; CODE XREF: sub_10ADE+39_j
.text:00010B1C  mov     [ebp+ms_exc.disabled], ebx
.text:00010B1F  mov     esi, [edi]          ; Here it crashes

- -----------/

It access to that memory, and if that is invalid memory the system will
crash.

/-----------

.text:00010B21                 mov     [ebp+var_1C], esi
.text:00010B24                 or      [ebp+ms_exc.disabled], 0FFFFFFFFh
.text:00010B28                 jmp     short loc_10B3B
.text:00010B28 sub_10ADE       endp

- -----------/


2) COMODO FIREWALL PRO (BID 28742, CVE-2008-1736)

In Comodo there are problems in the arguments validation of
'NtDeleteFile', 'NtCreateFile' and 'NtSetThreadContext' functions.
'NtDeleteFile' receives just one parameter, a pointer to an
'OBJECT_ATTRIBUTES' structure. These attributes would include the
'ObjectName' and the 'SECURITY_DESCRIPTOR', for example. This is the
hook placed by Comodo at 'NtDeleteFile'.

/-----------

NTDeleteFile (POBJECT_ATTRIBUTES ObjectAttributes)

.text:0001ACB0  push    1Ch
.text:0001ACB2  push    offset stru_1E3F0
.text:0001ACB7  call    __SEH_prolog
.text:0001ACBC  xor     ebx, ebx
.text:0001ACBE  inc     ebx
.text:0001ACBF  mov     [ebp+var_1C], ebx
.text:0001ACC2  xor     esi, esi
.text:0001ACC4  mov     [ebp+var_24], esi
.text:0001ACC7  mov     [ebp+var_20], ebx
.text:0001ACCA  mov     [ebp+var_28], esi
.text:0001ACCD  mov     [ebp+ms_exc.disabled], esi
.text:0001ACD0  call    ds:ExGetPreviousMode
.text:0001ACD6  mov     edi, [ebp+ObjectAttributes]

- -----------/

Here it does a lot of 'ProbeForRead' checks to see if the pointers of
the structure are valid. Nice! ('EDI' still has a pointer to the
'OBJECT_ATTRIBUTES' structure)

/-----------

....
.text:0001AD25  push    edi             ; ObjectAttributes
.text:0001AD26  call    sub_1A692       ; Here it passes the
OBJECT_ATTRIBUTES structure pointer to the next function.

sub_1A692
.text:0001A692  push    28h
.text:0001A694  push    offset stru_1E3C0
.text:0001A699  call    __SEH_prolog
.text:0001A69E  xor     edi, edi
....
.text:0001A6B3  mov     [ebp+ms_exc.disabled], edi
.text:0001A6B6  push    72747052h       ; Tag
.text:0001A6BB  mov     ebx, 400h
.text:0001A6C0  push    ebx             ; NumberOfBytes
.text:0001A6C1  push    1               ; PoolType
.text:0001A6C3  call    ds:ExAllocatePoolWithTag  ; Allocates memory to
hold the data retrieved by ZwQueryObject
.text:0001A6C9  mov     esi, eax
.text:0001A6CB  mov     [ebp+var_28], esi
.text:0001A6CE  cmp     esi, edi
.text:0001A6D0  jz      short loc_1A74F

.text:0001A6D2  mov     edi, [ebp+ObjectAttributes]
.text:0001A6D5  mov     eax, [edi+OBJECT_ATTRIBUTES.RootDirectory] ;
Here, the code retrieves the RootDirectory's field value from the
structure, controled by us.
.text:0001A6D8  test    eax, eax
.text:0001A6DA  jz      short loc_1A71B

.text:0001A6DC  push    0               ; ReturnLength
.text:0001A6DE  push    ebx             ; ObjectInformationLength
.text:0001A6DF  push    esi             ; ObjectInformation
; buffer where ZwQueryObject will put the object information

.text:0001A6E0  push    1               ; ObjectInformationClass
; Specifies an OBJECT_INFORMATION_CLASS value that determines the type
; of information returned in the ObjectInformation buffer. It's using
; an undocumented type (OBJECT_NAME_INFORMATION) which returns an
UNICODE_STRING structure
.text:0001A6E2  push    eax             ; ObjectHandle
; Now, the user-controlled handle 'll be used here to identify the
object by ZwQueryObject,
.text:0001A6E3  call    ds:ZwQueryObject
.text:0001A6E9  mov     [ebp+var_20], eax
.text:0001A6EC  test    eax, eax
.text:0001A6EE  jl      short loc_1A746

- -----------/

Here is where the problem shows up. The code does not properly validates
the data retrieved by 'ZwQueryObject', expecting an 'UNICODE_STRING'
structure. But it is possible to make multiple calls to the function
using different handlers to obtain a null structure crashing the system
when the code tries to dereference its 'Buffer' field.

/-----------

.text:0001A6F0  movzx   eax, [esi+UNICODE_STRING.Length]
.text:0001A6F3  shr     eax, 1
.text:0001A6F5  mov     ecx, [esi+UNICODE_STRING.Buffer]
.text:0001A6F8  movzx   eax, word ptr [ecx+eax*2-2] ; Here is the problem
.text:0001A6FD  mov     [ebp+var_30], eax
.text:0001A700  cmp     ax, 5Ch
.text:0001A704  jz      short loc_1A725

- -----------/


3) SOPHOS ANTIVIRUS (BID 28743, CVE-2008-1737)

Insufficient argument validation of hooked SSDT functions on Sophos lead
to a DoS. An attacker, utilizing this flaw, would be able to locally
reboot the whole system shutting down the Firewall or AV protection.
Although neither the vendor nor Core Security has found a means of
exploiting the flaw to execute arbitrary code, it has not been possible
to rule this out.

In Sophos AV there is a problem in the arguments validation of
'NtCreateKey' function.

/-----------

int __cdecl NtCreateKeyHook(PHANDLE pKeyHandle,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
ULONG TitleIndex,PUNICODE_STRING Class,
ULONG CreateOptions,
PULONG Disposition)

[...]
.text:0001C01C  push    4               ; Alignment
.text:0001C01E  push    18h             ; Length
.text:0001C020  mov     esi, [ebp+ObjectAttributes]
.text:0001C023  push    esi             ; Address
.text:0001C024  call    ds:ProbeForRead

- -----------/

Here it checks for 'ObjectAttributes' to be pointing to a valid address.

/-----------

.text:0001C02A  mov     eax, [esi+OBJECT_ATTRIBUTES.RootDirectory]
.text:0001C02D  mov     [ebp+Handle], eax
.text:0001C030  mov     esi, [esi+OBJECT_ATTRIBUTES.ObjectName]
.text:0001C033  mov     [ebp+pUnicodeString], esi

- -----------/

Now, it gets from 'OBJECT_ATTRIBUTES' a handle and a pointer to an
'UNICODE_STRING' structure.

/-----------

.text:0001C095  push    4
.text:0001C097  push    8
.text:0001C099  push    esi
.text:0001C09A  mov     ebx, ds:ProbeForRead
.text:0001C0A0  call    ebx             ; ProbeForRead, it checks the
pointer before the dereference.

.text:0001C0A2  mov     eax, dword ptr [esi+UNICODE_STRING.Length]
.text:0001C0A4  mov     dword ptr [ebp+stUnicodeString.Length], eax
.text:0001C0A7  mov     esi, [esi+UNICODE_STRING.Buffer]   ; And gets
from the UNICODE_STRING structure
; a pointer to the unicode buffer.
.text:0001C0AA  mov     [ebp+stUnicodeString.Buffer], esi
.text:0001C0AD  push    2               ; Alignment
.text:0001C0AF  shr     eax, 10h
.text:0001C0B2  push    eax             ; Length
.text:0001C0B3  push    esi             ; Address
.text:0001C0B4  call    ebx             ; ProbeForRead

- -----------/

It does the check, but here is the problem

/-----------

.text:0001C0B6  push    gdwValue
.text:0001C0BC  lea     eax, [ebp+stUnicodeString]
.text:0001C0BF  push    eax
.text:0001C0C0  push    [ebp+Object]
.text:0001C0C3  call    sub_1cb40

- -----------/

The problem relies in the function not properly checking the 'Length'
field of the 'UNICODE_STRING' structure. When doing the check,
'ProbeForRead' receives the length field of the structure as a parameter
without any kind of validation.

So, if we set this field to 0, 'ProbeForRead' will not raise any
exception even though we were passing it an invalid address. And it will
crash when trying to access to the desired invalid memory.

/-----------

sub_1cb40

[...]
.text:0001CB5E  xor     esi, esi
.text:0001CB60  mov     [ebp+ms_exc.disabled], esi
.text:0001CB63  mov     edi, [ebp+pUnicodeString]
.text:0001CB66  mov     eax, [edi+UNICODE_STRING.Buffer]

- -----------/

And here is where it will crash:

/-----------

.text:0001CB69  cmp     word ptr [eax], '\'  ; Reference the first
pointed byte

- -----------/


4) RISING ANTIVIRUS (BID 28744, CVE-2008-1738)

In Rising antivirus the code of the 'NtOpenProcess' hook does not
validates if the pointer to the structure

/-----------

typedef struct _CLIENT_ID {
HANDLE  UniqueProcess;
HANDLE  UniqueThread;}

- -----------/

is really pointing to mapped memory. So, when the code tries to
dereference the pointer to check the 'CLIENT_ID->UniqueProcess' value,
if it is pointing to invalid memory, will crash.

/-----------

NtOpenProcess( OUT PHANDLE ProcessHandle,
IN ACCESS_MASK AccessMask,
IN POBJECT_ATTRIBUTES ObjectAttributes,
IN PCLIENT_ID ClientId )

.text:00010EAA  push    ebp
.text:00010EAB  mov     ebp, esp
.text:00010EAD  push    esi
.text:00010EAE  mov     esi, offset Addend
.text:00010EB3  push    edi
.text:00010EB4  mov     ecx, esi            ; Addend
.text:00010EB6  call    ds:InterlockedIncrement
.text:00010EBC  call    PsGetCurrentProcessId
.text:00010EC1  cmp     eax, dword_11C8C
.text:00010EC7  jnz     short loc_10ECE
.text:00010EC9
.text:00010EC9 loc_10EC9:                   ; CODE XREF: sub_10EAA+37_j
.text:00010EC9  push    [ebp+ClientId]
.text:00010ECC  jmp     short loc_10EF0

.text:00010ECE
.text:00010ECE loc_10ECE:                   ; CODE XREF: sub_10EAA+1D_j
.text:00010ECE  call    PsGetCurrentProcessId
.text:00010ED3  mov     ecx, dword_11C80
.text:00010ED9  push    eax
.text:00010EDA  call    sub_11070
.text:00010EDF  test    al, al
.text:00010EE1  jnz     short loc_10EC9
.text:00010EE3  call    PsGetCurrentProcessId
.text:00010EE8  mov     edi, [ebp+ClientId] ; Here is the bug, if
ClientId is pointing to an invalid address
.text:00010EEB  cmp     eax, [edi]	        ; it will crash.
.text:00010EED  jnz     short loc_10F0D

- -----------/


*Report Timeline*

. 2008-01-11: Core Security Technologies found a security vulnerability
in BitDefender antivirus.
. 2008-01-14: BitDefender team is contacted by Core.
. 2008-01-15: BitDefender team asks Core for technical description of
the vulnerability.
. 2008-01-15: Technical details are sent to BitDefender team by Core.
. 2008-01-22: BitDefender notifies Core that a fix has been produced and
the flaw was corrected through automatic updates.
. 2008-02-04: According to the original schedule, the CORE-2008-0320
advisory would be released at this date, but similar flaws in other
antivirus products were discovered by Core exploit writers team.
Considering all BitDefender users are patched, Core Security
Technologies does not release the advisory and continues the research of
this issue in other products.
. 2008-03-20: Core analyzes similar vulnerabilities in Comodo Firewall,
Sophos Antivirus and Rising Antivirus.
. 2008-03-25: Core notifies the Comodo, Sophos and Rising teams of the
vulnerabilities.
. 2008-03-27: Comodo team asks Core for technical description of the
vulnerability.
. 2008-03-27: Technical details are sent to Comodo team by Core.
. 2008-03-31: Rising team asks Core for technical description of the
vulnerability.
. 2008-04-01: Technical details are sent to Rising team by Core.
. 2008-04-02: Rising team inform Core that the flaw has been fixed in
the Rising AV 2008 version.
. 2008-04-02: Sophos team asks Core for technical description of the
vulnerability.
. 2008-04-07: Technical details are sent to Sophos team by Core.
. 2008-04-11: Sophos team informs that the flaw is found in one of the
antivirus drivers, and fixing it will require a reboot for all of Sophos
Windows customers. Sophos would like to fix the bug in the next major
version (second quarter 2009), in particular considering the fact that
they were unable to come up with any practical use of this vulnerability.
. 2008-04-14: Comodo notifies Core that a fix has been produced.
. 2008-04-14: Sophos informs Core that they will be able to release a
fix to the vulnerability at the end of October 2008.
. 2008-04-21: Core responds that they will reschedule the publication to
April 24th, 2008. Since the vulnerability is not critical, and has been
found using publicly available tools, like the other vulnerabilities
included in the advisory, Core doesn't see a reason to postpone the
publication of the Sophos bug until October 2008.
. 2008-04-21: Sophos asks Core not to release details of the
vulnerability until a fix is available, and not to publish Proof of
Concept code. Sophos informs that they do not believe that arbitrary
code execution is possible.
. 2008-04-24: Core responds that the advisory does not contain Proof of
Concept code. Core confirms its intention of publishing the advisory,
including the technical description, but decides to postpone it to April
28th, to give the participants more time to coordinate the release of
public information.
. 2008-04-25: Sophos provides additional information, included in the
"vendor information" section of the advisory.
. 2008-04-28: CORE-2008-0320 advisory is published.


*References*

[1] http://www.bitdefender.com 
[2] http://www.comodo.com 
[3] http://www.sophos.com 
[4] http://www.rising-global.com 
[5] http://www.matousec.com/downloads 
[6]
http://www.matousec.com/info/articles/plague-in-security-software-drivers.php 


*About CoreLabs*

CoreLabs, the research center of Core Security Technologies, is charged
with anticipating the future needs and requirements for information
security technologies. We conduct our research in several important
areas of computer security including system vulnerabilities, cyber
attack planning and simulation, source code auditing, and cryptography.
Our results include problem formalization, identification of
vulnerabilities, novel solutions and prototypes for new technologies.
CoreLabs regularly publishes security advisories, technical papers,
project information and shared software tools for public use at:
http://www.coresecurity.com/corelabs/. 


*About Core Security Technologies*

Core Security Technologies develops strategic solutions that help
security-conscious organizations worldwide develop and maintain a
proactive process for securing their networks. The company's flagship
product, CORE IMPACT, is the most comprehensive product for performing
enterprise security assurance testing. CORE IMPACT evaluates network,
endpoint and end-user vulnerabilities and identifies what resources are
exposed. It enables organizations to determine if current security
investments are detecting and preventing attacks. Core Security
Technologies augments its leading technology solution with world-class
security consulting services, including penetration testing and software
security auditing. Based in Boston, MA and Buenos Aires, Argentina, Core
Security Technologies can be reached at 617-399-6980 or on the Web at
http://www.coresecurity.com. 


*Disclaimer*

The contents of this advisory are copyright (c) 2008 Core Security
Technologies and (c) 2008 CoreLabs, and may be distributed freely
provided that no fee is charged for this distribution and proper credit
is given.


*GPG/PGP Keys*

This advisory has been signed with the GPG key of Core Security
Technologies advisories team, which is available for download at
http://www.coresecurity.com/files/attachments/core_security_advisories.asc. 

-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.7 (MingW32)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org 

iD8DBQFIFl26yNibggitWa0RAnDqAJ0b0FZeUC5U4OiG8ErOuEF0dzUVgACggHC2
OHDF+apsYohQyMN3ZjAfzbk=c4NZ
-----END PGP SIGNATURE-----

TUCoPS is optimized to look best in Firefox® on a widescreen monitor (1440x900 or better).
Site design & layout copyright © 1986-2024 AOH