0%

逆向学习 0x08密码学基础

密码学

概要:

在逆向中,非常有必要了解一下密码学,在逆向过程中,有些参数不知道来源,可能是随机生成、标准算法、自写算法加密

在安卓中,标准算法加密通常会 出现在Java、so(C\C++)、JS中

对于标准算法,Java中有现成的API调用,如果要使用这些API就需要使用指定的方法,就有了hook的机会,这些前面已经操作过了

C\C++中没有现成的系统API调用,开发者要么自己去实现算法,要么调用别人写好的模块,算法的运行就不依靠系统API,因此这些方法名可以进行混淆。我们需要根据各种标准算法的特征,去识别是否为标准算法

JS中没有系统API但是有知名的第三方库:CryptoJS、jsencrypt等

常见的算法:

​ 信息摘要算法(散列函数、哈希函数)

​ MD5、SHA、MAC

​ 对称加密算法

​ DES、3DES、AES

​ 非对称加密算法

​ RSA

​ 数字签名算法

​ MD5withRSA、SHA1withRSA、SHA256withRSA

H5的app逆向

使用前面提到过的查看app界面控件的小东西,看一下

这个页面只有框架,这个就是将一个网页做的像app登录的页面,然后放到这个框架里

image-20241102162750820

那么这个时候的逆向思路是什么呢

1、H5的核心代码通常在JS文件中

​ 远程调试、修改JS代码注入代码

2、WebView远程调试

​ 注意:a. 手机端的WebView版本要比电脑端的chrome版本低

​ b. 手机端的WebView要开启可调式

​ c. 需要VPN,因为点击inspect时要下载一些东西

​ d. 通常app中的WebView是不可调试的,需要hook来开启调试

WebView调试准备

上面有四条了,chrome访问

1
chrome://inspect

可以看到是有设备的一些信息的

image-20241102164435629

在手机中尝试随便访问一个网站,然后刷新chrome页面,是可以看到有页面情况的

image-20241102164619306

也是有webView的,而且为啥拿百度呢,因为百度是开启了webview调试的

image-20241102164936217

点击chrome中的inspect就可以进入谷歌的开发者工具,可以打断点,调试什么的,剩下的就交给JS逆向了

image-20241102165507956

再回头来看怎么开启WebView调试

不开启调试是获取不到东西的

image-20241102165834207

hook掉WebView方法,设置为开启调试状态

代码hook了所有的构造方法,在构造方法中设置开启调试,再hook掉开启方法,始终保持为true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
Java.perform(function(){
var WebView = Java.use('android.webkit.WebView');
WebView.$init.overload('android.content.Context').implementation = function(a){
console.log("WebView.$init is called!");
var retval = this.$init(a);
this.setWebContentsDebuggingEnabled(true);
return retval;
}
WebView.$init.overload('android.content.Context', 'android.util.AttributeSet').implementation = function(a, b){
console.log("WebView.$init is called!");
var retval = this.$init(a, b);
this.setWebContentsDebuggingEnabled(true);
return retval;
}
WebView.$init.overload('android.content.Context', 'android.util.AttributeSet', 'int').implementation = function(a, b, c){
console.log("WebView.$init is called!");
var retval = this.$init(a, b, c);
this.setWebContentsDebuggingEnabled(true);
return retval;
}
WebView.$init.overload('android.content.Context', 'android.util.AttributeSet', 'int', 'boolean').implementation = function(a, b, c, d){
console.log("WebView.$init is called!");
var retval = this.$init(a, b, c, d);
this.setWebContentsDebuggingEnabled(true);
return retval;
}
WebView.$init.overload('android.content.Context', 'android.util.AttributeSet', 'int', 'int').implementation = function(a, b, c, d){
console.log("WebView.$init is called!");
var retval = this.$init(a, b, c, d);
this.setWebContentsDebuggingEnabled(true);
return retval;
}
WebView.$init.overload('android.content.Context', 'android.util.AttributeSet', 'int', 'java.util.Map', 'boolean').implementation = function(a, b, c, d, e){
console.log("WebView.$init is called!");
var retval = this.$init(a, b, c, d, e);
this.setWebContentsDebuggingEnabled(true);
return retval;
}
WebView.$init.overload('android.content.Context', 'android.util.AttributeSet', 'int', 'int', 'java.util.Map', 'boolean').implementation = function(a, b, c, d, e, f){
console.log("WebView.$init is called!");
var retval = this.$init(a, b, c, d, e, f);
this.setWebContentsDebuggingEnabled(true);
return retval;
}

WebView.setWebContentsDebuggingEnabled.implementation = function(){
this.setWebContentsDebuggingEnabled(true);
console.log("setWebContentsDebuggingEnabled is called!");
}

});

将这个脚本重启附加到app中

必须要重启不能附加,因为页面都构建完成了,时机过去了

1
frida -U -f com.zngst.app -l hook_webview.js

这个时候就成功了

image-20241102171105422

登录以下是,抓个包

从这里可以跳转到登录有关的方法,跳转,打断点就不展示了

image-20241102171451165

H5的app也分很多种

​ 1、纯JS发包,这个时候可以在远程调试工具上抓到包,也有相应JS代码

​ 2、部分JS发包,部分Java发包,这个时候有些包可以在调试工具上抓到,有些不行,需要分析

​ 比如:Java和JS的相互调用,从这个角度入手,找Java里面的接口。

​ 3、纯Java发包,典型的就是uni-app,但是uni-app核心代码是在JS中,因为就是用JS写的。这个就牛逼了。发包在Java中,谷歌中看不到代码,这个时候远程调试就没啥用了。只能修改JS代码注入代码。对uni-app感兴趣可以下载一个HBuilderX耍一下

HEX编码

闲话少扯,看密码学内容,H5的app弄到调试的位置剩下的就和JS逆向差不多了

概述:

​ HEX编码是一种用16进制字符表示任意二进制数据的方法

​ 是一种编码,不存在加密

一个字节的范围是 0-255 ,hex编码用两个16进制字符表示这个一个字节

第一个F就代表了二进制中前四个比特位 1111 。也就是说十六进制中的一个十六进制字符占四个比特位,半个字节。两个十六进制字符,就可以表示八个比特位一个字节的数据。

image-20241102174147974

注意和字符编码区分开来:

​ 字符编码有 ASCII、UTF-8、GBK

​ 字符编码说白了是一种映射规则,将字符映射到唯一一种状态(二进制字符串),这个就是编码。也就是说如果没有字符编码来表示特定二进制的意思,那我们就只能读0101101这样的01字符串了

USC2、URL和HEX差不多是一种编码方式

hex编码的代码实现及码表

这个是一个针对字符串的实现,还有char的

是用函数将字符串转换成二进制,然后转换成16进制的表示方式

char数组直接转换

image-20241102181035429

不够底层,看一下Java第三方库的一个处理方法

看注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public String hex(data) {
// 码表
char[] HEX_DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}
// 创建char数组,长度为明文字节的长度 * 2,因为两个十六进制字符才能表示一个字节信息
char[] result = new char[data.length * 2];
int c = 0;
// 取出字节,挨个处理
for(byte b: data) {
// 先将二进制数据右移四位
// 比如: 1111 1111
// 右移: 0000 1111 1111
// 结果后面的四个字符被舍去为:0000 1111
// 然后进行 & 操纵,只有两个都为1才是1,0xf的值为0000 1111
// 比如:1010 1111 右移四位 0000 1010
// & 操作之后的结果为 0000 1010 ,成功取出前四位
result[c++] = HEX_DIGITS[(b >> 4) & 0xf];
// 再进行 & 操作 1010 1111 和 0000 1111
// 结果为 0000 1111 ,成功取出后四位
result[c++] = HEX_DIGITS[b & 0xf];
// 利用这样的操作,两个代码,第一行获取高四位,第二行获取低四位。来转换成HEX编码
}
return new String(result)
}
// 写一个小小的注释,这个中括号内传入的是二进制数据,就会根据二进制找到对应的索引

光看这个hex编码有点无聊,思路打开,为什么码表一定要0-9+a-f呢,将码表改一下,那就是一个非标准的hex编码。这个时候就需要找到这个码表了

hex编码的特点:

​ 1、用0-9 a-f 十六个字符表示

​ 2、每个十六进制字符代表4bit,也就是2个十六进制字符代表一个字节

​ 3、在实际的应用中,比如密钥初始化,一定要分清楚传进去的密钥是哪种编码,采用对应的方式解析,才能得到对应的密钥

​ 4、编程中的很多问题,需要从字节甚至二进制位的角度去考虑

Base64编码

用64个字符表示任意二进制数据的方法

base64是一种编码,而非加密

64个字符表示256种情况。和hex编码不同,hex编码是16*16正好为256,所以正好可以将一个字节拆开,用两个十六进制字符表示。一个十六进制字符占4bit

与hex不同的是base64编码的一个字符占6bit,用四个字符表示三个字节 6*4=8*3

base64使用的字符为 A-Z a-z 0-9 + / =

实际使用的是65个,最后的等号是用来补位填充的,看上面的表示方式应该也能理解为啥需要补位吧

base64的应用比较光,比如RSA密钥、加密后的密文、图片等数据,会有一些不可见的字符,直接转成文本传输的话,会有乱码、数据错误、数据丢失等情况,就需要用到base64编码

瞅一下实现代码

image-20241105193154873

大致了解一下这个处理过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// 定义一个方法,作用于 ByteArray 类型,它接受一个可选参数 map
// 参数的默认值为BASE64,这个是BASE64编码的字典
// internal val BASE64 =
// "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".encodeUtf8().data
internal fun ByteArray.encodeBase64(map: ByteArray = BASE64): String {
// 因为base64四位字符表示三个字节,+2是防止数据丢失
val length = (size + 2) / 3 * 4
// 创建新字节数组,来存储编码后的结果
val out = ByteArray(length)
// 定义索引
var index = 0
// 因为之前无脑+2了,这里处理一下结束位置,避免不必要的补位
val end = size - size % 3
var i = 0
// 遍历数组
while (i < end) {
// 先读取前三个字节的二进制数据,转换成int类型
val b0 = this[i++].toInt()
val b1 = this[i++].toInt()
val b2 = this[i++].toInt()

// 截取b0 的前六位数据,b0 的后两位和 b1 的前四位,b1 的后四位和 b2 的前两位,b2 的后六位
out[index++] = map[(b0 and 0xff shr 2)]
out[index++] = map[(b0 and 0x03 shl 4) or (b1 and 0xff shr 4)]
out[index++] = map[(b1 and 0x0f shl 2) or (b2 and 0xff shr 6)]
out[index++] = map[(b2 and 0x3f)]
}

// 判断是否有剩余字节长度,如果有进入when
when (size - end) {
// 如果剩一个字节
1 -> {
// 用两位base64字符读取这一个字节,再来两位字符“==”补位
val b0 = this[i].toInt()
out[index++] = map[b0 and 0xff shr 2]
out[index++] = map[b0 and 0x03 shl 4]
out[index++] = '='.code.toByte()
out[index] = '='.code.toByte()
}
// 如果剩两个字节
2 -> {
// 用三位base64字符读取剩余的两个字节,再使用一个“=”补位
val b0 = this[i++].toInt()
val b1 = this[i].toInt()
out[index++] = map[(b0 and 0xff shr 2)]
out[index++] = map[(b0 and 0x03 shl 4) or (b1 and 0xff shr 4)]
out[index++] = map[(b1 and 0x0f shl 2)]
out[index] = '='.code.toByte()
}
}
// 返回处理好的数据
return out.toUtf8String()
}

