内核中的 telnet 服务

来源:developerWorks 中国 作者:杨 广翔
  
通过在 Linux 的 ICMP 协议栈中嵌入一个私有的类 telnet 服务(称为 ktelnetd),开发人员可以从外部连接到嵌入式设备的内核中,并利用预置的命令检查内核的参数,甚至调整运行状态。这种方法有助于开发人员诊断和定位系统的异常。

内核中的 telnet 服务

开发人员依赖串口对嵌入式 Linux 设备进行调试和开发。串口是开发人员与设备之间的操作界面。依靠它,开发人员获得与操作系统交互的能力。对于拥有网络界面的硬件设备来说, telnet 是另一个选择。通过 telnet 接入到设备将使开发人员获得与串口界面相同能力。

但是,当内核中的代码(如驱动)产生无限循环或其它耗时操作时,操作系统将无法调度和处理串口界面上的输入。串口界面将停止响应开发人员的操作。一些其它的异常也会造成串口无法工作。同时,出现这些问题时刻, telnet 服务也往往停止了工作。开发人员无法通过 telnet 界面来接入设备,也就无力进行任何调试操作了。

在实际的开发环境中,出现上述类似问题时,运行在设备上的 Linux 操作系统仍然可以响应外部的 PING 请求。这意味着操作系统的 ICMP 协议栈仍然可以正常工作。那么,如果可以将 telnet 服务植入 Linux 操作系统的 ICMP 协议栈中,我们就可以得到一个可靠性更高的接入界面。另一方面,由于 ICMP 协议栈是运行在 Linux 内核中,那么在协议栈中的 telnet 服务将不会受到任何限制,而获得访问内核的能力。通过这个接入界面,开发人员可以自由的访问内核中的数据,甚至调整内核中的某些参数和运行状态。这个方式要比 proc 界面方便了许多。

为了植入 telnet 服务,我们首先需要了解 Linux 的 ICMP 协议栈的工作原理。在这之前,简单的介绍 ICMP 协议是有必要的。

ICMP 是 IP 协议的一个组成部分。 ICMP 报文使用 IP 数据报 ( UDP ) 的方式传输。 ICMP 报文中所含的 IP 头部中需要注明协议类型。图 1 是 IP 头部的结构图。


图 1. IP 头部
IP 头部

如图 1 所示,对于 ICMP 报文,位于 IP 头部第 10 个字节的“协议”字段必须为 1 。这指明了协议类型为 ICMP 。在内核代码中,这个类型被定义为 IPPROTO_ICMP 。 IP 头部后面的载荷为 ICMP 报文。 ICMP 报文的格式见图 2 。


图 2. ICMP 头部
ICMP 头部

所有 ICMP 报文的前 4 个字节即为 ICMP 头部。它们总是相同的,即类型、代码和检验和。 ICMP 报文有多种类型。 PING 请求属于“请求回显”类型。这个类型的值被定义为 8 。在内核代码中则定义为 ICMP_ECHO 。在 PING 请求中,ICMP 头部之后就是净载荷。收到 PING 请求的主机通过将净载荷原封不动的回应回去来声明通信链路处于正常工作状态。回应的 ICMP 报文的类型为 ICMP_ECHOREPLY ,其值为 0 。

在 Linux 操作系统中,ICMP 协议栈的代码位于目录 /linux-2.6.x/net/ipv4 中的 icmp.c 文件。这部分代码属于 Linux 操作系统的网络部分。

Linux 对于 PING 请求的简明处理流程是这样的:

函数 icmp_rcv 在收到被封装到 SKB 结构中的报文后,获取到 ICMP 头部。根据头部中携带的类型,调用相对应的处理函数。代码如下:

int icmp_rcv(struct sk_buff *skb) 
        { 
               ……
               icmp_pointers[icmph->type].handler(skb); 
               ……
        }

对于 PING 请求来说, icmph->type 的值为 ICMP_ECHO ,也就是 8 。对应的处理函数即是 icmp_echo 。 在 icmp_echo 函数中,修改收到的 SKB 结构中的 ICMP 头部,使其类型为 ICMP_ECHOREPLY 。然后,调用 icmp_reply 函数,回应收到的 PING 请求。

从整个流程来看, Linux 对于 PING 请求报文的处理是较为简单的。这就为我们植入 telnet 服务带来了便利。

RFC854 定义了 telnet 协议的规范。 telnet 协议基于 TCP 传输协议。因此, telnet 协议无法平滑的移植到 ICMP 协议栈中。定义一个私有的协议是唯一的选择。为了可以在 ICMP 协议栈中实现 telnet 服务,这个协议必须基于数据报( UDP ),而且,这个协议必须基于 ICMP 协议,并且使用 ICMP_ECHO 类型。在这个限制条件下,我们可以利用这个类型的 ICMP 报文中的净载荷来承载私有协议。

我们定义的私有协议的结构是:


图 3. 私有协议结构 - 请求
私有协议结构-请求

请求报文中,除去 4 个字节的 ICMP 报文头部,前两个字节是 magic 数,用来标明报文是我们的私有协议。这个 maigc 可任意定义,比如是两个 ASCII 码字符“ LX ”。后两个字节指示命令的长度。在长度字段之后,是一个字符串。它是客户端请求执行的命令。以字符串的形式存储在长度字段之后,并以 0 为结束符。

与之对应的是,回应报文中携带的是字符串形式的命令执行结果。如下图:


图 4. 私有协议结构 - 回应
私有协议结构 - 回应

由于利用这个私有协议实现的类 telnet 服务是在内核中实现的。因此,这个私有协议被命名为 ktelnet 协议。

