在Boost ASIO中如何设置源IP地址以模拟另一个服务器的IP地址? [英] In Boost ASIO how can I set the source IP address to impersonate another server's IP address?

查看:354
本文介绍了在Boost ASIO中如何设置源IP地址以模拟另一个服务器的IP地址?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个基于Boost ASIO的C ++服务器程序,我想能够将TCP使用的源IP地址设置为另一个服务器的源IP地址。我知道一个人可以读取源和目的IP地址,但是可以设置它们吗?



假设如果我在C ++代码中设置错误源IP地址会有一些与网络堆栈的交互。即使C ++代码是正确的,网络堆栈是否会重新设置源IP地址的出口?是正确的方法来做这个写C ++ ASIO代码选择一个特定的虚拟网络接口?一个配置有错误静态IP地址?我已经看到这个以前作为一种方式来控制源IP地址。这是我需要做的吗?



我想知道这将是什么的后果。有两个配置相同静态IP地址的机器可能会导致正常服务器停止工作,这将是坏的。



我有Windows和Linux端口我的服务器我可以使用的情况下代码提出将工作在一个操作系统,而不是其他。我目前倾向于Kali Linux,因为我可以arpspoof主服务器并有效地关闭一段时间。

解决方案

可以通过手动构造网络和传输层头部来将源IP设置为出站数据上的任意地址,然后将头部和期望的有效载荷发送到 raw socket 。使用原始套接字可能需要提高权限,或者可能被内核禁用或限制,例如某些Microsoft 平台。此外,由于TCP的三次握手和不可预测的序列号,伪造的TCP段的有效性超出了潜在的 TCP重置攻击,是有问题的。



路由是一个不同的主题,取决于各种路由器和配置。例如,设备可以执行出口过滤并丢弃具有设备不能验证的源地址的分组。此外,IP地址冲突的影响可以不同,但​​它通常导致间歇性连接。






Boost.Asio提供 basic_raw_socket< Protocol> ; 模板,该模板需要符合协议类型要求。例如,下面是开始或 raw 协议:

  struct raw 
{
typedef boost :: asio :: ip :: basic_endpoint< raw>端点
int type()const {return SOCK_RAW; }
int protocol()const {return IPPROTO_RAW; }
int family()const {return PF_INET; }
};

boost :: asio :: basic_raw_socket< raw>插座;

当处理原始套接字时,困难往往不在于使用Boost.Asio,实现网络和传输线协议。下面是一个完整的最小示例,其中我尽量通过创建一个 raw 协议和使用 basic_raw_socket 发送UDP消息:

  #include< algorithm> 
#include< iostream>

#include< boost / array.hpp>
#include< boost / asio.hpp>
#include< boost / cstdint.hpp>

/// @brief raw socket提供了原始套接字的协议。
class raw
{
public:
/// @ brief原始端点的类型。
typedef boost :: asio :: ip :: basic_endpoint< raw>端点

/// @ brief原始套接字类型。
typedef boost :: asio :: basic_raw_socket< raw>插座;

/// @ brief原始解析器类型。
typedef boost :: asio :: ip :: basic_resolver< raw>解析器

/// @ brief构造为表示IPv4 RAW协议。
static raw v4()
{
return raw(IPPROTO_RAW,PF_INET);
}

/// @ brief构造以表示IPv6 RAW协议。
static raw v6()
{
return raw(IPPROTO_RAW,PF_INET6);
}

/// @ brief默认构造函数。
explicit raw()
:protocol_(IPPROTO_RAW),
family_(PF_INET)
{}

/// @ brief获取类型的协议。
int type()const
{
return SOCK_RAW;
}

/// @ brief获取协议的标识符。
int protocol()const
{
return protocol_;
}

/// @ brief获取协议族的标识符。
int family()const
{
return family_;
}

/// @ brief比较两个协议的相等性。
friend bool operator ==(const raw& p1,const raw& p2)
{
return p1.protocol_ == p2.protocol_&& p1.family_ == p2.family_;
}

///比较两个协议的不等式。
friend bool operator!=(const raw& p1,const raw& p2)
{
return!(p1 == p2);
}

private:
explicit raw(int protocol_id,int protocol_family)
:protocol_(protocol_id),
family_(protocol_family)
{}

int protocol_;
int family_;
};

