标签 java 下的文章

java Agent 简单学习

java_logo

前言

java.lang.instrument包是Java中来增强JVM上的应用的一种方式,机制是在JVM启动前或启动后attach上去进行修改方法字节码的方式。
instrument包的用途很多,主要体现在对代码侵入低的优点上,例如一些监控不方便修改业务代码,但是可以使用这种方式在方法中植入特定逻辑,这种方式能够直接修改JVM中加载的字节码的内容,而不需要在像Spring AOP实现中创建新的代理类,所以在底层侵入更高,但是对开发者更透明。用于自动添加getter/setter方法的工具lombok就使用了这一技术。另外btrace和housemd等动态诊断工具也是用了instrument技术。

创建一个简单的Agent

关于premain的介绍:

通常agent的包里面MATE-INF目录下的MANIFEST.MF中会有这样一段声明

Premain-Class: cn.org.javaweb.test.Agent

在你通过启动命令添加-javaagent:xxx.jar 的时候,JVM会去xxx.jar中找其中Premain-Class是你在MANIFEST.MF中声明的cn.org.javaweb.test.Agent这个类中的public static void premain(String agentOps, Instrumentation instrumentation)或者public static void premain(String agentOps)方法.

注意这里,public static void premain(String agentOps, Instrumentation instrumentation)public static void premain(String agentOps)这两个是有优先级的,其中当premain有两个参数,也就是public static void premain(String agentOps, Instrumentation instrumentation)的时候优先级最高,如果两个都存在,public static void premain(String agentOps)则会被忽略。

其中agentOps将获得程序的参数,会随着-javaagent一起传入,如下:

java -javaagent:agent-0.0.1.jar="Hello World.?"  -jar agent-1.0-SNAPSHOT.jar

将会将Hello World.?传入进去,和main方法不一样的是,该处传入的是一串完整的字符串,并不会传入解析以后的字符串,所以此处如果有需要的话,那么此处传入字符串将由agent完成解析

其中instrumentationjava.lang.instrument.Instrumentation 的实例,由 JVM 自动传入。java.lang.instrument.Instrumentationinstrument 包中定义的一个接口,也是这个包的核心部分,集中了其中几乎所有的功能方法,例如类定义的转换和操作等等。


了解了以上知识以后,我们来创建一个agent。

创建agent

首先创建一个名为cn.org.javaweb.test的包,然后在该包下创建一个为Agent的类,
如下:

package cn.org.javaweb.test;
/*
 * Copyright sky 2018-11-20 Email:sky@03sec.com.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * @author sky
 */
public class Agent {
}

然后写一个premain方法

public static void premain(String agentOps, Instrumentation inst) {
        System.out.println("=======this is agent premain function=======");
    }

将其使用maven打包为jar文件,pom.xml文件中build的内容为:

<plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.0.2</version>
                <configuration>
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                        </manifest>
                        <manifestEntries>
                            <Premain-Class>
                                cn.org.javaweb.test.Agent
                            </Premain-Class>
                            <can-redefine-classes>
                                true
                            </can-redefine-classes>
                        </manifestEntries>
                    </archive>
                </configuration>
            </plugin>
</plugins>

然后使用maven中的clean install命令安装即可(或者使用mvn clean package),将会生成一个agent.jar的文件。

创建测试程序

此时我们随便新建一个带有main方法的程序,

package cn.org.javaweb.test;
/*
 * Copyright sky 2018-11-20 Email:sky@03sec.com.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * @author sky
 */
public class TestAgent {

    public static void main(String[] args) {
        System.out.print("Hello,This is TestAgent Program!");
    }
}

pom.xml文件中build的内容是


        <plugins>
            <plugin>
                <artifactId>maven-shade-plugin</artifactId>
                <groupId>org.apache.maven.plugins</groupId>
                <version>1.2.1</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <transformers>
                                <transformer
                                        implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <mainClass>cn.org.javaweb.test.TestAgent</mainClass>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
</plugins>

然后使用maven生成jar文件。

测试agent

这时候我们会得到两个jar文件,

agent.jar --> agent生成的jar文件
test.jar  --> 测试程序

我们先来运行下测试程序,看是否正常

java -jar test.jar 

输出

Hello,This is TestAgent Program!

然后我们加入agent运行试试看

