Byteman | 字节码操作的瑞士军刀

2022-06-27 15:08:04 浏览数 (1)

在前文中已经介绍了JVM的premainattach功能, 本文介绍下JBOSS开源的代码注入工具: byteman. 与之前讲的premainattach不同, byteman是通过规则文件完成代码注入等功能.

一. Hello World

1.1

byteman安装

byteman的安装非常简单, 从官网中下载安装包 https://byteman.jboss.org/downloads.html

设置BYTEMAN_HOME

代码语言:javascript复制
BYTEMAN_HOME=/xxx/byteman-download-4.0.13

添加path路径

代码语言:javascript复制
PATH=$PATH:$BYTEMAN_HOME/bin

1.2

hello world源代码

代码语言:javascript复制
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, world!");
    }
}

编译

代码语言:javascript复制
javac HelloWorld.java

运行

代码语言:javascript复制
java HelloWorld

执行结果

代码语言:javascript复制
Hello, world

1.3

appmain.btm 规则文件

编写注入规则文件, 分别在main()执行开始和结束时, 打印日志.

代码语言:javascript复制
RULE trace main entry
CLASS HelloWorld
METHOD main
AT ENTRY
IF true
DO traceln("entering main")
ENDRULE

RULE trace main exit
CLASS HelloWorld
METHOD main
AT EXIT
IF true
DO traceln("exiting main")
ENDRULE

1.4

验证byteman文件语法

验证规则文件语法是否正确:

代码语言:javascript复制
bmcheck.sh -cp . -v scripts/helloworld.btm

验证结果

代码语言:javascript复制
Checking rule trace main entry against class HelloWorld
Parsed rule "trace main entry" for class HelloWorld
Type checked rule "trace main entry"

Checking rule trace main exit against class HelloWorld
Parsed rule "trace main exit" for class HelloWorld
Type checked rule "trace main exit"

TestScript: no errors

1.5

javaagent方式运行

按javaagent方式方式运行程序:

代码语言:javascript复制
java -javaagent:$BYTEMAN_HOME/lib/byteman.jar=script:scripts/helloworld.btm HelloWorld

运行结果:

代码语言:javascript复制
entering main
Hello, world!
exiting main

1.6

简便运行方式

代码语言:javascript复制
bmjava.sh -l scripts/helloworld.btm HelloWorld

运行结果:

代码语言:javascript复制
entering main
Hello, world!
exiting main

以上, 成功修改main()代码逻辑.

二. byteman规则文件

2.1

规则文件基本格式

byteman规则文件的基本格式如下:

代码语言:javascript复制
RULE <规则名>
CLASS/INTERFACE <类名或接口名>
METHOD <方法名>
HELPER <规则增强类>
BIND <绑定事件>
IF <条件>
DO <动作>
ENDRULE

2.2

hello world规则文件简析

以main()进入时注入规则为例, 了解下规则文件.

代码语言:javascript复制
# 规则名称
RULE trace main entry
# 要进行注入的类全名
CLASS HelloWorld
# 方法名
METHOD main
# 绑定事件: 方法进入时进行代码注入
AT ENTRY
# 执行条件
IF true
# 以sout方式打印日志
DO traceln("entering main")
ENDRULE

2.3

绑定事件

byteman除了上述方法进入和退出时能进行注入外, 还有其他很多事件可以进行绑定事件.

代码语言:javascript复制
AT ENTRY //目标方法的第一个指令之前
AT EXIT //目标方法的所有正常返回点
AT LINE number //目标方法行号
AT READ [type .] field [count | ALL ] //读取field之前
AFTER READ [ type .] field [count | ALL ] //读取field之后
AT READ $var [count | ALL ] //读取local variable、parameter variable之前
AFTER READ $var [count | ALL ] //读取local variable、parameter variable之后
AT WRITE [ type .] field [count | ALL ] //写入field之前
AFTER WRITE [ type .] field [count | ALL ]//写入field之后
AT WRITE $var [count | ALL ] //写入local variable、parameter variable之前
AFTER WRITE $var [count | ALL ] //写入local variable、parameter variable之后
AT INVOKE [ type .] method [ ( argtypes ) ] [count | ALL ] //调用指定方法之前
AFTER INVOKE [ type .] method [ ( argtypes ) ][count | ALL ] //调用指定方法之后
AT SYNCHRONIZE [count | ALL ] //进入同步快之前
AFTER SYNCHRONIZE [count | ALL ] //离开同步快之后
AT THROW [count | ALL ] //抛出异常之前