/// @ brief Mockup ipv4_header for没有选项。
//
// IPv4线格式:
//
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// + ------- + ------- + ------- + ------- + ------- + ------- + ------- + ------ + ---
// | version | header |类型|总长度(以字节为单位) ^
// | (4)| length |服务| | |
// + ------- + ------- + ------- + ------- + ------- + --- ---- + ------- + ------ + |
// |识别|标志|片段偏移| |
// + ------- + ------- + ------- + ------- + ------- + --- ---- + ------- + ------ + 20字节
// |时间生活|协议|头校验和| |
// + ------- + ------- + ------- + ------- + ------- + --- ---- + ------- + ------ + |
// |源IPv4地址| |
// + ------- + ------- + ------- + ------- + ------- + --- ---- + ------- + ------ + |
// |目的IPv4地址| v
// + ------- + ------- + ------- + ------- + ------- + - ----- + ------- + ------ + ---
// / options(如果有)/
// + ------ - + ------- + ------- + ------- + ------- + ------- + ------- + ------ +
class ipv4_header
{
public:
ipv4_header(){std :: fill(buffer_.begin(),buffer_.end ); }

void version(boost :: uint8_t value){
buffer_ [0] =(value<< 4)| (buffer_ [0]& 0x0F);
}

void header_length(boost :: uint8_t value)
{
buffer_ [0] =(value& 0x0F)| (buffer_ [0]& 0xF0);
}

void type_of_service(boost :: uint8_t value){buffer_ [1] = value; }
void total_length(boost :: uint16_t value){encode16(2,value); }
void identifier(boost :: uint16_t value){encode16(4,value); }

void dont_fragment(bool value)
{
buffer_ [6] ^ =(-value ^ buffer_ [6])& 0x40;
}

void more_fragments(bool value)
{
buffer_ [6] ^ =(-value ^ buffer_ [6])& 0x20;
}

void fragment_offset(boost :: uint16_t value)
{
//保留标记。
auto flags = static_cast< uint16_t>(buffer_ [6]& 0xE0)< 8;
encode16(6,(value& 0x1FFF)| flags);
}

void time_to_live(boost :: uint8_t value){buffer_ [8] = value; }
void protocol(boost :: uint8_t value){buffer_ [9] = value; }
void checksum(boost :: uint16_t value){encode16(10,value); }

void source_address(boost :: asio :: ip :: address_v4 value)
{
auto bytes = value.to_bytes();
std :: copy(bytes.begin(),bytes.end(),& buffer_ [12]);
}

void destination_address(boost :: asio :: ip :: address_v4 value)
{
auto bytes = value.to_bytes();
std :: copy(bytes.begin(),bytes.end(),& buffer_ [16]);
}

public:

std :: size_t size()const {return buffer_.size(); }

const boost :: array< uint8_t,20> data()const {return buffer_; }

private:

void encode16(boost :: uint8_t index,boost :: uint16_t value)
{
buffer_ [index] = >> 8)& 0xFF;
buffer_ [index + 1] = value& 0xFF;
}

boost :: array< uint8_t,20>缓冲_;
};

void calculate_checksum(ipv4_header& header)
{
//清除校验和。
header.checksum(0);

//校验和是一个补码的16位补码
//头中的所有16位字。

//总计所有16位字。
auto data = header.data();
auto sum = std :: accumulate< boost :: uint16_t *,boost :: uint32_t>(
reinterpret_cast< boost :: uint16_t *>(& data [0]),
reinterpret_cast< boost :: uint16_t *>(& data [0] + data.size()),
0);

//将32位转换为16位。
while(sum>> 16)
{
sum =(sum& 0xFFFF)+(sum>>
}

header.checksum(〜sum);
}

/// @ brief Mockup IPv4 UDP头。
//
// UDP线格式:
//
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// + ------- + ------- + + ------- + ------- + ------- + ------- + ------ + ---
// |源端口|目的端口| ^
// + ------- + ------- + ------- + ------- + ------- + - ----- + ------- + ------ + 8字节
// |长度|校验和| v
// + ------- + ------- + ------- + ------- + ------- + - ----- + ------- + ------ + ---
// / data(如果有)/
// + ------ - + ------- + ------- + ------- + ------- + ------- + ------- + ------ +
class udp_header
{
public:
udp_header(){std :: fill(buffer_.begin(),buffer_.end ); }

void source_port(boost :: uint16_t value){encode16(0,value); }
void destination_port(boost :: uint16_t value){encode16(2,value); }
void length(boost :: uint16_t value){encode16(4,value); }
void checksum(boost :: uint16_t value){encode16(6,value); }

public:

std :: size_t size()const {return buffer_.size(); }

const boost :: array< uint8_t,8& data()const {return buffer_; }

private:

void encode16(boost :: uint8_t index,boost :: uint16_t value)
{
buffer_ [index] = >> 8)& 0xFF;
buffer_ [index + 1] = value& 0xFF;
}

boost :: array< uint8_t,8>缓冲_;
};


int main()
{
boost :: asio :: io_service io_service;

//创建所有I / O对象。
boost :: asio :: ip :: udp :: socket receiver(io_service,
boost :: asio :: ip :: udp :: endpoint(boost :: asio :: ip :: udp: :v4(),0));
boost :: asio :: basic_raw_socket< raw> sender(io_service,
raw :: endpoint(raw :: v4(),0));

const auto receiver_endpoint = receiver.local_endpoint();

//使用有效负载hello处理来自
的UDP消息// 8.8.8.8:54321
const boost :: asio :: ip :: udp ::端点spoofed_endpoint(
boost :: asio :: ip :: address_v4 :: from_string(8.8.8.8),
54321);
const std :: string payload =hello;

//创建UDP头。
udp_header udp;
udp.source_port(spoofed_endpoint.port());
udp.destination_port(receiver_endpoint.port());
udp.length(udp.size()+ payload.size()); // Header + Payload
udp.checksum(0); // Optioanl for IPv4

//创建IPv4标头。
ipv4_header ip;
ip.version(4); // IPv4
ip.header_length(ip.size()/ 4); // 32位字
ip.type_of_service(0); //差分服务代码点
auto total_length = ip.size()+ udp.size()+ payload.size();
ip.total_length(total_length); //整个消息。
ip.identification(0);
ip.dont_fragment(true);
ip.more_fragments(false);
ip.fragment_offset(0);
ip.time_to_live(4);
ip.source_address(spoofed_endpoint.address()。to_v4());
ip.destination_address(receiver_endpoint.address()。to_v4());
ip.protocol(IPPROTO_UDP);
calculate_checksum(ip);

//收集所有缓冲区并通过原始套接字发送。
boost :: array< boost :: asio :: const_buffer,3> buffers = {{
boost :: asio :: buffer(ip.data()),
boost :: asio :: buffer(udp.data()),
boost :: asio :: buffer(payload)
}};
auto bytes_transferred = sender.send_to(buffers,
raw :: endpoint(receiver_endpoint.address(),receiver_endpoint.port()));
assert(bytes_transferred == total_length);

//读取接收者。
std :: vector< char> buffer(payload.size(),'\0');
boost :: asio :: ip :: udp :: endpoint sender_endpoint;
bytes_transferred = receiver.receive_from(
boost :: asio :: buffer(buffer),sender_endpoint);

//验证。
assert(bytes_transferred == payload.size());
assert(std :: string(buffer.begin(),buffer.end())== payload);
assert(spoofed_endpoint == sender_endpoint);

//打印端点。
std :: cout<<
实际发送方端点:< sender.local_endpoint()<< \\\

Receiver endpoint:<< receiver.local_endpoint()<< \\\

Receiver's remote endpoint:<< sender_endpoint<< std :: endl;
}

输出:

  $ sudo ./a.out 
实际发送方端点:0.0.0.0:255
接收方端点:0.0.0.0:44806
接收者的远程端点:8.8.8.8:54321

如输出中所示,发送方端点是 0.0.0.0:255 ,接收方认为发送方的端点是 8.8.8.8:54321


