实时监控TCP Reset信息的二进制hook手艺_hook tcp

玩二进制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来跟踪内核等等。但我更倾向于 用这些功能来实现一些功能 ,我本身对调试内核没有兴趣,我只对内核可以为我们实现什么功能感兴趣。

调试内核,跟踪内核这些,只有在出了问题的时候才会使用,然而一旦内核出了问题,难道这些不是基本思路吗?我并于倾向于花费额外的时间去学习如何解决问题,这些都是印在每个人心中的方法论问题,这是一个风格问题。每个人都有自己的一套独特的解决问题的思路,不光是解决内核问题,就连解决生活中遇到的一切问题,其基本思路都不会有太大的变化,这个思路一般不会改变,不同的只是你手边现在有什么工具,以及你现在会用什么工具。有工具还怕不会用吗?

当然了,虽然红色的领带👔是经理最喜欢的,但蓝色带条纹的也是经理经常戴的,只是不很。


浙江温州皮鞋湿,下雨进水不会胖。

原文链接: https://blog.csdn.net/dog250/article/details/105231866

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍;

也有高质量的技术群,里面有嵌入式、搜广推等BAT大佬

    实时监控TCP Reset信息的二进制hook手艺_hook tcp

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/406049

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年4月26日 上午9:35
下一篇 2023年4月26日 上午9:35

相关推荐