对称加密-DES学习笔记及天翼云逆向
仅仅学习其实现、用法,以做逆向知识储备,不深入其原理
常见算法归纳
- DES
- AES
DES
DES算法的入口参数有三个:Key
、Data
、Mode
。
Key
为8个字节共64位,是DES算法的工作密钥;Data
也为8个字节64位,是要被加密或被解密的数据;Mode
为DES的工作方式,有两种:加密或解密。
其中Mode
支持以下工作模式:
- ECB 电子密码本模式(最常用之一)
- CBC 密文分组链接模式(最常用之一)
- CFB 密文反馈模式
- OFB 输出反馈模式
- CTR 计数器模式
对不不满足宽度的数据,会进行填充,即padding
,支持以下:
- ZeroPadding
- NoPadding
- AnsiX923
- Iso10126
- Iso97971
- Pkcs7(最常用)
ECB模式
介绍
引自分组加密的四种模式 (ECB、CBC、CFB、OFB) - yanzi_meng
DES ECB(电子密本方式)其实非常简单,就是将数据按照8个字节一段进行DES加密或解密得到一段8个字节的密文或者明文,最后一段不足8个字节,按照需求补足8个字节进行计算,之后按照顺序将计算所得的数据连在一起即可,各段数据之间互不影响。
特点:
- 简单,有利于并行计算,误差不会被传送;
- 对于相同的明文块,会生成相同的密文块
- 可能对明文进行主动攻击:加密消息块相互独立成为被攻击的弱点
JS实现
与下CBC类似,不再演示,转而用实战演示:
某小网站(注意不要相信任何赌博等非法内容,这里仅用于技术学习)
登录表单被加密,通过XHR断点找到登录的js代码处:
涉及到表单加密的代码:
utils.desEncrypt(JSON.stringify(a), i)
跳转至utils.desEncrypt()
源码,为:
desEncrypt: function(e, t) {
var n = CryptoJS.enc.Utf8.parse(t);
return CryptoJS.DES.encrypt(e, n, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
}).toString()
}
其中a
在控制台输出结果为:
{
"username": "asda551313",
"password": "assd45646",
"captcha": ""
}
i
为:
'PSfqxlZRUXMuiX7R'
对应源码为:
var i = utils.rndString();
//rndString()源码:
function rndString() {
for (var e = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz", t = "", n = 0; n < 16; n++) {
var a = Math.floor(Math.random() * e.length);
t += e.substring(a, a + 1)
}
return t
}
可以发现其为随机值且与时间戳无关,因此基本可以将其写死
我们分别拿到desEncrypt()
和rndString()
的源码进行扣代码处理
最终结果:
let CryptoJS = require('crypto-js')
function rndString() {
for (var e = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz", t = "", n = 0; n < 16; n++) {
var a = Math.floor(Math.random() * e.length);
t += e.substring(a, a + 1)
}
return t
}
function desEncrypt(e, t) {
var n = CryptoJS.enc.Utf8.parse(t);
return CryptoJS.DES.encrypt(e, n, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
}).toString()
}
const login_data = {
"username": "asda551313",
"password": "assd45646",
"captcha": ""
}
console.log(desEncrypt(JSON.stringify(login_data),rndString()))
输出结果:
sVkZh+drU7OD3GcrLn25d5KP+jc2igSM7BgcTxaMT2vhyZTFHXM58+0a2sHYMICCIoYm7IS/efeUyzsZ51Y27w==
逆向完毕
CBC模式
介绍
CBC模式会多出一个初始化向量iv
,会在每个明文块加密前做异或处理
这样的操作是防止相同明文生成相同密文的情况,减少被破解几率
JS实现
Node环境,需要crypto-js模块
源码如下:
let crypto = require('crypto-js')
function desEncrypt(desKey,desIv,desMsg){
let key = crypto.enc.Utf8.parse(desKey),
iv = crypto.enc.Utf8.parse(desIv),
msg = crypto.enc.Utf8.parse(desMsg),
encrypted = crypto.DES.encrypt(msg,key,{
iv:iv,
mode:crypto.mode.CBC,
padding:crypto.pad.Pkcs7
});
return encrypted.toString()
}
console.log(desEncrypt('asd4a5d7adz','zxjgcjahsgbdhj','Hello World!'))
输出结果:
MG3fwFspzdTydWuP4chAKA==
逆向完毕
因此对于逆向来说,我们至少要找到明文,密钥以及初始向量,即上面的msg
、key
和iv
(如果没有初始向量则不需要)
实战示例
某教育网站
同样步骤:
通过XHR找到对应代码:
这里关注pass
表单项
pass: encryptByDES(s)
s
为输入的密码,跳转到encryptByDes()
源码处:
可以看到很标准的一套CBC加密流程,这里不再重复写代码,逆向结束。
天翼云实战
逆向
这里以天翼云为例:
我们主要关注表单项里的password
项,通过XHR断点,层层观察,发现在此处涉及到了表单项
并且是被传入(这不废话?),追溯到上一调用处,发现传入的是s
,而s
的定义赫然就摆在了上面:
现在来进行一个扣代码:
首先通过之前的表单发现,userName
即我们输入的账号,并为进行任何处理,因此我们用账号替换掉Object(v["g"])(o.value)
,
const account = 'asdasdadss@163.com'
const s = {
userName: account,
password: encodeURI(Object(v["c"])(a.value, Object(v["f"])(account)))
}
再通过控制台输出,发现a.value
的值就是我们输入的密码,替换之:
let account = 'asdasdadss@163.com'
let pswd = 'asd445zx48zs4as3d5'
const s = {
userName: account,
password: encodeURI(Object(v["c"])(pswd, Object(v["f"])(account)))
}
再来解决Object(v["f"])(account)
,通过观察其输出值发现为:
'asdasdadss@163.com000000'
在控制台多次调用并输出其它结果对比发现,其输出值长度固定为24,如果不满足则在结尾加0
致使其补足长度至24,结合其源码在本地复现:
function vf(e){
let a = 24,
t = "0";
if (e.length < a)
for (let r = e.length; r < a; r++)
e += t;
else
e = e.substring(0, a);
return e
}
再来跳转至Object(v["c"])
的源码处,源码片段为:
T = function(e) {
var n = arguments.length > 1 && void 0 !== arguments[1] ? arguments[1] : ""
, t = arguments.length > 2 && void 0 !== arguments[2] ? arguments[2] : {}
, a = t.enc
, r = void 0 === a ? "Utf8" : a
, i = t.mode
, c = void 0 === i ? "ECB" : i
, o = t.padding
, u = void 0 === o ? "Pkcs7" : o
, d = p.a.enc[r].parse(n)
, l = {
mode: p.a.mode[c],
padding: p.a.pad[u]
}, s = p.a.TripleDES.encrypt(e, d, l);
return s.toString()
}
可以发现其参数只有一个,即原始输入的密码值,因此vf(account)
完全可以删除,
尝试观察p.a
的值
发现涵盖了多种加密算法,基本可以断定其为第三方加密库,而crypto-js
更是恰好包含了这个TripleDES
的加密方式,因此暂时使用crypto-js
平替,待最终对比结果
搜索crypto-js
中关于TripleDES
的用法
前端加解密库 CryptoJS 使用(Triple DES 对称加密)
const key = CryptoJS.enc.Utf8.parse("4c43c365a4ac05b91eb5fa95"); // key
const iv = CryptoJS.enc.Utf8.parse("4c43c365"); // iv
// 直接使用 key 是不对的,需要像上面那样处理
// const key = "4c43c365a4ac05b91eb5fa95"; // key
// const iv = key.substr(0, 8); // iv
function encrypted(){
const encrypted = CryptoJS.TripleDES.encrypt(params, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
return encrypted.toString(); // 返回加密后的字符串
}
对比之下就非常明了了,p.a.TripleDES.encrypt(e, d, l)
中e
为原始密码,l
为加密模式相关,其值基本固定为下:
- mode: ECB
- padding: Pkcs7
而d
的值为Key,与下面代码片段相关:
d = p.a.enc[r].parse(n)
//p.a.enc[r].parse()
parse: function(t) {
return s.parse(unescape(encodeURIComponent(t)))
}
//s.parse
parse: function(t) {
for (var e = t.length, n = [], r = 0; r < e; r++)
n[r >>> 2] |= (255 & t.charCodeAt(r)) << 24 - r % 4 * 8;
return new a.init(n,e)
}
其中p.a.enc[r].parse(n)
中的n
观察其值发现即为vf(account)
,再次观察d
的值,并结合源码,return new a.init(n,e)
处我们可直接改为return n
,要的就是生成的长度为6的数组,但是在填入加密函数时,l
实际为对象,例如:
l = {
sigBytes:24,
words:[]
}
其中sigBytes
为固定值,words
数组便是上面函数生成后的长度6的数组n
,因此这里可以直接返回n
然后再自行处理,或者干脆直接返回上列格式的对象
最终在本地复现生成d
数组的代码为:
function parse(t) {
const Sparse = function(t) {
for (var e = t.length, n = [], r = 0; r < e; r++)
n[r >>> 2] |= (255 & t.charCodeAt(r)) << 24 - r % 4 * 8;
return n
}
return Sparse(unescape(encodeURIComponent(t)))
}
最终本地代码为:
let crypto = require('crypto-js')
function parse(t) {
const Sparse = function(t) {
for (var e = t.length, n = [], r = 0; r < e; r++)
n[r >>> 2] |= (255 & t.charCodeAt(r)) << 24 - r % 4 * 8;
return n
}
return Sparse(unescape(encodeURIComponent(t)))
}
function vf(e){
let a = 24,
t = "0";
if (e.length < a)
for (let r = e.length; r < a; r++)
e += t;
else
e = e.substring(0, a);
return e
}
function getD(account){
return {
sigBytes:24,
words:parse(vf(account))
}
}
function desEntrypt(desMsg,desKey){
const encrypted = crypto.TripleDES.encrypt(desMsg,desKey,{
mode:crypto.mode.ECB,
padding:crypto.pad.Pkcs7
});
return encrypted.toString()
}
let account = 'asdasdadss@163.com'
let pswd = 'abc123456789'
const s = {
userName: account,
password:encodeURI(desEntrypt(pswd,getD(account)))
}
console.log(s)
生成结果:
{userName: 'asdasdadss@163.com', password: 'M4rpUkzQiMGE0YP6YR26aQ=='}
与请求对比
结果一致
模拟返回
根据编辑重放
发现仅需图上四个参数即可正常返回,除账号密码外其余均为固定值,因此可很快写出脚本
为了Python的便捷调用,给上面的js代码新增以下函数:
function getPswd(account,pswd){
return encodeURI(desEntrypt(pswd,getD(account)))
}
Python代码如下:
import os
import execjs
import requests
os.environ["NODE_PATH"] = os.getcwd()+"/node_modules"
def get_encrypt(account,pswd):
with open('tyy.js') as f:
js_code = f.read()
js = execjs.compile(js_code)
return js.call('getPswd', account,pswd)
def main(account,pswd):
header = {
"Accept": "application/json, text/plain, */*",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6,ja;q=0.5",
"User-Agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1 Edg/108.0.0.0"
}
res = requests.get(url='https://m.ctyun.cn/account/login',headers=header,params={
'userName':account,
'password':get_encrypt(account,pswd),
'referrer':'wap',
'mainVersion':'300031500'
})
if res.status_code == 200:
res_json = res.json()
return res_json
else:
return res.text
if __name__ == '__main__':
print(main('abc123456@163.com','ashdakozxbces'))
响应结果:
{'resultCode': '-1', 'resultMsg': '账户或密码不正确', 'data': None, 'extendObj': None, 'code': '', 'success': False}
逆向及模拟返回成功