zuiyou登录算法逆向

  1. 算法分析

打开app登录,使用HttpCanary抓包

1
2
3
4
5
6
7
8
9
10
11
POST /stat/action?sign=v2-161b1aa20aa4c8f5b5f97007e77f75a7 HTTP/1.1
ZYP: mid=304623228
X-Xc-Agent: av=5.7.3,dt=0
User-Agent: okhttp/3.12.2 Zuiyou/5.7.3 (Android/30)
X-Xc-Proto-Req: duck-1717933249-4grdwokVrZzGxu+FnZp7Ar5XtPZgg+anpDOIMIeiHOldgtTMZIzxFlfhdGnDjRYkUfWrC+O3oX/GPXoIKaROH+z9Ny+mCLTRe3+gKlAUCWM=
Request-Type: text/json
Content-Type: application/xcp
Content-Length: 912
Host: stat.izuiyou.com
Connection: Keep-Alive
Accept-Encoding: gzip

jadx直接搜索“sign”,定位到关键函数

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
package com.izuiyou.network;

import android.app.ContextProvider;
import com.meituan.robust.ChangeQuickRedirect;
import com.meituan.robust.PatchProxy;
import com.meituan.robust.PatchProxyResult;

/* loaded from: classes4.dex */
public class NetCrypto {
public static ChangeQuickRedirect changeQuickRedirect;

static {
we5.a(ContextProvider.get(), "net_crypto");//loadlibrary
native_init();
}

public static String a(String str, byte[] bArr) {
String str2;
PatchProxyResult proxy = PatchProxy.proxy(new Object[]{str, bArr}, null, changeQuickRedirect, true, 47387, new Class[]{String.class, byte[].class}, String.class);
if (proxy.isSupported) {
return (String) proxy.result;
}
String sign = sign(str, bArr);
if (str.contains("?")) {
str2 = "&sign=" + sign;
} else {
str2 = "?sign=" + sign;
}
return str + str2;
}

public static native byte[] decodeAES(byte[] bArr, boolean z);

public static native byte[] encodeAES(byte[] bArr);

public static native String generateSign(byte[] bArr);

public static native String getProtocolKey();

public static native void native_init();

public static native boolean registerDID(byte[] bArr);

public static native void setProtocolKey(String str);

public static native String sign(String str, byte[] bArr);
}

使用fridahook一下a方法看

1
2
3
4
5
6
7
8
9
10
11
//frida
function callsign(){
Java.perform(function(){
var NetCry = Java.use("com.izuiyou.network.NetCrypto");
var JStr = Java.use("java.lang.String");
var arg1 = "n1ng";
var arg1Bytes = JStr.$new(arg1).getBytes("UTF-8");//a方法第二个参数为byte数组,需要进行转换
var retval = NetCry.a("12345",arg1Bytes);//传入“12345”和“n1ng”
console.log("result :" + retval);
});
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//result
(frida) C:\Users\n1ng>frida -UF -l E:\Learn\Unidbg\最右app\callsign.js
____
/ _ | Frida 16.1.4 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at https://frida.re/docs/home/
. . . .
. . . . Connected to MI 9 (id=21ce24db)
[MI 9::最右 ]-> callsign()
result :12345?sign=v2-c4a2c9fbbcad24427a76f999fe406f61
[MI 9::最右 ]-> callsign()
result :12345?sign=v2-c24557ab0d6fcdc08d5edac4dd76b899
[MI 9::最右 ]-> callsign()
result :123245?sign=v2-c24557ab0d6fcdc08d5edac4dd76b899
[MI 9::最右 ]->

可以看到sign的值由两个传入的参数共同决定,由三部分第一个参数+固定字符+第二个参数生成的32位字符(猜测可能是哈希)

接下来使用unidbg尝试进行模拟执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//unidbg
public class zuiyou extends AbstractJni {
private final VM vm;
private final AndroidEmulator emulator;
private final Module module;
zuiyou(){
emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.xiaochuankeji.tieba").build();
final Memory memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));
vm = emulator.createDalvikVM(new File("unidbg-android\\src\\test\\java\\com\\test2\\right573.apk"));
DalvikModule dm = vm.loadLibrary(new File("unidbg-android\\src\\test\\java\\com\\test2\\libnet_crypto.so"),true);
module = dm.getModule();

vm.setJni(this);
vm.setVerbose(true);
dm.callJNI_OnLoad(emulator);
}
public static void main(String[] args)throws Exception{
zuiyou test = new zuiyou();
}
}

