初識 Java Agent - 實現(xiàn)簡單AOP操作

引言

相信大家對 IAST(Interactive Application Security Testing,交互式應(yīng)用程序安全測試) 和 RASP(Runtime application self-protection,運行時應(yīng)用自我保護)這兩款產(chǎn)品已經(jīng)不陌生了,那究竟是什么神仙技術(shù)能衍生出這么牛皮的安全產(chǎn)品?

帶著疑問,我開始了這一次”修行“。

3333.jpg

0x01 簡介


在Java SE 5及后續(xù)版本中,開發(fā)者可以構(gòu)建一個獨立于應(yīng)用程序的代理程序(Agent),用來監(jiān)測和協(xié)助運行在 JVM 上的程序,甚至能夠替換和修改某些類的定義。

利用這一特性衍生出了 IAST(Interactive Application Security Testing,交互式應(yīng)用程序安全測試) 和 RASP(Runtime application self-protection,運行時應(yīng)用自我保護)等相關(guān)安全產(chǎn)品。

  • 問題:

    • Java Agent 如何調(diào)試呢?
    • Java Agent 實現(xiàn)原理是什么?

0x02 加載方式


在官方API文檔中提到,Java Agent 有兩種加載方式:

(1)premain, 當以指示代理類的方式啟動JVM時。 在這種情況下, Instrumentation實例被傳遞給代理類的premain方法。(-javaagent 啟動)

(2)agentmain, 當JVM在JVM啟動后的某個時間提供啟動代理的機制時。 在這種情況下, Instrumentation實例將傳遞給代理代碼的agentmain方法。(利用attach api,動態(tài)啟動)

premain 與 agentmain 的區(qū)別:

運行模式不同:

premain 相當于在main前類加載時進行字節(jié)碼修改,而agentmain則是main后在類調(diào)用前通過重新轉(zhuǎn)換類完成字節(jié)碼修改。

部署方式不同:

由于加載方式不同,所以premain只能在程序啟動時指定Agent文件進行部署,而agentmain需要通過Attach API在程序運行后根據(jù)進程ID動態(tài)注入agent到j(luò)vm中。

0x03 編譯構(gòu)建


(1)那如何構(gòu)建一個Java Agent 呢?創(chuàng)建一個新項目并新建一個MyAgent類:
public class MyAgent {
    /**
     * premain jvm 參數(shù)形式啟動,運行此方法
     *
     * @param agentArgs
     * @param inst
     */
    public static void premain(String agentArgs, Instrumentation inst){
        System.out.println("premainAgent Start");
    }

    /**
     * agentmain 動態(tài) attach 方式啟動,運行此方法
     *
     * @param agentArgs
     * @param inst
     */
    public static void agentmain(String agentArgs, Instrumentation inst){
        System.out.println("agentmainAgent Start");
    }
}

(2)利用maven插件,將代碼打包為jar包,通常有兩種方式:

a. Pom 指定配置

在 pom.xml 文件中,添加如下配置:

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.1.0</version>
                <configuration>
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                        </manifest>
                        <manifestEntries>
                            <Premain-Class>com.bug1024.MyAgent</Premain-Class>
                            <Agent-Class>com.bug1024.MyAgent</Agent-Class>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                            <Can-Set-Native-Method-Prefix>true</Can-Set-Native-Method-Prefix>
                        </manifestEntries>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>

在配置的打包參數(shù)中,通過manifestEntries的方式添加屬性到MANIFEST.MF文件中,解釋下里面的幾個參數(shù):

  1. Premain-Class:包含premain方法的類,需要配置為類的全路徑
  2. Agent-Class:包含agentmain方法的類,需要配置為類的全路徑
  3. Can-Redefine-Classes:為true時表示能夠重新定義Class
  4. Can-Retransform-Classes:為true時表示能夠重新轉(zhuǎn)換Class,實現(xiàn)字節(jié)碼替換
  5. Can-Set-Native-Method-Prefix:為true時表示能夠設(shè)置native方法的前綴

配置完成后使用mvn命令打包:

mvn clean package

打包完成后生成AgentTest-1.0-SNAPSHOT.jar文件,解壓jar文件我們可以看到生成的MANIFEST.MF文件:

