-
-
[原创]java用safenet与wibu加壳的原理介绍
-
发表于: 2012-9-3 11:14 11944
-
0.1 读者对象
菜鸟入门 ,大牛们不用进了。呵呵
0.2 参考文档
《Think in Java 2E》
《The JavaTM Virtual Machine Specification》
0.3 术语与缩写解释
缩写、术语 解 释
●
[*]class文件:java源代码编译后生成的文件,可执行(由java.exe加载)
[*]jar包:多个class文件和资源、清单文件打成的包,可双击执行。
[*]字节码:java虚拟机(JVM)的指令。
[*]Native代码:CPU的机器指令。
1. 背景介绍
研究SafeNet和Wibu的Java程序保护功能的实现。
2. 技术研究目标
掌握Java外壳的实现原理。
3. 技术研究取得的工作成果
概述:Java的编译、运行、打包、调试和运行机制
●
[*]说明
[*]环境|JDK(Java SE 1.6.0_18), OS=Win7 | 备用IDE:Net Beans 6.8
[*]编译|jdk1.6.0_18\bin\javac -g hello.java|在源代码同目录下生成.class文件 调试信息
[*]运行|java -classpath . hello|源代码中没有指定package.命令行中不能输入.class扩展名
[*]运行|java -classpath E:\ com/world/hello|源码中有package com.world;假设class文件在E:\com\world;
[*]|java -classpath . com/sun/sample/scriptpad/Main|package .com.sun.sample.scriptpad;注意以上例子中'/'字符,切不可写成'\'
[*]打包|echo "Main-Class: com.world.hello" >manifest.mf
jar -cvfm e:\hello.jar e:\manifest.mf com
java -jar e:\hello.jar|创建manifest文件(结尾须有\n);
以package根目录为根目录进行打包;
打好的包可用java直接运行
[*]调试|jdb -classpath e:\classes|设欲调试的文件是e:\class\com\world\hello.class
[*]调试|> stop in com.world.hello.main|下断点
[*]调试|> run com/world/hello are you readyVM已启动,设置延迟断点com.world.hello.main|开始调试
[*]调试|> next 已完成步骤:...hello.main(),line=6,bci=8|执行一行源代码其他命令:step、stepi 均与gdb类似
[*]调试|>print args[0]args[0] = "are"|显示变量的值同义命令:eval
[*]调试|>list|察看源代码,与相同
[*]调试|>stop 断点集:com.world.hello.main|显示当前所有断点
[*]|> where (显示堆栈) [1]javax.script.ScriptEngineManager.init (ScriptEngineManager.java:73)
[2]javax.script.ScriptEngineManager.<init>(ScriptEngineManager.java:51)|
[*]调试|>classes |显示当前已加载哪些类
[*]调试|>methods <className>|列出指定的类的方法
[*]调试|>fields <className> |列出指定的类的成员变量
[*]调试|>locals |列出当前堆栈帧内的实参、局部变量
运行机制说明
图1
3.1 WiBu的外壳保护(AxProtector6.40)
图2
图3
以上是威步AxProtector的Java保护相关的功能界面,其中,
防调试:JVMPI检测、Callback检测:防止执行期监视到class的字节码;
基于类的加密: 黑白名单:默认是加密main类,可选加密其他的类;
重命名加密后的类:文件自动加.wibu扩展名;
总体思路是对整个.class文件加密,然后提供ClassLoader,
配合Native代码在执行期解密。
实测情况如下:
原始包test.jar
加壳后protect.jar
说明
原包中文件都在,其中.class文件被加密,其他文件的名字和内容都没有变。
新增了com/wibu目录,多出了许多类,
其中ClassLoader.class.wibu是加密的,其他类可反编译出源码。
com/wibu/xpm/encrypted文件记录了被加密的类;vm.properties列出了虚拟机的参数,应是执行期检测所用。
原Manifest.fm文件指定MainClass:
com.sun.sample.scriptpad.Main
加壳后Manifest.fm文件MainClass:
com.wibu.xpm.Wrapper
也就是jar包入口点变成了xpm.Wrapper,到这里后,再调用原来的入口。
运行机制:外壳从xpm.Wrapper开始运行,先调用Native代码(dll)加载自己提供的ClassLoader,再使用后者解密并加载原包,没有发现混淆等其他处理。
详细信息和调试过程见附录D
3.2 SafeNet的保护功能(Envelope5.1)
图xx
功能概述:
基于方法的保护,先在左边列表框中勾选要保护的方法,右边指定对该方法的加密选项。
1、 FeatureID、Frequency: 加密锁验证,License检测用;
2、 String encryption: 对类名、方法名称加密;
3、 Cache expiration: 执行期每隔一段时间,重新动态生成加密类;
4、 Code obfuscation: 流程混淆、代码扰乱等;
5、 Symbol obfuscation: 对变量名称等符号混淆
限制:
构造函数不能保护;
public方法和静态方法不能符号混淆
实测情况如下:
在内存中抓取动态生成的类代码,发现是混淆过的,对比如下;调试过程见附录E。
源程序SaveClass.java:
脱壳后的class文件有4个,一个是总壳(下图左侧),其他3个是原程序中的3个函数,都自占一个类。(下图右侧,类名方法名都是动态生成的)
总结:
通过动态生成,防止了静态反编译;(上图右面的代码不在jar包的class文件中)
通过每次调用动态生成,防止了一次从内存中抓到全部的字节码映像;
通过符号混淆,隐藏原设计。(如上图左侧F786B31A...函数,原为私有的priSaveIt)
通过流程扰乱,隐藏原设计。(如上图右下,priSaveIt的对应物,对f.write和
f.close的调用被拆散到新增的两个函数中)。
通过直接插入字节码(跳转、堆栈操作、废指令),迷惑反编译器(如上图红框中部分,
对照字节码,实际分别是this.name=s和goto 103/废指令(不可到达的代码))。
通过使用关键字作为符号名称,使调试更为困难。
附录A:辅助工具
名称
主要用途
说明
Jad1.5.8g
将.class文件反编译为源代码
命令行字符界面,用法:
Jad [-r][-a][-f] my.class
由于字节码的文件格式中,包含各种明文符号表,
未经保护的普通.class文件几乎可以被完全还原为源代码。
输出my.jad文件,改名为.java即可察看和编译
-r参数是保留原有的package相对路径。
-a参数是提供字节指令作为注释。
-f参数是提供所有外部引用的全称类名。
proGuard
jasmin
附录B:虚拟机原理
JVM 模型
class文件格式
struct const_pool {
ushort const_cnt_plus1;
struct {
byte tag; //1=Utf8, 7=class, 8=string, 9=FieldRef, A=MethodRef...
byte data[depend_on_tag];
} consts[const_cnt];
};
类型的编码规则
I: 整数; J: long; S:short; Lclassname:对象
函数原形编码规则
(参数类型;参数类型;...)[返回值类型|V表示void]
例如clsIdx=03, n&t= append,-(Lj/l/String;)Lj/l/StringBuilder;
表示 StringBuilder StringBuilder.append(java.lang.String);
一个class文件的例子
constant pool
索引
Tag(类型)
说明 (蓝色是解析一次索引得出, 青色是解析两次索引得出)
1
A(mthRef)
clsIdx=0A(java/lang/Object), n&tIdx=14(<init>,()V)
2
9(fldRef)
clsIdx=15(java/lang/System), n&tIdx=16(out,Ljava/io/PrintStream;)
3
7(Class)
nameIdx=17 (java/lang/StringBuilder)
4
A(mthRef)
clsIdx=03(java/lang/StringBuilder), n&tIdx=14(<init>,()V)
5
8(String)
nameIdx=18 (Hello )
6
A(mthRef)
clsIdx=03, n&tIdx=19 (append,-(Lj/l/String;)Lj/l/StringBuilder;)
7
A(mthRef)
clsIdx=03, n&tIdx=1A (toString, ()Ljava/lang/String;)
8
A(mthRef)
clsIdx=1B(java/io/PrintStream), n&tIdx=1C(println,(Lj/l/String;)V)
9
7(Class)
nameIdx=1D(com/world/hello)
A
7(Class)
nameIdx=1E(java/lang/Object)
B
1(Utf8)
"<init>"
C
1(Utf8)
"()V"
D
1(Utf8)
"Code"
E
1(Utf8)
"LineNumberTable"
F
1(Utf8)
"main"
10
1(Utf8)
"([Ljava/lang/String;)V"
11
1(Utf8)
"StackMapTable"
12
1(Utf8)
"SourceFile"
13
1(Utf8)
"hello.java"
14
C(n&t)
nameIdx=0B, descIdx=0C
15
7(Class)
nameIdx=1F
16
C(n&t)
nameIdx=20, descIdx=21
17
1(Utf8)
"java/lang/StringBuilder"
18
1(Utf8)
"Hello "
19
C(n&t)
nameIdx=22, descIdx=23
1A
C(n&t)
nameIdx=24, descIdx=25
1B
7(Class)
nameIdx=26
1C
C(n&t)
nameIdx=27, descIdx=28
1D
1(Utf8)
"com/world/hello"
1E
1(Utf8)
"java/lang/Object"
1F
1(Utf8)
"java/lang/System"
20
1(Utf8)
"out"
21
1(Utf8)
"Ljava/io/PrintStream;"
22
1(Utf8)
"append"
23
1(Utf8)
"-(Ljava/lang/String;)Ljava/lang/StringBuilder;"
24
1(Utf8)
"toString"
25
1(Utf8)
"()Ljava/lang/String;"
26
1(Utf8)
"java/io/PrintStream"
27
1(Utf8)
"println"
28
1(Utf8)
"(Ljava/lang/String;)V"
Methods[1]
struct method_info {
值
u2 access_flags;
09 (static | public)
u2 name_idx;
0F (main)
u2 desc_idx;
10 ([Ljava/lang/String;)V) = void ()(String[])
u2 attr_cnt;
01
struct code_attr {
u2 name_idx;
0D (code)
u4 attr_len;
5D
u2 max_stack;
04
u2 max_local;
02
u4 code_len;
2A
u1 code[code_len];
03 3C 1B 2A BE A2 00 24-B2 00 02 BB 00 03 59 B7
00 04 12 05 B6 00 06 2A-1B 32 B6 00 06 B6 00 07
B6 00 08 84 01 01 A7 FF-DC B1 (解释见下表)
u2 except_tbl_len;
00
e[except_tbl_len];
u2 attr_cnt;
02
u2 name_idx;
0E (LineNumberTable)
u4 attr_len;
12
u2 tbl_len;
04
u4 map[tbl_len]
0->5 8->6 23->5 29->8
u2 name_idx;
11 (StackMapTable)
u4 attr_len;
09 (JVM1.6中新增属性,与执行期基本无关)
}}};
上述方法的字节码:
地址
指令
助记符
执行后当前堆栈
源代码
00
03
iconst_0
i (未初始化)
(String[])args
RetAddr
(int)0
for(
int i=0;
i < args.length;
i++)
{
01
3c
istore_1
i (=0)
(String[])args
RetAddr
02
1b
iload_1
i
(String[])args
RetAddr
(int)0
03
2a
aload_0
i
(String[])args
RetAddr
(String[])args
(int)0
04
be
arraylength
i
(String[])args
RetAddr
(int)(args.length)
(int)0
05L1:
a2 00 24
if_icmpge L2
i
(String[])args
RetAddr
08
b2 00 02
getstatic
i
(String[])args
RetAddr
system.out
System.out.println(
"Hello "+args[i]);
}
0B
bb 00 03
new
StringBuilder
i
(String[])args
RetAddr
StringBuilder(未初始化)
system.out
0E
59
dup
i
(String[])args
RetAddr
StringBuilder(未初始化)
StringBuilder(未初始化)
system.out
0F
B7 00 04
invokespecial
<init>
i
(String[])args
RetAddr
StringBuilder(null)
system.out
12
12 05
ldc
i
(String[])args
RetAddr
(String)"Hello "
StringBuilder(null)
system.out
14
B6 00 06
invokevirtual
append
i
(String[])args
RetAddr
(StringBuilder)"Hello "
system.out
17
2a
aload_0
i
(String[])args
RetAddr
(String[])args
(StringBuilder)"Hello "
system.out
18
1b
iload_1
i
(String[])args
RetAddr
(int)i
(String[])args
(StringBuilder)"Hello "
system.out
19
32
aaload
i
(String[])args
RetAddr
(String)(args[i])
(StringBuilder)"Hello "
system.out
1A
B6 00 06
invokevirtual
append
i
(String[])args
RetAddr
(StringBuilder)"Hello?"
system.out
1D
B6 00 07
invokevirtual
toString
i
(String[])args
RetAddr
(String)"Hello?"
system.out
20
B6 00 08
invokevirtual
println
i
(String[])args
RetAddr
23
84 01 01
iinc local1,1
i
(String[])args
RetAddr
26
A7 FF DC
goto L1
29L2:
B1
return
附录C:调试技术
首先,需要准备源代码,并将其-g编译以提供debug信息;
下断点有两种方式:stop in 类名.方法名 和 stop at 类名:行号;后一种方式需要提供源代码文件;
单行、查看参数和局部变量都需要有编译器调试信息支持,而查看this成员变量不需要;
欲调试没有源代码的类,可以先观察其调用的运行库等,然后-g编译对应的运行库相关函数,调试时在运行库中下断点,间接取得执行期部分变量的值。
重新编译或patch运行库:从jdk\src.zip中提取对应的源代码,修改,-g编译之,然后jar -cvfm重新打包,覆盖jdk\jre\lib\rt.jar.
如果方法是重载的,则简单下断点会有问题;可以用methods命令查看所有方法的原形,然后用stop in 类名.方法名(参数列表)来下断点。
遇到invokevirtual时要仔细,可结合上下文、print命令和跟踪等,获得对象的实际类型,以免被多态所欺骗而跑飞。
类的外部接口终究要保存在class文件的constant pool中,因此,若想在整个jar包中寻找是哪个类调用了某个外部接口,可以以不压缩方式重打jar包,然后Hex编辑器搜索那个字符串,再搜索CaFeBaBe,对应起来即知。
附录D:威步的调试过程
测试步骤
发生现象
说 明
其他机器上,运行加壳的包
提示找不到某个DLL
察看Wrapper.class反编译源代码,
得知是system32\wibuXpm4J32.dll
Depends工具查上述DLL
得到8个导出函数的地址
其中有函数名为ClassLoader
UltraEdit修改上述DLL,
将ClassLoader函数入口处字节改为0xCC
WinDbg调试
JavaW.exe -jar protect.jar
时在该处断下。
CS:EIP = 0x20011B20
在内存中搜索ClassLoader.class.wibu文件内容,下读写访问断点
断下,CS:EIP = 0x20021640
察看调用堆栈,发现AES算法和字节表
继续跟踪解密,但结果还不是class码
AES参数:11轮,128位密钥,CBC方式
初始密码:"AxProtector/Java"
在解密出的数据下访问断点
断下,CS:EIP = 0x2001D930
发现LZW解压缩代码
继续跟踪,保存解压后数据,反编译
得到wibu的ClassLoader源代码
继续执行
在导出函数入口处断点又触发
重复跟踪解密解压
得到原jar包Main.class代码
未发现混淆处理,反编译,脱壳成功。
附录E:SafeNet的调试过程
步骤
现象/结果/说明
jdb -classpath e:\test.jar;e:\
正在初始化jdb...
E:\test.jar即SafeNet加壳后的包
E:\java指向运行库源代码,调试信息
>stop in javax.script.ScriptEngineManager.init
ScriptEngineManager为目标程序使用的外部接口
>run com/sun/sample/scriptpad/Main
断点命中: "thread=main",
javax.script.ScriptEngineManager.init(), line=73 bci=0
>where
注意输出结果中的[7]和[3],它们之间是动态代码
[1] javax.script.ScriptEngineManager.init
(ScriptEngineManager.java:73)
[2] javax.script.ScriptEngineManager.<init>
(ScriptEngineManager.java:51)
[3] DYTAX1FyX6fnllWf.MrmuYWh6v2ZTFkk8未知
[4] sun.reflect.NativeMethodAccessorImpl.
invoke0 (本机方法)
[5] sun.reflect.NativeMethodAccessorImpl.
invoke (NativeMethodAccessorImpl.java:39)
[6] sun.reflect.DelegatingMethodAccessorImpl.
invoke(DelegatingMethodAccessorImpl.java:25)
[7] java.lang.reflect.Method.invoke
(Method.java:597)
[8] com.aladdin.nemesis.a.do (uc:73)
[9] com.aladdin.nemesis.d.b.do (k:344)
[10]com.aladdin.nemesis.d.a.do (j:80)
[11]com.aladdin.nemesis.JANemsis.aldn_
1989undnochda_mwtbdltr (vc:119)
[12]com.sun.sample.scriptpad.Main.main (null)
>stop in java.lang.reflect.Method.invoke
>run ...
>print this
this = "public static void
DYTAX1FyX6fnllWf.MrmuYWh6v2ZTFkk8(
java.lang.String[]) throws java.lang.Exception"
可见this正是[3]步中要调用的函数,但多次调试同一jar包,this的名称却不一样,可见它是动态生成的。
>class DYTAX1FyX6fnllWf
>fields DYTAX1FyX6fnllWf
>methods DYTAX1FyX6fnllWf
类:DYTAX1FyX6fnllWf 扩展:java.lang.Object
** 字段列表为空 **
** 方法列表 **
DYTAX1FyX6fnllWf <init>()
MrmuYWh6v2ZTFkk8(java.lang.String[])
java.lang.Object <init>()
... (一系列Object的方法)
java.lang.Object getClass()
多次调试同一jar包,只有类和那个静态方法的名称不一样,其他接口参数和名称相同。
写一个测试程序hello.java,察看reflector的使用
package com.world;
import java.lang.reflect.*;
class test_refl {
public static void random(String[] args){
for(int i=0; i<args.length; i++)
System.out.println(args[i]);
}
}
public class hello {
public static void main(String[] args)
throws Exception {
Class class1 = test_refl.class;
Method methods[] = class1.getDeclaredMethods();
for(int j=0; j<methods.length; j++)
{
if( methods[j].getName().equals("random"))
methods[j].invoke(null, (Object)args);
}
for(int i=0; i<args.length; i++)
System.out.println("Hello "+args[i]);
}
}
>stop in com.world.test_refl.random
>wherei
[1] com.world.test_refl.random
(hello.java:8), pc = 0
[2] sun.reflect.NativeMethodAccessorImpl.
invoke0 (本机方法)
[3] sun.reflect.NativeMethodAccessorImpl.Invoke
(NativeMethodAccessorImpl.java:39), pc=87
[4] sun.reflect.DelegatingMethodAccessorImpl.
invoke(DelegatingMethodAccessorImpl.java25), pc=6
[5] java.lang.reflect.Method.invoke
(Method.java:597), pc = 161
[6] com.world.hello.main
(hello.java:24), pc = 43
发现[2]~[5]与加壳包的[4]~[7]一样,可认为只是使用了普通的反射接口,这里没有做手脚。
那个类是怎样生成出来的?
> stop in com.aladdin.nemesis.a.do
> run ...
> stop at java.lang.ClassLoader:466
> cont
> locals
方法参数:
name = "GBeKwfkHswafuLV4"
b = instance of byte[28574] (id=447)
off = 0
len = 28574
> print b[0]
> print b[1]
...
CA FE BA BE 00 00 00 31|经观察,基本上是加壳
00 6E 0A 00 18 00 32 07|后的main.class内容
00 33 0A 00 02 00 32 08|略有出入,下文比较。
磁盘上main.class文件
内存中的这个类
数据量(字节数)
28477
28574
const pool项数
0x5F
0x6E
const_pool[5E]
01 68 F0 7F 7F ... 7F
相同
const_pool[5F]
01 00 04 43 6F 64 65 Utf8"Code"
const_pool[60]
Utf8 "java/lang/Object"
const_pool[61]
Utf8 "<init>"
const_pool[62]
Utf8 "()V"
const_pool[63]
Class "java/lang/Object"
const_pool[64]
n&t void <init>(void)
const_pool[65]
mthRefjava/lang/Object.<init>
const_pool[66]
Utf8 "<init>"
const_pool[67]
Utf8 "()V"
const_pool[68]
Utf8 "LyK6SOAcqVROpuS8"
const_pool[69]
Utf8 "([Ljava/lang/String;)V"
const_pool[6A]
Utf8 "GBeKwfkHswafuLV4"
const_pool[6B]
Class "GBeKwfkHswafuLV4"
const_pool[6C]
Utf8 "java/lang/Object"
const_pool[6D]
Class "java/lang/Object"
access_flags
@6E18, 21
@6EB5, 21
this
const_pool[0D]
GBeKwfkHswafuLV4
super
const_pool[18]
java/lang/Object
interface_cnt
field_cnt
method_cnt
0
0
3
0
0
2
method[0]
略
public void <init>(void) code=17
max_stack =1, max_local =2,
code[5]=2A B7 00 65 B1
super(this); return;
method[1]
防止编译:函数重载
防止反编译:插入机器指令
防止调试:
使用java关键字
加入废代码扰乱视线
多态
代码混淆
public static void LyK6SOAcqVROpuS8(String[]) throws(Exception)
code=74, max_stack=3, max_local=3
00: BB 00 02 new 2
04: 59 dup
06: B7 00-03 invokespecial 3
0A: 4C astore_1
0C: 2B aload_1
0E: 12 04 ldc 4
11: B6 00 05 invokevirtual 5
15: 4D astore_2
17: 2C aload_2
19: 12 06 ldc 6
1C: 2C aload_2
1E: B9 00 07 03 00 invokeinterface 7,3
23: 2C aload_2
25: 12 08 ldc 8
28: B8 00 09 invokestatic 9
2C: 2C aload_2
2E: 12 0A ldc 0A
31: B8 00 09 invokestatic 9
35: 2C aload_2
37: 12 0B ldc 0B
3A: B8 00 09 invokestatic 9
3E: 2C aload_2
40: 12 0C ldc 0C
43: B8 00 09 invokestatic 9
47: B1 return
剩余字节都是废的,每条指令与原main函数中的一样,只是都插入一个nop。对const_pool的引用及const_pool对应索引的内容也一样。
动态生成的类型存在那个文件?
将整个jar包解出来,然后以不压缩方式重新打包,
用UltraEdit察看包,区分大小写搜索"File",发现
java/io/File
java/util/zip/ZipFile
java/io/FileInputStream
字样各出现几处,再Hex搜索"CA FE BA BE",将发现的位置与上述对应以下,锁定了以下几个类:
com/aladdin/nemesis/b/a.class
com/aladdin/nemesis/b.class
com/aladdin/nemesis/c/a.class
然后jad观察、jdb下断点观察
断点命中:
[1] java.util.zip.ZipFile.getInputStream
(ZipFile.java:180)
[2] com.aladdin.nemesis.g.a.do (q:300)
[3] com.aladdin.nemesis.d.a.do (j:129)
[4] com.aladdin.nemesis.JANemsis.aldn_1989
(vc:119)
[5] com.sun.sample.scriptpad.Main.main
>print entry.name
"SFNT/A04E68FF9DC2DEF03319SE0A73C11902.neurom"
类似的文件共有5个,都在SFNT目录下,在不同的位置被打开。
jad静态分析,知读出的文件由a.a.a.b.do()处理。
读文件: util.zip.InflaterInputStream.read()
静态分析发现,其解密算法较繁琐,且无法直接重新编译回去进行调试,算法中使用的外部接口只有arraycopy.
arraycopy是native函数,断不了。
故修改运行库,添加System.arraycopY函数,
再修改b.class,将引用arraycopy改为arraycopY.
下断点,观察。
>stop at java.lang.System:456
>cont
断点命中:...
>where
[1] java.lang.System.arraycopY
(System.java:456)
[2] com.aladdin.a.a.a.b.do (xc:385)
[3] com.aladdin.nemesis.g.a.do (q:74)
[4] com.aladdin.nemesis.d.a.do (j:70)
[5] com.aladdin.nemesis.JANemsis.aldn_1989
[6] com.sun.sample.scriptpad.Main.main
>locals
方法参数:
src = instance of byte[72] (id=407)
srcPos = 0
dest = instance of byte[8] (id=408)
destPos = 0
length = 8
查找堆栈得知,这是
.neurom文件,原长88/解密72。
00 00 00 01 "com.sun.sample.scriptpad"
00 "Main"
00 "main"
00 "([Ljava/lang/String;)V
11 dup(0)
.neurot文件,原长1000/解密1000
01 00 00 00 00 00 00 01
,wQjml/6hXnnfwH ...
.neurox文件,原长5664/解密5664
00 00 00 01
L"Sentinel HASP Protection System"
00 00 00 00 00 03
L"Could not find H" ...
.neuroc文件,原长28208/解密28208
"com.sun.sample.scriptpad"
00 "Main" 00 00 00
00 5F 0A 00 18 00 32 07
00 33 0A 00 02 00 32 08
28208=0x6e18+32-sizeof(cafebabe00000032)
.neuron文件,原长250/解密224
00 00 00 BC 00 09 00 20
00 21 00 02 00 1B 00 00
00 A4 00 03 00 03 00 00
00 4A BB 00 02 00 59 00 ...
快速脱壳法: patch java.lang.ClassLoader.java
protected final Class<?> defineClass(
String name, byte[] b, int off, int len)
throws ClassFormatError
{
if (16 == name.length())
saveFile(name, b);
return defineClass(name,b,off,len,null);
}
protected void saveFile(String name, byte[] b)
{
try {
FileOutputStream f = new FileOutputStream(name);
f.write(b);
f.close();
} catch (Exception e){}
}
然后直接运行带壳的jar包,则每个动态生成的类都会
被存成文件放在当前目录下.
2011/08/11 21,073 UjpCHY08VKiqvlKD
2011/08/11 21,011 YK4b2fBIorNcyq71
2011/08/11 21,143 GQwav22YHnxWzwMq
再用jad分别处理即可.
附录F Java对象的内部表示和字节码运行细节
按Java虚拟机规格书所述,任何“对象”都是使用Reference来操作,不限制具体的实现方式。但在字节码的分析中,
可以把对象的reference想象成一个指针,
指向一个结构,
结构的成员就是类的methods和fields,
结构中,父类的对象放在最前面,因此super的“值”等于this,只是类型不一样。
如上图,右面是想象的C表示。
对于构造函数:
每个类都必须有至少一个构造函数;
如果源代码中没有构造函数,则编译器会自动生成一个没有参数的默认构造函数;
构造函数的内部名称都是“<init>”;
编译器会在任何构造函数中插入代码,使其执行顺序如下:
调用父类构造函数;
初始化各成员变量;
源代码中给出初值的,按源代码初始化;
(源代码中没初值的,已在构造函数之前由系统自动初始化为0或null)
构造函数源代码中的语句;
上图左边是源代码,右边是编译后的示意代码。
其他方法(函数):
所有非静态方法,在内部都有一个额外的参数this,在参数列表中排最前。
关于内部类(InnerClass):
内部类可以声明在类定义中、函数中,可以有名字,也可以匿名,还可以是静态的;
在非静态的情况下,内部类的实现大致如下:
1、 和其他类一样,内部类编译后也拥有自己的class文件,名称是外部类名$内部类
名,如果是匿名,则为外部类名$1等依次类推。
2、 内部类有一个隐含的成员变量this$0,类型是外部类。故必须有外部类的对象才能
够获得内部类的对象。
3、 内部类的默认构造函数至少有两个参数,第一个类型是内部类,第二个类型是外部类,
构造函数执行时,先将第二参数值赋给this$0,然后调父类构造等常规动作。
4、 内部类与外部类并没有继承等其他关系,内部类的父类就是源代码中明确声明的它
的父类,如果没有明确指定,则为Object。内部类通过this$0访问外部类的成员
5、 特定情况下编译器会暗自产生匿名内部类,例如下图左的源代码:
编译后产生的内部指令,相当于上图右的。
可见,“通过public方法导出private类型”(内部类是私有的,而方法cont是公共
的),就会产生一个匿名内部类$1。因为私有的内部类,其构造函数也是private,
不能直接调用?编译器就重载了一个,访问权限变为friendly,参数也多了一个,多
出来的参数正是匿名类(可能是防止源代码中正好有同样的参数列表)。
异常处理
每个catch(ExceptType){Handling}块在class文件中,都登记在它所在方法的method_info的code_attribute的exception_table的一项.
struct {
u2 start_pc; //对应try块起始的字节码在函数体内的偏移
u2 end_pc; //对应try块结束~
u2 handler_pc; //catch块起始~
u2 catch_type; //catch的异常类型在const_pool中索引
} exception_table[exception_table_length];
此外,每个用throws声明了可能抛出异常的函数, method_info中会有一项exceptions_attribute, 标明所有可能抛出的异常.
Struct Exceptions_attribute {
u2 number_of_exceptions;
u2 exception_index_table[number_of_exceptions];}
而如果有finally语句,编译器会为匹配的try块添加一个catch(any)块,内容是先执行finally里面的语句,然后将当前异常重新throw,这样,对于当前函数没有catch到的异常,还是先执行finally,然后再向上抛。而try块尾、try块中的return和其他原有的catch块,每个的处理代码后,都被编译器自动加上执行finally. 总的执行顺序是:
try - catch - finally (如果捕捉到)
try - finally - 上级catch (若没捕捉到)
try - finally (没发生异常,无论try块执行完或中途return)
字节码invoke
JVM的字节码中,用于调用函数(方法)的指令共有4个,分别为
invokevirtual (调用一个instance method, 当前对象在编译时类型为class)
invokeinterface(调用一个interface method, 当前对象编译时类型为interface)
invokespecial (调用一个构造函数或私有函数)
invokestatic (调用一个静态函数)
这四个指令的第一操作数都是方法名称在常量池中的索引,invokeinterface还有额外的两个操作数,其他三个指令都只有一个操作数。
Invoke之前,参数压栈的先后顺序与参数列表一致(invokestatic不需要压this,其他三个指令最先压this)。
控制转移到目标方法后,原堆栈帧中的参数都被pop再重新push到当前堆栈帧,成为局部变量,先后次序也与参数列表一致(除静态方法外,其他方法的local var0都是this)
目标方法返回时,执行一组return指令中的一个,视其返回值类型而定:
return指令 (void)
ireturn指令 (int)
areturn指令 (reference)等,
如果有返回值,则areturn/ireturn等指令将返回值放回原堆栈帧的栈顶,若调用者不要返回值,编译器会生成一个pop指令抛弃之。
long和double数据类型的特别处理:
他们总是占用两个JVM word,常量池中占两项,堆栈单元也占两个,须注意。
stackMapTable
附录G:SafeNet的查锁线程
提要: SafeNet保护程序会在动态生成派遣函数中生成两个线程,其中一个是检测加密锁是否存在,另一个可能是监控用的. 若检测不到加密锁,会出提示并冻结其他线程.
测试步骤:
1、 将如下代码保存为hello.java并编译、打包为hello.jar:
package world;
public class hello {
public static void main(String[] args) {
try { Thread.sleep(999999); } catch (Exception e) {}
}
}
2、 使用SafeNet Envelop对hello.jar进行保护,默认设置,除查锁间隔设为5秒外。
3、 运行保护后jar包,用jvisualVM工具观察,比未保护的jar包多两个线程:
4、 jvisualVM的线程dump:
5、 用jad对nemesis/e/c.class和nemesis/s/c.class进行反编译,找到创建线程的代码
6、 JDB调试:
见下图,分别拦截到两个启动线程的调用
都是在JANemsis.aldn_1989...函数中发生的,参照附录E可知,该函数是运行时生成的派遣函数。
7、 jad对nemesis.JANemsis.class和nemesis.d.a.class反编译。发现类似例行检查的机制
若线程不存在,则创建之。
菜鸟入门 ,大牛们不用进了。呵呵
0.2 参考文档
《Think in Java 2E》
《The JavaTM Virtual Machine Specification》
0.3 术语与缩写解释
缩写、术语 解 释
●
[*]class文件:java源代码编译后生成的文件,可执行(由java.exe加载)
[*]jar包:多个class文件和资源、清单文件打成的包,可双击执行。
[*]字节码:java虚拟机(JVM)的指令。
[*]Native代码:CPU的机器指令。
1. 背景介绍
研究SafeNet和Wibu的Java程序保护功能的实现。
2. 技术研究目标
掌握Java外壳的实现原理。
3. 技术研究取得的工作成果
概述:Java的编译、运行、打包、调试和运行机制
●
[*]说明
[*]环境|JDK(Java SE 1.6.0_18), OS=Win7 | 备用IDE:Net Beans 6.8
[*]编译|jdk1.6.0_18\bin\javac -g hello.java|在源代码同目录下生成.class文件 调试信息
[*]运行|java -classpath . hello|源代码中没有指定package.命令行中不能输入.class扩展名
[*]运行|java -classpath E:\ com/world/hello|源码中有package com.world;假设class文件在E:\com\world;
[*]|java -classpath . com/sun/sample/scriptpad/Main|package .com.sun.sample.scriptpad;注意以上例子中'/'字符,切不可写成'\'
[*]打包|echo "Main-Class: com.world.hello" >manifest.mf
jar -cvfm e:\hello.jar e:\manifest.mf com
java -jar e:\hello.jar|创建manifest文件(结尾须有\n);
以package根目录为根目录进行打包;
打好的包可用java直接运行
[*]调试|jdb -classpath e:\classes|设欲调试的文件是e:\class\com\world\hello.class
[*]调试|> stop in com.world.hello.main|下断点
[*]调试|> run com/world/hello are you readyVM已启动,设置延迟断点com.world.hello.main|开始调试
[*]调试|> next 已完成步骤:...hello.main(),line=6,bci=8|执行一行源代码其他命令:step、stepi 均与gdb类似
[*]调试|>print args[0]args[0] = "are"|显示变量的值同义命令:eval
[*]调试|>list|察看源代码,与相同
[*]调试|>stop 断点集:com.world.hello.main|显示当前所有断点
[*]|> where (显示堆栈) [1]javax.script.ScriptEngineManager.init (ScriptEngineManager.java:73)
[2]javax.script.ScriptEngineManager.<init>(ScriptEngineManager.java:51)|
[*]调试|>classes |显示当前已加载哪些类
[*]调试|>methods <className>|列出指定的类的方法
[*]调试|>fields <className> |列出指定的类的成员变量
[*]调试|>locals |列出当前堆栈帧内的实参、局部变量
运行机制说明
图1
3.1 WiBu的外壳保护(AxProtector6.40)
图2
图3
以上是威步AxProtector的Java保护相关的功能界面,其中,
防调试:JVMPI检测、Callback检测:防止执行期监视到class的字节码;
基于类的加密: 黑白名单:默认是加密main类,可选加密其他的类;
重命名加密后的类:文件自动加.wibu扩展名;
总体思路是对整个.class文件加密,然后提供ClassLoader,
配合Native代码在执行期解密。
实测情况如下:
原始包test.jar
加壳后protect.jar
说明
原包中文件都在,其中.class文件被加密,其他文件的名字和内容都没有变。
新增了com/wibu目录,多出了许多类,
其中ClassLoader.class.wibu是加密的,其他类可反编译出源码。
com/wibu/xpm/encrypted文件记录了被加密的类;vm.properties列出了虚拟机的参数,应是执行期检测所用。
原Manifest.fm文件指定MainClass:
com.sun.sample.scriptpad.Main
加壳后Manifest.fm文件MainClass:
com.wibu.xpm.Wrapper
也就是jar包入口点变成了xpm.Wrapper,到这里后,再调用原来的入口。
运行机制:外壳从xpm.Wrapper开始运行,先调用Native代码(dll)加载自己提供的ClassLoader,再使用后者解密并加载原包,没有发现混淆等其他处理。
详细信息和调试过程见附录D
3.2 SafeNet的保护功能(Envelope5.1)
图xx
功能概述:
基于方法的保护,先在左边列表框中勾选要保护的方法,右边指定对该方法的加密选项。
1、 FeatureID、Frequency: 加密锁验证,License检测用;
2、 String encryption: 对类名、方法名称加密;
3、 Cache expiration: 执行期每隔一段时间,重新动态生成加密类;
4、 Code obfuscation: 流程混淆、代码扰乱等;
5、 Symbol obfuscation: 对变量名称等符号混淆
限制:
构造函数不能保护;
public方法和静态方法不能符号混淆
实测情况如下:
在内存中抓取动态生成的类代码,发现是混淆过的,对比如下;调试过程见附录E。
源程序SaveClass.java:
脱壳后的class文件有4个,一个是总壳(下图左侧),其他3个是原程序中的3个函数,都自占一个类。(下图右侧,类名方法名都是动态生成的)
总结:
通过动态生成,防止了静态反编译;(上图右面的代码不在jar包的class文件中)
通过每次调用动态生成,防止了一次从内存中抓到全部的字节码映像;
通过符号混淆,隐藏原设计。(如上图左侧F786B31A...函数,原为私有的priSaveIt)
通过流程扰乱,隐藏原设计。(如上图右下,priSaveIt的对应物,对f.write和
f.close的调用被拆散到新增的两个函数中)。
通过直接插入字节码(跳转、堆栈操作、废指令),迷惑反编译器(如上图红框中部分,
对照字节码,实际分别是this.name=s和goto 103/废指令(不可到达的代码))。
通过使用关键字作为符号名称,使调试更为困难。
附录A:辅助工具
名称
主要用途
说明
Jad1.5.8g
将.class文件反编译为源代码
命令行字符界面,用法:
Jad [-r][-a][-f] my.class
由于字节码的文件格式中,包含各种明文符号表,
未经保护的普通.class文件几乎可以被完全还原为源代码。
输出my.jad文件,改名为.java即可察看和编译
-r参数是保留原有的package相对路径。
-a参数是提供字节指令作为注释。
-f参数是提供所有外部引用的全称类名。
proGuard
jasmin
附录B:虚拟机原理
JVM 模型
class文件格式
struct const_pool {
ushort const_cnt_plus1;
struct {
byte tag; //1=Utf8, 7=class, 8=string, 9=FieldRef, A=MethodRef...
byte data[depend_on_tag];
} consts[const_cnt];
};
类型的编码规则
I: 整数; J: long; S:short; Lclassname:对象
函数原形编码规则
(参数类型;参数类型;...)[返回值类型|V表示void]
例如clsIdx=03, n&t= append,-(Lj/l/String;)Lj/l/StringBuilder;
表示 StringBuilder StringBuilder.append(java.lang.String);
一个class文件的例子
constant pool
索引
Tag(类型)
说明 (蓝色是解析一次索引得出, 青色是解析两次索引得出)
1
A(mthRef)
clsIdx=0A(java/lang/Object), n&tIdx=14(<init>,()V)
2
9(fldRef)
clsIdx=15(java/lang/System), n&tIdx=16(out,Ljava/io/PrintStream;)
3
7(Class)
nameIdx=17 (java/lang/StringBuilder)
4
A(mthRef)
clsIdx=03(java/lang/StringBuilder), n&tIdx=14(<init>,()V)
5
8(String)
nameIdx=18 (Hello )
6
A(mthRef)
clsIdx=03, n&tIdx=19 (append,-(Lj/l/String;)Lj/l/StringBuilder;)
7
A(mthRef)
clsIdx=03, n&tIdx=1A (toString, ()Ljava/lang/String;)
8
A(mthRef)
clsIdx=1B(java/io/PrintStream), n&tIdx=1C(println,(Lj/l/String;)V)
9
7(Class)
nameIdx=1D(com/world/hello)
A
7(Class)
nameIdx=1E(java/lang/Object)
B
1(Utf8)
"<init>"
C
1(Utf8)
"()V"
D
1(Utf8)
"Code"
E
1(Utf8)
"LineNumberTable"
F
1(Utf8)
"main"
10
1(Utf8)
"([Ljava/lang/String;)V"
11
1(Utf8)
"StackMapTable"
12
1(Utf8)
"SourceFile"
13
1(Utf8)
"hello.java"
14
C(n&t)
nameIdx=0B, descIdx=0C
15
7(Class)
nameIdx=1F
16
C(n&t)
nameIdx=20, descIdx=21
17
1(Utf8)
"java/lang/StringBuilder"
18
1(Utf8)
"Hello "
19
C(n&t)
nameIdx=22, descIdx=23
1A
C(n&t)
nameIdx=24, descIdx=25
1B
7(Class)
nameIdx=26
1C
C(n&t)
nameIdx=27, descIdx=28
1D
1(Utf8)
"com/world/hello"
1E
1(Utf8)
"java/lang/Object"
1F
1(Utf8)
"java/lang/System"
20
1(Utf8)
"out"
21
1(Utf8)
"Ljava/io/PrintStream;"
22
1(Utf8)
"append"
23
1(Utf8)
"-(Ljava/lang/String;)Ljava/lang/StringBuilder;"
24
1(Utf8)
"toString"
25
1(Utf8)
"()Ljava/lang/String;"
26
1(Utf8)
"java/io/PrintStream"
27
1(Utf8)
"println"
28
1(Utf8)
"(Ljava/lang/String;)V"
Methods[1]
struct method_info {
值
u2 access_flags;
09 (static | public)
u2 name_idx;
0F (main)
u2 desc_idx;
10 ([Ljava/lang/String;)V) = void ()(String[])
u2 attr_cnt;
01
struct code_attr {
u2 name_idx;
0D (code)
u4 attr_len;
5D
u2 max_stack;
04
u2 max_local;
02
u4 code_len;
2A
u1 code[code_len];
03 3C 1B 2A BE A2 00 24-B2 00 02 BB 00 03 59 B7
00 04 12 05 B6 00 06 2A-1B 32 B6 00 06 B6 00 07
B6 00 08 84 01 01 A7 FF-DC B1 (解释见下表)
u2 except_tbl_len;
00
e[except_tbl_len];
u2 attr_cnt;
02
u2 name_idx;
0E (LineNumberTable)
u4 attr_len;
12
u2 tbl_len;
04
u4 map[tbl_len]
0->5 8->6 23->5 29->8
u2 name_idx;
11 (StackMapTable)
u4 attr_len;
09 (JVM1.6中新增属性,与执行期基本无关)
}}};
上述方法的字节码:
地址
指令
助记符
执行后当前堆栈
源代码
00
03
iconst_0
i (未初始化)
(String[])args
RetAddr
(int)0
for(
int i=0;
i < args.length;
i++)
{
01
3c
istore_1
i (=0)
(String[])args
RetAddr
02
1b
iload_1
i
(String[])args
RetAddr
(int)0
03
2a
aload_0
i
(String[])args
RetAddr
(String[])args
(int)0
04
be
arraylength
i
(String[])args
RetAddr
(int)(args.length)
(int)0
05L1:
a2 00 24
if_icmpge L2
i
(String[])args
RetAddr
08
b2 00 02
getstatic
i
(String[])args
RetAddr
system.out
System.out.println(
"Hello "+args[i]);
}
0B
bb 00 03
new
StringBuilder
i
(String[])args
RetAddr
StringBuilder(未初始化)
system.out
0E
59
dup
i
(String[])args
RetAddr
StringBuilder(未初始化)
StringBuilder(未初始化)
system.out
0F
B7 00 04
invokespecial
<init>
i
(String[])args
RetAddr
StringBuilder(null)
system.out
12
12 05
ldc
i
(String[])args
RetAddr
(String)"Hello "
StringBuilder(null)
system.out
14
B6 00 06
invokevirtual
append
i
(String[])args
RetAddr
(StringBuilder)"Hello "
system.out
17
2a
aload_0
i
(String[])args
RetAddr
(String[])args
(StringBuilder)"Hello "
system.out
18
1b
iload_1
i
(String[])args
RetAddr
(int)i
(String[])args
(StringBuilder)"Hello "
system.out
19
32
aaload
i
(String[])args
RetAddr
(String)(args[i])
(StringBuilder)"Hello "
system.out
1A
B6 00 06
invokevirtual
append
i
(String[])args
RetAddr
(StringBuilder)"Hello?"
system.out
1D
B6 00 07
invokevirtual
toString
i
(String[])args
RetAddr
(String)"Hello?"
system.out
20
B6 00 08
invokevirtual
println
i
(String[])args
RetAddr
23
84 01 01
iinc local1,1
i
(String[])args
RetAddr
26
A7 FF DC
goto L1
29L2:
B1
return
附录C:调试技术
首先,需要准备源代码,并将其-g编译以提供debug信息;
下断点有两种方式:stop in 类名.方法名 和 stop at 类名:行号;后一种方式需要提供源代码文件;
单行、查看参数和局部变量都需要有编译器调试信息支持,而查看this成员变量不需要;
欲调试没有源代码的类,可以先观察其调用的运行库等,然后-g编译对应的运行库相关函数,调试时在运行库中下断点,间接取得执行期部分变量的值。
重新编译或patch运行库:从jdk\src.zip中提取对应的源代码,修改,-g编译之,然后jar -cvfm重新打包,覆盖jdk\jre\lib\rt.jar.
如果方法是重载的,则简单下断点会有问题;可以用methods命令查看所有方法的原形,然后用stop in 类名.方法名(参数列表)来下断点。
遇到invokevirtual时要仔细,可结合上下文、print命令和跟踪等,获得对象的实际类型,以免被多态所欺骗而跑飞。
类的外部接口终究要保存在class文件的constant pool中,因此,若想在整个jar包中寻找是哪个类调用了某个外部接口,可以以不压缩方式重打jar包,然后Hex编辑器搜索那个字符串,再搜索CaFeBaBe,对应起来即知。
附录D:威步的调试过程
测试步骤
发生现象
说 明
其他机器上,运行加壳的包
提示找不到某个DLL
察看Wrapper.class反编译源代码,
得知是system32\wibuXpm4J32.dll
Depends工具查上述DLL
得到8个导出函数的地址
其中有函数名为ClassLoader
UltraEdit修改上述DLL,
将ClassLoader函数入口处字节改为0xCC
WinDbg调试
JavaW.exe -jar protect.jar
时在该处断下。
CS:EIP = 0x20011B20
在内存中搜索ClassLoader.class.wibu文件内容,下读写访问断点
断下,CS:EIP = 0x20021640
察看调用堆栈,发现AES算法和字节表
继续跟踪解密,但结果还不是class码
AES参数:11轮,128位密钥,CBC方式
初始密码:"AxProtector/Java"
在解密出的数据下访问断点
断下,CS:EIP = 0x2001D930
发现LZW解压缩代码
继续跟踪,保存解压后数据,反编译
得到wibu的ClassLoader源代码
继续执行
在导出函数入口处断点又触发
重复跟踪解密解压
得到原jar包Main.class代码
未发现混淆处理,反编译,脱壳成功。
附录E:SafeNet的调试过程
步骤
现象/结果/说明
jdb -classpath e:\test.jar;e:\
正在初始化jdb...
E:\test.jar即SafeNet加壳后的包
E:\java指向运行库源代码,调试信息
>stop in javax.script.ScriptEngineManager.init
ScriptEngineManager为目标程序使用的外部接口
>run com/sun/sample/scriptpad/Main
断点命中: "thread=main",
javax.script.ScriptEngineManager.init(), line=73 bci=0
>where
注意输出结果中的[7]和[3],它们之间是动态代码
[1] javax.script.ScriptEngineManager.init
(ScriptEngineManager.java:73)
[2] javax.script.ScriptEngineManager.<init>
(ScriptEngineManager.java:51)
[3] DYTAX1FyX6fnllWf.MrmuYWh6v2ZTFkk8未知
[4] sun.reflect.NativeMethodAccessorImpl.
invoke0 (本机方法)
[5] sun.reflect.NativeMethodAccessorImpl.
invoke (NativeMethodAccessorImpl.java:39)
[6] sun.reflect.DelegatingMethodAccessorImpl.
invoke(DelegatingMethodAccessorImpl.java:25)
[7] java.lang.reflect.Method.invoke
(Method.java:597)
[8] com.aladdin.nemesis.a.do (uc:73)
[9] com.aladdin.nemesis.d.b.do (k:344)
[10]com.aladdin.nemesis.d.a.do (j:80)
[11]com.aladdin.nemesis.JANemsis.aldn_
1989undnochda_mwtbdltr (vc:119)
[12]com.sun.sample.scriptpad.Main.main (null)
>stop in java.lang.reflect.Method.invoke
>run ...
>print this
this = "public static void
DYTAX1FyX6fnllWf.MrmuYWh6v2ZTFkk8(
java.lang.String[]) throws java.lang.Exception"
可见this正是[3]步中要调用的函数,但多次调试同一jar包,this的名称却不一样,可见它是动态生成的。
>class DYTAX1FyX6fnllWf
>fields DYTAX1FyX6fnllWf
>methods DYTAX1FyX6fnllWf
类:DYTAX1FyX6fnllWf 扩展:java.lang.Object
** 字段列表为空 **
** 方法列表 **
DYTAX1FyX6fnllWf <init>()
MrmuYWh6v2ZTFkk8(java.lang.String[])
java.lang.Object <init>()
... (一系列Object的方法)
java.lang.Object getClass()
多次调试同一jar包,只有类和那个静态方法的名称不一样,其他接口参数和名称相同。
写一个测试程序hello.java,察看reflector的使用
package com.world;
import java.lang.reflect.*;
class test_refl {
public static void random(String[] args){
for(int i=0; i<args.length; i++)
System.out.println(args[i]);
}
}
public class hello {
public static void main(String[] args)
throws Exception {
Class class1 = test_refl.class;
Method methods[] = class1.getDeclaredMethods();
for(int j=0; j<methods.length; j++)
{
if( methods[j].getName().equals("random"))
methods[j].invoke(null, (Object)args);
}
for(int i=0; i<args.length; i++)
System.out.println("Hello "+args[i]);
}
}
>stop in com.world.test_refl.random
>wherei
[1] com.world.test_refl.random
(hello.java:8), pc = 0
[2] sun.reflect.NativeMethodAccessorImpl.
invoke0 (本机方法)
[3] sun.reflect.NativeMethodAccessorImpl.Invoke
(NativeMethodAccessorImpl.java:39), pc=87
[4] sun.reflect.DelegatingMethodAccessorImpl.
invoke(DelegatingMethodAccessorImpl.java25), pc=6
[5] java.lang.reflect.Method.invoke
(Method.java:597), pc = 161
[6] com.world.hello.main
(hello.java:24), pc = 43
发现[2]~[5]与加壳包的[4]~[7]一样,可认为只是使用了普通的反射接口,这里没有做手脚。
那个类是怎样生成出来的?
> stop in com.aladdin.nemesis.a.do
> run ...
> stop at java.lang.ClassLoader:466
> cont
> locals
方法参数:
name = "GBeKwfkHswafuLV4"
b = instance of byte[28574] (id=447)
off = 0
len = 28574
> print b[0]
> print b[1]
...
CA FE BA BE 00 00 00 31|经观察,基本上是加壳
00 6E 0A 00 18 00 32 07|后的main.class内容
00 33 0A 00 02 00 32 08|略有出入,下文比较。
磁盘上main.class文件
内存中的这个类
数据量(字节数)
28477
28574
const pool项数
0x5F
0x6E
const_pool[5E]
01 68 F0 7F 7F ... 7F
相同
const_pool[5F]
01 00 04 43 6F 64 65 Utf8"Code"
const_pool[60]
Utf8 "java/lang/Object"
const_pool[61]
Utf8 "<init>"
const_pool[62]
Utf8 "()V"
const_pool[63]
Class "java/lang/Object"
const_pool[64]
n&t void <init>(void)
const_pool[65]
mthRefjava/lang/Object.<init>
const_pool[66]
Utf8 "<init>"
const_pool[67]
Utf8 "()V"
const_pool[68]
Utf8 "LyK6SOAcqVROpuS8"
const_pool[69]
Utf8 "([Ljava/lang/String;)V"
const_pool[6A]
Utf8 "GBeKwfkHswafuLV4"
const_pool[6B]
Class "GBeKwfkHswafuLV4"
const_pool[6C]
Utf8 "java/lang/Object"
const_pool[6D]
Class "java/lang/Object"
access_flags
@6E18, 21
@6EB5, 21
this
const_pool[0D]
GBeKwfkHswafuLV4
super
const_pool[18]
java/lang/Object
interface_cnt
field_cnt
method_cnt
0
0
3
0
0
2
method[0]
略
public void <init>(void) code=17
max_stack =1, max_local =2,
code[5]=2A B7 00 65 B1
super(this); return;
method[1]
防止编译:函数重载
防止反编译:插入机器指令
防止调试:
使用java关键字
加入废代码扰乱视线
多态
代码混淆
public static void LyK6SOAcqVROpuS8(String[]) throws(Exception)
code=74, max_stack=3, max_local=3
00: BB 00 02 new 2
04: 59 dup
06: B7 00-03 invokespecial 3
0A: 4C astore_1
0C: 2B aload_1
0E: 12 04 ldc 4
11: B6 00 05 invokevirtual 5
15: 4D astore_2
17: 2C aload_2
19: 12 06 ldc 6
1C: 2C aload_2
1E: B9 00 07 03 00 invokeinterface 7,3
23: 2C aload_2
25: 12 08 ldc 8
28: B8 00 09 invokestatic 9
2C: 2C aload_2
2E: 12 0A ldc 0A
31: B8 00 09 invokestatic 9
35: 2C aload_2
37: 12 0B ldc 0B
3A: B8 00 09 invokestatic 9
3E: 2C aload_2
40: 12 0C ldc 0C
43: B8 00 09 invokestatic 9
47: B1 return
剩余字节都是废的,每条指令与原main函数中的一样,只是都插入一个nop。对const_pool的引用及const_pool对应索引的内容也一样。
动态生成的类型存在那个文件?
将整个jar包解出来,然后以不压缩方式重新打包,
用UltraEdit察看包,区分大小写搜索"File",发现
java/io/File
java/util/zip/ZipFile
java/io/FileInputStream
字样各出现几处,再Hex搜索"CA FE BA BE",将发现的位置与上述对应以下,锁定了以下几个类:
com/aladdin/nemesis/b/a.class
com/aladdin/nemesis/b.class
com/aladdin/nemesis/c/a.class
然后jad观察、jdb下断点观察
断点命中:
[1] java.util.zip.ZipFile.getInputStream
(ZipFile.java:180)
[2] com.aladdin.nemesis.g.a.do (q:300)
[3] com.aladdin.nemesis.d.a.do (j:129)
[4] com.aladdin.nemesis.JANemsis.aldn_1989
(vc:119)
[5] com.sun.sample.scriptpad.Main.main
>print entry.name
"SFNT/A04E68FF9DC2DEF03319SE0A73C11902.neurom"
类似的文件共有5个,都在SFNT目录下,在不同的位置被打开。
jad静态分析,知读出的文件由a.a.a.b.do()处理。
读文件: util.zip.InflaterInputStream.read()
静态分析发现,其解密算法较繁琐,且无法直接重新编译回去进行调试,算法中使用的外部接口只有arraycopy.
arraycopy是native函数,断不了。
故修改运行库,添加System.arraycopY函数,
再修改b.class,将引用arraycopy改为arraycopY.
下断点,观察。
>stop at java.lang.System:456
>cont
断点命中:...
>where
[1] java.lang.System.arraycopY
(System.java:456)
[2] com.aladdin.a.a.a.b.do (xc:385)
[3] com.aladdin.nemesis.g.a.do (q:74)
[4] com.aladdin.nemesis.d.a.do (j:70)
[5] com.aladdin.nemesis.JANemsis.aldn_1989
[6] com.sun.sample.scriptpad.Main.main
>locals
方法参数:
src = instance of byte[72] (id=407)
srcPos = 0
dest = instance of byte[8] (id=408)
destPos = 0
length = 8
查找堆栈得知,这是
.neurom文件,原长88/解密72。
00 00 00 01 "com.sun.sample.scriptpad"
00 "Main"
00 "main"
00 "([Ljava/lang/String;)V
11 dup(0)
.neurot文件,原长1000/解密1000
01 00 00 00 00 00 00 01
,wQjml/6hXnnfwH ...
.neurox文件,原长5664/解密5664
00 00 00 01
L"Sentinel HASP Protection System"
00 00 00 00 00 03
L"Could not find H" ...
.neuroc文件,原长28208/解密28208
"com.sun.sample.scriptpad"
00 "Main" 00 00 00
00 5F 0A 00 18 00 32 07
00 33 0A 00 02 00 32 08
28208=0x6e18+32-sizeof(cafebabe00000032)
.neuron文件,原长250/解密224
00 00 00 BC 00 09 00 20
00 21 00 02 00 1B 00 00
00 A4 00 03 00 03 00 00
00 4A BB 00 02 00 59 00 ...
快速脱壳法: patch java.lang.ClassLoader.java
protected final Class<?> defineClass(
String name, byte[] b, int off, int len)
throws ClassFormatError
{
if (16 == name.length())
saveFile(name, b);
return defineClass(name,b,off,len,null);
}
protected void saveFile(String name, byte[] b)
{
try {
FileOutputStream f = new FileOutputStream(name);
f.write(b);
f.close();
} catch (Exception e){}
}
然后直接运行带壳的jar包,则每个动态生成的类都会
被存成文件放在当前目录下.
2011/08/11 21,073 UjpCHY08VKiqvlKD
2011/08/11 21,011 YK4b2fBIorNcyq71
2011/08/11 21,143 GQwav22YHnxWzwMq
再用jad分别处理即可.
附录F Java对象的内部表示和字节码运行细节
按Java虚拟机规格书所述,任何“对象”都是使用Reference来操作,不限制具体的实现方式。但在字节码的分析中,
可以把对象的reference想象成一个指针,
指向一个结构,
结构的成员就是类的methods和fields,
结构中,父类的对象放在最前面,因此super的“值”等于this,只是类型不一样。
如上图,右面是想象的C表示。
对于构造函数:
每个类都必须有至少一个构造函数;
如果源代码中没有构造函数,则编译器会自动生成一个没有参数的默认构造函数;
构造函数的内部名称都是“<init>”;
编译器会在任何构造函数中插入代码,使其执行顺序如下:
调用父类构造函数;
初始化各成员变量;
源代码中给出初值的,按源代码初始化;
(源代码中没初值的,已在构造函数之前由系统自动初始化为0或null)
构造函数源代码中的语句;
上图左边是源代码,右边是编译后的示意代码。
其他方法(函数):
所有非静态方法,在内部都有一个额外的参数this,在参数列表中排最前。
关于内部类(InnerClass):
内部类可以声明在类定义中、函数中,可以有名字,也可以匿名,还可以是静态的;
在非静态的情况下,内部类的实现大致如下:
1、 和其他类一样,内部类编译后也拥有自己的class文件,名称是外部类名$内部类
名,如果是匿名,则为外部类名$1等依次类推。
2、 内部类有一个隐含的成员变量this$0,类型是外部类。故必须有外部类的对象才能
够获得内部类的对象。
3、 内部类的默认构造函数至少有两个参数,第一个类型是内部类,第二个类型是外部类,
构造函数执行时,先将第二参数值赋给this$0,然后调父类构造等常规动作。
4、 内部类与外部类并没有继承等其他关系,内部类的父类就是源代码中明确声明的它
的父类,如果没有明确指定,则为Object。内部类通过this$0访问外部类的成员
5、 特定情况下编译器会暗自产生匿名内部类,例如下图左的源代码:
编译后产生的内部指令,相当于上图右的。
可见,“通过public方法导出private类型”(内部类是私有的,而方法cont是公共
的),就会产生一个匿名内部类$1。因为私有的内部类,其构造函数也是private,
不能直接调用?编译器就重载了一个,访问权限变为friendly,参数也多了一个,多
出来的参数正是匿名类(可能是防止源代码中正好有同样的参数列表)。
异常处理
每个catch(ExceptType){Handling}块在class文件中,都登记在它所在方法的method_info的code_attribute的exception_table的一项.
struct {
u2 start_pc; //对应try块起始的字节码在函数体内的偏移
u2 end_pc; //对应try块结束~
u2 handler_pc; //catch块起始~
u2 catch_type; //catch的异常类型在const_pool中索引
} exception_table[exception_table_length];
此外,每个用throws声明了可能抛出异常的函数, method_info中会有一项exceptions_attribute, 标明所有可能抛出的异常.
Struct Exceptions_attribute {
u2 number_of_exceptions;
u2 exception_index_table[number_of_exceptions];}
而如果有finally语句,编译器会为匹配的try块添加一个catch(any)块,内容是先执行finally里面的语句,然后将当前异常重新throw,这样,对于当前函数没有catch到的异常,还是先执行finally,然后再向上抛。而try块尾、try块中的return和其他原有的catch块,每个的处理代码后,都被编译器自动加上执行finally. 总的执行顺序是:
try - catch - finally (如果捕捉到)
try - finally - 上级catch (若没捕捉到)
try - finally (没发生异常,无论try块执行完或中途return)
字节码invoke
JVM的字节码中,用于调用函数(方法)的指令共有4个,分别为
invokevirtual (调用一个instance method, 当前对象在编译时类型为class)
invokeinterface(调用一个interface method, 当前对象编译时类型为interface)
invokespecial (调用一个构造函数或私有函数)
invokestatic (调用一个静态函数)
这四个指令的第一操作数都是方法名称在常量池中的索引,invokeinterface还有额外的两个操作数,其他三个指令都只有一个操作数。
Invoke之前,参数压栈的先后顺序与参数列表一致(invokestatic不需要压this,其他三个指令最先压this)。
控制转移到目标方法后,原堆栈帧中的参数都被pop再重新push到当前堆栈帧,成为局部变量,先后次序也与参数列表一致(除静态方法外,其他方法的local var0都是this)
目标方法返回时,执行一组return指令中的一个,视其返回值类型而定:
return指令 (void)
ireturn指令 (int)
areturn指令 (reference)等,
如果有返回值,则areturn/ireturn等指令将返回值放回原堆栈帧的栈顶,若调用者不要返回值,编译器会生成一个pop指令抛弃之。
long和double数据类型的特别处理:
他们总是占用两个JVM word,常量池中占两项,堆栈单元也占两个,须注意。
stackMapTable
附录G:SafeNet的查锁线程
提要: SafeNet保护程序会在动态生成派遣函数中生成两个线程,其中一个是检测加密锁是否存在,另一个可能是监控用的. 若检测不到加密锁,会出提示并冻结其他线程.
测试步骤:
1、 将如下代码保存为hello.java并编译、打包为hello.jar:
package world;
public class hello {
public static void main(String[] args) {
try { Thread.sleep(999999); } catch (Exception e) {}
}
}
2、 使用SafeNet Envelop对hello.jar进行保护,默认设置,除查锁间隔设为5秒外。
3、 运行保护后jar包,用jvisualVM工具观察,比未保护的jar包多两个线程:
4、 jvisualVM的线程dump:
5、 用jad对nemesis/e/c.class和nemesis/s/c.class进行反编译,找到创建线程的代码
6、 JDB调试:
见下图,分别拦截到两个启动线程的调用
都是在JANemsis.aldn_1989...函数中发生的,参照附录E可知,该函数是运行时生成的派遣函数。
7、 jad对nemesis.JANemsis.class和nemesis.d.a.class反编译。发现类似例行检查的机制
若线程不存在,则创建之。
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
赞赏
看原图
赞赏
雪币:
留言: