以P2P的方式追踪 DDG 僵尸网络(下)

admin 2023-12-01 13:54:09 AnQuanKeInfo 来源:ZONE.CI 全球网 0 阅读模式

本系列文章从 Botnet(僵尸网络)的基础概念说起,围绕实现了 P2P 特性的 DDG.Mining.Botnet,一步一步设计一个基于 P2P 的僵尸网络追踪程序,来追踪 DDG。DDG 是一个目前仍十分活跃的 Botnet,读懂本文,再加上一些辅助分析工作,就可以自行实现一套针对 DDG 的 P2P 僵尸网络跟踪程序。内容分 上、下 两部分:

  1. 上 半部分写本人理解的 Botnet 相关概念,然后介绍 DDG Botnet,着重介绍其涉及的 P2P 特性;
  2. 下 半部分写如何根据 DDG.Mining.Botnet 的 P2P 特性,来设计一个僵尸网络跟踪程序 DDG.P2P.Tracker,用以遍历 Botnet 中的节点、及时获取最新的云端配置文件、及时下载到 Botnet 中最新的恶意样本、及时获知 Botnet 中最新启用的 C&C 服务器。

上半部分传送门: 以P2P的方式追踪 DDG 僵尸网络(上)

 

3. 追踪程序设计

3.1 追踪程序的执行流程

前文说过,设计追踪程序的最终目标,有 4 个,其中涉及到 Peer 信息的获取和保存、样本与配置数据的解析和保存、记录最新启用的 C&C Server ……这样一来,就不可避免地将相关数据和文件保存到本地或数据库中。

我们可以把最新一次探测到的 P2P 节点信息存储到数据库中,把样本文件、配置数据、最新的 C&C Server 列表保存到本地文件中。根据 Memberlist 框架的实现,程序要调用 memberlist.Join() 函数来加入一个已存在的 P2P 网络,而这个函数需要一个 IP List( Go 变量 [] string ,下文简称 init_peers) 来作为加入 P2P 网络的“介绍人”。当然,这个 IP List 中的 IP,应该是当前已加入 P2P 网络的 IP (按照这个概念,这些 IP 应该是对应常规 P2P 网络中的 Node,P2P 网络中的 Node 和 Peer 的概念可以自行了解,为了简化描述,本文把 P2P 网络中的节点统称为 Peer)。

前文还说过,ddg 主样本中有一份内置硬编码的 HUB IP List。其实,这一份 HUB IP List 就可以拿来当做 memberlist.Join() 函数的参数,即 init_peers。为了方便程序运行,我们可以把这一份 IP List 提前保存到数据库中,追踪程序每次运行,都要先从数据库中读取最新的 init_peers,通过 init_peers 加入 ddg 的 P2P 网络。

这里先说一下追踪程序的概要执行流程,后面分步骤详细说明:

  1. 从数据库中读取 init_peers IP List ,并调用 memberlist.Join() 加入 ddg 的 P2P 网络;
  2. 成功加入 P2P 网络后,调用 memberlist.Members() 获取当前网络中的最新 Peers List;
  3. 解析获取到的 Peers List 中的 Peers 信息,将每个 Peer 信息拆解成 IP:Port:Versioin:Hash:DateTime 5 元组,存到数据库中;
  4. 将每个 Peer IP ,拼接 URL 串 http://<peer_ip>:8000/slave ,并向该 URL 发送 Post 请求,以获取经过 msgPack 编码的配置数据;
  5. 如果成功从某个 Peer 上获取到了配置数据,则:
    • 将该 Peer IP 暂存到一个非重复的、并发安全的 IP List 结构中;
    • 保存 RAW 格式的配置数据到本地;
    • 用该 Peer IP 拼接 URL 串 http://<peer_ip>:8000/i.sh ,并用 HTTP GET 请求的方式尝试获取最新的恶意 Shell 脚本;
    • 对比上述 i.sh 下载链接与刚获取到的最新配置数据中执行的 i.sh 下载链接是否相同,不同则对最新配置数据中指定的 i.sh 脚本也做下载&解析操作。
  6. 如果成功获取到 i.sh 脚本,则解析其中的样本 Download URL,下载样本,同本地已下载到的其他样本 MD5 和下载 URL 作对比,MD5 和 下载 URL 其中之一是新的,就保留样本,否则删除刚下载到的样本。对于新样本,通过 Slack 的 Message 接口 Push 相关消息到自己的 Slack Channel 中;
  7. 最后,将非重复的最新活跃的 C&C Server 列表保存到本地文件中。