完整的系统由三个部分组成: ktelnet 、 ktelnetd 和 kshell 。

ktelnet 运行在客户端,开发人员使用它接入 ktelnetd 。通常情况下,ktelnet 是一个运行在 windows 的可执行程序。

ktelnetd 运行在嵌入式设备上,接收来自 ktelnet 的报文,并从报文解析出命令。

kshell 是一个逻辑模块。它与 ktelnetd 一起运行在内核中,负责执行 ktelnetd 提交的命令。执行结果将反馈给 ktelnetd 。 ktelnetd 将执行结果返回给 ktelnet 。

ktelnetd 是 ktelnet 的服务端。 Kshell 则是 ktelnetd 的后台支持模块。 ktelnetd 和 kshell 与 ICMP 协议栈一起运行在 Linux 内核中。

这样的一个系统其执行过程如下图所示:


图 5. 系统工作流程
系统工作流程

Ktelnet 在启动后,生成和维护一个 CLI (命令行)界面。它利用这个界面接收用户输入的命令,再将命令按上一节描述的 ktelnet 协议格式封装成 ICMP 报文发送给 ktelnetd 。

ktelnted 接收来自 ktelnet 的报文,将从报文中解析出的命令提交给 kshell 。 kshell 维护着一个命令列表。这个列表中记录了命令名称和相应的执行函数。 kshell 根据 ktelnetd 提交的命令,调用相应的函数,完成这个命令的执行任务。执行的结果以字符串的形式保存在内存中。 ktelnetd 在命令执行完毕后,生成一个 ICMP_ECHOREPLY 报文,将执行结果封装到报文中,回应给 ktelnet 。

ktelnet 从收到的报文中解析出执行结果,并将结果输出在 CLI 界面上。由于命令和执行结果都是以字符串的形式表现的,因此,在整个过程中,用户的使用感受与 telnet 服务一样。

在通常的应用环境中,ktelnet 应基于 Windows 。即使是 Linux 开发人员也往往使用 Windows 做为客户端。

ktelnet 首先需要初始化 winsock 库,创建一个原始( RAW )的 socket 。

		WSADATA wsd; 
        SOCKET s; 

        /* init windows socket libarary */ 
        if (WSAStartup(MAKEWORD(2,2), &wsd)!=0) 
               perror ("init fail\n"); 

        if ((s = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP)) < 0) 
        { 
               perror("ktelnet: socket fail\n"); 
               return -1; 
        }

然后,输出一个提示符(#),等待用户输入命令。

	char cmd[128], c; 

        printf("#"); /* print prompt */ 

        i = 0; 
        memset(cmd, 0, sizeof(cmd)); 
        while ( (c=getchar()) != '\n' && (i < (sizeof(cmd)-1))) 
        { 
               cmd[i++] = c; 
        }

当用户结束命令的输入按下回车键后,保存在字符数组 cmd 中的数据需要被封装到 ICMP 报文中发送出去。我们定义了下面所示的 ICMP 头部。

	struct icmphdr { 
            __u8   type; 
    __u8   code; 
    __u16  checksum; 
            union { 
               struct { 
                    __u16    id; 
                    __u16    sequence; 
               } echo; 
               __u32    gateway; 
               struct { 
                    __u16    __unused; 
            __u16    mtu; 
               } frag; 
            } un; 
        };

利用这个头部结构,我们可以构造出一个 ICMP_ECHO 类型的报文,再将用户输入的命令存入这个报文的净载荷中。

struct icmphdr *icp; 
        struct sockaddr whereto; 

        icp = (struct icmphdr *)buff; 
        icp->type = ICMP_ECHO;                             /* TYPE */ 
        icp->code = 0; 
        icp->checksum = 0; 
        icp->un.echo.id = 0x400; 			 /* ID */ 

        /* compute ICMP checksum here */ 
        icp->checksum = in_cksum((u_short *)icp, size); 

        payload = buff + sizeof(struct icmphdr);      /* payload of icmp */ 
        payload[0] = 'L';                                  /* magic */ 
        payload[1] = 'X'; 
        *((short *)(payload+2)) = htons(strlen(cmd)); /* length */ 
        strcpy(&payload[4], cmd);                     /* command */ 

        i = sendto(s, (char *)buff, size, 0, &whereto, sizeof(struct sockaddr));

ktelnetd 执行了命令后,执行结果仍使用 ICMP 报文发送回来。 ktelnet 需要解析这些回应报文。

struct iphdr { 
                __u8 	 ihl:4, 
                       version:4; 
                __u8 	 tos; 
                __u16 	 tot_len; 
                __u16 	 id; 
                __u16 	 frag_off; 
                __u8 	 ttl; 
                __u8 	 protocol; 
                __u16 	 check; 
                __u32 	 saddr; 
                __u32 	 daddr; 
                /*The options start here. */ 
        }; 

        struct iphdr *ip; 

        /* Check the IP header */ 
        ip = (struct iphdr *)buff; 
        hlen = ip->ihl << 2;                /* head length */ 
 
        payload  = buff + hlen + sizeof(struct icmphdr); 

        /* check magic */ 
        if (payload[0] != 'L' || payload[1] != 'X') 
                return -1; 

        printf("%s", payload+4);            /* print result */

时间:2009-07-24 13:11 来源:developerWorks 中国 作者:杨 广翔 原文链接

好文,顶一下
(3)
100%
文章真差,踩一下
(0)
0%
------分隔线----------------------------


把开源带在你的身边-精美linux小纪念品
无觅相关文章插件,快速提升流量