前言
现在一些网站对 JavaScript 代码采取了一定的保护措施,比如变量名混淆、执行逻辑混淆、反调试、核心逻辑加密等,有的还对数据接口进行了加密,这次的案例是对 MD5 + AES 加密方式的破解。
MD5 对数据进行有损压缩,结果不可逆无法还原,不能算为加密算法,是摘要算法,不论数据有多长,都会生成固定的 128 位的散列值,但是可以检测数据是否被篡改,因为只要改动了任何一个 bit 的数据,摘要结果都会不一样。
AES 是对称加密,对称加密是指加密和解密时使用同一个密钥,这种加密方式加密速度非常快,适合经常发送数据的场合,缺点是密钥的传输比较麻烦。
声明
本文章中所有内容仅供学习交流,相关链接做了脱敏处理,若有侵权,请联系我立即删除!
案例目标
网址:aHR0cHM6Ly93ZWIuZXd0MzYwLmNvbS9yZWdpc3Rlci8jL2xvZ2lu
登录接口:aHR0cHM6Ly9nYXRld2F5LmV3dDM2MC5jb20vYXBpL2F1dGhjZW50ZXIvdjIvb2F1dGgvbG9naW4vYWNjb3VudA==
以上均做了脱敏处理,Base64 编码及解码方式:
import base64
# 编码
# result = base64.b64encode('待编码字符串'.encode('utf-8'))
# 解码
result = base64.b64decode('待解码字符串'.encode('utf-8'))
print(result)
常规 JavaScript 逆向思路
一般情况下,JavaScript 逆向分为三步:
- 寻找入口:逆向在大部分情况下就是找一些加密参数到底是怎么来的,关键逻辑可能写在某个关键的方法或者隐藏在某个关键的变量里,一个网站可能加载了很多 JavaScript 文件,如何从这么多的 JavaScript 文件的代码行中找到关键的位置,很重要
- 调试分析:找到入口后,我们定位到某个参数可能是在某个方法中执行的了,那么里面的逻辑是怎么样的,调用了多少加密算法,经过了多少赋值变换,需要把整体思路整理清楚,以便于断点或反混淆工具等进行调试分析
- 模拟执行:经过调试分析后,差不多弄清了逻辑,就需要对加密过程进行逻辑复现,以拿到最后我们想要的数据
接下来开始正式进行案例分析:
寻找入口
进入到某升学助考网的登录页面,F12 打开开发者人员工具,切换到 network 准备查看网络抓包请求,随便输入一个账号、密码,查看网络抓包请求情况,如下图,可以看到抓包到的这条数据,返回了登录响应的内容,即此处为登录的接口:
通过观察请求头中响应返回的数据,可以发现 Request Headers 中的 sign 和 Request Payload 中的 password 是经过加密了的,用户名则为明文传输:
调试分析
sign 参数
CTRL + SHIFT + F 全局搜索 sign 参数会发现出现了很多结果,筛选难度很大,因为 sign 是以 JSON 形式返回的数据,所以可以直接搜索 sign 加冒号,筛选难度大大降低,通过观察会发现 sign 的值在 request.js 中被加密了:
点击进入 request.js,可以看到以下加密代码内容:
td 值为 0,now 为时间戳,所以 sign 参数的加密方式即为:时间戳 + 字符串(bdc739ff2dcf)经过 MD5 摘要算法进行加密,然后转换为字符串形式,最后转换成大写,我们可以通过 python 对其进行复现:
import time
import hashlib
timestamp = str(int(time.time() * 1000))
sign = hashlib.md5((timestamp + 'bdc739ff2dcf').encode('utf-8')).hexdigest().upper()
print(sign)
password 参数
方法一:直接跟栈调试
加密参数 password 的具体加密位置如果通过全局搜索的方式来确认的话,工作量会相当繁杂,如下可以看到搜索到了很多结果:
好在办法总比困难多,这里讲解两种方法解决,首先我们可以通过直接跟栈来尝试找到加密的位置 ,切换到 Network,找到 account 数据包,会看到 initiator(发起请求的对象或进程)选项下有个 js 文件样式(index.esm.js):
鼠标悬停在上面,可以看到出现如下图所示的一个个堆栈,并且名称被混淆了,以下 Y 函数,它位于调用栈的最顶层,表示经过此函数后,浏览器就会发送登录的请求,密码的加密过程已经处理完毕,从下到上即执行流程:
- 通过观察堆栈的调用可以回溯逻辑的执行流程,堆栈调用先进后出,就像是一条道路堵塞了,越后过来排队的车越能先倒出去
也可以点击打开 account,右侧 initiator 选项中也能查看到堆栈调用过程:
点击 Y 后链接,即可跳转到它定义的位置,在第 710 行打下断点,再次点击登录,即可成功断住, 这里是登录完成前的最后一步,密码已经被加密完毕了,所以要想找到密码的具体加密位置,就需要接着一步步往下找:
例如在 Call Stack(调用栈)中点击 Y 函数下面的 o 函数,可以看到其断住位置的参数 params 的 data 中密码是已经经过加密了的,所以加密位置还在下面的函数中,我们就需要继续挨个往下找:
接下来就是挨个往下跟栈的过程,就不赘述了,最后我们能在 utils.ts 中的第 174 行找到 password 的加密位置,在这行打断点再次点击登录,鼠标悬停在 requestParams.password 上可以看到这里就是密码未加密前的明文状态:
可以看到明文是通过 passwordEncrypt( ) 方法进行了加密处理,所以鼠标悬停其上,进一步跟进到加密的位置 encode.ts 中:
这里是很明显的 AES 加密:
复制下来,通过 nodejs 对其复现,代码如下:
var CryptoJS = require('crypto-js');
function encryptPwd(word){
const key = CryptoJS.enc.Utf8.parse("20171109124536982017110912453698");
const iv = CryptoJS.enc.Utf8.parse('2017110912453698');
let srcs = CryptoJS.enc.Utf8.parse(word);
let encrypted = CryptoJS.AES.encrypt(srcs, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
return encrypted.ciphertext.toString().toUpperCase();
}
console.log(encryptPwd(123456))
// A7428361DEF118911783F446A129FFCE
结果匹配:
方法二:Hook 注入
Hook 技术又称为钩子技术,他是一种特殊的消息处理机制,可以监视系统或者进程中的各种事件消息,截获发往目标窗口的消息并进行处理,因为 Windows 操作系统是建立在事件驱动机制上的,通过消息传递实现,而 Hook 相当于对其的拦截。
相关资料可参考:
Hook 的方式有很多种这里通过 Fiddler 中的插件完成 Hook 注入,通过观察可以发现 password 是以字符串形式传输的:
我们知道在 JavaScript 中 JSON.stringify() 方法用于将 JavaScript 对象或值转换为 JSON 字符串,某些站点在向 web 服务器传输用户名密码时,会用到这个方法,在本案例中,就用到了 JSON.stringify() 方法,针对该方法,可以写一个 Hook 注入脚本:
(function() {
var stringify = JSON.stringify;
JSON.stringify = function(params) {
console.log("Hook JSON.stringify ——> ", params);
debugger;
return stringify(params);
}
})();
以下是 Fiddler 中插件使用的基本流程:
添加好代码后,F12 启动抓包,然后如下清除浏览器 Cookies,刷新页面即可完成注入:
这里刷新页面后发现直接就被断住了,证明页面生成之前就会响应生成一些字符串数据,我们可以点击上方蓝色箭头 或者 F8 跳过这个断点,即可进入到页面:
在输入账户密码进行登陆之前遇到 JSON.stringify() 方法就会执行 debugger 语句,立即断下,比如上述情况,输入账户密码点击登录,如下图所示成功断下,可以从 params 中看到加密后的密码内容:
接着在 Call Stack 处从 JSON.stringify 依次往下跟栈,直到找到加密位置,之后的操作跟方法一一样:
模拟执行
完整代码
注意:如果使用了 Hook 注入,运行前停止 Fiddler 抓包操作
以下代码 url 部分进行了脱敏处理(Base64),同时需要输入自己的用户名密码,不能直接运行:
import time
import execjs
import requests
import json
import hashlib
def encrypt_sign():
timestamp = str(int(time.time() * 1000))
sign = hashlib.md5((timestamp + 'bdc739ff2dcf').encode('utf-8')).hexdigest().upper()
return sign
def encrypt_pwd(pwd):
with open('e.js', 'r', encoding='utf-8') as f:
pwd_encrypt = f.read()
pwd_result = execjs.compile(pwd_encrypt).call('encryptPwd', pwd)
return pwd_result
def login(user, enc_pwd, sign):
timestamp = str(int(time.time() * 1000))
headers = {
"user-agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36",
# content-type 必须加
"content-type": "application/json;charset=UTF-8",
"timestamp": timestamp,
"sign": sign,
}
url = "aHR0cHM6Ly9nYXRld2F5LmV3dDM2MC5jb20vYXBpL2F1dGhjZW50ZXIvdjIvb2F1dGgvbG9naW4vYWNjb3VudA=="
data = {
"platform": 1,
"userName": user,
"password": enc_pwd,
"autoLogin": True
}
data = json.dumps(data)
session = requests.session()
response = session.post(url=url, headers=headers, data=data)
print(response.text)
def main():
user = '你的用户名'
pwd = '你的密码'
enc_pwd = encrypt_pwd(pwd)
sign = encrypt_sign()
login(user, enc_pwd, sign)
if __name__ == '__main__':
main()
运行结果:
总结
以上是对某升学助考网加密参数的逆向分析,如有任何见解欢迎评论区或私信指正交流~