技術(shù)分享 | 專項(xiàng)測(cè)試技術(shù)初識(shí)Hook
本文節(jié)選自霍格沃茲測(cè)試學(xué)院內(nèi)部教材
Hook 技術(shù)需要預(yù)先分析目標(biāo)應(yīng)用的源代碼和邏輯,根據(jù)目標(biāo)測(cè)試場(chǎng)景設(shè)置目標(biāo)、邏輯和數(shù)據(jù),然后運(yùn)行時(shí)動(dòng)態(tài)的對(duì)目標(biāo)函數(shù)參數(shù)值、邏輯或者返回值做修改,達(dá)到修改現(xiàn)有函數(shù)邏輯、實(shí)現(xiàn)目標(biāo)測(cè)試場(chǎng)景的目的。
Hook的價(jià)值
在測(cè)試中,雖然通過(guò)修改數(shù)據(jù)以實(shí)現(xiàn)測(cè)試場(chǎng)景的需求,大部分情況下都可以通過(guò) Mock 技術(shù)實(shí)現(xiàn),但是還有一小部分場(chǎng)景,例如需要修改應(yīng)用內(nèi)部函數(shù)的參數(shù)、返回值或運(yùn)行邏輯等情況,這時(shí)就需要用到 Hook 技術(shù)。
單元測(cè)試之外,Mock 技術(shù)的主要作用是對(duì)服務(wù)、接口進(jìn)行 Mock,通過(guò)代理等方式將被測(cè)服務(wù)發(fā)送到依賴服務(wù)的請(qǐng)求轉(zhuǎn)發(fā)給 Mock 服務(wù),再由 Mock 服務(wù)根據(jù)規(guī)則組裝預(yù)期的返回?cái)?shù)據(jù)響應(yīng)給被測(cè)服務(wù),達(dá)到預(yù)期的測(cè)試場(chǎng)景。
Hook 技術(shù)主要用于服務(wù)內(nèi)部代碼邏輯上的修改,當(dāng)函數(shù)間傳遞的參數(shù)或者函數(shù)內(nèi)的邏輯需要進(jìn)行修改時(shí),數(shù)據(jù)的傳遞并沒(méi)有經(jīng)過(guò)網(wǎng)絡(luò),Mock 服務(wù)無(wú)法對(duì)其進(jìn)行操作,只能通過(guò) Hook 技術(shù)通過(guò)在運(yùn)行的代碼中插入額外的代碼或者在內(nèi)存中進(jìn)行操作。這種更精細(xì)更底層的修改,相比 Mock 技術(shù)能實(shí)現(xiàn)更多的修改范圍,適用性更廣,難度也更大。

JVM Sandbox簡(jiǎn)介
JVM-Sandbox 是 alibaba 開(kāi)源的一個(gè) JVM 沙箱容器,只能處理目標(biāo)為 Java 應(yīng)用的場(chǎng)景,主要的特點(diǎn)是支持熱插拔(可以在目標(biāo)應(yīng)用運(yùn)行中隨時(shí)進(jìn)行 Hook 的加載和解除)、可以同時(shí)操作掛載多個(gè)目標(biāo)應(yīng)用,相互之間獨(dú)立設(shè)置互不干擾、支持的目標(biāo)應(yīng)用 JDK 版本較廣(6-11)。工具本身功能很多,在這里僅介紹和使用它用作 Hook 的部分功能。
JVM Sandbox安裝與啟動(dòng)
下載
項(xiàng)目的 github 地址:https://github.com/alibaba/jvm-sandbox。
下載所需版本的二進(jìn)制壓縮包,解壓(演示所使用的版本為 1.3.3)。
環(huán)境準(zhǔn)備
官方聲明支持的系統(tǒng)有:Linux/UNIX/MacOS,這幾個(gè)系統(tǒng)只需要下載解壓縮就可以直接運(yùn)行。
官方并未支持 Windows 系統(tǒng),所以需要進(jìn)行如下修改:
安裝 Git Bash。
安裝 JDK(版本 6-11,演示所用版本為 1.8.0_192),路徑中不能帶有空格。
在 Shell 腳本中會(huì)有 Java 命令的調(diào)用,所以電腦中需要,并且因?yàn)?Git Bash 運(yùn)行 Shell 腳本時(shí)的目錄問(wèn)題。
修改啟動(dòng)腳本
bin/sandbox.sh
?,將腳本中 183-188 行內(nèi)容注釋。