I have a Boost ASIO-based C++ server program and I'd like to be able to set the source IP address used by TCP to that of another server. I know one can read the source and destination IP addresses but presumably they can be set as well?

Presumably if I set the "wrong" source IP address in the C++ code there will be some interaction with the network stack. Won't the network stack re-set the source IP address on the way out even if the C++ code is right? Is the right way to do this to write C++ ASIO code to pick a specific virtual network interface? One that is configured with the "wrong" static IP address? I have seen this before as a way to control the source IP address. Is that what I need to do instead?

I am wondering what the consequences of this would be. Having two machines with the same static IP address configured might cause the "normal" server to stop working completely, which would be bad.

I have both Windows and Linux ports of my server I can use in case the code proposed will work on one OS and not the other. I'm currently leaning towards Kali Linux as I can "arpspoof" the main server and effectively shut it off for a while.

解决方案

One can set the source IP to an arbitrary address on outbound data by manually constructing both network and transport layer headers, then sending the headers and the desired payload to a raw socket. The use of raw sockets may require elevated permissions, or may be disabled or restricted by the kernel, such as on some Microsoft platforms. Furthermore, due to TCP's three-way handshake and unpredictable sequence numbers, the effectiveness of the forged TCP segment, outside of potential TCP reset attacks, is questionable.

Routing is a different topic and depends on the various routers and configurations. For instance, devices may perform egress filtering and drop packets with a source address for which the device cannot validate. Furthermore, the affects of an IP address conflict can vary, but it often results in an intermittent connection.


Boost.Asio provides a basic_raw_socket<Protocol> template that expects a type meeting the Protocol type requirement. For instance, below is the start or a raw protocol:

struct raw
{
  typedef boost::asio::ip::basic_endpoint<raw> endpoint;
  int type() const     { return SOCK_RAW;    }
  int protocol() const { return IPPROTO_RAW; }
  int family() const   { return PF_INET;     }
};

boost::asio::basic_raw_socket<raw> socket;

When dealing with raw sockets, the difficulty is often not in using Boost.Asio, but rather having to implement the Network and Transport wire protocol. Below is a complete minimal example where I tried to keep it as simple as possible by creating a raw protocol and using basic_raw_socket to send a UDP message:

#include <algorithm>
#include <iostream>

#include <boost/array.hpp>
#include <boost/asio.hpp>
#include <boost/cstdint.hpp>

/// @brief raw socket provides the protocol for raw socket.
class raw
{
public:
  ///@brief The type of a raw endpoint.
  typedef boost::asio::ip::basic_endpoint<raw> endpoint;

  ///@brief The raw socket type.
  typedef boost::asio::basic_raw_socket<raw> socket;

  ///@brief The raw resolver type.
  typedef boost::asio::ip::basic_resolver<raw> resolver;

  ///@brief Construct to represent the IPv4 RAW protocol.
  static raw v4()
  {
    return raw(IPPROTO_RAW, PF_INET);
  }

  ///@brief Construct to represent the IPv6 RAW protocol.
  static raw v6()
  {
    return raw(IPPROTO_RAW, PF_INET6);
  }

  ///@brief Default constructor.
  explicit raw()
    : protocol_(IPPROTO_RAW),
      family_(PF_INET)
  {}

  ///@brief Obtain an identifier for the type of the protocol.
  int type() const
  {
    return SOCK_RAW;
  }

  ///@brief Obtain an identifier for the protocol.
  int protocol() const
  {
    return protocol_;
  }

  ///@brief Obtain an identifier for the protocol family.
  int family() const
  {
    return family_;
  }

  ///@brief Compare two protocols for equality.
  friend bool operator==(const raw& p1, const raw& p2)
  {
    return p1.protocol_ == p2.protocol_ && p1.family_ == p2.family_;
  }

  /// Compare two protocols for inequality.
  friend bool operator!=(const raw& p1, const raw& p2)
  {
    return !(p1 == p2);
  }

private:
  explicit raw(int protocol_id, int protocol_family)
    : protocol_(protocol_id),
      family_(protocol_family)
  {}

  int protocol_;
  int family_;
};

