文章目录
- 1. 写在前面
- 2. 接口分析
- 3. 算法还原
【🏠作者主页】:吴秋霖
【💼作者介绍】:擅长爬虫与JS加密逆向分析!Python领域优质创作者、CSDN博客专家、阿里云博客专家、华为云享专家。一路走来长期坚守并致力于Python与爬虫领域研究与开发工作!
【🌟作者推荐】:对爬虫领域以及JS逆向分析感兴趣的朋友可以关注《爬虫JS逆向实战》《深耕爬虫领域》
未来作者会持续更新所用到、学到、看到的技术知识!包括但不限于:各类验证码突防、爬虫APP与JS逆向分析、RPA自动化、分布式爬虫、Python领域等相关文章
作者声明:文章仅供学习交流与参考!严禁用于任何商业与非法用途!否则由此产生的一切后果均与作者无关!如有侵权,请联系作者本人进行删除!
1. 写在前面
爬虫业务中自动登录获取Cookie的操作,之前作者提到也出过相关的文章。采集业务场景中很常见、也较为重要!一些第三方的网站定期或者持续抓取Cookie都是有时效性的。那么想要保障爬虫稳定的运行就需要定期更新Cookie信息,手动更新肯定是不现实的!自动更新的话采用浏览器自动化的方案也是不可取的,有条件的都采用接口协议的方式自动登录更新Cookie信息
分析目标:
aHR0cHM6Ly9zZWxsZXIua3VhamluZ21haWh1by5jb20vbG9naW4/cmVkaXJlY3RVcmw9aHR0cHMlM0ElMkYlMkZzZWxsZXIua3VhamluZ21haWh1by5jb20lMkZzZXR0bGUlMkZtYWxsLWluZm8tZW50cnk=
2. 接口分析
打开网站查看登录入口,随便输入一个手机号密码查看一下发包情况,如下所示:
可以看到login接口中除了提交参数中的密码是经过加密的还有请求头中有一个参数也是加密的Anti-Content,所以要解决自动登录的就需要先逆向分析这两个参数的加密,还原出来!
这篇文章主要针对encryptPassword参数的加密进行分析,请求头的验签在以往的文章中有写过,这里就不再赘述,感兴趣的翻看之前的文章
3. 算法还原
这个我们全局搜索一下encryptPassword就能定位到相关的加密位置,如下所示:
看下面JS,接受一个明文密码,一个公钥。然后new g.a在这里创建了一个加密对象的实例,如下所示:
g.a即下面JS中的ut里面封装了公钥、私钥管理及其相关操作,构造函数t初始化了默认配置(密钥的默认大小、公钥的默认指数-一般为010001即65537),setKey初始化密钥,还有一堆管理RSA密钥的,比如那个ot…
分析重点,调用d(this.getKey().encrypt(t))使用密钥对待加密数据进行加密,d方法再对加密结果进行Base64编码,如下所示:
ut = function() {
function t(t) {
void 0 === t && (t = {}),
t = t || {},
this.default_key_size = t.default_key_size ? parseInt(t.default_key_size, 10) : 1024,
this.default_public_exponent = t.default_public_exponent || "010001",
this.log = t.log || !1,
this.key = null
}
return t.prototype.setKey = function(t) {
this.log && this.key && console.warn("A key was already set, overriding existing."),
this.key = new ot(t)
}
,
t.prototype.setPrivateKey = function(t) {
this.setKey(t)
}
,
t.prototype.setPublicKey = function(t) {
this.setKey(t)
}
,
t.prototype.decrypt = function(t) {
try {
return this.getKey().decrypt(g(t))
} catch (e) {
return !1
}
}
,
t.prototype.encrypt = function(t) {
try {
return d(this.getKey().encrypt(t))
} catch (e) {
return !1
}
}
,
t.prototype.sign = function(t, e, r) {
try {
return d(this.getKey().sign(t, e, r))
} catch (n) {
return !1
}
}
,
t.prototype.verify = function(t, e, r) {
try {
return this.getKey().verify(t, g(e), r)
} catch (n) {
return !1
}
}
,
t.prototype.getKey = function(t) {
if (!this.key) {
if (this.key = new ot,
t && "[object Function]" === {}.toString.call(t))
return void this.key.generateAsync(this.default_key_size, this.default_public_exponent, t);
this.key.generate(this.default_key_size, this.default_public_exponent)
}
return this.key
}
,
t.prototype.getPrivateKey = function() {
return this.getKey().getPrivateKey()
}
,
t.prototype.getPrivateKeyB64 = function() {
return this.getKey().getPrivateBaseKeyB64()
}
,
t.prototype.getPublicKey = function() {
return this.getKey().getPublicKey()
}
,
t.prototype.getPublicKeyB64 = function() {
return this.getKey().getPublicBaseKeyB64()
}
,
t.version = st.version,
t
}();
如果想扣JS代码的话可以从上面开始扣了,把那几个缺失的都扣全。然后把加密方法导出即可!
继续往下进入到RSA加密处理的里面,来分析这段JS,可以看到它主要实现了一个针对publicKey的Base64解码器。纯算还原这段方法类似Python中的base64.b64decode,如下所示:
在闭包内的变量中存储了Base64字符映射表,标准的字符集!通过处理后返回一个解码字节的数组,如下所示:
JS算法中主要通过使用RSA公钥加密对密码进行加密实现,这里我们使用Python对加密算法进行还原实现,如下所示:
import base64
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import serialization, hashes
def encrypt_password(public_key: str, password: str) -> str:
public_key_bytes = base64.b64decode(public_key)
public_key_obj = serialization.load_der_public_key(public_key_bytes)
encrypted = public_key_obj.encrypt(
password.encode(),
padding.PKCS1v15()
)
return base64.b64encode(encrypted).decode()
public_key = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDJpaLPONg3qWfi2kdwauqOT3L7AEK6lrMZqKYZv0ikWPZtzTqnVLMeolsE7x3Sc4UxS6ews+WS0ijwIEU8GAbhr2aJh0F+o6bpYQgWosHBts4lMW/IdtKg45DSGgBWg8gWDTK7K+T+zWfTjEQGwEgNHsZ2NbQBtTEZ35CDWUP7iQIDAQAB"
password = "test123"
encrypted_password = encrypt_password(public_key, password)
serialization.load_der_public_key将DER格式的公钥字节数据加载为可用的公钥对象(可以自行去了解一下RSA的相关知识~),转换的目的即加密操作需要的是一个公钥对象,而不是字符串或字节数据!而上面的方法则可以将DER格式的公钥转换为Py的公钥对象
为什么选择使用PKCS#1 v1.5的填充方式,可以查看setPublicKey设置公钥,另一个就是JS加密中并没有使用到OAEP填充模式,所以大概率就是使用的默认的PKCS#1 v1.5