Начни с VMStructs. Это та самая таблица которую HotSpot
экспортирует как gHotSpotVMStructs глобальным символом, и
через неё все внешние тулзы (jhsdb, jmap -F, MoneyShot,
любые memory walker-ы) узнают layout InstanceKlass, Method,
ConstMethod — где лежит _methods, где code_base, какие там
оффсеты. Даже с вырезанным JVMTI у тебя эта таблица скорее
всего осталась. Убирается двумя шагами: strip через .def
файл где в экспортах только JNI_* и JVM_*, и плюс добавь
пару приватных полей в середину InstanceKlass и ConstMethod
между сборками. Это бесплатно сдвигает все оффсеты и ломает
публичные тулзы заточенные под апстримный layout.
Дальше — тела методов. У тебя CP расшифровывается, это гуд,
но сами байты методов где лежат? Если в ConstMethod::code_base()
они остаются в plaintext после линковки — дампер читает
метаспейс ручками через VMStructs, восстанавливает _fast_*
опкоды через Rewriter::restore_bytecodes (эта функция
публичная внутри HotSpot, её дёргает reconstituter) и
получает чистый байткод. То есть шифровать только CP
недостаточно, надо и код тоже. Либо декриптить в интерпретаторе
перед каждым dispatch, либо один раз при link-е метода.
И вот дальше самое вкусное. После того как C2 скомпилил
метод в nmethod, байткод в ConstMethod::_code для нормального
исполнения больше не нужен — все вызовы идут через
Method::_from_compiled_entry сразу в code cache, интерпретатор
не трогается. Байткод нужен ТОЛЬКО для деопта. А значит если
ты можешь гарантировать что для конкретного метода деопт не
произойдёт — можно его занулить. Как гарантировать: отключить
OSR, поставить методу ACC_FINAL чтобы класс-иерархия не
инвалидировалась, и в C2 pass-е заменить uncommon traps на
explicit throw через shared stub. После этого в Method::set_code
сразу после публикации nmethod делаешь memset(code_base, 0,
code_size) и всё — класс физически больше не существует как
байткод, только как x86 в code cache. Recaf, CFR, narumii,
threadtear становятся нахуй не нужны потому что дампить
нечего. Это самая мощная штука которую можно сделать поверх
того что у тебя уже есть.
Чтобы ничего случайно не сломалось — добавь в nmethod.hpp
два бита _is_pinned и _is_sealed. Pinned — не даёт nmethod
флашиться из code cache (is_cold возвращает false, is_relocatable
тоже), sealed — флаг что байткод уже занулен. И в
Deoptimization::fetch_unroll_info_helper поставь guard: если
пришёл sealed метод — vm_exit_during_runtime сразу, без
попыток прочитать занулённый code. Это на случай если
проглядел какой-то путь где деопт всё-таки может сработать —
лучше упасть чем отдать плейнтекст.
По мелочам. Проверь что ты вырубил не только JVMTI но и
INCLUDE_SERVICES — иначе у тебя всё ещё работает Attach API
(jcmd, jmap, jstack) и Serviceability Agent, это отдельно
от JVMTI. INCLUDE_JFR тоже руби, JFR ставит копию оригинальных
байтов в _cached_class_file если заинструментировал класс.
jvm.dll должна проверять сама себя при старте. SHA-256 от
.text секции сравнивается с захардкоженным значением до того
как ты расшифровал первый класс. Не совпало — крэш или,
лучше, возвращаешь мусорный ключ из декриптера чтобы всё
работало но классы не расшифровывались. Без этого любой
может занопить твой декрипт в jvm.dll и получить рабочую
копию без крипты.