Change string constant in a compiled class

14,192

Solution 1

If you have the sources for this class, then my approach is:

  • Get the JAR file
  • Get the source for the single class
  • Compile the source with the JAR on the classpath (that way, you don't have to compile anything else; it doesn't hurt that the JAR already contains the binary). You can use the latest Java version for this; just downgrade the compiler using -source and -target.
  • Replace the class file in the JAR with the new one using jar u or an Ant task

Example for an Ant task:

        <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>

Here I also update the manifest. Put the new classes first and then add all the files from the original JAR. duplicate="preserve" will make sure that the new code will not be overwritten.

If the code isn't signed, you can also try to replace the bytes if the new string has the exact same length as the old one. Java does some checks on the code but there is no checksum in the .class files.

You must preserve the length; otherwise the class loader will get confused.

Solution 2

The only extra data required when modifying a string (technically a Utf8 item) in the constant pool is the length field (2 bytes big endian preceding the data). There are no additional checksums or offsets that require modification.

There are two caveats:

  • The string may be used in other places. For example "Code" is used for a method code attribute, so changing it would break the file.
  • The string is stored in Modified Utf8 format. So null bytes and unicode characters outside the basic plane are encoded differently. The length field is the number of bytes, not characters, and is limited to 65535.

If you plan to do this a lot, it's better to get a class file editor tool, but the hex editor is useful for quick changes.

Solution 3

I recently wrote my own ConstantPool mapper because ASM and JarJar had the following issues:

  • To slow
  • Didn't support rewriting without all class dependencies
  • Didn't support streaming
  • Didn't support Remapper in Tree API mode
  • Had to expand and collapse StackMaps

I ended up with the following:

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);
}

And then

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);
}

Just thought I'd show alternatives without libraries as it's quite a nice place to start hacking from. My code is was inspired by javap source code

Solution 4

You can modify .class using many bytecode engineering libraries. For e.g., using javaassist.

However, if you're trying to replace a static final member, it may not give you the desired effect, because the compiler would inline this constant wherever it is used.

Sample code using 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);
 }
}

//ModifyConstant.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);
  }
 }  
}

In this case, the program successfully modifies the value of HELLO from "Hello" to "Hell". However, when you run ConstantHolder class, it would still print "Value:Hello" because of inlining by the compiler.

Hope it helps.

Share:
14,192
Bart van Heukelom
Author by

Bart van Heukelom

Professional software developer, online games, full stack but mostly backend. Electronics tinkerer. Maker. Freelance. See LinkedIn for more details. My UUID is 96940759-b98b-4673-b573-6aa6e38272c0

Updated on June 14, 2022

Comments

  • Bart van Heukelom
    Bart van Heukelom almost 2 years

    I need to change a string constant in a deployed Java program, i.e. the value inside the compiled .class-files. It can be restarted, but not easily recompiled (though it's an inconvenient option if this question yields no answers). Is this possible?

    Update: I just looked at the file with a hex editor and it looks like I can easily change the string there. Would that work, i.e. won't that invalidate some kind of signature of the file? The old and new string are both alphanumeric, and can be the same length if needed.

    Update 2: I fixed it. Because the specific class I needed to change is very small and didn't change in the new version of the project, I could just compile that and take the new class from there. Still interested in an answer that doesn't involve compilation though, for educational purposes.

  • Bart van Heukelom
    Bart van Heukelom almost 12 years
    I fixed it with the source (see question), but am still interested in finding a no-source answer. When inspecting the class in a hex editor, I saw that the length of the string is stored in one or more bytes before it. If I update that too, could I change the length of the string, or are there further length fields that need to be edited as well?
  • user unknown
    user unknown almost 12 years
    What does it mean for a signature?
  • Aaron Digulla
    Aaron Digulla almost 12 years
    If you change the length of the string, you need to insert/delete as many bytes after the string because the class reader will read the string and then expect the next valid item -> crash
  • krishnakumarp
    krishnakumarp almost 12 years
    please explain. I did not understand your question.
  • Antimony
    Antimony almost 12 years
    @Aaron, that is not correct. As long as you update the length field at the start of the string, everything will work fine. It's not like the classloader has magic offsets hardcoded in.
  • Aaron Digulla
    Aaron Digulla almost 12 years
    @Antimony: Duh. You're right of course. I don't know what I was thinking when I wrote this.
  • Marcin
    Marcin almost 10 years
    Sample tool for editing a compiled class is: sourceforge.net/projects/classeditor (you need to pick a .class file, it can't open jars).