Перейти к содержимому

Как декомпилировать jar в intellij idea

  • автор:

Декомпилятор Intellij

Есть проект с кучей классов и надо его декомпилировать, однако вручную с каждым классом возиться (1000+ классов) весьма затруднительно. Нельзя ли каким то образом заставить intellij декомпилить все сразу? просто сроки поджимают

Отслеживать
задан 25 июл 2017 в 17:05
Алексей Серебряков Алексей Серебряков
21 1 1 бронзовый знак

1 ответ 1

Сортировка: Сброс на вариант по умолчанию

  1. Сбилдите идеевский декомпилятор fernflower
  2. Запустите java -jar fernflower.jar

Результат будет лежать в папочке

Отслеживать
ответ дан 25 июл 2017 в 17:21
Дмитрий Игоревич Дмитрий Игоревич
271 1 1 серебряный знак 3 3 бронзовых знака

  • java
  • intellij-idea
  • jar
    Важное на Мете
Похожие

Подписаться на ленту

Лента вопроса

Для подписки на ленту скопируйте и вставьте эту ссылку в вашу программу для чтения RSS.

Дизайн сайта / логотип © 2024 Stack Exchange Inc; пользовательские материалы лицензированы в соответствии с CC BY-SA . rev 2024.1.3.2953

Нажимая «Принять все файлы cookie» вы соглашаетесь, что Stack Exchange может хранить файлы cookie на вашем устройстве и раскрывать информацию в соответствии с нашей Политикой в отношении файлов cookie.

Декомпиляция Java

Такое вообще возможно?
Я с жавой почти не знаком, но есть академический интерес заглянуть внутрь одной проги.
Пробовал JD и что то еще — на выходе куча файлов а.сlass, a$a.class, a$a$a.class и т.д.
Часть пустые, в других белиберда.
Это я что-то не так делаю?

Компилировать обратно не надо.

yirk ★★★
23.09.16 19:02:52 MSK

Если скомпилированные файлы не обфусцированы — не просто возможно, но и очень легко, исходники восстанавливаются практически вплоть до имён переменных (а если есть отладочная информация, то и с ними).

Legioner ★★★★★
( 23.09.16 19:05:46 MSK )
Последнее исправление: Legioner 23.09.16 19:06:17 MSK (всего исправлений: 1)

Ответ на: комментарий от Legioner 23.09.16 19:05:46 MSK

А как узнать, обфусцированы или нет?

yirk ★★★
( 23.09.16 19:07:19 MSK ) автор топика

А ты думал, что тебе из jar исходники нормально восстановятся? Лучше всего декомпилировать при помощи плагина в intellij idea. Но всё равно нормального исходника ты не получишь, + очень мало программ можно декомпилировать не нарушая лицензию.

peregrine ★★★★★
( 23.09.16 19:08:28 MSK )
Ответ на: комментарий от yirk 23.09.16 19:07:19 MSK

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

Midael ★★★★★
( 23.09.16 19:12:43 MSK )

Это ProGuard скорей всего, оно читабельно. Но если проект большой и классов много, то ты просто с ума сойдешь во всех этих a-классах.
Вот что нашлось, не факт что может и оно вообще работает.

anonymous_sama ★★★★★
( 23.09.16 19:19:34 MSK )
Последнее исправление: anonymous_sama 23.09.16 19:24:52 MSK (всего исправлений: 1)

ищите слитые версии в гугле или демку пробуйте https://www.pnfsoftware.com/jeb2/

anonymous
( 23.09.16 21:15:57 MSK )

Возможно. Из того что более-менее реально работает — cfr/jd (из гита).

а.сlass — почищены имена классов. Если внутри белиберда — скорее всего декомпилятор не умееет новые конструкции, или обфускация.

vasily_pupkin ★★★★★
( 23.09.16 21:28:49 MSK )

очень хорошо восстанавливаются class файлы скомпилированные 1.4.2 и меньше. 1.5, 1.6 и 1.7 хуже. сложные 1.8 с лямдами очень плохо. лучше jd к сожалению ничего нет.

vtVitus ★★★★★
( 23.09.16 21:35:38 MSK )

iZEN ★★★★★
( 23.09.16 23:04:31 MSK )

Поздравляю, то что ты декомпилишь пропустили через обфускатор, логику работы понять до сих пор возможно, но разбирать все придется руками. Если с жабой (и косяками её декомплеров) не знаком, то вряд ли что-то поймешь.

ya-betmen ★★★★★
( 23.09.16 23:20:17 MSK )
Ответ на: комментарий от peregrine 23.09.16 19:08:28 MSK

Пункт три. В общем-то можно, в зависимости от намерений.

a1batross ★★★★★
( 23.09.16 23:32:01 MSK )
Последнее исправление: a1batross 23.09.16 23:33:44 MSK (всего исправлений: 1)

