Java反编译、逆向工作技术
反编译
1、恢复意外丢失的代码
2、了解一种特性的窍门和实现
3、排除不具有良好稳当说明的应用程序或库文件中的Bug
4、修复不存在源代码的第三方代码中的紧急Bug
5、学习保护自己的代码免于破译
编译Java源代码会生成中间字节码,这是一种与平台无关的源代码表示方式。字节码携带了在源文件中可以找到的所有重要信息。尽管注释和格式丢了,但所有方法变量和编程逻辑都完好的保留下来。如果字节码被混淆了,混淆程序给出的名称会导致编译时的模棱两可。在加载时,字节码是经过验证的,字节码验证器就不像编译器那样严格,混淆程序就可以利用这一优势更好的保护知识产权。
混淆是将字节码变换成难以读懂的格式,典型的混淆包括去除所有的调适信息,例如:变量表和行编号以及机器生成名称的重命名包、类和方法。调试信息对运行类是没有用的,但调试程序用它将字节码和源代码结合起来。先进的混淆器则更进一步,通过重构现有的逻辑和插入不执行的伪代码来改变控制流程。混淆的前提是变换不会破坏字节码的有效性,也不会改变所展示的功能性。
混淆的可行和反编译的可行原因相同:Java字节码是标准化的,而且是很好归档的。好的方法名称对于开发和维护是非常关键的,但对于JVM则毫无意义。
混淆器
1、删除调试信息
2、对类方法名称的处理
3、对Java字符串编码修改
4、改变控制流
5、插入讹用的代码
6、压缩,删除未使用的代码
7、优化字节码
8、堆栈更棕的重构
封装:
一个类的私有方法和私有成员变量不能被其它类静态引用,然后Java具有一种强大的反射机制,使得在运行时可以查询以及访问变量和方法。由于反射是动态的,因此编译时的检查就不适用了,取而代之的是Java运行时依靠一种安全管理器来检验调用代码对某一特定的访问而言是否有足够的权限。安全管理器提供了足够的保护,因为所有的反射API函数在执行自身逻辑之前都必须委派给它。破坏这种保护机制的原因是安全管理器常常没有被设置。默认情况下,安全管理器是没有被设置的,除非代码明确的安装一个默认的或定制的安全管理器,否则运行时的访问控制检查并不起作用。即使设置了安全管理器,典型情况是通过一个政策文件来配置它,这一政策文件可以扩展为允许访问反射的API。
跟踪:
跟踪技术的准则包括确保每个跟踪都包括时间、类和方法名,这是产生跟踪信息的基本条件;当第一次捕捉到异常事件要输出堆栈信息;一定要加入参数值和环境变量,这将有助于进行跟踪时更好的了解执行环境。
当前有两种主要类型的日志API,第一个是Apache的Log4j;另外一种是Sun公司的Java日志API,它已经被制定为官方标准,无论好坏与否,JCP已经决定制定一个新的标准而不再适用Log4j。它缺少Log4j中包含的一些特性:记录到系统日志中;可根据日期自动轮换;类似C语言的输出格式;可以重新加载配置文件。
滥用跟踪技术的做法有:频繁使用System.out.println写入信息,给程序带来永久的花销而缺乏灵活性;跟踪成百上千次的循环中;跟踪到会频繁调用的小方法中;使用Exception.printStackTrace()输出异常事件。
Java安全
运行时的各种核心类使用安全框架核查调用者是否被允许执行所有请求的操作。在安全模型的中心是一个java.lang.SecurityManager的实例,担当java.security包的一个门面。Java使用了授权许可的概念表示对系统信息和资源的访问。
有关类的许可授权信息存储在Java政策文件中,系统范围的java.policy文件首先从${java.home}/lib/security目录中加载,其中${java.home}是JRE的安装目录。如果存在,接下来就加载${user.home}/java.policy文件。定制的Java政策文件可以在命令行中用参数-Djava.security.policy来指定。Java安全包包括JCE加密、验证和JAAS授权、安全套接字支持JSSE和其它API。
性能分析
这个术语以前被用于说明测量方法执行次数以发现和修复性能瓶颈的过程。在Java世界中,它还包括收集各种度量信息并允许运行时线程和对象的调试。使用它的原因:
1、研究堆栈的使用和垃圾回收频率以提高性能
2、浏览对象分配和引用以发现和修复内存泄漏
3、研究线程分配和同步以发现死锁和数据竞争问题、提高性能
4、识别开销大的方法以提高性能
5、研究在运行时应用程序以便更好的理解其内部结构
当没有足够的空闲内存来满足分配要求时,JVM会运行第一代垃圾回收以释放由不再使用的对象所占有的内存。第一代垃圾回收只会查看最近分配的对象,目的是为了避免耗费时间遍历整个对象图。如果释放的内存还不够,那么就会运行完成的垃圾回收,完整垃圾回收从对象树的根目录开始进行,通常是那些活动栈和静态变量引用的对象,完整垃圾回收识别所有可以从根目录到达的对象,而那些不能从根目录到达的对象就会被回收。如果完整垃圾回收还不能释放足够的内存,JVM就会检查是否允许从操作系统中分配更多的内存。如果达到允许的最大运行限值或者操作系统不能提供更多的内存,将会发出一个OutOfMemory的异常事件。
负载
可用性规定了可服务时间的要求,描述应用程序在不重新启动的前提下需要运行多长时间;可伸缩性规定了随着请求数量增加而应用程序能够提供同样水平服务的能力;容错性规定了当应用程序的一个组件出现错误时应用程序能够继续提供相同水平服务的能力。
忽略负载测试是一种危险的习惯,特别是如果希望应用程序成为成百上千用户提供服务时。有些错误只会在有一定负载情况下才会出现,这可能是由于运行依赖于像线程、数据库连接和内存等资源的缘故。多线程应用程序内部的大多数问题都发生在一个特别的同时请求数量出现的时候。
窃听
Http消息由消息头和内容构成,通过TCP/IP协议在网上发送,客户端能够与服务器通话,它必须知道服务器的主机名称或IP地址和服务器监听的端口,Http消息以纯文本形式发送,所以人很容易阅读和理解。
所有分布式通信都要通过一个由JVM、操作系统和网络驱动支持的协议层。协议是批次堆叠的,意思就是较高层的协议依赖于底层协议执行更为基础的任务。这样Http依赖于TCP,后者依赖于IP。单个Http消息可以由几个底层的IP包表示,网络层的工作就是将包分解然后重新组装。大多数物理网络是由互相连接的工作站以太网构成的。在每个段里,包都要从一个节点发送到所有的节点而不管目标节点地址。为了与节点外的计算机通信,路由器将包重定向到其它段。简而言之,当一台主机上的应用程序与不同的主机上的远程应用程序互相通信时,协议包在到达目标之前必须穿过很多其它的网络主机,网络嗅探和监视就是利用这一原理暗中监视通信的。
大多数服务应用程序都使用数据库存储和检索数据,了解应用程序如何与数据库相互作用以及使用了哪些SQL语句对于性能调整和逆向工程都有很大的帮助。占绝对优势的技术选择一直是JDBC。JBDC API提供了一种在驱动级别通过DriverManager’s setLogWriter方法记录数据库操作的方法,它输出诸如驱动注册、创建数据库连接的URL和连接类名称等信息。驱动管理日志的问题是它不提供最重要的信息,例如SQL声明和传递到数据库的值。
理论上,我们还可以浏览应用程序的源代码文件或者字节码收集所有存储为字符串的SQL语句。还可以依靠数据库自身提供的SQL语句记录,但它需要对数据库的管理特权,而且如果多个应用程序共享同一个数据也并不是很容易的事。
类加载
大部分Java API都是用Java编写,只有一小部分本机代码通过JNI访问。Java应用程序包括字节码和本机动态库文件。本机库文件在Windows中以动态链接库dll形式发布,在Unix中以共享库so形式发布。要执行一个Java程序,JVM需要使用类加载程序加载和初始化系统和应用类,它能导致应用程序本机库文件的加载。
控制这个过程的能力运行时的字节码修改和字符完整性保护等。类加载程序是以链条的形式组织起来的,因此当试图自己寻找类之前,子类需要让父类寻找它。通常,类加载程序首先会检查是否已经加载并初始化了该类,这被称之为双亲委派模式。
Java允许应用程序控制如何创建以及利用定制或用户自定义的类加载程序加载类。例如,Web和应用服务器为每个部署的应用程序分配一个自己的类加载程序,以更好的隔离开运行在同一JVM内的逻辑应用程序。定制的类加载器还允许Web和应用服务器程序在不重新启动JVM的情况下重新加载类。另一种强大技术是使用定制的类加载程序即使创建类或在Java运行时定义类之前改变类的二进制结构。
控制流
当没有活动线程时,JVM处理正常的终止。线程作为daemon运行,不禁止JVM关闭。在包括Swing GUI和RMI服务程序的多线程应用程序中,不容易通过让所有线程都适当的结束来实现整洁关闭。常常是用System.exit强制关闭JVM并终止处理。依赖System.exit已经成为了不很复杂的程序中一个惯例。但当一个Web应用程序对exit的无意调用可能会导致Web服务程序突然停止,并可能会禁止用户访问其它的Web应用和静态HTML网页。
看看exit()的源代码,它所做的第一件事就是检查是否安装了安全管理器,如果已经安装,exit()方法核实调用者是否具有退出JVM的许可,不允许就抛出非校验的SecurityException来改变控制流。
通常JVM的关闭是用户通过Unix上的kill命令或者Windows上的Ctrl+C信号启动的,JVM也会由于用退出登录或操作系统被关闭而关闭。自JDK1.3以来应用程序可以使用java.lang.Runtime的addShutdownHook()方法安装一个关闭钩子,关闭钩子是已经初始化但还没有启动的线程实例,Hook能够访问所有的Java API,而且对微妙的JVM状态非常敏感,Hook线程不应该执行任何耗时操作而且应该是线程安全的。它的用途就是在关闭日志文件之前将记录写入日志文件并释放所有其它资源(数据库连接或打开的文件)。
JVMPI(JVM Profiler Interface)首先出现在JDK1.2中,在1.4得到进一步发展。API是一种双向接口,规定了虚拟机应该将虚拟机内的时间通报给性能测量工具的代理(如线程启动、方法调用很内存分配等等)。API还为Profiler规定了获得关于JVM状态信息和配置它感兴趣的事件的接口。性能测量工具的代理在JVM内运行,所有的API方法均通过JNI调用C类型函数。要访问API,JVM必须以-XrunProfilerLibrary参数启动,其中ProfilerLibrary是需要就加载的本机库文件的名称。
字节码
BCEL提供了一套面向对象的API,用于处理组成类的结构和字段,它可以用于阅读现有类文件并用分层的对象表示之;通过添加字段、方法和二进制代码转换类的表达方式,程序化的从头生成新类。类的表达方式可以存储在文件中或作为字节数组传递给JVM,支持运行中的增强和生成。BCEL创建一个完整的目标树表示二进制类文件,一直到单独的字节码指令。所以,它可能会为一个类文件创建数百个对象,从而导致性能下降。
ASM采用了访问者设计模式,避免在不需要的时候实例化对象。框架提供的类分析程序调用用户定义的访问者类传递方法和字段数据作为参数。对于大多数参数,访问者实现只是简单的传递到下一个访问者,保持数据的二进制格式,对于需要改进的字段或方法,访问者实现从框架中获得对象表达式然后对对象进行操作。这种情况下,绝大部分字节码保持二进制格式,并且运行开销最小。
C语言源代码是直接编译成二进制机器码的,不像Java字节码需要有JIT进一步编译或解释。机器码是处理器可以直接执行的代码,其直接含义就是已编译的可执行文件职能运行在构建它的处理器体系结构上。间接含义是不容易将机器码反编译回源代码。没有如何用机器指令表示C语言成分的标准,并且每种编译期都会进行不同的优化,所以对二进制可执行文件进行逆向工程的唯一途径就是在汇编语言层进行处理。