Изменение строковой константы в скомпилированном классе
Мне нужно изменить строковую константу в развернутой программе Java, т.е. значение внутри скомпилированных файлов .class
. Его можно перезапустить, но не легко перекомпилировать (хотя это неудобно, если этот вопрос не дает ответов). Возможно ли это?
Обновление: я просто посмотрел на файл с шестнадцатеричным редактором, и похоже, что я могу легко изменить строку там. Будет ли это работать, т.е. Не приведет к аннулированию какой-либо подписи файла? Старая и новая строка являются как буквенно-цифровыми, так и могут иметь одинаковую длину при необходимости.
Обновление 2: я исправил его. Поскольку конкретный класс, который мне нужно изменить, очень мал и не изменился в новой версии проекта, я мог бы просто скомпилировать его и взять новый класс оттуда. Все еще интересуется ответом, который не связан с компиляцией, хотя в образовательных целях.
Ответы
Ответ 1
Если у вас есть источники для этого класса, то мой подход:
- Получить файл JAR
- Получить источник для одного класса
- Скомпилируйте источник с JAR в пути к классам (таким образом, вам не нужно компилировать что-либо еще, это не значит, что JAR уже содержит двоичный файл). Вы можете использовать последнюю версию Java для этого; просто снимите компилятор с помощью
-source
и -target
.
- Замените файл класса в JAR на новый с помощью
jar u
или Ant task
Пример задачи 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.
Вы должны сохранить длину; иначе загрузчик классов будет запутан.
Ответ 2
Единственными дополнительными данными, необходимыми при изменении строки (технически для элемента Utf8) в пуле констант, является поле длины (2 байта большого конца, предшествующего данным). Нет дополнительных контрольных сумм или смещений, требующих модификации.
Есть два оговорки:
- Строка может использоваться в других местах. Например, "Код" используется для атрибута кода метода, поэтому его изменение разбивает файл.
- Строка хранится в формате Modified Utf8. Таким образом, нулевые байты и символы Unicode вне базовой плоскости кодируются по-разному. Поле длины - это количество байтов, а не символов, и оно ограничено 65535.
Если вы планируете сделать это много, лучше получить инструмент редактора файлов классов, но шестнадцатеричный редактор полезен для быстрых изменений.
Ответ 3
Недавно я написал собственный калькулятор 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 исходным кодом
Ответ 4
Вы можете изменить .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" из-за встроенного компилятора.
Надеюсь, что это поможет.