1
2
3
4
5
6
7
8
9
10
11
//日志输出
JNIEnv->FindClass(com/izuiyou/network/NetCrypto) was called from RX@0x40049fc5[libnet_crypto.so]0x49fc5
JNIEnv->RegisterNatives(com/izuiyou/network/NetCrypto, RW@0x401c9010[libnet_crypto.so]0x1c9010, 8) was called from RX@0x40049fd7[libnet_crypto.so]0x49fd7
RegisterNative(com/izuiyou/network/NetCrypto, native_init()V, RX@0x4004a069[libnet_crypto.so]0x4a069)
RegisterNative(com/izuiyou/network/NetCrypto, encodeAES([B)[B, RX@0x4004a0b9[libnet_crypto.so]0x4a0b9)
RegisterNative(com/izuiyou/network/NetCrypto, decodeAES([BZ)[B, RX@0x4004a14d[libnet_crypto.so]0x4a14d)
RegisterNative(com/izuiyou/network/NetCrypto, sign(Ljava/lang/String;[B)Ljava/lang/String;, RX@0x4004a28d[libnet_crypto.so]0x4a28d)
RegisterNative(com/izuiyou/network/NetCrypto, getProtocolKey()Ljava/lang/String;, RX@0x4004a419[libnet_crypto.so]0x4a419)
RegisterNative(com/izuiyou/network/NetCrypto, setProtocolKey(Ljava/lang/String;)V, RX@0x4004a479[libnet_crypto.so]0x4a479)
RegisterNative(com/izuiyou/network/NetCrypto, registerDID([B)Z, RX@0x4004a4f5[libnet_crypto.so]0x4a4f5)
RegisterNative(com/izuiyou/network/NetCrypto, generateSign([B)Ljava/lang/String;, RX@0x4004a587[libnet_crypto.so]0x4a587)

可以看到动态注册了函数

接下来看native_init和sign函数

1
2
3
4
5
6
7
public void native_init(){
List<Object> list = new ArrayList<>(10);
list.add(vm.getJNIEnv());
list.add(0);

module.callFunction(emulator, 0x4a096, list.toArray());
}

产生了报错

1
java.lang.UnsupportedOperationException: com/izuiyou/common/base/BaseApplication->getAppContext()Landroid/content/Context;

这是因为调用callStaticObjectiMethodV方法时遇到了无法处理的函数,此时可以查看AbstractJni.java中看看Unidbg开发者是如何处理的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    case "java/lang/CharSequence->toString()Ljava/lang/String;": {
return new StringObject(vm, dvmObject.value.toString());
}
case "java/lang/String->toLowerCase()Ljava/lang/String;": {
return new StringObject(vm, dvmObject.value.toString().toLowerCase());
}
case "android/content/pm/PackageManager->getApplicationInfo(Ljava/lang/String;I)Landroid/content/pm/ApplicationInfo;":
StringObject packageName = vaList.getObjectArg(0);
if(packageName.value.equals(vm.getPackageName())){
return new ApplicationInfo(vm);
} else {
throw new UnsupportedOperationException(signature);
}
case "java/lang/String->trim()Ljava/lang/String;":{
StringObject stringObject = (StringObject) dvmObject;
return new StringObject(vm, stringObject.value.trim());
}
}

throw new UnsupportedOperationException(signature);

可见开发者对一些常见的类和方法已经做了一些处理,因此我们可以类似的在AbstractJni中或者我们自己的类(这里是zuiyou,因为zuiyou类继承了AbstractJni类)加上我们的代码进行补环境

1
2
3
4
5
6
7
8
//重写callStaticObjectMethodV    
public DvmObject<?> callStaticObjectMethodV(BaseVM vm , DvmClass dvmClass,String signature, VaList valist){
switch (signature){
case "com/izuiyou/common/base/BaseApplication->getAppContext()Landroid/content/Context;":
System.out.println("TODO");
}
return super.callStaticObjectMethodV(vm, dvmClass, signature, valist);
}

从上面打印的报错日志的签名信息中可以看出,返回的类型是Landroid/content/Context,因此我们也返回一个Context即可

1
2
3
4
5
6
7
8
public DvmObject<?> callStaticObjectMethodV(BaseVM vm , DvmClass dvmClass,String signature, VaList valist){
switch (signature){
case "com/izuiyou/common/base/BaseApplication->getAppContext()Landroid/content/Context;":
System.out.println("TODO");
return vm.resolveClass("android/content/Context").newObject(null);
}
return super.callObjectMethodV(vm, dvmClass, signature, valist);
}

重新运行,发现不会报错了,接下来看sign方法

1
2
3
4
5
6
7
8
9
10
private String callSign(){
List<Object> list = new ArrayList<>(10);
list.add(vm.getJNIEnv());
list.add(0);
list.add(vm.addLocalObject(new StringObject(vm,"12345")));
ByteArray byteArray = new ByteArray(vm, "n1ng".getBytes(StandardCharsets.UTF_8));
list.add(vm.addLocalObject(byteArray));
Number number = module.callFunction(emulator, 0x4a28d, list.toArray());
return vm.getObject(number.intValue()).getValue().toString();
}
1
2
3
4
5
//报错
java.lang.UnsupportedOperationException: android/content/Context->getClass()Ljava/lang/Class;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:417)
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:262)
at com.github.unidbg.linux.android.dvm.DvmMethod.callObjectMethodV(DvmMethod.java:89)

可以看到是我们上面传入的Context的getClass方法报错,根据报错,现在重写一下callObjectMethodV

1
2
3
4
5
6
7
8
public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList){
switch (signature){
case "android/content/Context->getClass()Ljava/lang/Class;":{
return dvmObject.getObjectType();//直接返回一个空的类
}
}
return super.callObjectMethodV(vm, dvmObject, signature, vaList);
}
1
2
3
java.lang.UnsupportedOperationException: java/lang/Class->getSimpleName()Ljava/lang/String;
at
com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:417)

可以看到这次是找不到getSimpleName

sign方法先在com/izuiyou/common/base/BaseApplication中调用getAppContext方法,获取了Context,再用getClass方法获取他的类,现在需要查看类名,前面两步其实在为这一步服务,获取类名才是他的目的,那么类名是什么呢,可以用objection的插件wallbreaker来查看

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
(frida) C:\Users\n1ng>objection -g cn.xiaochuankeji.tieba explore -P ~/.objection/plugins/WallBreaker
Using USB device `MI 9`
Agent injected and responds ok!
Loaded plugin: wallbreaker

_ _ _ _
___| |_|_|___ ___| |_|_|___ ___
| . | . | | -_| _| _| | . | |
|___|___| |___|___|_| |_|___|_|_|
|___|(object)inject(ion) v1.11.0

Runtime Mobile Exploration
by: @leonjza from @sensepost

[tab] for command suggestions
cn.xiaochuankeji.tieba on (Xiaomi: 11) [usb] # plugin wallbreaker classdump com.izuiyou.common.base.BaseApplication

package com.izuiyou.common.base

class BaseApplication {

/* static fields */
static ChangeQuickRedirect changeQuickRedirect; => null
static ax5 globalLifecycleCallbacks; => [0x3e5a]: ax5@7dd9267
static Application ___APPLICATION; => [0x3e3a]: cn.xiaochuankeji.tieba.AppController@94e6829

/* instance fields */

/* constructor methods */
BaseApplication();

/* static methods */
static Context getAppContext();
static Application __getApplication();

/* instance methods */
void attachBaseContext(Context);
void onCreate();

}

可以看到完整的类名是cn.xiaochuankeji.AppController,继续重写callObjectMethodV

1
2
3
4
5
6
7
8
9
10
11
12
public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList){
switch (signature){
case "android/content/Context->getClass()Ljava/lang/Class;":{
return dvmObject.getObjectType();
}
//调用getSimpleName时返回真正的类名"AppController"
case "java/lang/Class->getSimpleName()Ljava/lang/String;":{
return new StringObject(vm, "AppController");
}
}
return super.callObjectMethodV(vm, dvmObject, signature, vaList);
}

再次报错

1
2
java.lang.UnsupportedOperationException: android/content/Context->getFilesDir()Ljava/io/File;
at com.github.unidbg.linux.android.dvm.AbstractJni.callObjectMethodV(AbstractJni.java:417)

这里是在获取类的路径,继续重写callOBjectMethodV

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject, String signature, VaList vaList){
switch (signature){
case "android/content/Context->getClass()Ljava/lang/Class;":{
return dvmObject.getObjectType();
}
case "java/lang/Class->getSimpleName()Ljava/lang/String;":{
return new StringObject(vm, "AppController");
}
case "android/content/Context->getFilesDir()Ljava/io/File;":
case "java/lang/String->getAbsolutePath()Ljava/lang/String;":{
return new StringObject(vm, "/data/user/0/cn.xiaochuankeji.tieba/files");
}
}
return super.callObjectMethodV(vm, dvmObject, signature, vaList);
}
1
2
3
java.lang.UnsupportedOperationException: android/os/Debug->isDebuggerConnected()Z
at
com.github.unidbg.linux.android.dvm.AbstractJni.callStaticBooleanMethodV(AbstractJni.java:154)

