0x01 问题

这个月在逛着https://stackoverflow.com突然发现个有点意思的一段代码,或者说是有点意思的猴戏
怎么说呢,就是看完以后,不知道为啥子,我就在懵逼中跪下了…
为了解决疑问,快速爬起来,我就决定解决这个疑问

先给你们看看这个问题是啥,你们就知道我为何懵逼了
https://stackoverflow.com/questions/15182496/why-does-this-code-using-random-strings-print-hello-world
翻译过来就是一句话:下面的代码将打印“hello world”,有人能解释一下吗?
大概是这个意思,我也是有道云翻译的….
image.png

给出的代码也超级简单,可以拿idea跑一下看看结果

  1. // 让人懵逼的代码
  2. package Test2;
  3. import java.util.Random;
  4. public class Test1 {
  5. public static void main(String[] args) {
  6. System.out.println(randomString(-229985452) + " " + randomString(-147909649));
  7. }
  8. public static String randomString(int i) {
  9. Random ran = new Random(i);
  10. StringBuilder sb = new StringBuilder();
  11. while (true) {
  12. int k = ran.nextInt(27);
  13. if (k == 0) {
  14. break;
  15. }
  16. sb.append((char) ('`' + k));
  17. }
  18. return sb.toString();
  19. }
  20. }
  21. // 运行结果
  22. hello world

image.png
就问你…
这个代码给你,你第一眼看到输出个“hello world”懵逼不懵逼?

0x02 解答

0x02.1 初步解答

懵逼完了以后,就可以开始想想为什么了

先看了一眼源码,有点拗口,让我有点懒的思考,于是决定去看看文章的评论
image.png

我这么懒的逼,当然是选择看评论拉,看到一个高赞回答,看看写了啥先
image.png
有道翻译是这么说的:
当使用特定的种子值(seed)(在本例中是-229985452-147909649)构建java.util.Random的实例时
那么java.util.Random将从指定的种子值(seed)开始生成随机数
用相同的种子值(seed)构建的每一个java.util.Random对象,每次都会产生相同的数字

是不是感觉还是有点懵逼,简单的说就是当这个种子值(seed)是固定的时,那么生成出来的结果也是固定的
这里我们做个小实验,写一段代码,运行一下,你就会恍然大悟说的是啥了

  1. // 随机数固定结果测试
  2. package Test2;
  3. import java.util.Random;
  4. public class Test2 {
  5. public static void main(String[] args) {
  6. randomString(-229985452);
  7. System.out.println("--------------");
  8. randomString(-229985452);
  9. }
  10. private static void randomString(int i) {
  11. Random ran = new Random(i);
  12. System.out.println(ran.nextInt());
  13. System.out.println(ran.nextInt());
  14. System.out.println(ran.nextInt());
  15. System.out.println(ran.nextInt());
  16. System.out.println(ran.nextInt());
  17. }
  18. }
  19. // 运行结果
  20. -755142161
  21. -1073255141
  22. -369383326
  23. 1592674620
  24. -1524828502
  25. --------------
  26. -755142161
  27. -1073255141
  28. -369383326
  29. 1592674620
  30. -1524828502

image.png
可以发现,我这边在种子值(seed)一致的情况下,运行二次的结果返回都是一致的
当然读者也可以试试运行,你的结果一定也是会和我一致的

也就是说在使用java.util.Random时,如果指定的种子值(seed)是相同的
那么他们生成并返回的其实是看起来是随机的固定数字

而且我们都知道java.util.Random本身就是一个伪随机算法
而当使用特定的种子值(seed)构建java.util.Random的实例时,那就成了一个更加伪的伪随机算法了
这么说是因为如果能猜测出,种子值(seed)或是种子值(seed)泄漏了,那么理论上就可以推测出随机数生成的结果

0x02.2 回看问题

