使用osquery监控auditd

栏目: 服务器 · 发布时间: 7年前

内容简介:本篇文章是在osquery最重要的功能之一就是能够记录进程的执行情况以及网络通信。在osquery中,这些特性被称为

本篇文章是 Auditing with osquery: Part One — Introduction to the Linux Audit FrameworkAuditing with osquery: Part Two — Configuration and Implementation 是的翻译合稿,这篇文章和我之前写过的 osquery源码解读之auditd设置 文章内容其实差不多,只不过所研究的东西稍有侧重。看到有其他人也在研究osquery,而且audit也是osquery中比较核心的功能,于是就翻译了。如果大家有兴趣也可以去阅读原文。

Auditing with osquery: Part One — Introduction to the Linux Audit Framework

info

osquery Across the Enterprise ,我们对osquery的整个架构进行了一个简要的介绍,但缺少了osquery的实现细节。我们将有两篇文章对osquery的audit的实现进行详细地说明。本篇文章主要介绍 Linux 中的audit的基本概念,第二篇文章 Auditing with osquery: Part Two — Configuration and Implementation 将会聚焦于osquery中的audit的实现。

osquery最重要的功能之一就是能够记录进程的执行情况以及网络通信。在osquery中,这些特性被称为 processsocket 审计。然而,我们发现有哪些使用这个特性的很多人对audit的实现机理以及性能损耗方面并不了解。如果对audit不了解,那么用户将会面临性能问题,资源冲突和各种故障。而且不正确的audit配置将会导致数据不完成,从而影响基于这些数据的检测和报警功能。

本篇文章将会说明audit是如何工作的,然后会给一个audit的配置方针。

Terminology

首先,我们先来定义一些在本篇文章中会用到的一些术语。当我们说 审计 ( auditing ),我们指的是那个处理来自内核中的审计系统产生的审计事件的用户态程序。总结一下,我们在本篇文章中需要用到的术语。

  • 审计框架/子系统 ( Audit Framework/Subsystem ),Linux内核中基于配置规则生成审计消息的审计组件
  • 审计过程 ( Auditing ),使用审计子系统产生审计事件日志的过程
  • 审计消费 ( Audit Consumer ),处理内核audit产生的审计事件的用户态审计程序
  • 审计程序 ( Auditd ),用于收集和存储(通常是文件)audit的事件日志的用户态程序

当说到 auditing ,osquery和auditd server实现的功能差不多。两者都是收集系统内核产生的审计事件的日志然后以某种方式存储或者是处理这些日志。

在我们讲解osquery之前,先来说明一下 audit subsystem 是如何工作的。

Understanding audit

自Linux内核2.6版本之后就引入了audit的功能,其主要目的是为了能够更好地记录系统中的各种安全事件。例如文件修改事件和系统调用事件。

这个 Auditing 主要是由一系列的规则(通常是保存在 /etc/audit 的目录下)来配置和管理的。规则主要是分为三类:

  • 控制规则 ( Control rules ),主要是设置audit系统的一些行为以及修改其默认设置。
  • 文件系统规则 ( File system rules ),主要是用于审计文件的,例如记录特殊文件或者目录的访问情况
  • 系统调用规则 ( System call rules ),记录一些特殊应用程序的系统调用行为。

这篇文章仅仅只介绍控制规则和系统调用规则。下方显示了 osquery 中开启了audit之后显示的规则情况:

# This file contains the auditctl rules that are loaded
# whenever the audit daemon is started via the initscripts.
# The rules are simply the parameters that would be passed
# to auditctl 
# First rule - delete all
-D
# Increase the buffers to survive stress events.
# Make this bigger for busy systems
-b 1024
# Feel free to add below this line. See auditctl man page
-a always,exit -S execve
-a always,exit -S bind
-a always,exit -S connect

前面两条是控制规则。 -D 表示删除了audit审计系统预先存在的规则; -b 1024 设置了 backlog 缓冲区的大小。接下来的三条规则是系统调用规则,表示audit将会记录这 execve / bind / connect 三种类型的系统调用。设置audit系统调用的规则的语法是:

auditctl -a action,filter -S system_call -F field=value -k key_name