检测是否有调试,重写callStaticBooleanMethodV,

1
2
3
4
5
6
7
8
public boolean callStaticBooleanMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList){
switch (signature){
case "android/os/Debug->isDebuggerConnected()Z":{
return false;
}
}
throw new UnsupportedOperationException(signature);
}
1
2
3
4
//报错
java.lang.UnsupportedOperationException: android/os/Process->myPid()I
at
com.github.unidbg.linux.android.dvm.AbstractJni.callStaticIntMethodV(AbstractJni.java:174)

获取pid,可以用unidbg自带的api重写callStaticIntMethodV

1
2
3
4
5
6
7
8
public int callStaticIntMethodV(BaseVM vm, DvmClass dvmClass, String signature, VaList vaList){
switch (signature){
case "android/os/Process->myPid()I":{
return emulator.getPid();
}
}
throw new UnsupportedOperationException(signature);
}

再次运行

1
2
3
[11:10:45 334]  INFO [com.github.unidbg.linux.ARM32SyscallHandler] (ARM32SyscallHandler:1898) - openat dirfd=-100, pathname=/proc/11724/status, oflags=0x20000, mode=0
JNIEnv->NewStringUTF("v2-c4a2c9fbbcad24427a76f999fe406f61") was called from RX@0x4004a373[libnet_crypto.so]0x4a373
v2-c4a2c9fbbcad24427a76f999fe406f61

