Qemu Fuzzer 学习

本文章来自于开源软件供应链点亮计划的Openeuler社区项目
项目名称:No.112 qemu设备fuzz测试完善

前言

一提到 Qemu + Fuzz 的组合,我首先想起的是AFL的Qemu模式,或者是各种使用虚拟化技术对IoT设备进行Fuzzing的工具。

而本文学习的是Qemu自身的Fuzzing框架,即对Hypervisor的Fuzzing。它借助Qtest框架模拟Guest OS对设备的读写,并使用LibFuzzer的启发式算法提供数据驱动。该框架最早源于Google Summer of Code 2019项目,在Qemu5.0.0版本后被加入到master分支中。

Qemu 设备模拟原理

Qemu是一款开源的虚拟化和仿真工具,由Fabrice Bellard实现。Qemu支持两种模式的仿真:

  • system mode:对于CPU,内存以及外设的全系统仿真,提供KVM,Hyper-V等加速方式
  • user mode:通过指令翻译在一种架构的CPU上运行另一种CPU架构的二进制程序

首先需要理解虚拟化的本质在于:使用一个用户态的程序,在只使用用户态内存的情况下,来处理模拟设备对内存以及其他特殊硬件的访问。由于Qemu需要仿真不同架构,不同指令集的设备,所以其采用了OOP的编程思想,实现了Qemu Object Model来描述设备模型。

  • 设备模型:每个模拟出来的设备都对应一个TypeInfo对象,由设备名唯一标识,并存储在hash table中
  • 设备启动:启动设备需要经过设备注册,设备类型初始化,设备实例化等步骤,
  • 指令翻译:真正运行时,Qemu通过TCG(软件)或者KVM(硬件)等方式,接受模拟设备的指令,并翻译到物理设备上执行。这样会带来性能开销,相比起来硬件虚拟化带来的开销更低
  • 内存模拟:Qemu负责提供内存映射给客户机。当客户机访问这部分内存以便写磁盘时,Qemu会捕获访问,并且将请求传送给qemu的IDE控制器设备模型,模型会解析I/O请求并且通过宿主机的系统调用来模拟指令。最终将客户机的内存拷贝至宿主机的磁盘中。

总之,在Guest OS中,它认为自己可以直接和Host OS上的硬件设备打交道,Qemu充当了中间人的角色,可以用下图来概括:

+----------+ +----------+ +----------+ +----------+ +----------+
| UserSpace| | UserSpace| | UserSpace| | UserSpace| | UserSpace|
+----------+ +----------+ +----------+ +----------+ +----------+
|  Linux   | | Mac OS   | | Windows  | | Linux    | | Solaris  |
+----------+ +----------+ +----------+ +----------+ +----------+
|  Drivers | | Drivers  | | Drivers  | | Drivers  | | Drivers  |
+----------+ +----------+ +----------+ +----------+ +----------+
+----------+ +----------+ +----------+ +----------+ +----------+
| QEMU x86 | | QEMU x86 | | QEMU ARM | | QEMU PPC | | QEMU MIPS|
+----------+ +----------+ +----------+ +----------+ +----------+
+----------+-+----------+-+----------+-+----------+-+----------+
|               Host System:Linux,Mac OS,Windows               |
+--------------------------------------------------------------+
+--------------------------------------------------------------+
|         Hardware:CPU,memory,disk,networking,USB,etc          |
+--------------------------------------------------------------+

关于Qemu设备模拟的方法,可以参考User Documentation
关于Qemu设备模拟的理解,可以参考understanding Qemu devices
Qemu作者的论文:QEMU, a Fast and Portable Dynamic Translator

Qemu Fuzzer 的使用方法

文档说明

实验环境

采用了本机的WSL上的Docker环境

root@31b23c4c00b7:~/qemu# lscpu
Architecture:                    x86_64
CPU op-mode(s):                  32-bit, 64-bit
Byte Order:                      Little Endian
CPU(s):                          16
root@31b23c4c00b7:~/qemu# cat /etc/os-release
NAME="Ubuntu"
VERSION="20.04.2 LTS (Focal Fossa)"

环境问题

  • 描述:Qemu的Fuzzing还没有适配AArch64架构,虽然编译成功,但运行报错。

  • 解决方案:适配AArch64下的Qemu Fuzzing正是本项目要做的内容,但在学习阶段,还是采用x86环境来运行。

