防止JAVA字节码反编译这个问题在java语言雏形期就有了,尽管市面上存在一些反编译的工具可以利用,但是JAVA程序员还是不断的努力寻找新的更有效的方法来保护他们的智慧结晶。在此,我将详细给大家解释这一直来在论坛上有争议的话题。
QZ Sk}.7 |+ !!gb Class文件能被很轻松的重构生成JAVA源文件与最初JAVA字节码的设计目的和商业交易有紧密地联系。另外,JAVA字节码被设计成简洁、平台独立性、网络灵活性,并且易于被字节码解释器和JIT (just-in-time)/HotSpot 编译器所分析。可以清楚地了解程序员的目的, Class文件要比JAVA源文件更易于分析。
3e2K1(`#[ 如果不能阻止被反编译的话,至少可以通过一些方法来增加它的困难性。例如: 在一个分步编译里,你可以打乱Class文件的数据以使其难读或者难以被反编译成正确的JAVA源文件,前者可以采用极端函数重载,后者用操作控制流建立控制结构使其难以恢复正常次序。有更多成功的商业困惑者采用这些或其他的技术来保护自己的代码。
a?r(cj 不幸的是,哪种方法都必须改变JVM运行的代码,并且许多用户害怕这种转化会给他们的程序带来新的Bug。而且,方法和字段重命名会调用反射从而使程序停止工作,改变类和包的名字会破坏其他的JAVA APIS(JNDI, URL providers, etc),除了改变名字,如果字节码偏移量和源代码行数之间的关系改变了,在恢复这有异常的堆栈将很困难。
5c,WpW._7 于是就有了一些打乱JAVA源代码的选项,但是这将从本质上导致一系列问题的产生。
gI6_)C# |m$I ' 加密而不打乱
DTF?'R ujM!} o 或许上述可能会使你问,假如我把字节码加密而不是处理字节码,并且JVM运行时自动将它解密并装入类加载器,然后JVM运行解密后的字节码文件,这样就不会被反编译了对吗?
&j /iz9_ 考虑到你是第一个提出这种想法的并且它又能正常运行,我表示遗憾和不幸,这种想法是错误的。
6;8q<u+Jk ^0 3RG/vS 下面是一个简单的类编码器:
wVB+{Yk 'G0]a-" 为了阐明这种思想,我采用了一个实例和一个很通用的类加载器来运行它,该程序包括两个类:
4-#[9 y!^ )y>#>B public class Main
tVeq" |J {
(&g xHcg public static void main (final String [] args)
vI Ax1 {
Z,SX}5,_ System.out.println ("secret result = " + MySecretClass.mySecretAlgorithm ());
ce!(es0> }
E3UiF|>L _~fN: SFH } // End of class
Q[Bz,P Mc MJ6JH Cc}j S|k}ix|$u package my.secret.code;
KQ5`b8hfJ Fmi3E2N import java.util.Random;
`w(g`Zh 3@tPeF]TJ public class MySecretClass
}) 1J/l {
BX+)C /**
r751dP1 L_ * Guess what, the secret algorithm just uses a random number generator...
v)~y?L 7 */
Z[vD, public static int mySecretAlgorithm ()
s-,A~NDd {
wY$+3pkH return (int) s_random.nextInt ();
"D/=*e }
<?.|Rc hHC \j56), private static final Random s_random = new Random (System.currentTimeMillis ());
u, &_P;Xv[ Q[0}Ta<G } // End of class
Q7Px.Gb8Y 我想通过加密相关的class文件并在运行期解密来隐藏my.secret.code.MySecretClass的执行。用下面这个工具可以达到效果(你可以到这里下载Resources):
*jc$&% dG]eIX68 public class EncryptedClassLoader extends URLClassLoader
7V@&R'8 {
zTc!rvh GD public static void main (final String [] args)
Fr:Y^16/X throws Exception
J'qIn2+p {
[zdX>R if ("-run".equals (args [0]) && (args.length >= 3))
A^r! : {
6cDD#|Q // Create a custom loader that will use the current loader as
94)=$v. // delegation parent:
rANvPb_ final ClassLoader appLoader =
* qG _H* new EncryptedClassLoader (EncryptedClassLoader.class.getClassLoader (),
gLJ}NhO new File (args [1]));
ocB(P+- 'a,UiR // Thread context loader must be adjusted as well:
@B s;I ) Thread.currentThread ().setContextClassLoader (appLoader);
A4V`C2 ^8gkQ}6 final Class app = appLoader.loadClass (args [2]);
@cTSQ?NO c-~te2Z final Method appmain = app.getMethod ("main", new Class [] {String [].class});
T*%Chx final String [] appargs = new String [args.length - 3];
3NBEXb System.arraycopy (args, 3, appargs, 0, appargs.length);
j2IPY\G3 V=E8HC appmain.invoke (null, new Object [] {appargs});
`qI,kIxG }
Ciewy9LC else if ("-encrypt".equals (args [0]) && (args.length >= 3))
jZXfe3 {
7U 4yb ... encrypt specified classes ...
m`mx^N;U }
VYI=iAno else
9D+W#ae: throw new IllegalArgumentException (USAGE);
.~i(Wx#l }
> }r=Q9nf p"|]j;t}& /**
~Wbh? * Overrides java.lang.ClassLoader.loadClass() to change the usual parent-child
u|k 'r<, * delegation rules just enough to be able to "snatch" application classes
%q]HmxJD * from under system classloader's nose.
oVDR>-w */
sM_J:h public Class loadClass (final String name, final boolean resolve)
Y J} throws ClassNotFoundException
F {8]qQ4 {
w6h|Cob# if (TRACE) System.out.println ("loadClass (" + name + ", " + resolve + ")");
Vz[WKwS 008S( k3 Class c = null;
%ZjV< 3d { // First, check if this class has already been defined by this classloader
-+5:Sa // instance:
Ry'En!C1d c = findLoadedClass (name);
7`dWvHm w{_cS~W W if (c == null)
<\Cm8o4C {
1]t /"KFE Class parentsVersion = null;
28Tz"oD51d try
0bt-"XCS5l {
?4&z/;gv`| // This is slightly unorthodox: do a trial load via the
_{>U/2mW // parent loader and note whether the parent delegated or not;
)IQPq- // what this accomplishes is proper delegation for all core
2v,F$2d // and extension classes without my having to filter on class name:
8oa 07Wp< parentsVersion = getParent ().loadClass (name);
7l'`O+GG |-b\$(b{ if (parentsVersion.getClassLoader () != getParent ())
(Zd^ \ c = parentsVersion;
{v[M @ }
X7;S catch (ClassNotFoundException ignore) {}
O0 RY${" catch (ClassFormatError ignore) {}
c<;f&d{5 'R+D*&%, if (c == null)
N ^+rQ3 t {
bCEu?> try
J'o2?H_n {
vN2n\M^J^ // OK, either 'c' was loaded by the system (not the bootstrap
eS1!G3Y // or extension) loader (in which case I want to ignore that
as8gy" Q // definition) or the parent failed altogether; either way I
iv_VV}"JJo // attempt to define my own version:
0P5k c = findClass (name);
s]e'1Jc }
s;&nQ nYK9 catch (ClassNotFoundException ignore)
7*i":(< {
~S^g[w // If that failed, fall back on the parent's version
6eF~u.i // [which could be null at this point]:
F8-&\^v%) c = parentsVersion;
/lBp"$D }
z!!h}fF}U }
_`}2 ,aK }
rl\1/["# \uNZ&qy( if (c == null)
x|* y;dB throw new ClassNotFoundException (name);
V}@34Fu! uBD*G:=d if (resolve)
sH-TpY [| resolveClass (c);
RN_\M1Jf} MjdM*`d_9 return c;
`zByn%[ }
P7PzWy 6unof_a3 /**
a1u=NC}z \ * Overrides java.new.URLClassLoader.defineClass() to be able to call
(krFO%N * crypt() before defining a class.
q]+1yrj$ */
?&/r3`h}& protected Class findClass (final String name)
6}CuFq % throws ClassNotFoundException
O),x&z}O {
C`>M3@J!Xk if (TRACE) System.out.println ("findClass (" + name + ")");
uA4M4Nahr Uq Kqdq // .class files are not guaranteed to be loadable as resources;
=C|E71^FU // but if Sun's code does it, so perhaps can mine...
2w^:] Y> final String classResource = name.replace ('.', '/') + ".class";
Ef2,0\'PD final URL classURL = getResource (classResource);
c~ zR(BO U^j]]S. if (classURL == null)
j}ukz0l+ throw new ClassNotFoundException (name);
}tF36fjG else
CGz_pI# {
m6-G5-S InputStream in = null;
zj`i6g try
w_/t]X Jg {
>WaF-Z /qL in = classURL.openStream ();
j# e@]V% PZ`Geu/EM final byte [] classBytes = readFully (in);
g4! Ke wph`O // "decrypt":
Cd5iDvi, crypt (classBytes);
10(A7fD if (TRACE) System.out.println ("decrypted [" + name + "]");
3sb}Xq4kV a&k"Er$_ return defineClass (name, classBytes, 0, classBytes.length);
4csGE\V }
Us`*xP4 catch (IOException ioe)
?k3;lPO {
,m N <M throw new ClassNotFoundException (name);
V;qsPGmR }
~kyhv$- finally
?t?hr+;5 {
s?;) 39}e/ if (in != null) try { in.close (); } catch (Exception ignore) {}
1w|:h}~ }
y3 ` K. +u }
J <!xl4 }
&\Ms iNlQ). / /**
oS{?.X@ * This classloader is only capable of custom loading from a single directory.
*7FLogi */
rI]AC8|, private EncryptedClassLoader (final ClassLoader parent, final File classpath)
`p<OLdH throws MalformedURLException
i,3r ^ro {
i{x`xo5 super (new URL [] {classpath.toURL ()}, parent);
'V8H`#WQ :V\8Z5Yg if (parent == null)
4 E m FN throw new IllegalArgumentException ("EncryptedClassLoader" +
JT Z [3 " requires a non-null delegation parent");
*P&>`.+G9p }
VR6V$oH"& H-n07/& /**
PKxA65 1 * De/encrypts binary data in a given byte array. Calling the method again
NL{B OM{]V * reverses the encryption.
^(NAa)*] */
ojK8kBR+ private static void crypt (final byte [] data)
7EVm4= {
OB!FZ` for (int i = 8; i < data.length; ++ i) data
^= 0x5A; /#G iT
} <. J`NZ7C
o:{#NZ`
... more helper methods ... bg.0r%
RbA,x*.>
} // End of class V:q|ik.Z
DaPH<
这个累加载器(EncryptedClassLoader)有两个基本的操作,在给定的类路径下加密一系列Class文件并且运行一个先前加密的程序。加密后的文件很简单,有一些极讨厌的各个字节的位组成。(当然,XOR运算符不可能被加密,这只是一个范例,请多多包涵。) {^4B zY
97\gWq}
通过EncryptedClassLoader来加载类需要注意一些问题,我实现的是继承自java.net.URLClassLoader并且重载了loadClass()和defineClass()两个方法来实现自己的两个功能。一个是专心于JAVA 2 类加载器的委托规则并且在系统类加载器做之前先加载一个经加密过的类;二是在执行defineClass()之前立即调用crypt()方法,否则会执行URLClassLoader.findClass()。 oB ]0
S g\j
执行下面的语句: !Jtg/VdQ0t
>javac -d bin src/*.java src/my/secret/code/*.java 0aq|+xt
我把Main.class和MySecretClass.class进行了.加密: { 'kSFo=1
>java -cp bin EncryptedClassLoader -encrypt bin Main my.secret.code.MySecretClass 0xVlPl,0
encrypted [Main.class] !^]ee<XTB?
encrypted [my\secret\code\MySecretClass.class] 1%>l.tD
[M-N~}2{
现在原先编译的class文件已经被加密后的文件所替代了,如果我想运行原始类文件,需要使用EncryptedClassLoader来操作: lGH!n9GvR
_RIp1$c"
>java -cp bin Main &W4- b a
Exception in thread "main" java.lang.ClassFormatError: Main (Illegal constant pool type) Zk_SZ1dF
at java.lang.ClassLoader.defineClass0(Native Method) g rI3H
at java.lang.ClassLoader.defineClass(ClassLoader.java:502) `9s5 _1f!
at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:123) %qOe4om%8m
at java.net.URLClassLoader.defineClass(URLClassLoader.java:250) 8 ^}udt!(
at java.net.URLClassLoader.access$100(URLClassLoader.java:54) }U|CtY
at java.net.URLClassLoader$1.run(URLClassLoader.java:193) R09C>GFSG
at java.security.AccessController.doPrivileged(Native Method) |?C{b>Va
at java.net.URLClassLoader.findClass(URLClassLoader.java:186) +[<fnz5vP6
at java.lang.ClassLoader.loadClass(ClassLoader.java:299) .q8I|`/
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:265) tfp3u!
at java.lang.ClassLoader.loadClass(ClassLoader.java:255) qc{mdc0
at java.lang.ClassLoader.loadClassInternal(ClassLoader.java:315) F2okTCdK
i;* n
>java -cp bin EncryptedClassLoader -run bin Main nt[TLMA@)
decrypted [Main] 9 rI7:Vd
decrypted [my.secret.code.MySecretClass] Q-B "z r%
secret result = 1362768201 1f\_=:
nNAn)HY.h
现在可以确信,采用任何反编译工具对加密后的Class文件都不会起作用的。 7I1tz 6K
DWa$'%zk`
现在添加一个可靠的密码保护机制,把它打包成本地可执行文件,并且使其对外收费。这样子可以吗?当然不能这样了。 z5:" L(3a\
_L4t'>
ClassLoader.defineClass():必然经过的接口 !E`Dn>}
所有的类加载器必须经过明确地API把类定义传递到JVM里,这就需要java.lang.ClassLoader.defineClass()方法了。类加载器的API有多个这个方法的重载,但是所有的方法都会调用defineClass(String, byte[], int, int, ProtectionDomain),这是一个在经过一些简单验证后放入到JVM里的最终的方法。如果你想建立一个新的Class文件的话,这对于理解每个类加载器都会不可避免的调用该方法是很重要的。 [`:OKNDg
你只能在方法defineClass()里把一些单调的字节数组生成Class对象,并且我们猜测这些字节数组文件会包含一些文档格式化(查看class文件格式规范well-document.d format)的未加密的class定义,通过拦截对该方法的所有调用可以很简单的破坏这种加密模式,并且很方便的反编译你感兴趣的Class文件。 &26VSf6!U
做这种拦截并不困难,实际上破坏自己建立的保护模式比用工具更加迅速的。首先,我取得基于J2SDK的java.lang.ClassLoader源文件,并修改defineClass(String, byte[], int, int, ProtectionDomain)方法,在里面加入其他的类。正如下面: KZM5TPfl
... P_u1lB7~
c = defineClass0(name, b, off, len, protectionDomain); YVw~Z
y-4x9X]J
// Intercept classes defined by the system loader and its children: d'f tG=i
if (isAncestor (getSystemClassLoader ().getParent ())) s' L/>7>
{ `WjFlJ
// Choose your own dump location here [use an absolute pathname]: = k{/LQ6cK
final File parentDir = new File ("c:/TEMP/classes/"); . |r&V g
File dump = new File (parentDir, B/EA K#
name.replace ('.', File.separatorChar) + "[" + Ak`1A_r{1
getClass ().getName () + "@" + mO. a#?
Long.toHexString (System.identityHashCode (this)) + "].class"); o>} \b_ 6
ElU Y3To
dump.getParentFile ().mkdirs (); &VeRH{n:
?N"(%&7oY
FileOutputStream out = null; gl.{". |
try cr}2:C2#]
{ :L9gI'fz(
out = new FileOutputStream (dump); >&njF|Gw
out.write (b, off, len); ;&J"}N
} 'U`Zu'E2
catch (IOException ioe) b`lR%av!
{ vroKDu8.
ioe.printStackTrace (System.out); /!\>mA t&>
} bn .W<}d
finally @bUkWD(
{ <fP{4Zm+n
if (out != null) try { out.close (); } catch (Exception ignore) {} |gwHxb|bm
} j=}2\$R
} k f:{ OH
... J[UB?$Bbp
8}gIqU
注意if里的语句可以过滤系统类加载器及其子类加载器,同样在defineClass()方法可以正常工作的情况下才能载入类。很难以相信不只有一个类加载器实例加载一个类,可通过在文件名堆里面加入类加载器标志我还是最终把这一问题给解决了。:-) Ihgu>FZ
最后一步是用包含java.lang.ClassLoader类的可执行文件临时替换由JRE使用的文件rt.jar,你也可以使用-Xbootclasspath/p选项。 _A_njIH
我再一次运行加密的程序,并恢复了所有的未加密的文件,这么说可以很容易的把.class文件正确的反编译。我先声明我并没有用EncryptedClassLoader类的内部机制来完成此壮举的。 \+PI5P0u
3&VvcGGMu
在这里注意一点,假如我没去使用一个系统类,我可以使用别的方法,比如自定义一个JVMPI代理来处理JVMPI_EVENT_CLASS_LOAD_HOOK事件。 VvPWM
kS%L6Hou
学习小结: QM:4aG+Yp
我希望你能对本文有所兴趣,你必须认识到得很重要的一点是在购买市面上任何反编译工具前要三思而行,除非JVM体系结构进行改革以支持class字节码在本地能进行译码转换,你才会更好的从传统的困惑中走出来,上演一场字节码的改革浪潮! 848*)!U^6L
当然也有其他的更有效的方法:对类加载进行调试。尽可能地得到类加载的轨迹是很有价值的,特别是在类加载时你去捕获异常情况下使用。因此,JAVA的诞生可能纯粹是为了开源项目,当然,其他一些体系结构(如:。NET)也正在倾向于反编译。目前我就说说这种思想了.查看本文来源