白乐天

道阻且长,行则将至。

DYM登录参数逆向

App信息

包名:com.yoloho.dayima

root检测

狐妖面具

使用狐妖面具配置排除列表

脚本绕过

开源项目脚本:https://github.com/AshenOneYe/FridaAntiRootDetection

反编译Apk找root检测逻辑

Jadx搜索Root设备

双击跳转

isRooted()

这里直接返回false就可以了

isSimulater()

获取build.prop文件中的属性进行检测,返回false即可。

hook_root

1
2
3
4
5
6
7
8
9
10
11
function hook_root(){
Java.perform(function(){
let MiscUtil = Java.use("com.yoloho.libcore.util.MiscUtil");
MiscUtil["isRooted"].implementation = function () {
console.log(`MiscUtil.isRooted is called`);
let result = this["isRooted"]();
console.log(`MiscUtil.isRooted result=${result}`);
return false;
};
})
}

就可以进入app了

登录抓包分析

输入账号密码:18888888888 12345678

抓包数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
url: https://uicapi.yoloho.com/user/login
params: {
"device": "63122cb8da575c7e71ace0dccb1e35b0f2a43758",
"ver": "900",
"screen_width": "1080",
"screen_height": "2028",
"model": "Pixel 3",
"sdkver": "29",
"platform": "android",
"releasever": "10",
"channel": "vivobbg",
"latt": "0",
"lngt": "0",
"networkType": "0",
"token": "",
"userStatus": "0",
"oaid": "",
"installDate": "0"
}

headers: {
"Accept-Encoding": "gzip",
"User-Agent": "Mozilla/5.0 (Linux; Android 10; Pixel 3 Build/QQ3A.200705.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/74.0.3729.186 Mobile Safari/537.36",
"Content-Length": "211",
"Content-Type": "application/x-www-form-urlencoded",
"Host": "uicapi.yoloho.com",
"Connection": "Keep-Alive"
}

body: {
"username": "18888888888",
"password": "ET4Sdfjs+/VkbhxXerUBFg==",
"sign": "c6337760363ed264b287178c340f9cb8",
"androidid": "9000290bea7be29b",
"mac": "EE:9E:96:57:AB:FD",
"imei": "",
"density": "2.75",
"brand": "google",
"oaid": "",
"installDate": "0"
}

这里面需要解的参数有devicepasswordsign

逆向

device参数

jadx搜索device,出现很多结果,可以双击每个结果查看他们的上下文中的其他参数与接口中的参数进行比较,定位到device参数的位置。也可以搜索接口中其他参数,在其他参数的上下文中可以找到device

定位到如下位置。

device的值是经过URL编码后的getDeviceCode()的值

getDeviceCode()

这里面通过setDeviceCode方法来设置device的值

setDeviceCode()

如下是源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public void setDeviceCode() {
String str;
String str2;
String str3;
if (NetUtil.getSharedPreferences(SettingsConfig.KEY_USER_PERMISSION, false) && deviceCode == null) {
deviceCode = "NotFound";
try {
try {
str = Build.BOARD + Build.BRAND + Build.CPU_ABI + Build.DEVICE + Build.DISPLAY + Build.HOST + Build.ID + Build.MANUFACTURER + Build.MODEL + Build.PRODUCT + Build.TAGS + Build.TYPE + Build.USER;
} catch (Exception e2) {
e2.printStackTrace();
str = null;
}
try {
str2 = AppInfo.getAndroidId();
} catch (Exception e3) {
e3.printStackTrace();
str2 = null;
}
try {
str3 = DayimaUtil.getPhoneMac();
} catch (Exception e4) {
e4.printStackTrace();
str3 = null;
}
String str4 = "";
if (!Build.MODEL.equals("vivo X1w") || !Build.VERSION.RELEASE.equals(FaceEnvironment.SDK_VERSION)) {
try {
BluetoothAdapter defaultAdapter = BluetoothAdapter.getDefaultAdapter();
if (defaultAdapter != null) {
str4 = defaultAdapter.getAddress();
}
}
}
String str5 = ((String) null) + str2 + str + str3 + str4;
MessageDigest messageDigest = MessageDigest.getInstance("sha-1");
messageDigest.update(str5.getBytes());
byte[] digest = messageDigest.digest();
StringBuffer stringBuffer = new StringBuffer();
for (byte b : digest) {
String lowerCase = Integer.toHexString(b & 255).toLowerCase(Locale.getDefault());
if (lowerCase.length() < 2) {
lowerCase = "0" + lowerCase;
}
stringBuffer.append(lowerCase);
}
deviceCode = stringBuffer.toString();
}
}
}

分析

这里面先是拼接了一个字符串String str5 = ((String) null) + str2 + str + str3 + str4;然后对这个字符串进行了sha1签名