3.2 加入 P2P 网络

前文提到,调用 memberlist.Join() 来加入 ddg 的 P2P 网络,需要一个 init_peers 的 IP List。这个 IP List 最初来自 ddg 主样本中硬编码的 HUB IP List,而以后追踪程序每次执行,都要先从数据库中获取这个 IP List。这里先给出一个可用的数据表结构,用来存储 Peer 信息:

+---------+----------------------+------+-----+---------+----------------+
| Field   | Type                 | Null | Key | Default | Extra          |
+---------+----------------------+------+-----+---------+----------------+
| id      | int(10) unsigned     | NO   | PRI | <null>  | auto_increment |
| ip      | char(16)             | NO   |     | <null>  |                |
| port    | smallint(5) unsigned | NO   |     | <null>  |                |
| version | smallint(5) unsigned | NO   |     | <null>  |                |
| hash    | char(32)             | YES  |     | <null>  |                |
| tdate   | datetime             | NO   |     | <null>  |                |
+---------+----------------------+------+-----+---------+----------------+

最新的 Peers 信息在我们加入 ddg 的 P2P 网络后可以调用 memberlist.Members() 来获取。在 Memberlist 框架的源码中,这个函数返回的是一个 Node 信息指针列表 (Go 语言变量 []*Node)。Memberlist 框架中的 Node 结构体的定义如下:

// Node represents a node in the cluster.
type Node struct {
    Name string
    Addr net.IP
    Port uint16
    Meta []byte // Metadata from the delegate for this node.
    PMin uint8  // Minimum protocol version this understands
    PMax uint8  // Maximum protocol version this understands
    PCur uint8  // Current version node is speaking
    DMin uint8  // Min protocol version for the delegate to understand
    DMax uint8  // Max protocol version for the delegate to understand
    DCur uint8  // Current version delegate is speaking
}

其中第一项 Name 是形如 VerNumber.HashValue 的一个字符串,如:3020.b1634b9e0c747a6ae728e07c40883e2d 。这里的 Hash 值在 Memberlist 框架中被定义为 UID ,每一个 Peer 都不同,其值是通过对当前 Peer 主机的网络配置用 MD5 算法计算得出。

Memberlist 的开源项目主页上,有一个简单的 Usage Demo,演示加入一个集群(本文就指 ddg 的 P2P 网络了)并获取节点信息的最简方法:

/* Create the initial memberlist from a safe configuration.
   Please reference the godoc for other default config types.
   http://godoc.org/github.com/hashicorp/memberlist#Config
*/
list, err := memberlist.Create(memberlist.DefaultLocalConfig())
if err != nil {
    panic("Failed to create memberlist: " + err.Error())
}

// Join an existing cluster by specifying at least one known member.
n, err := list.Join([]string{"1.2.3.4"})
if err != nil {
    panic("Failed to join cluster: " + err.Error())
}

// Ask for members of the cluster
for _, member := range list.Members() {
    fmt.Printf("Member: %s %sn", member.Name, member.Addr)
}

// Continue doing whatever you need, memberlist will maintain membership
// information in the background. Delegates can be used for receiving
// events when members join or leave.

可以看到在执行 Join() 函数加入集群之前,还要调用 memberlist.Create() 函数生成一个 Peer 对象(代表当前 Peer),然后用当前对象执行 Join 以及后续操作。这里有一个关键点是当前 Peer 的配置。这份配置的底层结构体定义,在 Memberlist 的 Godoc 文档中有详细说明,此处不赘述。这份配置结构中的两个关键配置项(网络配置和密钥),关乎到追踪程序能否成功加入到 ddg 的 P2P 网络中,以及加入之后能否正常与其他 Peers 通信,这两个关键点要逆向 ddg 主样本和熟知 Memberlist 的原理和实现才能搞定,这里也不赘述。想要自行实现这么一套追踪程序,需要自行完成这两个工作。

需要一提的是,配置项中有一个关于日志输出的配置项:

// Logger is a custom logger which you provide. If Logger is set, it will use
// this for the internal logger. If Logger is not set, it will fall back to the
// behavior for using LogOutput. You cannot specify both LogOutput and Logger
// at the same time.
Logger *log.Logger

