[笔记]-winpcap学习记录

前言

这段时间要用到 winpcap 的函数包来完成一些项目,从没接触过,所以在各种函数学习的过程中做一下记录,列出我在学习过程中遇到的问题以及解决方法,如有错误欢迎评论XD。

学习一个第三方包最好的方式就是看官方文档,运行一下提供的函数以及观察可以实现的功能等等,这里我找到一个临时使用的中文翻译的官方文档 (当然可能时间长了后网站不存在。。。)官方英文文档

winpcap环境配置

我当前的开发环境是:win10下使用vscode来编写并用mingw64编译c/c++程序,对于第三方库函数 winpcap的一些环境配置可以参考我的上一篇博客

以下内容的一个大致流程如下:


当然不是按照严格的流程来的,一些函数的使用会略有不同。。

获取已安装设备及其高级信息

对于计算机网络上的操作显然第一步是要获取计算机上所有的网络设备,像linux对外设的看法一样,可以将所有的网卡看作一个个文件设备,这样就可以方便的通过一个 “标识符” (指针)来获取到对应的所有设备。

获取网卡设备可以使用 pcap_lookupdev() 函数,它将返回系统中第一个合法的设备,,当然,可以使用 int pcap_findalldevs (pcap_if_t **alldevsp, char *errbuf)int pcap_findalldevs_ex (char *source, struct pcap_rmtauth *auth, pcap_if_t **alldevs, char *errbuf)注意后者仅在winpcap中可以使用,在linux中的函数包中没有,它是对前者的一个扩展,可以指定源等信息。

以下内容的部分来自这里

一个简单的demo如下,注意,如果你像我一样是使用非IDE来敲代码,注意程序的头部要主动添加一定的内容,如 #define HAVE_REMOTE 等等,同时对于编译指令也要指定 include 和 winpcap 的lib的路径,并添加 -lwpcap 等编译指令才能正常编译(可以看上一篇博客,有一定的介绍):

#include <iostream>

// #define u_int unsigned int
// #define u_short unsigned short
// #define u_char unsigned char
// #define u_long unsigned long

#define HAVE_REMOTE

#include<ws2tcpip.h>
#include "pcap.h"

// 函数原型
void ifprint(pcap_if_t *d);
char *iptos(u_long in);
char *ip6tos(struct sockaddr *sockaddr, char *address, int addrlen);

int main()
{
    pcap_if_t *alldevs;
    pcap_if_t *d;
    char errbuf[PCAP_ERRBUF_SIZE + 1];
    char source[PCAP_ERRBUF_SIZE + 1];

    printf("Enter the device you want to list:\n"
           "rpcap://              ==> lists interfaces in the local machine\n"
           "rpcap://hostname:port ==> lists interfaces in a remote machine\n"
           "                          (rpcapd daemon must be up and running\n"
           "                           and it must accept 'null' authentication)\n"
           "file://foldername     ==> lists all pcap files in the give folder\n\n"
           "Enter your choice: ");

    fgets(source, PCAP_ERRBUF_SIZE, stdin);
    source[PCAP_ERRBUF_SIZE] = '\0';

    // 获得接口列表
    if (pcap_findalldevs_ex(source, NULL, &alldevs, errbuf) == -1){
        fprintf(stderr, "Error in pcap_findalldevs: %s\n", errbuf);
        exit(1);
    }

    // 扫描列表并打印每一项 
    for (d = alldevs; d; d = d->next){
        ifprint(d);
    }

    pcap_freealldevs(alldevs);

    return 1;
}