系统调用表示的是用户态的程序如何和内核进行通信。上面看到的系统调用的规则是由osquery设置的。内核态的audit接受这些规则之后就会对响应的系统内核调用进行审计/记录。

为了记process事件,就需要记录 execve() 系统调用

  • execve() — Execute program

为了记录socket事件,就需要记录 bind()connect() 系统调用

  • bind() — Assigns an address to a socket
  • connect() — Initiates a connection on a socket

值得注意的是,上面列举的三条系统调用并不能覆盖所有的进程和网络活动。例如osquery无法记录到进程的fork和clone活动。因为osquery无法解析 fork()clone() 的系统调用所产生的日志。而且,正如中 #5084 所指出的,osquery不支持主动的网络连接以及非阻塞的socket行为。

为了观察一个程序到底使用了哪些系统调用,可以使用 strace

strace curl google.com 2>&1 | egrep 'execve|bind|connect'
execve("/bin/curl", ["curl", "google.com"], [/* 22 vars */]) = 0
connect(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("216.58.194.174")}, 16) = -1 EINPROGRESS (Operation now in progress)

Configuring audit parameters with auditctl

为了能够修改audit中的一些系统设置,我们可以利用 auditctl

root@localhost# auditctl -s
enabled 1
failure 0
pid 2021
rate_limit 0
backlog_limit 1024
lost 0
backlog 0

Auditctl 允许你配置

  • auditing是否开启
  • 系统失败应该如何告警( silently , verbosely , panic )
  • 速率限制( Rate limit ),一秒钟能够记录最多数量的事件。如果吵了此阈值,后面的数据将不会被记录同时flag将会被设置为失败
  • 积压限制( Backlog limit ),socket缓冲区能够保存最多数量的事件。如果超过了此阈值就会被舍弃同时flag将会被设置为失败
  • 丢失( Lost ),由于超过了 Backlog limit 而丢失了多少事件

Rate limitBacklog limit 是非常重要的,因为这两个阈值涉及到了audit如何处理他的审计日志。在很多时候,你无法保证在用户态能够全部收到audit产生的日志。因为当日志产生时,audit系统在缓冲区中保存的事件的数量会收到 Backlog limit 限制。事件保存在缓冲区中,然后 auditd/osquery 从缓冲区取数据。如果 auditd/osquery 不能及时地取走数据,那么新的时间就难以进入到缓冲区中从而被丢失,同样时间产生的速率也会受到 Rate limit 的限制。当超过了 Rate limitBacklog limit 的阈值,会将 failure 标志设置为1。

Basic audit architecture

或许通过 Syscall Auditing at Scale 中的图片能够帮助我们更好地理解audit的架构。

使用osquery监控auditd

在这张图片中, go-audit 的角色等同于 auditd 以及 osquery .当软件运行时,它会产生系统调用。当audit启动之后,内核中的audit程序(即图中中的 kauditd )就会判断这些系统调用是否在已经设置的审计规则当中。如果存在,那么这条事件就会被发送至 audit netlink socket .然后像 auditd / osquery / go-audit 就会监听并接受 audit netlink socket 中的事件.

osquery的文档中也说过不允许同时运行多个 audit consumer .

auditd should not be running when using osquery’s process auditing, as it will conflict with osqueryd over access to the audit netlink socket.

通过 index : kernel/git/torvalds/linux.git ,似乎在3.16内核以上会支持 multicast netlink sockets ,当时目前不知道如何使用 osquery 进行相关的配置。 multicast netlink sockets 能够支持同时运行多个 audit consumers 而不会出现文档中所说的冲突的问题。

Auditing with osquery

相比较其他的审计 工具 和安全工具,使用osquery有以下几个优点:

  1. 不需要内核模块
  2. 使用自定义查询过滤输出
  3. JSON格式化日志

一些安全工具声称通过安装额外的内核模块就能够记录网络和日志信息,但是这些内核模块的代码一般是安全工具私有的.虽然相比较与audit,内核模块能够提供性能,但是他们有时与不同Linux的系统产生兼容性的问题。