版本问题

  • 描述:官网给出的示例使用的是clang-8,而最新的clang版本已经来到了clang-14,如果采用较新的版本,则会在编译阶段报错(原因为开启了Werror)。

  • 解决方案:

    • (推荐)直接采用包管理器(apt/dnf)安装clang
    • git checkout到LLVM8-10之间的版本
    • 到官方Release页面下载LLVM8-10之间版本的源代码
# sudo apt/dnf search xxx 可以查看包管理器中包含的软件以及对应版本,默认为clang-10
sudo apt/dnf install clang llvm compiler-rt

Docker打包

为了方便部署,采用了docker的形式来配置实验环境:镜像地址。

简单使用

配置好环境后,通过如下命令编译运行Fuzzing程序,即显示LibFuzzer的输出。

CC=clang-10 CXX=clang++-10 ./configure --enable-sanitizers --enable-fuzzing
# qemu-fuzz-isa isa为模拟设备的架构
make qemu-fuzz-i386 qemu-fuzz-aarch64
# 查看可用的Fuzzing对象
build/qemu-fuzz-i386 --fuzz-target=FUZZ_NAME

除了上述命令外,Qemu Fuzzer还支持LibFuzzer的编译指令,可以通过-help=1来查看

对于Fuzzing的结果,可以考虑采用Clang Sanitizer查看。该工具需要使用CORPUS参数,并对编译指令进行一定的修改。在Fuzzing结束后,通过llvm-cov命令将生成的default.profraw文件转化为.html的可视化文件。

添加自定义Fuzzer

添加一个新的Fuzzer需要以下三个步骤:

  1. 编写Fuzzer源文件foo-device-fuzz.c,放在tests/qtest/fuzz目录下
  2. 参考已有的Fuzzer,使用libqos和libqtest中的API与模拟设备通信
  3. tests/qtest/fuzz/meson.build文件中注册该Fuzzer

通用Fuzzer

为某些特定的设备类型写Fuzzer是非常费时费力的事情,尤其是那些libqos并没有包含的设备驱动。Qemu提供了generic-fuzz的选项,用于对所有设备进行初步的Fuzzing,包括设备的PIO,MMIO和DMA。如果要启用generic-fuzz,至少需要设置以下两个环境变量:

  • QEMU_FUZZ_ARGS=:配置设备所需要的参数,比如网卡,用户名
  • QEMU_FUZZ_OBJECTS=:采用字符串匹配的方式来指定Fuzzing的内存区域。可以用./qemu-fuzz-i386 --fuzz-target=generic-fuzz -runs=0来检查匹配到的内存区域。如果匹配到的内存区域越多,Fuzzing的input-space就越大,越难发现导致该设备crash的输入,所以MemoryRegion的选取要适度。

集成OSS-Fuzz

OSS-Fuzz是集成式的Fuzzing工具,它默认对所有的对象进行Fuzz。由于通用Fuzzer的启动需要额外对环境变量进行设置,所以Qemu在tests/qtest/fuzz/generic_fuzz_configs.h文件中自定义了一些设备类型,用于OSS-Fuzz。开发者可以在该文件中添加新的设备类型,官方也提供了用于搭建环境的Dockerfile

crash复现工具

当复现crash的时候,需要不包含Fuzzer的Qemu,这样可以过滤调误报,并增强调试功能。可以使用OSS-Fuzz的脚本来创建一个one-line reproducer

Fuzzer生命周期

Qemu Fuzzer为LibFuzzer提供了两处入口点,在LibFuzzer自己的main函数之后调用:

  • LLVMFuzzerInitialize:Fuzzing开始前调用,初始化环境
  • LLVMFuzzerTestOneInput:每个Fuzzing任务运行时调用,提供输入并在Fuzzing结束后重置状态

因为Fuzzer的进程会在每次Fuzzing运行后被重置(reset),所以Qemu的状态也需要重置,有两种方法来实现重置,各有优劣。

  • Reboot:在每次运行之间重启Guest OS
  • Fork:在子进程中运行test case,和AFL的fork-server模式很相似

Qemu Fuzzer的依赖库

Qemu Fuzzer实现主要依赖的是libqtest和libqos两个库,它们的关系可以参考Testing QEMU emulated devices using qtest,这篇文章介绍了qtest的基本原理,提供的API函数,并给出了添加testcase的方法。