///@ brief Mockup ipv4_header for with no options.
//
//  IPv4 wire format:
//
//  0                   1                   2                   3
//  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-------+-------+-------+-------+-------+-------+-------+------+   ---
// |version|header |    type of    |    total length in bytes     |    ^
// |  (4)  | length|    service    |                              |    |
// +-------+-------+-------+-------+-------+-------+-------+------+    |
// |        identification         |flags|    fragment offset     |    |
// +-------+-------+-------+-------+-------+-------+-------+------+  20 bytes
// | time to live  |   protocol    |       header checksum        |    |
// +-------+-------+-------+-------+-------+-------+-------+------+    |
// |                      source IPv4 address                     |    |
// +-------+-------+-------+-------+-------+-------+-------+------+    |
// |                   destination IPv4 address                   |    v
// +-------+-------+-------+-------+-------+-------+-------+------+   ---
// /                       options (if any)                       /
// +-------+-------+-------+-------+-------+-------+-------+------+
class ipv4_header
{
public:
  ipv4_header() { std::fill(buffer_.begin(), buffer_.end(), 0); }

  void version(boost::uint8_t value) {
    buffer_[0] = (value << 4) | (buffer_[0] & 0x0F);
  }

  void header_length(boost::uint8_t value)
  {
    buffer_[0] = (value & 0x0F) | (buffer_[0] & 0xF0);
  }

  void type_of_service(boost::uint8_t value) { buffer_[1] = value; }
  void total_length(boost::uint16_t value) { encode16(2, value); }
  void identification(boost::uint16_t value) { encode16(4, value); }

  void dont_fragment(bool value)
  {
    buffer_[6] ^= (-value ^ buffer_[6]) & 0x40;
  }

  void more_fragments(bool value)
  {
    buffer_[6] ^= (-value ^ buffer_[6]) & 0x20;
  }

  void fragment_offset(boost::uint16_t value)
  {
     // Preserve flags.
     auto flags = static_cast<uint16_t>(buffer_[6] & 0xE0) << 8;
     encode16(6, (value & 0x1FFF) | flags);
  }

  void time_to_live(boost::uint8_t value) { buffer_[8] = value; }
  void protocol(boost::uint8_t value) { buffer_[9] = value; }
  void checksum(boost::uint16_t value) { encode16(10, value); }

  void source_address(boost::asio::ip::address_v4 value)
  {
    auto bytes = value.to_bytes();
    std::copy(bytes.begin(), bytes.end(), &buffer_[12]);
  }

  void destination_address(boost::asio::ip::address_v4 value)
  {
    auto bytes = value.to_bytes();
    std::copy(bytes.begin(), bytes.end(), &buffer_[16]);
  }

public:

  std::size_t size() const { return buffer_.size(); }

  const boost::array<uint8_t, 20>& data() const { return buffer_; }

private:

  void encode16(boost::uint8_t index, boost::uint16_t value)
  {
    buffer_[index] = (value >> 8) & 0xFF;
    buffer_[index + 1] = value & 0xFF;
  }

  boost::array<uint8_t, 20> buffer_;
};

void calculate_checksum(ipv4_header& header)
{
  // Zero out the checksum.
  header.checksum(0);

  // Checksum is the 16-bit one's complement of the one's complement sum of
  // all 16-bit words in the header.

  // Sum all 16-bit words.
  auto data = header.data();
  auto sum = std::accumulate<boost::uint16_t*, boost::uint32_t>(
    reinterpret_cast<boost::uint16_t*>(&data[0]),
    reinterpret_cast<boost::uint16_t*>(&data[0] + data.size()),
    0);

  // Fold 32-bit into 16-bits.
  while (sum >> 16)
  {
    sum = (sum & 0xFFFF) + (sum >> 16);
  }

  header.checksum(~sum);
}

///@brief Mockup IPv4 UDP header.
//
// UDP wire format:
//
//  0                   1                   2                   3
//  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-------+-------+-------+-------+-------+-------+-------+------+   ---
// |          source port          |      destination port        |    ^
// +-------+-------+-------+-------+-------+-------+-------+------+  8 bytes
// |            length             |          checksum            |    v
// +-------+-------+-------+-------+-------+-------+-------+------+   ---
// /                        data (if any)                         /
// +-------+-------+-------+-------+-------+-------+-------+------+
class udp_header
{
public:
  udp_header() { std::fill(buffer_.begin(), buffer_.end(), 0); }