它其实还有一套码表,是BASE64_URL_SAFE

1
2
internal val BASE64_URL_SAFE =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".encodeUtf8().data

看名字就知道用途了,如果用于url编码,+就变为空格了,数据就出大问题,所以就需要把+/给替换掉,就换成了-_,这样一替换就不需要再进行url编码了

再了解一下核心科技,转换字符部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 比如进去的三个字节为 e f g
// toInt是将字节根据ASCII码表将其转化为数字
val b0 = this[i++].toInt() // 101
val b1 = this[i++].toInt() // 102
val b2 = this[i++].toInt() // 103

// 101 的二进制 0110 0101 和1111 1111 进行 & 操作,结果不变再进行shr 2的操作,得到结果 0001 1001(25='Z')
out[index++] = map[(b0 and 0xff shr 2)]
// 将 0110 0101 和 0000 0011 进行 & 操作,得到01
// 再将 0110 0110 和 1111 1111 进行 & 操作,结果不变,再右移四位 0000 0110。合并这两部分 0001 0110(22='W')
out[index++] = map[(b0 and 0x03 shl 4) or (b1 and 0xff shr 4)]
// 将 0110 0110 和 0000 1111 & 操作,得到 0000 0110, 再从取 0110 0111 的前两位,组成 0001 1001(25='Z')
out[index++] = map[(b1 and 0x0f shl 2) or (b2 and 0xff shr 6)]
// 得到最后的六个 0010 0111(39='n')
out[index++] = map[(b2 and 0x3f)]

// efg的base64编码结果为ZWZn

base64编码的特点

​ 1、Base64编码是一种编码,编码后会增加字节数

​ 2、算法可逆,解码很方便,不用于私密信息通信

​ 3、标准的Base64每行76个字符,行末添加换行符(这个需要注意,在拿到base64字符串进行解密操作的时候,一定不要忘了加上换行符)

​ 4、加密后的字符串只有65种字符,不可打印的字符也能传输

​ 5、在Java层可以通过hook对应的方法名来快速定位关键代码

​ 6、在so层可以通过输入输出的数据和码表来确定算法

工具函数封装

使用安卓的一个API,ByteString将byte数组转换成对应的hex编码或者base64编码,方便数据的查找和显示

1
2
3
4
5
6
7
8
9
10
11
12
13
Java.perform(function () {
var ByteString = Java.use("com.android.okhttp.okio.ByteString")

function toBase64(tag, data) {
console.log(tag + "Base64:" + ByteString.of(data).base64());
}
function toHex(tag, data) {
console.log(tag + "hex:" + ByteString.of(data).hex());
}
function toUtf8(tag, data) {
console.log(tag + "Utf8:" + ByteString.of(data).utf8());
}
})

信息摘要算法

特点:

​ 1、不同长度的输入,产生固定长度的输出

​ 2、信息摘要算法/单向散列函数/哈希函数

​ 3、散列后的密文不可逆

​ 4、散列后的结果唯一

​ 5、哈希碰撞

​ 6、一般用于校验数据完整性、签名sign

​ 由于密文不可逆,所以服务端也无法解密

​ 想要验证,只能跟前端一样再重新验证、计算签名

​ 签名算法一般会把源数据和签名后的值一起提交到数据段

​ 要保证在签名的时候的数据和提交上去的源数据一致

常见算法:MD5、SHA1、SHA256、SHA512、HmacMD5、HmacSHA1、HmacSHA256、HmacSHA512、RIPEMD160、HmacRIPEMD160、PBKDF2、EvpKDF

根据密文不可逆就可以推测出,签名的明文一定在数据包内,让服务端知道,否则服务端无法验证密文真伪

之前在Java层逆向的时候,小记了一下如何区分签名算法

image-20241105210941137

SHA-0已经废弃了,SHA-3虽然2011发布,但是不常见,常见的是MD5、SHA-1、SHA-256,这个256是SHA-2的256,SHA-2的256和SHA-3的256算出来的结果是不一样的

image-20241105211506561

输出散列值长度是输出的结果占的字节数,比如MD5占128个字节

资料区块长度是算法进行分组摘要,如MD5每组分512bit

哈希碰撞:即出现不同的信息算出的MD5相同的情况,所以现在很多都使用SHA-256进行签名,虽然哈希碰撞的概率很小,但是是客观存在的

MD5

MD5的Java实现,想要实现MD5很简单,因为Java已经写好这个底层了,直接拿来主义就好

1
2
3
4
5
6
7
// 先获取到MD5的对象
MessageDigest md5 = MessageDigest.getInstance("MD5");
// 使用 .update 进行加密操作,注意传进去的要是byte数组
md5.update("demo".getBytes());
// 返回的结果也需要byte数组接收
byte[] bytes = md5.digest();
// 想要打印结果再将byte数组使用hex或者base64编码一下即可

算法加盐

通俗点来说就是在明文前/后加上一段固定的字符串,来计算MD5,想要获取盐值也很简单,传一个空的得到MD5去网站上,爆破一下就好了

https://www.cmd5.com/

image-20241106184835852

SHA

SHA的Java实现

和MD5的方法几乎一模一样,就是把传进去的名字改一下

1
2
3
4
5
6
7
// 先获取到SHA-1的对象
MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
// 使用 .update 进行加密操作,注意传进去的要是byte数组
sha1.update("demo".getBytes());
// 返回的结果也需要byte数组接收
byte[] bytes = sha1.digest();
// 想要打印结果再将byte数组使用hex或者base64编码一下即可

算法通杀

所谓的通杀也只是hook标准算法需要走的Java层一些函数,如果不走这个函数也杀不掉,如果不是标准算法更杀不掉

MD5的hook

将之前的工具函数和打印堆栈先拿过来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Java.perform(function () {
function showStacks() {
console.log(
Java.use("android.util.Log")
.getStackTraceString(
Java.use("java.lang.Throwable").$new()
)
);
}

var ByteString = Java.use("com.android.okhttp.okio.ByteString")

function toBase64(tag, data) {
console.log(tag + "Base64:" + ByteString.of(data).base64());
}
function toHex(tag, data) {
console.log(tag + "hex:" + ByteString.of(data).hex());
}
function toUtf8(tag, data) {
console.log(tag + "Utf8:" + ByteString.of(data).utf8());
}
})

如果要hook安卓的md5,根据上方的实现代码,有两个方法需要hook,一个是 update 另一个是 digest,因为 digest 也是可以传明文数值的

update这个方法在 java.security 包下的 MessageDigest 类中,有很多重载方法

image-20241106193133857

hook update方法

因为加密的数据可能不是明文,而是乱码,这个时候就需要之前的工具函数来进行转换显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
var messageDigest = Java.use("java.security.MessageDigest")
messageDigest.update.overload('byte').implementation = function (data) {
console.log("MessageDigest update('byte'): " + data);
showStacks();
return this.update(data);
}
messageDigest.update.overload('java.nio.ByteBuffer').implementation = function (data) {
console.log("MessageDigest update('java.nio.ByteBuffer'): " + data);
showStacks();
return this.update(data);
}

// 注意下面的这两个是比较常用到的
messageDigest.update.overload('[B').implementation = function (data) {
console.log("MessageDigest update('[B')");
showStacks();
// getAlgorithm() 获取算法名称
var algorithm = this.getAlgorithm();
var tag = algorithm + " update('[B')";
toBase64(tag, data);
toHex(tag, data);
toUtf8(tag, data);
console.log("===========================================");
return this.update(data);
}
// 这个是定义字符串从什么地方开始截取,取多长
messageDigest.update.overload('[B', 'int', 'int').implementation = function (data,start, len) {
console.log("MessageDigest update('[B', 'int', 'int')");
showStacks();
var algorithm = this.getAlgorithm();
var tag = algorithm + " update('[B')";
toBase64(tag, data);
toHex(tag, data);
toUtf8(tag, data);
console.log("==============================================", start, len);
return this.update(data, start, len);
}

image-20241106200233800

SHA

SHA算法在安卓中实现的时候,就和MD5几乎一样,只有一个名字不一样,但是又使用了 MessageDigest 类的 getAlgorithm 方法,来获取传入的名字,这个时候MD5的和SHA的hook就是互通的

image-20241106200618601

hook digest方法

toUtf8就是看热闹,包乱码的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// hook digest方法
messageDigest.digest.overload().implementation = function () {
console.log("MessageDigest digest()");
showStacks();
var algorithm = this.getAlgorithm();
var tag = algorithm + " digest data";
var result = this.digest();
toBase64(tag, result);
toHex(tag, result);
toUtf8(tag, result);
console.log("==================================");
return result;
}
messageDigest.digest.overload('[B').implementation = function (data) {
console.log("MessageDigest digest()");
showStacks();
var algorithm = this.getAlgorithm();
var tag = algorithm + " digest data";
toBase64(tag, data);
toHex(tag, data);
toUtf8(tag, data);
var result = this.digest(data);
toBase64(tag, result);
toHex(tag, result);
toUtf8(tag, result);
console.log("==================================");
return result;
}
messageDigest.digest.overload('[B', 'int', 'int').implementation = function (data, start, len) {
console.log("MessageDigest digest()");
showStacks();
var algorithm = this.getAlgorithm();
var tag = algorithm + " digest data";
toBase64(tag, data);
toHex(tag, data);
toUtf8(tag, data);
var result = this.digest(data, start, len);
toBase64(tag, result);
toHex(tag, result);
toUtf8(tag, result);
console.log("==================================");
return result;
}

