Я пишу курсовую работу для университета. Задача — написать две программы, одна из которых собирает некоторую информацию, подписывает ее цифровым ключом и сохраняет зашифрованные данные и ключ в отдельные файлы. Следующая программа собирает ту же информацию, считывает зашифрованные данные и ключи и проверяет цифровую подпись.
Шаг номер два — написать агент, использующий java.lang.instrument.Instrumentation; и, возможно, сторонние библиотеки, такие как javaassist, для замены байт-кода, чтобы программа всегда выдавала успешный результат проверки.
Я пытался реализовать это с помощью туториалов, но моя программа застревает на этапе изменения.
Вот анализ байткода, который нужно изменить
static void checkSign(java.lang.String, java.lang.String, java.lang.String) throws java.lang.Exception;
Code:
0: new #206 // class java/io/BufferedReader
3: dup
4: new #258 // class java/io/FileReader
7: dup
8: aload_0
9: invokespecial #260 // Method java/io/FileReader."<init>":(Ljava/lang/String;)V
12: invokespecial #219 // Method java/io/BufferedReader."<init>":(Ljava/io/Reader;)V
15: astore_3
16: aload_3
17: invokevirtual #225 // Method java/io/BufferedReader.readLine:()Ljava/lang/String;
20: astore 4
22: invokestatic #262 // Method java/util/Base64.getDecoder:()Ljava/util/Base64$Decoder;
25: aload 4
27: invokevirtual #268 // Method java/util/Base64$Decoder.decode:(Ljava/lang/String;)[B
30: astore 5
32: aload_3
33: invokevirtual #245 // Method java/io/BufferedReader.close:()V
36: new #206 // class java/io/BufferedReader
39: dup
40: new #258 // class java/io/FileReader
43: dup
44: aload_1
45: invokespecial #260 // Method java/io/FileReader."<init>":(Ljava/lang/String;)V
48: invokespecial #219 // Method java/io/BufferedReader."<init>":(Ljava/io/Reader;)V
51: astore 6
53: aload 6
55: invokevirtual #225 // Method java/io/BufferedReader.readLine:()Ljava/lang/String;
58: astore 7
60: aload 6
62: invokevirtual #245 // Method java/io/BufferedReader.close:()V
65: invokestatic #262 // Method java/util/Base64.getDecoder:()Ljava/util/Base64$Decoder;
68: aload 7
70: invokevirtual #268 // Method java/util/Base64$Decoder.decode:(Ljava/lang/String;)[B
73: astore 8
75: ldc_w #274 // String RSA
78: invokestatic #276 // Method java/security/KeyFactory.getInstance:(Ljava/lang/String;)Ljava/security/KeyFactory;
81: astore 9
83: new #281 // class java/security/spec/X509EncodedKeySpec
86: dup
87: aload 8
89: invokespecial #283 // Method java/security/spec/X509EncodedKeySpec."<init>":([B)V
92: astore 10
94: aload 9
96: aload 10
98: invokevirtual #286 // Method java/security/KeyFactory.generatePublic:(Ljava/security/spec/KeySpec;)Ljava/security/PublicKey;
101: checkcast #290 // class java/security/interfaces/RSAPublicKey
104: astore 11
106: ldc_w #274 // String RSA
109: invokestatic #292 // Method javax/crypto/Cipher.getInstance:(Ljava/lang/String;)Ljavax/crypto/Cipher;
112: astore 12
114: aload 12
116: iconst_2
117: aload 11
119: invokevirtual #297 // Method javax/crypto/Cipher.init:(ILjava/security/Key;)V
122: aload 12
124: aload 5
126: invokevirtual #301 // Method javax/crypto/Cipher.doFinal:([B)[B
129: astore 13
131: new #17 // class java/lang/String
134: dup
135: aload 13
137: invokespecial #304 // Method java/lang/String."<init>":([B)V
140: astore 14
142: aload 14
144: aload_2
145: invokevirtual #237 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
148: ifeq 163
151: getstatic #10 // Field java/lang/System.out:Ljava/io/PrintStream;
154: ldc_w #305 // String Verification successfully!
157: invokevirtual #26 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
160: goto 171
163: getstatic #10 // Field java/lang/System.out:Ljava/io/PrintStream;
166: ldc #204 // String Verification failed!
168: invokevirtual #26 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
171: return
Это мой агент
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
public class AgentMain {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("Ha, ha! This program was hacked ;-)");
inst.addTransformer(new ClassFileTransformer() {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) {
if (className.equals("Defender")) {
try {
System.out.println("I'm here");
ClassPool cp = ClassPool.getDefault();
System.out.println("Class pool created");
CtClass cc = cp.get("Defender");
CtMethod method = cc.getDeclaredMethod("checkSign");
String newBody = "{" +
" System.out.println(\"Verification successfully!\");" +
"}";
method.setBody(newBody);
return cc.toBytecode();
} catch (Exception e) {
e.printStackTrace();
}
}
return classfileBuffer;
}
});
}
}
Я специально настроил вывод сообщения «Я здесь», чтобы понять, где ошибка.
Скомпилировав все классы в jar, я запустил его командой
java -javaagent:HackAgent.jar -jar Defender.jar
Но в результате я получаю вот это
Что я делаю не так?
Возможно, я что-то упускаю, но если цель состоит в том, чтобы «[сделать что-то] так, чтобы [вторая] программа всегда давала успешный результат проверки», зачем коду нужно что-то делать? Просто скажите: «Да, эти файлы проверены!» и выйти.
Я заметил, что «Созданный пул классов» никогда не выводился.
Как писал Дэвид, либо [A] код, который вы запускаете, не является тем кодом, который вы вставили, либо [B] код ClassPool.getDefault()
выдает исключение, и почему-то это не печатается, потому что вы перенаправили syserr в другое место. Похоже на вариант [А]: вы используете что-то другое. Вероятно, вы что-то написали, скомпилировали, заархивировали, изменили то, что написали, скомпилировали, не смогли заархивировать и теперь используете старый jar.
@Mike'Pomax'Kamermans Задача - манипулировать баткодом, чтобы обойти все проверки и отобразить результат
@DavidConrad Да, но я не знаю, почему на этапе получения ClassPool он просто застрял
@rzwitserloot Я все еще склоняюсь к тому, что агент работает некорректно, метод ClassPool.getDefault()
не работает, но ошибку по этому поводу не выдает. Единственное, что странно, это не улавливается try catch
Кроме того, установлено ли у вас значение Can-Retransform-Classes
в манифесте вашего агента?
@rzwitserloot По какой-то полезной причине Java молча игнорирует Throwables, брошенные трансформерами. В коде Exception
улавливаются, а другие Trowable
, такие как Error
, — нет. ClassNotFoundError
тут приходит на ум.
Попытка получить класс через ClassPool.getDefault()
(без дополнительной настройки) не является правильным решением, поскольку вы просто перехватываете механизм загрузки классов по умолчанию. Вы должны использовать определение класса, полученное в параметре classfileBuffer
. См. ByteArrayClassPath . Обычно я бы использовал библиотеку ASM; это более низкий уровень и требует большего понимания, но именно такое понимание того, что происходит, необходимо для таких задач. Javassist только делает вид, что все просто.
Хотя конкретная проблема может быть намного проще. Как упомянул @JohannesKuhn, вы не ловите ClassNotFoundError
(и возможно, вы все равно не сможете его поймать, например, когда верификатор уже выходит из строя до того, как ваш метод будет введен). И, насколько мы видим в вашей командной строке, в пути к классам Агента нет Javassist…
Этот вопрос явно является продолжением того вопроса, который был закрыт, поскольку вы представили только исходный код, но не объяснили, что вы пытались решить проблему. Здесь все наоборот. В любом случае, объединив оба вопроса, я вижу, что вы попробовали что-то сами, а не просто ищете кого-то, кто сделает всю работу за вас, и это похвально. Итак, хотя это не Javassist, а ответ AspectJ, я хочу показать вам, как решить проблему. Я попробовал использовать ваш исходный код.
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class DefenderAspect {
@Around("cflow(execution(void checkSign(String, String, String)) && args(*, *, hash)) && call(String.new(byte[]))")
public String replaceDecryptedSignString(String hash) {
return hash;
}
}
Pointcut сообщает AspectJ выбирать вызовы конструктора String(byte[])
, если они находятся в потоке управления метода checkSign
. Он также связывает третий параметр hash
с аргументом метода рекомендации и использует его для простого возврата того, что должен вернуть вызов конструктора, чтобы удовлетворить условие dectyptedSignString.equals(hash)
в вашем операторе if
.
Ваш пример кода теперь создает этот журнал консоли:
...
General information:
Author: Artem Lebid
Number in student list: 9
Task number: 9
Task description: Program Defender
...
MD5 Hash: 19ef14ef0778ac786cd0a32e83b5ab19
Verification successfully!
...
Это решение работает как с переплетением после компиляции, так и во время загрузки.
Конечно, как я уже сказал в своем комментарии, вы можете разработать аналогичное решение с помощью ASM, Byte Buddy, BCEL, Javassist или подобных фреймворков. AspectJ — мой выбор, потому что мне нравится его элегантный синтаксис.
Спасибо, ваше решение было полезным, но я также смог сделать это через Java-агент. Как я писал @Johannes Kuhn, действительно моя проблема заключалась в том, что агенту необходимо перехватывать Throwable, а не простые Exceptions
Я не понимаю вашего комментария. В моем ответе уже сказано, что он работает как для посткомпиляции, так и для плетения во время загрузки. Последний (LTW) через AspectJ является Java-агентом, так что же нового или нового в вашем (неописанном) решении?
JavaScript — это не Java.