  void source_port(boost::uint16_t value)      { encode16(0, value); }
  void destination_port(boost::uint16_t value) { encode16(2, value); }
  void length(boost::uint16_t value)           { encode16(4, value); }
  void checksum(boost::uint16_t value)         { encode16(6, value); }

public:

  std::size_t size() const { return buffer_.size(); }

  const boost::array<uint8_t, 8>& data() const { return buffer_; }

private:

  void encode16(boost::uint8_t index, boost::uint16_t value)
  {
    buffer_[index] = (value >> 8) & 0xFF;
    buffer_[index + 1] = value & 0xFF;
  }

  boost::array<uint8_t, 8> buffer_;
};


int main()
{
  boost::asio::io_service io_service;

  // Create all I/O objects.
  boost::asio::ip::udp::socket receiver(io_service,
    boost::asio::ip::udp::endpoint(boost::asio::ip::udp::v4(), 0));
  boost::asio::basic_raw_socket<raw> sender(io_service,
    raw::endpoint(raw::v4(), 0));

  const auto receiver_endpoint = receiver.local_endpoint();

  // Craft a UDP message with a payload 'hello' coming from
  // 8.8.8.8:54321
  const boost::asio::ip::udp::endpoint spoofed_endpoint(
    boost::asio::ip::address_v4::from_string("8.8.8.8"),
    54321);
  const std::string payload = "hello";

  // Create the UDP header.
  udp_header udp;
  udp.source_port(spoofed_endpoint.port());
  udp.destination_port(receiver_endpoint.port());
  udp.length(udp.size() + payload.size()); // Header + Payload
  udp.checksum(0); // Optioanl for IPv4

  // Create the IPv4 header.
  ipv4_header ip;
  ip.version(4);                   // IPv4
  ip.header_length(ip.size() / 4); // 32-bit words
  ip.type_of_service(0);           // Differentiated service code point
  auto total_length = ip.size() + udp.size() + payload.size();
  ip.total_length(total_length); // Entire message.
  ip.identification(0);
  ip.dont_fragment(true);
  ip.more_fragments(false);
  ip.fragment_offset(0);
  ip.time_to_live(4);
  ip.source_address(spoofed_endpoint.address().to_v4());
  ip.destination_address(receiver_endpoint.address().to_v4());
  ip.protocol(IPPROTO_UDP);
  calculate_checksum(ip);

  // Gather up all the buffers and send through the raw socket.
  boost::array<boost::asio::const_buffer, 3> buffers = {{
    boost::asio::buffer(ip.data()),
    boost::asio::buffer(udp.data()),
    boost::asio::buffer(payload)
  }};
  auto bytes_transferred = sender.send_to(buffers,
    raw::endpoint(receiver_endpoint.address(), receiver_endpoint.port()));
  assert(bytes_transferred == total_length);

  // Read on the reciever.
  std::vector<char> buffer(payload.size(), '\0');
  boost::asio::ip::udp::endpoint sender_endpoint;
  bytes_transferred = receiver.receive_from(
      boost::asio::buffer(buffer), sender_endpoint);

  // Verify.
  assert(bytes_transferred == payload.size());
  assert(std::string(buffer.begin(), buffer.end()) == payload);
  assert(spoofed_endpoint == sender_endpoint);

  // Print endpoints.
  std::cout <<
    "Actual sender endpoint: " << sender.local_endpoint() << "\n"
    "Receiver endpoint: " << receiver.local_endpoint() << "\n"
    "Receiver's remote endpoint: " << sender_endpoint << std::endl;
}

Output:

$ sudo ./a.out
Actual sender endpoint: 0.0.0.0:255
Receiver endpoint: 0.0.0.0:44806
Receiver's remote endpoint: 8.8.8.8:54321

As indicated in the output, although the true sender endpoint is 0.0.0.0:255, the receiver believes the sender's endpoint is 8.8.8.8:54321.

这篇关于在Boost ASIO中如何设置源IP地址以模拟另一个服务器的IP地址?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