文章旨在帮助那些认为标准混淆器可以保护其知识产权不被盗用的初级开发人员,而不是培训黑客。
案例1:认证服务器
客户端-服务器模型早在互联网出现之前就已经存在,并且仍然广泛使用。同时,有一个传言说,在没有客户端-服务器部分的情况下无法构建软件保护,但接下来我们将展示这只是一个谬论。
下图显示了服务器-客户端数据流的图表。出于清晰起见,程序片段位于图表外部。所谓的关键点已经标出,并将从后面的内容中得到解释。
对于认证服务器可靠性的谬论非常普遍,因此我们将从这开始讲解。我们有一个服务器,它接收来自程序的请求,并在检查后发送响应:程序是否可以继续工作或需要付费注册?我们通过搜索与局域网或互联网通信相关的类(即java.net.*包中的类,如HttpURLConnection等)来找到简单获取请求的关键点。
让我们从开发人员的角度和黑客的角度来看待这个图表。在此模型中使用的应用程序中有两个重要点:发送请求的位置和从服务器接收响应的位置。
原始Java方法的片段,用于执行请求-响应操作:
boolean authenticate(String requestURL, String params) { URL url = null; HttpURLConnection conn = null; try { url = new URL(requestURL); conn = (HttpURLConnection) url.openConnection(); conn.connect(); } catch (IOException e) { showError("Failed connect to " + requestURL + "."); return false; // Warning } String response = ""; if (conn != null) { try (OutputStreamWriter writer = new OutputStreamWriter(conn.getOutputStream())) { writer.write(params); // Warning writer.flush(); // Warning writer.close(); } catch (IOException e) { showError("Failed write params " + params + "."); return false; // Warning } try (BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()))) { String line = ""; while ((line = reader.readLine()) != null) { response += line + "\n"; } reader.close(); } catch (IOException e) { showError("Failed read " + response + "."); return false; // Warning } if (conn != null) conn.disconnect(); } return response.indexOf("activated") > -1; // Error }
容易受攻击的命令后面跟随着注释。
与黑客所看到的混淆后的 Java 方法相同:
static boolean Z(String a, String aa) { String var2 = a; HttpURLConnection var4 = null; try { (var4 = (HttpURLConnection)(new URL(var2)).openConnection()).connect(); } catch (IOException var36) { return true; /* false; */ } String var6 = ""; if (var4 != null) { Object var5; Throwable var10000; try { a = null; var5 = null; try { OutputStreamWriter var3 = new OutputStreamWriter(var4.getOutputStream()); try { // var3.write(a); // var3.flush(); var3.close(); } finally { if (var3 != null) { var3.close(); } } } catch (Throwable var38) { if (a == null) { var10000 = var38; } else { if (a != var38) { a.addSuppressed(var38); } var10000 = a; } // throw var10000; } } catch (IOException var39) { z("Failed connect to " + a + "."); return true; /* false; */ } HttpURLConnection var45; label534: { try { a = null; var5 = null; try { BufferedReader var43 = new BufferedReader(new InputStreamReader( var4.getInputStream())); boolean var21 = false; try { var21 = true; a = ""; BufferedReader var44 = var43; while(true) { if ((a = var44.readLine()) == null) { var43.close(); var21 = false; break; } var6 = var6 + a + "\n"; var44 = var43; } } finally { if (var21) { if (var43 != null) { var43.close(); } } } if (var43 != null) { var45 = var4; var43.close(); break label534; } } catch (Throwable var41) { if (a == null) { var10000 = var41; } else { if (a != var41) { a.addSuppressed(var41); } var10000 = a; } // throw var10000; } } catch (IOException var42) { z("Failed to read " + var2 + "."); return true; /* false; */ } var45 = var4; } if (var45 != null) { var4.disconnect(); } } return true; /* var6.indexOf(z("0'%-'%%!5")) > -1; */ }
很容易看出,通过将第 97 行以及之前的所有return...s替换为return true, 以及注释第 19 行和第 20 行,我们就得到了程序的免费版本。请注意,服务器尝试确定当前谁正在申请身份验证以及有多少此类请求将失败(这是多个人的许可证的情况)。
在混淆的代码中,找到方法的输出可能要困难得多,但是使用探路者方法,或者更好地像狗寻找猎物一样,迟早可以找到它。黑客从类HttpURLConnection变量或类似类变量的第一个定义开始,沿着线索到达关键点:var4 -> var4 -> ... var3 -> var3 ... -> var4 -> return ... 混淆器引入的其余垃圾可以被忽略。在这种情况下,就像在许多其他情况下一样,程序的安全性仅取决于黑客的聪明才智,而不取决于您的努力。我们将在下一部分讨论这个问题。
案例2:支付并运行
以下是Pay and Play图表,显示对应的组件和数据流。
本模型没有试用版。首先付款,然后获取服务。黑客无法进行任何攻击。
对于开发人员来说,下面的句子已经成为常规规则:“几乎任何代码在足够的时间和努力下都可以被逆向工程。混淆器可以使反向工程变得更加困难和经济上不切实际。”但这是一个错误的观点。存在一种所谓的“偷一次,卖很多次”的攻击。黑客不是为自己而是为了出售而入侵程序。例如,你仍然可以以几乎零成本在线购买非法副本的MS Office、Windows 7或10以及许多其他软件。
因此,黑客需要购买该程序并执行之前描述的客户端-服务器方案中的步骤:将接受激活密钥的相应行替换为返回true,并删除付款订单中的命令。
为了抵御这种最简单的攻击,开发人员使用从服务器(激活密钥)接收的任何Java加密算法的密钥加密部分代码。加密部分包括密钥验证和成功后启动主程序的过程。以下是代码片段,包括解密和加载类的代码。
这段代码在这里以及接下来的部分中都使用了。认证器
public class Authenticator { public Authenticator(byte[] key, long ... l) { SecretKeySpec keySpec = new SecretKeySpec(key, "AES"); IvParameterSpec iv = new IvParameterSpec(key); try { Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); cipher.init(Cipher.DECRYPT_MODE, keySpec, iv); CipherInputStream cis = new CipherInputStream(getClass().getResourceAsStream("anyfolder/anyname"), cipher); ByteArrayOutputStream baos = new ByteArrayOutputStream(); copyStream(cis, baos); byte[] bytes = baos.toByteArray(); Class<?> klass = new ClassLoader() { Class<?> define(byte[] bytes, int offset, int length) { Class<?> klass = super.defineClass(null, bytes, 0, length); return klass; } }.define(bytes, 0, bytes.length); klass.getConstructor().newInstance(); } catch (Throwable t) { System.exit(1); } } }
和启动器,应使用相同的激活密钥进行加密,并放置在任何资源文件夹中。
public class Launcher { args = Preloader.args; public Launcher() { Application.run(args); } }
其中封装了Preloader类,该类设置args值和在付款确认后从服务器接收到的激活密钥。
public class Preloader { private static byte[] key; public static String[] args; public static void main(String[] args) { receiveConfirmation(); encryptClass(); new Authenticator(key); } private static void receiveConfirmation() { String confirm = responce(); String[] parts = confirm.split(":"); key = hexToBytes(parts[0]); } private static void encryptClass() { IvParameterSpec iv = new IvParameterSpec(key); SecretKeySpec keySpec = new SecretKeySpec(key, "AES"); try { Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv); InputStream is = Preloader.class.getResourceAsStream("/Launcher.class"); CipherInputStream cis = new CipherInputStream(is, cipher); String file = "C:\\Workspaces\\anyfolder\\anyname"; File targetFile = new File(file); OutputStream outStream = new FileOutputStream(targetFile); copyStream(cis, outStream); cis.close(); outStream.close(); } catch (Throwable t) { System.exit(1); } } private static byte[] hexToBytes(String hex) { byte[] bytes = new byte[hex.length() / 2]; for (int i= 0; i < bytes.length; i++) { try { bytes[i] = (byte) (0xff & Integer.parseInt(hex.substring(i * 2, i * 2 + 2), 16)); } catch (Exception e) { return null; } } return bytes; } private static void copyStream(InputStream in, OutputStream out) { byte[] buffer = new byte[4096]; int read; try { while ((read = in.read(buffer)) != -1) { out.write(buffer, 0, read); } } catch (IOException e) { System.exit(1); } } private static void sendPaymentOrder(float sum) { ... } private static String responce() { ... } }
这种保护方法的优点是明显的:
没有可以被修改的逻辑表达式和变量。
路径追踪在认证器类结束,无法伪造或规避。
没有进入主程序的入口点,甚至没有名称。
我们暂且不涉及复制保护的问题。
重命名变量、文件和类不会影响安全级别,但可能会产生某种印象。
案例3:"Time Bomb"
下图显示了Time Bomb模型的部件和数据流。
此模型适用于限时试用和订阅。
在这里,逆时针计时器充当服务器。它位于程序内部,因为将其放置在服务器上会使我们返回到之前的方案。请求是"剩余多少时间",响应是时间 > 0 ? 运行 : 退出。关键点在于计数器本身和计数器的输出。类似于这样的请求->调用计数器->响应->切换过期/否->
Launcher类中添加了静态字段start和period,并对构造函数进行了小幅修改。
public class Launcher { private static long start; private static long period; private static String[] args; public class Application { public static void main(String[] args) { ... } } static { args = Preloader.args; start = Preloader.start; period = Preloader.period; } public Launcher() { if (System.currentTimeMillis() - start < period) { Application.main(args); } else { System.exit(1); } } }
与案例1不同,黑客无法使用与时间相关的Java关键字(方法)以及要更改的行或行来找到Launcher类。
因此,必须可靠地隐藏此类以防止黑客攻击。目前,加密是最合适的手段。我们采取以下措施:首先,加密Launcher.class字节,其次,将它们移动到/anyfolder文件夹中,将类重命名为任何名称,然后使用与案例 2中的Authenticator类相同的密钥解密。
public class Authenticator { public Authenticator(byte[] key, long ... l) { ... try { Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); ... klass.getConstructor().newInstance(); } catch (Throwable t) { System.exit(1); } } }
Preloader和Launcher类是Pay and Play模型类的修改版本;分别添加了工作开始时间和持续时间参数start和period,以及总金额。
但是,所有试图通过加密隐藏激活密钥的尝试都受挫,因为密钥和类都会在JVM内存中解密。黑客可以使用内存转储获取所需的数据。首先,它使用Java Tools API,下面是代码:
DumperAgent:
public class DumperAgent implements ClassFileTransformer { public static void premain(String args, Instrumentation instr) { agentmain(args, instr); } public static void agentmain(String agentArgs, Instrumentation instr) { instr.addTransformer(new DumperAgent(), true); Class<?>[] classes = instr.getAllLoadedClasses(); try { instr.retransformClasses(classes); } catch (UnmodifiableClassException e) {} } public byte[] transform(ClassLoader loader, String className, Class<?> redefinedClass, ProtectionDomain protDomain, byte[] classBytes) { dumpClass(className, classBytes); return null; } private static void dumpClass(String className, byte[] classBytes) { try { className = className.replace("/", File.separator); // ... FileOutputStream fos = new FileOutputStream(fileName); fos.write(classBytes); fos.close(); } catch (Exception e) { e.printStackTrace(); } } }
Preloader and Launcher classes are modifications of the Pay and Play model classes; parameters have been added for the Start and Time of work, start and period, respectively, as well as sum.
public class Preloader { private static float sum = 1000000.00f; // added for Case 3 private static byte[] key; public static String[] args; public static long start; // added for Case 3 public static long period; // added for Case 3 public static void main(String[] args) { sendPaymentOrder(sum); // added for Case 3 receiveConfirmation(); encryptClass(); new Authenticator(key, start, period); } private static void receiveConfirmation() { String confirm = responce(); String[] parts = confirm.split(":"); key = hexToBytes(parts[0]); start = Long.parseLong(parts[1]); // added for Case 3 period = Long.parseLong(parts[2]); // added for Case 3 } private static void encryptClass() { IvParameterSpec iv = new IvParameterSpec(key); SecretKeySpec keySpec = new SecretKeySpec(key, "AES"); try { Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv); InputStream is = Preloader.class.getResourceAsStream("/Launcher.class"); CipherInputStream cis = new CipherInputStream(is, cipher); String file = "C:\\Workspaces\\anyfolder\\anyname"; File targetFile = new File(file); OutputStream outStream = new FileOutputStream(targetFile); copyStream(cis, outStream); cis.close(); outStream.close(); } catch (Throwable t) { System.exit(1); } } private static byte[] hexToBytes(String hex) { ... return bytes; } private static void copyStream(InputStream in, OutputStream out) { ... } private static void sendPaymentOrder(float sum) { ... } }
但是,所有通过加密隐藏 激活密钥的尝试都会因密钥和类在 JVM 内存中被解密这一事实而受挫。黑客可以使用内存转储来获取他需要的数据。首先,它使用 Java 工具 API。代码如下:
public class DumperAgent implements ClassFileTransformer { public static void premain(String args, Instrumentation instr) { agentmain(args, instr); } public static void agentmain(String agentArgs, Instrumentation instr) { instr.addTransformer(new DumperAgent(), true); Class<?>[] classes = instr.getAllLoadedClasses(); try { instr.retransformClasses(classes); } catch (UnmodifiableClassException e) {} } public byte[] transform(ClassLoader loader, String className, Class<?> redefinedClass, ProtectionDomain protDomain, byte[] classBytes) { dumpClass(className, classBytes); return null; } private static void dumpClass(String className, byte[] classBytes) { try { className = className.replace("/", File.separator); // ... FileOutputStream fos = new FileOutputStream(fileName); fos.write(classBytes); fos.close(); } catch (Exception e) { e.printStackTrace(); } } }
And Attacher:
public class Attacher { private static String pathToAttacherJar, pid; public static void main(String[] args) throws Exception { VirtualMachine vm = VirtualMachine.attach(pid); vm.loadAgent(pathToAttacherJar, null); } }
不幸的是,这次尝试也没有成功。对 Java 运行时参数的简单分析会检测到不需要的参数,并且程序会在初始化时停止工作,而没有时间加载任何内容。
static { RuntimeMXBean mxBean = ManagementFactory.getRuntimeMXBean(); if (mxBean.getInputArguments().contains("-XX:-DisableAttachMechanism") || mxBean.getInputArguments().contains("-javaagent:")) { System.exit(1); } }
这个片段应该放在通常包含main(String[] args)方法的第一个类中。
你能做什么?
首先,你需要学会以黑客的眼光来看待问题。这意味着要识别你的程序所属的架构,寻找现有的漏洞,以及可能存在的程序漏洞和如何进行攻击。
记住,你总是可以找到针对任何攻击的保护措施。同时也要记住,“偷一次,多次出售”的攻击存在,但“做好保护,长期保护”的防御也同样存在。
不要仅仅依赖混淆器、服务器或加密技术。只保护关键点和程序的重要部分。利用程序本身的结构,从外部看待它。安全性应与你的程序设计一样精心设计。
结束语
代码保护是一个过程,而不是最终结果。新的黑客攻击方法正在被发明,新版本的JVM发布,允许更多地操纵JVM内存等等。这类似于病毒与杀毒软件之间的战争。
换句话说,绝对的武器和绝对的保护都不存在,并且也无法存在。任何新的攻击方法都会引起相应的防御方法来抵御这种攻击。
在几乎所有安全算法开发的情况下,应使用类似于量子物理学中的观察效应的入侵检测过程。任何观察(干预)、重置、远程代理等操作都会对观察环境造成干扰,这可以被注意到并采取保护措施。文章来源:https://www.toymoban.com/diary/java/670.html
因此,我们有一个攻防系统的经典例子。这意味着对于每一次攻击行动,都会有相应的防御行动,反之亦然。请注意,防御始终处于最佳位置。文章来源地址https://www.toymoban.com/diary/java/670.html
到此这篇关于如何加固基于Java的程序以防止黑客攻击的文章就介绍到这了,更多相关内容可以在右上角搜索或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!