java -javaagent:agent.jar -jar test.jar

输出

=======this is agent premain function=======
Hello,This is TestAgent Program!

可以看到premain会在程序的main方法之前运行,此时我们可以简单的看下jvm中加入agent以及不加入agent的运行过程。

不加入agent的运行过程

加入agent的运行过程

可以简单明了的看到agent会在运行程序之前运行,但是其后面涉及到其他复杂的逻辑,这里不做解释。

使用instrumentation参数

在上面我们只是进入了premain方法,其中premain方法的instrumentation参数我们并没有用到,这里我们将简单的介绍使用instrumentation参数。

关于Transformer解释

此方法的实现可以转换提供的类文件,并返回一个新的替换类文件。
有两种装换器,由 Instrumentation.addTransformer(ClassFileTransformer,boolean)canRetransform 参数确定:

  • 可重转换 转换器,将 canRetransform 设为 true 可添加这种转换器
  • 不可重转换 转换器,将 canRetransform 设为 false 或者使用 Instrumentation.addTransformer(ClassFileTransformer)可添加这种转换器

在转换器使用 addTransformer 注册之后,每次定义新类和重定义类时都将调用该转换器。
每次重转换类时还将调用可重转换转换器。
对新类定义的请求通过 ClassLoader.defineClass或其本机等价方法进行。
对类重定义的请求通过 Instrumentation.redefineClasses 或其本机等价方法进行。
对类重转换的请求将通过Instrumentation.retransformClasses 或其本机等价方法进行。
转换器是在验证或应用类文件字节之前的请求处理过程中调用的。
当存在多个转换器时,转换将由 transform 调用链组成。 也就是说,一个 transform 调用返回的 byte 数组将成为下一个调用的输入(通过 classfileBuffer 参数)。

转换将按以下顺序应用:

  • 不可重转换转换器
  • 不可重转换本机转换器
  • 可重转换转换器
  • 可重转换本机转换器

对于重转换,不会调用不可重转换转换器,而是重用前一个转换的结果。对于所有其他情况,调用此方法。在每个这种调用组中,转换器将按照注册的顺序调用。本机转换器由 Java 虚拟机 Tool 接口中的 ClassFileLoadHook 事件提供。

ClassLoader loader                  ----> 这个没什么解释的,就是classloader
String className                    ----> 包名(jvm中包名是/而不是.)
Class<?> classBeingRedefined        ----> 如果是被重定义或重转换触发,则为重定义或重转换的类;如果是类加载,则为 null
ProtectionDomain protectionDomain   ----> 保护域
byte[] classfileBuffer              ----> 二进制字节码

添加一个新的Transformer类

我们新建一个名为TestTransformer的类,内容入下:

/*
 * Copyright sky 2018-11-20 Email:sky@03sec.com.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package cn.org.javaweb.test;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

/**
 * @author sky
 */
public class TestTransformer implements ClassFileTransformer {

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                            ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {

        System.out.println(className.replace("/", "."));
        return classfileBuffer;
    }
}

在Agent.java中修改premain方法为

System.out.println("=======this is agent premain function=======");
inst.addTransformer(new TestTransformer());

将我们新建的TestTransformer注册进去
这时候我们在重新生成agent后运行如下命令

java -javaagent:agent.jar -jar test.jar

将会输出

=======this is agent premain function=======
sun.launcher.LauncherHelper
cn.org.javaweb.test.TestAgent
java.lang.Void
Hello,This is TestAgent Program!
java.lang.Shutdown
java.lang.Shutdown$Lock

可以看到会输出很多包名,这是因为我们在TestTransformer中将包名打印了出来。

结束语

关于agent的简单使用就介绍到了此处,此文仅抛砖引玉,更多用法可以发挥想象,比如性能监控,日志监控、管理会话、安全过滤、请求管理等。

文章中涉及到的代码全部都在下面地址:

https://gitlab.com/iiusky/javaagent

参考:

乐尚视界APP数据包加解密脚本

突然看到一个APP叫乐尚视界,能看十几个网站的vip视频,感觉和之前的网页在线观看vip视频没有实名区别。
在抓包的时候发现了APP对数据包进行了加密,对APP进行反编译后,一边练手一边写出下面的加解密脚本,仅供参考。