通过osquery,你可以通过一个泛查询( select * from process_events )查询所有的process/network事件,也可以通过 SQL 添加白名单或者黑名单( select * fromSELECT * FROM process_events WHERE path!="/usr/bin/sed" ).通过白名单或者黑名单的方式并不能降低性能开销,但却能够减少写入到osquery中的日志量。原因在于osquery已经完成解析了所有的audit的事件然后加载到虚拟表的工作。在文章的第二部分,我们将提供一个能够减少计算开销的过滤方法。

最后,我们通过比较原生的 auditd 产生的审计日志和使用 osquery 记录的系统调用的日志来观察他们之间的差异。以下是通过 curl google.com 两者产生的日志。

auditd

type=SYSCALL msg=audit(1521667806.157:217090): arch=c000003e syscall=59 success=yes exit=0 a0=fbb6c0 a1=fbb760 a2=fb55c0 a3=7ffe4ead6520 items=2 ppid=2000 pid=42971 auid=890466808 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=pts0 ses=26 comm="curl" exe="/usr/bin/curl" subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 key=(null)
type=EXECVE msg=audit(1521667806.157:217090): argc=2 a0="curl" a1="google.com"
type=CWD msg=audit(1521667806.157:217090):  cwd="/etc/audit/rules.d"
type=PATH msg=audit(1521667806.157:217090): item=0 name="/bin/curl" inode=306502 dev=fd:01 mode=0100755 ouid=0 ogid=0 rdev=00:00 obj=unconfined_u:object_r:bin_t:s0 objtype=NORMAL
type=PATH msg=audit(1521667806.157:217090): item=1 name="/lib64/ld-linux-x86-64.so.2" inode=8485228 dev=fd:01 mode=0100755 ouid=0 ogid=0 rdev=00:00 obj=unconfined_u:object_r:ld_so_t:s0 objtype=NORMAL
type=PROCTITLE msg=audit(1521667806.157:217090): proctitle=6375726C00676F6F676C652E636F6D
type=SYSCALL msg=audit(1521667806.163:217091): arch=c000003e syscall=42 success=no exit=-2 a0=3 a1=7f1c7f451610 a2=6e a3=7f1c7f451b20 items=1 ppid=2000 pid=42971 auid=890466808 uid=0 gid=0 euid=0 suid=0 fsuid=0 egid=0 sgid=0 fsgid=0 tty=pts0 ses=26 comm="curl" exe="/usr/bin/curl" subj=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 key=(null)
type=SOCKADDR msg=audit(1521667806.163:217091): saddr=01002F7661722F72756E2F6E7363642F736F636B657400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

osqueryi

使用osquery监控auditd

osqueryd

使用osquery监控auditd

根据我们的经验,相比较与audit产生的原始的audit日志,osquery产生的JSON格式化的日志显然更加清晰明了。通过修改SQL中的查询语句我们就能够轻易地增加或者删除列。

然而并没有事情是完美的,当我们使用osquery进行系统调用审计时,我们需要牢记:

  • shell的内置函数( echo , env , export 等)不会产生系统调用
  • 正如之前提到的,audit会丢日志。。当event table溢出时osquery同样也会丢日志
  • osquery无法实时记录事件。(你即使将查询事件设置为1秒或者更短也只能接近实时)
  • 开启audit就会存在性能损耗,使用osquery也不例外。性能损耗将会在第二章进行详细说明。

Conclusion

通过本篇文章,我们希望你对audit framework的工作原理有一个更加清晰的认识同时也了解哪些选项可以修改audit程序的行为。

第一章完

Auditing with osquery: Part Two — Configuration and Implementation

info

在上篇文章中,我们对 Linux Audit Framework 的基本概念进行了说明。本篇文章讲深入osquery中的audit实现细节。除此之外,我们还会提供一些能够减少性能损耗以及日志大小的一些配置策略。

Enabling auditing with osquery