好了前面逼逼那么多,现在也应该知道java.util.Random中指定种子值(seed)的关键了
现在让读者们,随我在回去看看问题,应该就可以看出来是为啥了
image.png
主要看循环里面的代码即可
先了解个基础的小知识,带参的nextInt(int x)会生成一个范围在0~x(不包含x)内的任意正整数
现在看int k = ran.nextInt(27);这句话,这表示k这个变量返回的值一定是[0,26]内的一个正整数
if (k == 0)的意思就是说,如果k这个变量,返回0就退出循环,这个没啥子好说的
在进行下一步之前,打印看看int k = ran.nextInt(27);具体会返回什么
image.png

  1. // int k = ran.nextInt(27);两个种子值(seed)的返回结果
  2. ----------------
  3. 种子值(seed): -229985452
  4. 返回值:8
  5. 返回值:5
  6. 返回值:12
  7. 返回值:12
  8. 返回值:15
  9. 返回值:0
  10. ----------------
  11. 种子值(seed): -147909649
  12. 返回值:23
  13. 返回值:15
  14. 返回值:18
  15. 返回值:12
  16. 返回值:4
  17. 返回值:0
  18. ----------------

有个印象即可,无需特别在意

在看个基础的小知识,Java中的单引号表示字符一般是char类型
现在看(char) ('‘ + k)其中'是个char类型,看到char``+``int条件反射的想到ASCII
而且'ASCII码是96<br />并且k返回的是[0,26]内的一个正整数<br />因此(char) (‘' + k)这个代码的范围就是[96+1,96+26]

去除返回值为0的,最终只需要对照着ASCII码表,就能看出是对其的那些字母了

  1. 96 + 8 = 104 -> h
  2. 96 + 5 =101 -> e
  3. 96 + 12 = 108 -> l
  4. 96 + 12 = 108 -> l
  5. 96 + 15 = 111 -> o
  6. 96 + 23 = 119 -> w
  7. 96 + 15 = 111 -> o
  8. 96 + 18 = 114 -> r
  9. 96 + 12 = 108 -> l
  10. 96 + 4 = 100 -> d

到这里,对于为什么这一段谜一样的代码能输出“hello world”,我们已经了然于胸了
看穿了以后,也就是个小把戏罢了

0x03 思考

然后我就开始想这个东西如果拿来写马子,用于关键字混淆什么的,那不是会很棒?
于是就开始思考如何改造最前面的demo,让它啥子单词都能打出来,因为现在这个代码只能输出“hello world”