image-20241106203420408

MAC算法

MAC系列算法

算法 摘要长度 备注
HmacMD5 128 Java6实现
HmacSHA1 160 Java6实现
HmacSHA256 256 Java6实现
HmacSHA384 384 Java6实现
HmacSHA512 512 Java6实现
HmacMD2 128 Bouncy Castle实现
HmacMD4 128 Bouncy Castle实现
HmacSHA224 224 Bouncy Castle实现

MAC算法和MD和SHA算法的唯一区别就是多了一个密钥,密钥可以随便给

Java实现

1
2
3
4
5
6
7
8
9
10
11
   // 首先实例化密钥,指明算法名字,得到密钥对象
SecretKeySpec secretKeySpec =
new SecretKeySpec("a12345678".getBytes(), "HmacSHA1");
// 获取算法名字,这里也可以直接给明文
Mac mac = Mac.getInstance(secretKeySpec.getAlgorithm());
// 初始化密钥
mac.init(secretKeySpec);
// 这个就是和md5等相同了,放入明文,有重载函数
mac.update("demo".getBytes());
// 和digest相似
mac.doFinal();

hook MAC算法

根据Java实现来hook算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
var mac = Java.use("javax.crypto.Mac")
// 获取密钥,可以hook init方法也可以hook SecretKeySpec构造方法
mac.init.overload('java.security.Key').implementation = function (key) {
console.log("Mac Key")
showStacks();
var algorithm = this.getAlgorithm();
var tag = algorithm + " Mac Key";
// key.getEncoded() 获取密钥的字节数组
toBase64(tag, key.getEncoded());
toHex(tag, key.getEncoded());
toUtf8(tag, key.getEncoded());
return this.init(key);
}
mac.init.overload('java.security.Key', 'java.security.spec.AlgorithmParameterSpec').implementation = function (key) {
console.log("Mac Key")
// showStacks();
var algorithm = this.getAlgorithm();
var tag = algorithm + "Mac Key";
toBase64(tag, key.getEncoded());
toHex(tag, key.getEncoded());
toUtf8(tag, key.getEncoded());
return this.init(key);
}


mac.update.overload('byte').implementation = function (data) {
console.log("Mac update('byte')")
// showStacks();
var algorithm = this.getAlgorithm();
var tag = algorithm + " Mac update('byte')";
toBase64(tag, data);
toHex(tag, data);
toUtf8(tag, data);
console.log("=====================================");
return this.update(data);
}
mac.update.overload('java.nio.ByteBuffer').implementation = function (data) {
console.log("Mac update('java.nio.ByteBuffer')")
// showStacks();
var algorithm = this.getAlgorithm();
var tag = algorithm + " Mac update('java.nio.ByteBuffer')";
toBase64(tag, data);
toHex(tag, data);
toUtf8(tag, data);
console.log("=====================================");
return this.update(data);
}
mac.update.overload('[B').implementation = function (data) {
console.log("Mac update('[B')")
// showStacks();
var algorithm = this.getAlgorithm();
var tag = algorithm + " Mac update('[B')";
toBase64(tag, data);
toHex(tag, data);
toUtf8(tag, data);
console.log("=====================================");
return this.update(data);
}
mac.update.overload('[B', 'int', 'int').implementation = function (data, start, len) {
console.log("Mac update('[B', 'int', 'int')")
// showStacks();
var algorithm = this.getAlgorithm();
var tag = algorithm + " Mac update('[B', 'int', 'int')";
toBase64(tag, data);
toHex(tag, data);
toUtf8(tag, data);
console.log("=====================================", start, len);
return this.update(data, start, len);
}


mac.doFinal.overload().implementation = function () {
console.log("Mac doFinal()")
// showStacks();
var result = this.doFinal();
var algorithm = this.getAlgorithm();
var tag = algorithm + " Mac doFinal()";
toBase64(tag, result);
toHex(tag, result);
toUtf8(tag, result);
console.log("======================================");
return result;
}
mac.doFinal.overload('[B').implementation = function (data) {
console.log("Mac doFinal('[B')")
var result = this.doFinal(data);
var algorithm = this.getAlgorithm();
var tag = algorithm + " Mac doFinal()";
toBase64(tag, result);
toHex(tag, result);
toUtf8(tag, result);
console.log("======================================");
return result;
}
mac.doFinal.overload('[B', 'int').implementation = function (data, start) {
console.log("Mac doFinal('[B', 'int')")
return this.doFinal(data, start);
}

对称加密算法

1、加密/解密过程可逆的算法,叫做加密算法

2、加密/解密使用相同的密钥,叫做对称加密算法

3、对称加密算法的密钥可以随便给,但是有位数要求

4、对称加密算法的输入数据没有长度要求,加密速度快

5、各算法的密钥长度

​ RC4 密钥长度1-256字节

​ DES 密钥长度8字节

​ 3DES/DESede/TripleDES 密钥长度24字节

​ AES 密钥长度16、24、32字节

​ 根据密钥长度不同的AES又分为AES-128、AES-192、AES-256

6、对称加密分类

​ a. 序列加密/流加密:以字节流的方式,依次加密(解密)明文(密文)中的每一个字节

​ RC4

​ b. 分组加密:将明文信息分组(每组有多个字节),逐组进行加密

​ DES、3DES、AES

PS:MAC密钥是可以无限给的,如果密钥长度超过512bit,就计算密钥的MD5,使用MD5值作为密钥

PS:流加密和分组加密很好区分,流加密每增加一个字符密文长度就会变化,而分组加密是在一定的字节范围内,密文长度是不变的,超出这个长度,密文长度变长一倍

DES算法

Java实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 加密
// 还是定义密钥key和加密方式
SecretKeySpec secretKeySpec = new SecretKeySpec("123454678".getBytes(), "DES");
// 还有一种方式,这个API有密钥长度判断,如果传入的密钥过长,会自动截取8个字节
DESKeySpec desKeySpec = new DESKeySpec("12345678".getBytes());

// getInstance得到一个单例,一个虚拟机只此一份
Cipher des = Cipher.getInstance("DES/ECB/PKCS5Padding");

// ENCRYPT_MOOE的值为1代表加密模式,DECRYPT_MOOD为2代表解密模式
des.init(Cipher.ENCRYPT_MODE, secretKeySpec);
// 这里使用 doFinal 进行加密操作获取加密结果,因为update方法有问题,所以实现对称加密算法,一般传到 doFinal 方法中
byte[] result = des.doFinal("demo".getBytes());
String hexStr = ByteString.of(result).hex();
System.out.println(hexStr);
System.out.println(Base64.getEncoder().encodeToString(result));


// 解密
des.init(Cipher.DECRYPT_MOOD, secretKeySpec);
byte[] result1 = des.doFinal();
System.out.println(new String(result1));

特点:

1、对称加密算法里,使用NOPadding,加密的明文必须等于分组长度倍数,否则报错

2、没有指明加密模式和填充模式的,表示使用默认的 DES/ECB/PKCS5Padding

3、加密后的字节数组可以编码成hex、base64

4、要复现一个对称加密算法,需要得到明文,key、iv、mode、padding

5、明文、key、iv需要注意解析方式,看是utf-8还是hex,不一定都是字符串形式

6、ECB模式和CBC模式的区别

7、如果加密模式是ECB,则不需要加iv,加了的话包报错的

8、如果使用PKCS5Padding,会对加密的明文填充1字节至一个分组的长度

9、DES算法明文按64位进行分组加密

10、如果明文中有两个分组的内容相同,ECB会得到完全一样的密文,CBC不会

11、加密算法的结果通常与明文等长或者更长,如果变短了,可能是gzip、protobuf,或者信息摘要算法

加密模式

对称加密算法中,不光有密钥,还有一个IV值,上方的演示因为是 ECB 模式,没有用到 IV 值,但是逆向中常见的是 CBC 模式,这个是存在一个IV向量值的

这个IV向量是有长度限制的,根据算法的分组长度不同,IV长度也不同,DES加密的IV向量占8个字节

ECB模式和CBC模式

这二者都是分组加密的形式

ECB模式,分八个字节一组,分别加密,得到密文

1
2
3
4
5
6
7
8
明文:sjjwsnb666
分割为: sjjwsnb6 66
sjjwsnb666 加密结果: Vmkul5panJ +bu+Jo4DZ7Lw==
56692e979a5a9c9f 9bbbe268e0367b2f
sjjwsnb6 加密结果: Vmkul5panJ /+uVm31GQvyw==
56692e979a5a9c9f feb959b7d4642fcb

空格我自己加的,可以看到有很明显的分组痕迹,各管各的,拿出一段密文就可以获取部分明文,甚至可以自己加密一部分密文替换掉,达到替换明文的目的

ECB模式是不安全的

image-20241107122725336

CBC模式

1
2
3
4
5
6
7
8
9
10
11
12
13
明文:sjjwsnb666
IV: 12345678

明文分组: sjjwsnb6 66

CBC模式处理时先异或操作
sjjwsnb6 & 12345678
再将异或的结果进行加密操作
假如结果是:asddfgfh

再将下一组和上一组加密的结果异或
asddfgfh & 66
再进行加密,这样前后串连起来,断绝了更改密文影响明文的操作,更加的安全

CBC加密模式实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
      SecretKeySpec secretKeySpec = new SecretKeySpec("123454678".getBytes(), "DES");

// 这里定义iv向量值
IvParameterSpec ivParameterSpec = new IvParameterSpec("12345678".getBytes());
Cipher des = Cipher.getInstance("DES/CBC/PKCS5Padding");

// 这里初始化时需要添加上iv值
des.init(Cipher.ENCRYPT_MODE ,secretKeySpec, ivParameterSpec);
byte[] result = des.doFinal("demo".getBytes());
String hexStr = ByteString.of(result).hex();
System.out.println(hexStr);
System.out.println(Base64.getEncoder().encodeToString(result));


// 解密亦同
des.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec);
byte[] result1 = des.doFinal(result);
System.out.println(new String(result1));

填充方式

“DES/CBC/PKCS5Padding”中 CBC 是加密模式,PKCS5Padding是填充方式

NOPadding方式:必须是整组的数据,否则加密失败,拿到ECB的整组数据,进行NOPadding就可以得到明文

PKCS5Padding填充:如果使用了PKCS5Padding填充,填充的是一个字节到一个分组的长度,这就是为啥刚好8位明文,还会增加一个空分组的原因,如果最后只有一个字节,那么就会填充7个字节上去

DES密钥的漏洞