我们要用到日志功能,把全局的日志句柄配置在这里,这样 Memberlist 整个框架的运行日志都会打到我们指定的日志文件中。

3.3 获取并解析最新的 Peers List

前文提到,获取最新的 Peers List,只需在加入 ddg 的 P2P 网络后调用 memberlist.Members() 即可。

其实只说了一半,因为这里还有个偶然发现的小 Trick:这个函数获取到的 Peers List 数量并不大,反倒是从 Memberlist 框架的运行日志中可以抽取更多 Peer 信息。

根据 Memberlist 的框架特性,当前节点加入 P2P 网络之后,会随机与其他 Peers 以 Gossip 的形式通信,这种通信具有节点探测的功能。通信的结果会记录在日志中,尤其是通信失败的日志,记录的比较详细。一条失败的 Gossip 通信日志如下:

2019/01/23 08:44:53 [ERR] memberlist: Failed to send gossip to 58.144.150.24:7946: write udp 127.0.0.1:7946->58.144.150.24:7946: sendto: invalid argument

打出这段日志的代码,在 memberlist/stat.go 中实现:

不过,这段错误信息还不足以提供我们想要的 Peer Info 5 元组。那就动手 Patch 一下这段代码,让它打出我们想要的信息。Patch 后的代码如下:

然后,打出来的日志内容就会是如下形式:

2019/02/24 18:01:06 [ERR] memberlist: Failed to send gossip to (114.118.18.70:7946:3020:91f7f67194e0d31d9b58d9e6bef4f711)

这样,既缩减了日志文件的体积,也能精准捕获到我们需要的信息。然后,就可以把 memberlist.Members() 函数获取到的 Peers 信息和日志文件中打出来的 Peers 信息汇总起来,保存到一个变量中,以待后用。

3.4 保存 Peers 信息

将上述步骤获取到的 Peers 信息保存到数据库中,最新的 20 条 Peers 信息示例如下:

3.5 探测最新活跃的 C&C,拉取最新的配置数据

对上面获取到的 Peers Info 中的每一个 Peer IP,拼接成 URL 串 http://<peer_ip>:8000/slave ,向该 URL 发 Post 请求。能获取符合格式的配置数据的,即为当前存活的 C&C IP。把存活的 C&C Host 信息保存到一个非重复的、并发安全的 List 结构的变量中,最后把这份 C&C Host 列表保存到本地文件中。本地 C&C Host 文件列表部分内容如下:

➜ tail cc_server.list
20190503060101  132.148.241.138:8000
20190503060101  109.237.25.145:8000
20190503060101  104.128.230.16:8000
20190503060101  117.141.5.87:8000
20190506060101  132.148.241.138:8000
20190506060101  104.128.230.16:8000
20190506060101  117.141.5.87:8000
20190506120102  104.128.230.16:8000
20190506120102  132.148.241.138:8000
20190506120102  117.141.5.87:8000

前面提到过 ddg 配置数据是经过 msgPack 编码的,前文也列出了解码后的配置数据示例。受限于 Memberlist 的框架实现,我们的追踪程序也只能用 Go 语言来实现。要解码这份配置数据,直接调用 msgPack 的 Go 语言 API 是不够的,还需要逆向分析出配置数据的正确结构,并用 Go 语言的语法来定义这个配置数据的结构。下面是掉了两把头发才逆向出来的配置数据结构,以 Go 语言来定义的结构体:

import msgpack
/*
    Salve conf struct
*/

type Conf struct {
    Data      []byte
    Signature []byte
}

type ConfData struct {
    CfgVer int
    Config MainConf
    Miner  []MinerConf
    Cmd    CmdConf
}

type MainConf struct {
    Interval string
}

type MinerConf struct {
    Exe string
    Md5 string
    Url string
}

type CmdConf struct {
    AAredis CmdConfDetail
    AAssh   CmdConfDetail
    Sh      []ShConf
    Killer  []ProcConf
    LKProc  []ProcConf
}

type CmdConfDetail struct {
    Id         int
    Version    int
    ShellUrl   string
    Duration   string
    NThreads   int
    IPDuration string
    GenLan     bool
    GenAAA     bool
    Timeout    string
    Ports      []int
}

type ShConf struct {
    Id      int
    Version int
    Line    string
    Timeout string
}