所以当务之急是找或写一个可以把字符串变成种子值(seed)的函数
当然我这么懒,所以我选择了找,然后还真被我找到了
image.png
拷贝一下,本地试试
image.png
很好,但是还差点,因为它只能跑a-z,这可不太行啊
因为我们写马的时候,各种特殊符号之类的,可是都要有的,所以还是需要一个小小的改动

  1. // 最终改动完成的代码
  2. // 注意: 种子生成的时间,会因为代码的长度与复杂度的增加而增加
  3. import java.util.*;
  4. import java.util.stream.Collectors;
  5. public class Test3 {
  6. public static void main(String[] args) {
  7. long start = System.currentTimeMillis();
  8. long[] seedList = generateSeedList("java.lang.Runtime");
  9. for (long seed : seedList) {
  10. System.out.println("种子值(seed): " + seed);
  11. System.out.println("对应字符串: " + seedConversionString(seed));
  12. System.out.println("------");
  13. }
  14. System.out.println("种子生成花费时间: " + (double) (System.currentTimeMillis() - start) / 1000 + "秒");
  15. String data = seedListConversionString(seedList);
  16. System.out.println("种子列表转换结果: " + data);
  17. }
  18. /**
  19. * 测试使用
  20. * 输出所有字符的种子与解析结果
  21. */
  22. public static void test() {
  23. String str = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~";
  24. String[] strs = str.split("");
  25. for (String s : strs) {
  26. System.out.println("----");
  27. long dataSeed = generateSeed(s);
  28. System.out.println("种子值(seed): " + dataSeed);
  29. System.out.println("对应字符串: " + seedConversionString(dataSeed));
  30. System.out.println("-----");
  31. }
  32. }
  33. /**
  34. * 功能: 输入字符串获取种子数组
  35. *
  36. * @param goal 要转为种子的字符串
  37. * @return
  38. */
  39. public static long[] generateSeedList(String goal) {
  40. List<String> dataSourceList = Arrays.asList(goal.split(""));
  41. int groupSize = (int) Math.ceil((double) goal.length() / 3);
  42. List<Long> seedList = new ArrayList<>();
  43. for (List<String> stringList : listChunkSplit(dataSourceList, groupSize)) {
  44. long seed = generateSeed(stringList.stream().collect(Collectors.joining("")));
  45. seedList.add(seed);
  46. }
  47. return seedList.stream().mapToLong(t -> t).toArray();
  48. }
  49. /**
  50. * 功能: 输入字符串获取种子
  51. * 注: 单词越长,需要查找的时间就越长,个人建议1-3个字符为一个种子,可以基本可以无感知的快速生成种子
  52. *
  53. * @param goal 要转为种子的字符串
  54. * @return
  55. */
  56. public static long generateSeed(String goal) {
  57. String str = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~";
  58. for (String s : goal.split("")) {
  59. if (!str.contains(s)) {
  60. throw new RuntimeException(String.format("%s 该字符,不是符合条件的字符,请修改", s));
  61. }
  62. }
  63. char[] input = goal.toCharArray();
  64. char[] pool = new char[input.length];
  65. label:
  66. for (long seed = Integer.MIN_VALUE; seed < Integer.MAX_VALUE; seed++) {
  67. Random random = new Random(seed);
  68. for (int i = 0; i < input.length; i++) {
  69. pool[i] = (char) (31 + random.nextInt(96));
  70. }
  71. if (random.nextInt(96) == 0) {
  72. for (int i = 0; i < input.length; i++) {
  73. if (input[i] != pool[i]) {
  74. continue label;
  75. }
  76. }
  77. return seed;
  78. }
  79. }
  80. throw new NoSuchElementException("对不起该字符串找不到对应的种子");
  81. }
  82. /**
  83. * 功能: 将种子数组转换字符串
  84. *
  85. * @param is 种子数组
  86. * @return
  87. */
  88. public static String seedListConversionString(long[] is) {
  89. StringBuilder dataSource = new StringBuilder();
  90. for (long seed : is) {
  91. dataSource.append(seedConversionString(seed));
  92. }
  93. return dataSource.toString();
  94. }
  95. /**
  96. * 功能: 将种子转换字符串
  97. *
  98. * @param i 种子
  99. * @return
  100. */
  101. public static String seedConversionString(long i) {
  102. Random ran = new Random(i);
  103. StringBuilder sb = new StringBuilder();
  104. while (true) {
  105. int k = ran.nextInt(96);
  106. if (k == 0) {
  107. break;
  108. }
  109. sb.append((char) (31 + k));
  110. }
  111. return sb.toString();
  112. }
  113. /**
  114. * 列表块分割函数
  115. * 功能: 把列表按照size分割成指定的list快返回
  116. * 例子1:
  117. * a = [1, 2, 3, 4, 5, 6, 7, 8, 9]
  118. * listChunkSplit(a, 2)
  119. * 返回: [[1, 2, 3, 4, 5], [6, 7, 8, 9]]
  120. * 例子2:
  121. * a = [1, 2, 3, 4, 5, 6, 7, 8, 9]
  122. * listChunkSplit(a, 10)
  123. * 返回: [[1], [2], [3], [4], [5], [6], [7], [8], [9]]
  124. *
  125. * @param dataSource 数据源
  126. * @param groupSize 一个整数, 规定最多分成几个list
  127. * @return List<List < String>>
  128. */
  129. public static List<List<String>> listChunkSplit(List<String> dataSource, Integer groupSize) {
  130. List<List<String>> result = new ArrayList<>();
  131. if (dataSource.size() == 0 || groupSize == 0) {
  132. return result;
  133. }
  134. // 偏移量
  135. int offset = 0;
  136. // 计算 商
  137. int number = dataSource.size() / groupSize;
  138. // 计算 余数
  139. int remainder = dataSource.size() % groupSize;
  140. for (int i = 0; i < groupSize; i++) {
  141. List<String> value = null;
  142. if (remainder > 0) {
  143. value = dataSource.subList(i * number + offset, (i + 1) * number + offset + 1);
  144. remainder--;
  145. offset++;
  146. } else {
  147. value = dataSource.subList(i * number + offset, (i + 1) * number + offset);
  148. }
  149. if (value.size() == 0) {
  150. break;
  151. }
  152. result.add(value);
  153. }
  154. return result;
  155. }
  156. }
  157. // 运行结果
  158. 种子值(seed): -2080435608
  159. 对应字符串: jav
  160. ------
  161. 种子值(seed): -2060785532
  162. 对应字符串: a.l
  163. ------
  164. 种子值(seed): -2147149194
  165. 对应字符串: ang
  166. ------
  167. 种子值(seed): -2107467938
  168. 对应字符串: .Ru
  169. ------
  170. 种子值(seed): -1949527326
  171. 对应字符串: nti
  172. ------
  173. 种子值(seed): -2146859157
  174. 对应字符串: me
  175. ------
  176. 种子生成花费时间: 21.273
  177. 种子列表转换结果: java.lang.Runtime