DES的密钥虽然是64个字节,但是真正使用的只有56个字节,每个字节的最后一位会被舍去,看一下演示

1
2
3
4
5
6
7
8
9
10
先令密钥位12345678
字节表示为:00110001 00110010 00110011 00110100 00110101 00110110 00110111 00111000

将字节最后一位随便改,这里都改成0了:
00110000 00110010 00110010 00110100 00110100 00110110 00110110 00111000
转换成数字为:02244668


使用12345678来加密字符串,结果为96d0028878d58c89feb959b7d4642fcb
使用02244668来加密字符串,结果为96d0028878d58c89feb959b7d4642fcb

加密结果是一样的,最后一个bit位是不重要的,也就是说可以有好几个密钥来进行加解密得到同样的结果

image-20241107135559234

DESede算法

又叫3DES,DESede算法进行三次DES操作,需要24位密钥,先用前八位密钥进行DES加密,再用中间八位密钥进行DES解密,最后用剩下的八位密钥进行DES加密。所以说如果前两组的密钥是一样的话,就相当于只用了后八位进行一次DES加密,效果是一样的

同样的有密钥问题

看看怎么用

1
2
3
4
5
6
7
8
9
10

SecretKeySpec secretKeySpec = new SecretKeySpec("123456781234567812345678".getBytes(), "DESede");

// DESede也是有一个API,也是如果密钥长度过长取前24字节
DESedeKeySpec deskeySpec = new DesedeKeySpec("123456788765432112345678".getBytes())

IvParameterSpec iv = new IvParameterSpec("12345678".getBytes());
Cipher desede = Cipher.getInstance("DESede/CBC/PKCS5Padding");
desede.init(Cipher.ENCRYPT_MODE, secretKeySpec, iv);
desede.doFinal("demo".getBytes());

AES算法

DES算法还是有很大的问题的,现在使用AES算法居多

根据密钥长度不同,分为AES128、AES192、AES256。分别对应16、20、32个字节

AES 密钥长度(bit) 分组长度(bit) 向量长度(bit) 加密轮数
AES-128 128 128 128 10
AES-192 192 128 128 12
AES-256 256 128 128 14

实现

AES算法明文按128位进行分组加密,其余特征与DES一致

1
2
3
4
5
6
7
8
   // AES算法没有特定的key的方法,使用SecretKeySpec定义密钥
SecretKeySpec secretKeySpec = new SecretKeySpec("1234567890abcdef".getBytes(), "AES");

// iv 长度为16字节
AlgorithmParameterSpec iv = new IvParameterSpec("1234567890abcdef".getBytes());
Cipher aes = Cipher.getInstance("AES/CBC/PKCS5Padding");
aes.init(Cipher.ENCRYPT_MODE, secretKeySpec, iv);
aes.doFinal("demodemodemo1234567890abcdef".getBytes());

通杀hook

DES

看到这就可以发现,hook的函数其实是大同小异的,主要的是看hook的一个思路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
// DES算法
var des = Java.use("javax.crypto.Cipher")

// 这里之所以不传值,是因为 return this.init.apply(this, arguments) ,this表示调用原始方法的对象,arguments表示原始方法的参数
des.init.overload('int', 'java.security.cert.Certificate').implementation = function () {
console.log("Cipher init('int', 'java.security.cert.Certificate')")
console.log("=============================================================================");
return this.init.apply(this, arguments);
}
des.init.overload('int', 'java.security.Key', 'java.security.SecureRandom').implementation = function () {
console.log("Cipher init('int', 'java.security.Key', 'java.security.SecureRandom')")
console.log("=============================================================================");
return this.init.apply(this, arguments);
}
des.init.overload('int', 'java.security.cert.Certificate', 'java.security.SecureRandom').implementation = function () {
console.log("Cipher init('int', 'java.security.cert.Certificate', 'java.security.SecureRandom')")
console.log("=============================================================================");
return this.init.apply(this, arguments);
}

des.init.overload('int', 'java.security.Key', 'java.security.spec.AlgorithmParameterSpec', 'java.security.SecureRandom').implementation = function () {
console.log("Cipher init('int', 'java.security.Key', 'java.security.spec.AlgorithmParameterSpec', 'java.security.SecureRandom')")
console.log("=============================================================================");
return this.init.apply(this, arguments);
}
des.init.overload('int', 'java.security.Key', 'java.security.AlgorithmParameters').implementation = function () {
console.log("Cipher init('int', 'java.security.Key', 'java.security.AlgorithmParameters')")
console.log("=============================================================================");
return this.init.apply(this, arguments);
}
des.init.overload('int', 'java.security.Key', 'java.security.AlgorithmParameters', 'java.security.SecureRandom').implementation = function () {
console.log("Cipher init('int', 'java.security.Key', 'java.security.AlgorithmParameters', 'java.security.SecureRandom')")
console.log("=============================================================================");
return this.init.apply(this, arguments);
}
// 上面的不重要,随随便便打印一下看看有没有运行就行了,这两个则是传密钥和IV向量的方法
des.init.overload('int', 'java.security.Key', 'java.security.spec.AlgorithmParameterSpec').implementation = function () {
console.log("Cipher init('int', 'java.security.Key', 'java.security.spec.AlgorithmParameterSpec')")
var algorithm = this.getAlgorithm();
var tag = algorithm + "init key";
var key = arguments[1].getEncoded();
toBase64(tag, key);
toHex(tag, key);
toUtf8(tag, key);
var tag = algorithm + "init iv";
var iv = Java.cast(arguments[2], Java.use("javax.crypto.spec.IvParameterSpec"));
iv = iv.getIV();
toBase64(tag, iv);
toHex(tag, iv);
toUtf8(tag, iv);
console.log("=============================================================================");
return this.init.apply(this, arguments);
}

des.init.overload('int', 'java.security.Key').implementation = function () {
console.log("Cipher init('int', 'java.security.Key')")
var algorithm = this.getAlgorithm();
var tag = algorithm + "init key";
var key = arguments[1].getEncoded();
toBase64(tag, key);
toHex(tag, key);
toUtf8(tag, key);
console.log("=============================================================================");
return this.init.apply(this, arguments);
}


// doFinal方法
des.doFinal.overload().implementation = function () {
console.log("Cipher doFinal()")
console.log("================================");
return this.doFinal.apply(this, arguments);
}
des.doFinal.overload("java.nio.ByteBuffer", "java.nio.ByteBuffer").implementation = function () {
console.log("Cipher doFinal('java.nio.ByteBuffer', 'java.nio.ByteBuffer')")
console.log("================================");
return this.doFinal.apply(this, arguments);
}
des.doFinal.overload('[B').implementation = function () {
console.log("Cipher doFinal('[B')")
console.log("================================");
return this.doFinal.apply(this, arguments);
}
des.doFinal.overload('[B', 'int').implementation = function () {
console.log("Cipher doFinal('[B', 'int')")
console.log("=======================================");
return this.doFinal.apply(this, arguments);
}
des.doFinal.overload('[B', 'int', 'int', '[B').implementation = function () {
console.log("Cipher doFinal('[B', 'int', 'int', '[B')")
console.log("===============================");
return this.doFinal.apply(this, arguments);
}
des.doFinal.overload('[B', 'int', 'int', '[B', 'int').implementation = function () {
console.log("Cipher doFinal('[B', 'int', 'int', '[B', 'int')")
console.log("===================================");
return this.doFinal.apply(this, arguments);
}


// 和其他的加密方法差不多,这两个方法重要
des.doFinal.overload('[B', 'int', 'int').implementation = function () {
console.log("Cipher doFinal('[B', 'int', 'int')")
showStacks();
var algorithm = this.getAlgorithm();
var tag = algorithm + " doFinal data";
var data = arguments[0];
toBase64(tag, data);
toHex(tag, data);
toUtf8(tag, data);
var result = this.doFinal.apply(this, arguments);
var tag = algorithm + " doFinal result";
toBase64(tag, result);
toHex(tag, result);
toUtf8(tag, result);
console.log("=======================================================");
return result;
}
des.doFinal.overload('[B').implementation = function () {
console.log("Cipher doFinal('[B')")
showStacks();
var algorithm = this.getAlgorithm();
var tag = algorithm + " doFinal data";
var data = arguments[0];
toBase64(tag, data);
toHex(tag, data);
toUtf8(tag, data);
var result = this.doFinal.apply(this, arguments);
var tag = algorithm + " doFinal result";
toBase64(tag, result);
toHex(tag, result);
toUtf8(tag, result);
console.log("===============================================");
return result;
}

这个不只是hookDES算法,DESede、AES和RSA都包含在内的,通杀了属于是

DESede

看一下两个加密方法的差别

可以看到中加密方式几乎使用的方法是一模一样的,DES直接就给他杀了,不用写其他的了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// DES
SecretKeySpec secretKeySpec = new SecretKeySpec("123454678".getBytes(), "DES");
Cipher des = Cipher.getInstance("DES/ECB/PKCS5Padding");
des.init(Cipher.ENCRYPT_MODE, secretKeySpec);
des.doFinal("demo".getBytes());



// DESede
SecretKeySpec secretKeySpec = new SecretKeySpec("123456781234567812345678".getBytes(), "DESede");
IvParameterSpec iv = new IvParameterSpec("12345678".getBytes());
Cipher desede = Cipher.getInstance("DESede/CBC/PKCS5Padding");
desede.init(Cipher.ENCRYPT_MODE, secretKeySpec, iv);
desede.doFinal("demo".getBytes());

AES

同上,一起杀了,就是IV不一样,但是不需要hook这个iv

1
2
3
4
5
6
7
8
   // AES算法没有特定的key的方法,使用SecretKeySpec定义密钥
SecretKeySpec secretKeySpec = new SecretKeySpec("1234567890abcdef".getBytes(), "AES");

// iv 长度为16字节
AlgorithmParameterSpec iv = new IvParameterSpec("1234567890abcdef".getBytes());
Cipher aes = Cipher.getInstance("AES/CBC/PKCS5Padding");
aes.init(Cipher.ENCRYPT_MODE, secretKeySpec, iv);
aes.doFinal("demodemodemo1234567890abcdef".getBytes());

非对称加密算法

RSA

RSA算法是一个非对称加密算法,就是说加密和解密不使用同一个密钥,分为公钥和私钥,公钥用来加密数据,私钥用来解密数据

私钥的格式

pkcs1格式通常开头是 -----BEGIN RSA PRIVATE KEY -----

pkcs8格式通常开头是 -----BEGIN PRIVATE KEY-----

java中的私钥必须是pkcs8格式,如果得到了pkcs1,需要转成pkcs8

