Phân tích các kĩ thuật chống gian lận trong phần mềm FPT-Exam

Trong bài viết trước mình đã giới thiệu cách làm việc của phần mềm FPT-Exam, tuy nhiên vì thời lượng bài viết nên mình không giới thiệu các biện pháp chống gian lận trong phần mềm mà mình sẽ viết trong bài viết này.

Xóa clipboard và disable các phím tắt

Ở constructor của class TestMetro.Form1:

public Form1() {
    [...]
    ProcessModule mainModule = Process.GetCurrentProcess().MainModule;
    this.delegate0_0 = new Form1.Delegate0(this.method_11);
    this.intptr_0 = Form1.SetWindowsHookEx_1(13, this.delegate0_0, Form1.GetModuleHandle(mainModule.ModuleName), 0u);
    [...]
}

Đoạn code tiếp theo gọi hàm SetWindowsHookEx của Win32 API. Hàm này cho phép chương trình theo dõi các sự kiện như sự kiện chuột hoặc bàn phím, bằng cách chạy hàm hook khi các sự kiện này xảy ra. Các tham số cho ta biết method method_11() sẽ được gọi khi sự kiện WH_KEYBOARD_LL xảy ra. Sự kiện WH_KEYBOARD_LL xảy ra khi người dùng nhấn một phím bất kì, vậy method method_11() sẽ được gọi khi người dùng nhấn một phím bất kì. Chúng ta cũng biết ứng với sự kiện 13 thì method sẽ có signature của LowLevelKeyboardProc: (mình đã đổi tên tham số để giống với tên tham số cho LowLevelKeyboardProc).

private IntPtr method_11(int nProc, IntPtr wParam, IntPtr lParam) {
    if (nProc >= 0) {
        Form1.Struct0 @struct = (Form1.Struct0)Marshal.PtrToStructure(lParam, typeof(Form1.Struct0));

        Clipboard.Clear();

        if (@struct.keys_0 == Keys.RWin || @struct.keys_0 == Keys.LWin)
            return (IntPtr)1;

        if (@struct.keys_0 == Keys.RMenu || @struct.keys_0 == Keys.LMenu || @struct.keys_0 == Keys.RControlKey || @struct.keys_0 == Keys.LControlKey)
            return (IntPtr)1;

        if (@struct.keys_0 == Keys.Snapshot || @struct.keys_0 == Keys.Print)
            return (IntPtr)1;
    }
    return Form1.CallNextHookEx(this.intptr_0, nProc, wParam, intptr_3);
}

Lý do của đoạn if đầu tiên là hook chỉ được phép xử lý sự kiện nếu nProc >= 0, nếu không thì hook phải truyền sự kiện cho hook tiếp theo bằng hàm CallNextHookEx. lParam là một con trỏ tới cấu trúc KBDLLHOOKSTRUCT, vààđoạn code tiếp theo sẽ biến cấu trúc này thành cấu trúc Form1.Struct0. Ta có thể thấy sự giống nhau giữa hay cấu trúc này: // TODO

Sau đó thì hook xóa clipboard. Mình coi đây là một hành vi chống gian lận bằng cách không cho phép sao chép nội dung từ ngoài vào chương trình.

Sau đó, hàm kiểm tra nếu người dùng vừa nhấn một trong các phím sau: phím Windows ở trái/phải bàn phím, phím Menu (là phím có sổ xuống) ở trái/phải bàn phím, phím Control ở trái/phải bàn phím, phím print screen và phím print (mình không biết phím này là gì, TODO). Nếu người dùng có nhấn những phím trên thì hàm sẽ trả về 1, còn không thì hàm sẽ thoát ra đoạn if và gọi hook tiếp theo trong chuỗi. Đọc tài liệu của LowLevelKeyboardProc, hàm này trả về giá trị khác không nếu hàm này không muốn sự kiện đi tới hook tiếp theo và tới cửa sổ. Nghĩa là nếu người dùng nhấn các phím trên thì hook sẽ ngăn cửa sổ process phím nhấn. Mình coi đây cũng là một hành vi chống gian lận bằng cách disable các phím tắt như Ctrl-C/V hay việc nhấn Windows để hiện menu và thoát ra ngoài (vì phần mêm thi chạy full screen)

Theo dõi hành vi chuột

Cũng ở constructor của class TestMetro.Form1:

public Form1() {
    [...]
    MouseHook.Start();
    MouseHook.MouseAction += this.method_12;
    [...]
}

Xem xét hàm MouseHook.Start():

public static void Start() {
    MouseHook.intptr_0 = MouseHook.smethod_0(MouseHook.delegate5_0);
}

MouseHook.delegate5_0 là một delegate được định nghĩa như sau:

private delegate IntPtr Delegate5(int nCode, IntPtr wParam, IntPtr lParam);
private static MouseHook.Delegate5 delegate5_0 = new MouseHook.Delegate5(MouseHook.smethod_1);

Vậy MouseHook.delegate5_0 là một delegate để gọi MouseHook.smethod_1. Chúng ta sẽ trở lại đây sau khi phân tích hàm Start.

