Больше месяца назад автор статьи Decrypting Java Malware using ASM прислал мне оригинальный текст на русском языке для публикации в блоге, с целью донести широкому кругу лиц определенные аспекты защиты программ написанных на java от действий недоброжелателей. Автор статьи — разработчик, который долгое время занимается разработкой средств для противодействия реверс-инжирингу и взлому ПО, специалист в области информационной безопасности.
К сожалению, в связи с большой загруженностью (казалось бы лето, все должны быть в отпусках, а нет — работы только привалило), никак не мог заняться блогом и выложить эту статью.
Исправляюсь, надеюсь статья всё еще актуальна.
В настоящее время очень популярны Java Malware, использующие уязвимости в Java. Многие из них для скрытия своих действий используют шифрование строк. Как правило в них могут храниться ключи реестра, команды для запуска различных программ и т.д.
В этой статье мы рассмотрим самый распространенный метод шифрования строк в Java Malware и напишем утилиту для снятия такой защиты.
1) Небольшое введение в формат байткода class-файлов.
Для дальнейшего понимания, необходимо знать следующие вещи — все константы (строки/примитивные типы) хранятся в специальной структуре внути class-файла, которая называется Constant Pool и для получения элемента из Constant Pool используется инструкция ldc.
Посмотрим как выглядит байт код вызова метода System.out.println(«HelloWorld»);
Пишем простой класс
public class Hello { public static void main(String[] args) throws Exception { System.out.println("HelloWorld"); } } |
Компилируем
javac Hello.java
Декомпилируем с помощью javap
javap -c -v Hello
ConstanPool:
... const #3 = String #20; // HelloWorld const #20 = Asciz HelloWorld; 3: ldc #3; //String HelloWorld 5: invokevirtual #4; //Method java/io/PrintStream.println:(Ljava/lang/String;)V |
2) Dark side
Для шифрования строк авторы Malware обычно применяют следующий подход:
— шифруют строку с помощью некоторой функции
— заменяют исходное значение строки на зашифрованное
— перед использование строки вызывают функцию расшифрования
Вот как выглядит байткод после применения такого подхода:
ConstanPool:
... const #3 = String #20; // CryptedString const #20 = Asciz CryptedString; 3: ldc #3; //String CryptedString 5: invokestatic #4; //Method decrypt:(Ljava/lang/String;)Ljava/lang/String; 8: invokevirtual #5; //Method java/io/PrintStream.println:(Ljava/lang/String;)V |
3) Джедай side
Для написания автоматического расшифровщика нам понадобится ASM. Основная идея нашего подхода — грузим class, содержащий функцию дешифрования, заменяем зашифрованные строки на расшифрованные и вырезаем инструкцию вызова функции дешифрования из байткода.
Главный метод:
// input file JarFile jin = new JarFile(new File(args[0]), false); // output file JarOutputStream jon = new JarOutputStream(new FileOutputStream(args[1])); Enumeration en = jin.entries(); byte[] buffer = new byte[1024]; URLClassLoader jcl = new URLClassLoader(new URL[]{(new File(args[0])).toURI().toURL()}); Method decMethod = jcl.loadClass(decClassName).getDeclaredMethod(decMethoName, new Class[]{String.class}); while (en.hasMoreElements()) { JarEntry je = en.nextElement(); if (je.isDirectory()) { jon.putNextEntry(new JarEntry(je)); } else if (je.getName().endsWith(".class")) { System.out.println("Processing class " + je.getName()); ClassReader cr = new ClassReader(jin.getInputStream(je)); ClassWriter cw = new ClassWriter(0); Decryptor transformer = new Decryptor(cw, decMethod); cr.accept(transformer, 0); jon.putNextEntry(new JarEntry(je.getName())); jon.write(cw.toByteArray()); } else { jon.putNextEntry(new JarEntry(je)); BufferedInputStream bis = new BufferedInputStream(jin.getInputStream(je)); int readed = bis.read(buffer); while (readed > 0) { jon.write(buffer, 0, readed); readed = bis.read(buffer); } bis.close(); } } jon.close(); |
ASM часть:
class Decryptor extends ClassVisitor implements Opcodes { Method decMethod; public LCDec(ClassVisitor cv, Method decMethod) throws Exception { super(ASM4, cv); this.decMethod = decMethod; } @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions); return new MethodDecryptor(mv, decMethod); } class MethodDecryptor extends MethodVisitor implements Opcodes { Method decMethod; String decClassName; String decMethodName; public MethodDecryptor(MethodVisitor mv, Method decMethod) { super(ASM4, mv); this.decMethod = decMethod; decClassName = decMethod.getDeclaringClass().getName().replace(".","/"); decMethodName = decMethod.getName(); } @Override public void visitMethodInsn(opcode, owner, name, desc) { if (decClassName.equals(owner) && decMethodName.equals(decMethodName) && "(Ljava/lang/String;)Ljava/lang/String;".equals(desc)) { } else { super.visitMethodInsn(opcode, owner, name, desc); } } @Override public void visitLdcInsn(Object o) { if (o instanceof String) { try { o = decMethod.invoke(null, new Object[]{o}); } catch (Exception ex) { } } super.visitLdcInsn(o); } } } |
Выводы:
Cтатическое шифрование строк не обеспечивает никакой защиты.
ASM очень мощный инструмент для манипулирования байт-кодом.
Guys не пишите malware пишите лучше полезные open-source программы.