https://www.ssleye.com/ssltool/pkcs.html

http://web.chacuo.net/netrsakeypair

RSA密钥的解析

去在线网站上弄一个没有密码的RSA密钥对

公钥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void pubKey() throws InvalidKeySpecException, NoSuchAlgorithmException {
String key = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC2kIa+rcK0oirgwxOM9tIkW95i" +
"GNpo7tmiR8KLOapz13kYcy0csBeIJ6R44J3KEWVQcrjSqkPGeQtOrPYl1LxX5evb" +
"DsIjmKBZXu4w8FkFxCW0ItaJu2qrX+3vr14g+JXcOYrcED+l53pwQ5sls1bbHqAe" +
"V+I+MfaZ2XWYTNRbRwIDAQAB";
// 解码密钥
byte[] keyBytes = ByteString.decodeBase64(key).toByteArray();

// 公钥使用 X509EncodedKeySpec ,私钥使用 PKCS8EncodedKeySpec,这两个是限于Base64的,如果是hex还有其他API
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);

// 然后使用 KeyFactory 来指明密钥类型
KeyFactory keyFactory = KeyFactory.getInstance("RSA");

// 然后使用 publicKey 来生成public类型的密钥
PublicKey publicKey = keyFactory.generatePublic(keySpec);

// 使用publickey的getEncode方法可以返回来得到密钥的byte数组,这个时候再输出base64的编码结果和原文是一样的
byte[] keyBytes = publickey.getEncode();
System.out.println(Base64.getEncoder().encodeToString(bytes));
}

私钥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public static void staticKey() throws InvalidKeySpecException, NoSuchAlgorithmException {
String stakey = "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALaQhr6twrSiKuDD" +
"E4z20iRb3mIY2mju2aJHwos5qnPXeRhzLRywF4gnpHjgncoRZVByuNKqQ8Z5C06s" +
"9iXUvFfl69sOwiOYoFle7jDwWQXEJbQi1om7aqtf7e+vXiD4ldw5itwQP6XnenBD" +
"myWzVtseoB5X4j4x9pnZdZhM1FtHAgMBAAECgYBDKOW4zZk79BBMANd3WvExWO51" +
"LeljAsLjFPz3VK5k0RaGLRCiZhEyEEtMAG1rgXzA3IMrVGF8aNkFB1HB1wG1wC4P" +
"t7hZl5s2tdseXsIMmS0mpDUZZu+tnFwHcqZS4K6rIFjQ8lZn7epv32Pc/0aSliWp" +
"5II5OZG2rrcG5C+y4QJBAPDQHba3wbuirDkq1QE3qUv05RAXGurideos9audGbf7" +
"RXu4RjiYT7xlEVdZBARL/eAu36PDZy+GTb6No5Iqn5ECQQDCE//3YVKgUbfMDLrq" +
"2LznXk+S5NXNbj2tocKsvQJLIa29qfd3eouQpR1osCJvqmA8aV2Z90y03yFBUOmv" +
"B5FXAkEAzyp7JYGYDQ+5EcUjUdTMtCeOF/WIlqETx82921FfmsNz1yeEYZPGpNBd" +
"xsMxjXDCi2ZHxt6Hmn7zywaWvVwlwQJALFNVCrL3pBYF3Fyr9Cc8PbuUgQAytJCR" +
"Fa70P2+Lro0qmT7QfkFGzupnJRnVQ5uuDx4heqC4rDap6bkJJiicUQJAfWRkNXyX" +
"WFvfM04BMjWSBgBOUmAVyB8GFAGh3e4uUvZYIr9oAnzdErFLDdXUyl1uCw5qrYkj" +
"hM7Kq5Lkby/iaw==";
// 解码密钥
byte[] keyBytes = ByteString.decodeBase64(stakey).toByteArray();

// 解析密钥
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
// 经过密钥工厂处理
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
// 得到私钥
PrivateKey privateKey = keyFactory.generatePrivate(keySpec);

// 同理
Log.d("demo", ByteString.of(privateKey.getEncoded().base64()))
}

虽然要是64的倍数,但是一般是 512,1024,2048这样的

image-20241114213144356

RSA加解密

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public static void RSACipher() throws NoSuchAlgorithmException, InvalidKeySpecException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
String key = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC2kIa+rcK0oirgwxOM9tIkW95i\n" +
"GNpo7tmiR8KLOapz13kYcy0csBeIJ6R44J3KEWVQcrjSqkPGeQtOrPYl1LxX5evb\n" +
"DsIjmKBZXu4w8FkFxCW0ItaJu2qrX+3vr14g+JXcOYrcED+l53pwQ5sls1bbHqAe\n" +
"V+I+MfaZ2XWYTNRbRwIDAQAB";
byte[] keyBytes = ByteString.decodeBase64(key).toByteArray();
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(keySpec);
byte[] bytes = publicKey.getEncoded();


String stakey = "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALaQhr6twrSiKuDD" +
"E4z20iRb3mIY2mju2aJHwos5qnPXeRhzLRywF4gnpHjgncoRZVByuNKqQ8Z5C06s" +
"9iXUvFfl69sOwiOYoFle7jDwWQXEJbQi1om7aqtf7e+vXiD4ldw5itwQP6XnenBD" +
"myWzVtseoB5X4j4x9pnZdZhM1FtHAgMBAAECgYBDKOW4zZk79BBMANd3WvExWO51" +
"LeljAsLjFPz3VK5k0RaGLRCiZhEyEEtMAG1rgXzA3IMrVGF8aNkFB1HB1wG1wC4P" +
"t7hZl5s2tdseXsIMmS0mpDUZZu+tnFwHcqZS4K6rIFjQ8lZn7epv32Pc/0aSliWp" +
"5II5OZG2rrcG5C+y4QJBAPDQHba3wbuirDkq1QE3qUv05RAXGurideos9audGbf7" +
"RXu4RjiYT7xlEVdZBARL/eAu36PDZy+GTb6No5Iqn5ECQQDCE//3YVKgUbfMDLrq" +
"2LznXk+S5NXNbj2tocKsvQJLIa29qfd3eouQpR1osCJvqmA8aV2Z90y03yFBUOmv" +
"B5FXAkEAzyp7JYGYDQ+5EcUjUdTMtCeOF/WIlqETx82921FfmsNz1yeEYZPGpNBd" +
"xsMxjXDCi2ZHxt6Hmn7zywaWvVwlwQJALFNVCrL3pBYF3Fyr9Cc8PbuUgQAytJCR" +
"Fa70P2+Lro0qmT7QfkFGzupnJRnVQ5uuDx4heqC4rDap6bkJJiicUQJAfWRkNXyX" +
"WFvfM04BMjWSBgBOUmAVyB8GFAGh3e4uUvZYIr9oAnzdErFLDdXUyl1uCw5qrYkj" +
"hM7Kq5Lkby/iaw==";
byte[] staKeyBytes = ByteString.decodeBase64(stakey).toByteArray();
PKCS8EncodedKeySpec staKeySpec = new PKCS8EncodedKeySpec(staKeyBytes);
KeyFactory staKeyFactory = KeyFactory.getInstance("RSA");
PrivateKey privateKey = staKeyFactory.generatePrivate(staKeySpec);


// 加密,使用公钥初始化
Cipher cipher = Cipher.getInstance("RSA/None/NOPadding");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] bt_encrypted = cipher.doFinal("1232345456".getBytes());


// 解密,使用私钥初始化
Cipher cipher1 = Cipher.getInstance("RSA/None/NOPadding");
cipher1.init(Cipher.DECRYPT_MODE, privateKey);
byte[] bt_original = cipher1.doFinal(bt_encrypted);
}

可以明显的看到RSA的实现还是cipher,那么DES的代码改都不用改

所谓的加密结果不固定就是使用padding的时候 bt_encrypted 的值不固定,会发生变化,但是NOpadding就是固定的

RSAd的加密模式和填充方式

RSA模式和填充细节

1、None模式与ECB模式是一致的

2、NOPadding

​ 明文最多字节数为密钥字节数,如果密钥1024bit,最多加密1024bit

​ 密文与密钥等长

​ 填充字节0(不是字符是占8个bit位的字节),加密后的密文不变

​ NOPadding解密之后还需要进行去零,0是byte数组中的0,在数据的前面添加的

3、PKCS1Padding

​ 明文最大字节数为密钥字节数 -11,如果密钥128字节,最多加密117字节,剩余的随机填充

​ 密文与密钥等长

​ 因为是随机填充,每一次的填充不一样,使得加密后的密文会变

​ 有个小秘密,PKCS1Padding解密的时候不指明是会自动取出前面的填充,指明了反而不去除

RSA密钥的转换

全面使用的密钥都是使用base64编码的,也有hex的密钥

1
2
3
4
5
6
7
8
9
10
11
12
// hex不是简单的将base64的结果解码然后hex编码
将转换后的数据进行这样的分割
30819f300d06092a864886f70d010101050003818d0030818902818100 | bdab976385934fb3ca17ec8775da752e191ce4ef6b5dc6398b3faf60615d45ad06e01216f8f11f4cab9e43a789b296a7bb318882bf320d2a21c00f6da233607576de16e7f6f552d97c2a4db345c97db5b5fc15127bd77ff1f1f588c577959d62694819a7eecb1f23d91c45654fbe90f300f68b64429cd4770d6685a761a1ed2d | 0203 | 010001


// 这两部分没啥用了
30819f300d06092a864886f70d010101050003818d0030818902818100
0203

// 需要操作这两部分 RSA 的核心原理就是两个大数相乘
bdab976385934fb3ca17ec8775da752e191ce4ef6b5dc6398b3faf60615d45ad06e01216f8f11f4cab9e43a789b296a7bb318882bf320d2a21c00f6da233607576de16e7f6f552d97c2a4db345c97db5b5fc15127bd77ff1f1f588c577959d62694819a7eecb1f23d91c45654fbe90f300f68b64429cd4770d6685a761a1ed2d
010001
1
2
3
4
5
6
7
8
9
10
11
12
public static String modulus = "bdab976385934fb3ca17ec8775da752e191ce4ef6b5dc6398b3faf60615d45ad06e01216f8f11f4cab9e43a789b296a7bb318882bf320d2a21c00f6da233607576de16e7f6f552d97c2a4db345c97db5b5fc15127bd77ff1f1f588c577959d62694819a7eecb1f23d91c45654fbe90f300f68b64429cd4770d6685a761a1ed2d"
public static String publicExponent = "010001"


