百世来取:DeviceId、PassWord逆向分析

1,172 阅读3分钟

apk下载地址

主要使用的工具和环境如下: 设备:一部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生成的条件
  1. 目录/storage/emulated/0/.sys_config/pro与/storage/emulated/0/.android/pro下的文件都要删除
  2. 卸载重装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。

学习python实现的加密算法点这里,作者写的很详细

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)