Change string constant in a compiled class
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.
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, 2022Comments
-
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 almost 12 yearsI 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 almost 12 yearsWhat does it mean for a signature?
-
Aaron Digulla almost 12 yearsIf 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 almost 12 yearsplease explain. I did not understand your question.
-
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 almost 12 years@Antimony: Duh. You're right of course. I don't know what I was thinking when I wrote this.
-
Marcin almost 10 yearsSample tool for editing a compiled class is: sourceforge.net/projects/classeditor (you need to pick a .class file, it can't open jars).