5 minute read

Read first: Deep dive into class internals: viewing bytecode, hex dump and first look at the ClassFile structure.

Class hacking use case

Suppose there is a Java library with closed implementation enclosed in a private method. This library is imported into a project that is currently under development.

public class Lib {

    public static void main(String[] args) {

    private static String implementationDetails() {
        return "The method is closed.";


Suppose there is a need for some change within the implementationDetails() method. The method behaviour should be different, at least for some time (during development or for testing). Extension of importedLib class and writing own implementation is futile as private method overriding is forbidden. Let’s assume there is no usual workaround (like forking the library repo, cloning and changing local version. We have the compiled file and nothing else. There must be a way of changing access modifier or even tampering with the body of the method.

Java bytecode editing

As previously explained, command-line javap tool only shows the content of a compiled class. It reveals bytecode and metadata without edit option.

IntelliJ does not allow editing of compiled class. And it shows Java code, not bytecode, using default FernFlower decompiler.

IntelliJ class viewer

IntelliJ tool: bytecode viewer - no edit option:

IntelliJ bytecode viewer

There are multiple, unofficial plugins for IntelliJ. I tried the most recommended, and it failed a few times, so I gave up and uninstalled. And probably it is not quite a good idea to plug something “unofficial” into IDE used for developing production code, I would say. Then I found Recaf: better option, open-source code. It’s a simple, external Java application with plesant GUI and several features.

Now, we can use editor in decompiler mode and assembler tool to change access modifer or method body to reqiured extend:

Recaf bytecode viewer

Save and export command applies changes that are mirrored in decompiled .class view in IntelliJ:

// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)

public class Lib {
    public Lib() {

    public static void main(String[] var0) {

    public static String implementationDetails() {
        return "The method is now open.";

and when the class file is run:

$ java Lib
The method is now open.

This way is enough at a basic level.

Java hex editing

Simple decompilation view, even with Recaf assembler tool, does not reveal all the details of .class file. On the other hand, javap shows more interesting things: the bytecode and metadata without editing possibility:

$ javap -v -c Lib.class
Classfile /home/kotfot/IdeaProjects/blog/assets/java/Lib.class
  Last modified Feb 20, 2023; size 620 bytes
  SHA-256 checksum ad5de624d6828ca76eeb8b9bd62e0d52090bf5c7905ea4f4fa3dadeb303e2ab9
  Compiled from "Lib.java"
public class Lib
  minor version: 0
  major version: 61
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #14                         // Lib
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 3, attributes: 1
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #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 = Methodref          #14.#15        // Lib.implementationDetails:()Ljava/lang/String;
  #14 = Class              #16            // Lib
  #15 = NameAndType        #17:#18        // implementationDetails:()Ljava/lang/String;
  #16 = Utf8               Lib
  #17 = Utf8               implementationDetails
  #18 = Utf8               ()Ljava/lang/String;
  #19 = Methodref          #20.#21        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #20 = Class              #22            // java/io/PrintStream
  #21 = NameAndType        #23:#24        // println:(Ljava/lang/String;)V
  #22 = Utf8               java/io/PrintStream
  #23 = Utf8               println
  #24 = Utf8               (Ljava/lang/String;)V
  #25 = String             #26            // The method now open.
  #26 = Utf8               The method now open.
  #27 = Utf8               Code
  #28 = Utf8               LineNumberTable
  #29 = Utf8               LocalVariableTable
  #30 = Utf8               this
  #31 = Utf8               LLib;
  #32 = Utf8               main
  #33 = Utf8               ([Ljava/lang/String;)V
  #34 = Utf8               var0
  #35 = Utf8               [Ljava/lang/String;
  #36 = Utf8               SourceFile
  #37 = Utf8               Lib.java
  public Lib();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
        line 3: 0
        Start  Length  Slot  Name   Signature
            0       5     0  this   LLib;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
      stack=2, locals=1, args_size=1
         0: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: invokestatic  #13                 // Method implementationDetails:()Ljava/lang/String;
         6: invokevirtual #19                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         9: return
        line 5: 0
        line 6: 9
        Start  Length  Slot  Name   Signature
            0      10     0  var0   [Ljava/lang/String;

  public static java.lang.String implementationDetails();
    descriptor: ()Ljava/lang/String;
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
      stack=1, locals=0, args_size=0
         0: ldc           #25                 // String The method now open.
         2: areturn
        line 9: 0
SourceFile: "Lib.java"

Finally,xxd makes a dump to hexadecimal code. There are ways to edit the hex from command line. Recaf also gives this opportunity:

Recaf hex viewer

The hex view consists on three parts: offset on the left is not a part of the bytecode (it belongs to the viewing application). In the centre, there is the bytecode itself (as hexadecimal code). On the left, ASCII transcription of textual layer (with dots when the layer could not be rendered).

Hex dump as Java stream

xxd -p Lib.class >> hex.txt
jshell> String hex = "/home/IdeaProjects/blog/assets/java/hex.txt"
hex ==> "/home/IdeaProjects/blog/assets/java/hex.txt"

jshell> Stream<String> stream = Files.lines(Paths.get(hex))
stream ==> java.util.stream.ReferencePipeline$Head@2d363fb3

jshell> stream.forEach(s->System.out.println(s)) 
# or as one String: System.out.println(stream.collect(Collectors.joining()))

or InputStream from byte array:

InputStream is = new ByteArrayInputStream(Files.readAllBytes(Paths.get(hex)))

Slightly reworked (but still working) example of parsing hex file into Python stream from Jared Folkins

import os

_directory = "/path/to/dir"
_file = "Lib.class"
if os.path.exists(_directory):
    with open(_directory + _file, "rb") as f:
        stream = f.read()

Breaking Java bytecode in hex editor

Every Java .class file must start with hex magic number CAFEBABE in the bytecode. Let’s change magic number CAFEBABE to CAFE DEAD using any hex editor and see what happens.

$ java Lib
Error: LinkageError occurred while loading main class Lib
	java.lang.ClassFormatError: Incompatible magic value 3405700781 in class file Lib

Now, changing Java version encoded in the ClassFile to any unsupported one should break it. Following this guide showing how to find Java version in hex dump it appears as:


After changing 003d to 003e (or higher - not supported) we got:

$ java Lib
Error: LinkageError occurred while loading main class Lib
	java.lang.UnsupportedClassVersionError: Lib has been compiled by a more recent version of the Java Runtime (class file version 62.0), 
	this version of the Java Runtime only recognizes class file versions up to 61.0

To be continued.

Other tricks with .jar hacking