// 打印所有可用信息
void ifprint(pcap_if_t *d){
    pcap_addr_t *a;
    char ip6str[128];

    // 设备名(Name)
    printf("%s\n", d->name);

    // 设备描述(Description) 
    if (d->description)
        printf("\tDescription: %s\n", d->description);

    // Loopback Address
    printf("\tLoopback: %s\n", (d->flags & PCAP_IF_LOOPBACK) ? "yes" : "no");

    // IP addresses 
    for (a = d->addresses; a; a = a->next)
    {
        printf("\tAddress Family: #%d\n", a->addr->sa_family);

        switch (a->addr->sa_family){
        case AF_INET:
            printf("\tAddress Family Name: AF_INET\n");
            if (a->addr)
                printf("\tAddress: %s\n", iptos(((struct sockaddr_in *)a->addr)->sin_addr.s_addr));
            if (a->netmask)
                printf("\tNetmask: %s\n", iptos(((struct sockaddr_in *)a->netmask)->sin_addr.s_addr));
            if (a->broadaddr)
                printf("\tBroadcast Address: %s\n", iptos(((struct sockaddr_in *)a->broadaddr)->sin_addr.s_addr));
            if (a->dstaddr)
                printf("\tDestination Address: %s\n", iptos(((struct sockaddr_in *)a->dstaddr)->sin_addr.s_addr));
            break;
        case AF_INET6:
            printf("\tAddress Family Name: AF_INET6\n");
            if (a->addr)
                printf("\tAddress: %s\n", ip6tos(a->addr, ip6str, sizeof(ip6str)));
            break;
        default:
            printf("\tAddress Family Name: Unknown\n");
            break;
        }
    }
    printf("\n");
}

// 将数字类型的IP地址转换成字符串类型的 
#define IPTOSBUFFERS 12
char *iptos(u_long in)
{
    static char output[IPTOSBUFFERS][3 * 4 + 3 + 1];
    static short which;
    u_char *p;

    p = (u_char *)&in;
    which = ((which + 1) == IPTOSBUFFERS ? 0 : (which + 1));
    sprintf(output[which], "%d.%d.%d.%d", p[0], p[1], p[2], p[3]);
    return output[which];
}

// 将ipv转为字符串形式
char* ip6tos(struct sockaddr *sockaddr, char *address, int addrlen){
    socklen_t sockaddrlen;
    #ifdef WIN32
    sockaddrlen = sizeof(struct sockaddr_in6);
    #else
    sockaddrlen = sizeof(struct sockaddr_storage);
    #endif
    if(getnameinfo(sockaddr,
        sockaddrlen,
        address,
        addrlen,
        NULL,
        0,
        NI_NUMERICHOST) != 0) address = NULL;
    return address;
}

代码是官方文档里的示例的一个修改版,#include<ws2tcpip.h> 这个头文件在ipv6的转换中使用到了,代码很简单,整体上类似一个简单的文件操作。

pcap_findalldevs_ex() 函数获取计算机上所有的网络设备,其中 alldevs 存储的即为所有设备信息的一个链表指针,既然是链表,对每一项 d 可以通过 d->next 来访问所有的元素。每个设备元素存储的信息有: 设备名、环回地址、地址组及IP和掩码等信息。因为与IP有关的信息存储不是字符串所有用了两个函数来处理。

编译指令如下(不要直接复制,,为了排版我截断换行了):

D:\mingw64\bin\g++.exe -g -std=c++17 
-LG:\实验报告\实训\WpdPack_4_1_2\WpdPack\Lib\x64 
-IG:\实验报告\实训\WpdPack_4_1_2\WpdPack\Include 
-finput-charset=UTF-8 -fexec-charset=GBK 
g:\实验报告\实训\program\net.cpp -o g:\实验报告\实训\program\net.exe 
-lwsock32 -lwpcap -lws2_32

运行的结果如下(其中各选项依次表示本地设备、路由设备以及文件(后续会用到)):

Enter the device you want to list:
rpcap://              ==> lists interfaces in the local machine
rpcap://hostname:port ==> lists interfaces in a remote machine
                          (rpcapd daemon must be up and running
                           and it must accept 'null' authentication)       
file://foldername     ==> lists all pcap files in the give folder

Enter your choice: rpcap://
rpcap://\Device\NPF_{XXXXXXXXXXXXXXXXXXXXXXXXXXX}
        Description: Network adapter 'Realtek PCIe GBE Family Controller' on local host
        Loopback: no
        Address Family: #23
        Address Family Name: AF_INET6
        Address: fe80::00
        Address Family: #2
        Address Family Name: AF_INET
        Address: xxxxxxxxxxx
        Netmask: 255.255.255.0
        Broadcast Address: 0.0.0.0

rpcap://\Device\NPF_{XXXXXXXXXXXXXXXXXXXXXXXX}
        Description: Network adapter 'Microsoft' on local host
        Loopback: no
        Address Family: #23
        Address Family Name: AF_INET6
        Address: fe80::00
        Address Family: #23
        Address Family Name: AF_INET6
        Address: fe80::00

