内容简介:上半部分传送门:前文说过,设计追踪程序的最终目标,有 4 个,其中涉及到 Peer 信息的获取和保存、样本与配置数据的解析和保存、记录最新启用的 C&C Server ……这样一来,就不可避免地将相关数据和文件保存到本地或数据库中。
本系列文章从 Botnet(僵尸网络)的基础概念说起,围绕实现了 P2P 特性的 DDG.Mining.Botnet,一步一步设计一个基于 P2P 的僵尸网络追踪程序,来追踪 DDG。DDG 是一个目前仍十分活跃的 Botnet,读懂本文,再加上一些辅助分析工作,就可以自行实现一套针对 DDG 的 P2P 僵尸网络跟踪程序。内容分 上、下 两部分:
- 上 半部分写本人理解的 Botnet 相关概念,然后介绍 DDG Botnet,着重介绍其涉及的 P2P 特性;
- 下 半部分写如何根据 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 网络。
这里先说一下追踪程序的概要执行流程,后面分步骤详细说明:
- 从数据库中读取 init_peers IP List ,并调用 memberlist.Join() 加入 ddg 的 P2P 网络;
- 成功加入 P2P 网络后,调用
memberlist.Members()
获取当前网络中的最新 Peers List; - 解析获取到的 Peers List 中的 Peers 信息,将每个 Peer 信息拆解成 IP:Port:Versioin:Hash:DateTime 5 元组,存到数据库中;
- 将每个 Peer IP ,拼接 URL 串
http://<peer_ip>:8000/slave
,并向该 URL 发送 Post 请求,以获取经过 msgPack 编码的配置数据; - 如果成功从某个 Peer 上获取到了配置数据,则:
http://<peer_ip>:8000/i.sh
- 如果成功获取到 i.sh 脚本,则解析其中的样本 Download URL,下载样本,同本地已下载到的其他样本 MD5 和下载 URL 作对比,MD5 和 下载 URL 其中之一是新的,就保留样本,否则删除刚下载到的样本。对于新样本,通过 Slack 的 Message 接口 Push 相关消息到自己的 Slack Channel 中;
- 最后,将非重复的最新活跃的 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 不同,则进行以下两步操作:
- 对比上述 i.sh 下载链接与刚获取到的最新配置数据中执行的 i.sh 下载链接是否相同,不同则对最新配置数据中指定的 i.sh 脚本也做下载&解析操作;
- 成功获取到 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 消息推送:
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 以P2P的方式追踪 DDG 僵尸网络(上)
- 什么是僵尸进程,如何找到并杀掉僵尸进程?
- BYOB:我的天!又一个僵尸网络开源了BYOB僵尸网络开源代码
- 僵尸扫描
- Linux 系统中僵尸进程
- 追踪分析LiquorBot僵尸网络
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
程式之美-微軟技術面試心得
編程之美小 / 悅知文化 / 2008.06.20 / 490元
書內容分為以下幾個部分: ▓ 遊戲之樂:從遊戲和其他有趣問題出發,化繁為簡,分析總結。 ▓ 數字之魅:程式設計的過程實際上就是和數字及字元打交道的過程。這一部分收集了一些這方面的有趣探討。 ▓ 結構之法:彙集了常見的對字串、鏈表、佇列,以及樹進行操作的題目。 ▓ 數學之趣:列舉了一些不需要寫具體程式的數學問題,鍛煉讀者的抽象思考能力。 ▓ 書中絕大部分題目都提供了詳細......一起来看看 《程式之美-微軟技術面試心得》 这本书的介绍吧!
RGB转16进制工具
RGB HEX 互转工具
HSV CMYK 转换工具
HSV CMYK互换工具