15

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
  • 40,403
  • 57
  • 174
  • 291
  • There are a couple of bytecode manipulation libraries, like ASM and BCEL, that allow you to tweak classfiles to your liking. The better solution, IMO, is to extract the constant as a property and go through the inconvenience of recompilation the one time necessary for the extraction. – Nathan Ryan May 21 '12 at 08:52
  • @NathanD.Ryan I'm certainly going to extract it and put it into a config file for the future, but in this specific case it'd be very inconvenient to recompile the deployed version, which is rather old. – Bart van Heukelom May 21 '12 at 08:54
  • Is the string in question a compile-time constant? – Nathan Ryan May 21 '12 at 08:58
  • Okay, since you can see the string in a hex editor, it's in the constant pool of the classfile. Is this string limited to this particular class? If not, it could be inlined elsewhere. – Nathan Ryan May 21 '12 at 09:07
  • @NathanD.Ryan It is inlined, but only once, so I only need to edit the location where it is. – Bart van Heukelom May 21 '12 at 09:42
  • 1
    I'm almost 100% sure this should work, then, as long as the strings are the same length (and UTF8). Classfile constants are indexed, and it is the index that is referenced from within the classfile. Since the string is isolated to this one classfile, it should be trivial to make the change and run a test. – Nathan Ryan May 21 '12 at 09:48
  • If it is a public constant, all importing classes copied the constant inside their own class, so you have to clean+recompile them too. But I take it, the use case is for some internal usage like license key or whatever. – Joop Eggen Sep 05 '18 at 08:36

5 Answers5

8

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.

Aaron Digulla
  • 297,790
  • 101
  • 558
  • 777
  • 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? – Bart van Heukelom May 21 '12 at 09:47
  • 2
    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 – Aaron Digulla May 21 '12 at 14:24
  • 3
    @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. – Antimony Jun 17 '12 at 05:34
  • @Antimony: Duh. You're right of course. I don't know what I was thinking when I wrote this. – Aaron Digulla Jun 18 '12 at 15:52
5

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.

Martin Thompson
  • 15,738
  • 1
  • 36
  • 52
Antimony
  • 33,711
  • 9
  • 88
  • 96
  • Sample tool for editing a compiled class is: http://sourceforge.net/projects/classeditor/ (you need to pick a .class file, it can't open jars). – Marcin Jul 14 '14 at 14:31
3

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.

krishnakumarp
  • 7,666
  • 2
  • 44
  • 51
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

0

I had a similar issue in the past. My solution was to use one of the mentioned bytecode engineering libraries. I could not find javaassist, however there is a great tool called dirtyJOE that allows you (among many things) to edit constants in your .class file.

Here is a screenshot

You just import the .class file and click on the constant

You just import the .class file and click on the constant

hjbello
  • 607
  • 4
  • 16