前言
近年来,网络安全领域面临着日益复杂的挑战,其中,恶意软件的演化速度和策略的多样性尤为显著。随着传统的检测和防护手段在面对新型攻击方式时表现出明显的局限性,研究人员和安全专家们不断探索和应对这些威胁的有效策略。域生成算法(Domain Generation Algorithm,简称DGA)作为一种高级的恶意软件通信技术,逐渐成为攻击者规避检测的重要工具。其通过生成大量伪随机域名,使恶意软件能够持续与其控制服务器(Command and Control, C2)保持联系,从而规避基于签名和黑名单的防御机制。ATT&CK-ID: T1568.002
本文旨在提供一个从基础到高级的全方位分析,深入探讨DGA技术的内涵、原理、实现方式、应用场景以及其在恶意软件中的使用策略。首先,我们将从DGA的基本概念出发,解释其如何通过伪随机域名生成过程实现与C2服务器的通信。其次,文章将详细介绍DGA的不同实现方式,包括算法的选择、种子的使用、以及域名生成的具体流程。此外,本文还将探讨DGA在实际恶意软件中的应用实例,分析其如何被用于逃避安全防御,并评估其对网络安全造成的影响。
通过对DGA技术的系统化研究,本文不仅为理解和识别此类攻击提供理论基础,也为开发更有效的检测和防御策略提供参考。最后,我们将讨论未来研究的方向,提出如何通过技术创新和策略调整来应对DGA技术带来的挑战,推动网络安全防御体系的持续进化。
一、什么是域生成算法 (DGA)
域生成算法(DGA)是一种由恶意软件使用的方法,用于生成伪随机域名。这些域名通常用于与指挥与控制(C2)服务器建立通信。DGA的核心目标是通过生成大量随机域名来绕过传统的黑名单技术,同时提供一种动态且不可预测的通讯渠道。
基本概念
DGA的基本原理是通过使用一个预定义的算法来生成域名,这个算法可以基于日期、时间、种子值等因素,生成一系列域名。恶意软件和其C2服务器共享这个算法,允许它们在互联网上寻找并建立联系,而无需依靠固定域名或IP地址。
历史背景
DGA的概念并非新鲜事,但随着网络安全技术的进步和恶意软件的发展,DGA技术也随之演化。从早期的Conficker蠕虫使用简单的DGA,到现在复杂的恶意软件使用基于机器学习的DGA,其复杂性和隐蔽性不断提升。
二、DGA域生成算法逻辑
DGA的核心逻辑在于如何生成伪随机域名。以下是几种常见的DGA逻辑:
1、基于时间的DGA生成算法逻辑
概念:
基于时间的DGA是指一种通过利用当前时间或特定的时间段来生成域名的算法。这种算法主要用于恶意软件与其控制服务器之间的通信,旨在通过动态变更域名来避免被传统的网络安全措施(如域名黑名单)所检测到。
算法逻辑:
-
时间种子:
- 使用当前系统时间作为随机数生成器的种子。时间种子确保了每一次生成的域名都具有时间上的关联性。例如,使用Python的
time
模块可以获取当前时间:seed = time.time()
。
- 使用当前系统时间作为随机数生成器的种子。时间种子确保了每一次生成的域名都具有时间上的关联性。例如,使用Python的
-
加密哈希:
- 通过哈希函数(如SHA-256)将时间种子转换为一个固定的哈希值。这个哈希值确保了即使是微小的时间变化也会导致完全不同的输出,从而增加了域名的随机性和不可预测性。例如:
hash_value = hashlib.sha256(seed.encode()).hexdigest()
。
- 通过哈希函数(如SHA-256)将时间种子转换为一个固定的哈希值。这个哈希值确保了即使是微小的时间变化也会导致完全不同的输出,从而增加了域名的随机性和不可预测性。例如:
-
域名生成:
- 从哈希值中提取特定数量的字符,通常是前若干位,然后根据需要加上顶级域名(TLD)以形成完整的域名。例如:
domain = hash_value[:10] + ".com"
。这样生成的域名在特定时间段内是可预测的,但对于外界来说仍然是随机的。
- 从哈希值中提取特定数量的字符,通常是前若干位,然后根据需要加上顶级域名(TLD)以形成完整的域名。例如:
示例代码:
简单的基于时间的DGA域名
import time import hashlib def generate_dga_domain(): seed = str(time.time()) hash_value = hashlib.sha256(seed.encode()).hexdigest() domain = hash_value[:10] + ".com" return domain print(generate_dga_domain())
【使用当前系统时间作为随机数生成器的种子】
【使用当前系统时间作为随机数生成器的种子】
基于时间的DGA算法生成逻辑思维导图:
2、基于随机数的DGA生成算法逻辑
首先,种子值的生成是关键。种子值可以是固定值,也可以是根据特定条件动态变化的,例如当前时间或某种外部输入。种子值决定了后续随机数生成的起点和序列的走向。选择一个好的种子是确保生成的域名序列具有足够随机性和不可预测性的基础。
其次,使用伪随机数生成器(PRNG)根据种子值生成一系列随机数。这些随机数是整个域名生成过程的核心,它们决定了将要生成的域名的字符。常见的PRNG包括线性同余生成器、Mersenne Twister等。PRNG的选择直接影响到生成的域名的质量和复杂性。
接着,域名生成过程开始。首先,需要决定字符集,这通常包括字母、数字以及某些特殊字符。然后,根据随机数的输出,从字符集中选择字符来组成域名。域名的长度也可以是随机的,或根据某种规则确定。顶级域名(TLD)如 .com, .net 等通常是固定的,或有一定范围的选择。
在生成过程中,为了增加域名的复杂性和防护性,可能会使用哈希函数或其他变换方法对原始随机数进行处理。这可以使生成的域名更难以通过字典攻击猜测,同时增加了域名的变换和不可预测性。
生成的域名列表可以是周期性的,每个周期生成一批新的域名以供使用。这意味着恶意软件能够在一定时间段内保持与C&C服务器的连接,而无需担心域名被封锁或预注册。
基于随机数的DGA算法生成逻辑思维导图:
这里使用上面思维导图中的基于随机数的DGA核心逻辑编写的Python示例代码:
import random import string import time import hashlib class DGAGenerator: def __init__(self, seed_type='time'): """ 初始化DGA生成器 :param seed_type: 种子类型,'time'为时间种子,'fixed'为固定种子 """ self.seed_type = seed_type self.seed = None def _set_seed(self): """根据种子类型设置随机种子""" if self.seed_type == 'time': self.seed = int(time.time()) elif self.seed_type == 'fixed': self.seed = 42 # 使用一个固定的种子值 random.seed(self.seed) def _generate_random_number(self, generator_type='mersenne'): """生成伪随机数""" if generator_type == 'linear': return random.randint(0, 2**31 - 1) # 线性同余生成器的简化版本 elif generator_type == 'mersenne': return random.getrandbits(32) # 使用Mersenne Twister def _select_characters(self): """选择用于生成域名的字符集""" return string.ascii_lowercase + string.digits def _determine_length(self): """确定域名的长度""" return random.randint(8, 12) # 域名长度在8到12之间 def _generate_domain(self, tld='.com'): """生成域名""" domain_length = self._determine_length() characters = self._select_characters() domain = ''.join(random.choice(characters) for _ in range(domain_length)) return domain, tld # 返回域名和TLD def _transform_domain(self, domain): """对域名进行变换""" # 使用哈希函数进行变换 hash_obj = hashlib.sha256(domain.encode()) transformed = hash_obj.hexdigest()[:10] # 取前10个字符作为新的域名 return transformed def _apply_dictionary_protection(self, domain): """增加字典攻击防护""" # 这里简单地插入一个随机字符 insert_position = random.randint(1, len(domain)-1) return domain[:insert_position] + random.choice(string.ascii_letters) + domain[insert_position:] def generate_dga_domains(self, num_domains=10, tld='.com', subdomains=None): """ 生成一系列DGA域名 :param num_domains: 要生成的域名数量 :param tld: 顶级域名 :param subdomains: 子域名列表(如果有的话) :return: 生成的域名列表 """ self._set_seed() domains = [] for _ in range(num_domains): domain, tld = self._generate_domain(tld) transformed_domain = self._transform_domain(domain) protected_domain = self._apply_dictionary_protection(transformed_domain) if subdomains: subdomain = random.choice(subdomains) protected_domain = f"{subdomain}.{protected_domain}{tld}" else: protected_domain += tld domains.append(protected_domain) return domains # 使用示例 dga = DGAGenerator(seed_type='time') generated_domains = dga.generate_dga_domains(num_domains=5, tld='.net', subdomains=['www', 'mail']) print("Generated DGA Domains:") for domain in generated_domains: print(domain)
【基于随机数的GDA算法逻辑生成的结果】
最后基于随机数的DGA的优点在于其难以预测性和高度的动态性。攻击者可以根据需要调整随机数生成器的参数或种子值,来生成新的域名序列,从而逃避检测和预防措施。通过分析生成域名的频率和特征,安全研究人员可以识别出DGA的模式。此外,预先注册可能被DGA使用的域名或通过网络流量分析识别出与DGA相关的通信,也是一些常见的对策。
3、基于字典的DGA生成算法逻辑
基于词典的DGA与其他随机生成方法不同,它利用了人类语言的词汇库或字典来生成域名。以下是这种算法的工作原理:
-
词典选择:首先,选择一个词汇库或字典。这可以是任何语言的词汇库,通常选择较为常见的词汇以提高域名的“自然度”,减少被标记为恶意的概率。
-
生成过程:
- 随机选择:从字典中随机选择一系列词汇。
- 组合:这些词汇可以直接拼接,或者通过插入数字、特殊字符、或改变字母大小写等方式来增加域名的复杂度。
- 后缀:最后,添加一个或多个常见的顶级域名(TLD)后缀,如
.com
,.org
,.net
等。
例如,假设我们的字典包含了以下单词:['apple', 'tree', 'house', 'dog']。基于字典的DGA可能生成如下域名:
appletreehouse.com
doghouse.net
treeapple.org
- 算法特性:
- 可预测性:尽管生成的域名看似随机,但由于使用了固定字典和生成规则,理论上可以通过分析算法和词典来预测未来的域名。
- 自然性:生成的域名由于基于真实的词汇,具有更高的“自然性”,减少了被检测为恶意域名的风险。
基于字典的DGA算法生成逻辑思维导图:
基于子典的DGA生成算法可以在上面示例中,现有的基于随机数的算法基础上进行扩展,通过导入一个文本字典来实现。采用这种方法,可以生成模仿或类似于正规域名的字符串,以此来提高逃避检测的能力,同时降低用户的识别率。
4、混合型DGA
混合型DGA(Domain Generation Algorithm)是一种融合了多种生成机制的域名生成策略(大杂烩),其设计目的在于通过增加域名生成的复杂性和不可预测性来逃避检测和封锁。以下是混合型DGA的几大特点:
-
时间依赖:混合型DGA利用当前日期、时间或其他时间相关信息作为种子值的一部分,使得生成的域名随时间变化。通过这种方法,即使一个域名被封锁,恶意软件也可以通过下一个时间点生成的新域名来继续与控制服务器通信,从而增加了预测域名生成的难度。
-
随机数生成:通过使用伪随机数生成器(PRNG),混合型DGA生成随机字符序列。这些字符可以包括字母、数字或特殊字符,提供域名生成的随机性和多样性,确保生成的域名不容易被预测。
-
子典选择:DGA从预定义的词典中选择单词或其组合,这样生成的域名看起来更像合法域名,从而增加了混淆性和可读性,使得传统的基于域名黑名单的防御措施难以有效识别。
-
哈希函数:将时间、随机数、以及词典选择的组合通过哈希函数(如SHA-256、MD5等)处理,确保生成的域名具有唯一性和随机性。这种方法不仅增加了域名的不可预测性,还可以使域名生成过程更难以被逆向工程分析。
-
算法混淆:使用混淆技术来隐藏或复杂化DGA的逻辑。通过这种方式,即使分析人员获取了恶意软件样本,也很难通过静态分析或逆向工程来理解DGA的实际生成逻辑。
-
字符映射:将生成的字符或单词进行映射,可能会将数字映射到字母,或者应用其他规则来增加域名的可读性和混淆性。这有助于使生成的域名看起来更加自然。
-
截取规则:从哈希结果中截取特定长度的字符串作为域名的一部分,以确保生成的域名长度符合规范,符合常见的域名长度要求。
-
顶级域名选择:DGA会选择或生成顶级域名(TLD),例如
.com
、.net
、.org
等,甚至可以使用不常见的TLD来增加复杂性,进一步提高了域名生成的多样性。 -
域名注册:一些复杂的DGA可能还会自动注册一小部分生成的域名,以确保通信通道的可用性。这不仅可以减少网络流量,还能误导分析人员,使其难以识别真正的C2服务器。
-
动态更新:为了进一步增加预测难度,DGA可能会在运行时动态更新其算法逻辑或种子值。这种动态性使得恶意软件即使在被检测到后也能快速调整策略,保持与控制服务器的通信。
综上混合型DGA通过结合时间依赖、随机数生成、词典选择、哈希函数、算法混淆、字符映射、截取规则、顶级域名选择、域名注册和动态更新等多种机制,实现了高度复杂和不可预测的域名生成策略。其主要目的在于逃避传统的防御措施,如域名黑名单和静态分析,使得恶意软件能够持续与控制服务器通信,次技术增加了预测和封锁域名的难度。
基于混合DGA算法生成逻辑思维导图:
三、有名的Conficker和Gameover Zeus恶意软件GDA手段
Conficker和Gameover Zeus是两个在其时代引起广泛关注的恶意软件实例,它们不仅展示了恶意软件的复杂性,也突显了网络犯罪的多样化和专业化。
1、Conficker:一个网络蠕虫的传奇
Conficker(也被称为Downup、Downadup或Kido)于2008年首次被发现,是一种自传播能力极强的蠕虫病毒。它利用了Windows系统中的多个漏洞,尤其是MS08-067(一个Windows服务的远程代码执行漏洞),进行快速传播。
-
传播方式:Conficker通过多种途径传播,包括利用网络共享、USB驱动器、弱口令以及利用其他已感染的计算机进行网络扫描和攻击。
-
DGA技术:Conficker的最大亮点之一是其使用了域名生成算法(DGA)。每天生成250个不同的域名,用于与其命令与控制(C2)服务器建立连接。这大大增加了其抗检测性,因为传统的安全措施很难预知这些域名。
-
影响:Conficker曾感染了数百万台计算机,形成了一个庞大的僵尸网络(botnet),被用于DDoS攻击、信息窃取以及其他恶意活动。
-
应对措施:安全社区对抗Conficker的主要策略包括预先注册其可能使用的域名、域名陷阱(sinkholing)以及不断更新的补丁和安全措施。
2、Gameover Zeus:金融恶意软件的巅峰
Gameover Zeus(GOZ)是Zeus恶意软件的一个变种,于2011年首次被发现,主要针对金融机构和个人用户的银行信息进行攻击。
-
功能:除了传统的Zeus恶意软件功能外,Gameover Zeus还引入了点对点(P2P)通信机制,使其在没有C2服务器的情况下也能保持活动。此外,它还能够进行勒索软件活动,进一步提高了其威胁级别。
-
DGA技术:Gameover Zeus每天生成1000个域名,通过一个复杂的算法使用32位计数器、当前时间和一个硬编码的密钥来生成,这些域名用于与其C2服务器通信。
-
传播方式:通过垃圾邮件、漏洞利用、以及其他恶意软件作为载体进行传播。
-
影响:Gameover Zeus不仅能够窃取银行信息,还通过其勒索软件变种CryptoLocker进行文件加密,要求用户支付赎金。其僵尸网络规模庞大,被用于多种网络犯罪活动。
-
应对措施:对抗Gameover Zeus需要复杂的策略,包括动态域名陷阱、DNS黑名单、机器学习检测以及逆向工程DGA算法。
【Conficker和Gameover Zeus技术手段流程】
GameOverZeus (GoZ) 恶意软件域生成算法 (DGA)
/* #-----------------------------------# # # # Copyright (C) 2016 Azril Rahim # # # # [email protected] # # # #-----------------------------------# */ #include "gozdga.h" GozDga::GozDga(QObject *parent) : QObject(parent) { } GozDga::~GozDga() { } int GozDga::executeA(int argc, char *argv[]) { quint32 numOfDomain; QString outfile; QString agStr; quint16 DD,MM,YY; QString dateAttack; numOfDomain = 0; outfile.clear(); DD = 0; MM = 0; YY = 0; for (int i = 1; i < argc; i++){ agStr = agStr + argv[i] + " "; } agStr = agStr.replace("&", " "); QStringList agStrL = agStr.trimmed().split(" "); for (int ag = 0; ag < agStrL.size(); ag++) { //qDebug() << "ag:" << ag; agStr = agStrL[ag]; agStr = agStr.trimmed(); if (agStr.isEmpty()){ continue; } //printf("%s\n",agStr.toStdString().c_str()); QStringList argvL = agStr.split("="); if (argvL.size() != 2){ printf("Invalid argument for input #%d\n",ag); return 0; } if (argvL.at(0) == "n"){ numOfDomain = argvL.at(1).toInt(); continue; } if (argvL.at(0) == "d") { dateAttack = argvL.at(1); QList<QString>dt = dateAttack.trimmed().split("-"); DD = dt.at(0).toInt(); MM = dt.at(1).toInt(); YY = dt.at(2).toInt(); //qDebug() << "done date"; continue; } if (argvL.at(0) == "f"){ outfile = argvL.at(1); continue; } } //qDebug() << "of looping"; if (numOfDomain == 0) { printf("GoZ: Missing or Invalid number of domain to be generated\n"); return -1; } if (dateAttack.trimmed().isEmpty()) { printf("Missing or Invalid date\n"); return -1; } this->generate(numOfDomain,DD,MM,YY,outfile); return 0; } void GozDga::generate(quint32 maxDomain, quint16 day, quint16 month, quint16 year, QString outfile) { QString dgaDomainName; //check endian this->imBE = this->isBE(); QFile fs; if (!outfile.trimmed().isEmpty()){ fs.setFileName(outfile); fs.open(QIODevice::WriteOnly | QIODevice::Text); } //generate domain name for maxDomain for (quint32 id = 0; id < maxDomain; id++){ dgaDomainName = this->getDomainName(id,day,month,year); if (!outfile.trimmed().isEmpty()){ dgaDomainName.append('\n'); fs.write(dgaDomainName.toLocal8Bit()); } else { printf("%s\n",dgaDomainName.toStdString().c_str()); } } if (!outfile.trimmed().isEmpty()){ fs.close(); } } QString GozDga::getDomainName(quint32 id, quint16 day, quint16 month, quint16 year){ QString seedHex1; QString seedHex2; quint8 mask; quint8 start; quint32 seedInt; QString domain; mask = 0; start = 0; domain.clear(); seedHex1 = this->getSeedHEX(id,day,month,year); for (mask = 0; mask < 16; mask+=4){ start = mask * 2; seedHex2 = seedHex1.mid(start,8); seedInt = this->hexToInt(seedHex2);//seedHex2.toUInt(0,16); domain.append(this->getDomainPart(seedInt,8)); } //qDebug() << "id:" << id; if ((id % 4) == 0) { domain.append(".com"); return domain; } if ((id % 3) == 0){ domain.append(".org"); return domain; } if ((id % 2) == 0){ domain.append(".biz"); return domain; } domain.append(".net"); return domain; } quint32 GozDga::hexToInt(QString hex){ QString tmp; quint16 loc = hex.size(); while(1){ tmp.append(hex.mid(loc -2,2)); loc = loc -2; if (loc == 0) break; } //qDebug() << "hextoint seed from hex" << tmp.toUInt(0,16); return tmp.toUInt(0,16); } quint16 GozDga::toLE(quint16 val){ unsigned char *ptr = (unsigned char *)&val; return(ptr[0] << 8 | ptr[1]); } quint32 GozDga::toLE(quint32 val){ unsigned char *ptr = (unsigned char *)&val; return (ptr[0] << 24) | (ptr[1] << 16) | (ptr[2] << 8) | ptr[3]; } bool GozDga::isBE() { quint16 id; unsigned char *p; id = 1; p = (unsigned char*)&id; if (p[0] == '\x01') return false; return true; } QString GozDga::getSeedHEX(quint32 id, quint16 day, quint16 month, quint16 year){ QCryptographicHash md5(QCryptographicHash::Md5); QString key("\x01\x05\x19\x35"); md5.reset(); if (this->imBE){ id = this->toLE(id); year = this->toLE(year); day = this->toLE(day); month = this->toLE(month); } md5.addData((const char*)&id,4); md5.addData((const char*)&year,2); md5.addData(key.toLatin1().data()); md5.addData((const char*)&month,2); md5.addData(key.toLatin1().data()); md5.addData((const char*)&day,2); md5.addData(key.toLatin1().data()); return md5.result().toHex(); } QString GozDga::getDomainPart(quint32 seed, quint8 maxSize){ QString tmpd; QString tmpd2; QChar c; quint32 edx; for (quint8 i = 0; i < maxSize; i++){ edx = seed % 36; seed /=36; if (edx > 9) c = QChar(QChar('a').toLatin1() + (edx - 10)); else c = QChar(edx + QChar('0').toLatin1()); tmpd2 = tmpd; tmpd = c; tmpd.append(tmpd2); if (seed == 0) break; } return tmpd; }
利用流程
-
参数输入:
- 用户提供所需生成的域名数量(
numOfDomain
)、日期(day
,month
,year
)以及可选的输出文件名(outfile
)。
- 用户提供所需生成的域名数量(
-
种子生成:
- 使用一个硬编码的密钥(例如,"\x01\x05\x19\x35"),结合日期信息和一个32位的计数器,通过MD5哈希生成一个种子值(
seedHex1
)。
- 使用一个硬编码的密钥(例如,"\x01\x05\x19\x35"),结合日期信息和一个32位的计数器,通过MD5哈希生成一个种子值(
-
域名生成:
- 将种子值分成4个部分,每部分8个字符。
- 通过一个算法将这些部分转换为域名的组成部分。该算法确保生成的域名符合DNS规范,每个部分可以是数字0-9或字母a-z。
- 根据域名的编号(
id
),选择不同的顶级域名(TLD),例如.com
,.org
,.biz
, 或.net
。
-
输出:
- 如果指定了输出文件,生成的域名将写入该文件;否则,域名将打印到标准输出。
-
循环生成:
- 根据
numOfDomain
参数,重复上述步骤生成指定数量的域名。
- 根据
-
C2通信:
- GOZ使用这些生成的域名尝试与其C2服务器建立连接,进行命令与控制通信。
总结
这篇文章深入探讨了域名生成算法(DGA)在恶意软件中的应用,从基本原理到具体实现方式,再到实际案例的分析,提供了从初学者到专业人员都能理解的全方位内容。DGA作为一种高级的恶意软件通信技术,通过生成大量伪随机域名,帮助恶意软件逃避传统的安全检测机制,保持与其控制服务器的联系。
我们首先介绍了DGA的基本概念和历史背景,指出其在网络安全领域中的重要性和不断演化的趋势。接着,通过详尽的逻辑分析和示例代码,展示了几种常见的DGA生成算法,包括基于时间、随机数、字典以及混合型DGA的实现方式。特别地,通过Conficker和Gameover Zeus的案例,详细介绍了这些恶意软件如何利用DGA技术进行传播和控制。
结束语
通过阅读这篇文章,你可能会对恶意软件的复杂性和安全领域的挑战有更深的认识。DGA技术确实给网络安全带来了极大的挑战,但同时也推动了安全技术的创新和进步。理解和研究DGA不仅有助于我们更好地识别和防范此类威胁,也启发了安全专家们开发更智能、更有效的防御策略。
尽管DGA使得恶意软件的检测和预防变得更加困难,但通过不断的研究和技术创新,我们可以找到对策。未来的网络安全将更加依赖于机器学习、行为分析、以及对网络流量更深入的理解。同时,用户的安全意识和良好的安全实践也是抵御这些威胁的关键。
作为读者,你可能没有每天与这些技术打交道的机会,但了解这些知识可以帮助你更好地理解网络安全的重要性,意识到网络犯罪的复杂性,并采取更安全的行为来保护自己。希望这篇文章不仅让你了解了DGA技术,还能激发你对网络安全或者恶意威胁分析的兴趣。
参考
https://www.zscaler.com/blogs/security-research/look-new-gameover-zeus-variant
https://www.linkedin.com/pulse/gameover-zeus-dga-explained-azril-rahim/
https://medium.com/threat-intel/conficker-anniversary-db03f2487241
4A评测 - 免责申明
本站提供的一切软件、教程和内容信息仅限用于学习和研究目的。
不得将上述内容用于商业或者非法用途,否则一切后果请用户自负。
本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑或手机中彻底删除上述内容。
如果您喜欢该程序,请支持正版,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。敬请谅解!
程序来源网络,不确保不包含木马病毒等危险内容,请在确保安全的情况下或使用虚拟机使用。
侵权违规投诉邮箱:4ablog168#gmail.com(#换成@)