JD херня, не? fernflower встроен теперь в идею, можно и отдельно собрать и заюзать.

migesok ★
( 25.09.16 01:17:54 MSK )
Ответ на: комментарий от a1batross 23.09.16 23:32:01 MSK

Все намного проще. Если об этом никто не узнает, то можно декомпилировать что угодно, как угодно и для чего угодно.

anonymous
( 25.09.16 09:59:04 MSK )

Посоветую Процион, самый продвинутый из доступных, на мой взгляд.

ruzisufaka ★
( 25.09.16 13:21:26 MSK )
Ответ на: комментарий от peregrine 23.09.16 19:08:28 MSK

Лучше всего декомпилировать при помощи плагина в intellij idea.

Кто это сказал? Fernflower не особо фичастый декомпилятор, есть ещё CFR и JADX.

EXL ★★★★★
( 25.09.16 14:13:41 MSK )
Ответ на: комментарий от migesok 25.09.16 01:17:54 MSK

Берём идейку, fernflower и рандомное, простое Java-приложение.

java -jar fernflower.jar -dgs=true ../MIDlets-JARs/stack_attack_2_pro_color_915.jar ../MIDlets-JARs/StackAttack-Decompiled/

Открываем класс a.java:

. public static int a(String param0, d param1) < // $FF: Couldn't be decompiled >public static String a(int param0, String param1) < // $FF: Couldn't be decompiled >. 

Ладно. Берём в правую руку старый-добрый JD, декомпилируем тот же самый мидлет, открываем тот же самый класс:

. public static int a(String paramString, d paramd) < System.gc(); int m = 0; if (paramd.d(paramString) == true) < return 0; >HttpConnection localHttpConnection = null; InputStream localInputStream = null; StringBuffer localStringBuffer = new StringBuffer(""); String str2 = c(paramString); try < str3 = System.getProperty("com.siemens.IMEI").trim(); str4 = "http://139.23.38.140/hs/user.asp?imei=" + str3 + "&nick=" + str2 + "&appid=001AEE0A&mnc=" + System.getProperty("com.siemens.MNC") + "&mcc=" + System.getProperty("com.siemens.MCC"); int i1 = 256; localInputStream = (localHttpConnection = (HttpConnection)Connector.open(str4)).openInputStream(); int i2; while (((i2 = localInputStream.read()) != -1) && (i1 >0)) < localStringBuffer.append((char)i2); i1--; >if (i1 == 0) < int i3 = -3; return i3; >> catch (Exception localException5) < m = -3; >finally < try < if (localInputStream != null) < localInputStream.close(); >> catch (Exception localException8) <> try < if (localHttpConnection != null) < localHttpConnection.close(); >> catch (Exception localException9) <> System.gc(); > if (m == -3) < return -3; >String str1 = localStringBuffer.toString(); String str3 = "registration successful"; String str4 = "alternative nick:"; if (str1.startsWith(str4)) < n = b(str1.substring(str4.length())); return -2; >if (str1.compareTo(str3) == 0) < paramd.b(paramString); paramd.a(); return 0; >return -1; > public static String a(int paramInt, String paramString) < System.gc(); paramString = paramString.trim(); HttpConnection localHttpConnection = null; InputStream localInputStream = null; StringBuffer localStringBuffer = new StringBuffer(""); String str2 = c(paramString); for (String str3 = Integer.toHexString(paramInt ^ 0xFEC17A11); str3.length() < 8; str3 = "0" + str3) <>int m = 0; try < String str4 = System.getProperty("com.siemens.IMEI").trim(); localInputStream = (localHttpConnection = (HttpConnection)Connector.open(str5 = "http://139.23.38.140/hs/hs.asp?appid=001AEE0A&imei=" + str4 + "&nick=" + str2 + "&hs=" + str3)).openInputStream(); int i1; while ((i1 = localInputStream.read()) != -1) < localStringBuffer.append((char)i1); m++; >> catch (Exception localException3) < String str5 = "Broken"; return str5; >finally < try < if (localInputStream != null) < localInputStream.close(); >> catch (Exception localException6) <> try < if (localHttpConnection != null) < localHttpConnection.close(); >> catch (Exception localException7) <> > if (m > 260) < return "Broken"; >String str1 = localStringBuffer.toString(); System.gc(); return str1; > . 

Та-дам! Всё декомпилировалось как нужно.

Закрываем идейку и их говноподелку fernflower и выбрасываем её на помойку.

EXL ★★★★★
( 25.09.16 14:28:23 MSK )
Ответ на: комментарий от EXL 25.09.16 14:28:23 MSK

Воу, братан, кинь джарку или подскажи, где достать? Меня тема дико интересует, хочу понять, что там такого хитрого, что fernflower не понимает.