命令行表示是定义osquery功能的核心。下方列出了几个和audit相关的配置。

  • –audit_allow_sockets=true ,开启osquery的记录网络连接功能( bind()connect() 系统调用)
  • –audit_allow_process_events=true ,开启osquery的记录进程执行功能( execve() 系统调用)
  • –audit_allow_config=true ,表示使用osquery自己定义的audit的规则。如果设置为false,就表示使用原系统中使用的audit的规则
  • –audit_persist=true , 表示当osquery与netlink socket失联之后里面进行重连
  • –disable_audit=false ,开启osquery的audit功能
  • –events_expiry=1 ,表示在查询一次之后,事件就失效
  • –events_max=500000 , 表示osuqery在两次 select 查询之间最多缓存500000条事件记录
  • –logger_plugin=filesystem ,表示osquery将查询结果保存到文件
  • –watchdog_memory_limit=350 ,表示osquery的内存限制为350M
    以上的这些配置一般都是保存在 /etc/osquery/osquery.flags .

除了上面这些配置外,你还需要在 osquery.conf 中写查询 socketprocess 记录的SQL语句(通过 process_eventssocket_events 表查询)。

Palantir已经开源了一份osquery的配置文件:

Logging considerations

Choose specific columns to log from each table

虽然通过通过通配符查询( SELECT * FROM process_events 或者 SELECT * FROM socket_events )能够查询到所有的事件,但是如果考虑到osquery是部署到10,1000甚至10000台主机上面,势必产生大量的日志。所以我们能够查询某些列的数据就能够使得process和socket时间的日志减少25%,那么整个osquery存储的日志量也会减少25%.所以在查询时,我们要有选择性地查询指定列的数据以减少数据量。

Filter out extraneous events using sql

想一想你是否需要记录系统中所有程序的系统调用行为。在系统中有些会产生大量的系统调用行为但是明显不是攻击行为的程序,比如 awk , sed , tr , cut .我们创建如下的一条日志不仅对性能不会产生影响同时还能够减少日志量, SELECT * FROM process_events WHERE path NOT IN ('/usr/bin/awk', '/usr/bin/sed', '/usr/bin/tr'); .

注意,正如在第一篇文章中所提到的,排除掉部分程序的系统调用并不会增加性能损耗。osquery仍然会处理所有的日志,只不过osquery不会记录那些被排除掉的日志.

Only record successful socket events

socket_events 表中的 success 字段就是用于表示此网络连接是成功还是失败。根据你自己的日志存储的要求,你首先可以确定在你所有的网络连接中成功和失败的比例,那么你再决定是否需要保留失败的网络连接。很显然,这是你的存储和你的可见性权衡。

Monitor top 10 processes by count

在Palantir,我们维护可一个dashboard用于记录产生日志量最多的进程。如果我们发现某个进程产生的日志量超过了其他的进程,那么我们将会考虑是否把这个进程去掉

Troubleshooting auditing

如果你在使用audit时发生了问题,你可以按照以下的列表进行排查。

  • 确保osquery是以root用户运行
  • osquery.flags 中配置正确(按照上文所说的配置进行设置)
  • osquery.conf 中有 socket_eventsprocess_events 的查询
  • osquery.confosquery.flags 在正确的目录中( /etc/osquery 目录)
  • osquery已经安装并且正在运行
  • 通过 auditclt -s 中查询到的 pidosqueryd 的pid相匹配,并且没有改变
  • 没有其他的 audit consumer ( auditd/go-audit/auditbeat )在运行
  • 通过 auditcl -l 中至少查询到一个系统调用的规则
  • 如果使用syslog,确保配置运行大消息同时取消速率限制

如果你检查了以上所有的选项之后还是存在问题,这里有两条主要的策略可供我们进一步排查

  1. 停掉一切正在运行的osquery进程,然后通过 --verbose 选项使 osqueryd 以前台进程运行,然后检查输出是否有错误。 $ osqueryctl stop; osqueryd --flagfile=/etc/osquery/osquery.flags --verbose
  2. 停掉一切正在运行的osquery进程, 然后通过 --audit_debug=true 运行 osqueryi .
    1. osqueryctl stop; osqueryi --flagfile=/etc/osquery/osquery.flags --audit_debug=true --verbose
    2. osqueryi会让你进入一个交互式shell,类似于 sqlite 提供的shell
    3. --audit_debug 将会输出osquery从 netlink socket 接受到的原始日志。如果没有审计日志出现,那么说明可能是配置错误。

Understanding performance implications