BigInteger n = new BigInteger(modulus, 16);
BigInteger e = new BigInteger(publicExponent, 16);
RSAPublicKeySpec spec = new RSAPublicKeySpec(n, e);
Public publicKey = keyFactory.generatePublic(spec);
byte[] publicBytes = publicKey.getEncoded();
System.out.printn(Base64.getEncoded().encodeToString(pubkeyBytes));
// 这样讲两个数传入得到的仍然是base64编码的私钥,这是hex编码的转换,不是直接hex过去的
// 先从一个00的后面开始截取,一般是最后一个00,最后是截取一个 0203

还可以利用工具转换 ==opensll==

1
2
3
4
5
// 这样即可输出公钥hex的编码内容
opensll rsa -public -in [文件] -text

// 私钥
opensll res -in [] -text

具体的加密解密操作和base64是一样的

RSAhook

其他的方法不用管,可以直接用之前hookDES的代码通杀hook,但是hex编码的密钥不能无脑 getEncoded了,需要进行一个判断

RSA的私钥无法进行 getEncoded ,这样就无法获取私钥,但是一般逆向也不需要RSA的私钥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
des.init.overload('int', 'java.security.Key').implementation = function () {
console.log("Cipher init('int', 'java.security.Key')")
var algorithm = this.getAlgorithm();
var tag = algorithm + "init key";
var className =JSON.stringify(arguments[1]);
if(className.indexOf("SecretKeySpec") === -1){
var key = arguments[1].getEncoded();
toBase64(tag, key);
toHex(tag, key);
toUtf8(tag, key);
}
console.log("================================================================");
return this.init.apply(this, arguments);
}

算法常见の套路

无论是对称加密算法还是非对称加密算法都有局限性,如果仅使用对称加密算法,使用固定的密钥的话,仅破解客户端,所有的东西都可以解密了,随机生成密钥的话,还需要传输密钥,更不安全。仅使用非对称加密算法能够加密的数据太有限了。

常见的套路就是AES+RSA,先随机生成AES密钥,将这个密钥进行RSA加密。得到加密密文,将数据密文和密钥密文发送给服务器。

数字签名算法

所谓数字签名算法是将信息摘要算法和RSA算法进行结合,先进行信息摘要算法,再进行非对称加密算法

主要作用就是用来防止发出去的数据被篡改

1
2
3
4
我要发送数据 "demo" 为了防止这个数据被篡改我先加一个信息摘要MD5
fe01ce2a7fbac8fafaed7c982a04e229
这样还有被篡改的可能,对方可以测试我使用的什么信息摘要算法,更改数据之后,再进行摘要算法
我就再给他一个RSA加密,使用私钥进行加密,公钥对外公开,对方可以用公钥来验证数据,这样就难以篡改了,除非我的私钥被拿到

签名实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 私钥签名
public static void MD5withRSA() throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException, SignatureException {
String stakey = "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALaQhr6twrSiKuDD" +
"E4z20iRb3mIY2mju2aJHwos5qnPXeRhzLRywF4gnpHjgncoRZVByuNKqQ8Z5C06s" +
"9iXUvFfl69sOwiOYoFle7jDwWQXEJbQi1om7aqtf7e+vXiD4ldw5itwQP6XnenBD" +
"myWzVtseoB5X4j4x9pnZdZhM1FtHAgMBAAECgYBDKOW4zZk79BBMANd3WvExWO51" +
"LeljAsLjFPz3VK5k0RaGLRCiZhEyEEtMAG1rgXzA3IMrVGF8aNkFB1HB1wG1wC4P" +
"t7hZl5s2tdseXsIMmS0mpDUZZu+tnFwHcqZS4K6rIFjQ8lZn7epv32Pc/0aSliWp" +
"5II5OZG2rrcG5C+y4QJBAPDQHba3wbuirDkq1QE3qUv05RAXGurideos9audGbf7" +
"RXu4RjiYT7xlEVdZBARL/eAu36PDZy+GTb6No5Iqn5ECQQDCE//3YVKgUbfMDLrq" +
"2LznXk+S5NXNbj2tocKsvQJLIa29qfd3eouQpR1osCJvqmA8aV2Z90y03yFBUOmv" +
"B5FXAkEAzyp7JYGYDQ+5EcUjUdTMtCeOF/WIlqETx82921FfmsNz1yeEYZPGpNBd" +
"xsMxjXDCi2ZHxt6Hmn7zywaWvVwlwQJALFNVCrL3pBYF3Fyr9Cc8PbuUgQAytJCR" +
"Fa70P2+Lro0qmT7QfkFGzupnJRnVQ5uuDx4heqC4rDap6bkJJiicUQJAfWRkNXyX" +
"WFvfM04BMjWSBgBOUmAVyB8GFAGh3e4uUvZYIr9oAnzdErFLDdXUyl1uCw5qrYkj" +
"hM7Kq5Lkby/iaw==";
byte[] staKeyBytes = ByteString.decodeBase64(stakey).toByteArray();
PKCS8EncodedKeySpec staKeySpec = new PKCS8EncodedKeySpec(staKeyBytes);
KeyFactory staKeyFactory = KeyFactory.getInstance("RSA");
PrivateKey privateKey = staKeyFactory.generatePrivate(staKeySpec);


// 上面就是处理私钥的不用管
// 指定签名算法
Signature sig = Signature.getInstance("SHA256withRSA");
// 导入私钥
sig.initSign(privateKey);
// 加密明文数据的字节数组
sig.update("demo".getBytes());
// 得到结果,值得注意的是,签名算法要保证运行结果不变的,所以填充是NOPadding
byte[] signResult = sig.sign();
}

验证实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
   String key = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC2kIa+rcK0oirgwxOM9tIkW95i\n" +
"GNpo7tmiR8KLOapz13kYcy0csBeIJ6R44J3KEWVQcrjSqkPGeQtOrPYl1LxX5evb\n" +
"DsIjmKBZXu4w8FkFxCW0ItaJu2qrX+3vr14g+JXcOYrcED+l53pwQ5sls1bbHqAe\n" +
"V+I+MfaZ2XWYTNRbRwIDAQAB";
byte[] keyBytes = ByteString.decodeBase64(key).toByteArray();
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(keySpec);

Signature sig = Signature.getInstance("SHA256withRSA");
// 导入公钥
sig.initVerify(publicKey);
// 导入明文
sig.update("demo".getBytes());
// 将给到密文传入,验证是否正确,返回布尔类型
boolean isTrue = sig.verify(signResult);
System.out.println(isTrue);

这个签名算法的结果是加密之后前面,将信息摘要算法的结果放在最后面,然后在前面填充东西,具体填充的啥还不知道

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public static void RSAwithRSA() throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException, SignatureException {
String stakey = "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALaQhr6twrSiKuDD" +
"E4z20iRb3mIY2mju2aJHwos5qnPXeRhzLRywF4gnpHjgncoRZVByuNKqQ8Z5C06s" +
"9iXUvFfl69sOwiOYoFle7jDwWQXEJbQi1om7aqtf7e+vXiD4ldw5itwQP6XnenBD" +
"myWzVtseoB5X4j4x9pnZdZhM1FtHAgMBAAECgYBDKOW4zZk79BBMANd3WvExWO51" +
"LeljAsLjFPz3VK5k0RaGLRCiZhEyEEtMAG1rgXzA3IMrVGF8aNkFB1HB1wG1wC4P" +
"t7hZl5s2tdseXsIMmS0mpDUZZu+tnFwHcqZS4K6rIFjQ8lZn7epv32Pc/0aSliWp" +
"5II5OZG2rrcG5C+y4QJBAPDQHba3wbuirDkq1QE3qUv05RAXGurideos9audGbf7" +
"RXu4RjiYT7xlEVdZBARL/eAu36PDZy+GTb6No5Iqn5ECQQDCE//3YVKgUbfMDLrq" +
"2LznXk+S5NXNbj2tocKsvQJLIa29qfd3eouQpR1osCJvqmA8aV2Z90y03yFBUOmv" +
"B5FXAkEAzyp7JYGYDQ+5EcUjUdTMtCeOF/WIlqETx82921FfmsNz1yeEYZPGpNBd" +
"xsMxjXDCi2ZHxt6Hmn7zywaWvVwlwQJALFNVCrL3pBYF3Fyr9Cc8PbuUgQAytJCR" +
"Fa70P2+Lro0qmT7QfkFGzupnJRnVQ5uuDx4heqC4rDap6bkJJiicUQJAfWRkNXyX" +
"WFvfM04BMjWSBgBOUmAVyB8GFAGh3e4uUvZYIr9oAnzdErFLDdXUyl1uCw5qrYkj" +
"hM7Kq5Lkby/iaw==";
byte[] staKeyBytes = ByteString.decodeBase64(stakey).toByteArray();
PKCS8EncodedKeySpec staKeySpec = new PKCS8EncodedKeySpec(staKeyBytes);
KeyFactory staKeyFactory = KeyFactory.getInstance("RSA");
PrivateKey privateKey = staKeyFactory.generatePrivate(staKeySpec);

Signature sig = Signature.getInstance("SHA256withRSA");
sig.initSign(privateKey);
sig.update("demo".getBytes());
byte[] signResult = sig.sign();




String key = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC2kIa+rcK0oirgwxOM9tIkW95i\n" +
"GNpo7tmiR8KLOapz13kYcy0csBeIJ6R44J3KEWVQcrjSqkPGeQtOrPYl1LxX5evb\n" +
"DsIjmKBZXu4w8FkFxCW0ItaJu2qrX+3vr14g+JXcOYrcED+l53pwQ5sls1bbHqAe\n" +
"V+I+MfaZ2XWYTNRbRwIDAQAB";
byte[] keyBytes = ByteString.decodeBase64(key).toByteArray();
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(keySpec);

sig.initVerify(publicKey);
sig.update("demo".getBytes());
boolean isTrue = sig.verify(signResult);
System.out.println(isTrue);
}

数字签名算法hook通杀

从实现可以看出,可hook的方法有 ==getPrivateKey== 、initSign、 update、 sign、initVerify、verify

hook update方法获取明文

