Изменить строковую константу в скомпилированном классе

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

Вы просто импортируете файл.class и нажимаете на константу

Другие вопросы по тегам