啟動(dòng)腳本
由于啟動(dòng)腳本中使用了相對(duì)路徑,所以運(yùn)行時(shí)需要切換到項(xiàng)目的 bin 目錄下操作。
在 bin 目錄中執(zhí)行語(yǔ)句
./sandbox.sh -p 目標(biāo)應(yīng)用pid
?,當(dāng)出現(xiàn)如下提示信息,說(shuō)明 JVM-Sandbox 已經(jīng)成功啟動(dòng)了。
$ ./sandbox.sh -p 6204
? ? ? ? ? ? ? ? ? ?NAMESPACE : default
? ? ? ? ? ? ? ? ? ? ?VERSION : 1.3.3
? ? ? ? ? ? ? ? ? ? ? ? MODE : ATTACH
? ? ? ? ? ? ? ? ?SERVER_ADDR : 0.0.0.0
? ? ? ? ? ? ? ? ?SERVER_PORT : 4543
? ? ? ? ? ? ? UNSAFE_SUPPORT : ENABLE
? ? ? ? ? ? ? ? SANDBOX_HOME : e:/Download/sandbox/bin/..
? ? ? ? ? ?SYSTEM_MODULE_LIB : e:/Download/sandbox/bin/..\module
? ? ? ? ? ? ?USER_MODULE_LIB : E:\Download\sandbox\sandbox-module;~/.sandbox-module;
? ? ? ? ?SYSTEM_PROVIDER_LIB : e:/Download/sandbox/bin/..\provider
? ? ? ? ? EVENT_POOL_SUPPORT : DISABLE
JVM-Sandbox 同時(shí)還會(huì)對(duì)外提供接口,可以通過(guò)請(qǐng)求直接操作 JVM-Sandbox,這樣就能方便的與自己的測(cè)試代碼結(jié)合使用。
JVM Sandbox示例
目標(biāo)應(yīng)用為一段簡(jiǎn)單的 Java 代碼,代碼中啟動(dòng)了一個(gè)死循環(huán),每次循環(huán)會(huì)打印
report
?方法接收到的參數(shù)值,參數(shù)值已經(jīng)在代碼中固定傳入,所以運(yùn)行之后的結(jié)果是一串相同的輸出內(nèi)容。具體內(nèi)容如下:
public class HookTarget {
? ?final void report(String stringParam, boolean boolParam, int intParam) {
? ? ? ?System.out.println("stringParam is " + stringParam);
? ? ? ?if (boolParam) {
? ? ? ? ? ?System.out.println("boolParam is true!");
? ? ? ?} else {
? ? ? ? ? ?System.out.println("boolParam is false");
? ? ? ?}
? ? ? ?System.out.println("intParam is " + intParam);
? ?}
? ?final void loopReport() throws InterruptedException {
? ? ? ?while (true) {
? ? ? ? ? ?report("a", false, 666);
? ? ? ? ? ?Thread.sleep(1000);
? ? ? ? ? ?System.out.println();
? ? ? ?}
? ?}
? ?public static void main(String... args) throws InterruptedException {
? ? ? ?new HookTarget().loopReport();
? ?}
}
要編寫(xiě)符合 JVM-Sandbox 的 hook 腳本,需要引入
sandbox-api和sandbox-debug-module
?兩個(gè)依賴。通過(guò)實(shí)現(xiàn) jvm.sandbox 中的 Module 接口,在 AdviceListener 方法中重寫(xiě) before 方法,這樣寫(xiě)入的語(yǔ)句就會(huì)在目標(biāo)方法體執(zhí)行之前進(jìn)行執(zhí)行,能夠修改目標(biāo)方法收到的參數(shù)數(shù)據(jù)。通過(guò)
advice.changeParameter
?方法,修改對(duì)應(yīng)位置的參數(shù)數(shù)值,第一個(gè)參數(shù)為目標(biāo)參數(shù)的位置,從 0 開(kāi)始,第二個(gè)參數(shù)為替換的值。具體代碼如下:
import com.alibaba.jvm.sandbox.api.Information;
import com.alibaba.jvm.sandbox.api.Module;
import com.alibaba.jvm.sandbox.api.annotation.Command;
import com.alibaba.jvm.sandbox.api.listener.ext.Advice;
import com.alibaba.jvm.sandbox.api.listener.ext.AdviceListener;
import com.alibaba.jvm.sandbox.api.listener.ext.EventWatchBuilder;
import com.alibaba.jvm.sandbox.api.resource.ModuleEventWatcher;
import org.kohsuke.MetaInfServices;
import javax.annotation.Resource;
import java.util.*;
@MetaInfServices(Module.class)
@Information(id = "ceshiren.com", author = "ceshiren.com")
public class hook_jvm implements Module {
? ?@Resource
? ?private ModuleEventWatcher moduleEventWatcher;
? ?@Command("ceshiren")
? ?public void ceshiren(final Map<String, String> param) {
? ? ? ?new EventWatchBuilder(moduleEventWatcher)
? ? ? ? ? ? ? ?.onClass("HookTarget")
? ? ? ? ? ? ? ?.onBehavior("report")
? ? ? ? ? ? ? ?.onWatch(new AdviceListener() {
? ? ? ? ? ? ? ? ? ?@Override
? ? ? ? ? ? ? ? ? ?protected void before(Advice advice) throws Throwable {
? ? ? ? ? ? ? ? ? ? ? ?advice.changeParameter(0, "Change By Hook!");
? ? ? ? ? ? ? ? ? ? ? ?advice.changeParameter(1, false);
? ? ? ? ? ? ? ? ? ? ? ?advice.changeParameter(2, 965);
? ? ? ? ? ? ? ? ? ?}
? ? ? ? ? ? ? ?});
? ?}
}
項(xiàng)目通過(guò) maven 管理依賴,對(duì)應(yīng)的
pom.xml
?文件內(nèi)容如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
? ? ? ? xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
? ? ? ? xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
? ?<modelVersion>4.0.0</modelVersion>
? ?<groupId>org.example</groupId>
? ?<artifactId>ceshiren_book</artifactId>
? ?<version>1.0-SNAPSHOT</version>
? ?<dependencies>
? ? ? ?<dependency>
? ? ? ? ? ?<groupId>com.alibaba.jvm.sandbox</groupId>
? ? ? ? ? ?<artifactId>sandbox-api</artifactId>
? ? ? ? ? ?<version>1.3.3</version>
? ? ? ?</dependency>
? ? ? ?<dependency>
? ? ? ? ? ?<groupId>com.alibaba.jvm.sandbox</groupId>
? ? ? ? ? ?<artifactId>sandbox-debug-module</artifactId>
? ? ? ? ? ?<version>1.3.3</version>
? ? ? ? ? ?<scope>compile</scope>
? ? ? ?</dependency>
? ?</dependencies>
? ?<build>
? ? ? ?<plugins>
? ? ? ? ? ?<plugin>
? ? ? ? ? ? ? ?<groupId>org.apache.maven.plugins</groupId>
? ? ? ? ? ? ? ?<artifactId>maven-assembly-plugin</artifactId>
? ? ? ? ? ? ? ?<executions>
? ? ? ? ? ? ? ? ? ?<execution>
? ? ? ? ? ? ? ? ? ? ? ?<goals>
? ? ? ? ? ? ? ? ? ? ? ? ? ?<goal>attached</goal>
? ? ? ? ? ? ? ? ? ? ? ?</goals>
? ? ? ? ? ? ? ? ? ? ? ?<phase>package</phase>
? ? ? ? ? ? ? ? ? ? ? ?<configuration>
? ? ? ? ? ? ? ? ? ? ? ? ? ?<descriptorRefs>
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?<descriptorRef>jar-with-dependencies</descriptorRef>
? ? ? ? ? ? ? ? ? ? ? ? ? ?</descriptorRefs>
? ? ? ? ? ? ? ? ? ? ? ?</configuration>
? ? ? ? ? ? ? ? ? ?</execution>
? ? ? ? ? ? ? ?</executions>
? ? ? ? ? ?</plugin>
? ? ? ?</plugins>
? ?</build>
</project>
腳本編寫(xiě)完畢之后,將項(xiàng)目打成 Jar 包,放到下載的 JVM-Sandbox 項(xiàng)目下 sandbox-module 目錄中。