libqtest

  • 代码路径
  • 文档说明

Qtest是一个用于对Qemu模拟出的硬件设备做单元测试的框架,它由Qtest ClientQtest Server两部分组成。两者之间通过UNIX Socket通信,支持PIO,MMIO,中断,QMP等指令。

  • Qtest Client:它是为某个设备编写的驱动程序,按照自底向上的封装顺序,它依赖于glib单元测试框架,libqtest,libqos和qgraph。如果需要添加新的测试程序,需要如下四个步骤:

    1. 在Qtest目录下编写新的测试代码tests/qtest/foo-test.c
    2. 在Makefile.include中添加编译指令
    3. 编译:make tests/qtest/foo-test.c
    4. 运行:
    QTEST_LOG=1 QTEST_QEMU_BINARY=i386-softmmu/qemu-system-i386 tests/qtest/foo-test`
    
  • Qtest Server:它和TCG,KVM的功能类似,是一种加速器(accelerator),通过编译指令-machine accel=qtest注册。在普通的使用场景中,VCPU直接和虚拟硬件交互;而测试场景中,Qtest直接和虚拟硬件交互,充当了Qtest Client和虚拟硬件的中间角色。Qtest用于验证设备的行为是否正确,实际上并没有启动Guest OS。

运行时,libqtest.c将启动Qemu为子进程。Qemu的启动主函数在vl.c中,在测试场景中,调用qtest.cqtest_init()函数,初始化Qtest Server。

两者的关系可以用下图表示:

+----------------+  socket  +----------------+----------------------+
|  Qtest Client  +---------->  Qtest Server  |                      |
+----------------+          +-------+--------+                      |
+----------------+                  |        |         Qemu         |
|  Qgraph        |               PIO|MMIO    |                      |
+----------------+                  |        |                      |
+----------------+          +-------v--------+----------------------+
|  libqos        |          |          Hardware Emulation           |
+----------------+          +---------------------------------------+
+----------------+
|  libqtest      |
+----------------+
+----------------+
|  glib test     |
+----------------+

libqos

  • 代码路径
  • 文档说明

libqos是用于编写qtest case的设备驱动框架,提供了关于memoryPCIvirtio的API函数。它的功能主要有以下两个:

  • 相当于总线的wrapper,为每类总线都实现了特定的函数
  • 统一了设备访问的模型,简化了开发者的工作

Qemu Fuzzer 的整体流程

  • 代码路径
  • 文档说明

以上的两个依赖库都是为Qemu的功能性测试设计,依赖于测试输入的选取。而Fuzzing正是通过对输入增加随机性和变异算法来提高测试输入的质量。从而可以使用LibFuzzer对Qtest的输入进行HOOK,提高Qtest的测试效率。综上所述,Qemu Fuzzer的整体结构如图所示。

从源代码的角度来看:

  • fuzz.h:定义了实现一个Fuzz Target用到的接口和数据结构,以及与LibFuzzer之间的交互
  • fork_fuzz.h:定义了并发Fuzzer之间的共享内存
  • qos_fuzz.h:定义了libqos在qtest上的进一步封装的接口
  • generic_fuzz_configs.h:定义了通用的Fuzzer设置
  • virtio_xxx_fuzz.h:实现了virtio设备(net,blk,scsi)的Fuzz Target

Qemu Fuzzer 的用例分析

Qemu的Developer documentation中写道:

QEMU does not have a high level design description document - only the source code tells the full story.

所以作为Qemu的开发者,阅读源码非常关键。本节以virtio-net设备为例,分析Qemu Fuzzing的流程,代码路径为tests/qtest/fuzz/virto_net_fuzz.c。virtio是一种半虚拟化的技术,需要Host OS提供对设备的emulation,Guest OS负责对设备的驱动。工作模式如下图所示:

实现方面,以自顶向下的逻辑进行说明:

  • 首先,实现注册函数register_virtio_net_fuzz_targets()。使用libqos提供的fuzz_add_qos_target()函数添加了三个Fuzzing对象。该函数是libqos对于fuzz_add_target()函数的wrapper,其原型如下:
void fuzz_add_qos_target(FuzzTarget *fuzz_opts,const char *interface,QOSGraphTestOptions *opts);
  • 最后,修改meson.build文件,添加条件编译选项。

接下来,以virtio-net-socket为主,对Fuzzing对象的参数以及参数中涉及到的函数进行分析

  • virtio-net-socket
fuzz_add_qos_target(&(FuzzTarget){.name = "virtio-net-socket",.description = "Fuzz the virtio-net virtual queues. Fuzz incoming ""traffic using the socket backend",.pre_fuzz = &virtio_net_pre_fuzz,.fuzz = virtio_net_fork_fuzz,},"virtio-net",&(QOSGraphTestOptions){.before = virtio_net_test_setup_socket});

第一个参数为指向FuzzTarget的临时对象的指针。该对象由fuzz.h定义,含标识信息和许多与Fuzzing特性相关的回调函数。介绍如下:

+-------------+-----------------------------------------------------------+-------------------------------------------------------------------------+
| Field       | Declaration                                               | Description                                                             |
+-------------+-----------------------------------------------------------+-------------------------------------------------------------------------+
| name        | const char *name                                          | target identifier (passed to --fuzz-target=)                            |
+-------------+-----------------------------------------------------------+-------------------------------------------------------------------------+
| description | const char *description                                   | help text                                                               |
+-------------+-----------------------------------------------------------+-------------------------------------------------------------------------+
| pre_fuzz    | void(*pre_fuzz)(QTestState *)                             | will run once, after QEMU has been initialized, prior to the fuzz-loop. |
|             |                                                           | eg: detect the memory map                                               |
|             |                                                           | Can be NULL                                                             |
+-------------+-----------------------------------------------------------+-------------------------------------------------------------------------+
| fuzz        | void(*fuzz)(QTestState *, const unsigned char *, size_t); | accepts and executes an input from libfuzzer.                           |
|             |                                                           | this is repeatedly executed during the fuzzing loop.                    |
|             |                                                           | Its should handle setup, input execution and cleanup.                   |
|             |                                                           | Cannot be NULL                                                          |
+-------------+-----------------------------------------------------------+-------------------------------------------------------------------------+

其中,virtio_net_pre_fuzz初始化了qos path和fork fuzz用到的共享内存。virtio_net_fork_fuzz首先fork出子进程,并调用virtio_net_fuzz_multi函数。该函数用自定义的vq_action来管理随机数据,根据数据包到来情况,将其加入virtioqueue中,并kick out出去。随后运行主循环。

第二个参数表示使用的设备接口名称,这里统一为virtio-net。

第三个参数为qgraph.h提供的关于测试选项的结构体。其中before参数接受原型为QOSBeforeTest的函数。virtio_net_test_setup_socket函数指定了Qemu网络设备的后端为socket。它负责和虚拟设备通信,并将虚拟设备的数据包发送到宿主机的网络设备中。

  • virtio-net-socket-check-use
fuzz_add_qos_target(&(FuzzTarget){.name = "virtio-net-socket-check-used",.description = "Fuzz the virtio-net virtual queues. Wait for the ""descriptors to be used. Timeout may indicate improperly handled ""input",.pre_fuzz = &virtio_net_pre_fuzz,.fuzz = virtio_net_fork_fuzz_check_used,},"virtio-net",&(QOSGraphTestOptions){.before = virtio_net_test_setup_socket});

virtio_net_fork_fuzz_check_usedvirtio_net_fork_fuzz非常相似,不同之处在于调用virtio_net_fuzz_multi函数的时候使用了true

  • cirtio-net-slirp
fuzz_add_qos_target(&(FuzzTarget){.name = "virtio-net-slirp",.description = "Fuzz the virtio-net virtual queues with the slirp "" backend. Warning: May result in network traffic emitted from the "" process. Run in an isolated network environment.",.pre_fuzz = &virtio_net_pre_fuzz,.fuzz = virtio_net_fork_fuzz,},"virtio-net",&(QOSGraphTestOptions){.before = virtio_net_test_setup_user});

virtio_net_test_setup_user指定了网络后端的类型为user

Qemu Fuzzer 案例 CVE-2017-12809

最后,简要分析一个由LibFuzzer发现的Qemu上的CVE漏洞。

  • 漏洞描述:该漏洞属于拒绝服务类。当通过IDE硬盘和CD/DVD-ROM模拟器来构建Guest OS时,Guest OS中的特权用户可以通过清除一个空的CDROM设备驱动,造成空指针解引用,进而导致Qemu进程崩溃。
  • 漏洞代码: 在hw/ide/core.c文件中调用blk_aio_flush()函数前没有检查s->blk是否为空
---hw/ide/core.c | 10 +++++++++-1 file changed, 9 insertions(+), 1 deletion(-)diff --git a/hw/ide/core.c b/hw/ide/core.c
index 0b48b64d3a..bea39536b0 100644
--- a/hw/ide/core.c
+++ b/hw/ide/core.c
@@ -1063,7 +1063,15 @@ static void ide_flush_cache(IDEState *s)s->status |= BUSY_STAT;ide_set_retry(s);block_acct_start(blk_get_stats(s->blk), &s->acct, 0, BLOCK_ACCT_FLUSH);
-    s->pio_aiocb = blk_aio_flush(s->blk, ide_flush_cb, s);
+
+    if (blk_bs(s->blk)) {
+        s->pio_aiocb = blk_aio_flush(s->blk, ide_flush_cb, s);
+    } else {
+        /* XXX blk_aio_flush() crashes when blk_bs(blk) is NULL, remove this
+         * temporary workaround when blk_aio_*() functions handle NULL blk_bs.
+         */
+        ide_flush_cb(s, 0);
+    }}static void ide_cfata_metadata_inquiry(IDEState *s)
--
  • 漏洞修复:除了在漏洞发生部分增加条件判断以外,还在tests/ide-test.c中增加了测试代码
---tests/ide-test.c | 19 +++++++++++++++++++1 file changed, 19 insertions(+)diff --git a/tests/ide-test.c b/tests/ide-test.c
index bfd79ddbdc..aa9de065fc 100644
--- a/tests/ide-test.c
+++ b/tests/ide-test.c
@@ -689,6 +689,24 @@ static void test_flush_nodev(void)ide_test_quit();}+static void test_flush_empty_drive(void)
+{
+    QPCIDevice *dev;
+    QPCIBar bmdma_bar, ide_bar;
+
+    ide_test_start("-device ide-cd,bus=ide.0");
+    dev = get_pci_device(&bmdma_bar, &ide_bar);
+
+    /* FLUSH CACHE command on device 0 */
+    qpci_io_writeb(dev, ide_bar, reg_device, 0);
+    qpci_io_writeb(dev, ide_bar, reg_command, CMD_FLUSH_CACHE);
+
+    /* Just testing that qemu doesn't crash... */
+
+    free_pci_device(dev);
+    ide_test_quit();
+}
+static void test_pci_retry_flush(void){test_retry_flush("pc");
@@ -954,6 +972,7 @@ int main(int argc, char **argv)qtest_add_func("/ide/flush", test_flush);qtest_acd qemugit checkout stable-2.10dd_func("/ide/flush/nodev", test_flush_nodev);
+    qtest_add_func("/ide/flush/empty_drive", test_flush_empty_drive);qtest_add_func("/ide/flush/retry_pci", test_pci_retry_flush);qtest_add_func("/ide/flush/retry_isa", test_isa_retry_flush);--
  • 漏洞复现:找到patch前的commit版本,加上新增test中的测试用例,运行qtest程序:
# 不能直接wget对应的release版本,因为受影响的版本都被修复了
git clone https://gitlab.com/qemu-project/qemu.git;cd qemu
git checkout stable-2.10
# 从mailing list中获取patch的作者,找对应的commit
git log --author=Hajnoczi
git reset --hard=4da97120d51a4383aa96d741a2b837f8c4bbcd0b
# 构建
mkdir build;cd build;../configure ----disable-werror
make qtest

其实Qemu虚拟机逃逸漏洞已经常见于CTF比赛和CVE中了,比如知名的CVE-2020-14364

有错误之处请批评指正,作者联系方式:cascades-sjtu

Qemu Fuzzer学习相关推荐

  1. QEMU零知识学习5 —— QEMU安装

    接前一篇文章<QEMU零知识学习4 -- QEMU编译>,链接如下: QEMU零知识学习4 -- QEMU编译_蓝天居士的博客-CSDN博客 安装QEMU QEMU编译完成之后,运行&qu ...

  2. QEMU零知识学习3 —— QEMU配置

    本文接前一篇文章<QEMU零知识学习2 -- QEMU源码下载>,链接如下: QEMU零知识学习2 -- QEMU源码下载_蓝天居士的博客-CSDN博客 基于第2种方式通过wget下载源码 ...

  3. 随想录(qemu的学习)

    [ 声明:版权所有,欢迎转载,请勿用于商业用途. 联系信箱:feixiaoxing @163.com] 这两天看了一下qemu,还是有不少收获的.对于没有开发板.或者是觉得使用开发板学习cpu不太方便 ...

  4. QEMU+KVM学习笔记

    1 QEMU和KVM的关系:      现在所说的虚拟化,一般都是指在CPU硬件支持基础之上的虚拟化技术.KVM也同hyper-V.Xen一样依赖此项技术.没有CPU硬件虚拟化的支持,KVM是无法工作 ...

  5. QEMU零知识学习2 —— QEMU源码下载

    QEMU源代码有多种方式进行下载.本文只介绍其中2种方法. 1. 通过git进行下载开发中的最新的qemu.git (1)获取源码 $ git clone http://git.qemu.org/qe ...

  6. 基于Qemu在ubuntu上构建linux学习环境

    本文的主要内容: Qemu及相关背景简单介绍 如何安装Qemu 如何获取体系结构相关的datasheet 如何使用Qemu启动kernel Qemu及相关背景简单介绍 Qemu是ubuntu下的虚拟机 ...

  7. 随想录(如何学习内核)

    [ 声明:版权所有,欢迎转载,请勿用于商业用途. 联系信箱:feixiaoxing @163.com] 很多学习linux kernel的同学都会陷入到一个误区当中.一方面,他们希望自己学习的东西大而 ...

  8. linux内存管理笔记(八)---内核临时页表的创建

    前面几节,我们已经看到了x86的分段和分页硬件单元把逻辑地址转换为线性地址,再由线性地址转换到物理地址的基本原理,那么这几章我们来主要是内核是怎么实现页表的创建,本章基于imx6ull和qemu来学习 ...

  9. 全志V3S嵌入式驱动开发(开发软件搭建)

    [ 声明:版权所有,欢迎转载,请勿用于商业用途. 联系信箱:feixiaoxing @163.com] 以前我也有个观点,认为学习嵌入式只要用qemu仿真平台来学学就可以了.后来发现了,其实不是那么一 ...

最新文章

  1. Linux下Java开发环境的搭建Tomcat6+jdk6+eclipse3.5.2+Myeclipse9.0+mysql5.1.47
  2. 遍历二叉树的神级方法(Morris)
  3. Vue.js 渲染函数 JSX
  4. day05 数据类型
  5. Python机器学习:KNN算法03训练数据集,测试数据集train test split
  6. Linux linux下的进程状态
  7. Windows 下使用 MinGW 和 CMake 进行开发
  8. 洛谷P2862 [USACO06JAN]把牛Corral the Cows
  9. mysql新手常见问题_MySQL数据库入门-新手常见问题答疑
  10. 美司法部揭秘俄黑客窃取雅虎5亿帐户资料全过程
  11. 【回文串9】LeetCode 409. Longest Palindrome
  12. 关于雅克比矩阵与黑塞矩阵
  13. 【总结整理】openlayer加载搜狗地图,qq地图,mapabc
  14. 【R】 Error in is.data.frame(x) : (list) object cannot be coerced to type 'double'
  15. 通过爬虫获取免费IP代理,搭建自己的IP池(http)
  16. Ol3中Map事件全解析
  17. Leetcode 电话号码的字母组合
  18. matlab 去高光,图像处理-去高光/直方图均衡
  19. 设计模式之状态机模式
  20. 【C++】栈的应用:逆波兰式的实现

热门文章

  1. 我的python学习(五)文件操作、异常捕获
  2. 1条命令解决使用kubeadm安装 kubernetes 从 k8s.gcr.io 拉取镜像失败的问题
  3. 自控原理入门详解**********
  4. 长盛兴业入股宝沃背后:为神州优车相关方 王百因是陆正耀同学
  5. 如何成为一名云计算工程师
  6. VPLS和MPLS 的区别
  7. Azure NSG网络安全组日志分析
  8. 单细胞论文记录(part19)--A comprehensive comparison on cell-type composition inference for ST data
  9. 谷歌浏览器配置微信浏览器_在 Chrome (谷歌浏览器) 中模拟微信内置浏览器
  10. JAVA数组元素比较