str2

1
str2 = AppInfo.getAndroidId(); 

获取AndroidId

str

1
str = Build.BOARD + Build.BRAND + Build.CPU_ABI + Build.DEVICE + Build.DISPLAY + Build.HOST + Build.ID + Build.MANUFACTURER + Build.MODEL + Build.PRODUCT + Build.TAGS + Build.TYPE + Build.USER;

设备指纹信息

str3

1
str3 = DayimaUtil.getPhoneMac();

获取设备Mac地址

str4

1
str4 = defaultAdapter.getAddress();

获取的是蓝牙地址

把这些内容拼接起来进行SHA1签名,然后全小写就是deviceid值

hook setDeviceCode

通过hook查看传入的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function hook_device() {
var flag = false;
Java.perform(function () {
let PeriodAPIV2 = Java.use("com.yoloho.controller.api.PeriodAPIV2");
PeriodAPIV2["setDeviceCode"].implementation = function () {
flag = true;
console.log(`========== PeriodAPIV2.setDeviceCode is called ==========`);
this["setDeviceCode"]();
};

var messageDigest = Java.use("java.security.MessageDigest");
var ByteString = Java.use("com.android.okhttp.okio.ByteString");
messageDigest.update.overload('[B').implementation = function (data) {
if (flag) {
console.log("MessageDigest.update('[B') is called!");
console.log(ByteString.of(data).utf8());
console.log("-------------------------------------------------------");
flag = false;
}
return this.update(data);
}
});
}

hook_device();

hook 结果

1
2
3
4
========== PeriodAPIV2.setDeviceCode is called ==========
MessageDigest.update('[B') is called!
null9000290bea7be29bbluelinegooglearm64-v8abluelineQQ3A.200705.002abfarm836QQ3A.200705.002GooglePixel 3bluelinerelease-keysuserandroid-buildEE:9E:96:57:AB:FD02:00:00:00:00:00
-------------------------------------------------------

password参数

搜索password

privateStrHandle的值是通过privateStrHandle()方法返回的,它传入的参数是密码和用户名

privateStrHandle()

通过AES加密返回一个encrypt

这里的三个参数,第一个参数明文是密码,第二个参数密钥是用户名进行MD5后取前16字节然后转小写,第三个参数iv向量是yoloho_dayima!%_

hook privateStrHandle()

1
2
3
4
5
6
7
8
9
10
11
12
function hook_aes() {
Java.perform(function () {let DayimaPrivateUtil = Java.use("com.yoloho.libcore.util.DayimaPrivateUtil");
DayimaPrivateUtil["privateStrHandle"].implementation = function (str, str2) {
console.log(`DayimaPrivateUtil.privateStrHandle is called: str=${str}, str2=${str2}`);
let result = this["privateStrHandle"](str, str2);
console.log(`DayimaPrivateUtil.privateStrHandle result=${result}`);
return result;
};
});
}

hook_aes();

hook 结果

1
2
DayimaPrivateUtil.privateStrHandle is called: str=12345678, str2=18888888888
DayimaPrivateUtil.privateStrHandle result=ET4Sdfjs+/VkbhxXerUBFg==

测试验证

没问题

sign参数还原

找到sign的生成位置

它是通过Crypt.encrypt_data()方法生成的,它里面的参数是由DeviceCode"user/login"strstr2拼接而成的

encrypt_data()

这是一个native方法

IDA分析

找到静态函数

双击跳转

sub_1DA0()

接下来使用rpc调用

hook encrypt_data()

1
2
3
4
5
6
7
8
9
10
11
12
13
function hook_encrypt_data() {
Java.perform(function () {
let Crypt = Java.use("com.yoloho.libcore.util.Crypt");
Crypt["encrypt_data"].implementation = function (j2, str, j3) {
console.log(`Crypt.encrypt_data is called: j2=${j2}, str=${str}, j3=${j3}`);
let result = this["encrypt_data"](j2, str, j3);
console.log(`Crypt.encrypt_data result=${result}`);
return result;
};
});
}

hook_encrypt_data();

hook 结果

1
2
Crypt.encrypt_data is called: j2=0, str=63122cb8da575c7e71ace0dccb1e35b0f2a43758user/login18888888888ET4Sdfjs+/VkbhxXerUBFg==, j3=85
Crypt.encrypt_data result=c6337760363ed264b287178c340f9cb8

rpc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import frida

device = frida.get_remote_device()
process = device.attach("大姨妈")

js_code = """
rpc.exports = {
endata: function (j2,str,j3) {
var res;
Java.perform(function () {
let Crypt = Java.use("com.yoloho.libcore.util.Crypt");
let encrypted_data = Crypt.encrypt_data(j2,str,j3);
res = encrypted_data;
});
return res;
}
}
"""