啟動(dòng)寫(xiě)好的 java 目標(biāo)程序,運(yùn)行之后命令行開(kāi)始循環(huán)打印之前設(shè)置好的語(yǔ)句,內(nèi)容如下:
stringParam is a
boolParam is false
intParam is 666
stringParam is a
boolParam is false
intParam is 666
在 gitbash 命令行中打開(kāi)
sandbox/bin
?目錄,執(zhí)行語(yǔ)句./sandbox.sh -p目標(biāo)應(yīng)用進(jìn)程號(hào)-d 'ceshiren.com/ceshiren'
?,啟動(dòng) JVM-Sandbox 并對(duì)目標(biāo)程序進(jìn)行 Hook 操作,變更report
?方法中傳入的參數(shù)值,這時(shí)再回到目標(biāo)程序運(yùn)行的命令行中查看,可以看到命令行中輸出的內(nèi)容已經(jīng)變更,如下:
stringParam is Change By Hook!
boolParam is false
intParam is 965
stringParam is Change By Hook!
boolParam is false
intParam is 965
輸出內(nèi)容的變更,說(shuō)明 Hook 已經(jīng)生效。這樣在目標(biāo)程序運(yùn)行中修改了方法傳入的參數(shù)值,達(dá)到了 Hook 的目的。
現(xiàn)在執(zhí)行語(yǔ)句
./sandbox.sh -p 目標(biāo)應(yīng)用進(jìn)程號(hào) -S
?可以關(guān)閉修改,命令行中輸出的內(nèi)容變回了原始的輸出內(nèi)容。
示例簡(jiǎn)單展示了 JVM-Sandbox 用作 Hook 工具的功能,通過(guò) Hook 功能就可以對(duì) Java 項(xiàng)目的內(nèi)部運(yùn)行邏輯和參數(shù)、返回值進(jìn)行修改。測(cè)試場(chǎng)景的構(gòu)建、測(cè)試用例的執(zhí)行都變得更加方便哦~