Я как-то пытался обмануть fernflower разными хитро подстроенными класс-файлами, но пока он меня громил в сухую. Ржака была, когда я составил метод с ациклическим control-flow графом не сводимым напрямую к if-ам, а он выплюнул пачку labeled-break-ов из блоков. Я даже не знал, что в Java так можно.

migesok ★
( 25.09.16 18:28:45 MSK )
Ответ на: комментарий от migesok 25.09.16 18:28:45 MSK

Не знаю, чего там fernflower не понимает, но говорит он при декомпиляции следующее:

INFO: Decompiling class a WARN: Heavily obfuscated exception ranges found! ERROR: Method a (Ljava/lang/String;Ld;)I couldn't be decompiled. java.lang.RuntimeException: parsing failure! at org.jetbrains.java.decompiler.modules.decompiler.DomHelper.parseGraph(DomHelper.java:217) at org.jetbrains.java.decompiler.main.rels.MethodProcessorRunnable.codeToJava(MethodProcessorRunnable.java:122) at org.jetbrains.java.decompiler.main.rels.ClassWrapper.init(ClassWrapper.java:86) at org.jetbrains.java.decompiler.main.ClassesProcessor.initWrappers(ClassesProcessor.java:303) at org.jetbrains.java.decompiler.main.ClassesProcessor.writeClass(ClassesProcessor.java:250) at org.jetbrains.java.decompiler.main.Fernflower.getClassContent(Fernflower.java:87) at org.jetbrains.java.decompiler.struct.ContextUnit.save(ContextUnit.java:147) at org.jetbrains.java.decompiler.struct.StructContext.saveContext(StructContext.java:72) at org.jetbrains.java.decompiler.main.Fernflower.decompileContext(Fernflower.java:54) at org.jetbrains.java.decompiler.main.decompiler.ConsoleDecompiler.decompileContext(ConsoleDecompiler.java:135) at org.jetbrains.java.decompiler.main.decompiler.ConsoleDecompiler.main(ConsoleDecompiler.java:96) WARN: Heavily obfuscated exception ranges found! ERROR: Statement cannot be decomposed although reducible! ERROR: Method a (ILjava/lang/String;)Ljava/lang/String; couldn't be decompiled. java.lang.RuntimeException: parsing failure! at org.jetbrains.java.decompiler.modules.decompiler.DomHelper.parseGraph(DomHelper.java:217) at org.jetbrains.java.decompiler.main.rels.MethodProcessorRunnable.codeToJava(MethodProcessorRunnable.java:122) at org.jetbrains.java.decompiler.main.rels.ClassWrapper.init(ClassWrapper.java:86) at org.jetbrains.java.decompiler.main.ClassesProcessor.initWrappers(ClassesProcessor.java:303) at org.jetbrains.java.decompiler.main.ClassesProcessor.writeClass(ClassesProcessor.java:250) at org.jetbrains.java.decompiler.main.Fernflower.getClassContent(Fernflower.java:87) at org.jetbrains.java.decompiler.struct.ContextUnit.save(ContextUnit.java:147) at org.jetbrains.java.decompiler.struct.StructContext.saveContext(StructContext.java:72) at org.jetbrains.java.decompiler.main.Fernflower.decompileContext(Fernflower.java:54) at org.jetbrains.java.decompiler.main.decompiler.ConsoleDecompiler.decompileContext(ConsoleDecompiler.java:135) at org.jetbrains.java.decompiler.main.decompiler.ConsoleDecompiler.main(ConsoleDecompiler.java:96) INFO: . done 

Вот только откуда там Heavily obfuscated exception ranges found — я не знаю.

IntelliJ Idea : Декомпиляция, Компиляция, Субституция (или как править чужие ошибки)

Java-университет

«Да не изобрети ты велосипед» — одно из главных правил успешной и эффективной работы. Но что делать, когда свой велосипед изобретать не хочется, а у чужого руль оказался кривой, а колёса квадратными? Данный обзор предназначен для по возможности краткого ознакомления с приёмом исправления в чужих библиотеках «на крайний случай» и о том, как это дело распространить дальше своего компьютера.

IntelliJ Idea : Декомпиляция, Компиляция, Субституция (или как править чужие ошибки) - 1

Введение

  • Подготовить испытуемое приложение для примера (на примере Hibernate проекта)
  • Поиск изменяемого места
  • Выполнение изменения
  • Разворачивание репозитория

Подготовка испытуемого

