實時監控TCP Reset信息的二進制hook手藝

玩二進制hook上癮可以,但不能走火入魔,繼監控TCP半連接隊列,計數iptables DROP以後,本文來實時監控TCP Reset報文信息,我保證,本文是這個關於二進制hook手藝的最後一篇。


當協議棧收到一個TCP Reset後,除了遞增一個計數器之外,並沒有記錄任何信息,但是我們仍然需要這些詳細的信息,怎麼辦?

最簡單的方法莫過於stap了,類似於下面的這種:

stap -e 'probe kernel.function("tcp_reset") {printf(" : %x  %x\n", $sk->__sk_common->skc_rcv_saddr, $sk->__sk_common->skc_daddr) }'

但是,stap的背後是kprobe,它基於int3陷入,這種並不適合線上生產環境的常態化監控機制,雖然stap/kprobe也可以基於更加優化的ftrace實現,但是無論是kprobe,還是ftrace,單從名字上看,probe,trace這種,明顯就是讓人來debug用的,換句話說就是用來查問題的,並非常態化的技術,經理不會同意這種技術上線運行。

此外,用iptables的LOG target也可以輕鬆實現TCP Reset審計:

[root@localhost ~]# iptables -A INPUT -p tcp --tcp-flags RST RST -j LOG --log-level notice

當Reset到來時,內核會記錄日誌:

[root@localhost ~]# dmesg
[ 2774.509997] IN=enp0s8 OUT= MAC=08:00:27:ff:26:e6:0a:00:27:00:00:00:08:00 SRC=192.168.56.1 DST=192.168.56.110 LEN=40 TOS=0x10 PREC=0x00 TTL=64 ID=0 DF PROTO=TCP SPT=123 DPT=51071 WINDOW=0 RES=0x00 ACK RST URGP=0
[ 2786.576283] IN=lo OUT= MAC=00:00:00:00:00:00:00:00:00:00:00:00:08:00 SRC=127.0.0.1 DST=127.0.0.1 LEN=40 TOS=0x10 PREC=0x00 TTL=64 ID=52656 DF PROTO=TCP SPT=1234 DPT=47350 WINDOW=0 RES=0x00 ACK RST URGP=0

然而,iptables被詬病已久,絕大多數不明就裏的人對iptales持有偏見,所以很難讓人覺得這樣做很有技術含量。而且就事論事而言,對於這個case,使用iptables卻是有點重了。不管怎樣,這也不是一個好的方案。

手藝人的優勢在於,可以自己動手。雖然這顯得比較麻煩但卻並不是什麼難事,真正的手藝人甚至並不覺得這件事很麻煩,茶餘飯後就當調侃了。

在前面的幾篇關於二進制hook手藝的文章中,我的stub函數代碼幾乎都是用匯編指令拼湊出來的,我曾經抱怨說無法使用C來編寫stub函數,因爲我害怕C編譯器由於對原始函數寄存器使用情況的無知而沖毀它們的值,但這只是意味着用C編碼不是很方便,並非不可能。

事實上,stub函數也是可以用C來寫的,只需要在函數最開始用asm基本內聯彙編進行所有原始函數指令中使用到的寄存器的save操作,然後在返回前restore它們即可保證stub調用的自我封閉。本文將採用這種方式來編寫stub函數。

以下是一個內核模塊代碼:

// dumprst.c
#include <linux/module.h>
#include <net/sock.h>
#include <linux/cpu.h>

char *stub;
char *addr = NULL;

// laddr爲模塊參數,其值爲tcp_reset函數的地址,當然,這並不安全,所以建議用find_symbol
// 真正的手藝人明明知道採用find_symbol會更好,但還是會用這種傳地址的方式
static unsigned long laddr = 0xffffffff8157cc50;
module_param(laddr, ulong, 0644);

void test_stub1(void)
{
  struct sock *sk = NULL;
  unsigned long addr = 0;

  // 由於rdi是tcp_reset的參數,即sock結構體,爲了不讓後面的C代碼破壞rdi,故而將其壓棧。
  asm ("push %rdi"); 
  // 將rdi的值用擴展內聯彙編傳遞給sk變量
  asm ( "mov %%rdi, %0;" :"=m"(addr) : :);
  // 這裏往下,就可以盡情用C語言來編碼了!
  sk = (struct sock *)addr;
  // 打印出四元組,時間戳等信息
  printk("aaaaaaaa yes :%d  dest:%X  source:%X\n",
      sk->sk_state,
      sk->sk_rcv_saddr,
      sk->sk_daddr);
  // 恢復rdi並返回tcp_reset函數
  asm ("pop %rdi");
}