有了上面的代码以后,我们就可以写一个最最简单的混淆马子了

  1. // 这是我能想到的最最最简单的用涂了
  2. // 或是拿来对哥斯拉的流量加密感觉也是可以的
  3. // 其它的自己发挥想象吧
  4. import org.apache.commons.io.IOUtils;
  5. import java.io.InputStream;
  6. import java.lang.reflect.Constructor;
  7. import java.lang.reflect.Method;
  8. import java.util.Random;
  9. public class ExecCmdTest {
  10. public static void main(String[] args) {
  11. try {
  12. String cmd = "whoami";
  13. // java.lang.Runtime 的 种子
  14. // -2080435608 -> jav
  15. // -2060785532 -> a.l
  16. // -2147149194 -> ang
  17. // -2107467938 -> Ru
  18. // -1949527326 -> nti
  19. // -2146859157 -> me
  20. long[] seedList = {-2080435608, -2060785532, -2147149194, -2107467938, -1949527326, -2146859157};
  21. String runtimePath = seedListConversionString(seedList);
  22. // 获取Runtime类对象
  23. Class runtimeClass = Class.forName(runtimePath);
  24. // 获取构造方法
  25. Constructor runtimeConstructor = runtimeClass.getDeclaredConstructor();
  26. runtimeConstructor.setAccessible(true);
  27. // 创建Runtime类实例 相当于 Runtime r = new Runtime();
  28. Object runtimeInstance = runtimeConstructor.newInstance();
  29. // 获取Runtime的exec(String cmd)方法
  30. Method runtimeMethod = runtimeClass.getMethod("exec", String.class);
  31. // 调用exec方法 等于 r.exec(cmd); cmd参数输入要执行的命令
  32. Process p = (Process) runtimeMethod.invoke(runtimeInstance, cmd);
  33. // 获取命令执行结果
  34. InputStream results = p.getInputStream();
  35. // 输出命令执行结果
  36. System.out.println(IOUtils.toString(results, "UTF-8"));
  37. } catch (Exception e) {
  38. e.printStackTrace();
  39. }
  40. }
  41. /**
  42. * 功能: 将种子数组转换字符串
  43. *
  44. * @param is 种子数组
  45. * @return
  46. */
  47. public static String seedListConversionString(long[] is) {
  48. StringBuilder dataSource = new StringBuilder();
  49. for (long seed : is) {
  50. dataSource.append(seedConversionString(seed));
  51. }
  52. return dataSource.toString();
  53. }
  54. /**
  55. * 功能: 将种子转换字符串
  56. *
  57. * @param i 种子
  58. * @return
  59. */
  60. public static String seedConversionString(long i) {
  61. Random ran = new Random(i);
  62. StringBuilder sb = new StringBuilder();
  63. while (true) {
  64. int k = ran.nextInt(96);
  65. if (k == 0) {
  66. break;
  67. }
  68. sb.append((char) (31 + k));
  69. }
  70. return sb.toString();
  71. }
  72. }

0x04 杂项

有关ASCII码表的内容可以看这一篇文章
https://www.yuque.com/pmiaowu/ppx2er/kfvuhv
里面详细记录了ASCII码对应字符

0x05 总结

偶尔能看看猴戏还是挺有意思的说