TL; DR:我正在编写一个内核模块,即使您的SSH崩溃了,该模块也会从ICMP有效负载读取命令并在服务器上执行它们。最不耐烦的是,所有代码都在github上。
警告!经验丰富的C程序员冒着流血的眼泪的危险!即使在术语上我也可能是错误的,但是任何批评都值得欢迎。这篇文章供那些对C编程最粗略的想法并想了解Linux内部的人使用。
在我的第一篇文章的评论中提到了SoftEther VPN,它可以模仿某些“常规”协议,特别是HTTPS,ICMP甚至DNS。我可以想象只有第一个的工作,因为我非常熟悉HTTP(S),而且我必须学习ICMP和DNS上的隧道。
是的,我在2020年了解到可以在ICMP数据包中插入任意有效负载。但是迟到总比没有好!并且由于您可以对此做一些事情,所以您需要这样做。由于在我的日常生活中我经常使用命令行(包括通过SSH),因此我首先想到了ICMP Shell的想法。为了组成一个完整的废话宾果游戏,我决定以我仅有粗略想法的语言作为Linux模块编写。这样的外壳将在进程列表中不可见,您可以将其加载到内核中,也不会位于文件系统上,在侦听端口列表中也不会看到任何可疑的东西。就其功能而言,这是一个完善的rootkit,但是当平均负载过高而无法通过SSH登录并至少执行时,我希望对其进行修改并将其用作最后的外壳
echo i > /proc/sysrq-trigger
恢复访问而无需重新启动。
我们将使用文本编辑器,Python和C的基本编程技能,Google和虚拟机,如果一切都中断了,您不介意(可选-本地VirtualBox / KVM /等),走吧!
客户部分
在我看来,对于客户端,我必须编写80行的脚本,但是有些人为我做了所有工作。该代码原来非常简单,可分为10条重要的代码行:
import sys
from scapy.all import sr1, IP, ICMP
if len(sys.argv) < 3:
print('Usage: {} IP "command"'.format(sys.argv[0]))
exit(0)
p = sr1(IP(dst=sys.argv[1])/ICMP()/"run:{}".format(sys.argv[2]))
if p:
p.show()
该脚本有两个参数,一个地址和一个有效负载。在发送之前,有效负载之前带有密钥
run:
,我们将需要它来排除具有随机有效负载的数据包。
内核需要特权才能制作软件包,因此该脚本必须以超级用户权限运行。不要忘记给予执行权限并自行安装scapy。Debian有一个名为的软件包
python3-scapy
。现在,您可以检查所有工作原理。
运行并输出命令
morq@laptop:~/icmpshell$ sudo ./send.py 45.11.26.232 "Hello, world!"
Begin emission:
.Finished sending 1 packets.
*
Received 2 packets, got 1 answers, remaining 0 packets
###[ IP ]###
version = 4
ihl = 5
tos = 0x0
len = 45
id = 17218
flags =
frag = 0
ttl = 58
proto = icmp
chksum = 0x3403
src = 45.11.26.232
dst = 192.168.0.240
\options \
###[ ICMP ]###
type = echo-reply
code = 0
chksum = 0xde03
id = 0x0
seq = 0x0
###[ Raw ]###
load = 'run:Hello, world!
这是在嗅探器中的外观
morq@laptop:~/icmpshell$ sudo tshark -i wlp1s0 -O icmp -f "icmp and host 45.11.26.232"
Running as user "root" and group "root". This could be dangerous.
Capturing on 'wlp1s0'
Frame 1: 59 bytes on wire (472 bits), 59 bytes captured (472 bits) on interface wlp1s0, id 0
Internet Protocol Version 4, Src: 192.168.0.240, Dst: 45.11.26.232
Internet Control Message Protocol
Type: 8 (Echo (ping) request)
Code: 0
Checksum: 0xd603 [correct]
[Checksum Status: Good]
Identifier (BE): 0 (0x0000)
Identifier (LE): 0 (0x0000)
Sequence number (BE): 0 (0x0000)
Sequence number (LE): 0 (0x0000)
Data (17 bytes)
0000 72 75 6e 3a 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 run:Hello, world
0010 21 !
Data: 72756e3a48656c6c6f2c20776f726c6421
[Length: 17]
Frame 2: 59 bytes on wire (472 bits), 59 bytes captured (472 bits) on interface wlp1s0, id 0
Internet Protocol Version 4, Src: 45.11.26.232, Dst: 192.168.0.240
Internet Control Message Protocol
Type: 0 (Echo (ping) reply)
Code: 0
Checksum: 0xde03 [correct]
[Checksum Status: Good]
Identifier (BE): 0 (0x0000)
Identifier (LE): 0 (0x0000)
Sequence number (BE): 0 (0x0000)
Sequence number (LE): 0 (0x0000)
[Request frame: 1]
[Response time: 19.094 ms]
Data (17 bytes)
0000 72 75 6e 3a 48 65 6c 6c 6f 2c 20 77 6f 72 6c 64 run:Hello, world
0010 21 !
Data: 72756e3a48656c6c6f2c20776f726c6421
[Length: 17]
^C2 packets captured
响应数据包中的有效载荷不变。
内核模块
要使用Debian构建虚拟机,您至少需要
make
和linux-headers-amd64
,其余的将作为依赖项加强。我不会在文章中给出完整的代码,您可以在github上克隆它。
挂钩设置
首先,我们需要两个函数来加载和卸载模块。不需要卸载功能,但是此功能
rmmod
将不起作用,仅在关闭模块后才能卸载模块。
#include <linux/module.h>
#include <linux/netfilter_ipv4.h>
static struct nf_hook_ops nfho;
static int __init startup(void)
{
nfho.hook = icmp_cmd_executor;
nfho.hooknum = NF_INET_PRE_ROUTING;
nfho.pf = PF_INET;
nfho.priority = NF_IP_PRI_FIRST;
nf_register_net_hook(&init_net, &nfho);
return 0;
}
static void __exit cleanup(void)
{
nf_unregister_net_hook(&init_net, &nfho);
}
MODULE_LICENSE("GPL");
module_init(startup);
module_exit(cleanup);
这里发生了什么:
- 引入了两个头文件来操作模块本身和netfilter。
- , . , . — , :
nfho.hook = icmp_cmd_executor;
.
:NF_INET_PRE_ROUTING
, .NF_INET_POST_ROUTING
.
IPv4:nfho.pf = PF_INET;
.
:nfho.priority = NF_IP_PRI_FIRST;
:nf_register_net_hook(&init_net, &nfho);
- .
- , .
-
module_init()
module_exit()
.
现在我们需要提取有效载荷,这是最困难的任务。内核没有用于处理有效负载的内置函数,您只能解析更高级别协议的标头。
#include <linux/ip.h>
#include <linux/icmp.h>
#define MAX_CMD_LEN 1976
char cmd_string[MAX_CMD_LEN];
struct work_struct my_work;
DECLARE_WORK(my_work, work_handler);
static unsigned int icmp_cmd_executor(void *priv, struct sk_buff *skb, const struct nf_hook_state *state)
{
struct iphdr *iph;
struct icmphdr *icmph;
unsigned char *user_data;
unsigned char *tail;
unsigned char *i;
int j = 0;
iph = ip_hdr(skb);
icmph = icmp_hdr(skb);
if (iph->protocol != IPPROTO_ICMP) {
return NF_ACCEPT;
}
if (icmph->type != ICMP_ECHO) {
return NF_ACCEPT;
}
user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
tail = skb_tail_pointer(skb);
j = 0;
for (i = user_data; i != tail; ++i) {
char c = *(char *)i;
cmd_string[j] = c;
j++;
if (c == '\0')
break;
if (j == MAX_CMD_LEN) {
cmd_string[j] = '\0';
break;
}
}
if (strncmp(cmd_string, "run:", 4) != 0) {
return NF_ACCEPT;
} else {
for (j = 0; j <= sizeof(cmd_string)/sizeof(cmd_string[0])-4; j++) {
cmd_string[j] = cmd_string[j+4];
if (cmd_string[j] == '\0')
break;
}
}
schedule_work(&my_work);
return NF_ACCEPT;
}
发生了什么:
- 我必须包括其他头文件,这一次才能处理IP和ICMP头。
- 指定字符串的最大长度:
#define MAX_CMD_LEN 1976
。为什么要这样呢?因为编译器是最重要的!他们已经告诉我,我需要处理堆栈和堆,总有一天,我一定会这样做,甚至可以更正代码。立即设置团队将基于的字符串:char cmd_string[MAX_CMD_LEN];
。它应该在所有函数中都可见,我将在第9段中对此进行详细讨论。 - (
struct work_struct my_work;
) (DECLARE_WORK(my_work, work_handler);
). , , . - , . ,
skb
. , , . - , , .
struct iphdr *iph; struct icmphdr *icmph; unsigned char *user_data; unsigned char *tail; unsigned char *i; int j = 0;
- . ICMP Echo, ICMP- Echo-.
NF_ACCEPT
, ,NF_DROP
.
iph = ip_hdr(skb); icmph = icmp_hdr(skb); if (iph->protocol != IPPROTO_ICMP) { return NF_ACCEPT; } if (icmph->type != ICMP_ECHO) { return NF_ACCEPT; }
, IP. C : - . , ! - , , . . , ICMP .
icmph
:user_data = (unsigned char *)((unsigned char *)icmph + (sizeof(icmph)));
skb
, :tail = skb_tail_pointer(skb);
.
, . - ,
cmd_string
,run:
, , , . - , :
schedule_work(&my_work);
. , .schedule_work()
, . . , , kernel panic. ! - , .
此功能最直接。它的名称在中给出
DECLARE_WORK()
,类型和接受的参数并不有趣。我们采用命令行并将其完全传递给shell。让他处理解析,搜索二进制文件以及他自己的所有其他内容。
static void work_handler(struct work_struct * work)
{
static char *argv[] = {"/bin/sh", "-c", cmd_string, NULL};
static char *envp[] = {"PATH=/bin:/sbin", NULL};
call_usermodehelper(argv[0], argv, envp, UMH_WAIT_PROC);
}
- 我们将参数设置为字符串数组
argv[]
。我将假设每个人都知道程序实际上是以这种方式运行的,而不是作为带有空格的实线运行的。 - 设置环境变量。我只插入了具有最少路径集的PATH,希望所有路径都已
/bin
与/usr/bin
和结合/sbin
在一起/usr/sbin
。在实践中,其他途径几乎无关紧要。 - , !
call_usermodehelper()
. , , . , , . , (UMH_WAIT_PROC
), (UMH_WAIT_EXEC
) (UMH_NO_WAIT
).UMH_KILLABLE
, .
构建内核模块是通过内核make框架完成的。
make
在链接到内核版本(在此定义:)的特殊目录中调用它KERNELDIR:=/lib/modules/$(shell uname -r)/build
,并将模块的位置传递到参数M
中的变量。icmpshell.ko和clean目标完全使用此框架。输入obj-m
指定将转换为模块的目标文件。他main.o
在icmpshell.o
(icmpshell-objs = main.o
)中删除的语法在我看来并不十分合逻辑,但可以这样。
放置:。加载:。完成后,您可以检查:。如果文件出现在您的计算机上,并且包含发送请求的日期,那么您所做的一切对,我所做的一切对。
KERNELDIR:=/lib/modules/$(shell uname -r)/build
obj-m = icmpshell.o
icmpshell-objs = main.o
all: icmpshell.ko
icmpshell.ko: main.c
make -C $(KERNELDIR) M=$(PWD) modules
clean:
make -C $(KERNELDIR) M=$(PWD) clean
make
insmod icmpshell.ko
sudo ./send.py 45.11.26.232 "date > /tmp/test"
/tmp/test
结论
我对核工程的第一次经验比我预期的要简单得多。即使没有C开发经验,也只专注于编译器提示和Google输出,我仍然能够编写一个工作模块,感觉就像是内核黑客,同时还是个脚本小子。另外,我转到了Kernel Newbies频道,他们告诉我在钩子本身内部使用
schedule_work()
而不是打电话call_usermodehelper()
,并羞辱了我,理所当然地怀疑是欺诈。一百行代码在业余时间花费了我大约一周的开发时间。一次成功的经历摧毁了我个人关于系统开发极其复杂的神话。
如果有人同意在github上进行代码审查,我将不胜感激。我敢肯定我犯了很多愚蠢的错误,尤其是在处理字符串时。