Manifest-Version: 1.0
Premain-Class: com.bug1024.MyAgent
Built-By: 07
Agent-Class: com.bug1024.MyAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Can-Set-Native-Method-Prefix: true
Created-By: Apache Maven 3.6.3
Build-Jdk: 1.8.0_261

此時,第一種打包方式就完成了。

b. MANIFEST.MF 配置文件

通過配置文件MANIFEST.MF打包的方式也比較常見,操作如下:

1. 在資源目錄(resources)下,新建目錄`META-INF`
2. 在`META-INF`目錄下,新建文件`MANIFEST.MF`

文件內(nèi)容可以直接復制我們上述內(nèi)容,然后在pom.xml配置,做對應(yīng)的修改,如下:

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.1.0</version>
                <configuration>
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                        </manifest>
                        <manifestFile>
                            src/main/resources/META-INF/MANIFEST.MF
                        </manifestFile>
<!--                        <manifestEntries>-->
<!--                            <Premain-Class>com.bug1024.MyAgent</Premain-Class>-->
<!--                            <Agent-Class>com.bug1024.MyAgent</Agent-Class>-->
<!--                            <Can-Redefine-Classes>true</Can-Redefine-Classes>-->
<!--                            <Can-Retransform-Classes>true</Can-Retransform-Classes>-->
<!--                            <Can-Set-Native-Method-Prefix>true</Can-Set-Native-Method-Prefix>-->
<!--                        </manifestEntries>-->
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>

同樣通過mvn clean package打包即可。

(3)Agent 打包完成后,接下來開始對兩種加載方式進行調(diào)試。

a. premain jvm 參數(shù)形式啟動 ,新建一個demo運行HelloWorld程序,代碼示例如下:

public class Demo {

    public void say() throws InterruptedException {
        for (int i = 0; i < 7; i++) {
            Thread.sleep(1000); // 為方便后續(xù)延時attach加載agent添加延時并循環(huán)
            System.out.println("Hello World");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Demo d = new Demo();
        d.say();
    }
}

程序運行結(jié)果:

Hello World
Hello World
Hello World
...

添加jvm參數(shù)啟動:

-javaagent:/Users/.../AgentTest/target/AgentTest-1.0-SNAPSHOT.jar

程序運行結(jié)果:

premainAgent Start
Hello World

注意此時我們上面MyAgent中的premain方法已經(jīng)執(zhí)行,并輸出了premainAgent Start 表示我們第一種加載方式執(zhí)行成功。

b. agentmain 動態(tài) attach 方式啟動

在上文也有提到過agentmain需要通過Attach API在程序運行后根據(jù)進程ID動態(tài)注入agent到j(luò)vm中,我們利用VirtualMachine的attach方法連接目標虛擬機,代碼如下:

public class AttachMain {

    public void attachAgent() throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {

        List<VirtualMachineDescriptor> vm_list = VirtualMachine.list();
        for(VirtualMachineDescriptor v : vm_list){  // 遍歷程序列表
            if (v.displayName().equals("com.bug1024.Demo")){    // 判斷我們要注入的程序
                VirtualMachine vm = VirtualMachine.attach(v.id());  // 獲取目標程序進程id并根據(jù)進程id連接目標程序
                vm.loadAgent("/Users/.../AgentTest/target/AgentTest-1.0-SNAPSHOT.jar"); //加載Agent
            }
        }
    }

    public static void main(String[] args) throws AgentLoadException, IOException, AttachNotSupportedException, AgentInitializationException {
        AttachMain attach = new AttachMain();
        attach.attachAgent();
    }
}

運行我們的Demo測試類后運行AttachMain輸出如下結(jié)果:

premainAgent Start
Hello World
Hello World
Hello World
agentmainAgent Start
Hello World
...

注意此時我們上面MyAgent中的agentmain方法已經(jīng)執(zhí)行,并輸出了agentmainAgent Start 表示我們第二種加載方式也執(zhí)行成功。

(4)小結(jié)

上述內(nèi)容描述了JavaAgent打包調(diào)試全過程,兩種agent加載方式:

加載方式 說明 操作說明
premain() agent以jvm方式加載時調(diào)用,在目標應(yīng)用啟動時指定agent -javaagent:/Users/.../AgentTest/target/AgentTest-1.0-SNAPSHOT.jar
agentmain() agent以attach方式運行時調(diào)用,在目標程序啟動后,通過attach api 注入agent VirtualMachine vm = VirtualMachine.attach(v.id()); // 獲取目標程序進程id并根據(jù)進程id連接目標程序
vm.loadAgent("/Users/.../AgentTest/target/AgentTest-1.0-SNAPSHOT.jar"); //加載Agent

兩種打包方式:

  • 在pom.xml中指定配置
  • 在配置文件META-INF/MANIFEST.MF中配置

那JavaAgent實現(xiàn)原理是什么?安全產(chǎn)品是如何利用的呢?繼續(xù)往下探索。

0x04 Instrumentation & ASM 實現(xiàn)簡單AOP


Java Agent 是通過使用Instrumentation構(gòu)建出來的一個獨立于應(yīng)用程序的代理程序,用來監(jiān)測和協(xié)助運行在 JVM 上的程序,甚至能夠替換和修改某些類的定義。

Instrumentation 是 Java SE 5 的新特性,它把 Java 的 instrument 功能從本地代碼中解放出來,使之可以用 Java 代碼的方式解決問題。

在 Java SE 6 里面,instrumentation 包被賦予了更強大的功能:啟動后的 instrument、本地代碼(native code)instrument,以及動態(tài)改變 classpath 等等。這些改變,意味著 Java 具有了更強的動態(tài)控制、解釋能力,它使得 Java 語言變得更加靈活多變。

java.lang.instrument包結(jié)構(gòu)【官方API】:

  • ClassFileTransformer 接口
//  轉(zhuǎn)換類文件的代理接口,我們可以在獲取到Instrumentation對象后通過addTransformer方法添加自定義類文件轉(zhuǎn)換器。

public interface ClassFileTransformer { 
  /**
     * 類文件轉(zhuǎn)換方法,重寫transform方法可獲取到待加載的類相關(guān)信息
     *
     * @param loader              定義要轉(zhuǎn)換的類加載器;如果是引導加載器,則為 null
     * @param className           類名,如:java/lang/Runtime
     * @param classBeingRedefined 如果是被重定義或重轉(zhuǎn)換觸發(fā),則為重定義或重轉(zhuǎn)換的類;如果是類加載,則為 null
     * @param protectionDomain    要定義或重定義的類的保護域
     * @param classfileBuffer     類文件格式的輸入字節(jié)緩沖區(qū)(不得修改)
     * @return 返回一個通過ASM修改后添加了防御代碼的字節(jié)碼byte數(shù)組。
     */
    byte[]
    transform(  ClassLoader         loader,
                String              className,
                Class<?>            classBeingRedefined,
                ProtectionDomain    protectionDomain,
                byte[]              classfileBuffer)
        throws IllegalClassFormatException;
}

  • Instrumentation 接口
/**
 * 注冊一個Transformer,從此之后的類加載都會被Transformer攔截。
 * Transformer可以直接對類的字節(jié)碼byte[]進行修改
 */
void addTransformer(ClassFileTransformer transformer);

/**
 * 對JVM已經(jīng)加載的類重新觸發(fā)類加載。使用的就是上面注冊的Transformer。
 * retransformation可以修改方法體,但是不能變更方法簽名、增加和刪除方法/類的成員屬性
 */
void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;

/**
 * 獲取一個對象的大小
 */
long getObjectSize(Object objectToSize);

/**
 * 將一個jar加入到bootstrap classloader的 classpath里
 */
void appendToBootstrapClassLoaderSearch(JarFile jarfile);

/**
 * 獲取當前被JVM加載的所有類對象
 */
Class[] getAllLoadedClasses();

以上是幾個比較常用的方法,其他可詳細看官方API文檔;addTransformer 方法配置后,后續(xù)的類加載會被Transformer攔截。對于已經(jīng)加載過的類,可以執(zhí)行retransformClasses來重新觸發(fā)Transformer攔截。

類加載的字節(jié)碼被修改后,除非再次被retransform,否則不會恢復。


結(jié)合上面的描述,我們要通過ASM實現(xiàn)簡單AOP操作,肯定是要利用Transformer對類進行攔截后操作,代碼示例如下:

    /**
     * premain jvm 參數(shù)形式啟動,運行此方法
     *
     * @param agentArgs
     * @param inst
     */
    public static void premain(String agentArgs, Instrumentation inst){
        System.out.println("premainAgent Start");
//        Class<?>[] classes =  inst.getAllLoadedClasses();
//        for (Class<?> cls : classes){
//            System.out.println("premainAgent get loaded class : " + cls.getName());
//        }
        inst.addTransformer(new ClassFileTransformer() {
            @Override
            public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
                System.out.println("premainAgent get loaded class : " + className);
                return classfileBuffer;
            }
        });
    }

運行結(jié)果:

remainAgent Start
premainAgent get loaded class : sun/nio/cs/ThreadLocalCoders
premainAgent get loaded class : sun/nio/cs/ThreadLocalCoders$1
premainAgent get loaded class : sun/nio/cs/ThreadLocalCoders$Cache
premainAgent get loaded class : sun/nio/cs/ThreadLocalCoders$2
premainAgent get loaded class : sun/misc/URLClassPath$JarLoader$2
premainAgent get loaded class : java/util/jar/Attributes
premainAgent get loaded class : java/util/jar/Manifest$FastInputStream
premainAgent get loaded class : java/util/jar/Attributes$Name
premainAgent get loaded class : sun/misc/ASCIICaseInsensitiveComparator
premainAgent get loaded class : com/intellij/rt/execution/application/AppMainV2$Agent
premainAgent get loaded class : com/intellij/rt/execution/application/AppMainV2
premainAgent get loaded class : java/lang/NoSuchMethodException
premainAgent get loaded class : java/lang/reflect/InvocationTargetException

...

獲取加載的類之后,怎么實現(xiàn)簡單AOP操作呢?我們需要修改字節(jié)碼來實現(xiàn),常用的字節(jié)碼修改工具主要有ASM、Javassist和byte buddy,下面主要已ASM框架來實現(xiàn)需求。

ASM 簡介:

ASM 是一個 Java 字節(jié)碼操控框架。它能被用來動態(tài)生成類或者增強既有類的功能。ASM 可以直接產(chǎn)生二進制 class 文件,也可以在類被加載入 Java 虛擬機之前動態(tài)改變類行為。Java class 被存儲在嚴格格式定義的 .class 文件里,這些類文件擁有足夠的元數(shù)據(jù)來解析類中的所有元素:類名稱、方法、屬性以及 Java 字節(jié)碼(指令)。ASM 從類文件中讀入信息后,能夠改變類行為,分析類信息,甚至能夠根據(jù)用戶要求生成新類。

ASM 實現(xiàn)簡單AOP:

由于 ASM 是直接對class文件的字節(jié)碼進行操作,因此,要修改class文件內(nèi)容時,也要注入相應(yīng)的java字節(jié)碼。所以,在注入字節(jié)碼之前,我們還需要了解下class文件的結(jié)構(gòu),JVM指令等知識。(還沒搞明白就不展開寫了,直接貼代碼)

為了方便測試,在Demo測試類新增了exp()方法:

    public void say() throws InterruptedException {
        for (int i = 0; i < 2; i++) {
            Thread.sleep(1000); // 為方便后續(xù)延時attach加載agent添加延時循環(huán)
            String say_str = "普通方法---say()---不操作";
            System.out.println(say_str);
        }
    }
    public void exp() throws InterruptedException {
        for (int i = 0; i < 2; i++) {
            Thread.sleep(1000); // 為方便后續(xù)延時attach加載agent添加延時循環(huán)
            String exp_str = "目標方法---exp()---攔截并退出";
            System.out.println(exp_str);
        }
    }