rpcap://\Device\NPF_{XXXXXXXXXXXXXXXXXXXXXXXX}
        Description: Network adapter 'Microsoft' on local host
        Loopback: no
        Address Family: #23
        Address Family Name: AF_INET6
        Address: fe80::00
        Address Family: #2
        Address Family Name: AF_INET
        Address: 192.168.0.185
        Netmask: 255.255.255.0
        Broadcast Address: 0.0.0.0

显然最后一个显示的就是我的无线网卡的信息。。

打开适配器并捕获数据包

这里的一些东西在文档中的参考地址

当用上面的方式获得所有的设备信息后,可以指定打开其中一个设备,这里使用的是 pcap_t * pcap_open (const char *source, int snaplen, int flags, int read_timeout, struct pcap_rmtauth *auth, char *errbuf) 函数,这个函数仅在 winpcap 可以使用,它将Linux中的 pcap_open_live() 等等集合函数包装了一下,返回的 “文件标识符” 将用于后续的数据包的捕获中,关于数据包的捕获,有两种方式,一种是使用回调函数的方式、另一种是使用类似结果集处理的方式,一般是用后者,因为可以控制流程操作,尤其是在多线程中会使用到,使用的函数是: int pcap_next_ex (pcap_t *p, struct pcap_pkthdr **pkt_header, const u_char **pkt_data) 这里的 pcap_t *ppcap_open() 的返回值,另两个参数即为数据包的头数数据和数据包内部数据,函数返回值-1表示结束,为0超时。

不使用回调函数的代码如下:

#include <stdlib.h>
#include <stdio.h>
#include<ws2tcpip.h>
//
// NOTE: remember to include WPCAP and HAVE_REMOTE among your
// preprocessor definitions.
//
#define HAVE_REMOTE
#include <pcap.h>

#define LINE_LEN 16

main(int argc, char **argv){   
    pcap_if_t *alldevs, *d;
    pcap_t *fp;
    u_int inum, i=0;
    char errbuf[PCAP_ERRBUF_SIZE];
    int res;
    struct pcap_pkthdr *header;
    const u_char *pkt_data;

    printf("pktdump_ex: prints the packets of the network using WinPcap.\n");
    printf("   Usage: pktdump_ex [-s source]\n\n"
           "   Examples:\n"
           "      pktdump_ex -s file://c:/temp/file.acp\n"
           "      pktdump_ex -s rpcap://\\Device\\NPF_{C8736017-F3C3-4373-94AC-9A34B7DAD998}\n\n");

    if(argc < 3){
        printf("\nNo adapter selected: printing the device list:\n");
        // The user didn't provide a packet source: Retrieve the local device list 
        if (pcap_findalldevs_ex(PCAP_SRC_IF_STRING, NULL, &alldevs, errbuf) == -1){
            fprintf(stderr,"Error in pcap_findalldevs_ex: %s\n", errbuf);
            return -1;
        }
        
        // Print the list 
        for(d=alldevs; d; d=d->next){
            printf("%d. %s\n    ", ++i, d->name);

            if (d->description)
                printf(" (%s)\n", d->description);
            else
                printf(" (No description available)\n");
        }
        
        if (i==0){
            fprintf(stderr,"No interfaces found! Exiting.\n");
            return -1;
        }
        
        printf("Enter the interface number (1-%d):",i);
        scanf("%d", &inum);
        
        if (inum < 1 || inum > i){
            printf("\nInterface number out of range.\n");

            // Free the device list 
            pcap_freealldevs(alldevs);
            return -1;
        }
        
        // Jump to the selected adapter 
        for(d=alldevs, i=0; i< inum-1 ;d=d->next, i++);
        
        // Open the device 
        if((fp= pcap_open(d->name,
                            100 /* snaplen */,
                            PCAP_OPENFLAG_PROMISCUOUS /* flags */,
                            20 /* read timeout */,
                            NULL /* remote authentication */,
                            errbuf)
                            ) == NULL){
            fprintf(stderr,"\nError opening adapter\n");
            return -1;
        }
    }
    else{
        // Do not check for the switch type ('-s')
        if ( (fp= pcap_open(argv[2],
                            100 /* snaplen */,
                            PCAP_OPENFLAG_PROMISCUOUS /* flags */,
                            20 /* read timeout */,
                            NULL /* remote authentication */,
                            errbuf)
                            ) == NULL){
            fprintf(stderr,"\nError opening source: %s\n", errbuf);
            return -1;
        }
    }
    
	pcap_freealldevs(alldevs);
	
    // Read the packets 
    while((res = pcap_next_ex( fp, &header, &pkt_data)) >= 0){

        if(res == 0)
            // Timeout elapsed 
            continue;

        // print pkt timestamp and pkt len 
        printf("%ld:%ld (%ld)\n", header->ts.tv_sec, header->ts.tv_usec, header->len);          
        
        // Print the packet 
        for (i=1; (i < header->caplen + 1 ) ; i++){
            printf("%.2x ", pkt_data[i-1]);
            if ( (i % LINE_LEN) == 0) printf("\n");
        }
        
        printf("\n\n");     
    }

    if(res == -1){
        fprintf(stderr, "Error reading the packets: %s\n", pcap_geterr(fp));
        return -1;
    }

    return 0;
}