2.4

规则变量

代码语言:javascript复制
$0 被注入类 $this
$1 $2.. 方法参数
$* 目标方法参数列表, 具体含义与$0 $1 $2相同
$# 目标方法参数个数
$! 方法返回值
$@ 方法参数, 与$1 $2相同
$^ 方法异常时, 异常信息

三. byteman的attach模式与helper

byteman除了上述的agent方式外, 还有attach模式, 能够在服务正常运行的情况下, 进行代码注入.

3.1

目标源代码

代码语言:javascript复制
package com.in;

import java.lang.management.ManagementFactory;

public class Main2 {
    public int add(int x, int y) {
        return x   y;
    }
    public static void main(String[] argv) {
        String name = ManagementFactory.getRuntimeMXBean().getName();
        System.out.println(name);
        Main2 m = new Main2();
        while (true) {
            System.out.println(m.add(1, 2));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

3.2

注入规则文件

在add()方法刚执行时, 打印方法参数; 在add()方法执行完毕后, 借助TraceHelper类打印方法执行结果.

代码语言:javascript复制
RULE trace arg1
CLASS com.in.Main2
METHOD add(int,int)
AT ENTRY
IF true
DO traceln("arg1="   $1   " arg2="   $2)
ENDRULE

RULE trace return value1
CLASS com.in.Main2
METHOD add(int,int)
HELPER com.in.TraceHelper
AT EXIT
IF true
DO myprint($!)
ENDRULE

3.3

TraceHelper类

代码语言:javascript复制
package com.in;

import org.jboss.byteman.rule.Rule;
import org.jboss.byteman.rule.helper.Helper;

public class TraceHelper extends Helper {

    protected TraceHelper(Rule rule) {
        super(rule);
    }

    public boolean myprint(String message) {
        return super.traceln("result:"   message);
    }
}

3.4

pom依赖

代码语言:javascript复制
<dependency>
  <groupId>org.jboss.byteman</groupId>
  <artifactId>byteman</artifactId>
  <version>4.0.13</version>
</dependency>

3.5

服务启动

编译

代码语言:javascript复制
javac com.in.Main2.java

运行

代码语言:javascript复制
java com.in.Main2

执行结果, 程序PID为47712

代码语言:javascript复制
47712@xxx-Pro.local
3
3
3

3.6

byteman注入

绑定JVM进程

代码语言:javascript复制
bminstall.sh 47712

提交规则

代码语言:javascript复制
bmsubmit.sh  -l scripts/appmain2.btm
install rule trace arg1
install rule trace return value1

3.7

服务运行变化

查看执行结果, 已经成功向服务注入代码

代码语言:javascript复制
arg1=1 arg2=2
result:3
3

3.8

注意事项

在mac环境中运行时, 需要对bminstall.sh进行修改和完善. 替换tools.jar和java ops变量

代码语言:javascript复制
function print_java_version()
{
    typeset java_version
    # grep output for line : 'java/openjdk version "AAAAAAAA"' where A is alpha, num, '''. or '-'
    java_version=`java -version 2>&1 |  grep "version" | cut -d'"' -f2`
    # format for JDK8- is 1.N.[n_] 
    if [ ${java_version%%.*} == 1 ] ; then
        echo $java_version | cut -d'.' -f2
    else
        # format may JDK9  may be N.n.[n_]  for proper
        # release or N-aaa for internal/ea build
        echo ${java_version%%[.-]*}
    fi
}

source ~/.bash_profile
# use BYTEMAN_HOME to locate installed byteman release
if [ -z "$BYTEMAN_HOME" ]; then
# use the root of the path to this file to locate the byteman jar
    BYTEMAN_HOME="${0%*/bin/bminstall.sh}"
# allow for rename to plain bminstall
    if [ "$BYTEMAN_HOME" == "$0" ]; then
 BYTEMAN_HOME="${0%*/bin/bminstall}"
    fi
    if [ "$BYTEMAN_HOME" == "$0" ]; then
 echo "Unable to find byteman home"
 exit
    fi
fi

# check that we can find  the byteman jar via BYTEMAN_HOME

# the Install class is in the byteman-install jar
if [ -r "${BYTEMAN_HOME}/lib/byteman.jar" ]; then
    BYTEMAN_JAR="${BYTEMAN_HOME}/lib/byteman.jar"
else
    echo "Cannot locate byteman jar"
    exit
fi
# the Install class is in the byteman-install jar
if [ -r "${BYTEMAN_HOME}/lib/byteman-install.jar" ]; then
    BYTEMAN_INSTALL_JAR="${BYTEMAN_HOME}/lib/byteman-install.jar"
else
    echo "Cannot locate byteman install jar"
    exit
fi
# for jdk6/7/8 we also need a tools jar from JAVA_HOME
JAVA_VERSION=$(print_java_version)
if [ $JAVA_VERSION -le 8 ]; then
  if [ -z "$JAVA_HOME" ]; then
     echo "please set JAVA_HOME"
     exit
  fi
# on Linux we need to add the tools jar to the path
# this is not currently needed on a Mac
  OS=`uname`
  if [ ${OS} != "Darwin" ]; then
    if [ -r "${JAVA_HOME}/lib/tools.jar" ]; then
      TOOLS_JAR="${JAVA_HOME}/lib/tools.jar"
      CP="${BYTEMAN_INSTALL_JAR}:${TOOLS_JAR}"
    else
      echo "Cannot locate tools jar"
      CP="${BYTEMAN_INSTALL_JAR}"
    fi
  else
    if [ $JAVA_VERSION -gt 6 ]; then
      if [ -r "${JAVA_HOME}/lib/tools.jar" ]; then
        TOOLS_JAR="${JAVA_HOME}/lib/tools.jar"
        CP="${BYTEMAN_INSTALL_JAR}:${TOOLS_JAR}"
      else
        echo "Cannot locate tools jar"
        CP="${BYTEMAN_INSTALL_JAR}"
      fi
    else
      CP="${BYTEMAN_INSTALL_JAR}"
    fi
  fi
else
  CP="${BYTEMAN_INSTALL_JAR}"
fi

# allow for extra java opts via setting BYTEMAN_JAVA_OPTS
# attach class will validate arguments
BYTEMAN_JAVA_OPTS="-Dorg.jboss.byteman.home=${BYTEMAN_HOME}"
java ${BYTEMAN_JAVA_OPTS} -classpath "$CP" org.jboss.byteman.agent.install.Install $*

四. byteman bash命令

bmcheck.sh 验证规则文件是否有语法错误 bminstall.sh 绑定JVM进程 bmsubmit.sh 将规则文件绑定到运行的JVM中或从JVM中解绑规则 bmjava.sh java agent运行方式的简写方式

五. IDE中开发

为了方便运行, 每次在命令行中执行是非常低效的. 所以, 在IDE中能开发运行是非常必要的. 在服务启动VM参数中添加启动参数:

代码语言:javascript复制
-javaagent:/xxx/byteman-download-4.0.13/lib/byteman.jar=script:/xxx/scripts/test.btm

小结

byteman可以将Java代码注入运行时服务, 而无需重新编译, 打包或重新部署应用程序, 可以很容易地跟踪, 监视和测试应用程序和JDK运行时代码的行为.

后文会介绍如何利用byteman进行流量记录和回复.

0 人点赞