现在得到的sign值和前面fridahook的值一致

算法分析

上面提到这个sign的值可能是hash算法得到的,使用ida的findhash找一下看

1
2
3
4
生成对应的hook脚本如下:
frida -UF -l E:\Learn\Unidbg\最右app\unidbg\right\libnet_crypto_findhash_1717654864.js
***********************************************************************************
花费 375.0495550632477 秒,因为会对全部函数反编译,所以比较耗时间哈
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//frida注入生成的hook脚本
(frida) C:\Users\n1ng>frida -UF -l E:\Learn\Unidbg\最右app\unidbg\right\libnet_crypto_findhash_1717654864.js
____
/ _ | Frida 16.1.4 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at https://frida.re/docs/home/
. . . .
. . . . Connected to MI 9 (id=21ce24db)
[MI 9::最右 ]->

函数sub_65540疑似哈希函数,包含初始化魔数的代码。
0xb95285fd libnet_crypto.so!0x655fd
0xb950d2db libnet_crypto.so!0x4a2db
0xbcfbd1cd base.odex!0x1381cd

ida查看sub_65540

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int __fastcall sub_65540(_BYTE *a1, int a2, int *a3)
{
char v5[24]; // [sp+4h] [bp-7Ch] BYREF
int v6[25]; // [sp+1Ch] [bp-64h] BYREF

v6[0] = 1733632769;
v6[1] = -305288311;
v6[2] = -1732583682;
v6[3] = 372397174;
v6[4] = 0;
v6[5] = 0;
sub_4B368((int)v5, a1, a2);
sub_63394((int)v6, (int)v5);
sub_4ABB8((int)v5);
sub_6533A(a3, v6);
return _stack_chk_guard - v6[22];
}

使用HookZz框架hook该方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//hook    
public void hook65540(){
//加载hookzz
IHookZz hookZz = HookZz.getInstance(emulator);
hookZz.wrap(module.base + 0x65540 + 1, new WrapCallback<RegisterContext>() {
@Override
//perCall类似frida的onEnter
public void preCall(Emulator<?> emulator, RegisterContext ctx, HookEntryInfo info) {
System.out.println(ctx.getLongArg(0));
System.out.println(ctx.getLongArg(1));
System.out.println(ctx.getLongArg(2));
}
public void postCall(Emulator<?> emulator, RegisterContext ctx, HookEntryInfo info){

}
});
}

