深入Linux网络核心堆栈netfilter详解精品资料.doc

上传人:封****n 文档编号:96697038 上传时间:2024-03-10 格式:DOC 页数:51 大小:202KB
返回 下载 相关 举报
深入Linux网络核心堆栈netfilter详解精品资料.doc_第1页
第1页 / 共51页
深入Linux网络核心堆栈netfilter详解精品资料.doc_第2页
第2页 / 共51页
点击查看更多>>
资源描述

《深入Linux网络核心堆栈netfilter详解精品资料.doc》由会员分享,可在线阅读,更多相关《深入Linux网络核心堆栈netfilter详解精品资料.doc(51页珍藏版)》请在taowenge.com淘文阁网|工程机械CAD图纸|机械工程制图|CAD装配图下载|SolidWorks_CaTia_CAD_UG_PROE_设计图分享下载上搜索。

1、深入Linux网络核心堆栈-netfilter详解目录1 - 简介 1.1 - 本文涉及的内容 1.2 - 本文不涉及的内容2 - 各种Netfilter hook及其用法 2.1 - Linux内核对数据包的处理 2.2 - Netfilter对IPv4的hook3 - 注册和注销Netfilter hook4 - Netfilter 基本的数据报过滤技术1 4.1 - 深入hook函数 4.2 - 基于接口进行过滤 4.3 - 基于地址进行过滤 4.4 - 基于TCP端口进行过滤5 - Netfilter hook的其它可能用法 5.1 - 隐藏后门的守护进程 5.2 - 基于内核的FTP

2、密码嗅探器 5.2.1 - 源代码 : nfsniff.c 5.2.2 - 源代码 : getpass.c6 - 在Libpcap中隐藏网络通信 6.1 - SOCK_PACKET、SOCK_RAW与Libpcap 6.2 - 给狼披上羊皮7 - 结束语A - 轻量级防火墙 A.1 - 概述 A.2 - 源代码 : lwfw.c A.3 - 头文件 : lwfw.hB - 第6节中的源代码- 1 - 简介 本文将向你展示,Linux的网络堆栈的一些怪异行为(并不一定是弱点)如何被用于邪恶的或者是其它形形色色的目的。在这里将要讨论的是将表面上看起来合法的Netfilter hook用于后门的通信

3、,以及一种使特定的网络通信在运行于本机的基于Libpcap的嗅探器中消声匿迹的技术。 Netfilter是Linux 2.4内核的一个子系统,Netfiler使得诸如数据包过滤、网络地址转换(NAT)以及网络连接跟踪等技巧成为可能,这些功能仅通过使用内核网络代码提供的各式各样的hook既可以完成。这些hook位于内核代码中,要么是静态链接的,要么是以动态加载的模块的形式存在。可以为指定的网络事件注册相应的回调函数,数据包的接收就是这样一个例子。- 1.1 - 本文涉及的内容 本文讨论模块编写者如何利用Netfilter hook来实现任意目的以及如何将将网络通信在基于Libpcap的应用程序中

4、隐藏。虽然Linux 2.4支持对IPv4、IPv6以及DECnet的hook,但在本文中将只讨论关于IPv4的话题,虽然如此,大部分关于IPv4的内容都同样可以运用于其它几种协议。出于教学的目的,附录A提供了一个可用的、提供基本的包过滤的内核模块。本文中所有的开发和试验都在运行于Intel主机上的Linux 2.4.5中完成。对Netfilter hook功能的测试在环回接口、以太网接口以及调制解调器点对点接口上完成。 本文也是出于我对Netfilter完全理解的尝试的兴趣而写的。我并不能保证文中附带的任何代码100%的没有错误,但是我已经测试了所有在这里提供的代码。我已经受够了核心错误的折

5、磨,因此真诚的希望你不会再如此。同样,我不会为任何按照本文所述进行的操作中可能发生的损害承担责任。本文假定读者熟悉C语言编程并且有一定的关于可加载模块的经验。 欢迎对本文中出现的错误进行批评指正,我同时开诚布公的接受对本文的改进以及其它各种关于Netfilter的优秀技巧的建议。 - 1.2 - 本文不涉及的内容 本文不是一个完全的关于Netfilter的细节上的参考资料,同样,也不是一个关于iptables的命令的参考资料。如果你想了解更多的关于iptables的命令,请参考相关的手册页。 好了,让我们从Netfilter的使用介绍开始 . - 2 - 各种Netfilter hook及其用