hook sign方法获取密文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// hook数字签名算法
// hook update方法,获取签名前的数据
var signature = Java.use("java.security.Signature")
signature.update.overload('byte').implementation = function (data) {
console.log("Signature update('byte)")
console.log("===============================================");
return this.update(data);
}
signature.update.overload('java.nio.ByteBuffer').implementation = function (data) {
console.log("Signature update('java.nio.ByteBuffer')")
console.log("===============================================");
return this.update(data);
}
signature.update.overload('[B').implementation = function (data) {
console.log("Signature update('[B')")
var algorithm = this.getAlgorithm();
var tag = algorithm + " Signature update('[B')";
toBase64(tag, data);
toHex(tag, data);
toUtf8(tag, data);
console.log("===============================================");
return this.update(data);
}
// 其实着重hook下面这个方法就够了,上面的byte数组需要走下面这个方法
signature.update.overload('[B', 'int', 'int').implementation = function (data, start, len) {
console.log("Signature update('[B', 'int', 'int')")
var algorithm = this.getAlgorithm();
var tag = algorithm + " Signature update('[B', 'int', 'int')";
toBase64(tag, data);
toHex(tag, data);
toUtf8(tag, data);
console.log("===============================================", start, len);
return this.update(data, start, len);
}


// hook sign方法,获取签名密文
signature.sign.overload('[B', 'int', 'int').implementation = function () {
console.log("Signature sign('[B', 'int', 'int')")
console.log("===============================================");
return this.sign.apply(this, arguments);
}
signature.sign.overload().implementation = function () {
console.log("Signature sign()")
var result = this.sign();
var algorithm = this.getAlgorithm();
var tag = algorithm + " Signature sign()";
toBase64(tag, result);
toHex(tag, result);
toUtf8(tag, result);
console.log("===============================================");
return result;
}

image-20241117195958891

verify 方法能获取密文,已经获取到了,再hook起来没有什么意义

initSign和initVerify方法可以获取到 公钥和私钥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// hook initSign方法,获取签名私钥
signature.initSign.overload('java.security.PrivateKey').implementation = function (key) {
console.log("Signature initSign('java.security.PrivateKey')")
var algorithm = this.getAlgorithm();
var tag = algorithm + " Signature initSign()";

var OpenSSLRSAPrivateKey = Java.use("com.android.org.conscrypt.OpenSSLRSAPrivateKey");
var realkey = Java.cast(key, OpenSSLRSAPrivateKey).getEncoded();
toBase64(tag, realkey);
toHex(tag, realkey);
toUtf8(tag, realkey);
console.log("================================================");
return this.initSign(key);
}

这个密钥是不能直接获取,因为传入的是PrivateKey类 ,PrivateKey 是一个接口,没有像之前一样的 getEncoded 方法,所以需要做一个多态转型,来获取,而且只能获取到base64的,不能获取到hex的密钥,有用处但是用处不大

image-20241118163848199

CryptoJS

学一下JS的加解密,因为可能要复现某算法,使用JS脚本来hook复现比较方便,而且被其他语言调用也比较方便

使用CryptoJS来实现信息摘要算法和对称加密算法

信息摘要算法的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var cryptoJS = require('crypto-js');

let demo = "123456";
let key = "demo";
console.log("MD5:" + cryptoJS.MD5(demo).toString());
console.log("HmacMD5:" + cryptoJS.HmacMD5(demo, key).toString())
console.log("SHA1:" + cryptoJS.SHA1(demo).toString())
console.log("HmacSHA1:" + cryptoJS.HmacSHA1(demo, key).toString())
console.log("SHA256:" + cryptoJS.SHA256(demo).toString())
console.log("HmacSHA256:" + cryptoJS.HmacSHA256(demo, key).toString())
console.log("SHA512:" + cryptoJS.SHA512(demo).toString())
console.log("HmacSHA512:" + cryptoJS.HmacSHA512(demo, key).toString())



// 输出
MD5:e10adc3949ba59abbe56e057f20f883e
HmacMD5:e8649f37712cdad94724b22154e4b83b
SHA1:7c4a8d09ca3762af61e59520943dc26494f8941b
HmacSHA1:8a497921f4d842b57f6e054cee4fd2d91577815a
SHA256:8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92
HmacSHA256:1b8e71ff0087551bb6f063b15ea850650ddbdf6a02e5e81c2a981b91efafebdd
SHA512:ba3253876aed6bc22d4a6ff53d8406c6ad864195ed144ab5c87621b6c233b548baeae6956df346ec8c17f5ea10f35ee3cbc514797ed7ddd3145464e2a0bab413
HmacSHA512:14f04320d0a4a21ed7517b94a8cf219f3e674fa95dd6281d5b61e3fc2e52a5567468e3ef4b4ace6edbecd6aa809f012606e482c367bbb7c285278a1bb3a0008d


// 不加toString 输出的形式是 wordArray
{
words: [ -519381959, 1236949419, -1101602729, -233863106 ],
sigBytes: 16
}
// 用一个字符串+ 后面的东西,即使没有toString也没问题,也是字符串形式,自动转换了就
console.log("MD5:" + cryptoJS.MD5(demo))

也存在一种,冗长,难写,复杂的实现形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var hasher = cryptoJS.algo.SHA256.create();
// reset是清空内容
hasher.reset();
// update是添加内容,可以多次添加
hasher.update("123456");
hasher.update("789");
// 进行加密
var hash = hasher.finalize().toString();
console.log(hash);

// Hmac还需要指定密钥
var hasherHmac = cryptoJS.algo.HMAC.create(cryptoJS.algo.SHA256, "demo");
hasherHmac.reset();
hasherHmac.update("123456");
hasherHmac.update("789");
var hmac = hasherHmac.finalize().toString();
console.log(hmac);

image-20241117205858280

怎么说呢,放着简单的不用,用复杂的干啥呢

字符串解析

在Java中字符串解析是转换成 base64或者hex或者UTF-8,在JS中当然没什么不一样了,换个写法而已

哦,说一下之前的wordArray

1
2
3
4
5
6
{
words: [ -519381959, 1236949419, -1101602729, -233863106 ],
sigBytes: 16
}

这个words是一个类似于字节数组的东西,将四个字节变成了一个整数,也可以理解成字节数组,反正就是存这个的

String转wordArray

1
2
3
4
5
6
7
8
// 这个就相当于Java中的 getBytes() 方法
cryptoJS.enc.Utf8.parse(utf8ToString)

// 就是HEX解码
cryptoJS.enc.Hex.parse(hexToString)

// 就是base64解码
cryptoJS.enc.Base64.parse(base64ToString)

image-20241117211621418

wordArray转String

1
2
3
4
5
6
7
8
9
10
11
// wordArray转string
let wordArray = cryptoJS.MD5("123456")
console.log(wordArray)
console.log(wordArray + '') // 强转
console.log(wordArray.toString())
console.log(wordArray.toString(cryptoJS.enc.Hex))
console.log(wordArray.toString(cryptoJS.enc.Base64))
// console.log(wordArray.toString(cryptoJS.enc.Utf8))
console.log(cryptoJS.enc.Hex.stringify(wordArray))
console.log(cryptoJS.enc.Base64.stringify(wordArray))
// cryptoJS.enc.Utf8.stringify(wordArray)

转UTF-8有报错,知道就行了,可以看到toString方法默认的是转成HEX编码

注意这个转base64是将字符数组转base64不是将hex转成base64编码,那样会变得很长,很显然这并没有发生

image-20241117212032481

这些的用处在于有时候得到的数据千奇百怪,需要各种处理,比如得到hex编码的明文

1
2
3
4
5
6
7
// 如果直接这样操作的话,是当作utf8来解析的
var mdhex = "313233343536"
console.log(cryptoJS.MD5(mdhex).toString())

// 这个时候就需要转换
var mdhex = "313233343536"
console.log(cryptoJS.MD5(cryptoJS.enc.Hex.parse(mdhex)).toString())

image-20241117212918100

对称加密算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 对称加密算法
// 不同算法的key长度和iv长度也不一样,就不进行初始化了,看一下格式就行,具体使用具体查找
// cfg一般包含加密模式、填充方式(PKcs7最常用和Java模式中的PKCS5结果一样)
var cfg = {
iv: iv,
mode: cryptoJS.mode.CBC,
padding: cryptoJS.pad.Pkcs7,
format: cryptoJS.format.Hex
}
var ciphertext = cryptoJS.DES.encrypt(message, key, cfg)
var plaintext = cryptoJS.DES.decrypt(ciphertext, key, cfg)

var ciphertext = cryptoJS.TripleDES.encrypt(message, key, cfg)
var plaintext = cryptoJS.TripleDES.decrypt(ciphertext, key, cfg)

var ciphertext = cryptoJS.AES.encrypt(message, key, cfg)
var plaintext = cryptoJS.AES.decrypt(ciphertext, key, cfg)

var ciphertext = cryptoJS.RC4.encrypt(message, key, cfg)
var plaintext = cryptoJS.RC4.decrypt(ciphertext, key, cfg)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 进行AES加密,iv和key都需要解析
var hexKeyBytes = cryptoJS.enc.Hex.parse("0123456789ABCDEF");
var hexIvBytes = cryptoJS.enc.Hex.parse("0123456789ABCDEF");

// cfg中不传mode和padding是默认的CBC/PKcs7的形式,format中可以定义格式化输出
var cfg = {
iv: hexIvBytes,
mode: cryptoJS.mode.CBC,
padding: cryptoJS.pad.Pkcs7,
format: cryptoJS.format.Hex
}
var encrypted = cryptoJS.AES.encrypt("123456", hexKeyBytes, cfg);

// 得到的这个 encrypted 是一个对象,加密的wordArray内容在 ciphertext 中
console.log(encrypted.ciphertext.toString());

注意:cfg中不指定输出形式的话,encrypted 默认以Base64的形式输出

image-20241118164530869

1
2
3
4
5
// 如果直接这样输出是会报错的
console.log(encrypted.toString(cryptoJS.enc.Base64));

// Hex则是无输出
console.log(encrypted.toString(cryptoJS.enc.Hex));

format属性中还可以夹带私货,自定义输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var cfg = {
iv: hexIvBytes,
mode: cryptoJS.mode.CBC,
padding: cryptoJS.pad.Pkcs7,
format: {
stringify: function (data){
let e = {
ct: data.ciphertext.toString(),
miaoshu: "这是自定义的输出"
};
return JSON.stringify(e)
},
// 定义parse是解密使用的
parse: function (data){
// 解析自己写的JSON
let json = JSON.parse(data)
// json.ct获取加密后的数据,以为是hex的形式,使用hex解析,再进行create创建,返回
let newVar = cryptoJS.lib.CipherParams.create({ciphertext: cryptoJS.enc.Hex.parse(json.ct)});
return newVar
}
}
}

image-20241118165839649

解密也没啥难度,不写了

其他算法

RIPEMD160 和 HmacRIPEMD160

