主要使用的工具和环境如下: 设备:一部root的手机或者手机模拟器; 抓包工具:fiddler; 分析工具:jadx-gui 1.1.0; 执行代码: pycharm + python3.7; hook框架:frida + Objection(很好用的);
1.抓包分析
1.1
经过多次登录操作发现headers中的Authorization、X-Sequence、X-DeviceId、netmonitor_linkid和body中的password都是加密字符,而且除了X-DeviceId以外的其它字段都会在每次登录时发生改变,首次登录时请求头中没有Authorization还能登录可以忽略。
2.开始逆向分析操作
2.1从请求头开始
Jadx反编译后搜索Authorization、X-Sequence、X-DeviceId、netmonitor_linkid这些关键字,得下图
其中X-Sequence与netmonitor_linkid只是随机的uuid,需要分析的只要DeviceId、Authorization、PassWord。
2.2分析DeviceId,也是随机的uuid,以分析代码的业务逻辑为主
从业务角度看只是实现一个自动化查询的功能而不是海量采集,所以这个字段的逆向分析是可以忽略的。
从图2中看到俩行关键代码
String a2 = com.a.a.a.a.a(com.best.android.laiqu.base.a.b());
Request.Builder addHeader2 = addHeader.addHeader("X-DeviceId", a2);
Ctrl+左键向上查询
查阅com.a.a.a.a.a与com.best.android.laiqu.base.a.b会看到加密逻辑是在com.a.a.a.a.a中。
下一步进入a方法:
上图是存在了直接调用,不存在就创建的业务逻辑,我们需要的就是看他怎么创建的,进入b方法。
可以看到b方法有多个返回值,真正有用的是最后一个return,查看序号5return的方法a。
新DeviceId生成的条件
- 目录/storage/emulated/0/.sys_config/pro与/storage/emulated/0/.android/pro下的文件都要删除
- 卸载重装apk
2.3分析password
请求参数java代码分析
先搜索,搜索技巧如下
.password
"password"
pwd等等
看见了login,那它就是最可疑的。
又看见了疑似RSA的publicKey,点进a方法看看。
import android.util.Base64;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import javax.crypto.Cipher;
public class RSA {
public static String pubKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC9xhBZOWWF5Icw384mJksmaJ53RBLPUbEq5hXWW4Xgf82r6Zj24e3MWOnBTcblDodXYtSsaRJilosdTQVWGetJewebKmyqh1l1lUagS1/dbII9GsGat5zMboMHLWUO9NoBS9VDxqYL2VLppNEj/Xe39gBRHIiSnmtggiHuYsEv8wIDAQAB";
public static PublicKey getPublicKey(String key) throws Exception {
return KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(Base64.decode(key, 0)));
}
public static byte[] encrypt(byte[] plaintext) throws Exception {
PublicKey publicKey = getPublicKey(pubKey);
Cipher cipher = Cipher.getInstance("RSA/None/NoPadding", "BC");
cipher.init(1, publicKey);
return cipher.doFinal(plaintext);
}
public static String rsa(String args) throws Exception {
return Base64.encodeToString(encrypt(args.getBytes()), 0);
}
}
上图和代码块的内容是不是很像,Hook一下看看。
Hook可疑代码
# -*- coding: utf-8 -*-
import frida
import sys
hook_code = """
Java.perform(function() {
var clazz = Java.use('com.best.android.b.d');
clazz.a.overload('java.lang.String', 'java.lang.String').implementation = function() {
var ret = clazz.a.apply(this, arguments);
console.log(JSON.stringify(arguments));
console.log(JSON.stringify(ret));
console.log('BD: A');
return ret;
}
clazz.b.overload('java.lang.String', 'java.lang.String').implementation = function() {
var ret = clazz.b.apply(this, arguments);
console.log(JSON.stringify(arguments));
console.log(JSON.stringify(ret));
console.log('BD: B');
return ret;
}
});
"""
def message(message, data):
print(data)
if message["type"] == "send":
print(message)
else:
print("message: ", message)
process = frida.get_usb_device().attach('com.best.android.laiqu')
script = process.create_script(hook_code)
script.on("message", message)
script.load()
sys.stdin.read()
可以确定就是它了。
加密算法与登录还原
由于我对python更熟悉所以这里选择使用python对加密与登陆过程进行还原。
请求的时候要注意headers中的Content-Type,需要先序列化为json字符串才能得到想要的response。
pip install pycryptodome
# -*- coding: utf-8 -*-
import uuid
import requests
from Crypto.Cipher import PKCS1_v1_5 as Cipher_pkcs1_v1_5
from Crypto.PublicKey import RSA
import base64
headers = {
"X-Auth-Type": "0",
"X-ClientTime": "2020-05-08T16:39:43.017+08:00", # 懒
"X-SerializationType": "json",
"X-Sequence": str(uuid.uuid4()),
"X-SystemType": "Android",
"X-SystemVersion": "6.0.1",
"X-AppVersion": "v5.14.1-240",
"X-PackageName": "com.best.android.laiqu.base",
"X-DeviceId": str(uuid.uuid4()).replace("-", ""),
"X-DeviceName": "Xiaomi MI 4LTE",
"Content-Type": "application/json;charset=UTF-8",
"netmonitor_linkid": str(uuid.uuid4()),
"Content-Length": "0",
"Host": "laiqu.800best.com",
"Connection": "Keep-Alive",
"Accept-Encoding": "gzip",
"User-Agent": "okhttp/3.12.1",
}
def task(username, password):
rts = requests.post(
"https://laiqu.800best.com/lqapi/Security/GetRsaPublicKey",
headers=headers
)
pp = rts.json().get("result").get("publicKey").encode("utf-8")
print(pp)
public_pem = b"-----BEGIN PUBLIC KEY-----\n" + pp + b"\n-----END PUBLIC KEY-----"
rsakey = RSA.importKey(public_pem)
cipher = Cipher_pkcs1_v1_5.new(rsakey) # 加密
cipher_text = base64.b64encode(cipher.encrypt(password.encode("utf-8"))).decode() # 序列化
data = {"userName": username, "password": cipher_text}
res = requests.post(
"https://laiqu.800best.com/lqapi/Security/UserLogin",
headers=headers,
json=data,
)
print(res.json())
if __name__ == '__main__':
username, password = "", ""
task(username, password)