
如果您不怕上面的图片,如果您知道big-endian与little-endian有何不同,如果您始终对二进制文件的“排列方式”感兴趣,那么本文适合您!
引言
在Habré上,已经有几篇有关二进制格式的逆向工程以及.class文件的字节码结构研究的文章:
常数池
Java字节码基础知识 ,
Java字节码“ Hello world” ,
JVM等字节码的Hello World
研究人员的任务是要么处理未知的二进制协议,要么挖掘有规范的二进制结构。
甚至在我还是学生的时候,我就对二进制格式产生了兴趣,并撰写了有关Linux文件系统驱动程序开发的学期论文。 几年后,我为法医专家讲授了Linux的基础知识-过去,Linux是新技术,大学毕业后年轻的专家可以向成年专家讲很多新知识。 告诉我如何使用dd从磁盘中删除转储,并将映像连接到另一台计算机进行研究后,我意识到磁盘映像包含许多有趣的信息。 如果您知道文件系统格式的规范并拥有适当的工具,则即使不挂载映像也可以提取此信息(呵呵,挂载-o循环...)。 不幸的是,我没有这样的工具。
几年后,我需要反编译Java库。 那时没有JD GUI,也没有意识形态反编译器,但有JAD。 对于我的库,JAD混合了Java操作码和错误消息。 此外,JAD不支持注释,在那时出现的Java 6中,它们已被完全使用。 有了Java虚拟机规范,我开始...
主意
我需要一个用于描述二进制结构的通用机制和一个通用加载程序。 加载程序将按照说明将二进制数据读入内存。 通常,您必须处理数字,字符串,数据数组和复合结构。 一切都很简单,用数字即可-它们具有固定的长度-1、2、4或8个字节,可以立即映射到该语言中可用的数据类型。 例如:Java的byte,short,int,long。 对于长于一个字节的数字类型,必须提供字节顺序标记(所谓的BigEndian / LittleEndiang表示形式)。
字符串更复杂-它们可以采用不同的编码(ASCII,UNICODE),具有固定或可变的长度。 固定长度的字符串可以视为字节数组。 对于可变长度的字符串,可以使用两个记录选项-在行的开头指示其长度(Pascal或Length前缀的字符串),或在行的末尾添加特殊字符以指示行的末尾。 作为这样的符号,使用值为零的字节(所谓的以空字符结尾的小数)。 这两种选择都有优点和缺点,对此的讨论不在本文讨论范围之内。 如果在开始时指定了大小,那么在开发格式时,您需要确定最大字符串长度:我们必须分配给长度标记的字节数取决于:1个字节2 8-1,2个字节2 16-1等。
我们将组合数据结构分为不同的类,继续分解为数字和字符串。
.class文件的结构
我们需要以某种方式描述Java .class文件的结构。 结果,我想拥有一组Java类,其中每个类仅包含与正在研究的数据结构相对应的字段,并且可能还包含在调用toString()方法时以人类可读形式显示对象的辅助方法。 从类别上讲,我不想让内部逻辑负责读取或写入文件。
我们采用Java虚拟机的规范,
JVM规范,Java SE 12版 。
我们将对第4节“类文件格式”感兴趣。
为了确定以什么顺序加载哪些字段,我们引入了@FieldOrder批注(index = ...)。 我们需要为加载程序明确指出字段的顺序,因为规范并不能保证我们将它们保存在二进制文件中的顺序。
Java .class文件以4个字节的幻数开头,两个字节为Java的次要版本,两个字节为主版本。 我们将魔术数字包装在int变量中,并将次要和主要版本号打包在一起:
@FieldOrder(index = 1) private int magic; @FieldOrder(index = 2) private short minorVersion; @FieldOrder(index = 3) private short majorVersion;
.class文件中还包含常量池(两个字节的变量)和常量池本身的大小。 我们引入@ContainerSize批注以声明数组和列表结构的大小。 大小可以是固定的(我们将通过value属性设置)或具有可变长度,该长度由先前读取的变量确定。 在这种情况下,我们将使用“ fieldName”属性,该属性指示我们将从哪个变量读取容器的大小。 根据规范(第4.1节,
“ ClassFile Structure”),常量池的实际大小与值相差1
将其写入constant_pool_count:
u2 constant_pool_count; cp_info constant_pool[constant_pool_count-1];
为了解决此类更正,我们在@ContainerSize批注中引入了一个额外的Corrector属性。
现在我们可以添加一个关于常量池的描述:
@FieldOrder(index = 4) private short constantPoolCount; @FieldOrder(index = 5) @ContainerSize(fieldName = "constantPoolCount", corrector = -1) private List<ConstantPoolItem> constantPoolList = new ArrayList<>();
对于更复杂的计算,您只需添加一个get方法即可返回所需的值: @FieldOrder(index= 1) private int containerSize; @FieldOrder(index = 2) @ContainerSize(filed="actualContainerSize") private List<ContainerItem> containerItems; public int getActualContainerSize(){ return containerSize * 2 + 3; }
恒定池
常量池中的每个元素要么是对类型为int,long,float,double,String的相应常量的描述,要么是对Java类的组件之一的描述-类字段(字段),方法,方法签名等。 术语“常量”在此处表示代码中使用的未命名值:
if (intValue > 100500)
100500的值将在常量池中表示为CONSTANT_Integer的实例。 Java 12的JVM规范定义了可以在常量池中的17种类型。
在我们的实现中,我们将创建一个ConstantPoolItem类,其中将有一个单字节字段标记,该标记确定当前正在读取的结构。 对于上表中的每个元素,创建一个Java类,即ConstantPoolItem的后代。 通用二进制文件加载器应能够基于已读取的标签来确定应使用哪个类。
(通常,标签可以是任何类型的变量)。 为此,请定义HasInheritor接口,并在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(); } }
通用加载器将实例化所需的类并继续阅读。 唯一的条件:后继类中的索引必须与父类具有端到端编号。 这意味着在所有ConstantPoolItem的FieldOrder派生类中,注释的索引必须大于1,因为在父类中,我们已经读取了数字为“ 1”的标记字段。
.class文件的结构(续)
在.class文件中的常量池的元素列表之后,有一个两字节的标识符定义了该类的详细信息-该类是否是注释,接口,抽象类,是否具有最终标志等。 其后是定义该类的两字节标识符(对常量池中元素的引用)。 该标识符必须指向ClassInfo类型的元素。 给定类的超类以类似的方式定义(在类定义中单词“ extends”之后指出)。 对于没有显式定义超类的类,此字段包含对Object类的引用。
在Java中,任何类都只能有一个超类,但是数量
此类可以实现几个接口:
@FieldOrder(index = 9) private short interfacesCount; @FieldOrder(index = 10) @ContainerSize(fieldName = "interfacesCount") private List<Short> interfaceIndexList;
interfaceIndexList中的每个元素代表一个指向常量池中的元素的链接(如指定
索引应该是ClassInfo类型的元素)。
类变量(属性,字段)和方法由相应的列表表示:
@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;
Java .class文件的描述中的最后一个元素是类属性的列表。 可以在此处列出描述与类,嵌套类等相关的源文件的属性。
Java字节码以big-endian表示形式处理数字数据,默认情况下将使用此表示形式。 对于带有小尾数的二进制格式,我们将使用LittleEndian批注。 对于没有预定义长度的字符串,但是
在终端字符(如C一样的以null终止的字符串)之前读取,我们将使用
@StringTerminator注释:
@FieldOrder(index = 2) @StringTerminator(0) private String nullTerminatedString;
有时在基础类中,您需要从更高级别转发信息。 methodList中的Method对象没有有关其所在类的名称的信息;此外,method对象不包含其名称和参数列表。 所有这些信息都以常量池中元素的索引形式显示。 这对于虚拟机来说已经足够了,但是我们想要实现toString()方法,以便它们以一种人类友好的形式显示有关该方法的信息,而不是以常量池中元素的索引的形式显示。 为此,Method类必须获取对ConstantPoolList和具有thisClassIndex值的变量的引用。 为了能够将链接传递到嵌套的基础层,我们将使用Inject注释:
@FieldOrder(index = 14) @ContainerSize(fieldName = "methodsCount") @Inject(fieldName = "constantPoolList") @Inject(fieldName = "thisClassIndex") private List<Method> methodList;
在当前类(ClassFile)中,将为constantPoolList和thisClassIndex变量调用getter方法,而在接收类(在本例中为Method)中,将调用setter方法(如果存在)。
通用引导程序
因此,我们有一个HasInheritor接口和五个批注@ FieldOrder,@ ContainerSize, LittleEndian , Inject和@StringTerminator,它们使我们能够以较高的抽象级别描述二进制结构。 有了正式的描述,我们可以将其传递给通用加载器,通用加载器可以实例化所描述的结构,解析二进制文件并将其读入内存。
因此,我们应该能够使用以下代码:
ClassFile classFile; try (InputStream is = new FileInputStream(inputFileName)) { Loader loader = new InputStreamLoader(is); classFile = (ClassFile) loader.load(); }
不幸的是,对于池中的八字节值,Java平台开发人员过于复杂。
为两个单元格提供了常量,第一个单元格必须包含一个值,第二个单元格保持不变
空的。 这适用于长常量和双常量。
JVM规范中的描述所有8字节常量在类的constant_pool表中占据两个条目
文件。 如果CONSTANT_Long_info或CONSTANT_Double_info结构是条目
在constant_pool表中的索引n处,则表中的下一个可用条目是
位于索引n + 2处。 constant_pool索引n +1必须有效,但必须考虑
无法使用。
显然,Java开发人员希望应用某种低级优化,但后来
人们认识到这个设计决定
不成功回想起来,让8字节常量采用两个常量池条目是一个糟糕的选择。
为了处理这些特定情况,我们将添加@EntrySize批注,我们将使用它,
标记八字节常量:
@EntrySize(value = 2, index = 1) public class EightByteNumberInfo extends ConstantPoolItem { @FieldOrder(index = 2) private int highBytes; @FieldOrder(index = 3) private int lowBytes; }
value属性指示元素将占用的单元格数量,index-元素的索引,
其中包含值。 LongInfo和DoubleInfo类将扩展EightByteNumberInfo类。
通用引导程序将需要使用支持@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); } }
用ClassFileLoader加载类后,可以停止调试器并在IDE的变量检查器中检查加载的类。
该类文件将如下所示:

常量池是这样的:

结论
可以读到最后的任何人都可能想要用自己的双手挑选Java字节码。 随意转到github并下载Java类文件的描述作为一组Java类: https : //github.com/esavin/annotate4j-classfile 。 通用加载程序和注释在这里: https : //github.com/esavin/annotate4j-core 。
要下载已编译的类文件,请使用annotate4j.classfile.loader.ClassFileLoader加载器。
大多数代码是为Java 6编写的,我只将常量池修改为现代版本。 我没有能力和欲望完全实现Java操作码的Java加载器,因此这部分只有很小的发展。
使用这个库(核心部分),我设法用Holter监视数据(每日心脏活动的ECG研究)还原了二进制文件。 另一方面,我无法解密用Delphi编写的一个记帐系统的二进制协议。 我不了解日期的传输方式,有时会出现一种情况,即实际数据与先前值建立的结构不符。
我试图为ELF格式(在Unix / Linux上可运行的格式)构建类似于Java类文件的模型,但是我无法完全理解该规范-事实证明这对我来说太模糊了。 相同的命运落在JPEG和BMP格式上-一直以来,我在理解规范方面遇到一些困难。