Изменить строковую константу в скомпилированном классе
Мне нужно изменить строковую константу в развернутой Java-программе, т.е. значение внутри скомпилированного .class
-файлы. Его можно перезапустить, но его нелегко перекомпилировать (хотя это неудобный вариант, если на этот вопрос нет ответов). Это возможно?
Обновление: я только что посмотрел на файл с помощью шестнадцатеричного редактора, и похоже, что я могу легко изменить строку там. Будет ли это работать, то есть не сделает ли это недействительной какую-то подпись файла? Старая и новая строки являются буквенно-цифровыми и при необходимости могут иметь одинаковую длину.
Обновление 2: я исправил это. Поскольку конкретный класс, который мне нужно было изменить, очень мал и не изменился в новой версии проекта, я мог бы просто скомпилировать его и взять новый класс оттуда. Все еще интересует ответ, который не включает компиляцию, хотя, в образовательных целях.
5 ответов
Если у вас есть источники для этого класса, то мой подход:
- Получить файл JAR
- Получить источник для одного класса
- Скомпилируйте исходный код с помощью JAR на пути к классам (таким образом, вам не нужно ничего компилировать; не повредит, что JAR уже содержит двоичный файл). Вы можете использовать последнюю версию Java для этого; просто понизить компилятор, используя
-source
а также-target
, - Замените файл класса в JAR новым, используя
jar u
или задача муравья
Пример для задачи 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 нет контрольной суммы.
Вы должны сохранить длину; в противном случае загрузчик классов будет сбит с толку.
Единственные дополнительные данные, которые требуются при изменении строки (технически элемент Utf8) в пуле констант, это поле длины (2 байта с прямым порядком байтов перед данными). Нет никаких дополнительных контрольных сумм или смещений, которые требуют модификации.
Есть две оговорки:
- Строка может быть использована в других местах. Например, "Код" используется для атрибута кода метода, поэтому изменение его приведет к повреждению файла.
- Строка хранится в формате Modified Utf8. Таким образом, нулевые байты и символы Юникода вне базовой плоскости кодируются по-разному. Поле длины представляет собой количество байтов, а не символов, и ограничено 65535.
Если вы планируете делать это много, лучше воспользоваться инструментом редактирования файлов классов, но шестнадцатеричный редактор полезен для быстрых изменений.
Недавно я написал свой собственный картограф ConstantPool, потому что у ASM и JarJar были следующие проблемы:
- Чтобы замедлить
- Не поддерживает переписывание без всех зависимостей классов
- Не поддерживает потоковую передачу
- Не поддерживает Remapper в режиме Tree API
- Пришлось расширить и свернуть 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
Вы можете изменить.class, используя множество библиотек инженерных байт-кодов. Например, используя javaassist.
Однако, если вы пытаетесь заменить статический конечный элемент, он может не дать желаемого эффекта, потому что компилятор будет вставлять эту константу везде, где он используется.
Пример кода с использованием 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);
}
}
}
В этом случае программа успешно изменяет значение HELLO с "Hello" на "Hell". Однако, когда вы запускаете класс ConstantHolder, он все равно выдает "Value:Hello" из-за встраивания компилятором.
Надеюсь, поможет.
У меня была похожая проблема в прошлом. Мое решение состояло в том, чтобы использовать одну из упомянутых инженерных библиотек байт-кода. Я не смог найти javaassist, однако есть отличный инструмент dirtyJOE, который позволяет вам (среди многих вещей) редактировать константы в вашем файле.class.
Вот скриншот
Вы просто импортируете файл.class и нажимаете на константу