开启了osquery的审计功能之后,会在两个方面存在性能损耗:

  1. 当开启了内核中审计功能之后并且存在审计规则,那么内核每次都会比对审计规则和实际产生的审计事件。
  2. 这个 audit consumer (在本例中是 osquery )将会从内核 netlink socket 中接受数据,然后将数据解析为了其内部定义的表的格式( socket_eventsprocess_events )并保存在早RocksDB中。最后一旦这个数据被查询,那么这个数据就会被写入到文件或者是通过日志插件发送。

正如我们前面所说到的,osquery使用audit主要是监控1到3个特殊的系统调用。因此,每一次在内核中产生这样的系统调用,就会产生相应的审计事件。如果这样的系统调用越多,那么内核产生这些系统调用的审计事件的工作量就越大,同时osquery解析这些审计事件并且保存在数据库中的工作量也越大。虽然这样说很简要,但这就是audit和osquery的运行机理。

osquery stress test

官方的repo中包含了一系列使用 Python 写的压力测试的 脚本 。这个压力测试工具会开启一个 shell 然后向其发送UDP socket.通过将压力测试与perf工具相结合,你可以观察到运行audit和osquery所产生的开销。

Note: If you are using the perf tool on a VM, be sure to enable performance counters / code profiling on the VM first.

注意:如果你想在VM中使用perf工具,记得 启用虚拟CPU性能计数器

Audit & osquery disabled:

# perf stat python system_stress.py -i lo -n 10
Expecting 10240 (default shell) processes
Executed 10240 (default shell) processes
Elapsed: 41.857626915
 Performance counter stats for 'python system_stress.py -i lo -n 10':
      83219.491922      task-clock (msec)         #    1.986 CPUs utilized
<snip>
      41.897018727 seconds time elapsed

Audit & osquery enabled:

# perf stat python system_stress.py -i lo -n 10
Expecting 10240 (default shell) processes
Executed 10240 (default shell) processes
Elapsed: 47.5021560192
 Performance counter stats for 'python system_stress.py -i lo -n 10':
      84672.567230      task-clock (msec)         #    1.781 CPUs utilized
<snip>
      47.535512135 seconds time elapsed

正如上面显示的那样,当osquery开启了audit之后,cpu将会多花费6秒的时间。虽然这种计算性能开销的方式不一定100%准确,但是最终的结果确实说明了一些问题。

Measuring syscalls generated by process

为了知道那些进程产生了哪些系统调用,我们必须可以结合 auditdausearchaureport 三个工具。 Auditd 将事件日志记录到文件中。 ausearch 配合 aureport 对事件日志能够做一些解码的工作。解码之后就能够展示具体的事件日志详情了。