编译的指令类似上一个程序,可能会出现warning,应该是强转的问题,输入输出如下:

 .\package.exe
pktdump_ex: prints the packets of the network using WinPcap.
   Usage: pktdump_ex [-s source]

   Examples:
      pktdump_ex -s file://c:/temp/file.acp
      pktdump_ex -s rpcap://\Device\NPF_{C8736017-F3C3-4373-94AC-9A34B7DAD998}


No adapter selected: printing the device list:
1. rpcap://\Device\NPF_{XXXXXXXXXXXXXXXXXXXXXXXX}
     (Network adapter 'Realtek PCIe GBE Family Controller' on local host)
2. rpcap://\Device\NPF_{XXXXXXXXXXXXXXXXXXXXXXXX}
     (Network adapter 'Microsoft' on local host)
3. rpcap://\Device\NPF_{XXXXXXXXXXXXXXXXXXXXXXXX}
     (Network adapter 'Microsoft' on local host)
Enter the interface number (1-3):3
1593871368:485862 (173)
9c da 3e bf b1 ca b8 3a 08 67 43 b0 08 00 45 00 
00 9f a6 a2 40 00 df 06 de 31 0d 7c 47 a7 c0 a8 
00 b9 01 bb 24 b4 95 f4 ea c3 39 10 da 15 50 18 
00 07 5b cc 00 00 16 03 03 00 72 04 00 00 6e 00 
02 a3 00 00 68 53 53 4b 2d 45 30 30 34 34 32 37
32 39 00 00 00 cf 7c 03 62 2d 87 43 2f c5 8e e2 
3e 12 7f 22 c4 4c db 11 1a aa ea e6 94 e6 57 f2
95 21 07 45 81 ef ab 79 e9 ab 51 ae b2 af 65 67 
62 36 b6 a5 11 42 19 79 b3 7f c7 cb cb c6 05 0a
5c a7 0c b9 90 a9 2a 4f a3 41 9f 60 9c 10 6b 22 
09 22 b8 2e da 21 d1 dd 6a 56 2b 9a 43

1593871368:486003 (54)
b8 3a 08 67 43 b0 9c da 3e bf b1 ca 08 00 45 00
00 28 4a 12 40 00 80 06 9a 39 c0 a8 00 b9 0d 7c
47 a7 24 b4 01 bb 39 10 da 15 95 f4 eb 6d 50 10 
02 03 dc 55 00 00 

程序将不断捕获指定设备经过的流量数据包,然后就可以用这些数据包进行一定的分析了XD

数据包的过滤与分析

winpcap函数库提供了一个过滤器,可以设置过滤器来抓取一定规则的数据,比如说只抓取tcp流量就可以用 ip and tcp 的规则来获取仅有 tcp 的数据包,过滤器需要先编译在设定后才能使用,也就是要调用 pcap_compliepcap_setfilter 这两个函数,之后就可以按照上面的方式,回调函数或者不使用,来抓取指定设备下的数据包,抓取到的数据包的形式数据链路层上的 以太网帧 大致结构如下:

可以看到抓取到的数据包往后14个字节就是数据部分,也就是网络层的 IP数据报 内容,可以用 ih = (ip_header *) (pkt_data + 14); 的方式直接强转获得(当然要定义IP数据包的结构),大致结构如下:


然后根据头部长度信息就可以获得报文数据,也即是应用层的报文,UDP或者TCP报文等等,用相同的思想: ip_len = (ih->ver_ihl & 0xf) * 4; th = (tcp_header*)((u_char*)ih + ip_len); 即可,然后定义报文的协议结构就可以读取到感兴趣的数据了:


根据以上思想可以得到获取某个网卡设备上一定时间段内的所有UDP报文信息或者TCP报文信息的代码:

TCP:

#include<cstdio>
#include<cstdlib>
#include<ws2tcpip.h>
#define HAVE_REMOTE
#include<pcap.h>

// IP地址结构
typedef struct ip_address{
    u_char byte1;
    u_char byte2;
    u_char byte3;
    u_char byte4;
}ip_address;

// IPv4数据包首部
#define IP_LEN_MIN 20
typedef struct ip_header{
    u_char  ver_ihl;        // 版本 (4 bits) + 首部长度 (4 bits)
    u_char  tos;            // 服务类型(Type of service) 
    u_short tlen;           // 总长(Total length) 
    u_short identification; // 标识(Identification)
    u_short flags_fo;       // 标志位(Flags) (3 bits) + 段偏移量(Fragment offset) (13 bits)
    u_char  ttl;            // 存活时间(Time to live)
    u_char  proto;          // 协议(Protocol)
    #define IP_ICMP     1
    #define IP_IGMP     2
    #define IP_TCP      6
    #define IP_UDP      17
    #define IP_IGRP     88
    #define IP_OSPF     89
    u_short crc;            // 首部校验和(Header checksum)
    ip_address  saddr;      // 源地址(Source address)
    ip_address  daddr;      // 目的地址(Destination address)
    u_int   op_pad;         // 选项与填充(Option + Padding)
}ip_header;

// tcp协议头结构
#define TCP_LEN_MIN 20
typedef struct _tcp_header
{
    u_short th_sport;			// source port 源端口
    u_short th_dport;			// destination port 目的端口
    u_int   th_seq;			    // sequence number field 序列号
    u_int   th_ack;			    // acknowledgement number field 确认号
    u_char  th_len:4;			// header length 报头长度
    u_char  th_x2:4;			// unused 保留
    u_char  th_flags;           // flag 标志
    #define TH_FIN	0x01
    #define TH_SYN	0x02
    #define TH_RST	0x04
    #define TH_PSH	0x08
    #define TH_ACK	0x10
    #define TH_URG	0x20
    u_short th_win;		    // window 窗口
    u_short th_sum;		    // checksum 校验和
    u_short th_urp;		    // urgent pointer 紧急
}tcp_header;

// 回调函数
void packet_handler(u_char *param, const struct pcap_pkthdr *header, const u_char *pkt_data);