script = process.create_script(js_code)
script.load()
en_data = script.exports_sync.endata(0, "63122cb8da575c7e71ace0dccb1e35b0f2a43758user/login18888888888ET4Sdfjs+/VkbhxXerUBFg==", 85)
print(en_data) # c6337760363ed264b287178c340f9cb8

Python实现登录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import requests

url = "https://uicapi.yoloho.com/user/login"

params = {
"device": "63122cb8da575c7e71ace0dccb1e35b0f2a43758",
"ver": "900",
"screen_width": "1080",
"screen_height": "2028",
"model": "Pixel 3",
"sdkver": "29",
"platform": "android",
"releasever": "10",
"channel": "vivobbg",
"latt": "0",
"lngt": "0",
"networkType": "0",
"token": "",
"userStatus": "0",
"oaid": "",
"installDate": "0"
}

headers = {
"Accept-Encoding": "gzip",
"User-Agent": "Mozilla/5.0 (Linux; Android 10; Pixel 3 Build/QQ3A.200705.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/74.0.3729.186 Mobile Safari/537.36",
"Content-Length": "211",
"Content-Type": "application/x-www-form-urlencoded",
"Host": "uicapi.yoloho.com",
"Connection": "Keep-Alive"
}

body = {
"username": "18888888888",
"password": "ET4Sdfjs+/VkbhxXerUBFg==",
"sign": "c6337760363ed264b287178c340f9cb8",
"androidid": "9000290bea7be29b",
"mac": "EE:9E:96:57:AB:FD",
"imei": "",
"density": "2.75",
"brand": "google",
"oaid": "",
"installDate": "0"
}

response = requests.post(url, params=params, headers=headers, data=body)
print(response.status_code)
print(response.text)

Unidbg调用encrypt_data()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public class MainActivity extends AbstractJni {
// 声明模拟器、内存和虚拟机(VM)实例
private final AndroidEmulator emulator;
private final Memory memory;
private final VM vm;

// MainActivity 的构造函数
public MainActivity() {
// 初始化 64 位架构的 Android 模拟器
emulator = AndroidEmulatorBuilder
.for64Bit()
.build();

// 从模拟器获取内存实例
memory = emulator.getMemory();

// 设置模拟器的库解析器,API 级别为 23
memory.setLibraryResolver(new AndroidResolver(23));

// 使用指定的 APK 文件创建 Dalvik VM 实例
vm = emulator.createDalvikVM(new File("unidbg-android/src/test/java/com/dym/encryptdata/DYM_8.9.0.apk"));

// 为 VM 设置 JNI(Java Native Interface)
vm.setJni(this);

// 启用 VM 的详细日志记录
vm.setVerbose(true);

// 将本地库 "Crypt" 加载到 VM 中
vm.loadLibrary("Crypt", true);
}

// 使用 Crypt 类加密数据的方法
public void encrypt_data() {
// 从 VM 中解析 Crypt 类
DvmClass Crypt = vm.resolveClass("com.yoloho.libcore.util.Crypt");

// 调用 Crypt 类中的静态 JNI 方法 "encrypt_data"
StringObject stringObject = Crypt.callStaticJniMethodObject(emulator,
"encrypt_data(JLjava/lang/String;J)Ljava/lang/String;",
0,
"63122cb8da575c7e71ace0dccb1e35b0f2a43758user/login18888888888ET4Sdfjs+/VkbhxXerUBFg==",
85);

// 从 StringObject 中获取结果
String result = stringObject.getValue();

// 将结果打印到控制台
System.out.println("result=" + result); // c6337760363ed264b287178c340f9cb8
}

// 运行应用程序的主方法
public static void main(String[] args) {
// 创建 MainActivity 的实例并调用 encrypt_data 方法
new MainActivity().encrypt_data();
}
}

在安卓项目中调用别人的so中的函数

新建安卓项目

在app目录下新建一个libs目录,把libCrypt.so放到libs目录下

build.gradle.kts中的android里添加如下内容

1
2
3
4
5
sourceSets {
getByName("main") {
jniLibs.srcDirs("libs")
}
}

新建一个包,包名必须要与方法所在的包一致,com.yoloho.libcore.util

新建一个类,类名也要与方法所在的类一致,DayimaPrivateUtil

添加代码

1
2
3
4
5
6
7
8
9
package com.yoloho.libcore.util;

public class Crypt {
static {
System.loadLibrary("Crypt");
}
public static native String encrypt_data(long j2, String str, long j3);

}

调用

1
2
3
String sign = Crypt.encrypt_data(0,"63122cb8da575c7e71ace0dccb1e35b0f2a43758user/login18888888888ET4Sdfjs+/VkbhxXerUBFg==",85);
TextView tv = findViewById(R.id.textview);
tv.setText(sign);

效果