15

我需要更改已部署的 Java 程序中的字符串常量,.class即已编译文件中的值。它可以重新启动,但不容易重新编译(尽管如果这个问题没有答案,这是一个不方便的选择)。这可能吗?

更新:我只是用十六进制编辑器查看了文件,看起来我可以轻松地更改那里的字符串。那会起作用吗,即不会使文件的某种签名无效吗?旧字符串和新字符串都是字母数字,如果需要,可以是相同的长度。

更新2:我修好了。因为我需要更改的特定类非常小,并且在新版本的项目中没有更改,所以我可以编译它并从那里获取新类。出于教育目的,仍然对不涉及编译的答案感兴趣。

4

5 回答 5

8

如果你有这个类的资源,那么我的方法是:

  • 获取 JAR 文件
  • 获取单个类的源代码
  • 使用类路径上的 JAR 编译源代码(这样,您不必编译其他任何东西;JAR 已经包含二进制文件并没有什么坏处)。您可以为此使用最新的 Java 版本;-source只需使用and降级编译器-target
  • 将 JAR 中的类文件替换为新的 usingjar u或 Ant 任务

Ant 任务示例:

        <jar destfile="${jar}"
            compress="true" update="true" duplicate="preserve" index="true"
            manifest="tmp/META-INF/MANIFEST.MF"
        >
            <fileset dir="build/classes">
                <filter />
            </fileset>
            <zipfileset src="${origJar}">
                <exclude name="META-INF/*"/>
            </zipfileset>
        </jar>

在这里,我还更新了清单。首先放置新类,然后添加原始 JAR 中的所有文件。duplicate="preserve"将确保新代码不会被覆盖。

如果代码未签名,如果新字符串与旧字符串的长度完全相同,您也可以尝试替换字节。Java 对代码进行了一些检查,但.class 文件中没有校验和

您必须保留长度;否则类加载器会感到困惑。

于 2012-05-21T09:02:55.623 回答
5

修改常量池中的字符串(技术上是 Utf8 项)时唯一需要的额外数据是长度字段(数据前的 2 个字节大端序)。没有其他需要修改的校验和或偏移量。

有两个警告:

  • 该字符串可以在其他地方使用。例如,“代码”用于方法代码属性,因此更改它会破坏文件。
  • 该字符串以修改后的 Utf8 格式存储。因此,基本平面之外的空字节和 unicode 字符的编码方式不同。长度字段是字节数,而不是字符数,限制为 65535。

如果您打算经常这样做,最好获得一个类文件编辑器工具,但十六进制编辑器对于快速更改很有用。

于 2012-06-16T01:02:16.137 回答
3

我最近编写了自己的 ConstantPool 映射器,因为 ASM 和 JarJar 存在以下问题:

  • 放慢速度
  • 不支持没有所有类依赖的重写
  • 不支持流式传输
  • 不支持 Tree API 模式下的 Remapper
  • 不得不展开和折叠 StackMaps

我最终得到以下结果:

public void process(DataInputStream in, DataOutputStream out, Function mapper) throws IOException {
    int magic = in.readInt();
    if (magic != 0xcafebabe) throw new ClassFormatError("wrong magic: " + magic);
    out.writeInt(magic);

    copy(in, out, 4); // minor and major

    int size = in.readUnsignedShort();
    out.writeShort(size);

    for (int i = 1; i < size; i++) {
        int tag = in.readUnsignedByte();
        out.writeByte(tag);

        Constant constant = Constant.constant(tag);
        switch (constant) {
            case Utf8:
                out.writeUTF(mapper.apply(in.readUTF()));
                break;
            case Double:
            case Long:
                i++; // "In retrospect, making 8-byte constants take two constant pool entries was a poor choice."
                // See http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.4.5
            default:
                copy(in, out, constant.size);
                break;
        }
    }
    Streams.copyAndClose(in, out);
}

private final byte[] buffer = new byte[8];

private void copy(DataInputStream in, DataOutputStream out, int amount) throws IOException {
    in.readFully(buffer, 0, amount);
    out.write(buffer, 0, amount);
}

接着

public enum Constant {
    Utf8(1, -1),
    Integer(3, 4),
    Float(4, 4),
    Long(5, 8),
    Double(6,8),
    Class(7, 2),
    String(8, 2),
    Field(9, 4),
    Method(10, 4),
    InterfaceMethod(11, 4),
    NameAndType(12, 4),
    MethodHandle(15, 3),
    MethodType(16, 2),
    InvokeDynamic(18, 4);

public final int tag, size;

Constant(int tag, int size) { this.tag = tag; this.size = size; }

private static final Constant[] constants;
static{
    constants = new Constant[19];
    for (Constant c : Constant.values()) constants[c.tag] = c;
}

public static Constant constant(int tag) {
    try {
        Constant constant = constants[tag];
        if(constant != null) return constant;
    } catch (IndexOutOfBoundsException ignored) { }
    throw new ClassFormatError("Unknown tag: " + tag);
}

只是想我会展示没有库的替代方案,因为这是一个开始黑客攻击的好地方。我的代码受到javap源代码的启发

于 2014-09-08T20:12:29.063 回答
3

您可以使用许多字节码工程库来修改 .class。例如,使用javaassist

但是,如果您尝试替换静态 final 成员,它可能不会给您想要的效果,因为编译器会在使用该常量的任何地方内联该常量。

使用 javaassist.jar 的示例代码

//ConstantHolder.java

public class ConstantHolder {

 public static final String HELLO="hello";

 public static void main(String[] args) {
  System.out.println("Value:" + ConstantHolder.HELLO);
 }
}

//修改Constant.java

import java.io.IOException;

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtField;
import javassist.NotFoundException;

//ModifyConstant.java
public class ModifyConstant {
 public static void main(String[] args) {
  modifyConstant();
 }

 private static void modifyConstant() {
  ClassPool pool = ClassPool.getDefault();
  try {
   CtClass pt = pool.get("ConstantHolder");
   CtField field = pt.getField("HELLO");
   pt.removeField(field);
   CtField newField = CtField.make("public static final String HELLO=\"hell\";", pt);
   pt.addField(newField);
   pt.writeFile();
  } catch (NotFoundException e) {
   e.printStackTrace();System.exit(-1);
  } catch (CannotCompileException e) {
   e.printStackTrace();System.exit(-1);
  } catch (IOException e) {
   e.printStackTrace();System.exit(-1);
  }
 }  
}

在这种情况下,程序成功地将 HELLO 的值从“Hello”修改为“Hell”。但是,当您运行 ConstantHolder 类时,由于编译器的内联,它仍会打印“Value:Hello”。

希望能帮助到你。

于 2012-05-21T09:54:35.670 回答
0

我过去也遇到过类似的问题。我的解决方案是使用上述字节码工程库之一。我找不到 javaassist,但是有一个很棒的工具叫做dirtyJOE,它允许你(在很多事情中)编辑你的.class 文件中的常量。

这是一个截图

您只需导入 .class 文件并单击常量

您只需导入 .class 文件并单击常量

于 2018-09-05T08:29:54.333 回答