#define FTRACE_SIZE    5
#define POKE_OFFSET    0
#define POKE_LENGTH    5

static void *(*_text_poke_smp)(void *addr, const void *opcode, size_t len);
static struct mutex *_text_mutex;

static int __init hotfix_init(void)
{
  unsigned char e8_call[POKE_LENGTH];
  s32 offset, i;

  // 建議用find_symbol,而不是直接傳地址
  addr = (void *)laddr;

  // 注意這兩個symbol,根據自己的版本自行替換,當然,最好的方法還是find_symbol
  _text_poke_smp = (void *)0xffffffff8163e1f0;
  _text_mutex = (void *)0xffffffff81984920;

  stub = (void *)test_stub1;

  offset = (s32)((long)stub - (long)addr - FTRACE_SIZE);

  // 將tcp_reset的頭5個字節poke成call test_stub1,採用32位相對跳轉,因此請一定保證test_stub1函數處在內核模塊的內存範圍內。
  e8_call[0] = 0xe8;
  (*(s32 *)(&e8_call[1])) = offset;
  for (i = 5; i < POKE_LENGTH; i++) {
    e8_call[i] = 0x90;
  }
  get_online_cpus();
  mutex_lock(_text_mutex);
  _text_poke_smp(&addr[POKE_OFFSET], e8_call, POKE_LENGTH);
  mutex_unlock(_text_mutex);
  put_online_cpus();

  return 0;
}

static void __exit hotfix_exit(void)
{
  // exit函數中負責將poke的內容還原
  get_online_cpus();
  mutex_lock(_text_mutex);
  // 這裏取了巧,有出錯的概率,因爲不敢保證stub是以ftrace nop開頭。
  // 正規的解法應該是save/restore模式
  _text_poke_smp(&addr[POKE_OFFSET], &stub[0], POKE_LENGTH);
  mutex_unlock(_text_mutex);
  put_online_cpus();
}

module_init(hotfix_init);
module_exit(hotfix_exit);
MODULE_LICENSE("GPL");

來試試效果。

telnet一個不存在的端口,期待對方返回的Reset被我們的hook stub捕獲:

[root@localhost ~]# telnet 127.0.0.1 1234
Trying 127.0.0.1...
telnet: connect to address 127.0.0.1: Connection refused
...
[root@localhost ~]# telnet 192.168.56.1 1234
Trying 192.168.56.1...
telnet: connect to address 192.168.56.1: Connection refused

然後看下dmesg:

[root@localhost ~]# dmesg -c
...
[38576.813900] aaaaaaaa yes :2  dest:100007F  source:100007F
...
[35631.501365] aaaaaaaa yes :2  dest:6E38A8C0  source:138A8C0

每當有RESET數據包被協議棧接收,都會打印出這一堆信息。

實現這一切幾乎是無損的,只是一個call/ret的開銷,就是一個函數調用罷了。順便說一句,標準的kpatch也是這麼個原理。

最後,強調兩點。

  • 首先,我寫這些只是希望能幫助工人們理解二進制hook以及kpatch,kprobe的核心原理,在不用重新編譯C代碼的情況下插入一些額外的邏輯,這個和諸如Ret2Text之類的技術完全是兩碼事。當然,我承認,Ret2Text也是另一種手藝,同樣具有可玩性。

  • 其次,對於絕大多數人而言,諸如kprobe,ftrace,eBPF之類的技術,其主要作用是對Linux內核的detect,debug,log,trace,probe…無論哪個動詞,均是一種輔助的 調試手段 ,網上的絕大多數文章在介紹這些技術的時候,也均是這種思路,比如如何使用eBPF來跟蹤內核等等。但我更傾向於 用這些功能來實現一些功能 ,我本身對調試內核沒有興趣,我只對內核可以爲我們實現什麼功能感興趣。

調試內核,跟蹤內核這些,只有在出了問題的時候纔會使用,然而一旦內核出了問題,難道這些不是基本思路嗎?我並於傾向於花費額外的時間去學習如何解決問題,這些都是印在每個人心中的方法論問題,這是一個風格問題。每個人都有自己的一套獨特的解決問題的思路,不光是解決內核問題,就連解決生活中遇到的一切問題,其基本思路都不會有太大的變化,這個思路一般不會改變,不同的只是你手邊現在有什麼工具,以及你現在會用什麼工具。有工具還怕不會用嗎?

當然了,雖然紅色的領帶👔是經理最喜歡的,但藍色帶條紋的也是經理經常戴的,只是不很。


浙江溫州皮鞋溼,下雨進水不會胖。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章