当前位置:网站首页>PAcP learning note 1: programming with pcap
PAcP learning note 1: programming with pcap
2022-07-07 13:09:00 【Mountain Ghost ballad me】
pcap Code flow
- We first decide which interface to sniff . stay Linux This may be eth0, stay BSD It could be xl1, wait . We can define this device in the string , Or we can let pcap Provide us with an interface name to complete this work .
- initialization pcap. This is actually telling pcap What device are we sniffing . If we are willing to sniff multiple devices . How do we distinguish them ? Use file handles . Just open a file to read and write , We must name our sniffer “ conversation ”, So that we can distinguish it from other such conversations .
- If we only want to sniff specific traffic ( for example : only TCP/IP Data packets , Send to port only 23 And so on ), We have to create a rule set ,“ compile ” And apply it . This is a three-stage process , All these are closely related . The rule set is saved in a string , And converted to pcap Formats that can be read ( So compile it ). Compilation is actually done by calling a function in our program ; It does not involve using external applications . Then we tell pcap Apply it to any session we want it to filter .
- Last , We tell pcap Enter its main execution loop . In this state ,pcap Will be waiting , Until it receives any packets we want it to receive . Every time it receives a new packet , It will call another function we have defined . The function it calls can do anything we want ; It can parse packets and print them to users , It can save it in a file , Or it does nothing .
- After meeting our sniffing needs , We close the session and finish .
This is actually a very simple process . There are five steps in total , One of them is optional ( step 3, If you want to know ). Let's look at each step and how to implement them .
Set up the device
It's very simple . There are two technologies to set up the devices we want to sniff .
First, we can simply let users tell us . Consider the following procedure :
#include <stdio.h>
#include <pcap.h>
int main(int argc, char *argv[])
{
char *dev = argv[1];
printf("Device: %s\n", dev);
return(0);
}
The user specifies the device by passing the device name to the program as the first parameter . Now string dev With pcap The interface name we will sniff is saved in an understandable format ( Of course , Suppose the user gives us a real interface ).
Another technique is equally simple . Look at this program :
#include <stdio.h>
#include <pcap.h>
int main(int argc, char *argv[])
{
char *dev, errbuf[PCAP_ERRBUF_SIZE];
dev = pcap_lookupdev(errbuf);
if (dev == NULL) {
fprintf(stderr, "Couldn't find default device: %s\n", errbuf);
return(2);
}
printf("Device: %s\n", dev);
return(0);
}
under these circumstances ,pcap Just set up your own device . “ But wait. , “errbuf What's the matter with strings ?”, majority pcap The command allows us to pass strings as parameters to them . What is the purpose of this string ? If the command fails , It will fill the string with the error description . under these circumstances , If pcap_lookupdev(3PCAP) Failure , It will be errbuf Error messages are stored in . This is how we set up the device .
Turn on the device to sniff
The task of creating sniffer sessions is very simple . So , We use pcap_open_live(3PCAP)
. The prototype of this function is as follows :
pcap_t *pcap_open_live(char *device, int snaplen, int promisc, int to_ms,
char *ebuf)
Parameter fields | explain |
---|---|
The first parameter | The first parameter is the device we specified in the previous section . |
The second parameter | snaplen It's an integer , It defines the pcap Maximum number of bytes captured . |
The third parameter | promisc, When set to true when , Put the interface into hybrid mode ( However , Even if it is set to false, In certain circumstances , Interfaces may also be in hybrid mode , in any case ). |
Fourth parameter | to_ms Is the read timeout in milliseconds ( value 0 Indicates that there is no timeout ; At least on some platforms , This means that you may have to wait until a sufficient number of packets arrive to see any packets , So you should use non-zero pause ) |
Fifth parameter | ebuf Is a string , We can store any error messages in it ( Just like we use on it errbuf What we did ). |
Return value | This function returns our session handler |
To demonstrate , Consider the following code snippet :
#include <pcap.h>
...
pcap_t *handle;
handle = pcap_open_live(dev, BUFSIZ, 1, 1000, errbuf);
if (handle == NULL) {
fprintf(stderr, "Couldn't open device %s: %s\n", dev, errbuf);
return(2);
}
This code fragment is opened and stored in the string dev Equipment in China , Tell it to read in BUFSIZ Many bytes specified in ( Usually by pcap.h stay /usr/include/stdio.h In the definition of ). We tell it to put the device into hybrid mode , Sniff until an error occurs , If there is an error , Store it in a string errbuf in ; It uses this string to print error messages .
Description of hybrid and non hybrid sniffing : These two technologies are very different in style . In standard non hybrid sniffing , The host only sniffs the traffic directly related to it . The sniffer will only get 、 Traffic from or routed through the host . On the other hand , Hybrid mode will sniff all traffic on the line . In a non switched environment , This may be all network traffic . The obvious advantage of this is that it provides more packets for sniffing , This may or may not help , It depends on the reason why you sniff the network . However , There are also setbacks . Hybrid mode sniffing is detectable ; One host can test to determine whether another host is performing hybrid sniffing . secondly , It only applies to non switched environments ( Hub, for example , Or be ARP Flooded switches ). Third , On High Traffic Networks , The host may occupy a lot of system resources .
Not all devices provide the same type of link layer header in the packet you read . Ethernet devices and some non Ethernet devices may provide Ethernet headers , But other equipment types , for example BSD and OS X Loopback device in 、PPP Interface and when captured in monitoring mode Wi-Fi Interface , No .
You need to determine the type of link layer header provided by the device , And use this type when processing packet content . pcap_datalink(3PCAP) Routine returns a value , Indicates the type of link layer header ; Please refer to the list of link layer header type values . The value it returns is DLT_ value .
If your program does not support the link layer header type provided by the device , I have to give up ; This will be done by code , for example :
if (pcap_datalink(handle) != DLT_EN10MB) {
fprintf(stderr, "Device %s doesn't provide Ethernet headers - not supported\n", dev);
return(2);
}
If the device does not provide Ethernet header , It will fail . This applies to the following code , Because it assumes Ethernet header .
Filter flow
A lot of times , Our sniffers may only be interested in specific traffic . for example , Sometimes all we want is to sniff ports 23 (telnet) To search for passwords . Or we may want to hijack the port 21 (FTP) file transferred . Maybe we just want DNS Traffic ( port 53 UDP). in any case , We seldom just want to blindly sniff all network traffic . This requires two methods : pcap_compile(3PCAP)
and pcap_setfilter(3PCAP)
.
The process is very simple . After we have called pcap_open_live() And after a working sniff session , We can apply our filters . Why not use our own if/else if Sentence? ? Two reasons . First ,pcap The filter efficiency is higher , Because it's directly related to BPF Use the filter together ; Let's get through BPF The driver executes directly to eliminate many steps . secondly , It's much easier :)
BPF Berkeley bag filter . Is in linux A packet filter under the platform . This filter can be used in socket When programming, it is very convenient to realize various filtering rules .
Before applying our filters , We have to “ compile ” it . Filter expressions are saved in regular strings (char Array ) in . Syntax in pcap-filter(7)
There is a good record in ; I let you read by yourself . however , We will use simple test expressions , So maybe you are sharp enough , It can be made clear from my example .
To compile the program , We call pcap_compile()
. The prototype defines it as :
int pcap_compile(pcap_t *p, struct bpf_program *fp, char *str, int optimize,
bpf_u_int32 netmask)
Parameter field description | describe |
---|---|
The first parameter | The first parameter is our session handle (pcap_t *handle In our previous example ). |
The second parameter | We will store The location reference of the compiled version of the filter . |
The third parameter | Expression itself ; Format : Regular string |
Fourth parameter | It's an integer , It determines whether the expression should “ Optimize ”(0 For false ,1 It's true —— Standard content ). |
Fifth parameter | We must specify the netmask of the network to which the filter applies . |
Return value | Failure to return -1, Other values are success |
After the expression is compiled , You can apply it . Next is pcap_setfilter()
. According to our explanation pcap The format of , Let's take a look at the prototype :
int pcap_setfilter(pcap_t *p, struct bpf_program *fp)
Parameter description | describe |
---|---|
The first parameter | Our session handler |
The second parameter | Is a reference to the compiled version of the expression ( Possible and pcap_compile() The second parameter of the same variable ) |
Perhaps another code example will help to better understand :
#include <pcap.h>
...
pcap_t *handle; /* Session handle Session handle */
char dev[] = "rl0"; /* Device to sniff on Devices to sniff */
char errbuf[PCAP_ERRBUF_SIZE]; /* Error string String recording error messages */
struct bpf_program fp; /* The compiled filter expression Compiled filter expression */
char filter_exp[] = "port 23"; /* The filter expression Filter expression */
bpf_u_int32 mask; /* The netmask of our sniffing device Netmask of our sniffer device */
bpf_u_int32 net; /* The IP of our sniffing device Our sniffer equipment IP */
if (pcap_lookupnet(dev, &net, &mask, errbuf) == -1) {
fprintf(stderr, "Can't get netmask for device %s\n", dev);
net = 0;
mask = 0;
}
handle = pcap_open_live(dev, BUFSIZ, 1, 1000, errbuf);
if (handle == NULL) {
fprintf(stderr, "Couldn't open device %s: %s\n", dev, errbuf);
return(2);
}
if (pcap_compile(handle, &fp, filter_exp, 0, net) == -1) {
fprintf(stderr, "Couldn't parse filter %s: %s\n", filter_exp, pcap_geterr(handle));
return(2);
}
if (pcap_setfilter(handle, &fp) == -1) {
fprintf(stderr, "Couldn't install filter %s: %s\n", filter_exp, pcap_geterr(handle));
return(2);
}
The program is sniffer ready on the device rl0
Sniff from or to ports in hybrid mode 23 All flows of .
You may notice that the previous example contains a function that we haven't discussed yet . pcap_lookupnet(3PCAP)
It's a function , Given the device name , Back to its IPv4 network number And the corresponding netmask ( network number yes IPv4 And of address and netmask , Therefore, it only contains the network part address of the device ). This is essential , Because we need to know the netmask to apply the filter . This function is described in other sections at the end of the document .
According to my experience , This filter is not applicable to all operating systems . In my test environment , I found that with the default kernel OpenBSD 2.9 This type of filter is indeed supported , But with the default kernel FreeBSD 4.3 I won't support it .
Actual sniffing
thus , We have learned how to define a device , Prepare for sniffing , And apply filters to determine what we should and should not sniff . Now it's time to actually capture some packets .
There are two main techniques for capturing packets . We can capture one packet at a time , You can also enter a cycle , wait for n Packets were sniffed before completion . We'll start by looking at how to capture a single packet , Then look at the method of using loops . So , We use pcap_next(3PCAP)
.
The prototype is quite simple :
u_char *pcap_next(pcap_t *p, struct pcap_pkthdr *h)
Specific description of parameters | explain |
---|---|
The first parameter | session handler |
The second parameter | It's a pointer to the structure , This structure contains general information about packets , Especially when it was sniffed 、 The length of the packet and the length of the specific part ( for example , If it is segmented ). |
Return value | Returns the u_char The pointer |
This is the use of pcap_next()
A simple demonstration of sniffing packets .
#include <pcap.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
pcap_t *handle; /* Session handle Session handle */
char *dev; /* The device to sniff on Sniff the device */
char errbuf[PCAP_ERRBUF_SIZE]; /* Error string Record the wrong string */
struct bpf_program fp; /* The compiled filter Compiled filter */
char filter_exp[] = "port 23"; /* The filter expression Filter expression */
bpf_u_int32 mask; /* Our netmask Our netmask */
bpf_u_int32 net; /* Our IP our IP */
struct pcap_pkthdr header; /* The header that pcap gives us Give us the header */
const u_char *packet; /* The actual packet Actual packets */
/* Define the device Defining devices */
dev = pcap_lookupdev(errbuf);
if (dev == NULL) {
fprintf(stderr, "Couldn't find default device: %s\n", errbuf);
return(2);
}
/* Find the properties for the device Find the properties of the device */
if (pcap_lookupnet(dev, &net, &mask, errbuf) == -1) {
fprintf(stderr, "Couldn't get netmask for device %s: %s\n", dev, errbuf);
net = 0;
mask = 0;
}
/* Open the session in promiscuous mode Open the session in hybrid mode */
handle = pcap_open_live(dev, BUFSIZ, 1, 1000, errbuf);
if (handle == NULL) {
fprintf(stderr, "Couldn't open device %s: %s\n", dev, errbuf);
return(2);
}
/* Compile and apply the filter Compile and apply filters */
if (pcap_compile(handle, &fp, filter_exp, 0, net) == -1) {
fprintf(stderr, "Couldn't parse filter %s: %s\n", filter_exp, pcap_geterr(handle));
return(2);
}
if (pcap_setfilter(handle, &fp) == -1) {
fprintf(stderr, "Couldn't install filter %s: %s\n", filter_exp, pcap_geterr(handle));
return(2);
}
/* Grab a packet Grab a packet */
packet = pcap_next(handle, &header);
/* Print its length */
printf("Jacked a packet with length of [%d]\n", header.len);
/* And close the session Close a session */
pcap_close(handle);
return(0);
}
This application uses pcap_lookupdev()
Any device returned is placed in hybrid mode to sniff it . It finds the first through port 23 (telnet) Data packets of , And tell the user the size of the packet ( In bytes ). Again , This program includes a new call ,pcap_close(3PCAP)
, We'll talk about it later ( Although it is really easy to explain ).
Another technology we can use is more complex , And it may be more useful . There are few sniffers ( If any ) The actual use pcap_next()
. They usually use pcap_loop(3PCAP)
or pcap_dispatch(3PCAP)
( Then they use it by themselves pcap_loop()
). To understand the use of these two functions , You must understand the idea of callback function .
Callback functions are nothing new , In many API It's very common in . The concept behind callback functions is quite simple . Suppose I have a program waiting for something . For the purpose of this example , Suppose my program wants the user to press a key on the keyboard . Every time they press a key , I want to call a function , Then the function will determine the operation to be performed . The function I am using is a callback function . Every time the user presses a key , My program will call the callback function . The callback is pcap Use in , But it is not called when the user presses a key , But in pcap Called when sniffing packets . The two functions that can be used to define its callback are pcap_loop()
and pcap_dispatch()
, They are very similar in the use of callbacks . Every time we sniff a packet that meets our filter requirements , They all call a callback function ( Of course , If there are any filters . If it doesn't exist , Then all sniffed packets are sent to the callback .)
pcap_loop() The prototype is as follows :
int pcap_loop(pcap_t *p, int cnt, pcap_handler callback, u_char *user)
Specific description of parameters | explain |
---|---|
The first parameter | The first parameter is our session handle |
The second parameter | It's an integer , It tells pcap_loop() How many packets should it sniff before returning ( A negative value means that it should sniff until an error occurs ) |
The third parameter | Is the name of the callback function ( Just its identifier , There are no brackets ) |
Fourth parameter | The last parameter is useful in some applications , But many times it is simply set to NULL. Suppose that in addition to pcap_loop() Outside the parameters sent , We also have parameters we want to send to the callback function |
In providing use pcap_loop()
Before the example of , We must check the format of the callback function . We cannot arbitrarily define the prototype of callback ; otherwise , pcap_loop() Will not know how to use this function . So we use this format as the prototype of our callback function . — This paragraph is for English Translation
First , You will notice that this function has void Return type . It's logical , because pcap_loop() In any case, I don't know how to deal with the return value .
void got_packet(u_char *args, const struct pcap_pkthdr *header, const u_char *packet);
Specific description of parameters | explain |
---|---|
The first parameter | The first parameter corresponds to pcap_loop() Last parameter of . Every time a function is called , As pcap_loop() Any value passed by the last parameter of will be passed to the first parameter of the callback function . |
The second parameter | The second parameter is pcap header (header ), It contains information about when to sniff packets 、 Packet size and other information . |
The third parameter | It's another direction u_char The pointer to , It points to the first byte of the data block containing the entire packet . |
The second parameter type :
struct pcap_pkthdr {
struct timeval ts; /* time stamp Time stamp */
bpf_u_int32 caplen; /* length of portion present The length of the current part */
bpf_u_int32 len; /* length this packet (off wire) The length of this packet */
};
The third parameter is normal pcap The most confusing parameter for novice programmers , It's another direction u_char
The pointer to , It points to the first byte of the data block containing the entire packet , from pcap_loop()
Sniffing . But how to use this variable ( Named in our prototype as packet)? A packet contains many attributes , So you can imagine , It is not actually a string , But a collection of structures ( for example , One TCP/IP Data packets There will be a Ethernet header 、 One IP header 、 One TCP header , Last , Packet Payload ). This u_char
The pointer points to the serialized version of these structures . In order to use it , We have to do some interesting type conversions .( header : Namely header
)
First , We must first define the actual structure , Then you can type them . The following is what I use to describe the Ethernet TCP/IP Structure definition of data package .
/* Ethernet addresses are 6 bytes */
/* The Ethernet address is 6 Bytes */
#define ETHER_ADDR_LEN 6
/* Ethernet header */
/* Ethernet header */
struct sniff_ethernet {
u_char ether_dhost[ETHER_ADDR_LEN]; /* Destination host address */
u_char ether_shost[ETHER_ADDR_LEN]; /* Source host address */
u_short ether_type; /* IP? ARP? RARP? etc */
};
/* IP header */
struct sniff_ip {
u_char ip_vhl; /* version << 4 | header length >> 2 */
u_char ip_tos; /* type of service */
u_short ip_len; /* total length */
u_short ip_id; /* identification */
u_short ip_off; /* fragment offset field Clip offset field */
#define IP_RF 0x8000 /* reserved fragment flag Keep the segmentation flag */
#define IP_DF 0x4000 /* don't fragment flag Do not keep segmentation marks */
#define IP_MF 0x2000 /* more fragments flag More clip marks */
#define IP_OFFMASK 0x1fff /* mask for fragmenting bits Segment mask */
u_char ip_ttl; /* time to live Time to live */
u_char ip_p; /* protocol agreement */
u_short ip_sum; /* checksum The checksum */
struct in_addr ip_src,ip_dst; /* source and dest address Source address and destination address */
};
#define IP_HL(ip) (((ip)->ip_vhl) & 0x0f)
#define IP_V(ip) (((ip)->ip_vhl) >> 4)
/* TCP header */
typedef u_int tcp_seq;
struct sniff_tcp {
u_short th_sport; /* source port Source port */
u_short th_dport; /* destination port Target port */
tcp_seq th_seq; /* sequence number Serial number */
tcp_seq th_ack; /* acknowledgement number Confirmation no. */
u_char th_offx2; /* data offset, rsvd Data offset */
#define TH_OFF(th) (((th)->th_offx2 & 0xf0) > 4)
u_char th_flags;
#define TH_FIN 0x01
#define TH_SYN 0x02
#define TH_RST 0x04
#define TH_PUSH 0x08
#define TH_ACK 0x10
#define TH_URG 0x20
#define TH_ECE 0x40
#define TH_CWR 0x80
#define TH_FLAGS (TH_FIN|TH_SYN|TH_RST|TH_ACK|TH_URG|TH_ECE|TH_CWR)
u_short th_win; /* window window */
u_short th_sum; /* checksum The checksum */
u_short th_urp; /* urgent pointer Pointer to an emergency */
};
These above are related to pcap
And our mysterious u_char
What does a pointer matter ? ok , These structures define the headers that appear in the packet data . So how can we separate it ?
We will assume that we are dealing with TCP/IP
Data packets . The same technology applies to any packet ; The only difference is the type of structure you actually use . therefore , Let's start by defining the variables and compile time definitions needed to deconstruct the package data .
/* ethernet headers are always exactly 14 bytes */
#define SIZE_ETHERNET 14
const struct sniff_ethernet *ethernet; /* The ethernet header Ethernet header */
const struct sniff_ip *ip; /* The IP header */
const struct sniff_tcp *tcp; /* The TCP header */
const char *payload; /* Packet payload Payload of message */
u_int size_ip;
u_int size_tcp;
Now? , We began a magical transformation :
ethernet = (struct sniff_ethernet*)(packet);
ip = (struct sniff_ip*)(packet + SIZE_ETHERNET);
size_ip = IP_HL(ip)*4;
if (size_ip < 20) {
printf(" * Invalid IP header length: %u bytes\n", size_ip);
return;
}
tcp = (struct sniff_tcp*)(packet + SIZE_ETHERNET + size_ip);
size_tcp = TH_OFF(tcp)*4;
if (size_tcp < 20) {
printf(" * Invalid TCP header length: %u bytes\n", size_tcp);
return;
}
payload = (u_char *)(packet + SIZE_ETHERNET + size_ip + size_tcp);
How does this work ? Consider the layout of packets in memory . u_char
The pointer is actually just a variable containing the memory address . This is the pointer . It points to a location in memory .
For the sake of simplicity , We will say that the address set by this pointer is the value X. ok , If our three structures just line up , The first of them (sniff_ethernet) Address in memory X , Then we can easily find the address of the structure behind it ; The address is X Plus the length of Ethernet header , namely 14.
IP Header is different from Ethernet header , It has no fixed length ; Its length is determined by IP The header length field of the header is in 4 The form of byte word number is given . Because it is 4 Count of byte words , Therefore, it must be multiplied by 4 To give the size in bytes . The minimum length of this header is 20 Bytes .
TCP The head also has variable length ; Its length is determined by TCP The head of the “ Data migration ” The fields give , As 4 The number of byte words , Its minimum length is also 20 byte .
So let's make a chart :
Variable | Location ( In bytes ) |
---|---|
sniff_ethernet | X |
sniff_ip | X + SIZE_ETHERNET |
sniff_tcp | X + SIZE_ETHERNET + {IP header length} |
payload | X + SIZE_ETHERNET + {IP header length} + {TCP header length} |
explain :
- The first line is the location of Ethernet X
- sniff_ip, Keep up with the sniff_ethernet after , In position X, Plus the space consumed by Ethernet header (14 byte , or SIZE_ETHERNET)
- sniff_tcp stay sniff_ip and sniff_ethernet after , So it is located in X Add Ethernet and IP The size of the header ( Respectively 14 Byte and IP Header length 4 times ).
- Last , Payload ( There is no corresponding single structure , Because its content depends on
TCP
Protocol used on ) After all these .
thus , We know how to set the callback function , Call it , And find out the properties of the sniffed packets .
summary
The key point is the following table , Parse the following message data according to the offset , The data comes out .
Variable | Location ( In bytes ) |
---|---|
sniff_ethernet | X |
sniff_ip | X + SIZE_ETHERNET |
sniff_tcp | X + SIZE_ETHERNET + {IP header length} |
payload | X + SIZE_ETHERNET + {IP header length} + {TCP header length} |
Reference address :
边栏推荐
- MySQL入门尝鲜
- 【无标题】
- 测试下摘要
- 单片机原理期末复习笔记
- 在字符串中查找id值MySQL
- 云检测2020:用于高分辨率遥感图像中云检测的自注意力生成对抗网络Self-Attentive Generative Adversarial Network for Cloud Detection
- Coscon'22 community convening order is coming! Open the world, invite all communities to embrace open source and open a new world~
- ClickHouse(03)ClickHouse怎么安装和部署
- Practical example of propeller easydl: automatic scratch recognition of industrial parts
- 滑轨步进电机调试(全国海洋航行器大赛)(STM32主控)
猜你喜欢
Sample chapter of "uncover the secrets of asp.net core 6 framework" [200 pages /5 chapters]
【学习笔记】AGC010
共创软硬件协同生态:Graphcore IPU与百度飞桨的“联合提交”亮相MLPerf
Awk of three swordsmen in text processing
Ogre入门尝鲜
Leetcode skimming: binary tree 27 (delete nodes in the binary search tree)
2022 examination questions and online simulation examination for safety production management personnel of hazardous chemical production units
关于 appium 如何关闭 app (已解决)
Day22 deadlock, thread communication, singleton mode
Blog recommendation | Apache pulsar cross regional replication scheme selection practice
随机推荐
Leetcode brush questions: binary tree 19 (merge binary tree)
DHCP 动态主机设置协议 分析
Sample chapter of "uncover the secrets of asp.net core 6 framework" [200 pages /5 chapters]
The URL modes supported by ThinkPHP include four common modes, pathinfo, rewrite and compatibility modes
MySQL入门尝鲜
Differences between MySQL storage engine MyISAM and InnoDB
Cookie and session comparison
Conversion from non partitioned table to partitioned table and precautions
在字符串中查找id值MySQL
迅为iTOP-IMX6ULL开发板Pinctrl和GPIO子系统实验-修改设备树文件
详细介绍六种开源协议(程序员须知)
将数学公式在el-table里面展示出来
Sed of three swordsmen in text processing
[untitled]
2022 practice questions and mock examination of the third batch of Guangdong Provincial Safety Officer a certificate (main person in charge)
学习突围2 - 关于高效学习的方法
Query whether a field has an index with MySQL
php——laravel缓存cache
RecyclerView的数据刷新
Leetcode skimming: binary tree 25 (the nearest common ancestor of binary search tree)