6、法- 2.1 - Linux内核对数据包的处理 看起来好像是我很喜欢深入到诸如Linux的数据包处理以及事件的发生以及跟踪每一个Netfilter hook这样的血淋淋的细节中,事实并非如此!原因很简单,Harald Welte已经写了一篇关于这个话题的优秀的文章?Journey of a Packet Through the Linux 2.4 Network Stack。如果你想了解更多的关于Linux数据包处理的内容,我强烈推荐你去拜读这篇文章。现在,仅需要理解:当数据包游历Linux内核的网络堆栈时,它穿过了几个hook点,在这里,数据包可以被分析并且选择是保留还是丢弃,这些hook点

7、就是Netfilter hook。 - 2.2 - Netfilter对IPv4的hook Netfilter中定义了五个关于IPv4的hook,对这些符号的声明可以在linux/netfilter_ipv4.h中找到。这些hook列在下面的表中: 表1 : 可用的IPv4 hook Hook 调用的时机NF_IP_PRE_ROUTING 在完整性校验之后,选路确定之前NF_IP_LOCAL_IN 在选路确定之后,且数据包的目的是本地主机NF_IP_FORWARD 目的地是其它主机地数据包NF_IP_LOCAL_OUT 来自本机进程的数据包在其离开本地主机的过程中NF_IP_POST_ROUT

8、ING 在数据包离开本地主机“上线”之前 NF_IP_PRE_ROUTING这个hook是数据包被接收到之后调用的第一个hook,这个hook既是稍后将要描述的模块所用到的。当然,其它的hook同样非常有用,但是在这里,我们的焦点是在NF_IP_PRE_ROUTING这个hook上。 在hook函数完成了对数据包所需的任何的操作之后,它们必须返回下列预定义的Netfilter返回值中的一个: 表2 : Netfilter返回值 返回值 含义NF_DROP 丢弃该数据包NF_ACCEPT 保留该数据包NF_STOLEN 忘掉该数据包NF_QUEUE 将该数据包插入到用户空间NF_REPEAT 再

9、次调用该hook函数 NF_DROP这个返回值的含义是该数据包将被完全的丢弃,所有为它分配的资源都应当被释放。NF_ACCEPT这个返回值告诉Netfilter:到目前为止,该数据包还是被接受的并且该数据包应当被递交到网络堆栈的下一个阶段。NF_STOLEN是一个有趣的返回值,因为它告诉Netfilter,“忘掉”这个数据包。这里告诉Netfilter的是:该hook函数将从此开始对数据包的处理,并且Netfilter应当放弃对该数据包做任何的处理。但是,这并不意味着该数据包的资源已经被释放。这个数据包以及它独自的sk_buff数据结构仍然有效,只是hook函数从Netfilter获取了该数据

10、包的所有权。不幸的是,我还不是完全的清楚NF_QUEUE到底是如果工作的,因此在这里我不讨论它。最后一个返回值NF_REPEAT请求Netfilter再次调用这个hook函数。显然,使用者应当谨慎使用NF_REPEAT这个返回值,以免造成死循环。 -3 - 注册和注销Netfilter hook 注册一个hook函数是围绕nf_hook_ops数据结构的一个非常简单的操作,nf_hook_ops数据结构在linux/netfilter.h中定义,该数据结构的定义如下: struct nf_hook_ops struct list_head list; /* 此下的值由用户填充 */ nf_ho

11、okfn *hook; int pf; int hooknum; /* Hook以升序的优先级排序 */ int priority; ; 该数据结构中的list成员用于维护Netfilter hook的列表,并且不是用户在注册hook时需要关心的重点。hook成员是一个指向nf_hookfn类型的函数的指针,该函数是这个hook被调用时执行的函数。nf_hookfn同样在linux/netfilter.h中定义。pf这个成员用于指定协议族。有效的协议族在linux/socket.h中列出,但对于IPv4我们希望使用协议族PF_INET。hooknum这个成员用于指定安装的这个函数对应的具体的h