目標是當程序運行到exp()方法時做出相應(yīng)操作,下面以premain加載方式為例進行調(diào)試;

    /**
     * premain jvm 參數(shù)形式啟動,運行此方法
     *
     * @param agentArgs
     * @param inst
     */
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("premainAgent Start");
        inst.addTransformer(new ClassFileTransformer() {
            @Override
            public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
                className = className.replace("/", ".");
                if (className.equals("com.bug1024.Demo")) {
                    ClassReader classReader = new ClassReader(classfileBuffer);
                    ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);
                    ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM5, classWriter) {
                        @Override
                        public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
                            MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
                            if ("exp".equals(name)) {
                                return new MethodVisitor(Opcodes.ASM5, mv) {
                                    @Override
                                    public void visitCode() {

                                        // 文章下面單獨展示

                                    }
                                };
                            }
                            return mv;
                        }
                    };
                    classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
                    classfileBuffer = classWriter.toByteArray();
                }
                return classfileBuffer;
            }
        });
    }

同樣利用Transformer進行攔截,正常情況將Transform抽出來單獨寫邏輯比較好,這里為了方便采用的流式寫法。

整個流程如下:

  • 根據(jù)className來判斷當前agent攔截的類是否要hook,如果是進入ASM修改流程;
  • 使用ASM提供的ClassReader類對字節(jié)碼進行讀取&遍歷,然后新建一個ClassWriterClassReader讀取的字節(jié)碼進行拼接;
    • ClassVisitor中調(diào)用visitMethod方法訪問hook類中的每個方法,根據(jù)方法名判斷是否為需要hook的方法,如果是,則調(diào)用visitCode方法實現(xiàn)后續(xù)邏輯,在目標方法exp()前插入輸出代碼并輸出:“目標方法即將運行”;
@Override
public void visitCode() {
    mv.visitCode();
    mv.visitFieldInsn(Opcodes.GETSTATIC,
            Type.getInternalName(System.class),
            "out",
            Type.getDescriptor(PrintStream.class));
    mv.visitLdcInsn("目標方法即將運行");
    mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
            Type.getInternalName(PrintStream.class), //"java/io/PrintStream"
            "println",
            "(Ljava/lang/String;)V",//方法描述符
            false);
    mv.visitEnd();
    super.visitCode();
}

實現(xiàn)效果如下:

premainAgent Start
普通方法---say()---不操作
普通方法---say()---不操作
目標方法即將運行
目標方法---exp()---攔截并退出
目標方法---exp()---攔截并退出

當然我們也可以在目標方法運行前插入其他方法的調(diào)用,代碼示例:

// 新建在目標法發(fā)前要執(zhí)行的方法
public static void Test()  {
    System.out.println("攔截目標方法并退出程序");
    System.exit(0);
}

// 修改visitCode()方法,添加調(diào)用Test()方法的邏輯
@Override
public void visitCode() {
    mv.visitCode();
    mv.visitMethodInsn(Opcodes.INVOKESTATIC,MyAgent.class.getName().replace(".","/"),"Test","()V",false);
    mv.visitEnd();
    super.visitCode();
}

運行結(jié)果如下:

premainAgent Start
普通方法---say()---不操作
普通方法---say()---不操作
攔截目標方法并退出程序

如上述結(jié)果,當程序執(zhí)行到目標方法exp()時直接退出不在往下進行,這樣一次簡單的AOP就實現(xiàn)了。

0x05 小結(jié)


在這次學習過程中發(fā)現(xiàn)不光是IAST 還是 RASP 中使用這種技術(shù),其實在安全防御實踐中很多場景都可以嘗試使用AOP技術(shù)去解決,如:日志審計、權(quán)限控制等等。(下圖為螞蟻集團的安全切面防御體系 - 安全切面:安全防御的平行空間

當然,如果我們要在Java中實現(xiàn)這些技術(shù),還是要好好了解下class文件結(jié)構(gòu)、jvm命令、ASM等相關(guān)知識。

螞蟻集團-安全切面防御體系

參考鏈接:

https://www.bilibili.com/video/av841675771/
https://my.oschina.net/ta8210/blog/162796
http://www.itdecent.cn/p/a85e8f83fa14
http://www.itdecent.cn/p/abd1b1b8d3f3
https://mp.weixin.qq.com/s/qZDvset94O2_2G-NTIvQUg
https://paper.seebug.org/1041/
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容