[Release] C# HWBP Hooks - The clean way menu

User Tag List

Page 1 of 2 12 LastLast
Results 1 to 15 of 16
  1. #1
    Apoc's Avatar Angry Penguin
    Reputation
    1387
    Join Date
    Jan 2008
    Posts
    2,750
    Thanks G/R
    0/12
    Trade Feedback
    0 (0%)
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)

    [Release] C# HWBP Hooks - The clean way

    So, fully OOP, no code modification hooks are always fun!

    Finally got around to writing a wrapper class for it, and might as well share it.

    Everything is self-contained for the most part (written with GreyMagic in mind, but about 4 lines changed is all you need)

    Usage:

    Code:
    _sendPacketHook = new ExternalProcessHook(memory, HookRegister.DR3, sendPacketLoc, HandleSendPacketHook);
    Make sure your "ExternalProcessHook" object isn't being disposed anywhere, until you want the hook released.

    Actual ExternalProcessHook code:

    Code:
        [Flags]    public enum HookRegister
        {
            None = 0,
            DR0 = 1,
            DR1 = 2,
            DR2 = 4,
            DR3 = 8
        }
    
    
        public class ExternalProcessHook : IDisposable
        {
            public delegate void HandleHookCallback(ref ThreadContext threadContext);
    
    
            private static int _hookCount;
            private static readonly List<HookItem> Hooks = new List<HookItem>();
            private static Thread _workerThread;
            private static bool _removeHook;
            private static bool _debugActiveAlready;
            private readonly HookItem _hook;
            private bool _disposed;
    
    
            public ExternalProcessHook(MemoryBase memory, HookRegister register, IntPtr hookLocation,
                HandleHookCallback callback)
            {
                var i = new HookItem {Callback = callback, Location = hookLocation, Register = register};
                _hook = i;
                Hooks.Add(i);
                _hookCount++;
    
    
                // So basically, DR hooks work off "waiting for a debug event" basically.
                // In actuality we're waiting on an exception, but for the sake of wrapping a hook,
                // we'll do it in a separate thread. This means we need to ensure we close the thread (IsBackground) when the app closes
                // and ensure we only ever create *one* polling thread.
                if (_hookCount == 0 || _workerThread == null)
                {
                    _workerThread =
                        new Thread(() => InstallHardwareHook(memory.Process));
                    _workerThread.IsBackground = true;
                    _workerThread.Start();
                }
                SetThreadHook(i, false);
            }
    
    
            public void Dispose()
            {
                if (_disposed)
                    return;
                _hookCount--;
    
    
                SetThreadHook(_hook, true);
                if (_hookCount == 0)
                {
                    _removeHook = true;
                }
                // Remove ourselves from the hook list... we're done. :)
                Hooks.Remove(_hook);
                _disposed = true;
            }
    
    
            private static void OpenAllThreads(Process proc)
            {
                // This isn't super needed, it's just to OpenThread a ton of things and get handles for later.
                // Unfortunately, the .NET ProcessThread stuff isn't always accurate, so we'll just skip it
                // entirely and do it the native win32 way.
                var te = new THREADENTRY32();
                te.dwSize = 28; // sizeof(THREADENTRY32)
    
    
                IntPtr hSnapshot = CreateToolhelp32Snapshot(4, 0);
    
    
                if (Thread32First(hSnapshot, ref te) && Thread32Next(hSnapshot, out te))
                {
                    do
                    {
                        if (te.th32OwnerProcessID == proc.Id)
                        {
                            OpenThreadHandles.Add(OpenThread(0x1FFFFF, false, te.th32ThreadID));
                        }
                    }
                    while (Thread32Next(hSnapshot, out te));
                }
            }
    
    
            private static void SetDebugRegisters(HookRegister register, IntPtr hookLocation, ref ThreadContext ct, bool remove)
            {
                if (remove)
                {
                    uint flagBit = 0;
                    switch (register)
                    {
                        case HookRegister.DR0:
                            flagBit = 1 << 0;
                            ct.Dr0 = 0;
                            break;
                        case HookRegister.DR1:
                            flagBit = 1 << 2;
                            ct.Dr1 = 0;
                            break;
                        case HookRegister.DR2:
                            flagBit = 1 << 4;
                            ct.Dr2 = 0;
                            break;
                        case HookRegister.DR3:
                            flagBit = 1 << 6;
                            ct.Dr3 = 0;
                            break;
                    }
                    ct.Dr7 &= ~flagBit;
                }
                else
                {
                    switch (register)
                    {
                        case HookRegister.DR0:
                            ct.Dr0 = (uint) hookLocation;
                            ct.Dr7 |= 1 << 0;
                            break;
                        case HookRegister.DR1:
                            ct.Dr1 = (uint) hookLocation;
                            ct.Dr7 |= 1 << 2;
                            break;
                        case HookRegister.DR2:
                            ct.Dr2 = (uint) hookLocation;
                            ct.Dr7 |= 1 << 4;
                            break;
                        case HookRegister.DR3:
                            ct.Dr3 = (uint) hookLocation;
                            ct.Dr7 |= 1 << 6;
                            break;
                    }
                    ct.Dr6 = 0;
                }
            }
    
    
            private static void SetThreadHook(HookItem item, bool remove)
            {
                var ctx = new ThreadContext();
                ctx.ContextFlags = 65559;
                foreach (IntPtr openThreadHandle in OpenThreadHandles)
                {
                    SuspendThread(openThreadHandle);
                    GetThreadContext(openThreadHandle, ref ctx);
    
    
                    SetDebugRegisters(item.Register, item.Location, ref ctx, remove);
                    item.Hooked = !remove;
    
    
                    SetThreadContext(openThreadHandle, ref ctx);
                    ResumeThread(openThreadHandle);
                }
            }
    
    
            public static void InstallHardwareHook(Process proc)
            {
                // Open the proc with full privs so we can attach the debugger later.
                OpenProcess(0x1FFFFFu, false, (uint) proc.Id);
                // Open all the threads for use with installing/removing the DR hooks
                OpenAllThreads(proc);
    
    
                // Ideally should never be hit, but we just need to ensure we check it anyway.
                if (!_debugActiveAlready && !DebugActiveProcess((uint) proc.Id))
                    throw new Exception("Failed to attach debugger!");
    
    
                _debugActiveAlready = true;
    
    
                DebugSetProcessKillOnExit(0);
    
    
                try
                {
                    while (!_removeHook)
                    {
                        // Useless double-checking of hook states. Sanity really...
                        if (Hooks.Any(i => !i.Hooked))
                        {
                            foreach (HookItem hookItem in Hooks)
                            {
                                if (!hookItem.Hooked)
                                {
                                    SetThreadHook(hookItem, false);
                                }
                            }
                        }
    
    
                        // And begin waiting for debug events...
                        var ctx = new ThreadContext();
                        ctx.ContextFlags = 0x10017;
                        DEBUG_EVENT evt;
                        if (!WaitForDebugEvent(out evt, 0xFFFFFFFF))
                        {
                            continue;
                        }
    
    
                        if (evt.Exception.ExceptionRecord.ExceptionCode != 0x80000004) // EXCEPTION_SINGLE_STEP
                        {
                            ContinueDebugEvent((uint) evt.dwProcessId, (uint) evt.dwThreadId, 0x80010001); // EXCEPTION_CONTINUE
                        }
                        else
                        {
                            // Re-open the thread so we can get the context info we need.
                            IntPtr hThread = OpenThread(0x1FFFFFu, false, (uint) evt.dwThreadId);
    
    
                            GetThreadContext(hThread, ref ctx);
    
    
                            ctx.EFlags |= 0x10040; // CONTEXT_FULL
    
    
                            // NOTE: The callback "call" part is in a catch-all exception handler.
                            // This is to prevent people from crashing the application with the DR hook installed.
                            // This won't stop people from breaking the context though!
                            // Feel free to modify this to ensure people see the exception if need be.
                            try
                            {
                                // Call our callback!
                                // Find it by the location we Hooked it to (eip is the current address FYI)
                                HookItem hook = Hooks.FirstOrDefault(h => (uint) h.Location == ctx.Eip);
                                if (hook != null)
                                    hook.Callback(ref ctx);
                            }
                            catch
                            {
                            }
    
    
                            // Set the new thread context (if it was changed)
                            SetThreadContext(hThread, ref ctx);
    
    
                            // And we're done with the thread.
                            CloseHandle(hThread);
    
    
                            // Move along...
                            ContinueDebugEvent((uint) evt.dwProcessId, (uint) evt.dwThreadId, 0x10002u);
                        }
                    }
                }
                finally
                {
                    // Thread is closing, or something else is making this function "leave"
                    // Make sure we drop all the DR hooks to make sure we don't start spewing exceptions at the client.
                    foreach (HookItem hookItem in Hooks)
                    {
                        if (hookItem.Hooked)
                        {
                            SetThreadHook(hookItem, true);
                        }
                    }
                }
            }
    
    
            ~ExternalProcessHook()
            {
                // Finalizer Dispose() is to ensure this gets run every single time, regardless of Dispose() being called.
                // Application closing doesn't *always* run Dispose. Hence this!
                Dispose();
            }
    
    
            #region Imports
    
    
            private static readonly List<IntPtr> OpenThreadHandles = new List<IntPtr>();
    
    
            [DllImport("kernel32.dll")]
            private static extern IntPtr OpenThread(uint dwDesiredAccess, bool bInheritHandle,
                uint dwThreadId);
    
    
            [DllImport("kernel32.dll")]
            private static extern IntPtr OpenProcess(uint dwDesiredAccess, bool bInheritHandle,
                uint dwThreadId);
    
    
            [DllImport("kernel32.dll")]
            private static extern bool Thread32First(IntPtr hSnapshot, ref THREADENTRY32 lpte);
    
    
            [DllImport("kernel32.dll")]
            private static extern bool Thread32Next(IntPtr hSnapshot, out THREADENTRY32 lpte);
    
    
            [DllImport("kernel32.dll", SetLastError = true)]
            private static extern IntPtr CreateToolhelp32Snapshot(int dwFlags, uint th32ProcessID);
    
    
            [DllImport("kernel32.dll")]
            private static extern uint SuspendThread(IntPtr hThread);
    
    
            [DllImport("kernel32.dll")]
            private static extern bool GetThreadContext(IntPtr hThread, ref ThreadContext lpContext);
    
    
            [DllImport("kernel32.dll")]
            private static extern bool SetThreadContext(IntPtr hThread,
                [In] ref ThreadContext lpContext);
    
    
            [DllImport("kernel32.dll")]
            private static extern uint ResumeThread(IntPtr hThread);
    
    
            [DllImport("kernel32.dll")]
            private static extern bool DebugActiveProcess(uint dwProcessId);
    
    
            [DllImport("kernel32.dll")]
            private static extern bool DebugSetProcessKillOnExit(uint dwProcessId);
    
    
            [DllImport("kernel32.dll", EntryPoint = "WaitForDebugEvent")]
            [return: MarshalAs(UnmanagedType.Bool)]
            private static extern bool WaitForDebugEvent(out DEBUG_EVENT lpDebugEvent, uint dwMilliseconds);
    
    
            [DllImport("kernel32.dll")]
            private static extern bool ContinueDebugEvent(uint dwProcessId, uint dwThreadId,
                uint dwContinueStatus);
    
    
            [DllImport("kernel32.dll", SetLastError = true)]
            [return: MarshalAs(UnmanagedType.Bool)]
            private static extern bool CloseHandle(IntPtr hObject);
    
    
            #endregion
    
    
            #region Structs
    
    
            [StructLayout(LayoutKind.Sequential)]
            private unsafe struct DEBUG_EVENT
            {
                public readonly uint dwDebugEventCode;
                public readonly int dwProcessId;
                public readonly int dwThreadId;
    
    
                [MarshalAs(UnmanagedType.ByValArray, SizeConst = 86, ArraySubType = UnmanagedType.U1)]
                private readonly byte[] debugInfo;
    
    
                public EXCEPTION_DEBUG_INFO Exception
                {
                    get
                    {
                        if (debugInfo == null)
                            return new EXCEPTION_DEBUG_INFO();
    
    
                        fixed (byte* ptr = debugInfo)
                        {
                            return *(EXCEPTION_DEBUG_INFO*) ptr;
                        }
                    }
                }
    
    
                public LOAD_DLL_DEBUG_INFO LoadDll
                {
                    get
                    {
                        if (debugInfo == null)
                            return new LOAD_DLL_DEBUG_INFO();
    
    
                        fixed (byte* ptr = debugInfo)
                        {
                            return *(LOAD_DLL_DEBUG_INFO*) ptr;
                        }
                    }
                }
            }
    
    
            [StructLayout(LayoutKind.Sequential)]
            private struct EXCEPTION_DEBUG_INFO
            {
                public EXCEPTION_RECORD ExceptionRecord;
                public readonly uint dwFirstChance;
            }
    
    
            [StructLayout(LayoutKind.Sequential)]
            private struct EXCEPTION_RECORD
            {
                public readonly uint ExceptionCode;
                public readonly uint ExceptionFlags;
                public readonly IntPtr ExceptionRecord;
                public readonly IntPtr ExceptionAddress;
                public readonly uint NumberParameters;
    
    
                //[MarshalAs(UnmanagedType.ByValArray, SizeConst = 15, ArraySubType = UnmanagedType.U4)]
                //public readonly uint[] ExceptionInformation;
    
    
                public unsafe fixed uint ExceptionInformation [15];
            }
    
    
            [StructLayout(LayoutKind.Sequential)]
            public struct FLOATING_SAVE_AREA
            {
                public uint ControlWord;
                public uint StatusWord;
                public uint TagWord;
                public uint ErrorOffset;
                public uint ErrorSelector;
                public uint DataOffset;
                public uint DataSelector;
    
    
                //[MarshalAs(UnmanagedType.ByValArray, SizeConst = 80)]
                //public byte[] RegisterArea;
    
    
                public unsafe fixed byte RegisterArea [80];
    
    
                public uint Cr0NpxState;
            }
    
    
            [StructLayout(LayoutKind.Sequential)]
            private struct LOAD_DLL_DEBUG_INFO
            {
                public readonly IntPtr hFile;
                public readonly IntPtr lpBaseOfDll;
                public readonly uint dwDebugInfoFileOffset;
                public readonly uint nDebugInfoSize;
                public readonly IntPtr lpImageName;
                public readonly ushort fUnicode;
            }
    
    
            [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
            private struct THREADENTRY32
            {
                internal UInt32 dwSize;
                internal readonly UInt32 cntUsage;
                internal readonly UInt32 th32ThreadID;
                internal readonly UInt32 th32OwnerProcessID;
                internal readonly UInt32 tpBasePri;
                internal readonly UInt32 tpDeltaPri;
                internal readonly UInt32 dwFlags;
            }
    
    
            [StructLayout(LayoutKind.Sequential)]
            public struct ThreadContext
            {
                public uint ContextFlags; //set this to an appropriate value 
                // Retrieved by CONTEXT_DEBUG_REGISTERS 
                public uint Dr0;
                public uint Dr1;
                public uint Dr2;
                public uint Dr3;
                public uint Dr6;
                public uint Dr7;
                // Retrieved by CONTEXT_FLOATING_POINT 
                public FLOATING_SAVE_AREA FloatSave;
                // Retrieved by CONTEXT_SEGMENTS 
                public uint SegGs;
                public uint SegFs;
                public uint SegEs;
                public uint SegDs;
                // Retrieved by CONTEXT_INTEGER 
                public uint Edi;
                public uint Esi;
                public uint Ebx;
                public uint Edx;
                public uint Ecx;
                public uint Eax;
                // Retrieved by CONTEXT_CONTROL 
                public uint Ebp;
                public uint Eip;
                public uint SegCs;
                public uint EFlags;
                public uint Esp;
                public uint SegSs;
                // Retrieved by CONTEXT_EXTENDED_REGISTERS 
                [MarshalAs(UnmanagedType.ByValArray, SizeConst = 512)]
                public byte[] ExtendedRegisters;
            }
    
    
            #endregion
    
    
            private class HookItem
            {
                public HandleHookCallback Callback;
                public bool Hooked;
                public IntPtr Location;
                public HookRegister Register;
            }
        }
    Enjoy, and feel free to give me feedback. (Yes, I know the code is ugly as sin, and there's redundancies all over the place, but deal with it...)

    [Release] C# HWBP Hooks - The clean way
  2. Thanks greenthing (1 members gave Thanks to Apoc for this useful post)
  3. #2
    Jaerin's Avatar Former Staff
    Reputation
    641
    Join Date
    Sep 2008
    Posts
    1,290
    Thanks G/R
    29/126
    Trade Feedback
    0 (0%)
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Originally Posted by Apoc View Post
    So, fully OOP, no code modification hooks are always fun!

    Finally got around to writing a wrapper class for it, and might as well share it.

    Everything is self-contained for the most part (written with GreyMagic in mind, but about 4 lines changed is all you need)

    Usage:

    Code:
     ...snip...
    Enjoy, and feel free to give me feedback. (Yes, I know the code is ugly as sin, and there's redundancies all over the place, but deal with it...)
    Apoc, we haven't always saw eye to eye on tutorials and such, but I have to say I still very much appreciate your contributions.

    Also I have a feeling your "ugly as sin" is probably a $10k/night hooker to most people.

    Thanks

  4. #3
    Sychotix's Avatar Moderator Authenticator enabled
    Reputation
    1421
    Join Date
    Apr 2006
    Posts
    3,942
    Thanks G/R
    285/572
    Trade Feedback
    1 (100%)
    Mentioned
    7 Post(s)
    Tagged
    0 Thread(s)
    Originally Posted by Jaerin View Post
    Also I have a feeling your "ugly as sin" is probably a $10k/night hooker to most people.
    You are right, it's impossible for Apoc to write ugly code. =P

  5. #4
    Jadd's Avatar 🐸 Premium Seller
    Reputation
    1511
    Join Date
    May 2008
    Posts
    2,432
    Thanks G/R
    81/333
    Trade Feedback
    1 (100%)
    Mentioned
    2 Post(s)
    Tagged
    0 Thread(s)
    Originally Posted by Jaerin View Post
    Apoc, we haven't always saw eye to eye on tutorials and such, but I have to say I still very much appreciate your contributions.

    Also I have a feeling your "ugly as sin" is probably a $10k/night hooker to most people.

    Thanks
    This isn't a tutorial lol

  6. #5
    Apoc's Avatar Angry Penguin
    Reputation
    1387
    Join Date
    Jan 2008
    Posts
    2,750
    Thanks G/R
    0/12
    Trade Feedback
    0 (0%)
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Originally Posted by Jaerin View Post
    Apoc, we haven't always saw eye to eye on tutorials and such, but I have to say I still very much appreciate your contributions.

    Also I have a feeling your "ugly as sin" is probably a $10k/night hooker to most people.

    Thanks
    Not really a tutorial, but thanks? I think...?

    Originally Posted by Sychotix View Post
    You are right, it's impossible for Apoc to write ugly code. =P
    No, the code is pretty damned ugly lol.

    Originally Posted by Jadd View Post
    This isn't a tutorial lol
    It schooled you didn't it!?

  7. #6
    Kanyle's Avatar Corporal
    Reputation
    9
    Join Date
    Jul 2011
    Posts
    19
    Thanks G/R
    1/0
    Trade Feedback
    0 (0%)
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Always nice to see releases from you Apoc-

    I actually wrote a similar class in C++ a while back, I'm sure yours is a lot cleaner, though

  8. #7
    Jaerin's Avatar Former Staff
    Reputation
    641
    Join Date
    Sep 2008
    Posts
    1,290
    Thanks G/R
    29/126
    Trade Feedback
    0 (0%)
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    I wasn't insinuating that it was a tutorial, I'm saying we've had conflicts over tutorials and knowledge sharing in the past, hence the "and such".

    I'm not sure why you dev types take compliments as though its a backhanded compliment. No sarcasm intended at all. I appreciate your contributions.

  9. #8
    DarkLinux's Avatar Former Staff
    CoreCoins Purchaser Authenticator enabled
    Reputation
    1584
    Join Date
    May 2010
    Posts
    1,828
    Thanks G/R
    188/531
    Trade Feedback
    16 (100%)
    Mentioned
    6 Post(s)
    Tagged
    0 Thread(s)
    The clean way? How can it be dirty? Also I dont think many will know what this is used for
    Last edited by DarkLinux; 02-20-2013 at 01:35 AM.

  10. #9
    Apoc's Avatar Angry Penguin
    Reputation
    1387
    Join Date
    Jan 2008
    Posts
    2,750
    Thanks G/R
    0/12
    Trade Feedback
    0 (0%)
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Originally Posted by DarkLinux View Post
    The clean way? How can it be dirty? Also I dont think many will know what this is used for
    The code alone is ugly as sin to make work. "The clean way" refers to how you just instantiate a class and viola, you have a hook.

    Also, if people don't know what it's for, so be it.

  11. #10
    suicidity's Avatar Contributor
    Reputation
    207
    Join Date
    Oct 2006
    Posts
    1,439
    Thanks G/R
    0/0
    Trade Feedback
    0 (0%)
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Ew, debug registers.. But good work none-the-less.


  12. #11
    ~Unknown~'s Avatar Contributor
    Reputation
    193
    Join Date
    Jan 2009
    Posts
    211
    Thanks G/R
    0/5
    Trade Feedback
    0 (0%)
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Originally Posted by DarkLinux View Post
    The clean way? How can it be dirty? Also I dont think many will know what this is used for
    Originally Posted by Apoc View Post
    The code alone is ugly as sin to make work. "The clean way" refers to how you just instantiate a class and viola, you have a hook.

    Also, if people don't know what it's for, so be it.
    Nice work as always Apoc.

    Just small question as what is referenced above because I'm a noob. With the function hooks you can essentially read or (more likely) manipulate the register values before the selected function executes in order to change its behavior in some way. Is this correct, and are there other uses? Also, any hints as to some useful wow functions one might use this on (cough I see sendpacket)? Just pondering how I could use it while I'm still sorta OOP. When I stop being lazy I'll finish my injected solution with Greymagic.

  13. #12
    DarkLinux's Avatar Former Staff
    CoreCoins Purchaser Authenticator enabled
    Reputation
    1584
    Join Date
    May 2010
    Posts
    1,828
    Thanks G/R
    188/531
    Trade Feedback
    16 (100%)
    Mentioned
    6 Post(s)
    Tagged
    0 Thread(s)
    You can use it to view all registers, change registers or even flags.

    I used a simple HWBP to get the players address over reading like 3 pointers.
    I have also coded some hacks using HWBP, a simple change to Eip or EFlags.
    You could also user it as a detour / hook. You would need to be create with that, be OOP and all...

    Only limiting problem with HWBP is that you can use 4 only.
    Last edited by DarkLinux; 02-20-2013 at 08:18 PM.

  14. #13
    Beaving's Avatar Sergeant
    Reputation
    21
    Join Date
    Apr 2010
    Posts
    67
    Thanks G/R
    0/0
    Trade Feedback
    0 (0%)
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    "So, fully OOP"
    "(Yes, I know the code is ugly as sin, and there's redundancies all over the place, but deal with it...)"

    How do you define fully OOP? haha

  15. #14
    Jadd's Avatar 🐸 Premium Seller
    Reputation
    1511
    Join Date
    May 2008
    Posts
    2,432
    Thanks G/R
    81/333
    Trade Feedback
    1 (100%)
    Mentioned
    2 Post(s)
    Tagged
    0 Thread(s)
    Originally Posted by Beaving View Post
    How do you define fully OOP? haha
    The code is not run within the target process' memory, as opposed to injected assembly or code -> "out of process".

  16. #15
    Apoc's Avatar Angry Penguin
    Reputation
    1387
    Join Date
    Jan 2008
    Posts
    2,750
    Thanks G/R
    0/12
    Trade Feedback
    0 (0%)
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Originally Posted by Jadd View Post
    The code is not run within the target process' memory, as opposed to injected assembly or code -> "out of process".
    ^ this.

    HWBPs are the only manageable way to do external hooks/detours in user mode.

Page 1 of 2 12 LastLast

Similar Threads

  1. Replies: 25
    Last Post: 05-12-2019, 12:49 PM
  2. [How-To] Automate your cache cleaning for NPCScan - the lazy way
    By vv3 in forum World of Warcraft Guides
    Replies: 18
    Last Post: 04-29-2014, 05:24 AM
  3. The PropEr Way To Release a Custom Made Anything
    By Mr. Herbert in forum World of Warcraft Emulator Servers
    Replies: 10
    Last Post: 02-20-2008, 06:41 PM
  4. Kiting the easy way
    By Kalen24 in forum World of Warcraft Guides
    Replies: 2
    Last Post: 11-17-2006, 09:45 AM
  5. Prepping for Vaelastrasz the easy way
    By Matt in forum World of Warcraft Guides
    Replies: 0
    Last Post: 04-01-2006, 10:11 AM
All times are GMT -5. The time now is 11:24 PM. Powered by vBulletin® Version 4.2.3
Copyright © 2024 vBulletin Solutions, Inc. All rights reserved. User Alert System provided by Advanced User Tagging (Pro) - vBulletin Mods & Addons Copyright © 2024 DragonByte Technologies Ltd.
Digital Point modules: Sphinx-based search