# Stop osquery so it doesn’t conflict with auditd
sudo osqueryctl stop
# Clear out any existing audit rules
sudo auditctl -D
# Insert audit rules used by osquery
sudo auditctl -a always,exit -S execve
sudo auditctl -a always,exit -S bind
sudo auditctl -a always,exit -S connect
# List audit rules
sudo auditctl -l
# Remove old audit logs 
sudo rm /var/log/audit/*.log
# Start auditd and let it run for 30 seconds
sudo service auditd start; sleep 30; sudo service auditd stop
# Delete audit rules
sudo auditctl -D
# Create a report using aureport for each syscall that we have a
# rule for
for SYSCALL in connect bind execve
  do 
  echo -e "\nReport for $SYSCALL syscall" 
  ausearch --syscall $SYSCALL --line-buffered --raw | aureport --executable --summary | head -15
done

在运行了 system_stress.py 之后,部分结果如下所示:

Report for connect syscall
Executable Summary Report
=================================
total  file
=================================
10240  /usr/bin/python2.7
532  /usr/sbin/aureport
14  /usr/sbin/ausearch

分析auditd日志的原因是在于在当前的系统上面会产生多少的系统调用并以此分析是否在某台机器上面会产生更多的系统调用。如果在这些高负载的机器上面开启audit审计功能必然会存在大量的系统调用。在这种情况下,我们就需要考虑是否需要开启audit功能,同时还要考虑过滤掉大量无用的进程所产生的系统调用。

Excluding processes using audit rules

在某些情况下,用户会发现开启了osquery中的audit功能之后osquery会占用大量的CPU的资源。如果这个问题得不到明显的改善,那么最后他们就会考虑不使用osquery的审计功能。诚然,audit在性能方面确实有一定的问题,但是我们还是提供一些选项用于减少audit的影响。

一个办法就是创建一些自定义的审计规则用于过滤掉那些不想监控的进程。以上面的压力测试的报告为例来进行说明。 /usr/bin/python2.7 产生了10240次 connect() 的系统调用。我们可以使用如下的审计规则过滤掉 python2.7 的进程。

-a never,exit -F exe=/path/to/bin -S [all|syscall_name]

上述的规则表示,对于程序 /path/to/bin ,系统不要产生任何的系统调用或者是某些系统调用。

Filtering noisy processes using audit rules

  1. 停掉osquery
  2. 编辑audit的审计规则文件(一般是位于 /etc/audit/rules.d/audit.rules )
  3. 按照上述的格式增加过滤规则
  4. 增加osquery需要过滤的审计规则
  5. 最终 audit.rules 的结果应该如下所示:

    # Delete any pre-existing rules
    -D
    # Don't generate audit events for python2.7
    -a never,exit -F exe=/usr/bin/python2.7 -S all
    # Increase the buffers to survive stress events.
    # Make this bigger for busy systems
    -b 1024
    # Add rules for osquery auditing
    -a always,exit -S execve
    -a always,exit -S bind
    -a always,exit -S connect
    # Enable audit
    -e 1
    
  6. osquery.flags 中将 --audit_allow_config 设置为 false .我们不希望使用osquery自带的规则,我们希望osquery使用我们自定义的规则

  7. 启动osquery
  8. 运行 augenrules --load 合并规则并把这些规则加载到内核中。你可能会看到要求你提供 arch 信息,你可以忽略他们
  9. auditctl -l 列出所有的规则,确保我们增加在过滤规则在里面

但是audit规则不允许在exe字段使用通配符过滤,这就意味着你们必须要单独地将每个进程加入到白名单中。当然除了在 path 字段上面加白名单之外,我们还可以在 PID / UID 或者是 SELinux 字段上面加入白名单。

由于osquery目前并不支持过滤的审计规则,于是我们提交一个issue, Linux Process Auditing: Allow users to specify excluded binaries/syscalls in osquery.conf

Conclusion

这两篇系列文章对audit框架进行了一个简要的介绍同时为了能够减少性能和日志的开销我们提供了audit规则的配置指导。展望未来, auditd 将会逐渐过时,类似于 eBPF 的技术将会取代 auditd 并会成为主流。但是目前,我们希望能够为 auditd 的配置提供一个最佳知道。

References

  1. https://slack.engineering/syscall-auditing-at-scale-e6a3ca8ac1b8
  2. http://blog.thinkst.com/2018/05/using-linux-audit-system-to-detect.html
  3. https://access.redhat.com/solutions/2482361
  4. https://people.redhat.com/sgrubb/audit/audit_ids_2011.pdf
  5. https://osquery.readthedocs.io/en/stable/deployment/process-auditing/
  6. https://www.digitalocean.com/community/tutorials/how-to-write-custom-system-audit-rules-on-centos-7
  7. https://wiki.archlinux.org/index.php/Audit_framework

总结

正如这个系列文章最后总结到的,这两篇文章主要是对audit框架进行了一个简单的说明同时为osquery中的audit提供了一个比较好的配置指导。相较于我写得两篇 osquery源码解读之分析socket_eventsosquery源码解读之auditd设置 ,这两篇文章更加偏向于理论配置指导而我的文章更加偏向于osquery中的技术实现。两者的侧重点不同,读者可以配合起来看。

拥有快速学习能⼒的⽩帽子,是不能有短板的。有的只是⼤量的标准板和⼏块长板

以上


以上所述就是小编给大家介绍的《使用osquery监控auditd》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Is Parallel Programming Hard, And, If So, What Can You Do About

Is Parallel Programming Hard, And, If So, What Can You Do About

Paul E. McKenney

The purpose of this book is to help you understand how to program shared-memory parallel machines without risking your sanity.1 By describing the algorithms and designs that have worked well in the pa......一起来看看 《Is Parallel Programming Hard, And, If So, What Can You Do About 》 这本书的介绍吧!

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具