1
2
3
4
5
6
7
8
9
// RIPEMD160 一种信息摘要算法
// 有两种形式,一种没有密钥--RIPEMD160
// 一种有密钥--HmacRIPEMD160
// 加密结果有40个十六进制字符

let ripemd160 = cryptoJS.RIPEMD160("123456");
console.log(ripemd160 + '')
let hmacRIPEMD160 = cryptoJS.HmacRIPEMD160("123456", "demo");
console.log(hmacRIPEMD160 + '')

image-20241118184812571

PBKDF2 和 EvpKDF

1
2
3
4
5
6
7
8
// PBKDF2
// keySize大小设定为 4/8/16 对应输出结果位数是 128/256/512
// iterations 表示迭代次数,这种算法的安全性随迭代次数增加而增加
let PBKDF2 = cryptoJS.PBKDF2("123456", "demo", { keySize: 8, iterations: 1000 });
console.log(PBKDF2 + '')
// EvpKDF
let EvpKDF = cryptoJS.EvpKDF("123456", "demo", { keySize: 8, iterations: 1000 });
console.log(EvpKDF + '')

image-20241118185931951

jsencrypt

非对称加密算法

非对称加密算法(RSA)CryptoJS没有,使用jsencrypt库来实现。

这个库是服务于web开发的,纯JS可能有问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
const JSEncrypt = require('jsencrypt');

// 这个密钥可以不加头不换行的,直接一个字符串进去
var publicKey = "-----BEGIN PUBLIC KEY-----\n" +
"MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC9zDcwxIE6ivfU821isEn0jpQM\n" +
"l+sSK+DA5fJYKEkaGUyOWyxLt2E89+wJx80Ezn2R62kK/NzQ84ZHH8bRG2P7upZ7\n" +
"LkYCDg+9EzmWzbPPIeSiTv5jHOFGCSmlC58S45eq5WBf8qU2haa50x004m87iW4S\n" +
"TrpCuA0mPjXP/ZkjcQIDAQAB\n" +
"-----END PUBLIC KEY-----";
var privateKey = "-----BEGIN RSA PRIVATE KEY-----\n" +
"MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAL3MNzDEgTqK99Tz\n" +
"bWKwSfSOlAyX6xIr4MDl8lgoSRoZTI5bLEu3YTz37AnHzQTOfZHraQr83NDzhkcf\n" +
"xtEbY/u6lnsuRgIOD70TOZbNs88h5KJO/mMc4UYJKaULnxLjl6rlYF/ypTaFprnT\n" +
"HTTibzuJbhJOukK4DSY+Nc/9mSNxAgMBAAECgYBPNCqP8mEPYjDcMB2kwnaKVPmZ\n" +
"a8hQU/k95nfErEMdXhNhkNCiZEty2u8ogbWf3N/wBfJXAIDRvd56Tdt1Jd4J1UJ+\n" +
"osHNkmWfx9ld6GYWkN8JRFGjI8Gj3FTNKqWhWCTlnpvWsyyK4F6aCHIWD6f211lk\n" +
"u6H1+r/q9zjvu1rNhQJBAOwTn38twXyW7CumZKfqtaBJfnhKOvVmDMyGia02oOx1\n" +
"ZVL9stsoYKCC/RxcBEi6E3ZfQOeDyBGxN5UxCiojyMsCQQDN0L9sG2KJHHa7BuwA\n" +
"lkXDVs6geE+a4SdNbHSp5JMfUPID7ul+r0wlnpljD7g3s/2+TkpqUseMiNPs5Hhb\n" +
"GwkzAkBDyTapC/hcz/Esb3DDjm9sgO3hmF7pi83tBEyQAfmfK+5WMCalKyjjrfkD\n" +
"paBNSbDA8oTudTaDbgFpw1UJ2JCVAkBxqlGtgMpAcunXjJEWGefZY72lvgwouyQb\n" +
"jEQ597SQ3QFrzqxBfMqPFDIeFXZlvQ/r5A0Q/zqZkI+KCvu1RQ8lAkAZEhjGasXX\n" +
"SNZuX0FZzcAUEs/rkhrPL4pqNg5XXda/nzYHdxdPVMsgv8A3RSKBc7P+2wMrB075\n" +
"yCszbgV/ovWx\n" +
"-----END PRIVATE KEY-----";


// 加密
function setEncrypt (msg) {
// 获取 JSEncrypt 对象
const jsencrypt = new JSEncrypt();
// 初始化公钥
jsencrypt.setPublicKey(publicKey);
// 返回加密结果
return jsencrypt.encrypt(msg);
}

console.log(setEncrypt("123456"));

解决这个报错

image-20241118195155371

1
2
3
windows = global;
或者
var windows = global;

image-20241118195128484

还有这个报错

image-20241118195326505

这个报错是由于错误的引用导致的

1
const JSEncrypt = require('jsencrypt');

虽然底下这俩和cryptoJS一样冒绿光,但是就是报错,很奇怪

image-20241118195526753

调教好之后运行

看结果很明显是 PKCS1Padding 的填充,如果想使用其他填充模式,就需要一些操作了

image-20241118195657998

JSEncrypt添加NOPadding填充

之前了解到NOPadding的填充是在明文前面填充字节0,如果我想实现的话就可以进行手动填充

打个断点进去

image-20241118201358518

结果在这直接return了,从这里进去

image-20241118201651343

导出了一个方法,这个方法是用来将 hex 的十六进制数据转换成 base64 编码

image-20241118202056572

继续进,getkey 方法是检验之前是否传入了key,不管,继续

image-20241118202319995

进入加密方法,这个是关键方法

maxLength 是检测最大字符长度,不用管他,PKSC1Padding 和 NOPadding 都需要这个数

经过 pkcs2 处理后进行判断是否有内容,有内容就进行加密,然后进行hex编码,再进行hex转base64将结果返回。

image-20241118202631605

这里就直接进PKSC1模式进行加密了,可以在这里加一个判断,让用户多传进来一个字符串,来判断模式,这样就可以二者兼得

开始尝试,依照pkcs写Nopadding照葫芦画瓢了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// NOPadding (type1, byte[0]) pad input string s to n byte[n] array, and return a bigint
// 入乡随俗了老铁
function nopkcs(s, n) {
if (n < s.length) {
console.error("Message too long for RSA")
return null;
}
var ba = [];
var i = s.length - 1;
while (i >= 0 && n > 0) {
var c = s.charCodeAt(i--);
if (c < 128) { // encode using utf-8
ba[--n] = c;
}
else if ((c > 127) && (c < 2048)) {
ba[--n] = (c & 63) | 128;
ba[--n] = (c >> 6) | 192;
}
else {
ba[--n] = (c & 63) | 128;
ba[--n] = ((c >> 6) & 63) | 128;
ba[--n] = (c >> 12) | 224;
}
}
// 前面的都一样,但是处理完数据之后,就该填充0了,这时候就不一样了
while (n > 0) {
ba[--n] = 0;
}
return new BigInteger(ba);
}

添一个判断model的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
RSAKey.prototype.encrypt = function (text, model) {
var maxLength = (this.n.bitLength() + 7) >> 3;
var m;
// 还是保留默认是PKCS#1
if (model == "PKCSPadding" || model == undefined || model == null) {
m = pkcs1pad2(text, maxLength);
} else if (model == "NOPadding") {
m = nopkcs(text, maxLength)
} else {
console.error("Invalid padding model");
return null;
}

if (m == null) {
return null;
}
var c = this.doPublic(m);
if (c == null) {
return null;
}
var h = c.toString(16);
var length = h.length;
// fix zero before result
for (var i = 0; i < maxLength * 2 - length; i++) {
h = "0" + h;
}
return h;
};

更改传入值,可以发现这之只是改了加密,还没改解密,不着急,先让这个传入两个参数,base形式的就先放一放

image-20241118210735767

在相应的文件中更改,打断点调试的时候还是源代码,待我稍作思量,更益其巧

找到这个文件,上面有各个文件的信息啥的,代码没有被解析,怎么说呢,直接改吧

image-20241119145222944

调用时,多接收一个参数

image-20241118211817786

// 处理

1
2
3
4
5
6
判断方法
\n var m;\n // 还是保留默认的是PKCS#1\n if (model == 'PKCSPadding' || model == undefined || model == null) {/n m = pkcs1pad2(text, maxLength);/n } else if (model == 'NOPadding') {\n m = nopkcs(text, maxLength);\n } else {\n console.error('Invalid padding model');\n returm null;\n }


Nopadding填充操作
function nopkcs(s, n) {\n if (n < s.length) {\n console.error('Message too long for RSA');\n return null;\n }\n var ba = [];\n var i = s.length - 1;\n while (i >= 0 && n > 0) {\n var c = s.charCodeAt(i--);\n if (c < 128) {\n ba[--n] = c;\n }\n else if ((c > 127) && (c < 2024)) {\n ba[--n] = (c & 63) | 128;\n ba[--n] = (c >> 6) | 192;\n }\n else {\n ba[--n] = (c & 63) | 128;\n ba[--n] = ((c >> 6) & 63) | 128;\n ba[--n] = (c >> 12) | 224;\n } \n }\n // 前面的都一样,但是处理完数据之后,就改填充0了,这时候就比PKSC简单了\n while (--n > 0) {\n ba[--n] = 0;\n }\n return new _jsbn__WEBPACK_IMPORTED_MODULE_0__.BigInteger(ba);\n}

大功告成

image-20241119144624973

验证一下

无需多言,还有解密

image-20241119145500510

接下来写NOPadding的解密

和加密的套路差不多,先检验是否有私钥,然后获取 base64转hex 的方法,将密文转成hex

image-20241119153831614

然后就是重点了

image-20241119154429072

这个C是处理hex数据的和解密无关,m我也看了一下直接用就行,真正处理的在 return上,这样也省事了,开始微操

加一个判断,通过打断点发现,这个m比较短,也就是说很可能就是明文的一种编码,反复实验之后先将这个 BigInteger 类型的数据转换成 byte 数组,然后将byte数组转换成utf-8的数据,有点弯弯绕。

image-20241119165204122

1
2
3
4
5
6
7
8
>return m;
>BigInteger { '0': 53753142, '1': 201507, s: 0, t: 2 }

>return m.toByteArray();
>[ 49, 50, 51, 52, 53, 54 ]

>return String.fromCharCode.apply(null, m.toByteArray());
>123456

需要更改的位置有

这个是调试视图,去那一堆没解析的乱码里去改!就不放图片了

image-20241119165810685