仅仅学习其实现、用法,以做逆向知识储备,不深入其原理

常见算法归纳

  • DES
  • AES

DES

DES算法的入口参数有三个:KeyDataMode

  • 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类似,不再演示,转而用实战演示:
某小网站(注意不要相信任何赌博等非法内容,这里仅用于技术学习)

LOGIN

登录表单被加密,通过XHR断点找到登录的js代码处:
login 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,会在每个明文块加密前做异或处理
https://blog.csdn.net/jdhellfire/article/details/79680456-加密算法学习总结---DES-CBC加密算法

这样的操作是防止相同明文生成相同密文的情况,减少被破解几率

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==

逆向完毕

因此对于逆向来说,我们至少要找到明文,密钥以及初始向量,即上面的msgkeyiv(如果没有初始向量则不需要)

实战示例

某教育网站
同样步骤:
login

通过XHR找到对应代码:
js

这里关注pass表单项

pass: encryptByDES(s)

s为输入的密码,跳转到encryptByDes()源码处:
encry

可以看到很标准的一套CBC加密流程,这里不再重复写代码,逆向结束。

天翼云实战

逆向

这里以天翼云为例:
tyy

我们主要关注表单项里的password项,通过XHR断点,层层观察,发现在此处涉及到了表单项
js-1

并且是被传入(这不废话?),追溯到上一调用处,发现传入的是s,而s的定义赫然就摆在了上面:
js-2

现在来进行一个扣代码:
首先通过之前的表单发现,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的值
pa

发现涵盖了多种加密算法,基本可以断定其为第三方加密库,而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}

逆向及模拟返回成功