12、ook类型,其值为表1中列出的值之一。最后,priority这个成员用于指定在执行的顺序中,这个hook函数应当在被放在什么地方。对于IPv4,可用的值在linux/netfilter_ipv4.h的nf_ip_hook_priorities枚举中定义。出于示范的目的,在后面的模块中我们将使用NF_IP_PRI_FIRST。 注册一个Netfilter hook需要调用nf_register_hook()函数,以及用到一个nf_hook_ops数据结构。nf_register_hook()函数以一个nf_hook_ops数据结构的地址作为参数并且返回一个整型的值。但是,如果你真正的看了在net

13、/core/netfilter.c中的nf_register_hook()函数的实现代码,你会发现该函数总是返回0。以下提供的是一个示例代码,该示例代码简单的注册了一个丢弃所有到达的数据包的函数。该代码同时展示了Netfilter的返回值如何被解析。 示例代码1 : Netfilter hook的注册/* * 安装一个丢弃所有到达的数据包的Netfilter hook函数的示例代码 */#define _KERNEL_#define MODULE#include #include #include #include /* 用于注册我们的函数的数据结构 */static struct nf_ho

14、ok_ops nfho;/* 注册的hook函数的实现 */unsigned int hook_func(unsigned int hooknum, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, int (*okfn)(struct sk_buff *) return NF_DROP; /* 丢弃所有的数据包 */* 初始化程序 */int init_module() /* 填充我们的hook数据结构 */ nfho.hook = hook_func; /* 处理函数 */ nfh

15、o.hooknum = NF_IP_PRE_ROUTING; /* 使用IPv4的第一个hook */ nfho.pf = PF_INET; nfho.priority = NF_IP_PRI_FIRST; /* 让我们的函数首先执行 */ nf_register_hook(&nfho); return 0;/* 清除程序 */void cleanup_module() nf_unregister_hook(&nfho); 这就是全部内容,从示例代码1中,你可以看到,注销一个Netfilter hook是一件很简单事情,只需要调用nf_unregister_hook()函数,并且以你之前用于注

16、册这个hook时用到的相同的数据结构的地址作为参数。 - 4 - Netfilter 基本的数据报过滤技术- 4.1 - 深入hook函数 现在是到了看看什么数据被传递到hook函数中以及这些数据如何被用于做过滤选择的时候了。那么,让我们更深入的看看nf_hookfn函数的原型吧。这个函数原型在linux/netfilter.h中给出,如下: typedef unsigned int nf_hookfn(unsigned int hooknum, struct sk_buff *skb, const struct net_device *in, const struct net_device

17、*out, int (*okfn)(struct sk_buff *); nf_hookfn函数的第一个参数用于指定表1中给出的hook类型中的一个。第二个参数更加有趣,它是一个指向指针的指针,该指针指向的指针指向一个sk_buff数据结构,网络堆栈用sk_buff数据结构来描述数据包。这个数据结构在linux/skbuff.h中定义,由于它的内容太多,在这里我将仅列出其中有意义的部分。 sk_buff数据结构中最有用的部分可能就是那三个描述传输层包头(例如:UDP, TCP, ICMP, SPX)、网络层包头(例如:IPv4/6, IPX, RAW)以及链路层包头(例如:以太网或者RAW)的

18、联合(union)了。这三个联合的名字分别是h、nh以及mac。这些联合包含了几个结构,依赖于具体的数据包中使用的协议。使用者应当注意:传输层包头和网络层包头可能是指向内存中的同一个位置。这是TCP数据包可能出现的情况,其中h和nh都应当被看作是指向IP头结构的指针。这意味着尝试通过h-th获取一个值,并认为该指针指向一个TCP头,将会得到错误的结果。因为h-th实际上是指向的IP头,与nh-iph得到的结果相同。 接下来让我们感兴趣的其它部分是len和data这两个域。len指定了从data开始的数据包中的数据的总长度。好了,现在我们知道如何在sk_buff数据结构中分别访问协议头和数据包中

19、的数据了。Netfilter hook函数中有用的信息中其它的有趣的部分是什么呢? 紧跟在skb之后的两个参数是指向net_device数据结构的指针,net_device数据结构被Linux内核用于描述所有类型的网络接口。这两个参数中的第一个?in,用于描述数据包到达的接口,毫无疑问,参数out用于描述数据包离开的接口。必须明白,在通常情况下,这两个参数中将只有一个被提供。例如:参数in只用于NF_IP_PRE_ROUTING和NF_IP_LOCAL_IN hook,参数out只用于NF_IP_LOCAL_OUT和NF_IP_POST_ROUTING hook。在这一个阶段中,我还没有测试对

20、于NF_IP_FORWARD hook,这两个参数中哪些是有效的,但是如果你能在使用之前先确定这些指针是非空的,那么你是非常优秀的! 最后,传递给hook函数的最后一个参数是一个命名为okfn函数指针,该函数以一个sk_buff数据结构作为它唯一的参数,并且返回一个整型的值。我不是很确定这个函数是干什么用的,在net/core/netfilter.c中查看,有两个地方调用了这个okfn函数。这两个地方是分别在函数nf_hook_slow()中以及函数nf_reinject()中,在其中的某个位置,当Netfilter hook的返回值为NF_ACCEPT时被调用。如果任何人有更多的关于okfn

21、函数的信息,请务必告知。 * 译注:Linux核心网络堆栈中有一个全局变量 : struct list_head nf_hooksNPROTONF_MAX_HOOKS,该变量是一个二维数组,其中第一维用于指定协议族,第二维用于指定hook的类型(表1中定义的类型)。注册一个Netfilter hook实际就是在由协议族和hook类型确定的链表中添加一个新的节点。 以下代码摘自 net/core/netfilter,nf_register_hook()函数的实现:int nf_register_hook(struct nf_hook_ops *reg) struct list_head *i;

22、br_write_lock_bh(BR_NETPROTO_LOCK); for (i = nf_hooksreg-pfreg-hooknum.next; i != &nf_hooksreg-pfreg-hooknum; i = i-next) if (reg-priority priority) break; list_add(?-list, i-prev); br_write_unlock_bh(BR_NETPROTO_LOCK); return 0; Netfilter中定义了一个宏NF_HOOK,作者在前面提到的nf_hook_slow()函数实际上就是NF_HOOK宏定义替换的对象,在

23、NF_HOOK中执行注册的hook函数。NF_HOOK在Linux核心网络堆栈的适当的地方以适当的参数调用。例如,在ip_rcv()函数(位于net/ipv4/ip_input.c)的最后部分,调用NF_HOOK函数,执行NF_IP_PRE_ROUTING类型的hook。ip_rcv()是Linux核心网络堆栈中用于接收IPv4数据包的主要函数。在NF_HOOK的参数中,页包含一个okfn函数指针,该函数是用于数据包被接收后完成后续的操作,例如在ip_rcv中调用的NF_HOOK中的okfn函数指针指向ip_rcv_finish()函数(位于net/ipv4/ip_input.c),该函数用于

24、IP数据包被接收后的诸如IP选项处理等后续处理。 如果在内核编译参数中取消CONFIG_NETFILTER宏定义,NF_HOOK宏定义直接被替换为okfn,内核代码中的相关部分如下(linux/netfilter.h):#ifdef CONFIG_NETFILTER.#ifdef CONFIG_NETFILTER_DEBUG#define NF_HOOK nf_hook_slow#else#define NF_HOOK(pf, hook, skb, indev, outdev, okfn) (list_empty(&nf_hooks(pf)(hook) ? (okfn)(skb) : nf_h

25、ook_slow(pf), (hook), (skb), (indev), (outdev), (okfn)#endif.#else /* !CONFIG_NETFILTER */#define NF_HOOK(pf, hook, skb, indev, outdev, okfn) (okfn)(skb)#endif /*CONFIG_NETFILTER*/ 可见okfn函数是必不可少的,当Netfilter被启用时,它用于完成接收的数据包后的后续操作,如果不启用Netfilter做数据包过滤,则所有的数据包都被接受,直接调用该函数做后续操作。 * 译注完 现在,我们已经了解了我们的hook函

26、数接收到的信息中最有趣和最有用的部分,是该看看我们如何以各种各样的方式来利用这些信息来过滤数据包的时候了! -4.2 - 基于接口进行过滤 这应该是我们能做的最简单的过滤技术了。还记得我们的hook函数接收的参数中的那些net_device数据结构吗?使用相应的net_device数据结构的name这个成员,你就可以根据数据包的源接口和目的接口来选择是否丢弃它。如果想丢弃所有到达接口eth0的数据包,所有你需要做的仅仅是将in-name的值与eth0做比较,如果名字匹配,那么hook函数简单的返回NF_DROP即可,数据包会被自动销毁。就是这么简单!完成该功能的示例代码见如下的示例代码2。注意

27、,Light-Weight FireWall模块将会提供所有的本文提到的过滤方法的简单示例。它还包含了一个IOCTL接口以及用于动态改变其特性的应用程序。 示例代码2 : 基于源接口的数据包过滤/* 安装一个丢弃所有进入我们指定接口的数据包的Netfilter hook函数的示例代码*/#define _KERNEL_#define MODULE#include #include #include #include #include /* 用于注册我们的函数的数据结构 */static struct nf_hook_ops nfho;/* 我们丢弃的数据包来自的接口的名字 */static c

28、har *drop_if = lo;/* 注册的hook函数的实现 */unsigned int hook_func(unsigned int hooknum, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, int (*okfn)(struct sk_buff *) if (strcmp(in-name, drop_if) = 0) printk(Dropped packet on %s.n, drop_if); return NF_DROP; else return NF_ACC

29、EPT; /* 初始化程序 */int init_module() /* 填充我们的hook数据结构 */ nfho.hook = hook_func; /* 处理函数 */ nfho.hooknum = NF_IP_PRE_ROUTING; /* 使用IPv4的第一个hook */ nfho.pf = PF_INET; nfho.priority = NF_IP_PRI_FIRST; /* 让我们的函数首先执行 */ nf_register_hook(&nfho); return 0; /* 清除程序 */void cleanup_module() nf_unregister_hook(&n

30、fho); 是不是很简单?接下来,让我们看看基于IP地址的过滤。 - 4.3 - 基于地址进行过滤 与根据数据包的接口进行过滤类似,基于数据包的源或目的IP地址进行过滤同样简单。这次我们感兴趣的是sk_buff数据结构。还记得skb参数是一个指向sk_buff数据结构的指针的指针吗?为了避免犯错误,声明一个另外的指向skb_buff数据结构的指针并且将skb指针指向的指针赋值给这个新的指针是一个好习惯,就像这样: struct sk_buff *sb = *skb; /* Remove 1 level of indirection* / 这样,你访问这个数据结构的元素时只需要反引用一次就可以了

31、。获取一个数据包的IP头通过使用sk_buff数据结构中的网络层包头来完成。这个头位于一个联合中,可以通过sk_buff-nh.iph这样的方式来访问。示例代码3中的函数演示了当得到一个数据包的sk_buff数据结构时,如何利用它来检查收到的数据包的源IP地址与被禁止的地址是否相同。这些代码是直接从LWFW中取出来的,唯一不同的是LWFW统计的更新被移除。 示例代码3 : 检查收到的数据包的源IP unsigned char *deny_ip = x7fx00x00x01; /* 127.0.0.1 */ . static int check_ip_packet(struct sk_buff

32、*skb) /* We dont want any NULL pointers in the chain to * the IP header. */ if (!skb )return NF_ACCEPT; if (!(skb-nh.iph) return NF_ACCEPT; if (skb-nh.iph-saddr = *(unsigned int *)deny_ip) return NF_DROP; return NF_ACCEPT; 这样,如果数据包的源地址与我们设定的丢弃数据包的地址匹配,那么该数据包将被丢弃。为了使这个函数能按预期的方式工作,deny_ip的值应当以网络字节序(Bi

33、g-endian,与Intel相反)存放。虽然这个函数不太可能以一个空的指针作为参数来调用,带一点点偏执狂从来不会有什么坏处。当然,如果错误确实发生了,那么该函数将会返回NF_ACCEPT。这样Netfilter可以继续处理这个数据包。示例代码4展现了用于演示将基于接口的过滤略做修改以丢弃匹配给定IP地址的数据包的简单模块。 示例代码4 : 基于数据包源地址的过滤/* 安装丢弃所有来自指定IP地址的数据包的Netfilter hook的示例代码 */#define _KERNEL_#define MODULE#include #include #include #include /* For

34、IP header */#include #include /* 用于注册我们的函数的数据结构 */static struct nf_hook_ops nfho;/* 我们要丢弃的数据包来自的地址,网络字节序 */static unsigned char *drop_ip = x7fx00x00x01;/* 注册的hook函数的实现 */unsigned int hook_func(unsigned int hooknum, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, int (

35、*okfn)(struct sk_buff *) struct sk_buff *sb = *skb; / 译注:作者提供的代码中比较地址是否相同的方法是错误的,见注释掉的部分 if (sb-nh.iph-saddr = *(unsigned int *)drop_ip) / if (sb-nh.iph-saddr = drop_ip) printk(Dropped packet from. %d.%d.%d.%dn, *drop_ip, *(drop_ip + 1), *(drop_ip + 2), *(drop_ip + 3); return NF_DROP; else return NF

36、_ACCEPT; /* 初始化程序 */int init_module() /* 填充我们的hook数据结构 */ nfho.hook = hook_func; /* 处理函数 */ nfho.hooknum = NF_IP_PRE_ROUTING; /* 使用IPv4的第一个hook */ nfho.pf = PF_INET; nfho.priority = NF_IP_PRI_FIRST; /* 让我们的函数首先执行 */ nf_register_hook(&nfho); return 0;/* 清除程序 */void cleanup_module() nf_unregister_hook

37、(&nfho);- 4.4 - 基于TCP端口进行过滤 另一个要实现的简单规则是基于数据包的TCP目的端口进行过滤。这只比检查IP地址的要求要高一点点,因为我们需要自己创建一个TCP头的指针。还记得我们前面讨论的关于传输层包头与网络层包头的内容吗?获取一个TCP头的指针是一件简单的事情?分配一个tcphdr数据结构(在linux/tcp.h中定义)的指针,并将它指向我们的数据包中IP头之后的数据。或许一个例子的帮助会更大一些,示例代码5给出了检查数据包的TCP目的端口是否与某个我们要丢弃数据包的端口匹配的代码。与示例代码3一样,这些代码摘自LWFW。 示例代码5 : 检查收到的数据包的TCP目

38、的端口 unsigned char *deny_port = x00x19; /* port 25 */ . static int check_tcp_packet(struct sk_buff *skb) struct tcphdr *thead; /* We dont want any NULL pointers in the chain * to the IP header. */ if (!skb ) return NF_ACCEPT; if (!(skb-nh.iph) return NF_ACCEPT; /* Be sure this is a TCP packet first *

39、/ if (skb-nh.iph-protocol != IPPROTO_TCP) return NF_ACCEPT; thead = (struct tcphdr *)(skb-data + (skb-nh.iph-ihl * 4); /* Now check the destination port */ if (thead-dest) = *(unsigned short *)deny_port) return NF_DROP; return NF_ACCEPT; 确实很简单!不要忘了,要让这个函数工作,deny_port必须是网络字节序。这就是数据包过滤的基础了,你应当已经清楚的理解了

40、对于一个特定的数据包,如何获取你想要的信息。现在,是该进入更有趣的内容的时候了! - 5 - Netfilter hook的其它可能用法 在这里,我将提出其它很酷的利用Netfilter hook的点子,5.1节将简单的给出精神食粮,而5.2节将讨论和给出可以工作的基于内核的FTP密码嗅探器的代码,它的远程密码获取功能是确实可用的。事实上,它工作的令我吃惊的好,并且我编写了它。 - 5.1 - 隐藏后门的守护进程 核心模块编程也许是Linux开发中最有趣的部分之一了,在内核中编写代码意味着你在一个仅受限于你的想象力的地方写代码。以恶意的观点来看,你可以隐藏文件、进程,并且做各式各样很酷的,任何的rootkit能够做的事情。那么,以不太恶意的观点来看(

展开阅读全文
相关资源
相关搜索

当前位置:首页 > 期刊短文 > 互联网

本站为文档C TO C交易模式,本站只提供存储空间、用户上传的文档直接被用户下载,本站只是中间服务平台,本站所有文档下载所得的收益归上传人(含作者)所有。本站仅对用户上传内容的表现方式做保护处理,对上载内容本身不做任何修改或编辑。若文档所含内容侵犯了您的版权或隐私,请立即通知淘文阁网,我们立即给予删除!客服QQ:136780468 微信:18945177775 电话:18904686070

工信部备案号:黑ICP备15003705号© 2020-2023 www.taowenge.com 淘文阁