eBPF 實時捕獲鍵盤輸入
本文將帶你一步步實現一個基于eBPF kprobe的鍵盤記錄功能,通過Go
語言配合libbpfgo
,你將學會如何無損地監控系統鍵盤輸入,并從中獲取實時數據,進一步提高系統安全和監控能力。
1. 說明
本文屬于專欄 Go語言+libbpfgo實戰eBPF開發,示例代碼目錄為 040
。
如何下載并運行代碼,請參考 專欄介紹。
注: 老學員可以直接
git pull
拉取最新代碼。
2. 引言
在本篇文章中,我們將利用 eBPF 的 kprobe
技術捕捉鍵盤輸入事件,實現一個簡單的鍵盤記錄功能。你將學習如何在內核層面監控 input_handle_event
函數,并將捕獲到的按鍵事件傳遞到用戶空間進行處理。整個過程高效且非侵入式,適用于實時監控場景。💡
eBPF 允許在內核中動態加載和執行代碼。借助 kprobe
,我們可以在 input_handle_event
函數入口處掛載探針,從而捕獲關鍵事件。
- 鍵盤事件類型:我們關注
EV_KEY
類型的按鍵事件,并僅記錄按下(value=1
)時的事件。 - 事件傳遞方式:通過
bpf_perf_event_output
將事件數據發送到用戶空間,用戶空間程序使用perf buffer
進行實時讀取。
這樣既能精準監控輸入事件,又無需對系統進行侵入式修改。
3. 原理詳解
3.1 當你按下一個鍵盤上的按鍵時,發生了什么?
流程圖
硬件中斷 → 鍵盤驅動 → 輸入子系統 → TTY 行規程 → Shell 進程 → TTY 回顯 → 終端渲染
在 Linux 系統中,從按下鍵盤到字符顯示在終端上,涉及 硬件中斷處理、內核子系統協作和用戶空間進程交互。以下是詳細流程和關鍵函數。
1. 硬件中斷觸發
- 硬件層面:鍵盤控制器(如 PS/2 或 USB)檢測到按鍵動作,生成 硬件中斷(PS/2 鍵盤通常使用 IRQ 1)。
- 中斷控制器:將中斷路由至 CPU,CPU 通過中斷向量表調用對應的 中斷處理程序。
2. 內核中斷處理
- 中斷處理函數:內核注冊的鍵盤中斷處理程序(
irq_handler
)被觸發。- 關鍵函數:
request_irq()
注冊中斷處理函數,如 PS/2 鍵盤的kbd_event
或 USB 鍵盤的usb_kbd_irq
。
- 關鍵函數:
- 讀取掃描碼:驅動從鍵盤控制器讀取 掃描碼(Scancode)(如
inb(0x60)
讀取 PS/2 鍵盤數據端口)。 - 轉換為鍵碼:掃描碼轉換為 鍵碼(Keycode)(如
kbd_keycode
處理映射關系)。
3. 輸入子系統(Input Subsystem)
- 生成輸入事件:鍵碼通過輸入子系統封裝為
input_event
結構(包含時間戳、鍵值、動作等)。- 關鍵函數:
input_event()
→input_handle_event()
。
- 關鍵函數:
- 傳遞事件:事件通過
/dev/input/eventX
設備節點傳遞,供用戶空間程序(如終端)讀取。- 關鍵結構:
struct input_handler
負責事件路由(如evdev_handler
)。
- 關鍵結構:
4. TTY 子系統處理
- 綁定到 TTY:輸入事件傳遞到當前活動的 TTY(如
/dev/tty1
)。- 關鍵函數:
tty_insert_flip_char()
將字符寫入 TTY 的 flip buffer。
- 關鍵函數:
- 行規程(Line Discipline):處理特殊字符(如回車、退格)。默認行規程為
n_tty
。- 關鍵函數:
n_tty_receive_char()
解析字符并執行回顯邏輯。
- 關鍵函數:
- 刷新緩沖區:調用
tty_flip_buffer_push()
推送數據至 TTY 讀隊列。
5. 用戶空間進程讀取輸入
- 前臺進程:Shell(如 Bash)通過
read()
系統調用讀取 TTY 設備的輸入。- 關鍵路徑:
read()
→tty_read()
→copy_from_read_buf()
。
- 關鍵路徑:
- 行編輯模式(Canonical Mode):啟用時,輸入緩存在內核直到用戶按下回車。
6. 字符顯示到終端
- 回顯(Echo):默認
n_tty
規程會自動回顯字符到終端。- 關鍵函數:
n_tty_receive_char()
調用echo_char()
進行回顯。
- 關鍵函數:
- 終端寫入:字符通過
write()
系統調用發送至終端顯示。- 關鍵路徑:
write()
→tty_write()
→do_tty_write()
→ 終端驅動的寫函數。
- 關鍵路徑:
- 終端渲染方式:
- 物理終端:通過顯卡驅動(如
vt_console_print()
)直接輸出。 - 偽終端(PTY):如 SSH 或終端模擬器(如 GNOME Terminal),字符通過 PTY 主從設備傳輸,最終由終端模擬器渲染。
- 物理終端:通過顯卡驅動(如
3.2 在哪里捕獲鍵盤輸入事件?
在 Linux 系統中,鍵盤輸入事件可以在不同層次進行捕獲,主要包括 內核態 和 用戶態,以下是常見的捕獲位置及方法。
1. 內核態捕獲
1.1 中斷處理程序(IRQ 級別)
- 最底層的捕獲方式,在鍵盤觸發 硬件中斷 時,內核的 中斷處理函數 被調用。
- 相關代碼位置:
drivers/input/keyboard/atkbd.c
(PS/2 鍵盤) 或drivers/hid/usbhid/usbkbd.c
(USB 鍵盤)。 - 關鍵函數:
irq_handler_t kbd_event()
(PS/2)usb_kbd_irq()
(USB)- 讀取 掃描碼 并傳遞至 輸入子系統。
1.2 輸入子系統(Input Subsystem)
- 內核的
input_event
機制封裝了鍵盤輸入事件。 - 相關設備節點:
/dev/input/eventX
。 - 關鍵函數:
input_event()
生成鍵盤事件。input_handle_event()
處理事件并分發至用戶空間。
2. 用戶態捕獲
2.1 通過 /dev/input/eventX
捕獲原始輸入事件
-
適用于讀取底層輸入設備(適用于鍵盤監聽、按鍵統計等)。
-
代碼示例(使用
evdev
接口):#include <stdio.h> #include <fcntl.h> #include <linux/input.h>int main() {int fd = open("/dev/input/event2", O_RDONLY);if (fd < 0) {perror("open");return 1;}struct input_event ev;while (read(fd, &ev, sizeof(ev)) > 0) {if (ev.type == EV_KEY && ev.value == 1) {printf("Key %d pressed\n", ev.code);}}close(fd);return 0; }
2.2 通過 /dev/tty
讀取終端輸入
-
適用于讀取當前終端的鍵盤輸入(受 TTY 規程控制)。
-
代碼示例(讀取標準輸入):
#include <stdio.h>int main() {char c;while (1) {c = getchar();printf("Pressed: %c\n", c);}return 0; }
2.3 使用 libinput
監聽鍵盤輸入(Wayland 環境)
-
適用于現代桌面環境(X11/Wayland)。
-
監聽系統級輸入事件,適用于圖形界面應用。
-
代碼示例(Python):
from evdev import InputDevice, categorize, ecodesdev = InputDevice('/dev/input/event2') for event in dev.read_loop():if event.type == ecodes.EV_KEY:print(categorize(event))
2.4 監聽 X11 按鍵事件(X Window System)
-
適用于 GUI 應用,使用
Xlib
監聽按鍵。 -
代碼示例(Python +
python-xlib
):from Xlib import display, Xd = display.Display() r = d.screen().root r.grab_keyboard(True, X.GrabModeAsync, X.GrabModeAsync, X.CurrentTime) while True:event = r.display.next_event()if event.type == X.KeyPress:print("Key pressed")
3.3 選擇合適的捕獲方式
需求 | 推薦方法 |
---|---|
低級輸入監控(掃描碼) | 內核 input_event 或者 input_handle_event API |
監聽所有鍵盤事件 | 讀取 /dev/input/eventX |
監聽終端輸入 | 讀取 /dev/tty |
GUI 應用按鍵捕獲 | Xlib 或 libinput |
不同場景下,可以選擇合適的方式進行鍵盤輸入捕獲。
本文將演示通過 hook 內核函數
input_handle_event
實現鍵盤記錄功能。
4. 代碼詳解
4.1 bpf代碼
4.1.1 整體邏輯
- 加載入口: 代碼通過
SEC("kprobe/input_handle_event")
掛載到內核的input_handle_event
函數。 - 事件過濾: 判斷傳入的
type
是否為EV_KEY
且value
是否等于1,只在按鍵按下時進行處理。 - 數據發送: 如果滿足條件且按鍵碼小于
MAX_KEYS
,則利用bpf_perf_event_output
將事件數據發送到用戶空間。
4.1.2 代碼細節解析
-
頭文件與宏定義:
- 包含了
vmlinux.h
、bpf_helpers.h
、bpf_tracing.h
和bpf_core_read.h
等頭文件,保證代碼能調用內核相關的API。 - 定義了
EV_KEY
和MAX_KEYS
,分別代表按鍵事件類型和允許的最大按鍵數量。
- 包含了
-
事件結構體:
struct event_t {u32 type;u32 code;u32 value; };
該結構體用于保存按鍵事件數據,包括事件類型、按鍵碼和按鍵狀態。
-
kprobe函數:
SEC("kprobe/input_handle_event") int BPF_KPROBE(hook_input_handle_event, struct input_dev *dev, unsigned int type, unsigned int code, int value) {struct event_t event = { 0, };event.type = type;event.code = code;event.value = value;if (type == EV_KEY && value == 1){if(code < MAX_KEYS) {bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &event, sizeof(event));} }return 0; }
- 整體邏輯: 在每次
input_handle_event
調用時,將輸入數據封裝到event
中;判斷條件滿足時,將事件通過perf event
發送。 - 細節說明:
SEC("kprobe/input_handle_event")
:聲明該函數為kprobe處理函數。BPF_KPROBE
宏用于自動生成探針入口。bpf_perf_event_output
負責將數據傳遞到用戶空間,不涉及錯誤處理代碼。
- 整體邏輯: 在每次
4.2 go代碼
4.2.1 main.go
整體邏輯
- 信號注冊: 利用
signal.NotifyContext
注冊系統信號(如SIGINT
、SIGTERM
),確保程序能優雅退出。 - 加載BPF程序: 調用
BpfLoadAndAttach
加載并附加BPF程序文件bpf.o
。 - 創建perf buffer: 初始化
perf buffer
,通過eventsChannel
和lostChannel
接收事件數據及丟失事件統計。 - 事件處理: 啟動一個事件處理循環,實時解析并打印鍵盤事件,直到收到退出信號。
代碼細節解析
- 日志設置: 使用
logrus
設置日志級別和時間格式,使調試信息更直觀。 - 資源管理: 利用
defer
語句確保BPF模塊和perf buffer在程序結束時被正確關閉。 - 事件循環:
該循環實時響應來自內核空間的事件數據。for {// 循環接收事件select {// 接收到事件時打印事件信息case data := <-eventsChannel:var event Eventerr := event.Parse(data)if err != nil {log.Printf("parse event error: %v", err)} else {log.Println(event.String())}} }
4.2.2 event.go
整體邏輯
- 數據結構: 定義了
Event
結構體,用于與內核傳來的event_t
數據一一對應。 - 數據解析:
Parse
方法利用binary.Read
將接收到的字節流轉化為結構體數據。 - 數據展示:
String
方法調用keyStr
函數,將按鍵碼轉換為對應的按鍵名稱,便于直觀展示。
代碼細節解析
package mainimport ("bytes""encoding/binary"
)type Event struct {Type uint32Code uint32Value uint32
}// 解析event數據
func (e *Event) Parse(data []byte) error {err := binary.Read(bytes.NewBuffer(data), binary.LittleEndian, e)if err != nil {return err}return nil
}// 轉換成字符串
func (e *Event) String() string {return keyStr(int(e.Code))
}const MAX_KEYS = 256var keyNames = [MAX_KEYS]string{0: "RESERVED",1: "ESC",2: "1", 3: "2", 4: "3", 5: "4", 6: "5", 7: "6", 8: "7", 9: "8", 10: "9", 11: "0",12: "MINUS", 13: "EQUAL", 14: "BACKSPACE", 15: "TAB",16: "Q", 17: "W", 18: "E", 19: "R", 20: "T", 21: "Y", 22: "U", 23: "I", 24: "O", 25: "P",26: "LEFTBRACE", 27: "RIGHTBRACE", 28: "ENTER", 29: "LEFTCTRL",30: "A", 31: "S", 32: "D", 33: "F", 34: "G", 35: "H", 36: "J", 37: "K", 38: "L", 39: "SEMICOLON",40: "APOSTROPHE", 41: "GRAVE", 42: "LEFTSHIFT", 43: "BACKSLASH",44: "Z", 45: "X", 46: "C", 47: "V", 48: "B", 49: "N", 50: "M", 51: "COMMA", 52: "DOT", 53: "SLASH",54: "RIGHTSHIFT", 55: "KPASTERISK", 56: "LEFTALT", 57: "SPACE",58: "CAPSLOCK", 59: "F1", 60: "F2", 61: "F3", 62: "F4", 63: "F5", 64: "F6", 65: "F7", 66: "F8", 67: "F9", 68: "F10",69: "NUMLOCK", 70: "SCROLLLOCK", 71: "KP7", 72: "KP8", 73: "KP9", 74: "KPMINUS", 75: "KP4", 76: "KP5", 77: "KP6", 78: "KPPLUS",79: "KP1", 80: "KP2", 81: "KP3", 82: "KP0", 83: "KPDOT",85: "ZENKAKUHANKAKU", 86: "102ND", 87: "F11", 88: "F12", 89: "RO", 90: "KATAKANA", 91: "HIRAGANA", 92: "HENKAN", 93: "KATAKANAHIRAGANA", 94: "MUHENKAN",95: "KPJPCOMMA", 96: "KPENTER", 97: "RIGHTCTRL", 98: "KPSLASH", 99: "SYSRQ", 100: "RIGHTALT", 101: "LINEFEED",102: "HOME", 103: "UP", 104: "PAGEUP", 105: "LEFT", 106: "RIGHT", 107: "END", 108: "DOWN", 109: "PAGEDOWN", 110: "INSERT", 111: "DELETE",112: "MACRO", 113: "MUTE", 114: "VOLUMEDOWN", 115: "VOLUMEUP", 116: "POWER", 117: "KPEQUAL", 118: "KPPLUSMINUS", 119: "PAUSE",120: "SCALE", 121: "KPCOMMA", 122: "HANGEUL", 123: "HANJA", 124: "YEN", 125: "LEFTMETA", 126: "RIGHTMETA", 127: "COMPOSE",128: "STOP", 129: "AGAIN", 130: "PROPS", 131: "UNDO", 132: "FRONT", 133: "COPY", 134: "OPEN", 135: "PASTE", 136: "FIND", 137: "CUT",138: "HELP", 139: "MENU", 140: "CALC", 141: "SETUP", 142: "SLEEP", 143: "WAKEUP", 144: "FILE", 145: "SENDFILE", 146: "DELETEFILE", 147: "XFER",148: "PROG1", 149: "PROG2", 150: "WWW", 151: "MSDOS", 152: "COFFEE", 153: "ROTATE_DISPLAY", 154: "CYCLEWINDOWS", 155: "MAIL", 156: "BOOKMARKS",157: "COMPUTER", 158: "BACK", 159: "FORWARD", 160: "CLOSECD", 161: "EJECTCD", 162: "EJECTCLOSECD", 163: "NEXTSONG", 164: "PLAYPAUSE",165: "PREVIOUSSONG", 166: "STOPCD", 167: "RECORD", 168: "REWIND", 169: "PHONE", 170: "ISO", 171: "CONFIG", 172: "HOMEPAGE", 173: "REFRESH",174: "EXIT", 175: "MOVE", 176: "EDIT", 177: "SCROLLUP", 178: "SCROLLDOWN", 179: "KPLEFTPAREN", 180: "KPRIGHTPAREN", 181: "NEW", 182: "REDO",183: "F13", 184: "F14", 185: "F15", 186: "F16", 187: "F17", 188: "F18", 189: "F19", 190: "F20", 191: "F21", 192: "F22", 193: "F23", 194: "F24",200: "PLAYCD", 201: "PAUSECD", 202: "PROG3", 203: "PROG4", 204: "ALL_APPLICATIONS", 205: "SUSPEND", 206: "CLOSE", 207: "PLAY", 208: "FASTFORWARD",209: "BASSBOOST", 210: "PRINT", 211: "HP", 212: "CAMERA", 213: "SOUND", 214: "QUESTION", 215: "EMAIL", 216: "CHAT", 217: "SEARCH", 218: "CONNECT",219: "FINANCE", 220: "SPORT", 221: "SHOP", 222: "ALTERASE", 223: "CANCEL", 224: "BRIGHTNESSDOWN", 225: "BRIGHTNESSUP", 226: "MEDIA",227: "SWITCHVIDEOMODE", 228: "KBDILLUMTOGGLE", 229: "KBDILLUMDOWN", 230: "KBDILLUMUP", 231: "SEND", 232: "REPLY", 233: "FORWARDMAIL", 234: "SAVE",235: "DOCUMENTS", 236: "BATTERY", 237: "BLUETOOTH", 238: "WLAN", 239: "UWB", 240: "UNKNOWN", 241: "VIDEO_NEXT", 242: "VIDEO_PREV",243: "BRIGHTNESS_CYCLE", 244: "BRIGHTNESS_AUTO", 245: "DISPLAY_OFF", 246: "WWAN", 247: "RFKILL", 248: "MICMUTE",
}func keyStr(code int) string {if code < 0 || code >= MAX_KEYS {return "UNKNOWN"}name := keyNames[code]if name == "" {return "UNKNOWN"}return name
}
Parse
方法:- 通過
binary.LittleEndian
格式解析數據,確保與內核傳輸的數據格式一致。
- 通過
String
方法與鍵值映射:- 利用預定義的
keyNames
數組映射按鍵碼到具體按鍵名稱。 keyStr
函數通過判斷碼值范圍返回對應的字符串,如果不存在則返回"UNKNOWN"
。
- 利用預定義的
5. 總結
本文通過Go
語言結合libbpfgo
演示了如何利用eBPF實現鍵盤記錄功能,具體如下:
- 實時性: 直接利用內核
kprobe
捕捉鍵盤事件,無需輪詢,實時性極佳??。 - 高效性: 通過
perf buffer
將數據高效傳遞到用戶空間,確保系統性能不受影響。
該方案不僅適用于鍵盤事件監控,還可以拓展到其他系統監控和安全檢測場景。動手實踐后,你也可以根據業務需求調整代碼,實現更多高級功能。
6. 練習題
- 按鍵過濾: 修改代碼使其只記錄特定按鍵(如
ESC
或ENTER
)的事件,并在用戶空間進行特別處理。 - 數據統計: 增加功能,對每種按鍵的出現次數進行統計,并定時輸出統計結果。
- 擴展應用: 在BPF程序中增加其他類型事件(如鼠標事件)的監控,探索更多內核事件的捕捉方式。
🚀 動手試試吧!遇到問題歡迎留言交流,一起進步!