type ProcConf struct {
    _msgpack struct{} `msgpack:",omitempty"`
    Id       int
    Version  int
    Expr     string
    Timeout  string
}

将解码成功的配置数据打到日志文件中,只把未解码的 RAW 配置数据保存到本地。最新获取到的配置数据如下:

➜ ll -t slave_conf | head
total 4.6M
2.0K May  6 12:34 117_141_5_87__20190506123410.raw    
2.0K May  6 12:34 132_148_241_138__20190506123410.raw 
2.0K May  6 12:33 104_128_230_16__20190506123309.raw  
2.0K May  6 06:33 104_128_230_16__20190506063312.raw  
2.0K May  6 06:31 117_141_5_87__20190506063142.raw    
2.0K May  6 06:30 132_148_241_138__20190506063042.raw 
2.0K May  6 00:39 104_128_230_16__20190506003932.raw  
2.0K May  6 00:37 117_141_5_87__20190506003723.raw    
2.0K May  6 00:34 132_148_241_138__20190506003442.raw

3.6 下载最新样本

对于上面步骤中,每一个可以获取合格配置数据的 C&C IP,拼接 URL 串 http://<cc_ip>:8000/i.sh ,这是 ddg 目前用到的最新恶意 Shell 脚本的下载链接。通过 HTTP GET 请求下载这个 i.sh 文件,跟本地已有的、相同 URL 下载到的 i.sh 文件对比 MD5 值,如果 MD5 跟旧的 i.sh 相同,则丢弃刚下载 i.sh 文件。

如果最新的 i.sh 文件跟旧 i.sh 文件 MD5 不同,则进行以下两步操作:

  1. 对比上述 i.sh 下载链接与刚获取到的最新配置数据中执行的 i.sh 下载链接是否相同,不同则对最新配置数据中指定的 i.sh 脚本也做下载&解析操作;
  2. 成功获取到 i.sh 脚本,则解析其中的样本 Download URL,下载样本,同本地相同 URL 下载到的样本对比 MD5 和 FileName(其实是 Download URL),如果 MD5 或者 FileName 不同,则保留样本,否则删除刚下载到的样本。样本 MD5 和 FileName 的对比结果,有三种情况:
    • 仅仅 MD5 不同而 FileName 相同,说明同一个 URL 中下到了不同 MD5 的样本,即样本有更新;
    • 仅仅 FileName 不同而 MD5 相同,则不同的 URL 想到了相同 MD5 的样本,通常意味着 C&C 有变动;
    • 两者都不同则说明 C&C 有变动并且样本有更新。

截至目前,我通过 ddg 追踪程序监控到的一部分 ddg 样本如下:

➜ ll sample
total 115M
1.7K 104_236_156_211__8000__i_sh+8801aff2ec7c44bed9750f0659e4c533
8.8M 104_236_156_211__8000__static__3019__fmt_i686+8c2e1719192caa4025ed978b132988d6
 11M 104_236_156_211__8000__static__3019__fmt_x86_64+d6187a44abacfb8f167584668e02c918
1.8K 104_248_181_42__8000__i_sh+dc477d4810a8d3620d42a6c9f2e40b40
3.6M 104_248_181_42__8000__static__3020__ddgs_i686+3ebe43220041fe7da8be63d7c758e1a8
3.9M 104_248_181_42__8000__static__3020__ddgs_x86_64+d894bb2504943399f57657472e46c07d
1.9K 104_248_251_227__8000__i_sh+55ea97d94c6d74ceefea2ab9e1de4d9f
3.6M 104_248_251_227__8000__static__3020__ddgs_i686+3ebe43220041fe7da8be63d7c758e1a8
3.9M 104_248_251_227__8000__static__3020__ddgs_x86_64+d894bb2504943399f57657472e46c07d
1.1K 117_141_5_87__8000__i_sh+100d1048ee202ff6d5f3300e3e3c77cc
1.7K 117_141_5_87__8000__i_sh+5760d5571fb745e7d9361870bc44f7a3
8.8M 117_141_5_87__8000__static__3019__fmt_i686+8c2e1719192caa4025ed978b132988d6
 11M 117_141_5_87__8000__static__3019__fmt_x86_64+d6187a44abacfb8f167584668e02c918