/*
 * Copyright sky 2018-01-18 Email:sky@03sec.com.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.javaweb.demo;

import javax.crypto.*;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.security.*;
import java.util.Base64;

public class LeeShang {


    public static String aaaaa(String var0) {
        byte[] var3;
        try {
            var3 = MessageDigest.getInstance("md5").digest(var0.getBytes());
        } catch (NoSuchAlgorithmException var2) {
            throw new RuntimeException("没有md5这个算法!");
        }

        var0 = (new BigInteger(1, var3)).toString(16);

        for (int var1 = 0; var1 < 32 - var0.length(); ++var1) {
            var0 = "0" + var0;
        }

        return var0;
    }

    public static byte[] encrypt(byte[] paramArrayOfByte, String password, String secureRandom) {
        try {
            SecretKeySpec keySpec     = secretKeySpec(password);
            Cipher        localCipher = Cipher.getInstance("AES/CBC/NoPadding");
            while (paramArrayOfByte.length % 16 != 0) { //如果paramArrayOfByte的长度不是16的倍数AES加密会报错,这边对paramArrayOfByte进行长度扩展,使它必须为16的倍数
                byte[] tmpByte = {0x00};
                paramArrayOfByte = ByteBuffer.allocate(paramArrayOfByte.length + 1).put(tmpByte).array();
            }
            localCipher.init(2, keySpec, ivParameterSpec(secureRandom));
            paramArrayOfByte = localCipher.doFinal(paramArrayOfByte);
            return paramArrayOfByte;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    public static byte[] decrypt(byte[] content, String password, String secureRandom) {
        try {
            SecretKeySpec keySpec = secretKeySpec(password);
            Cipher        cipher  = Cipher.getInstance("AES/CBC/NoPadding");// 创建密码器
            cipher.init(Cipher.DECRYPT_MODE, keySpec, ivParameterSpec(secureRandom));// 初始化
            byte[] result = cipher.doFinal(content);
            return result; // 加密
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    private static SecretKeySpec secretKeySpec(String var0) {
        Object var2 = null;
        String var1 = var0;
        if (var0 == null) {
            var1 = "";
        }

        StringBuffer var4 = new StringBuffer(16);
        var4.append(var1);

        while (var4.length() < 16) {
            var4.append("0");
        }

        if (var4.length() > 16) {
            var4.setLength(16);
        }

        byte[] var5;
        try {
            var5 = var4.toString().getBytes("UTF-8");
        } catch (UnsupportedEncodingException var3) {
            var3.printStackTrace();
            var5 = (byte[]) var2;
        }

        return new SecretKeySpec(var5, "AES");
    }


    private static IvParameterSpec ivParameterSpec(String var0) {
        Object var2 = null;
        String var1 = var0;
        if (var0 == null) {
            var1 = "";
        }

        StringBuffer var4 = new StringBuffer(16);
        var4.append(var1);

        while (var4.length() < 16) {
            var4.append("0");
        }

        if (var4.length() > 16) {
            var4.setLength(16);
        }

        byte[] var5;
        try {
            var5 = var4.toString().getBytes("UTF-8");
        } catch (UnsupportedEncodingException var3) {
            var3.printStackTrace();
            var5 = (byte[]) var2;
        }

        return new IvParameterSpec(var5);
    }

    public static String convertByteArrayToString(byte[] var1) {

        String value = new String(var1);
        return value;
    }

    public static void main(String[] args) {
        String key          = "$75k!xxH&$EhQLmv";
        String secureRandom = aaaaa("$75k!xxH&$EhQLmv").substring(0, 16);
        //解密
        String content      = "jStVIqaSUdIm0aF8mcs8GAjohYApwOUYVXenyG7zeQV86ZFrmO3Z1ixPLEM9srKRaVR1nE+0V3gTKGtlgwWgCg==";
        System.out.println(convertByteArrayToString(decrypt(Base64.getDecoder().decode(content), key, secureRandom)));
        //加密
        String s = "{'token':'x','code':'ghhhh'}";
        System.out.println(Base64.getEncoder().encodeToString(encrypt(s.getBytes(), key, secureRandom)));
    }
}

Java中isAssignableFrom()方法与instanceof()方法用法

一句话总结:

  • isAssignableFrom()方法是从类继承的角度去判断,instanceof()方法是从实例继承的角度去判断。
  • isAssignableFrom()方法是判断是否为某个类的父类,instanceof()方法是判断是否某个类的子类。
    ### 1. Class.isAssignableFrom()方法
    Class.isAssignableFrom()是用来判断一个类Class1和另一个类Class2是否相同或是另一个类的子类或接口。
    格式为:
Class1.isAssignableFrom(Class2) 

调用者和参数都是java.lang.Class类型。

2.Class.instanceof()方法

Class.instanceof()是用来判断一个对象实例是否是一个类或接口的或其子类子接口的实例。

格式是:

obj instanceof TypeName  

第一个参数是对象实例名,第二个参数是具体的类名或接口名,例如 String,InputStream。其返回值为boolean。

3.具体用法

转自(http://sunnylocus.iteye.com/blog/555676)

package com.bill99.pattern;

public class AssignableTest {
    
    public AssignableTest(String name) {
    }
    /**
     * 判断一个类是否是另一个类的父类
     * 是打印true
     * 否打印false
     */
    public static void testIsAssignedFrom1() {
        System.out.println("String是Object的父类:"+String.class.isAssignableFrom(Object.class));
    }
    /**
     * 判断一个类是否是另一个类的父类
     * 是打印true
     * 否打印false
     */
    public static void testIsAssignedFrom2() {
        System.out.println("Object是String的父类:"+Object.class.isAssignableFrom(String.class));
    }
    /**
     * 判断一个类是否和另一个类相同
     * 是打印true
     * 否打印false
     */
    public static void testIsAssignedFrom3() {
        System.out.println("Object和Object相同:"+Object.class.isAssignableFrom(Object.class));
    }

    /**
     * 判断str是否是Object类的实例
     * 是打印true
     * 否打印false
     */
    public static void testInstanceOf1() {
        String str = new String();
        System.out.print("str是Object的实例:");
        System.out.println(str instanceof Object);
    }
    /**
     * 判断o是否是Object类的实例
     * 是打印true
     * 否打印false
     */
    public static void testInstanceOf2() {
        Object o = new Object();
        System.out.print("o是Object的实例:");
        System.out.println(o instanceof Object);
    }
    
    public static void main(String[] args) {
        testIsAssignedFrom1();
        testIsAssignedFrom2();
        testIsAssignedFrom3();
        testInstanceOf1();
        testInstanceOf2();
    }
}