//result
JNIEnv->GetArrayLength([B@0x6e316e67 => 4) was called from RX@0x400671e1[libnet_crypto.so]0x671e1
3221223148
4
3221223020

三个参数,第一个和第三个很可能是一个地址,第一个可能就是加密前的sign,很多加密都习惯将结果放在参数中,因此猜测第三个参数是加密后的sign

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void hook65540(){
//加载hookzz
IHookZz hookZz = HookZz.getInstance(emulator);
hookZz.wrap(module.base + 0x65540 + 1, new WrapCallback<HookZzArm32RegisterContext>() {
@Override
//perCall类似frida的onEnter
public void preCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info) {
System.out.println(ctx.getLongArg(0));
System.out.println(ctx.getLongArg(1));
System.out.println(ctx.getLongArg(2));
//类似frida的hexdump
Inspector.inspect(ctx.getPointerArg(0).getByteArray(0,0x10),"Arg1");
Inspector.inspect(ctx.getPointerArg(2).getByteArray(0,0x10),"Arg3");

}
public void postCall(Emulator<?> emulator, HookZzArm32RegisterContext ctx, HookEntryInfo info){

}
});
}
1
2
3
4
5
6
7
8
9
10
11
12
//result
[20:42:57 481]Arg1, md5=bfffab558dd8ec67b6327ed6e40e9929, hex=6e316e6700f7ffbf0000000000000000
size: 16
0000: 6E 31 6E 67 00 F7 FF BF 00 00 00 00 00 00 00 00 n1ng............
^-----------------------------------------------------------------------------^

>-----------------------------------------------------------------------------<
[20:42:57 482]Arg3, md5=d525d6782a32c113f3c398d91c7afbcf, hex=ecf6ffbf00000000745e175788f6ffbf
size: 16
0000: EC F6 FF BF 00 00 00 00 74 5E 17 57 88 F6 FF BF ........t^.W....
^-----------------------------------------------------------------------------^

可以看到第一个参数是我们输入的字符,长度刚刚好等于第二个参数,猜测第二个就是长度

现在hook一下方法结束后的retval

使用push和pop来获取执行后的arg(类似frida中的this.xxx=args[x]的方法来保存)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
>-----------------------------------------------------------------------------<
[21:05:48 136]Arg1, md5=bfffab558dd8ec67b6327ed6e40e9929, hex=6e316e6700f7ffbf0000000000000000
size: 16
0000: 6E 31 6E 67 00 F7 FF BF 00 00 00 00 00 00 00 00 n1ng............
^-----------------------------------------------------------------------------^

>-----------------------------------------------------------------------------<
[21:05:48 137]Arg3, md5=d525d6782a32c113f3c398d91c7afbcf, hex=ecf6ffbf00000000745e175788f6ffbf
size: 16
0000: EC F6 FF BF 00 00 00 00 74 5E 17 57 88 F6 FF BF ........t^.W....
^-----------------------------------------------------------------------------^

>-----------------------------------------------------------------------------<
[21:05:48 140]retval, md5=a2e9ffee40f1d90a756834b1137fde3e, hex=c4a2c9fbbcad24427a76f999fe406f61
size: 16
0000: C4 A2 C9 FB BC AD 24 42 7A 76 F9 99 FE 40 6F 61 ......$Bzv...@oa
^-----------------------------------------------------------------------------^

可以看到方法结束后的第三个参数和一开始fridahook得到的结果一致,猜测正确,第一个是输入,第二个是长度,第三个是结果

分析函数

1
2
3
4
v6[0] = 0x67552301;
v6[1] = 0xEDCDAB89;
v6[2] = 0x98BADEFE;
v6[3] = 0x16325476;

发现四个值,猜测是md5的四个iv,但是并不是标准的,我们的输入md5加密后也和输出不同,可能存在魔改,修改一下iv加密一下看

1
2
plainText:  b'n1ng'
result: c4a2c9fbbcad24427a76f999fe406f61

和hook到的值相同,加密算法就是修改iv的md5


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 3049155267@qq.com

💰

×

Help us with donation