Results 1 to 2 of 2
  1. #1
    defaulto's Avatar
    Join Date
    Aug 2017
    Gender
    male
    Posts
    461
    Reputation
    427
    Thanks
    347
    My Mood
    Angelic

    I guess I am back again with another problem. Weeeeeeeee

    So. It's once again, me. Here are a lot of smart peeps concentrated. Maybe someone already made something similar. I am trying to do a Multiboxing Tool for another game this time. In the later progression of it, I want to use the knowledge I gathered from the last thread I made here (SendMessage / PostMessage I think it was). Came to a point again where I am frustrating.


    What I want to bypass:

    This is easy to bypass once you know that they detect it by the mutex. A well-known method to prevent multiple instances of an App.


    How to actually bypass it (sorry for the weird colors, the encoder is not perfect and tries to keep the file size low):



    What my small program does atm:

    Visually there is 0 difference, right? And both, my App and Process Hacker, do the same things (at least that's what PH2 detects).



    My Code:
    Code:
    using System;
    using System.Collections.Generic;
    using System.Runtime.InteropServices;
    using System.Diagnostics;
    using System.Text;
    using System.Threading;
    using System.Security.AccessControl;
    using System.Security.Principal;
    
    namespace FileLockInfo
    {
    
        public class Win32API
        {
            [DllImport("ntdll.dll")]
            public static extern int NtQueryObject(IntPtr ObjectHandle, int
                ObjectInformationClass, IntPtr ObjectInformation, int ObjectInformationLength,
                ref int returnLength);
    
            [DllImport("kernel32.dll", SetLastError = true)]
            public static extern uint QueryDosDevice(string lpDeviceName, StringBuilder lpTargetPath, int ucchMax);
    
            [DllImport("ntdll.dll")]
            public static extern uint NtQuerySystemInformation(int
                SystemInformationClass, IntPtr SystemInformation, int SystemInformationLength,
                ref int returnLength);
    
            [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
            public static extern IntPtr OpenMutex(UInt32 desiredAccess, bool inheritHandle, string name);
    
            [DllImport("kernel32.dll")]
            public static extern IntPtr OpenProcess(ProcessAccessFlags dwDesiredAccess, [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, int dwProcessId);
    
            [DllImport("kernel32.dll")]
            public static extern int CloseHandle(IntPtr hObject);
    
            [DllImport("kernel32.dll", SetLastError = true)]
            [return: MarshalAs(UnmanagedType.Bool)]
            public static extern bool DuplicateHandle(IntPtr hSourceProcessHandle,
               ushort hSourceHandle, IntPtr hTargetProcessHandle, out IntPtr lpTargetHandle,
               uint dwDesiredAccess, [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, uint dwOptions);
    
            [DllImport("kernel32.dll")]
            public static extern IntPtr GetCurrentProcess();
    
            public enum ObjectInformationClass : int
            {
                ObjectBasicInformation = 0,
                ObjectNameInformation = 1,
                ObjectTypeInformation = 2,
                ObjectAllTypesInformation = 3,
                ObjectHandleInformation = 4
            }
    
            [Flags]
            public enum ProcessAccessFlags : uint
            {
                All = 0x001F0FFF,
                Terminate = 0x00000001,
                CreateThread = 0x00000002,
                VMOperation = 0x00000008,
                VMRead = 0x00000010,
                VMWrite = 0x00000020,
                DupHandle = 0x00000040,
                SetInformation = 0x00000200,
                QueryInformation = 0x00000400,
                Synchronize = 0x00100000
            }
    
            [StructLayout(LayoutKind.Sequential)]
            public struct OBJECT_BASIC_INFORMATION
            { // Information Class 0
                public int Attributes;
                public int GrantedAccess;
                public int HandleCount;
                public int PointerCount;
                public int PagedPoolUsage;
                public int NonPagedPoolUsage;
                public int Reserved1;
                public int Reserved2;
                public int Reserved3;
                public int NameInformationLength;
                public int TypeInformationLength;
                public int SecurityDescriptorLength;
                public System.Runtime.InteropServices.ComTypes.FILETIME CreateTime;
            }
    
            [StructLayout(LayoutKind.Sequential)]
            public struct OBJECT_TYPE_INFORMATION
            { // Information Class 2
                public UNICODE_STRING Name;
                public int ObjectCount;
                public int HandleCount;
                public int Reserved1;
                public int Reserved2;
                public int Reserved3;
                public int Reserved4;
                public int PeakObjectCount;
                public int PeakHandleCount;
                public int Reserved5;
                public int Reserved6;
                public int Reserved7;
                public int Reserved8;
                public int InvalidAttributes;
                public GENERIC_MAPPING GenericMapping;
                public int ValidAccess;
                public byte Unknown;
                public byte MaintainHandleDatabase;
                public int PoolType;
                public int PagedPoolUsage;
                public int NonPagedPoolUsage;
            }
    
            [StructLayout(LayoutKind.Sequential)]
            public struct OBJECT_NAME_INFORMATION
            { // Information Class 1
                public UNICODE_STRING Name;
            }
    
            [StructLayout(LayoutKind.Sequential, Pack = 1)]
            public struct UNICODE_STRING
            {
                public ushort Length;
                public ushort MaximumLength;
                public IntPtr Buffer;
            }
    
            [StructLayout(LayoutKind.Sequential)]
            public struct GENERIC_MAPPING
            {
                public int GenericRead;
                public int GenericWrite;
                public int GenericExecute;
                public int GenericAll;
            }
    
            [StructLayout(LayoutKind.Sequential, Pack = 1)]
            public struct SYSTEM_HANDLE_INFORMATION
            { // Information Class 16
                public int ProcessID;
                public byte ObjectTypeNumber;
                public byte Flags; // 0x01 = PROTECT_FROM_CLOSE, 0x02 = INHERIT
                public ushort Handle;
                public int Object_Pointer;
                public UInt32 GrantedAccess;
            }
    
            public const int MAX_PATH = 260;
            public const uint STATUS_INFO_LENGTH_MISMATCH = 0xC0000004;
            public const int DUPLICATE_SAME_ACCESS = 0x2;
            public const int DUPLICATE_CLOSE_SOURCE = 0x1;
        }
    
        public class Win32Processes
        {
            const int CNST_SYSTEM_HANDLE_INFORMATION = 16;
            const uint STATUS_INFO_LENGTH_MISMATCH = 0xc0000004;
    
            public static string getObjectTypeName(Win32API.SYSTEM_HANDLE_INFORMATION shHandle, Process process)
            {
                IntPtr m_ipProcessHwnd = Win32API.OpenProcess(Win32API.ProcessAccessFlags.All, false, proces*****);
                IntPtr ipHandle = IntPtr.Zero;
                var objBasic = new Win32API.OBJECT_BASIC_INFORMATION();
                IntPtr ipBasic = IntPtr.Zero;
                var objObjectType = new Win32API.OBJECT_TYPE_INFORMATION();
                IntPtr ipObjectType = IntPtr.Zero;
                IntPtr ipObjectName = IntPtr.Zero;
                string strObjectTypeName = "";
                int nLength = 0;
                int nReturn = 0;
                IntPtr ipTemp = IntPtr.Zero;
    
                if (!Win32API.DuplicateHandle(m_ipProcessHwnd, shHandle.Handle,
                                              Win32API.GetCurrentProcess(), out ipHandle,
                                              0, false, Win32API.DUPLICATE_SAME_ACCESS))
                    return null;
    
                ipBasic = Marshal.AllocHGlobal(Marshal.SizeOf(objBasic));
                Win32API.NtQueryObject(ipHandle, (int)Win32API.ObjectInformationClass.ObjectBasicInformation,
                                       ipBasic, Marshal.SizeOf(objBasic), ref nLength);
                objBasic = (Win32API.OBJECT_BASIC_INFORMATION)Marshal.PtrToStructure(ipBasic, objBasic.GetType());
                Marshal.FreeHGlobal(ipBasic);
    
                ipObjectType = Marshal.AllocHGlobal(objBasic.TypeInformationLength);
                nLength = objBasic.TypeInformationLength;
                while ((uint)(nReturn = Win32API.NtQueryObject(
                    ipHandle, (int)Win32API.ObjectInformationClass.ObjectTypeInformation, ipObjectType,
                      nLength, ref nLength)) ==
                    Win32API.STATUS_INFO_LENGTH_MISMATCH)
                {
                    Marshal.FreeHGlobal(ipObjectType);
                    ipObjectType = Marshal.AllocHGlobal(nLength);
                }
    
                objObjectType = (Win32API.OBJECT_TYPE_INFORMATION)Marshal.PtrToStructure(ipObjectType, objObjectType.GetType());
                if (Is64Bits())
                {
                    ipTemp = new IntPtr(Convert.ToInt64(objObjectType.Name.Buffer.ToString(), 10) >> 32);
                }
                else
                {
                    ipTemp = objObjectType.Name.Buffer;
                }
    
                strObjectTypeName = Marshal.PtrToStringUni(ipTemp, objObjectType.Name.Length >> 1);
                Marshal.FreeHGlobal(ipObjectType);
                return strObjectTypeName;
            }
    
    
            public static string getObjectName(Win32API.SYSTEM_HANDLE_INFORMATION shHandle, Process process)
            {
                IntPtr m_ipProcessHwnd = Win32API.OpenProcess(Win32API.ProcessAccessFlags.All, false, proces*****);
                IntPtr ipHandle = IntPtr.Zero;
                var objBasic = new Win32API.OBJECT_BASIC_INFORMATION();
                IntPtr ipBasic = IntPtr.Zero;
                IntPtr ipObjectType = IntPtr.Zero;
                var objObjectName = new Win32API.OBJECT_NAME_INFORMATION();
                IntPtr ipObjectName = IntPtr.Zero;
                string strObjectName = "";
                int nLength = 0;
                int nReturn = 0;
                IntPtr ipTemp = IntPtr.Zero;
    
                if (!Win32API.DuplicateHandle(m_ipProcessHwnd, shHandle.Handle, Win32API.GetCurrentProcess(),
                                              out ipHandle, 0, false, Win32API.DUPLICATE_SAME_ACCESS))
                    return null;
    
                ipBasic = Marshal.AllocHGlobal(Marshal.SizeOf(objBasic));
                Win32API.NtQueryObject(ipHandle, (int)Win32API.ObjectInformationClass.ObjectBasicInformation,
                                       ipBasic, Marshal.SizeOf(objBasic), ref nLength);
                objBasic = (Win32API.OBJECT_BASIC_INFORMATION)Marshal.PtrToStructure(ipBasic, objBasic.GetType());
                Marshal.FreeHGlobal(ipBasic);
    
    
                nLength = objBasic.NameInformationLength;
    
                ipObjectName = Marshal.AllocHGlobal(nLength);
                while ((uint)(nReturn = Win32API.NtQueryObject(
                         ipHandle, (int)Win32API.ObjectInformationClass.ObjectNameInformation,
                         ipObjectName, nLength, ref nLength))
                       == Win32API.STATUS_INFO_LENGTH_MISMATCH)
                {
                    Marshal.FreeHGlobal(ipObjectName);
                    ipObjectName = Marshal.AllocHGlobal(nLength);
                }
                objObjectName = (Win32API.OBJECT_NAME_INFORMATION)Marshal.PtrToStructure(ipObjectName, objObjectName.GetType());
    
                if (Is64Bits())
                {
                    ipTemp = new IntPtr(Convert.ToInt64(objObjectName.Name.Buffer.ToString(), 10) >> 32);
                }
                else
                {
                    ipTemp = objObjectName.Name.Buffer;
                }
    
                if (ipTemp != IntPtr.Zero)
                {
    
                    byte[] baTemp2 = new byte[nLength];
                    try
                    {
                        Marshal.Copy(ipTemp, baTemp2, 0, nLength);
    
                        strObjectName = Marshal.PtrToStringUni(Is64Bits() ?
                                                               new IntPtr(ipTemp.ToInt64()) :
                                                               new IntPtr(ipTemp.ToInt32()));
                        return strObjectName;
                    }
                    catch (AccessViolationException)
                    {
                        return null;
                    }
                    finally
                    {
                        Marshal.FreeHGlobal(ipObjectName);
                        Win32API.CloseHandle(ipHandle);
                    }
                }
                return null;
            }
    
            public static List<Win32API.SYSTEM_HANDLE_INFORMATION>
            GetHandles(Process process = null, string IN_strObjectTypeName = null, string IN_strObjectName = null)
            {
                uint nStatus;
                int nHandleInfoSize = 0x10000;
                IntPtr ipHandlePointer = Marshal.AllocHGlobal(nHandleInfoSize);
                int nLength = 0;
                IntPtr ipHandle = IntPtr.Zero;
    
                while ((nStatus = Win32API.NtQuerySystemInformation(CNST_SYSTEM_HANDLE_INFORMATION, ipHandlePointer,
                                                                    nHandleInfoSize, ref nLength)) ==
                        STATUS_INFO_LENGTH_MISMATCH)
                {
                    nHandleInfoSize = nLength;
                    Marshal.FreeHGlobal(ipHandlePointer);
                    ipHandlePointer = Marshal.AllocHGlobal(nLength);
                }
    
                byte[] baTemp = new byte[nLength];
                Marshal.Copy(ipHandlePointer, baTemp, 0, nLength);
    
                long lHandleCount = 0;
                if (Is64Bits())
                {
                    lHandleCount = Marshal.ReadInt64(ipHandlePointer);
                    ipHandle = new IntPtr(ipHandlePointer.ToInt64() + 8);
                }
                else
                {
                    lHandleCount = Marshal.ReadInt32(ipHandlePointer);
                    ipHandle = new IntPtr(ipHandlePointer.ToInt32() + 4);
                }
    
                Win32API.SYSTEM_HANDLE_INFORMATION shHandle;
                List<Win32API.SYSTEM_HANDLE_INFORMATION> lstHandles = new List<Win32API.SYSTEM_HANDLE_INFORMATION>();
    
                for (long lIndex = 0; lIndex < lHandleCount; lIndex++)
                {
                    shHandle = new Win32API.SYSTEM_HANDLE_INFORMATION();
                    if (Is64Bits())
                    {
                        shHandle = (Win32API.SYSTEM_HANDLE_INFORMATION)Marshal.PtrToStructure(ipHandle, shHandle.GetType());
                        ipHandle = new IntPtr(ipHandle.ToInt64() + Marshal.SizeOf(shHandle) + 8);
                    }
                    else
                    {
                        ipHandle = new IntPtr(ipHandle.ToInt64() + Marshal.SizeOf(shHandle));
                        shHandle = (Win32API.SYSTEM_HANDLE_INFORMATION)Marshal.PtrToStructure(ipHandle, shHandle.GetType());
                    }
    
                    if (process != null)
                    {
                        if (shHandle.ProcessID != proces*****) continue;
                    }
    
                    string strObjectTypeName = "";
                    if (IN_strObjectTypeName != null)
                    {
                        strObjectTypeName = getObjectTypeName(shHandle, Process.GetProcessById(shHandle.ProcessID));
                        if (strObjectTypeName != IN_strObjectTypeName) continue;
                    }
    
                    string strObjectName = "";
                    if (IN_strObjectName != null)
                    {
                        strObjectName = getObjectName(shHandle, Process.GetProcessById(shHandle.ProcessID));
                        if (strObjectName != IN_strObjectName) continue;
                    }
    
                    string strObjectTypeName2 = getObjectTypeName(shHandle, Process.GetProcessById(shHandle.ProcessID));
                    string strObjectName2 = getObjectName(shHandle, Process.GetProcessById(shHandle.ProcessID));
                    Console.WriteLine("{0}   {1}   {2}", shHandle.ProcessID, strObjectTypeName2, strObjectName2);
    
                    lstHandles.Add(shHandle);
                }
                return lstHandles;
            }
    
            public static bool Is64Bits()
            {
                return Marshal.SizeOf(typeof(IntPtr)) == 8 ? true : false;
            }
        }
    
        class Program
        {
            #region Imports
    
            [DllImport("user32.dll")]
            static extern int SetWindowText(IntPtr hWnd, string text);
    
            [DllImport("kernel32.dll")]
            static extern IntPtr OpenThread(int dwDesiredAccess, bool bInheritHandle, uint dwThreadId);
    
            [DllImport("kernel32.dll")]
            public static extern int CloseHandle(IntPtr hObject);
    
            [DllImport("kernel32.dll")]
            static extern uint SuspendThread(IntPtr hThread);
    
            [DllImport("kernel32.dll")]
            static extern int ResumeThread(IntPtr hThread);
    
            #endregion
    
            static void Main(string[] args)
            {
                while (true)
                {
                    //SuspendProcess("Growtopia");
    
                    // CloseMutant("Growtopia", "\\Sessions\\3\\BaseNamedObjects\\Growtopia");
    
                    Console.ReadLine();
    
                    Process growtopia = new Process();
                    growtopia.StartInfo.FileName = @"C:\Users\reald\AppData\Local\Growtopia\Growtopia.exe";
                    growtopia.Start();
                    Console.WriteLine(growtopia.Id);
    
                    Console.ReadLine();
    
                    SuspendProcessByProcessID(growtopia.Id);
    
                    Console.ReadLine();
    
                    CloseMutant("Growtopia", "\\Sessions\\1\\BaseNamedObjects\\Growtopia");
                }
            }
    
            public static void CloseMutantByProcessID(int ProcessID, String MutexName)
            {
                try
                {
                    Process process = Process.GetProcessById(ProcessID);
                    var handles = Win32Processes.GetHandles(process, "Mutant", MutexName);
                    if (handles.Count == 0) throw new System.ArgumentException("NoMutex", "original");
                    foreach (var handle in handles)
                    {
                        IntPtr ipHandle = IntPtr.Zero;
                        if (!Win32API.DuplicateHandle(Process.GetProcessById(handle.ProcessID).Handle, handle.Handle, Win32API.GetCurrentProcess(), out ipHandle, 0, false, Win32API.DUPLICATE_CLOSE_SOURCE))
                            Console.WriteLine("DuplicateHandle() failed, error = {0}", Marshal.GetLastWin32Error());
    
                        Console.WriteLine("Für den Mutex wurde folgende Aktion ausgeführt: 'Close'");
                    }
                }
                catch (IndexOutOfRangeException)
                {
                    Console.WriteLine("PID '{0}' wird im Moment nicht ausgeführt oder ist falsch geschrieben!", ProcessID);
                }
                catch (ArgumentException)
                {
                    Console.WriteLine("Der Mutex '{0}' wurde im Prozess mit der PID '{1}' nicht gefunden!", MutexName, ProcessID);
                }
            }
    
            public static void SuspendProcessByProcessID(int ProcessID)
            {
                Process process = Process.GetProcessById(ProcessID);
                foreach (ProcessThread pT in process.Threads)
                {
                    // SUSPEND_RESUME = 0x0002
                    IntPtr pOpenThread = OpenThread(0x0002, false, (uint)pT.Id);
    
                    if (pOpenThread == IntPtr.Zero)
                    {
                        continue;
                    }
    
                    SuspendThread(pOpenThread);
                }
                Console.WriteLine("Für den Prozess '{0}' wurde folgende Aktion ausgeführt: 'Suspend'", process);
            }
    
            public static void ResumeProcessByProcessID(int ProcessID)
            {
                Process process = Process.GetProcessById(ProcessID);
                foreach (ProcessThread pT in process.Threads)
                {
                    // SUSPEND_RESUME = 0x0002
                    IntPtr pOpenThread = OpenThread(0x0002, false, (uint)pT.Id);
    
                    if (pOpenThread == IntPtr.Zero)
                    {
                        continue;
                    }
    
                    var suspendCount = 0;
                    do
                    {
                        suspendCount = ResumeThread(pOpenThread);
                    }
                    while (suspendCount > 0);
    
                    CloseHandle(pOpenThread);
                }
                Console.WriteLine("Für den Prozess '{0}' wurde folgende Aktion ausgeführt: 'Resume'", process);
            }
    
            public static void CloseMutant(String ProcessName, String MutexName)
            {
                try
                {
                    Process process = Process.GetProcessesByName(ProcessName)[0];
                    var handles = Win32Processes.GetHandles(process, "Mutant", MutexName);
                    if (handles.Count == 0) throw new System.ArgumentException("NoMutex", "original");
                    foreach (var handle in handles)
                    {
                        IntPtr ipHandle = IntPtr.Zero;
                        if (!Win32API.DuplicateHandle(Process.GetProcessById(handle.ProcessID).Handle, handle.Handle, Win32API.GetCurrentProcess(), out ipHandle, 0, false, Win32API.DUPLICATE_CLOSE_SOURCE))
                            Console.WriteLine("DuplicateHandle() failed, error = {0}", Marshal.GetLastWin32Error());
    
                        Console.WriteLine("Für den Mutex wurde folgende Aktion ausgeführt: 'Close'");
                    }
                }
                catch (IndexOutOfRangeException)
                {
                    Console.WriteLine("Der Prozess '{0}' wird im Moment nicht ausgeführt oder ist falsch geschrieben!", ProcessName);
                }
                catch (ArgumentException)
                {
                    Console.WriteLine("Der Mutex '{0}' wurde im Prozess '{1}' nicht gefunden!", MutexName, ProcessName);
                }
            }
    
            public static void SuspendProcess(String ProcessName)
            {
                Process process = Process.GetProcessesByName(ProcessName)[0];
                foreach (ProcessThread pT in process.Threads)
                {
                    // SUSPEND_RESUME = 0x0002
                    IntPtr pOpenThread = OpenThread(0x0002, false, (uint)pT.Id);
    
                    if (pOpenThread == IntPtr.Zero)
                    {
                        continue;
                    }
    
                    SuspendThread(pOpenThread);
                }
                Console.WriteLine("Für den Prozess '{0}' wurde folgende Aktion ausgeführt: 'Suspend'", process);
            }
    
            public static void ResumeProcess(String ProcessName)
            {
                Process process = Process.GetProcessesByName(ProcessName)[0];
                foreach (ProcessThread pT in process.Threads)
                {
                    // SUSPEND_RESUME = 0x0002
                    IntPtr pOpenThread = OpenThread(0x0002, false, (uint)pT.Id);
    
                    if (pOpenThread == IntPtr.Zero)
                    {
                        continue;
                    }
    
                    var suspendCount = 0;
                    do
                    {
                        suspendCount = ResumeThread(pOpenThread);
                    }
                    while (suspendCount > 0);
    
                    CloseHandle(pOpenThread);
                }
                Console.WriteLine("Für den Prozess '{0}' wurde folgende Aktion ausgeführt: 'Resume'", process);
            }
        }
    }
    My question here is, are there proper / better ways to do that? Seems like I don't delete the Mutants correctly. I tried to look into the Source of PH2 since it is open-source. But out of the overwhelming information you find in it I only get confused or I don't find what I am looking for...
    Hope that this time too there is someone who can help me out. As always I leave Credits to the people who helped me with a part.

    #LOGS
    12-02-2020 ⌨ [MPGH]defaulto got gifted the Premium Membership from [MPGH]Azuki - sponsored by [MPGH]Flengo.
    27-11-2019 ⌨ [MPGH]defaulto captured the GFX Team Base together with [MPGH]Howl & [MPGH]Poonce.
    08-14-2017 ⌨ defaulto joined the game.

  2. #2
    Threadstarter
    Team Ehrenlos Co-Founder
    Premium Member
    defaulto's Avatar
    Join Date
    Aug 2017
    Gender
    male
    Posts
    461
    Reputation
    427
    Thanks
    347
    My Mood
    Angelic
    Fixed it!
    I found the reason by looking into a familiar project. I haven't thought of that reason just yet.
    As it is now, unless all handles to the mutex get freed there will be still a kernel object that won't get destroyed. My bad.

    @MikeRohsoft, if you are still a mod in here and active you can mark this as solved. Thank you


    #LOGS
    12-02-2020 ⌨ [MPGH]defaulto got gifted the Premium Membership from [MPGH]Azuki - sponsored by [MPGH]Flengo.
    27-11-2019 ⌨ [MPGH]defaulto captured the GFX Team Base together with [MPGH]Howl & [MPGH]Poonce.
    08-14-2017 ⌨ defaulto joined the game.

Similar Threads

  1. [Serious] I'm Back with Another Problem
    By AdamseRP in forum General
    Replies: 25
    Last Post: 10-13-2017, 03:53 PM
  2. [Outdated] Hany`s Injector V1 | Back Again With Best Design !! | Work In All Windows !! |
    By hanyali2012 in forum CrossFire Spammers, Injectors and Multi Tools
    Replies: 23
    Last Post: 11-04-2012, 06:40 AM
  3. I come back, with another problem :P
    By Rainscape in forum Combat Arms Help
    Replies: 6
    Last Post: 09-27-2010, 07:22 AM
  4. Replies: 73
    Last Post: 12-22-2008, 08:28 PM