int main(){
    pcap_if_t *alldevs, *d;
    pcap_t *fd;
    char errbuf[PCAP_ERRBUF_SIZE];
    u_int netmask;
    char packet_filter[] = "ip and tcp";
    struct bpf_program fcode;

    // 获取设备列表
    if(!~pcap_findalldevs_ex(PCAP_SRC_IF_STRING, NULL, &alldevs, errbuf)){
        fprintf(stderr, "Error in pcap_findalldevs: %s\n", errbuf);
        exit(1);
    }
    
    // 显示所有的设备列表
    int i = 0;
    for(d = alldevs; d != NULL; d = d->next){
        printf("%d. %s", ++i, d->name);
        if(d->description)printf(" (%s)\n", d->description);
        else printf(" (No description abailable\n");
    }
    if(!i){
        printf("\n No interface found!, Make sure winpcap is installed. \n");
        return -1;
    }

    printf("Enter the interface number(1-%d): ", i);
    int inum = i;
    scanf("%d", &inum);
    if(inum < 1 || inum > i){
        printf("\n Interface number out of range.\n");
        pcap_freealldevs(alldevs);
        return -1;
    }

    // 跳转到指定设备
    for(d = alldevs, i = 0; i < inum - 1; d = d->next, ++i);

    // 打开适配器
    if((fd = pcap_open(d->name, 65536, PCAP_OPENFLAG_PROMISCUOUS, 1000, NULL, errbuf)) == NULL){
        fprintf(stderr, "\n Unalbel to open the adapter. %s, is not supported by winpcap.\n");
        pcap_freealldevs(alldevs);
        return -1;
    }

    // 检查数据连路层,仅考虑以太网
    if(pcap_datalink(fd) != DLT_EN10MB){
        fprintf(stderr, "\nThis program works only on Ethernet networks.\n");
        pcap_freealldevs(alldevs);
        return -1;
    }

    if(d->addresses != NULL){
        // 获得接口的饿第一个地址的掩码
        netmask = ((struct sockaddr_in*)(d->addresses->netmask))->sin_addr.S_un.S_addr;
    }
    else{
        // 没有地址,假设是一个C类掩码
        netmask = 0xffffff;
    }

    // 编译过滤器
    if(!~pcap_compile(fd, &fcode, packet_filter, 1, netmask)){
        fprintf(stderr, "\nUnable to compile the packet filter. Check the syntax. \n");
        pcap_freealldevs(alldevs);
        return -1;
    }

    // 设置过滤器
    if(!~pcap_setfilter(fd, &fcode)){
        fprintf(stderr, "\nError setting the filter.\n");
        pcap_freealldevs(alldevs);
        return -1;
    }

    printf("\nlistening on %s....\n", d->description);

    pcap_freealldevs(alldevs);

    // 开始捕获

    pcap_loop(fd, 0, packet_handler, NULL);

    return 0;
}

void packet_handler(u_char *param, const struct pcap_pkthdr *header, const u_char *pkt_data){
    struct tm *ltime;
    char timestr[16];
    ip_header *ih;
    // udp_header *uh;
    tcp_header *th;
    u_int ip_len;
    u_short sport,dport;
    time_t local_tv_sec;

    // 将时间戳转换成可识别的格式 
    local_tv_sec = header->ts.tv_sec;
    ltime=localtime(&local_tv_sec);
    strftime( timestr, sizeof timestr, "%H:%M:%S", ltime);

    // 打印数据包的时间戳和长度 
    printf("%s.%.6d len:%d ", timestr, header->ts.tv_usec, header->len);

    // 获得IP数据包头部的位置
    ih = (ip_header *) (pkt_data +
        14); //以太网头部长度
    
    // 获取TCP数据报首部位置
    ip_len = (ih->ver_ihl & 0xf) * 4;
    th = (tcp_header*)((u_char*)ih + ip_len);

    // 将网络字节序转为主机字节序
    sport = ntohs(th->th_sport);
    dport = ntohs(th->th_dport);

    // 显示主要信息
    printf("%d.%d.%d.%d:%d -> %d.%d.%d.%d:%d %u\n",
        ih->saddr.byte1,
        ih->saddr.byte2,
        ih->saddr.byte3,
        ih->saddr.byte4,
        sport,
        ih->daddr.byte1,
        ih->daddr.byte2,
        ih->daddr.byte3,
        ih->daddr.byte4,
        dport, 
        th->th_seq);
}

编译指令类似,运行结果如下:

PS G:\实验报告\实训\program> .\tcp_filter.exe
1. rpcap://\Device\NPF_{XXXXXXXXXXXXXXXXXXXXXXXXXX} (Network adapter 'Realtek 
PCIe GBE Family Controller' on local host)
2. rpcap://\Device\NPF_{XXXXXXXXXXXXXXXXXXXXXXXXXX} (Network adapter 'Microsoft' on local host)
3. rpcap://\Device\NPF_{XXXXXXXXXXXXXXXXXXXXXXXXXX} (Network adapter 'Microsoft' on local host)
Enter the interface number(1-3): 3

listening on Network adapter 'Microsoft' on local host....
16:36:55.972521 len:93 XXXXXXXXXX:443 -> 192.168.0.185:9570 3675412266
16:36:55.972522 len:78 XXXXXXXXXX:443 -> 192.168.0.185:9570 34821930
16:36:55.972676 len:54 192.168.0.185:9570 -> XXXXXXXXXX:443 870083001

侑摸了一天的🐟,,,溜了溜了

(未完loading…)

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值