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
-369383326
1592674620
-1524828502
--------------
-755142161
-1073255141
-369383326
1592674620
-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 -> h
96 + 5 =101 -> e
96 + 12 = 108 -> l
96 + 12 = 108 -> l
96 + 15 = 111 -> o
96 + 23 = 119 -> w
96 + 15 = 111 -> o
96 + 18 = 114 -> r
96 + 12 = 108 -> l
96 + 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 -> me
long[] 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 总结
偶尔能看看猴戏还是挺有意思的说