3.6M 117_141_5_87__8000__static__3020__ddgs_i686+3ebe43220041fe7da8be63d7c758e1a8
3.9M 117_141_5_87__8000__static__3020__ddgs_x86_64+d894bb2504943399f57657472e46c07d
1.3K 119_9_106_27__8000__i_sh+09a3a0f662738279e344b2a38dc93ecb
1.2K 119_9_106_27__8000__i_sh+9dc32a4a87d2b579d03b6adb27e3f604
1.6K 119_9_106_27__8000__i_sh+b8a64e8bfe4a69c36760505cc757c38d
3.6M 119_9_106_27__8000__static__3020__ddgs_i686+3ebe43220041fe7da8be63d7c758e1a8
3.9M 119_9_106_27__8000__static__3020__ddgs_x86_64+d894bb2504943399f57657472e46c07d
9.4M 119_9_106_27__8000__static__3022__ddgs_i686+c32bd921a71d82696517c22021173480
 11M 119_9_106_27__8000__static__3022__ddgs_x86_64+79d762d1ff16142ea3bdae560558e718
1.7K 132_148_241_138__8000__i_sh+44feb3cd31b957e24b18f97c46b57431
1.1K 132_148_241_138__8000__i_sh+fcc003280d8e9060e00fb7273d8edee7
8.8M 132_148_241_138__8000__static__3019__fmt_i686+8c2e1719192caa4025ed978b132988d6
 11M 132_148_241_138__8000__static__3019__fmt_x86_64+d6187a44abacfb8f167584668e02c918
