Изменение строковой константы в скомпилированном классе

Мне нужно изменить строковую константу в развернутой программе 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" из-за встроенного компилятора.

Надеюсь, что это поможет.