Больше месяца назад автор статьи 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 программы.