3.6M 132_148_241_138__8000__static__3020__ddgs_i686+3ebe43220041fe7da8be63d7c758e1a8
3.9M 132_148_241_138__8000__static__3020__ddgs_x86_64+d894bb2504943399f57657472e46c07d
➜
➜ md5sum sample/*
8801aff2ec7c44bed9750f0659e4c533  104_236_156_211__8000__i_sh+8801aff2ec7c44bed9750f0659e4c533
8c2e1719192caa4025ed978b132988d6  104_236_156_211__8000__static__3019__fmt_i686+8c2e1719192caa4025ed978b132988d6
d6187a44abacfb8f167584668e02c918  104_236_156_211__8000__static__3019__fmt_x86_64+d6187a44abacfb8f167584668e02c918
dc477d4810a8d3620d42a6c9f2e40b40  104_248_181_42__8000__i_sh+dc477d4810a8d3620d42a6c9f2e40b40
3ebe43220041fe7da8be63d7c758e1a8  104_248_181_42__8000__static__3020__ddgs_i686+3ebe43220041fe7da8be63d7c758e1a8
d894bb2504943399f57657472e46c07d  104_248_181_42__8000__static__3020__ddgs_x86_64+d894bb2504943399f57657472e46c07d
55ea97d94c6d74ceefea2ab9e1de4d9f  104_248_251_227__8000__i_sh+55ea97d94c6d74ceefea2ab9e1de4d9f
3ebe43220041fe7da8be63d7c758e1a8  104_248_251_227__8000__static__3020__ddgs_i686+3ebe43220041fe7da8be63d7c758e1a8
d894bb2504943399f57657472e46c07d  104_248_251_227__8000__static__3020__ddgs_x86_64+d894bb2504943399f57657472e46c07d
100d1048ee202ff6d5f3300e3e3c77cc  117_141_5_87__8000__i_sh+100d1048ee202ff6d5f3300e3e3c77cc
5760d5571fb745e7d9361870bc44f7a3  117_141_5_87__8000__i_sh+5760d5571fb745e7d9361870bc44f7a3
8c2e1719192caa4025ed978b132988d6  117_141_5_87__8000__static__3019__fmt_i686+8c2e1719192caa4025ed978b132988d6
d6187a44abacfb8f167584668e02c918  117_141_5_87__8000__static__3019__fmt_x86_64+d6187a44abacfb8f167584668e02c918
3ebe43220041fe7da8be63d7c758e1a8  117_141_5_87__8000__static__3020__ddgs_i686+3ebe43220041fe7da8be63d7c758e1a8
d894bb2504943399f57657472e46c07d  117_141_5_87__8000__static__3020__ddgs_x86_64+d894bb2504943399f57657472e46c07d
09a3a0f662738279e344b2a38dc93ecb  119_9_106_27__8000__i_sh+09a3a0f662738279e344b2a38dc93ecb
9dc32a4a87d2b579d03b6adb27e3f604  119_9_106_27__8000__i_sh+9dc32a4a87d2b579d03b6adb27e3f604
b8a64e8bfe4a69c36760505cc757c38d  119_9_106_27__8000__i_sh+b8a64e8bfe4a69c36760505cc757c38d
3ebe43220041fe7da8be63d7c758e1a8  119_9_106_27__8000__static__3020__ddgs_i686+3ebe43220041fe7da8be63d7c758e1a8
d894bb2504943399f57657472e46c07d  119_9_106_27__8000__static__3020__ddgs_x86_64+d894bb2504943399f57657472e46c07d
c32bd921a71d82696517c22021173480  119_9_106_27__8000__static__3022__ddgs_i686+c32bd921a71d82696517c22021173480
79d762d1ff16142ea3bdae560558e718  119_9_106_27__8000__static__3022__ddgs_x86_64+79d762d1ff16142ea3bdae560558e718
44feb3cd31b957e24b18f97c46b57431  132_148_241_138__8000__i_sh+44feb3cd31b957e24b18f97c46b57431
fcc003280d8e9060e00fb7273d8edee7  132_148_241_138__8000__i_sh+fcc003280d8e9060e00fb7273d8edee7
8c2e1719192caa4025ed978b132988d6  132_148_241_138__8000__static__3019__fmt_i686+8c2e1719192caa4025ed978b132988d6
d6187a44abacfb8f167584668e02c918  132_148_241_138__8000__static__3019__fmt_x86_64+d6187a44abacfb8f167584668e02c918
3ebe43220041fe7da8be63d7c758e1a8  132_148_241_138__8000__static__3020__ddgs_i686+3ebe43220041fe7da8be63d7c758e1a8
d894bb2504943399f57657472e46c07d  132_148_241_138__8000__static__3020__ddgs_x86_64+d894bb2504943399f57657472e46c07d

综合以上描述,本地文件目录及文件示例如下:

- ddg_tracker/
|-- cc_server.list
|-- log/
|   |-- 20190123164450.log
|   |-- 20190123180232.log
|   |-- 20190123211837.log
|   |-- 20190124000101.log
|   |-- 20190124060101.log
|   `-- ......
|-- sample/
|   |-- 104_236_156_211__8000__i_sh+8801aff2ec7c44bed9750f0659e4c533
|   |-- 104_236_156_211__8000__static__3019__fmt_i686+8c2e1719192caa4025ed978b132988d6
|   |-- 104_236_156_211__8000__static__3019__fmt_x86_64+d6187a44abacfb8f167584668e02c918
|   |-- 104_248_181_42__8000__i_sh+dc477d4810a8d3620d42a6c9f2e40b40
|   |-- 104_248_181_42__8000__static__3020__ddgs_i686+3ebe43220041fe7da8be63d7c758e1a8
|   |-- 104_248_181_42__8000__static__3020__ddgs_x86_64+d894bb2504943399f57657472e46c07d
|   |-- 104_248_251_227__8000__i_sh+55ea97d94c6d74ceefea2ab9e1de4d9f
|   |-- 104_248_251_227__8000__static__3020__ddgs_i686+3ebe43220041fe7da8be63d7c758e1a8
|   |-- 104_248_251_227__8000__static__3020__ddgs_x86_64+d894bb2504943399f57657472e46c07d
|   |-- 117_141_5_87__8000__i_sh+100d1048ee202ff6d5f3300e3e3c77cc
|   |-- 117_141_5_87__8000__i_sh+5760d5571fb745e7d9361870bc44f7a3
|   |-- 117_141_5_87__8000__static__3019__fmt_i686+8c2e1719192caa4025ed978b132988d6
|   `-- ......
`-- slave_conf/
    |-- 104_236_156_211__20190123165004.raw
    |-- 104_236_156_211__20190123185208.raw
    |-- 104_236_156_211__20190123223044.raw
    |-- 104_236_156_211__20190124012600.raw
    |-- 132_148_241_138__20190224191449.raw
    `-- ......

至此,我们就完成了 ddg 追踪程序的设计,为这个程序设置一个计划任务,定时运行一次即可。我个人的源码暂时不会放出来,有兴趣的朋友可以自己动手实现一下。目前的追踪成果(uniq peer ip):

一次探测到的活跃节点数:

部分 DDG 更新的 Slack 消息推送:

weinxin
版权声明
本站原创文章转载请注明文章出处及链接,谢谢合作!
评论:0   参与:  4