Xem xét hàm MouseStart.smethod_0 được gọi bởi MouseHoọk.Start():

private static IntPtr smethod_0(MouseHook.Delegate5 delegate5_1) {
    IntPtr result;
    using (Process currentProcess = Process.GetCurrentProcess()) {
        using (currentProcess.MainModule) {
            IntPtr intPtr = MouseHook.SetWindowsHookEx(
                14, delegate5_1, MouseHook.GetModuleHandle("user32"), 0u);

            if (intPtr == IntPtr.Zero)
                throw new Win32Exception();

            result = intPtr;
        }
    }
    return result;
}

Tương tự như trong phần hook vào sự kiện bàn phím, hàm này cũng sử dụng hàm SetWindowsHookEx để hook vào sự kiện chuột, có mã là WH_MOUSE_LL = 14. Hàm hook ở đây là delegate5_1, là MouseHoọ.delegate5_0 chúng ta đã phân tích ở trên, là hàm MouseHook.smethod_1. Sự kiện chuột yêu cầu hàm hook có signature LowLevelMouseProc, và ta có thể thấy signature của hàm này tương ứng với signature của Delegate5.

Phân tích hàm MouseHook.smethod_1(), hàm này có hai nhiệm vụ chính là: giết các tiến trình ngoại lệ nếu được enable, và không cho phép các hành vi chuột.

Giết các tiến trình ngoại lệ

Trong hàm MouseHook.smethod_1:

private static IntPtr smethod_1(int int_1, IntPtr intptr_1, IntPtr intptr_2) {
    if (MouseHook.CheckkillP) {
        IntPtr foregroundWindow = MouseHook.GetForegroundWindow();
        uint processId;
        MouseHook.GetWindowThreadProcessId(foregroundWindow, out processId);
        Process processById = Process.GetProcessById((int)processId);
        if (processById.ProcessName != Class7.string_2) {
            try {
                foreach (Process process in Process.GetProcessesByName(processById.ProcessName)) {
                    process.Kill();
                }
            }
            catch { }
        }
    }
    [...]
}

Theo dõi hành vi chuột

Cũng trong hàm MouseHook.smethod_1 (đã đổi tên tham số để trùng với tham số của LowLevelMouseProc)

private static IntPtr smethod_1(int nCode, IntPtr wParam, IntPtr lParam) {
    [...]
    IntPtr result;
    if (nCode >= 0 && 522 != (int)wParam && 513 != (int)wParam && 512 != (int)wParam && 514 != (int)wParam) {
        result = (IntPtr)1;
    } else {
        if (nCode >= 0 && 513 == (int)wParam) {
            MouseHook.checkup = 0;
            MouseHook.Struct6 @struct = (MouseHook.Struct6)Marshal.PtrToStructure(lParam, typeof(MouseHook.Struct6));
            if (MouseHook.xymD.x == @struct.struct5_0.int_0 && MouseHook.xymD.y == @struct.struct5_0.int_1) {
                return (IntPtr)1;
            }
            MouseHook.xymD = new MouseHook.Point(@struct.struct5_0.int_0, @struct.struct5_0.nCode);
        } else if (nCode >= 0 && 514 == (int)wParam) {
            MouseHook.checkup = 1;
        } else if (MouseHook.checkup == 0) {
            return (IntPtr)1;
        }
        result = MouseHook.CallNextHookEx(MouseHook.intptr_0, nCode, wParam, lParam);
    }
    return result;
}

Các điều kiện nCode >= 0 tương tự như hàm hook bàn phím (hàm hook chỉ được xử lý sự kiện nếu nCode >= 0). wParam là biến biểu thị loại sự kiện chuột:

Tên sự kiện Giá trị Ý nghĩa
WM_LBUTTONDOWN 0x0201 = 513 Nhấn nút chuột trái
WM_LBUTTONUP 0x0202 = 514 Thả nút chuột trái
WM_MOUSEMOVE 0x0200 = 512 Di chuyển con trỏ chuột
WM_MOUSEWHEEL 0x020a = 522 Sử dụng con lăn chuột
WM_RBUTTONUP 0x0205 = 517 Thả nút chuột phải
WM_RBUTTONDOWN 0x0204 = 516 Nhấn nút chuột phải
WM_MOUSEHWHEEL 0x020e = 562 Sử dụng con lăn chuột chiều ngang

Tương tự như trên, nếu hàm trả về >= 0 thì sự kiện sẽ không được truyền cho hook tiếp theo và ứng dụng đích. Đoạn if đầu tiên check xem nếu hành vi chuột không phải là hành vi nhấn/thả chuột trái, sử dụng con lăn chuột và di chuyển chuột thì sẽ không cho hành vi đó đến ứng dụng. Đây là một biện pháp để disable hành vi nhấn chuột phải vì trong cửa sổ trình duyệt, người dùng có thể nhấn chuột phải để sao chép và dán, thay vì dùng phím tắt Ctrl-C/V.

Tham số lParam là một con trỏ tới cấu trúc MSLLHOOKSTRUCT. Đoạn if tiếp theo convert từ struct này thành struct có dạng MouseHook.Struct6, sau đó.