Итак, нам нужен подопытный проект. Нам идеально подойдёт Hibernate, т.к. это «стильно, модно, современно». Не буду особо вдаваться в детали, т.к. статья не про Hibernate. Будем делать всё быстро и по делу. И будем, как правильные разработчики , использовать систему сборки. Например, нам так же подойдёт Gradle, который для данной статьи должен быть установлен (https://gradle.org/install/). Сначала, нам нужно создать проект. У Maven’а для этого есть архетипы, а у Gradle для этого есть особенный плагин: Gradle Init. Итак, открываем командную строку любым известным вам способом. Создаём каталог для проекта, переходим в него и выполняем комманду:

 mkdir javarush cd javarush gradle init --type java-application 

IntelliJ Idea : Декомпиляция, Компиляция, Субституция (или как править чужие ошибки) - 2

Прежде чем выполнять импорт проекта внесём некоторые изменения в файл, описывающий, каким образом нужно выполнять сборку. Этот файл называется build script’ом и имеет имя build.gradle. Находится он в том каталоге, в котором мы выполнили gradle init. Поэтому, просто открываем его (например, в windows командой start build.gradle). Находим там блок «dependencies», т.е. зависимости. Тут описываются все сторонние jar, которые мы будем использовать. Теперь надо понять, что тут описывать. Перейдём на сайт Hibernate (http://hibernate.org/). Нас интересует Hibernate ORM. Нам нужен последний релиз. В меню слева есть подраздел «Releases». Выбираем «latest stable». Проматываем вниз и находим «Core implementation (includes JPA)». Раньше нужно было поддержку JPA подключать отдельно, но теперь всё стало проще и достаточно только одной зависимости. Также нам понадобится при помощи Hibernate работать с базой данных. Для этого возьмём самый простой вариант – H2 Database. Выбор сделан, вот наши зависимости:

 dependencies < // Базовая зависимость для Hibernate (новые версии включают и JPA) compile 'org.hibernate:hibernate-core:5.2.17.Final' // База данных, к которой мы будем подключаться compile 'com.h2database:h2:1.4.197' // Use JUnit test framework testCompile 'junit:junit:4.12' > 

Отлично, что дальше? Надо настроить Hibernate. У Hibernate есть «Getting Started Guide», но он дурацкий и больше мешает, чем помогает. Поэтому пойдём как правильные люди сразу в «User Guide». В оглавлении видим раздел «Bootstrap», что переводится как «Начальная загрузка». То что надо. Там написано много умных слов, но смысл в том, что на classpath должен быть каталог META-INF, а там файл persistence.xml. На classpath по стандарту попадает каталог «resources». Поэтому создаём указанный каталог: mkdir src\main\resources\META-INF Создаём там файл persistence.xml и открываем его. Там же в документации есть пример «Example 268. META-INF/persistence.xml configuration file» из которого мы возьмём содержимое и вставим в файл persistence.xml. Далее запускаем IDE и импортируем в неё наш созданный проект. Теперь нам нужно что-то сохранять в базу. Это что-то называется сущности. Сущности представляют что-то из так называемой доменной модели. И в оглавлении, о чудо, видим «2. Domain Model». Спускаемся ниже по тексту и видим в главе «2.1. Mapping types» простой пример сущности. Забираем его к себе, чуть сократив:

 package entity; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; @Entity(name = "Contact") public class Contact < @Id @GeneratedValue private Integer id; private String name; public Contact(String name) < this.name = name; >> 

Теперь у нас появился класс, представляющий сущность. Вернёмся в persistence.xml и поправим там одно место: Там где указан class укажем свой класс entity.Contact . Отлично, осталось запуститься. Возвращаемся в главу «Bootstrap». Так как у нас нет сервера приложений, который нам предоставит особое EE окружение (т.е. окружение, которое реализует для нас определённое поведение системы), то мы работаем в SE окружении. Для него нам подойдёт только пример «Example 269. Application bootstrapped EntityManagerFactory». Например, сделаем так:

 public class App < public static void main(String[] args) < EntityManagerFactory emf = Persistence.createEntityManagerFactory("CRM"); EntityManager em = emf.createEntityManager(); em.getTransaction().begin(); Contact contact = new Contact("Vasya"); em.persist(contact); em.getTransaction().commit(); Query sqlQuery = em.createNativeQuery("select count(*) from contact"); BigInteger count = (BigInteger) sqlQuery.getSingleResult(); emf.close(); System.out.println("Entiries count: " + count); >> 

Ура, наш испытуемый готов. Эту часть я не хотел опускать, т.к. для следующих глав желательно понимать то, как появился наш испытуемый.

Поиск изменяемого поведения

Давайте встанем на место инициализации поля count типа BigInteger и поставим там точки останова (BreakPoint). Встав на нужной строке это можно сделать при помощи Ctrl+F8 или через меню Run -> Toggle Line Breakpoint. После чего запускаем наш main метод в дебаге (Run -> Debug):

IntelliJ Idea : Декомпиляция, Компиляция, Субституция (или как править чужие ошибки) - 3

Немного неуклижий пример, но, допустим, мы хотим изменить количество query spaces при старте. Как мы видим, наш sqlQuery это NativeQueryImpl. Нажимаем Ctrl+N , пишем название класса, переходим в него. Чтоб при переход в класс нас перебрасывало на то место, где лежит этот класс включил автоскрол:

IntelliJ Idea : Декомпиляция, Компиляция, Субституция (или как править чужие ошибки) - 4

Сразу заметим, что Idea не знает сейчас, где можно найти исходный код программы (исходники, то есть). Поэтому она милостиво декомпилировала для нас из class файла содержимое:

IntelliJ Idea : Декомпиляция, Компиляция, Субституция (или как править чужие ошибки) - 5

Заметим так же, что в заголовке окна IntelliJ Idea пишется, где Gradle сохраняет для нас артефакт. Теперь, получим в Idea путь, где лежит наш артефакт:

IntelliJ Idea : Декомпиляция, Компиляция, Субституция (или как править чужие ошибки) - 6

Перейдём в этот каталог в командной строке при помощи команды cd путь к каталогу . Сразу сделаю замечание: если есть возможность собрать проект из исходников, лучше собирать из исходников. Например, исходный код Hibernate доступен на официальном сайте. Лучше забрать его для нужной версии и сделать все изменения там и собраться при помощи скриптов сборки, которые указаны в проекте. Я же привожу в статье самый ужасный вариант – есть jar, а исходного кода нет. И замечание номер 2: Gradle может получить исходный код при помощи плагинов. Подробнее см. «How to download javadocs and sources for jar using Gradle.

Выполнение изменения

Нам нужно воссоздать структуру каталогов в соответствии с тем, в каком пакете лежит изменяемый нами класс. В данном случае: mkdir org\hibernate\query\internal , после чего создаём в этом каталоге файл NativeQueryImpl.java . Теперь открываем данный файл и копируем туда всё содержимое класса из IDE (то самое, которое для нас декомпилировала Idea). Изменяем нужные строки. Например:

IntelliJ Idea : Декомпиляция, Компиляция, Субституция (или как править чужие ошибки) - 7

  • [1] — hibernate-core-5.2.17.Final.jar
  • [2] — hibernate-jpa-2.1-api-1.0.0.Final.jar

IntelliJ Idea : Декомпиляция, Компиляция, Субституция (или как править чужие ошибки) - 8

Ура, теперь можно выполнить jar update. Можем руководствоваться официальными материалами: jar uf hibernate-core-5.2.17.Final.jar org\hibernate\query\internal\*.class Открытая IntelliJ Idea, скорей всего, не даст изменять файлы. Поэтому до выполнения jar update, скорей всего, придётся закрыть Idea, а после обновления — открыть. После этого можно заново открываем IDE, опять выполняем dubug. Break Points не сбрасываются между перезапусками IDE. Поэтому, выполнение программы остановится там, где и раньше. Вуаля, мы видим как работают наши изменения:

IntelliJ Idea : Декомпиляция, Компиляция, Субституция (или как править чужие ошибки) - 9

Отлично. Но тут возникает вопрос – благодаря чему? Просто благодаря тому, что когда gradle собирает проект, он анализирует блок dependencies и repositories. У грэдла есть некий build cache, который лежит в определённом месте (см. «How to set gradle cache location?». Если в кэше нет зависимости, то Gradle её скачает из репозитория. Т.к. мы меняли jar в самом кэше, то Gradle думает, что в кэше библиотека есть и ничего не выкачивает. Но любая очистка кэша приведёт к тому, что наши изменения пропадут. Плюс, никто кроме нас не может просто взять и получить их. Сколько неудобств, не правда ли? Что же делать. Хм, скачивает из репозитория? Значит, нам нужен наш репозиторий, с преферансом и поэтессами. Об этом следующий этап.

Разворачивание репозитория

Для разворачивания своего репозитория существуют разные бесплатные решения: одно из них Artifactory, а другое — Apache Archive. Артифактори выглядит модно, стильно, современно, но с ним у меня возникли трудности, никаких не хотел правильно размещать артефакты и генерировал ошибочные мавен метаданные. Поэтому, неожиданно для себя, у меня заработал апачевский вариант. Он оказался не такой красивый, зато работает надёжно. На странице загрузки ищем Standalone версию, распаковываем. У них есть свой «Quick Start». После запуска надо дождаться, когда по адресу http://127.0.0.1:8080/#repositorylist . После этого выбираем «Upload Artifact»:

IntelliJ Idea : Декомпиляция, Компиляция, Субституция (или как править чужие ошибки) - 10

Нажимаем «Start Upload», а после «Save Files». После этого появится зелёное сообщение об успешности и артефакт станет доступен в разделе «Browse». Так надо сделать для jar и для pom файлов:

IntelliJ Idea : Декомпиляция, Компиляция, Субституция (или как править чужие ошибки) - 11

Это связано с тем, что в pom файле прописаны дополнительные зависимости хибернейта. А нам осталось только 1 шаг — указать репозиторий в нашем билд скрипте:

 repositories < jcenter() maven < url "http://127.0.0.1:8080/repository/internal/" >> 

И, соответственно, версия нашего хибернейта станет: compile ‘org.hibernate:hibernate-core:5.2.17.Final-JAVARUSH’ . Вот и всё, теперь наш проект использует исправленный нами вариант, а не изначальный.

Заключение

  • Есть такой веб-сервер Undertow. До некоторого времени был баг, который при использовании прокси не давал узнать IP конечного пользователя.
  • До поры до времени WildFly JPA определённым образом обрабатывал один не учтённый спефицикацией момент, из-за этого сыпались Exception. И это не настраивалось.

Reverse-инжиниринг “чёрного ящика”: зачем поддержке исходный код?

Всем привет! Мы команда сопровождения GlowByte, занимаемся решением багов в различных системах крупного бизнеса. Большая часть продуктов, которые мы поддерживаем, – это маркетинговые комплексы банков, телекома и ритейла. Такие системы работают с огромным количеством данных заказчика: сегментируют их для создания программ лояльности и политики персонализированных предложений, делают рассылку по разным каналам коммуникации, принимают поступающие из разных источников данные в реальном времени и тут же их обрабатывают. Часто ядро таких систем разработано определённым вендором, а на стороне заказчика внедрены кастомизированные процессы.

Под нагрузками бизнеса в системе могут возникать инциденты, причём как в самом ядре, так и в кастомизированных процессах. И если код кастомизированных процессов нам виден, то код ядра скрыт, что затрудняет самостоятельный разбор багов. Ранее мы описали историю о том, как стали сопровождать систему, которая была похожа на “чёрный ящик”: баг случился в ядре системы, и мы не могли обратиться к разработчику и получить ответ. В этой статье хотим разобрать техническую сторону того, как мы выходили из ситуации и какой вид reverse-инжиниринга мы применили.

Немного предыстории о проекте

Однажды мы пришли на проект, который ранее сопровождала команда N. Заказчик не имел доступа к кастомизированному коду, ничего не знал о технических деталях устройства своей системы и не имел документации. Вся экспертиза была сконцентрирована на этой команде N, которая в череде недавних событий покинула российский рынок, фактически бросив заказчика наедине с проблемами. Придя к нам, заказчик знал только о бизнес-логике системы и имел инфраструктурную карту. Нашей задачей было нарастить экспертизу по проекту и начать быстро и эффективно исправлять инциденты.

Первым делом мы получили доступ к серверам и провели ресёрч директорий, запущенных процессов и логов. Сюрпризом оказалось то, что от логов были только access.log, то есть разработчик намеренно скрывал всю логику работы ПО, не выводя ничего даже в логи. В процессах мы тоже не нашли хоть чего-то полезного, а структура директорий и содержимое дали понять, что мы имеем дело с микросервисами на Java.

В процессе анализа мы записали все пункты, которые вызвали вопросы, составили карту исследования. Что делают микросервисы и как они взаимодействуют в кластере? С какими данными и с какими источниками работают? Какой логикой руководствуются? Всё это предстояло узнать. Мы решили использовать метод “снежного кома”: всякий раз, когда появляется новая деталь, записываются 3-4 следующих вопроса или шага к проверкам. Оценивая такой подход спустя время, стало очевидно, что именно он не дал нам зайти в тупик.

Итак, получив пока не понятно как связанные JAR микросервисов, мы решили, что было бы неплохо посмотреть на их исходный код и понять хотя бы верхнеуровнево, на каких технологиях они работают.

Мы начали с байт-кода. Он описывает стек и работу с ним, содержится в class-файлах, которые, в свою очередь, находятся в обнаруженных нами JAR. Это набор инструкций, который, подобно ассемблеру, содержит указания на то, как что-то положить в кусок памяти, как забрать и как переместить. Мы открыли документацию о структуре байт-кода и на самых простых примерах изучили его работу. Чтобы объяснить, как именно мы это делали, приведём простой пример Java-программы, которую можно собрать в единственный class-файл, запустить и получить вывод строчки “Hello World!” в консоль.

Исходный код:

class HelloWorld < public static void main(String[] args) < System.out.println("Hello, World!"); >> 

Если собрать этот код Javac HelloWorld.java, а затем к полученному байт-коду применить команду:

javap -c -p -v HelloWorld

мы получим следующие строки байт-кода в читабельном виде:

Classfile /Dev/demo/HelloWorld.class Last modified 24 окт. 2022 г.; size 427 bytes SHA-256 checksum e81c98f6672f78896481d924f3f4de59dd872fc595cd1dbb031b4ecd863ba11e Compiled from "HelloWorld.java" class HelloWorld minor version: 0 major version: 63 flags: (0x0020) ACC_SUPER this_class: #21 // HelloWorld super_class: #2 // java/lang/Object interfaces: 0, fields: 0, methods: 2, attributes: 1 Constant pool: #1 = Methodref #2.#3 // java/lang/Object."":()V #2 = Class #4 // java/lang/Object #3 = NameAndType #5:#6 // "":()V #4 = Utf8 java/lang/Object #5 = Utf8 #6 = Utf8 ()V #7 = Fieldref #8.#9 // java/lang/System.out:Ljava/io/PrintStream; #8 = Class #10 // java/lang/System #9 = NameAndType #11:#12 // out:Ljava/io/PrintStream; #10 = Utf8 java/lang/System #11 = Utf8 out #12 = Utf8 Ljava/io/PrintStream; #13 = String #14 // Hello, World! #14 = Utf8 Hello, World! #15 = Methodref #16.#17 // java/io/PrintStream.println:(Ljava/lang/String;)V #16 = Class #18 // java/io/PrintStream #17 = NameAndType #19:#20 // println:(Ljava/lang/String;)V #18 = Utf8 java/io/PrintStream #19 = Utf8 println #20 = Utf8 (Ljava/lang/String;)V #21 = Class #22 // HelloWorld #22 = Utf8 HelloWorld #23 = Utf8 Code #24 = Utf8 LineNumberTable #25 = Utf8 main #26 = Utf8 ([Ljava/lang/String;)V #27 = Utf8 SourceFile #28 = Utf8 HelloWorld.java < HelloWorld(); descriptor: ()V flags: (0x0000) Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: return LineNumberTable: line 1: 0 public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=1, args_size=1 0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #13 // String Hello, World! 5: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return LineNumberTable: line 3: 0 line 4: 8 > SourceFile: "HelloWorld.java" 

Изначально идут метаданные байт-кода.

Путь класса: Classfile /Dev/demo/HelloWorld.class

Дата последнего изменения и размер: Last modified 24 окт. 2022 г.; size 427 bytes

Чексумма: SHA-256 checksum e81c98f6672f78896481d924f3f4de59dd872fc595cd1dbb031b4ecd863ba11e

Название исходного файла с кодом Java: Compiled from «HelloWorld.java«

Версии класса: minor version: 0 major version: 63

ACC_SUPER – это указание на битовую маску для свойства модификатора.

Далее идёт описание используемых классов:

 this_class: #21 // HelloWorld super_class: #2 // java/lang/Object

Здесь HelloWorld – наш класс, а Object – это суперкласс, от которого наследуются все другие классы.

Далее идут счётчики интерфейсов, полей, методов и атрибутов в собранном классе:

interfaces: 0, fields: 0, methods: 2, attributes: 1

Далее – список констант, которые используются либо прямо в коде (заданы разработчиком), либо для поддержки этого кода. Теги всех Constant pool можно найти в официальной документации.

Следом для каждого объекта идут строки инструкций. Общая структура выглядит следующим образом:

 descriptor: … flags: … Code: stack=1, locals=1, args_size=1 LineNumberTable:

Descriptor – это адрес на дескриптор объекта. Указывает на тип возвращаемого объекта и тип сигнатуры.

означает, что метод принимает на вход ссылку на класс java/lang/String и возвращает пустоту V (void).

В общем виде формула выглядит так:

( ParameterDescriptor* ) ReturnDescriptor

В поле flags указываются маски модификаторов. Code содержит набор инструкций со стеком.

LineNumberTable содержит перечисления вида:

 line 4: 8,

где первое число – смещение в байт-коде, а второе – номер строки.

Разбирая таким образом строчку за строчкой, мы пришли к мнению, что это очень долгий и неэффективный ручной способ для такого количества кода, которое у нас есть. Поэтому решили поискать готовые инструменты, которые могут анализировать байт-код и выдавать результат в виде исходного кода на Java.

Погуглив, нашли несколько онлайн-декомпиляторов байт-кода Java, изучили логику их работы, обфускацию кода при сборке, версии и виды декомпиляторов. Попробовали множество их видов с GitHub. Они позволяли подгружать только по одному .class за раз и часто выдавали исходный код с высокой неточностью декомпиляции: были явные синтаксические ошибки, нечитабельные названия объектов (по всей видимости, код был ещё обфусцирован), отсутствовало форматирование кода и т. д. Словом, всё было не то. Идея с онлайн-декомпилятором не дала того результата, который мог бы дополнить общую картину по проекту.

У нас даже была мысль написать свой декомпилятор. Затем на очередной странице обзоров (которые мы уже стали читать по кругу) натолкнулись на CFR.

CFR – это декомпилятор с открытым исходным кодом (а значит бесплатный), который активно развивается и в нашем случае выдаёт прекрасный результат.

wget http://www.benf.org/other/cfr/cfr_0_115.jar

И затем декомпилировали все JAR-файлы командой вида:

java -jar cfr_0_115.jar javacontainer.jar --outputdir ./javacontainer.

В OutputDir появились красивые Java-файлы, дающие нам ключ к ответам на многие вопросы. Мы проанализировали библиотеки и фреймворки, которые используются в проекте, попытались понять общую логику и архитектуру, выявить паттерны построения проекта. Как мы упомянули выше, каждое наше действие сопровождалось записыванием вопросов, которые мы постепенно закрыли и задокументировали. У нас появилось техническое понимание внутренностей проекта. Этого отчасти хватило для того, чтобы понять, куда смотреть, если какой-то элемент не работает, но всё-таки было недостаточно, чтобы найти причину или исправить.

Сразу скажу, что среди команды сопровождения экспертиза по Java наклонена в пользу решения багов, а не разработки новых продуктов, сборок и поставок Java-кода. Исходя из этого, при декомпиляции мы преимущественно искали то, что нужно нам для решения какой-либо проблемы. Декомпилировав всё, мы обнаружили, что кода в этом проекте было настолько много, что изучить его комплексно одному человеку было не под силу, и мы пошли от обратного: начали изучать кусками по мере необходимости. Когда нам заводили заявку, мы уже по составленной логической схеме понимали, в какую часть микросервисов нужно смотреть, декомпилировали и описывали логику именно этой части. Такой подход значительно увеличил скорость решения инцидентов, привёл к ясности, почему случилась та или иная авария, но всё же оставил один открытый вопрос: как решать баг, если он является ошибкой в самом коде.

Первая идея – потратить кучу времени команды, разобрать весь код и затем собрать его обратно. Такой подход явно имел много рисков. Во-первых, декомпиляция всё ещё оставалась неточной, требовала внесения ручных правок (с последующим тестированием). Во-вторых, нужно было самостоятельно выяснять в огромном количестве кода, какие имеются зависимости, и решать проблемы сборки. В-третьих, мы работали над масштабным ритейл-проектом, в котором было критично важно, чтобы ПО не простаивало и чтобы наш новый подход не сделал хуже: риски были высокими, а сроков на тестирование почти не оставалось. Поэтому мы отмели идею с “разборкой” кода и начали работать в направлении поиска альтернативного пути. Обходное решение должно было быть таким, чтобы не требовалось вносить правки в код микросервисов.

В поисках универсальных обходных решений мы первым делом попробовали найти показатели, на которые можем ориентироваться: стали следить за метриками, выяснять, какие значения и каких показателей являются нормой, а какие нет. В системе мы нашли Zabbix, который содержал множество бесполезных метрик. Они перегружали систему, давали крайне мало полезной информации. Позднее мы провели работы по оптимизации дашбордов и собираемых данных, но на момент анализа пришлось работать с тем, что есть.

По Zabbix получилось оценить только метрики железа (CPU, RAM, нагрузку на диски и др.), а также отследить показатели в БД, логи и метрики внутри интерфейсов приложений. Это была очень скудная информация без глубины истории: она показывала метрику в моменте без анализа периодичности. Тем не менее так мы составили некоторое представление о том, как все должно быть.

Затем мы попробовали все виды перезагрузок (кстати, о том как правильно их выполнять, мы тоже узнали методом проб и ошибок): перезагрузки всех серверов сразу и по отдельности, выводы и вводы нод из балансировки, перезагрузки микросервисов в комплексе и по отдельности, перезагрузки отдельных компонент и т. д.

Также мы исследовали область интеграции приложения с данными. Если баг случается на уровне интеграции с процедурами в БД/с типами данных в таблицах/с самими данными, то легче поправить их. Смотря на ситуацию из будущего, оказалось, что это было полезно и многие правки на уровне данных (хотя они и были нетривиальными) помогли проекту.

На протяжении всей исследовательской работы и выстраивания фундамента для сопровождения полностью закрытого проекта, мы всё записывали. В конечном счёте наша работа стала похожа на детектив, в котором мы шаг за шагом создали технический портрет проекта.

Оглядываясь назад, подытожим шаги, которые мы предприняли. Попробовали ручной просмотр байт-кода. Это было непонятно, вместе с тем очень интересно, однако на нашей практике такой метод показал себя неэффективным.

Мы применили декомпиляцию с помощью CFR. Инструмент для нас оказался очень полезным и нужным: он дал знания о том, как работает система и чего от неё можно ожидать, а также ключи для поиска обходных решений инцидентов. В то же время декомпиляция в нашем случае оказалась применима только в качестве метода получения новых знаний: для обратной сборки проекта она не подошла в масштабах проекта.

Разобрав историю нашего заказчика по частям, мы стали готовы сопровождать проект без помощи разработчика. У нас появилась собственная экспертиза, которой мы делимся с заказчиком.

Сейчас наша команда продолжает также точечно декомпилировать части системы и делать новые открытия, но общий подход и инструменты уже выработаны.

  • java
  • декомпиляция
  • reverse engineering
  • support
  • исследование программ
  • консалтинг в it
  • консалтинг и поддержка
  • история it
  • декомпилятор
  • российский рынок

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *