Class internals: hacking Java bytecode
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) {
System.out.println(implementationDetails());
}
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 tool: bytecode viewer - no edit option:
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:
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) {
System.out.println(implementationDetails());
}
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
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
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
Code:
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
LineNumberTable:
line 5: 0
line 6: 9
LocalVariableTable:
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
Code:
stack=1, locals=0, args_size=0
0: ldc #25 // String The method now open.
2: areturn
LineNumberTable:
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:
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()))
cafebabe0000003d00260a000200030700040c000500060100106a617661
2f6c616e672f4f626a6563740100063c696e69743e010003282956090008
000907000a0c000b000c0100106a6176612f6c616e672f53797374656d01
00036f75740100154c6a6176612f696f2f5072696e7453747265616d3b0a
000e000f0700100c001100120100034c6962010015696d706c656d656e74
6174696f6e44657461696c7301001428294c6a6176612f6c616e672f5374
72696e673b0a001400150700160c001700180100136a6176612f696f2f50
72696e7453747265616d0100077072696e746c6e010015284c6a6176612f
6c616e672f537472696e673b295608001a010014546865206d6574686f64
206e6f77206f70656e2e010004436f646501000f4c696e654e756d626572
5461626c650100124c6f63616c5661726961626c655461626c6501000474
6869730100054c4c69623b0100046d61696e010016285b4c6a6176612f6c
616e672f537472696e673b2956010004766172300100135b4c6a6176612f
6c616e672f537472696e673b01000a536f7572636546696c650100084c69
622e6a6176610021000e00020000000000030001000500060001001b0000
002f00010001000000052ab70001b100000002001c000000060001000000
03001d0000000c000100000005001e001f00000009002000210001001b00
000038000200010000000ab20007b8000db60013b100000002001c000000
0a00020000000500090006001d0000000c00010000000a00220023000000
09001100120001001b0000001b00010000000000031219b000000001001c
0000000600010000000900010024000000020025
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()
f.close()
print(stream.hex())
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:
cafebabe0000003d
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.