结果:

String是Object的父类:false
Object是String的父类:true
Object和Object相同:true
str是Object的实例:true
o是Object的实例:true

批量反编译jar包

  • 首先把jar包改为后缀rar,然后进行解压。

解压过程中可能会报错,忽略即可

然后在终端输入下列命令进行批量反编译(xxx是上面rar解压出来的目录)

jad -r -ff -d src -s java xxx/**/*.class

这边有个坑,如果你是macos配置的zsh终端,那可能会提示下面这段话

zsh: argument list too long: jad

解决办法:
临时切换为bash,然后使用命令进行反编译

MAC JDK版本切换

通过命令'jdk7', 'jdk8'轻松切换到对应的Java版本:

  • 1.首先安装所有的JDk



    • Mac自带了的JDK6,安装在目录:/System/Library/Java/JavaVirtualMachines/1.6.0.jdk/下。
    • JDK7,JDK8,JDK9则需要自己到Oracle官网下载安装对应的版本。自己安装的JDK默认路径为:/Library/Java/JavaVirtualMachines/jdk1.8.0.jdk
  • 2.配置
    编辑~/.zshrc文件(我是zsh的shell,所有配置该文件,如果是bash,则配置.bash_profile)

在最下面加入下面的代码

#设置jdk版本
export JAVA_7_HOME=/Library/Java/JavaVirtualMachines/jdk1.7.0_80.jdk/Contents/Home
export JAVA_8_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home


#alias命令动态切换JAVA_HOME的配置
alias jdk7='export JAVA_HOME=$JAVA_7_HOME'
alias jdk8='export JAVA_HOME=$JAVA_8_HOME'


export JAVA_HOME=$JAVA_7_HOME

输入完成后保存,然后执行下面命令

source ~/.zshrc
  • 3.验证

使用:jdk7、jdk8即可切换jdk版本