JavaScript逆向之AES和DES加解密
简介
加密算法总共分为两类:对称加密和非对称加密。对称加密顾名思义就是加密和解密时使用的密钥是同一个,例如AES、DES、3DES;非对称加密就是加密和解密时使用的密钥不是同一个,例如RSA。
在python中有一个第三方库提供了加密逻辑,库名为PyCrypto
,安装命令为pip install PyCrypto
,但是装这个库很可能会出现问题。所以我们可以用另外一个库来平替,库名叫pycryptodome
,安装命令为pip install pycryptodome
,这个库里面包含了PyCrypto
。
安装pycryptodome
库成功之后,有可能加密包无法使用,如果无法使用,可以去site-packages
文件夹里找crypto
文件夹,把它的名字改为Crypto
就可以了。
AES算法
进行AES加密的时候需要三个参数:
(1)key:通常是16位的字节
(2)mode:CBC、ECB(常见的就这两种)
(3)iv:CBC模式需要,通常是16位的字节;ECB模式下不需要iv。
下面就介绍如何用python代码实现AES加解密。
AES加密代码:
点击查看代码
# 导入AES,请注意大小写问题
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad # 填充
import base64
# 如何进行aes加密
# 1.创建一个aes加密器
aes = AES.new(key=b'1234567891234567', IV=b'1234567891234567', mode=AES.MODE_CBC)
ming = "威海龙王和东海龙王谁猛".encode("utf-8") # 必须处理成字节,加密或者解密处理的都是字节
ming = pad(ming, 16) # 填充到16位
mi = aes.encrypt(ming) # 加密之后的内容是杂乱无章的字节,没有任何规律的字符
# 最合适的base64
s = base64.b64encode(mi).decode()
print(s) # 7ieeKO9ArGM9Ngs/MWshUA+L5S+J+6sk1ozroSTVhwvPcAvziA9gMQwDE2flBney
AES解密代码:
点击查看代码
# 导入AES,请注意大小写问题
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad # 填充
import base64
mi = "7ieeKO9ArGM9Ngs/MWshUA+L5S+J+6sk1ozroSTVhwvPcAvziA9gMQwDE2flBney"
# 解密逻辑(对称加密里)加密和解密逻辑几乎一样
aes = AES.new(key=b'1234567891234567', IV=b'1234567891234567', mode=AES.MODE_CBC)
mi = base64.b64decode(mi)
ming = aes.decrypt(mi)
ming = unpad(ming, 16) # 干掉填充即可
print(ming.decode("utf-8")) # 威海龙王和东海龙王谁猛
DES算法
进行DES加密的时候需要三个参数:
(1)key:通常是8位的字节
(2)mode:CBC、ECB(常见的就这两种)
(3)iv:CBC模式需要,通常是8位的字节;ECB模式下不需要iv。
下面就介绍如何用python代码实现DES加解密。
DES加密代码:
点击查看代码
from Crypto.Cipher import DES
from Crypto.Util.Padding import pad, unpad # 填充
import base64
s = "我爱吃鱼"
ming = s.encode("utf-8")
des = DES.new(key=b'12345678', iv=b'12345678', mode=DES.MODE_CBC)
ming = pad(ming, 8)
mi = des.encrypt(ming)
mi_s = base64.b64encode(mi).decode()
print(mi_s) # bXZ8WcOzh/SW0yuFPTTZog==
DES解密代码:
点击查看代码
from Crypto.Cipher import DES
from Crypto.Util.Padding import pad, unpad # 填充
import base64
mi_s = "bXZ8WcOzh/SW0yuFPTTZog=="
mi = base64.b64decode(mi_s)
des = DES.new(key=b'12345678', iv=b'12345678', mode=DES.MODE_CBC)
ming = des.decrypt(mi)
ming = unpad(ming, 8)
ming_s = ming.decode("utf-8")
print(ming_s) # 我爱吃鱼
下面就用两个例子来介绍DES和AES的具体应用
某招标网站——DES加解密
url地址:https://ctbpsp.com/#/
访问网站,打开开发者工具,这个网站用右键和快捷键都打不开开发者工具,需要手动的去浏览器的工具栏里的更多工具处调用。刷新界面,锁定Fetch/XHR,只触发了一条流量。
看到响应数据中是一段加密后的字符串。
想要知道这段字符串的意思就得知道加密逻辑是什么。切换到initiator,看到async关键词,说明是异步加载,可以直接搜索interceptors关键词来定位。
总共有5处地方,其中有三处地方是interceptors的源码,可以不用看。
看看另外两处地方。
由于响应内容是加密的,所以大概率加密逻辑是响应拦截器处。在响应拦截器中看到了非常明显的关键函数JSON.parse,该函数用于将json数据进行解析,所以C(e.data)大概率就是加密过后的数据了。在该行打断点进行调试。
定位到C函数的具体实现位置。
从C函数的代码中可以得到C函数其实是个DES加密算法。接下来就是得到key、iv和mode这三个关键参数了。一行一行代码往下执行,查看每一个变量的值。
t其实是我们需要的key,但是这里有个坑,因为t的长度是10位,而DES算法中的key应该是8位,所以在我们进行解密的时候需要对t进行处理。ciphertext是对数据进行base64编码后的结果,所以在解密的时候要先对其进行base64解码。mode为ECB,就不需要iv了。知道这三个参数后,就可以写python代码了。
点击查看代码
import requests
from Crypto.Cipher import DES
from Crypto.Util.Padding import pad, unpad # 填充
import base64
import json
url = "https://ctbpsp.com/cutominfoapi/recommand/type/5/pagesize/10/currentpage/1"
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 "
"Safari/537.36"
}
resp = requests.get(url, headers=headers)
mi_str = resp.text # 加密的相应内容
mi_str = mi_str.strip('"')
# 解密逻辑
# 用的什么加密方式 DES
# key:"1qaz@wsx3e"
# iv
# mode:ECB(没有iv)
mi = base64.b64decode(mi_str)
des = DES.new(key="1qaz@wsx3e"[:8].encode("utf-8"), mode=DES.MODE_ECB) # key取前8位即可
ming = des.decrypt(mi)
ming = unpad(ming, 8)
s = ming.decode("utf-8")
dic = json.loads(s)
print(dic)
运行结果:
看准网——AES加解密
url:https://www.kanzhun.com/search?cityCode=7&industryCodes=&pageNum=1&query=操作员&type=4
访问网站,看触发的数据包。
两条数据包,其中screen.json
这条数据包都是明文不需要解密,salary.json
数据包中的payload和response都是加密后的字符串。
开始找加密逻辑了。这里还是以搜索url关键词来定位,搜索salary.json
,只找到一条符合的。
发现search
是个字典,将/api_to/search/salary.json
赋值给了getSearchSalaryList
,接下来搜索哪里调用了getSearchSalaryList
,有两处地方。
由于不能确定触发哪个,所以在两处都打断点。
切换一个城市,看到底触发哪个。停在了第二处,说明第一处没用,可以把第一处的断点删除。
根据me.search.getSearchSalaryList(pe(pe({}, y), {}, {pageNum: f.current,limit: 15}));
这行代码可以知道getSearchSalaryList
是个函数,看下getSearchSalaryList
的具体实现。
在return处打断点,让程序运行到这里。
可以看到变量e
和url中的传参很相似。该函数还调用了U函数,定位U函数。
打上断点,运行到这里。
变量e
是当前url的地址,变量t
是url中的传参。看下T(T({data: t,url: e}, n), {}, {method: "get"})
得到的是什么。
其实就是将传过来的变量e
和变量t
进行了整合。再看a函数的具体实现。
打上断点,运行到这里。
看下运行到黄框处时,变量t
、n
、r
、a
、s
的值。
变量r
的赋值过程如下所示。
也是跟时间戳相关,这里不需要很关心。继续往下看代码。
0 == a.url.indexOf("/api_to") && -1 != a.url.lastIndexOf(".json") && (a.crypto = !0, a.responseType = "text", "string" == typeof a.data && a.data.indexOf("kiv") > -1 ? (t = (n = "string" == typeof a.data ? JSON.parse(a.data) : a.data).b, s = n.kiv) : (s = (0, M._A)(), n = "object" == typeof a.data ? JSON.stringify(a.data) : a.data, t = (0, M.mA)(n, { iv: s }).replace(/\//g, "_").replace(/\+/g, "-").replace(/=/g, "~")))
这段代码其实是个比较长的三目运算,为了方便,将其拆分成三段。
(1)条件判断:
0 == a.url.indexOf("/api_to") && -1 != a.url.lastIndexOf(".json") && (a.crypto = !0, a.responseType = "text", "string" == typeof a.data && a.data.indexOf("kiv") > -1
(2)操作一:
(t = (n = "string" == typeof a.data ? JSON.parse(a.data) : a.data).b, s = n.kiv)
(3)操作二:
(s = (0, M._A)(), n = "object" == typeof a.data ? JSON.stringify(a.data) : a.data, t = (0, M.mA)(n, { iv: s }).replace(/\//g, "_").replace(/\+/g, "-").replace(/=/g, "~"))
如果条件判断成立,执行操作一;不成立则执行操作二。
先看条件判断中的第一个语句0 == a.url.indexOf("/api_to")
,看url中是否包含/api_to
,如果包含则该语句为假,不包含为真。输出下a.url
。
包含/api_to
,故0 == a.url.indexOf("/api_to")
为假,又由于几个条件之间采用的是短路与,第一个为假,后面的就不用判断了,整条语句的结果就为假,执行操作二。操作二中又分为以下几个语句:
(1)s = (0,M._A)()
(2)n = "object" == typeof a.data ? JSON.stringify(a.data) : a.data
(3)t = (0,M.mA)(n, {iv: s}).replace(/\//g, "_").replace(/\+/g, "-").replace(/=/g, "~")
先分析第一个语句:(0,M._A)
相当于M._A
故s = M._A()
,定位到M._A函数。
从代码中可以知道M._A就是随机产生一个长度为16的字符串,故s就是长度为16的字符串。
再分析第二个语句:判断a.data的类型是不是object,如果是,则n = JSON.stringify(a.data)
,否则n = a.data
。输出一下a.data的数据类型。
a.data的数据类型是object,所以n = JSON.stringify(a.data)
,相当于把a.data转换成字符串类型,输出一下n。
其实就是url中的参数,将其转换成了字符串。
再分析第三个语句:(0,M.mA)
相当于M.mA
故(0,M.mA)(n, {iv: s})
相当于M.mA(n, {iv: s})
,看下M.mA
是如何实现的。
打上断点,让程序运行到这里。
也是一个三元运算,分析一下。e ? ("string" != typeof e && (e = e.toString()),l(e, t.iv)) : ""
,e如果有值就执行冒号前面的语句,如果没值,那e还是为空。
具体看下"string" != typeof e && (e = e.toString()),l(e, t.iv)
的执行。
e
的数据类型为string,故逗号前面的语句直接为假,只需要看l(e, t.iv)
就可以了。定位到l函数的实现。
终于找到最最重要的地方了。是一个AES加密,接下来就是找key,iv和mode了。key和mode都跟变量n
相关,所以看下n
是如何生成的。
打断点,让程序运行到这里。
i
为空,t
、n
、r
是三个正则匹配器,e
做了一个替换操作,可是你会发现是一个空字符串进行的替换操作,那么结果不就是个空字符串吗?其实不然,这个双引号里是有东西的,只不过我们看不见。往下运行就可以知道变量e
是什么了。跳出看函数,返回到n=u()
处,直接查看n
是什么。
那么key
就为n.key
,iv
就为t
,既然有了iv,就可以大胆猜测mode为CBC。
n.key
是字节形式,可以采用toString方法把它转换成字符串。
知道了采用的是AES算法,key、iv、mode都获取到了,就可以写python代码了。还要注意一点,在第三个语句中还会将\
替换成_
,+
替换成-
,=
替换成~
。
python代码如下:
点击查看代码
"""
/**
* 加密方式:AES
* key:'G$$QawckGfaLB97r'
* iv:'Qxdi5Py0QZg6Itj1'
* mode:CBC
*
* 加密之后的base64 把/处理成_,+处理成-,=处理成~
*/
"""
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import base64
import requests
import json
url = "https://www.kanzhun.com/api_to/search/salary.json"
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 "
"Safari/537.36",
}
data = {
"query": "操作员",
"cityCode": 7,
"industryCodes": "",
"pageNum": 1,
"limit": 15
}
kiv = 'Qxdi5Py0QZg6Itj1'
aes = AES.new(key='G$$QawckGfaLB97r'.encode("utf-8"), iv=kiv.encode("utf-8"), mode=AES.MODE_CBC)
ming_bytes = json.dumps(data).encode("utf-8")
ming_bytes = pad(ming_bytes, 16)
mi_bytes = aes.encrypt(ming_bytes)
mi = base64.b64encode(mi_bytes).decode()
mi = mi.replace("/", "_").replace("+", "-").replace("=", "~")
params = {
"b": mi,
"kiv": kiv
}
resp = requests.get(url, headers=headers, params=params)
# print(resp.request.url)
resp_text = resp.text # 获取到密文
resp_text_byte = base64.b64decode(resp_text)
# 解密,不可以用上面加密的那个aes,需要重新创建
de_aes = AES.new(key='G$$QawckGfaLB97r'.encode("utf-8"), iv=kiv.encode("utf-8"), mode=AES.MODE_CBC)
resp_ming_bytes = de_aes.decrypt(resp_text_byte)
resp_ming_bytes = unpad(resp_ming_bytes, 16)
resp_ming = json.loads(resp_ming_bytes.decode("utf-8"))
print(resp_ming)
运行结果如下: