
Jika Anda tidak takut dengan gambar di atas, jika Anda tahu bagaimana big-endian berbeda dari little-endian, jika Anda selalu tertarik pada bagaimana file biner "diatur", maka artikel ini adalah untuk ANDA!
Pendahuluan
Di Habrรฉ sudah ada beberapa artikel tentang rekayasa terbalik format biner dan tentang studi struktur bytecode dari file .class:
Kelompok konstanta
Java Bytecode Fundamentals ,
Bytecode Java "Halo dunia" ,
Hello World dari bytecode untuk JVM dll.
Peneliti memiliki tugas untuk berurusan dengan protokol biner yang tidak diketahui atau menggali struktur biner yang ada spesifikasi.
Ketertarikan saya pada format biner muncul bahkan ketika saya masih mahasiswa dan menulis makalah tentang pengembangan driver sistem file Linux. Beberapa tahun kemudian, saya memberi kuliah tentang dasar-dasar Linux untuk para ahli forensik - di masa lalu, Linux adalah baru dan spesialis muda setelah universitas dapat menceritakan banyak hal baru kepada para ahli dewasa. Memberitahu saya cara menghapus dump dari disk menggunakan dd, dan setelah menghubungkan gambar ke komputer lain untuk belajar, saya menyadari bahwa gambar disk berisi banyak informasi menarik. Informasi ini dapat diekstraksi bahkan tanpa memasang gambar (ya, mount -o loop ...) jika Anda tahu spesifikasi untuk format sistem file dan memiliki alat yang sesuai. Sayangnya, saya tidak punya alat seperti itu.
Setelah beberapa tahun, saya perlu mendekompilasi perpustakaan Java. Tidak ada JD GUI pada masa itu, serta dekompiler ideologis, tetapi ada JAD. Untuk pustaka saya, JAD menghasilkan campuran opcode Java dengan pesan kesalahan. Selain itu, JAD tidak mendukung anotasi, dan di Java 6, yang muncul kemudian, mereka sudah terbiasa. Berbekal spesifikasi mesin virtual Java, saya mulai ...
Ide
Saya membutuhkan mekanisme universal untuk menggambarkan struktur biner dan loader universal. Loader, menggunakan deskripsi, akan membaca data biner ke dalam memori. Anda biasanya harus berurusan dengan angka, string, susunan data, dan struktur majemuk. Semuanya sederhana dengan angka - mereka memiliki panjang tetap - 1, 2, 4 atau 8 byte dan dapat segera dipetakan ke tipe data yang tersedia dalam bahasa. Misalnya: byte, pendek, int, panjang untuk Java. Untuk tipe numerik yang lebih panjang dari satu byte, penanda urutan byte (yang disebut representasi BigEndian / LittleEndiang) harus disediakan.
Ini lebih sulit dengan string - mereka dapat dalam pengkodean yang berbeda (ASCII, UNICODE), memiliki panjang tetap atau variabel. String dengan panjang tetap dapat dianggap sebagai array byte. Untuk string panjang variabel, Anda dapat menggunakan dua opsi perekaman - tunjukkan panjangnya di awal baris (Pascal atau string awalan-awalan) atau letakkan karakter khusus di akhir baris untuk menunjukkan akhir baris. Byte dengan nilai nol (yang disebut null-sided dihentikan) digunakan sebagai tanda seperti itu. Kedua opsi memiliki kelebihan dan kekurangan, diskusi yang berada di luar cakupan artikel ini. Jika ukurannya ditentukan di awal, maka ketika mengembangkan format, Anda perlu memutuskan panjang string maksimum: berapa banyak byte yang harus kami alokasikan ke penanda panjang tergantung pada ini: 2 8 - 1 untuk satu byte, 2 16 - 1 untuk dua byte, dll.
Kami akan membedakan struktur data komposit menjadi kelas yang terpisah, melanjutkan dekomposisi ke angka dan string.
Struktur file .class
Kita perlu menggambarkan struktur file Java .class. Sebagai hasilnya, saya ingin memiliki satu set kelas Java, di mana setiap kelas hanya berisi bidang yang sesuai dengan struktur data yang diteliti dan, mungkin, metode bantu untuk menampilkan objek dalam bentuk yang dapat dibaca manusia ketika metode toString () dipanggil. Secara kategoris, saya tidak ingin memiliki logika di dalam yang bertanggung jawab untuk membaca atau menulis file.
Kami mengambil spesifikasi dari mesin virtual Java,
Spesifikasi JVM, Java SE 12 Edition .
Kami akan tertarik pada bagian 4 "Format File kelas".
Untuk menentukan bidang mana dalam urutan apa untuk memuat, kami memperkenalkan anotasi @FieldOrder (indeks = ...). Kita perlu secara eksplisit menunjukkan urutan bidang untuk bootloader, karena spesifikasi tidak memberi kami jaminan pada urutan di mana mereka akan disimpan dalam file biner.
File Java .class dimulai dengan 4 byte angka ajaib, dua byte versi minor Java, dan dua byte versi utama. Kami mengemas angka ajaib dalam variabel int, dan nomor versi minor dan utama singkatnya:
@FieldOrder(index = 1) private int magic; @FieldOrder(index = 2) private short minorVersion; @FieldOrder(index = 3) private short majorVersion;
Selanjutnya dalam file .class adalah ukuran pool konstan (variabel dua-byte) dan pool konstan itu sendiri. Kami memperkenalkan anotasi @ContainerSize untuk mendeklarasikan ukuran array dan daftar struktur. Ukuran dapat diperbaiki (kami akan mengaturnya melalui atribut nilai) atau memiliki panjang variabel, ditentukan oleh variabel yang sebelumnya dibaca. Dalam hal ini, kita akan menggunakan atribut "fieldName", yang menunjukkan dari variabel mana kita akan membaca ukuran kontainer. Menurut spesifikasi (bagian 4.1,
"Struktur ClassFile"), ukuran aktual dari kolam konstan berbeda dengan 1 dari nilai
yang ditulis ke constant_pool_count:
u2 constant_pool_count; cp_info constant_pool[constant_pool_count-1];
Untuk menjelaskan koreksi tersebut, kami memperkenalkan atribut korektor tambahan dalam penjelasan @ContainerSize.
Sekarang kita dapat menambahkan deskripsi dari kumpulan konstan:
@FieldOrder(index = 4) private short constantPoolCount; @FieldOrder(index = 5) @ContainerSize(fieldName = "constantPoolCount", corrector = -1) private List<ConstantPoolItem> constantPoolList = new ArrayList<>();
Dalam hal perhitungan yang lebih kompleks, Anda cukup menambahkan metode get yang akan mengembalikan nilai yang diinginkan: @FieldOrder(index= 1) private int containerSize; @FieldOrder(index = 2) @ContainerSize(filed="actualContainerSize") private List<ContainerItem> containerItems; public int getActualContainerSize(){ return containerSize * 2 + 3; }
Kolam renang konstan
Setiap elemen dalam kumpulan konstanta adalah deskripsi konstanta yang sesuai dari tipe int, long, float, double, String, atau deskripsi salah satu komponen kelas Java - bidang kelas (bidang), metode, tanda tangan metode, dll. Istilah "konstan" di sini berarti nilai yang tidak disebutkan namanya yang digunakan dalam kode:
if (intValue > 100500)
Nilai 100500 akan diwakili dalam kumpulan konstan sebagai turunan dari CONSTANT_Integer. Spesifikasi JVM untuk Java 12 mendefinisikan 17 jenis yang dapat berada dalam kumpulan konstan.
Kemungkinan instance elemen pool const Dalam implementasi kami, kami akan membuat ConstantPoolItem kelas di mana akan ada tag bidang bita tunggal, yang menentukan struktur mana yang sedang kita baca saat ini. Untuk setiap elemen dalam tabel di atas, buat kelas Java, turunan ConstantPoolItem. Pemuat file biner universal harus dapat menentukan kelas mana yang harus digunakan berdasarkan tag yang sudah dibaca.
(secara umum, tag dapat berupa variabel jenis apa pun). Untuk tujuan ini, tentukan antarmuka HasInheritor dan terapkan antarmuka ini di kelas ConstantPoolItem:
public interface HasInheritor<T> { public Class<? extends T> getInheritor() throws InheritorNotFoundException; public Collection<Class<? extends T>> getInheritors(); }
public class ConstantPoolItem implements HasInheritor<ConstantPoolItem> { private final static Map<Byte, Class<? extends ConstantPoolItem>> m = new HashMap<>(); static { m.put((byte) 7, ClassInfo.class); m.put((byte) 9, FieldRefInfo.class); m.put((byte) 10, MethodRefInfo.class); m.put((byte) 11, InterfaceMethodRefInfo.class); m.put((byte) 8, StringInfo.class); m.put((byte) 3, IntegerInfo.class); m.put((byte) 4, FloatInfo.class); m.put((byte) 5, LongInfo.class); m.put((byte) 6, DoubleInfo.class); m.put((byte) 12, NameAndTypeInfo.class); m.put((byte) 1, Utf8Info.class); m.put((byte) 15, MethodHandleInfo.class); m.put((byte) 16, MethodTypeInfo.class); m.put((byte) 17, DynamicInfo.class); m.put((byte) 18, InvokeDynamicInfo.class); m.put((byte) 19, ModuleInfo.class); m.put((byte) 20, PackageInfo.class); } @FieldOrder(index = 1) private byte tag; @Override public Class<? extends ConstantPoolItem> getInheritor() throws InheritorNotFoundException { Class<? extends ConstantPoolItem> clazz = m.get(tag); if (clazz == null) { throw new InheritorNotFoundException(this.getClass().getName(), String.valueOf(tag)); } return clazz; } @Override public Collection<Class<? extends ConstantPoolItem>> getInheritors() { return m.values(); } }
Pemuat universal akan membuat instance kelas yang diperlukan dan melanjutkan membaca. Satu-satunya syarat: indeks dalam kelas penerus harus memiliki penomoran end-to-end dengan kelas induk. Ini berarti bahwa di semua kelas ConstantPoolItem, yang diturunkan dari FieldOrder, anotasi harus memiliki indeks lebih dari satu, karena di kelas induk kita sudah membaca bidang tag dengan nomor "1".
Struktur file .class (lanjutan)
Setelah daftar elemen dari kumpulan konstan dalam file .class ada pengidentifikasi dua byte yang mendefinisikan rincian kelas ini - adalah kelas anotasi, antarmuka, kelas abstrak, apakah memiliki bendera akhir, dll. Ini diikuti oleh pengidentifikasi dua byte (referensi ke elemen dalam kumpulan konstan) yang mendefinisikan kelas ini. Pengidentifikasi ini harus menunjuk ke elemen tipe ClassInfo. Superclass untuk kelas yang diberikan didefinisikan dengan cara yang sama (apa yang ditunjukkan setelah kata "extends" dalam definisi kelas). Untuk kelas yang tidak memiliki superclasses secara eksplisit, bidang ini berisi referensi ke kelas Object.
Di Jawa, setiap kelas hanya dapat memiliki satu superclass, tetapi jumlahnya
Mungkin ada beberapa antarmuka yang mengimplementasikan kelas ini:
@FieldOrder(index = 9) private short interfacesCount; @FieldOrder(index = 10) @ContainerSize(fieldName = "interfacesCount") private List<Short> interfaceIndexList;
Setiap elemen di interfaceIndexList mewakili tautan ke elemen dalam kumpulan konstan (seperti yang ditentukan
indeks harus berupa elemen dengan tipe ClassInfo).
Variabel kelas (properti, bidang) dan metode diwakili oleh daftar yang sesuai:
@FieldOrder(index = 11) private short fieldsCount; @FieldOrder(index = 12) @ContainerSize(fieldName = "fieldsCount") private List<Field> fieldList; @FieldOrder(index = 13) private short methodsCount; @FieldOrder(index = 14) @ContainerSize(fieldName = "methodsCount") private List<Method> methodList;
Elemen terakhir dalam deskripsi file Java .class adalah daftar atribut kelas. Atribut yang menggambarkan file sumber yang terkait dengan kelas, kelas bersarang, dll. Dapat dicantumkan di sini.
Bytecode Java beroperasi dengan data numerik dalam representasi big-endian, kami akan menggunakan representasi ini secara default. Untuk format biner dengan angka little-endian, kami akan menggunakan anotasi LittleEndian . Untuk string yang tidak memiliki panjang yang telah ditentukan, tetapi
dibaca sebelum karakter terminal (seperti string null-terminated C-like) yang akan kita gunakan
@StringTerminator anotasi:
@FieldOrder(index = 2) @StringTerminator(0) private String nullTerminatedString;
Terkadang di kelas yang mendasarinya Anda perlu meneruskan informasi dari tingkat yang lebih tinggi. Objek Metode di methodList tidak memiliki informasi tentang nama kelas di mana ia berada, apalagi objek metode tidak mengandung nama dan daftar parameternya. Semua informasi ini disajikan sebagai indeks pada elemen-elemen dalam kumpulan konstan. Ini cukup untuk mesin virtual, tetapi kami ingin menerapkan metode toString () sehingga mereka menampilkan informasi tentang metode dalam bentuk yang ramah manusia, dan tidak dalam bentuk indeks pada elemen dalam kumpulan konstan. Untuk melakukan ini, kelas Metode harus mendapatkan referensi ke ConstantPoolList dan ke variabel dengan nilai thisClassIndex. Agar dapat meneruskan tautan ke tingkat sarang yang mendasarinya, kami akan menggunakan anotasi Suntikan :
@FieldOrder(index = 14) @ContainerSize(fieldName = "methodsCount") @Inject(fieldName = "constantPoolList") @Inject(fieldName = "thisClassIndex") private List<Method> methodList;
Dalam metode pengambil kelas (ClassFile) saat ini akan dipanggil untuk variabel constantPoolList dan thisClassIndex, dan di kelas penerima (dalam hal ini Metode), metode penyetel akan dipanggil (jika ada).
Bootloader universal
Jadi, kami memiliki satu antarmuka HasInheritor dan lima anotasi @FieldOrder, @ContainerSize, LittleEndian , Inject dan @StringTerminator, yang memungkinkan kami untuk menggambarkan struktur biner pada abstraksi tingkat tinggi. Memiliki deskripsi formal, kita dapat meneruskannya ke universal loader, yang dapat membuat struktur yang dijelaskan, mem-parsing file biner dan membacanya ke dalam memori.
Akibatnya, kita harus dapat menggunakan kode ini:
ClassFile classFile; try (InputStream is = new FileInputStream(inputFileName)) { Loader loader = new InputStreamLoader(is); classFile = (ClassFile) loader.load(); }
Sayangnya, pengembang platform Java sedikit terlalu canggih untuk nilai delapan byte di pool.
konstanta disediakan untuk dua sel, sel pertama harus mengandung nilai, dan yang kedua tetap
kosong Ini berlaku untuk konstanta panjang dan ganda.
Deskripsi dari spesifikasi JVMSemua konstanta 8-byte mengambil dua entri dalam tabel constant_pool kelas
file. Jika struktur CONSTANT_Long_info atau CONSTANT_Double_info adalah entri
pada indeks n pada tabel constant_pool, maka entri yang dapat digunakan selanjutnya dalam tabel adalah
terletak di indeks n + 2. Indeks constant_pool n +1 harus valid tetapi dipertimbangkan
tidak dapat digunakan.
Tampaknya, pengembang Java ingin menerapkan semacam optimasi tingkat rendah, tetapi kemudian
diakui bahwa keputusan desain ini berubah
tidak berhasil.Dalam retrospeksi, membuat konstanta 8-byte mengambil dua entri konstan adalah pilihan yang buruk.
Untuk menangani kasus-kasus khusus ini, kami akan menambahkan anotasi @EntrySize, yang akan kami gunakan,
untuk menandai konstanta delapan byte:
@EntrySize(value = 2, index = 1) public class EightByteNumberInfo extends ConstantPoolItem { @FieldOrder(index = 2) private int highBytes; @FieldOrder(index = 3) private int lowBytes; }
Atribut nilai menunjukkan jumlah sel yang akan ditempati elemen, indeks - indeks elemen,
yang mengandung nilai. kelas LongInfo dan DoubleInfo akan memperluas kelas EightByteNumberInfo.
Bootloader universal perlu diperluas dengan fungsional yang mendukung anotasi @EntrySize.
public ClassFileLoader(String fileName) { try { File f = new File(fileName); FileInputStream fis = new FileInputStream(f); loader = new EntrySizeSupportLoader(fis); } catch (FileNotFoundException e) { throw new RuntimeException(e); } }
Setelah memuat kelas dengan ClassFileLoader, Anda dapat menghentikan debugger dan memeriksa kelas yang dimuat di pengawas variabel di IDE.
File kelas akan terlihat seperti ini:

Dan Constant Pool seperti ini:

Kesimpulan
Siapa pun yang dapat membaca sampai akhir mungkin ingin memilih bytecode Java dengan tangan mereka sendiri. Jangan ragu untuk pergi ke github dan unduh deskripsi file kelas Java sebagai satu set kelas Java: https://github.com/esavin/annotate4j-classfile . Pemuat universal dan anotasi ada di sini: https://github.com/esavin/annotate4j-core .
Untuk mengunduh file kelas yang dikompilasi, gunakan annotate4j.classfile.loader.ClassFileLoader loader.
Sebagian besar kode ditulis untuk Java 6, saya hanya mengadaptasi kolam konstan ke versi modern. Saya tidak memiliki kekuatan dan keinginan untuk mengimplementasikan Java loader sepenuhnya untuk opcode Java, jadi hanya ada perkembangan kecil di bagian ini.
Menggunakan perpustakaan ini (bagian inti) saya berhasil mengembalikan file biner dengan data pemantauan Holter (studi EKG aktivitas jantung harian). Di sisi lain, saya tidak bisa mendekripsi protokol biner dari satu sistem akuntansi yang ditulis dalam Delphi. Saya tidak mengerti bagaimana tanggal dikirim, dan kadang-kadang muncul situasi ketika data aktual tidak sesuai dengan struktur yang dibangun pada nilai-nilai sebelumnya.
Saya mencoba membangun model yang mirip dengan file kelas Java untuk format ELF (format yang dapat dijalankan pada Unix / Linux), tetapi saya tidak dapat sepenuhnya memahami spesifikasi - ternyata terlalu kabur bagi saya. Nasib yang sama menimpa format JPEG dan BMP - sepanjang waktu saya menemukan beberapa kesulitan dengan memahami spesifikasi.